CHG: 登录页去除账号下拉与内置密码;house 模块前端;Gitea act_runner 自动部署 workflow
Some checks failed
Build and Deploy (admin.xpcool.com) / build-and-deploy (push) Failing after 5m25s
Some checks failed
Build and Deploy (admin.xpcool.com) / build-and-deploy (push) Failing after 5m25s
- login.vue:移除 selectAccount 下拉与 MOCK_USER_OPTIONS(代码不存账号密码),手输+记住账号
- .env.production:VITE_GLOB_API_URL=/api/service(同源反代)
- house:api/house.ts + views/house/{community,listing,dashboard,data}
- .gitea/workflows/deploy.yml:node22 + pnpm11.16 + vite 直跑构建 → /data/www/admin.xpcool.com
This commit is contained in:
parent
07da32e21b
commit
0ab6cf5f17
@ -1,49 +1,74 @@
|
||||
# deployed by gitea act_runner (push to main triggers this)
|
||||
# admin.xpcool.com 自动部署:Gitea act_runner(push main/dev 触发)
|
||||
# 注意点:
|
||||
# 1. job 基础镜像为 node:20-bullseye,但项目 pnpm@11 强依赖 node:sqlite(node 22.5+),
|
||||
# 故先下载 node 22 二进制替换 PATH(npmmirror 源)。
|
||||
# 2. 构建必须直接跑 vite build(pnpm/turbo build 会卡死 15min+ 无产出),产物 apps/web-tdesign/dist。
|
||||
# 3. 部署目标 /data/www/admin.xpcool.com(runner 已挂载 /data/www)。
|
||||
name: Build and Deploy (admin.xpcool.com)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install git & docker CLI
|
||||
- name: Install git & node 22 (npmmirror)
|
||||
run: |
|
||||
sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g; s|security.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list 2>/dev/null || true
|
||||
sed -i 's|deb.debian.org|mirrors.cloud.tencent.com|g; s|security.debian.org|mirrors.cloud.tencent.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true
|
||||
apt-get update
|
||||
apt-get install -y git docker.io
|
||||
set -e
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git xz-utils >/dev/null
|
||||
# node 22 二进制解压到 /opt,PATH 前置(覆盖镜像自带 node 20)
|
||||
curl -fsSL https://npmmirror.com/mirrors/node/v22.22.2/node-v22.22.2-linux-x64.tar.xz -o /tmp/node.tar.xz
|
||||
mkdir -p /opt && tar -xJf /tmp/node.tar.xz -C /opt
|
||||
export PATH="/opt/node-v22.22.2-linux-x64/bin:$PATH"
|
||||
echo "/opt/node-v22.22.2-linux-x64/bin" >> "$GITHUB_ENV"
|
||||
echo "PATH=$PATH" >> "$GITHUB_ENV"
|
||||
node -v && npm -v
|
||||
|
||||
- name: Checkout (local Gitea)
|
||||
run: |
|
||||
git clone --depth 1 --branch "${{ github.ref_name }}" https://oauth2:${{ github.token }}@git.xpcool.com/${{ github.repository }}.git .
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
- name: Docker build & deploy
|
||||
- name: Enable pnpm via corepack (npmmirror)
|
||||
env:
|
||||
COREPACK_NPM_REGISTRY: https://registry.npmmirror.com
|
||||
COREPACK_DEFAULT_TO_LATEST: "0"
|
||||
run: |
|
||||
# .npmrc 里的 store-dir 是 Windows 路径,Linux 容器里会导致 pnpm 异常,临时移除
|
||||
sed -i '/store-dir=/d' .npmrc
|
||||
docker build -f scripts/deploy/Dockerfile -t admin.xpcool.com:latest .
|
||||
docker rm -f admin.xpcool.com 2>/dev/null || true
|
||||
docker run -d \
|
||||
--name admin.xpcool.com \
|
||||
--restart unless-stopped \
|
||||
-p 127.0.0.1:8010:8080 \
|
||||
admin.xpcool.com:latest
|
||||
export PATH="/opt/node-v22.22.2-linux-x64/bin:$PATH"
|
||||
corepack enable
|
||||
corepack prepare pnpm@11.16.0 --activate
|
||||
pnpm -v
|
||||
|
||||
- name: Notify success (PushPlus)
|
||||
if: success()
|
||||
- name: Install dependencies
|
||||
env:
|
||||
NODE_OPTIONS: --experimental-sqlite
|
||||
run: |
|
||||
curl -s -X POST "https://www.pushplus.plus/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"${{ secrets.PUSHPLUS_TOKEN }}","title":"✅ 部署成功 admin.xpcool.com","content":"仓库: ${{ github.repository }}<br>分支: ${{ github.ref_name }}<br>提交: ${{ github.sha }}<br>触发人: ${{ github.actor }}<br>状态: 编译打包部署成功<br>访问: https://admin.xpcool.com","template":"html"}'
|
||||
export PATH="/opt/node-v22.22.2-linux-x64/bin:$PATH"
|
||||
pnpm install
|
||||
|
||||
- name: Notify failure (PushPlus)
|
||||
if: failure()
|
||||
- name: Build (vite direct, avoid turbo hang)
|
||||
env:
|
||||
NODE_OPTIONS: "--experimental-sqlite --max-old-space-size=8192"
|
||||
run: |
|
||||
curl -s -X POST "https://www.pushplus.plus/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"${{ secrets.PUSHPLUS_TOKEN }}","title":"❌ 部署失败 admin.xpcool.com","content":"仓库: ${{ github.repository }}<br>分支: ${{ github.ref_name }}<br>提交: ${{ github.sha }}<br>触发人: ${{ github.actor }}<br>状态: 构建或部署失败<br>日志: https://git.xpcool.com/${{ github.repository }}/actions","template":"html"}'
|
||||
export PATH="/opt/node-v22.22.2-linux-x64/bin:$PATH"
|
||||
cd apps/web-tdesign
|
||||
./node_modules/.bin/vite build --mode production
|
||||
|
||||
- name: Deploy to /data/www/admin.xpcool.com
|
||||
run: |
|
||||
set -e
|
||||
SRC=apps/web-tdesign/dist
|
||||
DST=/data/www/admin.xpcool.com
|
||||
if [ ! -d "$SRC" ]; then
|
||||
echo "BUILD OUTPUT NOT FOUND: $SRC"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$DST"
|
||||
find "$DST" -mindepth 1 -delete
|
||||
cp -r "$SRC"/. "$DST"/
|
||||
chmod -R a+rX "$DST"
|
||||
echo "Deployed admin.xpcool.com -> $DST"
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -57,7 +57,11 @@ vite.config.ts.*
|
||||
.agents
|
||||
.claude
|
||||
.codex
|
||||
.workbuddy
|
||||
# WorkBuddy 本地记忆:仅放行变更记录(CHANGELOG.md)
|
||||
.workbuddy/*
|
||||
!.workbuddy/memory/
|
||||
.workbuddy/memory/*
|
||||
!.workbuddy/memory/CHANGELOG.md
|
||||
skills-lock.json
|
||||
.atomcode
|
||||
datalog
|
||||
|
||||
21
.workbuddy/memory/CHANGELOG.md
Normal file
21
.workbuddy/memory/CHANGELOG.md
Normal file
@ -0,0 +1,21 @@
|
||||
# admin.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-08-26 | CFG | 新增 .gitea/workflows/deploy.yml(Gitea act_runner 自动部署):push main/dev → 下载 node22 二进制(npmmirror,覆盖镜像 node20,pnpm11 需 node:sqlite)→ corepack pnpm@11.16.0 → pnpm install → 直接 vite build(避开 turbo 卡死)→ cp dist 到 /data/www/admin.xpcool.com/;YAML 已校验
|
||||
2026-08-26 | CHG | 看房 house 模块前端落地:api/house.ts + views/house/{community,listing,dashboard,data}/index.vue(小区 CRUD、房源多维筛选+多选批量标记笋盘/低可信、看板 echarts 概览卡片/区域均价柱状/挂牌vs成交趋势);菜单走后端动态路由(011 种子 component=house/xxx/index),图表复用 @vben/plugins/echarts 的 EchartsUI/useEcharts
|
||||
2026-08-26 | DEP | 线上 dist 更新为「无下拉版」(vite build 产物,index-index-B-XlZ2uN.js)并部署 /data/www/admin.xpcool.com/,修复登录 no rows:根因=线上仍是 19:32 旧版(默认 admin/admin123),admin 已删故报错;验证:线上 index 新版 + xxcool 登录成功 + js 无 admin123;⚠️ webhook 自动部署未生效:deploy-webhook.service disabled/inactive(8/22 11:52 停止,9000 未监听),提交代码不会自动部署,需 `sudo systemctl enable --now deploy-webhook` + Gitea 仓库 webhook 配置
|
||||
2026-08-26 | CHG | 登录页移除账号下拉与代码内置账号密码:删除 selectAccount 字段、MOCK_USER_OPTIONS 及自动填充逻辑(BasicOption import 一并清理),前端不再存任何账号密码,改为手输 + 勾选「记住账号」(AuthenticationLogin 内置 rememberMe,localStorage 仅记用户名不存密码);本地 dev(5173) 浏览器验证:无下拉、仅用户名/密码/滑块 + 记住账号复选框
|
||||
2026-08-26 | CHG | 登录页默认账号替换为 xxcool/xxCool@2026:MOCK_USER_OPTIONS 仅留 Super(xxcool),selectAccount 默认值、密码自动填充同步;本地 dev(5173) 验证登录页下拉正常(combobox Super、账号密码自动填充、展开有 Super 选项),无 admin123 残留;⚠️ 构建注意:pnpm/turbo build 会卡死(两次 15min+ 无产出),需直接 `./node_modules/.bin/vite build --mode production`(约 20s)完成;线上 dist 仍为旧版,待部署
|
||||
2026-08-26 | DEP | 生产部署上线(193.112.118.168):pnpm build:tdesign 构建 web-tdesign 上传 /data/www/admin.xpcool.com/;nginx HTTPS 站点(证书 /data/nginx/ssl/admin.xpcool.com/,80→301,静态托管 + /api/service 反代 127.0.0.1:10100);https://admin.xpcool.com 首页 200、同源 API 登录通过
|
||||
2026-08-26 | CHG | .env.production 的 VITE_GLOB_API_URL 由 https://mock-napi.vben.pro/api 改为 /api/service(同源相对路径,由 nginx 反代到后端 10100)
|
||||
|
||||
2026-08-26 | REQ | RBAC 至按钮级细粒度全链路核查通过:backend 动态路由(menu/routes)+ 23 权限码下发(auth/codes)+ 三个管理页(admin/role/menu)按钮级 hasAccessByCodes 与后端 seed 权限码逐一匹配;dev server(20104)经 vite 代理→10100 登录/取码 200;后端配套修复禁用角色权限隔离漏洞(见 service CHANGELOG)
|
||||
|
||||
2026-08-26 | FIX | 登录接口 502 联调修复:根因为后端 10100 未启动(vite 代理连接拒绝→502);启动后端后 login/info/codes/menu-routes 全链路 200。另记:本机 corepack/pnpm shim 路径转换损坏,需用 `npm exec --package=pnpm@11.16.0` 代替 pnpm 命令
|
||||
2026-08-26 | CFG | CHANGELOG 纳入 git 管理(.gitignore 放行 .workbuddy/memory/CHANGELOG.md),中央工作空间迁移至 ../workbuddy.xpcool.com(相对路径索引)
|
||||
|
||||
2026-08-26 | CHG | 后端 API 统一前缀 /api/service/ + 端口 10100:vite 代理改为 '/api/service'→:10100(合并原 /admin、/api 两条);api 文件全部适配(auth/menu/user/system/log:去 v1、前缀 /api/service、全 POST、CRUD 改动作式路径)
|
||||
2026-08-26 | FIX | 修复依赖装不上:pnpm 11.16.0 强依赖 node:sqlite,需 `NODE_OPTIONS=--experimental-sqlite` 后 install 成功;同步更正 agents.md 主应用为 web-tdesign(dev:tdesign,5999/6000 端口)
|
||||
2026-08-26 | CHG | 修正登录页默认账号:selectAccount 默认 admin,密码映射改为种子账号 admin/admin123,便于联调
|
||||
2026-08-26 | CHG | 同步三铁律(中文注释/做记录/中文优先)至 AGENTS.md「统一工作约定」小节
|
||||
2026-08-26 | CFG | 建立统一变更记录机制(CHANGELOG),接入 xpcool.com 中央索引
|
||||
80
agents.md
80
agents.md
@ -13,23 +13,18 @@
|
||||
| 业务名 | admin.xpcool.com |
|
||||
| 工作目录 | `E:\xxcool\project\admin.xpcool.com` |
|
||||
| 基础框架 | Vue Vben Admin **5.7.0**(官方 monorepo 模板) |
|
||||
| 技术栈 | Vue 3 + TypeScript + Vite + pnpm 11 + turbo(monorepo)+ Ant Design Vue 等 |
|
||||
| 技术栈 | Vue 3 + TypeScript + Vite + pnpm 11 + turbo(monorepo)+ TDesign |
|
||||
| 代码来源 | 官方仓库 `vbenjs/vue-vben-admin` 克隆(remote 为 ghproxy 镜像:`https://ghproxy.net/https://github.com/vbenjs/vue-vben-admin.git`) |
|
||||
| 当前基线 | 模板原样,**无业务定制**(web-antd views 仅有 `_core/dashboard/demos` 示例页) |
|
||||
| 当前基线 | 已精简为单一应用 **web-tdesign**,并开始业务定制(`apps/web-tdesign/src/views/_core/authentication/login.vue` 已修改) |
|
||||
| git 分支 | `main`(仅 origin/main) |
|
||||
|
||||
## 2. 应用清单(apps/)
|
||||
|
||||
| 应用 | 包名 | UI 库 | dev 端口 | 启动命令 |
|
||||
|---|---|---|---|---|
|
||||
| web-antd | `@vben/web-antd` | Ant Design Vue | **5666** | `pnpm dev:antd` |
|
||||
| web-antdv-next | `@vben/web-antdv-next` | Ant Design Vue Next | - | `pnpm dev:antdv-next` |
|
||||
| web-ele | `@vben/web-ele` | Element Plus | - | `pnpm dev:ele` |
|
||||
| web-naive | `@vben/web-naive` | Naive UI | - | `pnpm dev:naive` |
|
||||
| web-tdesign | `@vben/web-tdesign` | TDesign | - | `pnpm dev:tdesign` |
|
||||
| backend-mock | - | Nitro mock 后端 | **5320** | 随 dev 自动启动(VITE_NITRO_MOCK=true) |
|
||||
| web-tdesign | `@vben/web-tdesign` | TDesign | **5999**(被占用自动切 6000) | `pnpm dev:tdesign` |
|
||||
|
||||
> 本业务主应用推测为 **web-antd**(端口 5666,已确认 .env.development)。
|
||||
> 本业务**唯一主应用为 web-tdesign**(`.env.development` 设 `VITE_PORT=5999`)。项目已从官方多应用模板精简,仅保留此应用。
|
||||
|
||||
## 3. 环境要求与本机踩坑记录(重要)
|
||||
|
||||
@ -40,30 +35,36 @@
|
||||
### 本机(Windows)已验证的运行方式 —— 直接复制可用
|
||||
|
||||
```bash
|
||||
# 前置:把 managed node 22.22.2 放到 PATH 最前(系统 node 23 会因 NODE_OPTIONS 报错)
|
||||
# 关键:pnpm 11.16.0 用到 node:sqlite,必须给 NODE_OPTIONS 加 --experimental-sqlite(Node 22 需 flag)
|
||||
export NODE_OPTIONS="--experimental-sqlite"
|
||||
# 建议:把 managed node 22.22.2 放到 PATH 最前(避免系统 node 23 引擎不匹配告警)
|
||||
export PATH="/c/Users/ybtdevxxl/.workbuddy/binaries/node/versions/22.22.2:$PATH"
|
||||
# 关键:清空 NODE_OPTIONS(否则 WorkBuddy 的 safe-delete shim 会拦截 pnpm 的删除操作导致安装失败)
|
||||
unset NODE_OPTIONS
|
||||
# 用 corepack 驱动 pnpm(PATH 里的 pnpm 挂在系统 node 23 下不可用)
|
||||
# 用 managed node 的 corepack 驱动 pnpm
|
||||
CORE="/c/Users/ybtdevxxl/.workbuddy/binaries/node/versions/22.22.2/corepack.cmd"
|
||||
"$CORE" pnpm install # 安装依赖
|
||||
"$CORE" pnpm dev:antd # 启动开发服务器
|
||||
"$CORE" pnpm dev:tdesign # 启动开发服务器
|
||||
```
|
||||
|
||||
### 坑 1:NODE_OPTIONS 污染
|
||||
- 现象:`pnpm install` 报 `[ERROR] [safe-delete] 操作失败 ... Some operations were aborted`
|
||||
- 原因:环境变量 `NODE_OPTIONS=--require=...genie-safe-delete.cjs --use-system-ca`(WorkBuddy 注入);且系统 Node 23 不支持 `--use-system-ca`,直接跑 PATH 里的 pnpm 会直接退出
|
||||
- 解法:`unset NODE_OPTIONS` + 用 managed node 22.22.2 的 corepack
|
||||
### 坑 1:node:sqlite 模块缺失(2026-08-26 实际遇到,本次修复根因)
|
||||
- 现象:`pnpm --version` / `pnpm install` 报 `ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite`(`at ../store/index/lib/index.js`)
|
||||
- 原因:**pnpm 11.16.0 在解析包元数据时强依赖 `node:sqlite`**,而系统 Node 23.0 / managed Node 22 默认未启用该内置模块(22.5+ 起在 `--experimental-sqlite` 标志后可用,23.4 才默认开)
|
||||
- 解法:`NODE_OPTIONS=--experimental-sqlite` 后再跑 pnpm(本次直接沿用该法装依赖、起服务均成功)
|
||||
|
||||
### 坑 2:Windows 符号链接权限
|
||||
- 现象:`pnpm install` 报 `UNKNOWN: unknown error, symlink '.pnpm\postcss@8.5.26\...' -> ...`,下载完 1723 包后在 hoist 阶段失败
|
||||
### 坑 2:NODE_OPTIONS 污染 / shim
|
||||
- 现象:`pnpm install` 报 `[ERROR] [safe-delete] 操作失败 ... Some operations were aborted`(WorkBuddy 注入 `--require=...genie-safe-delete.cjs --use-system-ca`)
|
||||
- 原因:该 shim 会拦截 pnpm 的删除/符号链接操作
|
||||
- 解法:在写死 `NODE_OPTIONS=--experimental-sqlite` 时已同时覆盖掉 shim;如要保留 shim,则先 `unset NODE_OPTIONS` 再补 flag
|
||||
|
||||
### 坑 3:Windows 符号链接权限
|
||||
- 现象:`pnpm install` 报 `UNKNOWN: unknown error, symlink ...`,hoist 阶段失败
|
||||
- 原因:本机未开「开发者模式/无管理员权限」,pnpm 默认 symlink 创建失败
|
||||
- 解法:项目 `.npmrc` 已添加 `package-import-method=copy`(用拷贝替代符号链接,代价是磁盘占用/安装略慢)
|
||||
- 解法:项目 `.npmrc` 已改用 `node-linker=hoisted`(扁平化)方案规避,见下
|
||||
|
||||
### 镜像配置(.npmrc)
|
||||
### 镜像与链接配置(.npmrc)
|
||||
```ini
|
||||
registry=https://registry.npmmirror.com
|
||||
package-import-method=copy
|
||||
node-linker=hoisted # 避免 symlink 权限问题,走扁平化安装
|
||||
store-dir=E:\.pnpm-store # store 与项目同盘(E 盘),走同卷硬链接,快且无需管理员权限
|
||||
```
|
||||
|
||||
## 4. 常用命令
|
||||
@ -74,11 +75,11 @@ pnpm install # 装依赖(preinstall 会校验必须用 pnpm;postins
|
||||
pnpm reinstall # 清 lock 重装(= clean --del-lock && install)
|
||||
|
||||
# 开发
|
||||
pnpm dev:antd # 启动 web-antd(默认 http://localhost:5666)
|
||||
pnpm dev:tdesign # 启动 web-tdesign(默认 http://localhost:5999,被占用自动切 6000)
|
||||
pnpm dev # turbo 并行起所有 app
|
||||
|
||||
# 构建 / 检查
|
||||
pnpm build:antd # 构建 web-antd
|
||||
pnpm build:tdesign # 构建 web-tdesign
|
||||
pnpm check:type # 类型检查(turbo run typecheck)
|
||||
pnpm lint # 代码检查
|
||||
pnpm test:unit # 单元测试(vitest)
|
||||
@ -87,19 +88,18 @@ pnpm test:unit # 单元测试(vitest)
|
||||
## 5. 架构速览(Vben Admin 5 monorepo)
|
||||
|
||||
```
|
||||
apps/ 应用层:web-antd(主)、web-ele、web-naive、web-tdesign、web-antdv-next、backend-mock
|
||||
apps/ 应用层:web-tdesign(唯一主应用)
|
||||
internal/ 内部工具:vite-config、tsconfig、eslint/stylelint/oxlint 配置、tailwind-config、turbo-run
|
||||
packages/ 共享包:@core(核心 UI/布局)、effects(请求/权限/hooks/插件)、stores、locales、
|
||||
preferences(偏好设置)、icons、constants、utils、styles、types
|
||||
```
|
||||
|
||||
- 主应用源码入口:`apps/web-antd/src/`
|
||||
- 主应用源码入口:`apps/web-tdesign/src/`
|
||||
- 路由:`src/router/routes/modules/`(dashboard.ts / demos.ts / vben.ts)
|
||||
- 页面:`src/views/`
|
||||
- API:`src/api/request.ts`(封装 `requestClient`)、`src/api/core/`
|
||||
- 偏好:`src/preferences.ts`(`defineOverridesPreferences`,官方默认 + 少量扩展字段)
|
||||
- 开发代理:`vite.config.ts` 中 `/api` → `http://localhost:5320/api`(backend-mock),rewrite 去掉 `/api` 前缀
|
||||
- 环境变量:`apps/web-antd/.env.development`(`VITE_PORT=5666`、`VITE_NITRO_MOCK=true`、`VITE_GLOB_API_URL=/api`)
|
||||
- 环境变量:`apps/web-tdesign/.env.development`(`VITE_PORT=5999`)
|
||||
- 路径别名:`#/*` → `./src/*`(package.json imports)
|
||||
|
||||
### 关键开发约定(Vben 5 特有)
|
||||
@ -113,6 +113,14 @@ packages/ 共享包:@core(核心 UI/布局)、effects(请求/权限
|
||||
|
||||
> 倒序(最新在上)。每条:日期 / 操作 / 结果 / 备注
|
||||
|
||||
### 2026-08-26(修复依赖安装)
|
||||
1. **问题**:`pnpm install` 装不上,报 `ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite`
|
||||
2. **根因**:pnpm 11.16.0 强依赖 `node:sqlite`,但系统 Node 23.0 / managed Node 22 默认未启用该内置模块(需 `--experimental-sqlite` 标志)
|
||||
3. **修复**:设 `NODE_OPTIONS=--experimental-sqlite` 后用 managed node 22.22.2 + corepack 跑 `pnpm install` → 成功(39 workspace 项目,Already up to date)✅
|
||||
4. **启动**:`pnpm dev:tdesign` → Vite ready,http://localhost:6000(5999 被占用自动切换)✅ HTTP 200,标题「Vben Admin Tdesign」
|
||||
5. **澄清项目结构**:确认项目已精简为**唯一应用 web-tdesign**(非 agents.md 旧记录的 web-antd),同步更新本档案
|
||||
6. **残留进程清理**:终止了此前多次失败重试遗留的 pnpm/tsdown/stub 后台进程
|
||||
|
||||
### 2026-08-24(首次搭建)
|
||||
1. **梳理项目**:确认为 Vben Admin 5.7.0 官方模板克隆,无业务定制;主应用 web-antd(端口 5666)
|
||||
2. **安装依赖**:
|
||||
@ -129,6 +137,14 @@ packages/ 共享包:@core(核心 UI/布局)、effects(请求/权限
|
||||
2. 必拷文件:`agents.md`(本文件)+ `.workbuddy/memory/`(历史工作日志)
|
||||
3. 本机环境要点(新机器):
|
||||
- Node ≥ 22.18;pnpm ≥ 11(corepack)
|
||||
- 跑 pnpm 前 `unset NODE_OPTIONS`
|
||||
- Windows 无开发者模式时保持 `.npmrc` 的 `package-import-method=copy`
|
||||
4. 装依赖后先 `pnpm dev:antd` 验证 5666 端口
|
||||
- 跑 pnpm 前设 `NODE_OPTIONS=--experimental-sqlite`(否则报 node:sqlite 缺失)
|
||||
- Windows 无开发者模式时保持 `.npmrc` 的 `node-linker=hoisted` 与 `store-dir=E:\.pnpm-store`
|
||||
4. 装依赖后先 `pnpm dev:tdesign` 验证 5999/6000 端口
|
||||
|
||||
## 统一工作约定(xpcool.com 中央规则)
|
||||
|
||||
> 与中央 `../workbuddy.xpcool.com/.workbuddy/memory/MEMORY.md` 保持一致;本项目变更流水在 `.workbuddy/memory/CHANGELOG.md`。
|
||||
|
||||
1. **中文注释**:写/改代码时,在应有处(函数、复杂逻辑、配置项、非显然分支)加中文注释。
|
||||
2. **做记录**:每次请求/变更/修复/文档动作,都在本项目 `.workbuddy/memory/CHANGELOG.md` 顶部追加一条,格式 `YYYY-MM-DD | 类型 | 一句话摘要`(类型:REQ/CHG/FIX/DOC/CFG/DEP)。
|
||||
3. **中文优先**:与用户的思考、输出、交流,能中文尽量中文。
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# 端口号
|
||||
VITE_PORT=5999
|
||||
VITE_PORT=20100
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||
# 接口地址(同源相对路径,由 nginx 将 /api/service 反代到后端 10100)
|
||||
VITE_GLOB_API_URL=/api/service
|
||||
|
||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||
VITE_COMPRESS=none
|
||||
|
||||
@ -23,7 +23,7 @@ export namespace AuthApi {
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>(
|
||||
'/admin/v1/system/auth/login',
|
||||
'/api/service/admin/system/auth/login',
|
||||
data,
|
||||
);
|
||||
}
|
||||
@ -33,7 +33,7 @@ export async function loginApi(data: AuthApi.LoginParams) {
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>(
|
||||
'/admin/v1/system/auth/refresh',
|
||||
'/api/service/admin/system/auth/refresh',
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
@ -44,7 +44,7 @@ export async function refreshTokenApi() {
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return baseRequestClient.post('/admin/v1/system/auth/logout', {
|
||||
return baseRequestClient.post('/api/service/admin/system/auth/logout', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
@ -53,8 +53,8 @@ export async function logoutApi() {
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
const data = await requestClient.get<{ codes: string[] }>(
|
||||
'/admin/v1/system/auth/codes',
|
||||
const data = await requestClient.post<{ codes: string[] }>(
|
||||
'/api/service/admin/system/auth/codes',
|
||||
);
|
||||
return data.codes ?? [];
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import type { RouteRecordStringComponent } from '@vben/types';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 后端返回的菜单路由结构(/admin/v1/system/menu/routes 的 data)
|
||||
* 后端返回的菜单路由结构(/api/service/admin/system/menu/routes 的 data)
|
||||
*/
|
||||
interface MenuRoutesResp {
|
||||
routes: RouteRecordStringComponent[];
|
||||
@ -13,8 +13,8 @@ interface MenuRoutesResp {
|
||||
* 获取用户所有菜单(vben backend 动态路由)
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
const data = await requestClient.get<MenuRoutesResp>(
|
||||
'/admin/v1/system/menu/routes',
|
||||
const data = await requestClient.post<MenuRoutesResp>(
|
||||
'/api/service/admin/system/menu/routes',
|
||||
);
|
||||
return data.routes ?? [];
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import type { UserInfo } from '@vben/types';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 后端返回的当前管理员信息结构(/admin/v1/system/auth/info 的 data)
|
||||
* 后端返回的当前管理员信息结构(/api/service/admin/system/auth/info 的 data)
|
||||
*/
|
||||
interface AdminInfoResp {
|
||||
adminId: number;
|
||||
@ -17,13 +17,13 @@ interface AdminInfoResp {
|
||||
* 将后端字段映射为 vben UserInfo 结构
|
||||
*/
|
||||
export async function getUserInfoApi(): Promise<UserInfo> {
|
||||
const data = await requestClient.get<AdminInfoResp>(
|
||||
'/admin/v1/system/auth/info',
|
||||
const data = await requestClient.post<AdminInfoResp>(
|
||||
'/api/service/admin/system/auth/info',
|
||||
);
|
||||
return {
|
||||
avatar: '',
|
||||
desc: '',
|
||||
homePath: '/system/admin',
|
||||
homePath: '/system/role',
|
||||
realName: data.nickname ?? data.username,
|
||||
roles: data.roles ?? [],
|
||||
token: '',
|
||||
|
||||
214
apps/web-tdesign/src/api/house.ts
Normal file
214
apps/web-tdesign/src/api/house.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// ---------- 小区/楼盘(/api/service/admin/house/community) ----------
|
||||
export interface CommunityItem {
|
||||
id: number;
|
||||
name: string;
|
||||
region: string;
|
||||
businessDistrict: string;
|
||||
address: string;
|
||||
lng: number;
|
||||
lat: number;
|
||||
buildYear: number;
|
||||
households: number;
|
||||
plotRatio: number;
|
||||
greenRate: number;
|
||||
propertyCompany: string;
|
||||
propertyFee: number;
|
||||
developer: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface CommunityListResp {
|
||||
list: CommunityItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getCommunityList(data: {
|
||||
page: number;
|
||||
size: number;
|
||||
keyword?: string;
|
||||
region?: string;
|
||||
}) {
|
||||
return requestClient.post<CommunityListResp>(
|
||||
'/api/service/admin/house/community/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function createCommunity(data: Partial<CommunityItem>) {
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/house/community/create',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function updateCommunity(id: number, data: Partial<CommunityItem>) {
|
||||
return requestClient.post(
|
||||
`/api/service/admin/house/community/update/${id}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteCommunity(id: number) {
|
||||
return requestClient.post(`/api/service/admin/house/community/delete/${id}`);
|
||||
}
|
||||
|
||||
// ---------- 房源(/api/service/admin/house/listing) ----------
|
||||
export interface ListingItem {
|
||||
id: number;
|
||||
communityId: number;
|
||||
communityName: string;
|
||||
buildingId: number;
|
||||
houseNo: string;
|
||||
layout: string;
|
||||
area: number;
|
||||
usableArea: number;
|
||||
orientation: string;
|
||||
floor: number;
|
||||
totalFloors: number;
|
||||
decoration: string;
|
||||
totalPrice: number;
|
||||
unitPrice: number;
|
||||
listPrice: number;
|
||||
source: string;
|
||||
sourceHouseId: string;
|
||||
sourceUrl: string;
|
||||
matchGroupId: number;
|
||||
onMarketDays: number;
|
||||
priceChangeCount: number;
|
||||
status: number;
|
||||
confidence: number;
|
||||
isBargain: number;
|
||||
listingTime: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ListingListResp {
|
||||
list: ListingItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ListingQuery {
|
||||
page: number;
|
||||
size: number;
|
||||
communityId?: number;
|
||||
keyword?: string;
|
||||
layout?: string;
|
||||
region?: string;
|
||||
source?: string;
|
||||
priceMin?: number;
|
||||
priceMax?: number;
|
||||
areaMin?: number;
|
||||
areaMax?: number;
|
||||
status?: number;
|
||||
isBargain?: number;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export function getListingList(data: ListingQuery) {
|
||||
return requestClient.post<ListingListResp>(
|
||||
'/api/service/admin/house/listing/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function createListing(data: Partial<ListingItem>) {
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/house/listing/create',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function updateListing(id: number, data: Partial<ListingItem>) {
|
||||
return requestClient.post(
|
||||
`/api/service/admin/house/listing/update/${id}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteListing(id: number) {
|
||||
return requestClient.post(`/api/service/admin/house/listing/delete/${id}`);
|
||||
}
|
||||
|
||||
export function batchMarkListing(data: {
|
||||
ids: number[];
|
||||
field: string;
|
||||
value: number;
|
||||
}) {
|
||||
return requestClient.post<{ affected: number }>(
|
||||
'/api/service/admin/house/listing/batch-mark',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- 看板(/api/service/admin/house/dashboard) ----------
|
||||
export interface OverviewResp {
|
||||
communityCount: number;
|
||||
listingCount: number;
|
||||
bargainCount: number;
|
||||
lowConfidence: number;
|
||||
avgUnitPrice: number;
|
||||
avgTotalPrice: number;
|
||||
avgListDays: number;
|
||||
}
|
||||
|
||||
export interface MapPoint {
|
||||
communityId: number;
|
||||
name: string;
|
||||
region: string;
|
||||
lng: number;
|
||||
lat: number;
|
||||
avgUnitPrice: number;
|
||||
listingCount: number;
|
||||
bargainCount: number;
|
||||
}
|
||||
|
||||
export interface TrendPoint {
|
||||
date: string;
|
||||
avgListPrice: number;
|
||||
avgDealPrice: number;
|
||||
}
|
||||
|
||||
export interface RegionAgg {
|
||||
region: string;
|
||||
avgUnitPrice: number;
|
||||
listingCount: number;
|
||||
bargainCount: number;
|
||||
}
|
||||
|
||||
export function getDashboardOverview(data: { region?: string }) {
|
||||
return requestClient.post<OverviewResp>(
|
||||
'/api/service/admin/house/dashboard/overview',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function getMapPoints(data: {
|
||||
region?: string;
|
||||
priceMin?: number;
|
||||
priceMax?: number;
|
||||
status?: number;
|
||||
}) {
|
||||
return requestClient.post<{ points: MapPoint[] }>(
|
||||
'/api/service/admin/house/dashboard/map-points',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPriceTrend(data: {
|
||||
communityId?: number;
|
||||
region?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
return requestClient.post<{ trend: TrendPoint[] }>(
|
||||
'/api/service/admin/house/dashboard/price-trend',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function getAggregateRegion() {
|
||||
return requestClient.post<{ list: RegionAgg[] }>(
|
||||
'/api/service/admin/house/dashboard/aggregate-region',
|
||||
);
|
||||
}
|
||||
@ -8,17 +8,18 @@ export interface LogFile {
|
||||
}
|
||||
|
||||
export function getLogFiles() {
|
||||
return requestClient.get<{ dir: string; files: LogFile[] }>(
|
||||
'/admin/v1/base/log/files',
|
||||
return requestClient.post<{ dir: string; files: LogFile[] }>(
|
||||
'/api/service/admin/base/log/files',
|
||||
);
|
||||
}
|
||||
|
||||
export function getLogTail(params: {
|
||||
export function getLogTail(data: {
|
||||
file: string;
|
||||
lines: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return requestClient.get<{ lines: string[] }>('/admin/v1/base/log/tail', {
|
||||
params,
|
||||
});
|
||||
return requestClient.post<{ lines: string[] }>(
|
||||
'/api/service/admin/base/log/tail',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
// ---------- 管理员(/admin/v1/admin) ----------
|
||||
// ---------- 管理员(/api/service/admin/admin) ----------
|
||||
export interface AdminItem {
|
||||
id: number;
|
||||
username: string;
|
||||
@ -16,12 +16,15 @@ export interface AdminListResp {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getAdminList(params: {
|
||||
export function getAdminList(data: {
|
||||
page: number;
|
||||
size: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return requestClient.get<AdminListResp>('/admin/v1/admin', { params });
|
||||
return requestClient.post<AdminListResp>(
|
||||
'/api/service/admin/admin/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function createAdmin(data: {
|
||||
@ -30,25 +33,30 @@ export function createAdmin(data: {
|
||||
nickname: string;
|
||||
roleIds: number[];
|
||||
}) {
|
||||
return requestClient.post<{ id: number }>('/admin/v1/admin', data);
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/admin/create',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function updateAdmin(
|
||||
id: number,
|
||||
data: { nickname: string; status: number; roleIds: number[] },
|
||||
) {
|
||||
return requestClient.put(`/admin/v1/admin/${id}`, data);
|
||||
return requestClient.post(`/api/service/admin/admin/update/${id}`, data);
|
||||
}
|
||||
|
||||
export function resetAdminPassword(id: number, password: string) {
|
||||
return requestClient.put(`/admin/v1/admin/${id}/password`, { password });
|
||||
return requestClient.post(`/api/service/admin/admin/resetPwd/${id}`, {
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAdmin(id: number) {
|
||||
return requestClient.delete(`/admin/v1/admin/${id}`);
|
||||
return requestClient.post(`/api/service/admin/admin/delete/${id}`);
|
||||
}
|
||||
|
||||
// ---------- 角色(/admin/v1/system/role) ----------
|
||||
// ---------- 角色(/api/service/admin/system/role) ----------
|
||||
export interface RoleItem {
|
||||
id: number;
|
||||
code: string;
|
||||
@ -63,12 +71,15 @@ export interface RoleListResp {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getRoleList(params: {
|
||||
export function getRoleList(data: {
|
||||
page: number;
|
||||
size: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return requestClient.get<RoleListResp>('/admin/v1/system/role', { params });
|
||||
return requestClient.post<RoleListResp>(
|
||||
'/api/service/admin/system/role/list',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function createRole(data: {
|
||||
@ -77,21 +88,24 @@ export function createRole(data: {
|
||||
status: number;
|
||||
menuIds: number[];
|
||||
}) {
|
||||
return requestClient.post<{ id: number }>('/admin/v1/system/role', data);
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/system/role/create',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function updateRole(
|
||||
id: number,
|
||||
data: { name: string; status: number; menuIds: number[] },
|
||||
) {
|
||||
return requestClient.put(`/admin/v1/system/role/${id}`, data);
|
||||
return requestClient.post(`/api/service/admin/system/role/update/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteRole(id: number) {
|
||||
return requestClient.delete(`/admin/v1/system/role/${id}`);
|
||||
return requestClient.post(`/api/service/admin/system/role/delete/${id}`);
|
||||
}
|
||||
|
||||
// ---------- 菜单(/admin/v1/system/menu) ----------
|
||||
// ---------- 菜单(/api/service/admin/system/menu) ----------
|
||||
export interface MenuNode {
|
||||
id: number;
|
||||
parentId: number;
|
||||
@ -108,17 +122,80 @@ export interface MenuNode {
|
||||
}
|
||||
|
||||
export function getMenuTree() {
|
||||
return requestClient.get<{ tree: MenuNode[] }>('/admin/v1/system/menu/tree');
|
||||
return requestClient.post<{ tree: MenuNode[] }>(
|
||||
'/api/service/admin/system/menu/tree',
|
||||
);
|
||||
}
|
||||
|
||||
export function createMenu(data: Partial<MenuNode>) {
|
||||
return requestClient.post<{ id: number }>('/admin/v1/system/menu', data);
|
||||
return requestClient.post<{ id: number }>(
|
||||
'/api/service/admin/system/menu/create',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function updateMenu(id: number, data: Partial<MenuNode>) {
|
||||
return requestClient.put(`/admin/v1/system/menu/${id}`, data);
|
||||
return requestClient.post(`/api/service/admin/system/menu/update/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteMenu(id: number) {
|
||||
return requestClient.delete(`/admin/v1/system/menu/${id}`);
|
||||
return requestClient.post(`/api/service/admin/system/menu/delete/${id}`);
|
||||
}
|
||||
|
||||
// ---------- 操作日志(/api/service/admin/system/log) ----------
|
||||
export interface LogItem {
|
||||
id: number;
|
||||
adminId: number;
|
||||
permission: string;
|
||||
method: string;
|
||||
path: string;
|
||||
ip: string;
|
||||
param: string;
|
||||
durationMs: number;
|
||||
statusCode: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface LogListResp {
|
||||
list: LogItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 操作日志分页查询(keyword 匹配权限码/路径/IP) */
|
||||
export function getSystemLogList(data: {
|
||||
page: number;
|
||||
size: number;
|
||||
keyword?: string;
|
||||
adminId?: number;
|
||||
}) {
|
||||
return requestClient.post<LogListResp>('/api/service/admin/system/log', data);
|
||||
}
|
||||
|
||||
// ---------- 登录日志(/api/service/admin/system/login-log) ----------
|
||||
export interface LoginLogItem {
|
||||
id: number;
|
||||
username: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
status: number; // 1 成功 0 失败
|
||||
failReason: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface LoginLogListResp {
|
||||
list: LoginLogItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 登录日志分页查询(username 匹配账号,status 按结果过滤) */
|
||||
export function getLoginLogList(data: {
|
||||
page: number;
|
||||
size: number;
|
||||
username?: string;
|
||||
status?: number;
|
||||
}) {
|
||||
return requestClient.post<LoginLogListResp>(
|
||||
'/api/service/admin/system/login-log',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import TDesign from 'tdesign-vue-next';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
@ -36,6 +37,9 @@ async function bootstrap(namespace: string) {
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 全局注册 tdesign-vue-next 组件(t-table/t-button/t-dialog 等业务页面直接使用)
|
||||
app.use(TDesign);
|
||||
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
|
||||
@ -14,6 +14,8 @@ import { $t } from '#/locales';
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
// 保持原始的 pageMap:vben 内部 normalizeViewPath 会在 generate-routes-backend
|
||||
// 中再次处理 keys 归一化。此处直接交给 vben 即可。
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
|
||||
@ -13,58 +12,13 @@ defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: '123456',
|
||||
username: findUser.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
|
||||
220
apps/web-tdesign/src/views/house/community/index.vue
Normal file
220
apps/web-tdesign/src/views/house/community/index.vue
Normal file
@ -0,0 +1,220 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CommunityItem } from '#/api/house';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
|
||||
import {
|
||||
createCommunity,
|
||||
deleteCommunity,
|
||||
getCommunityList,
|
||||
updateCommunity,
|
||||
} from '#/api/house';
|
||||
|
||||
// 小区管理:列表 + 新增/编辑弹窗 + 删除(软删除)
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<CommunityItem[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<null | CommunityItem>(null);
|
||||
|
||||
const regionOptions = [
|
||||
'云岩区', '南明区', '观山湖区', '花溪区', '乌当区', '白云区',
|
||||
'清镇市', '修文县', '开阳县', '息烽县',
|
||||
].map((r) => ({ label: r, value: r }));
|
||||
|
||||
const query = reactive({ page: 1, size: 10, keyword: '', region: '' });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
region: '',
|
||||
businessDistrict: '',
|
||||
address: '',
|
||||
lng: 0,
|
||||
lat: 0,
|
||||
buildYear: 0,
|
||||
households: 0,
|
||||
plotRatio: 0,
|
||||
greenRate: 0,
|
||||
propertyCompany: '',
|
||||
propertyFee: 0,
|
||||
developer: '',
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getCommunityList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, {
|
||||
name: '', region: '', businessDistrict: '', address: '', lng: 0, lat: 0,
|
||||
buildYear: 0, households: 0, plotRatio: 0, greenRate: 0,
|
||||
propertyCompany: '', propertyFee: 0, developer: '',
|
||||
});
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: CommunityItem) {
|
||||
editing.value = row;
|
||||
Object.assign(form, { ...row });
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (editing.value) {
|
||||
await updateCommunity(editing.value.id, form);
|
||||
} else {
|
||||
await createCommunity(form);
|
||||
}
|
||||
MessagePlugin.success('保存成功');
|
||||
dialogOpen.value = false;
|
||||
load();
|
||||
}
|
||||
|
||||
function onDelete(row: CommunityItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: `确认删除「${row.name}」?`,
|
||||
onConfirm: async () => {
|
||||
await deleteCommunity(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
load();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'name', title: '小区名称', width: 180 },
|
||||
{ colKey: 'region', title: '区县', width: 100 },
|
||||
{ colKey: 'businessDistrict', title: '板块', width: 120 },
|
||||
{ colKey: 'address', title: '地址' },
|
||||
{ colKey: 'buildYear', title: '建成年份', width: 90 },
|
||||
{ colKey: 'propertyCompany', title: '物业', width: 140 },
|
||||
{ colKey: 'op', title: '操作', width: 160 },
|
||||
];
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<t-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
placeholder="小区名称"
|
||||
style="width: 200px"
|
||||
@enter="load"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
clearable
|
||||
:options="regionOptions"
|
||||
placeholder="区县"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
</div>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:community:create'])"
|
||||
theme="primary"
|
||||
@click="openCreate"
|
||||
>
|
||||
新增小区
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:community:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:community:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
|
||||
<t-dialog
|
||||
v-model:visible="dialogOpen"
|
||||
:header="editing ? '编辑小区' : '新增小区'"
|
||||
width="640px"
|
||||
:on-confirm="submit"
|
||||
>
|
||||
<t-form label-align="right" label-width="90px">
|
||||
<div class="grid grid-cols-2 gap-x-4">
|
||||
<t-form-item label="小区名称" required>
|
||||
<t-input v-model="form.name" placeholder="如:中天未来方舟" />
|
||||
</t-form-item>
|
||||
<t-form-item label="区县">
|
||||
<t-select v-model="form.region" clearable :options="regionOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="板块">
|
||||
<t-input v-model="form.businessDistrict" placeholder="如:会展城" />
|
||||
</t-form-item>
|
||||
<t-form-item label="建成年份">
|
||||
<t-input-number v-model="form.buildYear" theme="normal" :min="0" />
|
||||
</t-form-item>
|
||||
<t-form-item label="物业公司">
|
||||
<t-input v-model="form.propertyCompany" />
|
||||
</t-form-item>
|
||||
<t-form-item label="开发商">
|
||||
<t-input v-model="form.developer" />
|
||||
</t-form-item>
|
||||
<t-form-item label="地址" class="col-span-2">
|
||||
<t-input v-model="form.address" />
|
||||
</t-form-item>
|
||||
<t-form-item label="经度">
|
||||
<t-input-number v-model="form.lng" theme="normal" :decimal-places="6" />
|
||||
</t-form-item>
|
||||
<t-form-item label="纬度">
|
||||
<t-input-number v-model="form.lat" theme="normal" :decimal-places="6" />
|
||||
</t-form-item>
|
||||
</div>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</Page>
|
||||
</template>
|
||||
132
apps/web-tdesign/src/views/house/dashboard/index.vue
Normal file
132
apps/web-tdesign/src/views/house/dashboard/index.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
import type { OverviewResp, RegionAgg, TrendPoint } from '#/api/house';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
getAggregateRegion,
|
||||
getDashboardOverview,
|
||||
getPriceTrend,
|
||||
} from '#/api/house';
|
||||
|
||||
// 数据看板:概览卡片 + 区域均价柱状图 + 价格趋势折线图
|
||||
const overview = ref<OverviewResp>({
|
||||
communityCount: 0,
|
||||
listingCount: 0,
|
||||
bargainCount: 0,
|
||||
lowConfidence: 0,
|
||||
avgUnitPrice: 0,
|
||||
avgTotalPrice: 0,
|
||||
avgListDays: 0,
|
||||
});
|
||||
|
||||
const regionChartRef = ref<EchartsUIType>();
|
||||
const trendChartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts: renderRegion } = useEcharts(regionChartRef);
|
||||
const { renderEcharts: renderTrend } = useEcharts(trendChartRef);
|
||||
|
||||
async function load() {
|
||||
overview.value = await getDashboardOverview({});
|
||||
const regionList: RegionAgg[] = (await getAggregateRegion()).list ?? [];
|
||||
const trend: TrendPoint[] = (await getPriceTrend({ limit: 30 })).trend ?? [];
|
||||
|
||||
renderRegion({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 60, right: 20, top: 30, bottom: 40 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: regionList.map((r) => r.region),
|
||||
axisLabel: { rotate: 30 },
|
||||
},
|
||||
yAxis: { type: 'value', name: '元/㎡' },
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: regionList.map((r) => Math.round(r.avgUnitPrice)),
|
||||
itemStyle: { color: '#5DCAA5' },
|
||||
barMaxWidth: 40,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
renderTrend({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['挂牌均价', '成交均价'] },
|
||||
grid: { left: 60, right: 20, top: 40, bottom: 30 },
|
||||
xAxis: { type: 'category', data: trend.map((t) => t.date) },
|
||||
yAxis: { type: 'value', name: '万元' },
|
||||
series: [
|
||||
{
|
||||
name: '挂牌均价',
|
||||
type: 'line',
|
||||
data: trend.map((t) => t.avgListPrice),
|
||||
itemStyle: { color: '#EF9F27' },
|
||||
lineStyle: { type: 'dashed' },
|
||||
smooth: true,
|
||||
},
|
||||
{
|
||||
name: '成交均价',
|
||||
type: 'line',
|
||||
data: trend.map((t) => t.avgDealPrice),
|
||||
itemStyle: { color: '#5DCAA5' },
|
||||
smooth: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mb-4 grid grid-cols-2 gap-3 md:grid-cols-6">
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-xs opacity-70">小区数</p>
|
||||
<p class="mt-1 text-2xl font-medium">{{ overview.communityCount }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-xs opacity-70">在售房源</p>
|
||||
<p class="mt-1 text-2xl font-medium">{{ overview.listingCount }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-xs opacity-70">笋盘</p>
|
||||
<p class="mt-1 text-2xl font-medium">{{ overview.bargainCount }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-xs opacity-70">低可信房源</p>
|
||||
<p class="mt-1 text-2xl font-medium">{{ overview.lowConfidence }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-xs opacity-70">在售均价(元/㎡)</p>
|
||||
<p class="mt-1 text-2xl font-medium">
|
||||
{{ Math.round(overview.avgUnitPrice) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-xs opacity-70">平均挂牌天数</p>
|
||||
<p class="mt-1 text-2xl font-medium">
|
||||
{{ Math.round(overview.avgListDays) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 rounded-lg border p-4">
|
||||
<h3 class="mb-2 text-sm font-medium">区域在售均价</h3>
|
||||
<div style="height: 300px">
|
||||
<EchartsUI ref="regionChartRef" style="height: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h3 class="mb-2 text-sm font-medium">价格趋势(挂牌 vs 成交)</h3>
|
||||
<div style="height: 320px">
|
||||
<EchartsUI ref="trendChartRef" style="height: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
18
apps/web-tdesign/src/views/house/data/index.vue
Normal file
18
apps/web-tdesign/src/views/house/data/index.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
// 数据明细:价格快照 / 成交记录 / 配套 / 学区划片。
|
||||
// 阶段 2 起接入采集器写入的数据后,此处按类别拆分为多个列表页。
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
|
||||
<p class="text-base font-medium">数据明细</p>
|
||||
<p class="mt-2 text-sm">
|
||||
价格快照、成交记录、配套 POI、学区划片将在采集器接入后于此展示。
|
||||
</p>
|
||||
<p class="mt-1 text-sm">当前阶段先完成小区 / 房源管理与数据看板。</p>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
291
apps/web-tdesign/src/views/house/listing/index.vue
Normal file
291
apps/web-tdesign/src/views/house/listing/index.vue
Normal file
@ -0,0 +1,291 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ListingItem } from '#/api/house';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
|
||||
import {
|
||||
batchMarkListing,
|
||||
deleteListing,
|
||||
getListingList,
|
||||
updateListing,
|
||||
} from '#/api/house';
|
||||
|
||||
// 房源管理:多维筛选 + 多选批量标记(笋盘/低可信/状态)+ 编辑/删除
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<ListingItem[]>([]);
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<null | ListingItem>(null);
|
||||
|
||||
const regionOptions = [
|
||||
'云岩区', '南明区', '观山湖区', '花溪区', '乌当区', '白云区',
|
||||
'清镇市', '修文县', '开阳县', '息烽县',
|
||||
].map((r) => ({ label: r, value: r }));
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
keyword: '',
|
||||
region: '',
|
||||
layout: '',
|
||||
source: '',
|
||||
priceMin: undefined as number | undefined,
|
||||
priceMax: undefined as number | undefined,
|
||||
status: 0,
|
||||
isBargain: 0,
|
||||
confidence: 0,
|
||||
});
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getListingList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(query, {
|
||||
keyword: '', region: '', layout: '', source: '',
|
||||
priceMin: undefined, priceMax: undefined, status: 0, isBargain: 0, confidence: 0,
|
||||
});
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
function onSelectChange(keys: Array<string | number>) {
|
||||
selectedKeys.value = keys.map(Number);
|
||||
}
|
||||
|
||||
/** 批量标记(笋盘/低可信/状态) */
|
||||
async function onBatchMark(field: string, value: number) {
|
||||
if (selectedKeys.value.length === 0) {
|
||||
MessagePlugin.warning('请先勾选房源');
|
||||
return;
|
||||
}
|
||||
await batchMarkListing({ ids: selectedKeys.value, field, value });
|
||||
MessagePlugin.success('标记成功');
|
||||
load();
|
||||
}
|
||||
|
||||
function openEdit(row: ListingItem) {
|
||||
editing.value = row;
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (editing.value) {
|
||||
await updateListing(editing.value.id, {
|
||||
status: editing.value.status,
|
||||
confidence: editing.value.confidence,
|
||||
isBargain: editing.value.isBargain,
|
||||
listPrice: editing.value.listPrice,
|
||||
unitPrice: editing.value.unitPrice,
|
||||
totalPrice: editing.value.totalPrice,
|
||||
});
|
||||
}
|
||||
MessagePlugin.success('保存成功');
|
||||
dialogOpen.value = false;
|
||||
load();
|
||||
}
|
||||
|
||||
function onDelete(row: ListingItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: '确认删除该房源?',
|
||||
onConfirm: async () => {
|
||||
await deleteListing(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
load();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'row-select', type: 'multiple', width: 46 },
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'communityName', title: '小区', width: 160 },
|
||||
{ colKey: 'layout', title: '户型', width: 100 },
|
||||
{ colKey: 'area', title: '面积㎡', width: 90 },
|
||||
{ colKey: 'totalPrice', title: '总价(万)', width: 100 },
|
||||
{ colKey: 'unitPrice', title: '单价', width: 100 },
|
||||
{ colKey: 'source', title: '来源', width: 90 },
|
||||
{ colKey: 'onMarketDays', title: '挂牌天数', width: 90 },
|
||||
{ colKey: 'statusText', title: '状态', width: 80 },
|
||||
{ colKey: 'flagText', title: '标记', width: 140 },
|
||||
{ colKey: 'op', title: '操作', width: 140 },
|
||||
];
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||
<t-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
placeholder="房号/户型"
|
||||
style="width: 160px"
|
||||
@enter="load"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
clearable
|
||||
:options="regionOptions"
|
||||
placeholder="区县"
|
||||
style="width: 130px"
|
||||
/>
|
||||
<t-input
|
||||
v-model="query.layout"
|
||||
clearable
|
||||
placeholder="户型(如3室2厅)"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<t-input-number
|
||||
v-model="query.priceMin"
|
||||
theme="normal"
|
||||
placeholder="总价下限"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<t-input-number
|
||||
v-model="query.priceMax"
|
||||
theme="normal"
|
||||
placeholder="总价上限"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.status"
|
||||
:options="[
|
||||
{ label: '全部状态', value: 0 },
|
||||
{ label: '在售', value: 1 },
|
||||
{ label: '下架', value: 2 },
|
||||
{ label: '成交', value: 3 },
|
||||
]"
|
||||
style="width: 110px"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
<span class="ml-auto flex items-center gap-2">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:listing:batchMark'])"
|
||||
variant="outline"
|
||||
@click="onBatchMark('isBargain', 1)"
|
||||
>
|
||||
标记笋盘
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:listing:batchMark'])"
|
||||
variant="outline"
|
||||
@click="onBatchMark('confidence', 1)"
|
||||
>
|
||||
标记低可信
|
||||
</t-button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:selected-row-keys="selectedKeys"
|
||||
@page-change="onPageChange"
|
||||
@select-change="onSelectChange"
|
||||
>
|
||||
<template #statusText="{ row }">
|
||||
<t-tag
|
||||
:theme="row.status === 1 ? 'success' : row.status === 2 ? 'default' : 'primary'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 1 ? '在售' : row.status === 2 ? '下架' : '成交' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #flagText="{ row }">
|
||||
<span class="flex items-center gap-1">
|
||||
<t-tag v-if="row.isBargain === 1" theme="warning" variant="light" size="small">笋盘</t-tag>
|
||||
<t-tag v-if="row.confidence === 1" theme="danger" variant="light" size="small">低可信</t-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:listing:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:listing:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
|
||||
<t-dialog
|
||||
v-model:visible="dialogOpen"
|
||||
header="编辑房源"
|
||||
width="560px"
|
||||
:on-confirm="submit"
|
||||
>
|
||||
<t-form v-if="editing" label-align="right" label-width="90px">
|
||||
<div class="grid grid-cols-2 gap-x-4">
|
||||
<t-form-item label="挂牌价(万)">
|
||||
<t-input-number v-model="editing.listPrice" theme="normal" />
|
||||
</t-form-item>
|
||||
<t-form-item label="单价(元/㎡)">
|
||||
<t-input-number v-model="editing.unitPrice" theme="normal" />
|
||||
</t-form-item>
|
||||
<t-form-item label="总价(万)">
|
||||
<t-input-number v-model="editing.totalPrice" theme="normal" />
|
||||
</t-form-item>
|
||||
<t-form-item label="状态">
|
||||
<t-select
|
||||
v-model="editing.status"
|
||||
:options="[
|
||||
{ label: '在售', value: 1 },
|
||||
{ label: '下架', value: 2 },
|
||||
{ label: '成交', value: 3 },
|
||||
]"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="笋盘">
|
||||
<t-switch v-model="editing.isBargain" :custom-value="[1, 0]" />
|
||||
</t-form-item>
|
||||
<t-form-item label="低可信">
|
||||
<t-switch v-model="editing.confidence" :custom-value="[1, 0]" />
|
||||
</t-form-item>
|
||||
</div>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</Page>
|
||||
</template>
|
||||
@ -94,7 +94,7 @@ async function submit() {
|
||||
|
||||
function onDelete(row: AdminItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: '确认删除该管理员?',
|
||||
header: '确认删除该人员?',
|
||||
onConfirm: async () => {
|
||||
await deleteAdmin(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
@ -159,11 +159,11 @@ onMounted(() => {
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
</div>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:admin:create'])"
|
||||
v-if="hasAccessByCodes(['system:personnel:create'])"
|
||||
theme="primary"
|
||||
@click="openCreate"
|
||||
>
|
||||
新增管理员
|
||||
新增人员
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
@ -179,7 +179,7 @@ onMounted(() => {
|
||||
<template #roleNamesText="{ row }">{{ (row.roleNames ?? []).join(', ') }}</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:admin:update'])"
|
||||
v-if="hasAccessByCodes(['system:personnel:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@ -188,7 +188,7 @@ onMounted(() => {
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:admin:resetPwd'])"
|
||||
v-if="hasAccessByCodes(['system:personnel:resetPwd'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@ -197,7 +197,7 @@ onMounted(() => {
|
||||
重置密码
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:admin:delete'])"
|
||||
v-if="hasAccessByCodes(['system:personnel:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@ -210,7 +210,7 @@ onMounted(() => {
|
||||
|
||||
<t-dialog
|
||||
v-model:visible="dialogOpen"
|
||||
:header="editing ? '编辑管理员' : '新增管理员'"
|
||||
:header="editing ? '编辑人员' : '新增人员'"
|
||||
:on-confirm="submit"
|
||||
>
|
||||
<t-form label-align="right" label-width="80px">
|
||||
|
||||
138
apps/web-tdesign/src/views/system/log/index.vue
Normal file
138
apps/web-tdesign/src/views/system/log/index.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LogItem } from '#/api/system';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
|
||||
import { getSystemLogList } from '#/api/system';
|
||||
|
||||
// 操作日志页面:常规分页 + 关键字筛选 + 列表展示
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<LogItem[]>([]);
|
||||
|
||||
// 查询条件:keyword 匹配权限码/路径/IP
|
||||
const query = reactive({ page: 1, size: 10, keyword: '' });
|
||||
// t-table 分页状态
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getSystemLogList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页 */
|
||||
function reset() {
|
||||
query.keyword = '';
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
/** 分页变化回调 */
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
/** 复制参数内容(调试用) */
|
||||
function onCopyParam(row: LogItem) {
|
||||
navigator.clipboard
|
||||
?.writeText(row.param ?? '')
|
||||
.then(() => MessagePlugin.success('参数已复制'));
|
||||
}
|
||||
|
||||
// 表格列定义:colKey 与行字段对应,插槽自定义渲染
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'permission', title: '权限标识', width: 160 },
|
||||
{ colKey: 'method', title: '方法', width: 80 },
|
||||
{ colKey: 'path', title: '请求路径', width: 260 },
|
||||
{ colKey: 'adminId', title: '操作人', width: 80 },
|
||||
{ colKey: 'ip', title: 'IP', width: 130 },
|
||||
{ colKey: 'durationMs', title: '耗时(ms)', width: 90 },
|
||||
{ colKey: 'statusCode', title: '状态码', width: 90 },
|
||||
{ colKey: 'param', title: '参数', width: 180 },
|
||||
{ colKey: 'createdAt', title: '操作时间', width: 170 },
|
||||
];
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 顶部筛选区:关键字 + 查询 / 重置 -->
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<t-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
placeholder="权限标识 / 路径 / IP"
|
||||
style="width: 260px"
|
||||
@enter="load"
|
||||
/>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:log:list'])"
|
||||
theme="primary"
|
||||
@click="load"
|
||||
>
|
||||
查询
|
||||
</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志列表 -->
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #method="{ row }">
|
||||
<t-tag
|
||||
:theme="row.method === 'POST' ? 'primary' : 'success'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.method }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #statusCode="{ row }">
|
||||
<t-tag
|
||||
:theme="row.statusCode < 400 ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.statusCode }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #param="{ row }">
|
||||
<t-tooltip :content="row.param" placement="top">
|
||||
<span
|
||||
class="cursor-pointer text-gray-500"
|
||||
@click="onCopyParam(row)"
|
||||
>
|
||||
{{ (row.param ?? '').slice(0, 30) || '-' }}
|
||||
</span>
|
||||
</t-tooltip>
|
||||
</template>
|
||||
</t-table>
|
||||
</Page>
|
||||
</template>
|
||||
124
apps/web-tdesign/src/views/system/login-log/index.vue
Normal file
124
apps/web-tdesign/src/views/system/login-log/index.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginLogItem } from '#/api/system';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { getLoginLogList } from '#/api/system';
|
||||
|
||||
// 登录日志页面:账号 + 结果筛选、分页列表
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<LoginLogItem[]>([]);
|
||||
|
||||
// 查询条件:username 匹配账号,status 按结果过滤(-1 全部)
|
||||
const query = reactive({ page: 1, size: 10, username: '', status: -1 });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getLoginLogList({
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
username: query.username || undefined,
|
||||
status: query.status >= 0 ? query.status : undefined,
|
||||
});
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页 */
|
||||
function reset() {
|
||||
query.username = '';
|
||||
query.status = -1;
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
/** 分页变化回调 */
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'username', title: '登录账号', width: 140 },
|
||||
{ colKey: 'ip', title: '来源 IP', width: 140 },
|
||||
{ colKey: 'userAgent', title: '浏览器 UA', width: 240 },
|
||||
{ colKey: 'status', title: '结果', width: 90 },
|
||||
{ colKey: 'failReason', title: '失败原因', width: 220 },
|
||||
{ colKey: 'createdAt', title: '登录时间', width: 170 },
|
||||
];
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 顶部筛选区:账号 + 结果 + 查询 / 重置 -->
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<t-input
|
||||
v-model="query.username"
|
||||
clearable
|
||||
placeholder="登录账号"
|
||||
style="width: 180px"
|
||||
@enter="load"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.status"
|
||||
style="width: 120px"
|
||||
:options="[
|
||||
{ label: '全部', value: -1 },
|
||||
{ label: '成功', value: 1 },
|
||||
{ label: '失败', value: 0 },
|
||||
]"
|
||||
/>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:login-log:list'])"
|
||||
theme="primary"
|
||||
@click="load"
|
||||
>
|
||||
查询
|
||||
</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录日志列表 -->
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<t-tag
|
||||
:theme="row.status === 1 ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 1 ? '成功' : '失败' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #failReason="{ row }">
|
||||
<span :title="row.failReason">{{ (row.failReason || '-').slice(0, 30) }}</span>
|
||||
</template>
|
||||
</t-table>
|
||||
</Page>
|
||||
</template>
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MenuNode } from '#/api/system';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
@ -18,10 +18,20 @@ import {
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const tree = ref<MenuNode[]>([]);
|
||||
// 扁平化后的菜单列表(含 depth 用于缩进展示),避免依赖 t-table 的 tree 渲染
|
||||
const flatList = ref<(MenuNode & { depth: number })[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<null | MenuNode>(null);
|
||||
|
||||
/** 递归将树扁平化为数组,按层级加 depth 字段用于 UI 缩进 */
|
||||
function flatten(nodes: MenuNode[], depth: number, acc: (MenuNode & { depth: number })[] = []): (MenuNode & { depth: number })[] {
|
||||
for (const n of nodes) {
|
||||
acc.push({ ...n, depth });
|
||||
if (n.children?.length) flatten(n.children, depth + 1, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
parentId: 0,
|
||||
name: '',
|
||||
@ -39,7 +49,8 @@ async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getMenuTree();
|
||||
tree.value = data.tree ?? [];
|
||||
// 扁平化:td-table 的 tree 模式在全量注册 TDesign 后渲染不稳定,改为按 depth 缩进的一维列表
|
||||
flatList.value = flatten(data.tree ?? [], 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -129,12 +140,17 @@ onMounted(load);
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="tree"
|
||||
:data="flatList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:tree="{ childrenKey: 'children', defaultExpandAll: true }"
|
||||
>
|
||||
<template #name="{ row }">
|
||||
<span :style="{ paddingLeft: `${row.depth * 20}px` }">
|
||||
<span v-if="row.depth > 0" class="mr-1 text-gray-400">└</span>
|
||||
{{ row.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template #typeText="{ row }">{{ row.type === 1 ? '菜单' : '按钮' }}</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
|
||||
@ -13,16 +13,10 @@ export default defineConfig(async () => {
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
// 后台管理接口 → GoFrame 后端
|
||||
'/admin': {
|
||||
// 统一后端前缀 → GoFrame 后端(端口 10100)
|
||||
'/api/service': {
|
||||
changeOrigin: true,
|
||||
target: 'http://localhost:8000',
|
||||
ws: true,
|
||||
},
|
||||
// 开放接口 → GoFrame 后端
|
||||
'/api': {
|
||||
changeOrigin: true,
|
||||
target: 'http://localhost:8000',
|
||||
target: 'http://localhost:10100',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
|
||||
42
dev_log.txt
Normal file
42
dev_log.txt
Normal file
@ -0,0 +1,42 @@
|
||||
$ pnpm -F @vben/web-tdesign run dev
|
||||
. | [WARN] Unsupported engine: wanted: {"node":"^22.18.0 || ^24.12.0"} (current: {"node":"v23.0.0","pnpm":"11.16.0"})
|
||||
$ pnpm vite --mode development
|
||||
[Vben Form] BREAKING CHANGE: Named field slot control bindings moved to `slotProps.componentProps`. Replace `v-bind="slotProps"` with `v-bind="slotProps.componentProps"`. See https://doc.vben.pro/components/common-ui/vben-form.html
|
||||
Port 5999 is in use, trying another one...
|
||||
|
||||
[32m[1mVITE[22m v8.2.2[39m [2mready in [0m[1m2781[22m[2m[0m ms[22m
|
||||
|
||||
[32m➜[39m [1mLocal[22m: [36mhttp://localhost:[1m6000[22m/[39m
|
||||
[32m➜[39m [1mNetwork[22m: [36mhttp://192.168.110.65:[1m6000[22m/[39m [2mWLAN[22m
|
||||
[32m➜[39m [1mNetwork[22m: [36mhttp://172.20.192.1:[1m6000[22m/[39m [2mvEthernet (Default …[22m
|
||||
➜ Vben Admin Docs: https://doc.vben.pro
|
||||
[2m[32m ➜[39m[22m[2m press [22m[1mh + enter[22m[2m to show help[22m
|
||||
[2m17:20:13[22m [36m[1m[vite][22m[39m [32m.env.development changed, restarting server...[39m
|
||||
[Vben Form] BREAKING CHANGE: Named field slot control bindings moved to `slotProps.componentProps`. Replace `v-bind="slotProps"` with `v-bind="slotProps.componentProps"`. See https://doc.vben.pro/components/common-ui/vben-form.html
|
||||
[2m17:20:14[22m [36m[1m[vite][22m[39m server restarted.
|
||||
|
||||
[32m➜[39m [1mLocal[22m: [36mhttp://localhost:[1m20000[22m/[39m
|
||||
[32m➜[39m [1mNetwork[22m: [36mhttp://192.168.110.65:[1m20000[22m/[39m [2mWLAN[22m
|
||||
[32m➜[39m [1mNetwork[22m: [36mhttp://172.20.192.1:[1m20000[22m/[39m [2mvEthernet (Default …[22m
|
||||
➜ Vben Admin Docs: https://doc.vben.pro
|
||||
[2m17:20:53[22m [36m[1m[vite][22m[39m [32m.env.development changed, restarting server...[39m
|
||||
[Vben Form] BREAKING CHANGE: Named field slot control bindings moved to `slotProps.componentProps`. Replace `v-bind="slotProps"` with `v-bind="slotProps.componentProps"`. See https://doc.vben.pro/components/common-ui/vben-form.html
|
||||
Port 20100 is in use, trying another one...
|
||||
[2m17:20:54[22m [36m[1m[vite][22m[39m server restarted.
|
||||
|
||||
[32m➜[39m [1mLocal[22m: [36mhttp://localhost:[1m20101[22m/[39m
|
||||
[32m➜[39m [1mNetwork[22m: [36mhttp://192.168.110.65:[1m20101[22m/[39m [2mWLAN[22m
|
||||
[32m➜[39m [1mNetwork[22m: [36mhttp://172.20.192.1:[1m20101[22m/[39m [2mvEthernet (Default …[22m
|
||||
➜ Vben Admin Docs: https://doc.vben.pro
|
||||
[2m18:53:59[22m [36m[1m[vite][22m[39m [32mvite.config.ts changed, restarting server...[39m
|
||||
[Vben Form] BREAKING CHANGE: Named field slot control bindings moved to `slotProps.componentProps`. Replace `v-bind="slotProps"` with `v-bind="slotProps.componentProps"`. See https://doc.vben.pro/components/common-ui/vben-form.html
|
||||
[2m18:54:00[22m [36m[1m[vite][22m[39m server restarted.
|
||||
[2m18:54:09[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/core/auth.ts[22m
|
||||
[2m18:54:13[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/core/menu.ts[22m
|
||||
[2m18:54:16[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/core/user.ts[22m
|
||||
[2m18:54:22[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/system.ts[22m
|
||||
[2m18:54:25[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/log.ts[22m
|
||||
E:\xxcool\project\admin.xpcool.com\apps\web-tdesign:
|
||||
[ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL] @vben/web-tdesign@5.7.0 dev: `pnpm vite --mode development`
|
||||
Exit status 4294967295
|
||||
[ELIFECYCLE] Command failed with exit code 4294967295.
|
||||
2371
pnpm-lock.yaml
generated
2371
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,7 @@ ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV CI=true
|
||||
|
||||
# 用 npm 直接装指定版本 pnpm(走 .npmrc 的 npmmirror 镜像,版本明确)
|
||||
RUN npm i -g pnpm@11.16.0
|
||||
RUN npm i -g corepack
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user