From 0ab6cf5f172b4e2674e6bea2984be2341520dd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Wed, 26 Aug 2026 23:55:07 +0800 Subject: [PATCH] =?UTF-8?q?CHG:=20=E7=99=BB=E5=BD=95=E9=A1=B5=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=E8=B4=A6=E5=8F=B7=E4=B8=8B=E6=8B=89=E4=B8=8E=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E5=AF=86=E7=A0=81=EF=BC=9Bhouse=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9BGitea=20act=5Frunner=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=83=A8=E7=BD=B2=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitea/workflows/deploy.yml | 79 +- .gitignore | 6 +- .workbuddy/memory/CHANGELOG.md | 21 + agents.md | 82 +- apps/web-tdesign/.env.development | 2 +- apps/web-tdesign/.env.production | 4 +- apps/web-tdesign/src/api/core/auth.ts | 10 +- apps/web-tdesign/src/api/core/menu.ts | 6 +- apps/web-tdesign/src/api/core/user.ts | 8 +- apps/web-tdesign/src/api/house.ts | 214 ++ apps/web-tdesign/src/api/log.ts | 13 +- apps/web-tdesign/src/api/system.ts | 113 +- apps/web-tdesign/src/bootstrap.ts | 4 + apps/web-tdesign/src/router/access.ts | 2 + .../src/views/_core/authentication/login.vue | 46 - .../src/views/house/community/index.vue | 220 ++ .../src/views/house/dashboard/index.vue | 132 + .../src/views/house/data/index.vue | 18 + .../src/views/house/listing/index.vue | 291 ++ .../src/views/system/admin/index.vue | 14 +- .../src/views/system/log/index.vue | 138 + .../src/views/system/login-log/index.vue | 124 + .../src/views/system/menu/index.vue | 26 +- apps/web-tdesign/vite.config.ts | 12 +- dev_log.txt | 42 + pnpm-lock.yaml | 2371 +---------------- scripts/deploy/Dockerfile | 3 +- 27 files changed, 1559 insertions(+), 2442 deletions(-) create mode 100644 .workbuddy/memory/CHANGELOG.md create mode 100644 apps/web-tdesign/src/api/house.ts create mode 100644 apps/web-tdesign/src/views/house/community/index.vue create mode 100644 apps/web-tdesign/src/views/house/dashboard/index.vue create mode 100644 apps/web-tdesign/src/views/house/data/index.vue create mode 100644 apps/web-tdesign/src/views/house/listing/index.vue create mode 100644 apps/web-tdesign/src/views/system/log/index.vue create mode 100644 apps/web-tdesign/src/views/system/login-log/index.vue create mode 100644 dev_log.txt diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 8f1b964..65b5ec5 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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 }}
分支: ${{ github.ref_name }}
提交: ${{ github.sha }}
触发人: ${{ github.actor }}
状态: 编译打包部署成功
访问: 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 }}
分支: ${{ github.ref_name }}
提交: ${{ github.sha }}
触发人: ${{ github.actor }}
状态: 构建或部署失败
日志: 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" diff --git a/.gitignore b/.gitignore index 3d94e02..326c881 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.workbuddy/memory/CHANGELOG.md b/.workbuddy/memory/CHANGELOG.md new file mode 100644 index 0000000..9f2c5d1 --- /dev/null +++ b/.workbuddy/memory/CHANGELOG.md @@ -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 中央索引 diff --git a/agents.md b/agents.md index a81d389..29adb86 100644 --- a/agents.md +++ b/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 install # 安装依赖 +"$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. **中文优先**:与用户的思考、输出、交流,能中文尽量中文。 diff --git a/apps/web-tdesign/.env.development b/apps/web-tdesign/.env.development index 1d10218..08aa2ad 100644 --- a/apps/web-tdesign/.env.development +++ b/apps/web-tdesign/.env.development @@ -1,5 +1,5 @@ # 端口号 -VITE_PORT=5999 +VITE_PORT=20100 VITE_BASE=/ diff --git a/apps/web-tdesign/.env.production b/apps/web-tdesign/.env.production index 5375847..f61880e 100644 --- a/apps/web-tdesign/.env.production +++ b/apps/web-tdesign/.env.production @@ -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 diff --git a/apps/web-tdesign/src/api/core/auth.ts b/apps/web-tdesign/src/api/core/auth.ts index 816875d..6983584 100644 --- a/apps/web-tdesign/src/api/core/auth.ts +++ b/apps/web-tdesign/src/api/core/auth.ts @@ -23,7 +23,7 @@ export namespace AuthApi { */ export async function loginApi(data: AuthApi.LoginParams) { return requestClient.post( - '/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( - '/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 ?? []; } diff --git a/apps/web-tdesign/src/api/core/menu.ts b/apps/web-tdesign/src/api/core/menu.ts index 86f260c..cf1d688 100644 --- a/apps/web-tdesign/src/api/core/menu.ts +++ b/apps/web-tdesign/src/api/core/menu.ts @@ -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( - '/admin/v1/system/menu/routes', + const data = await requestClient.post( + '/api/service/admin/system/menu/routes', ); return data.routes ?? []; } diff --git a/apps/web-tdesign/src/api/core/user.ts b/apps/web-tdesign/src/api/core/user.ts index 1d04cdf..1d1f88a 100644 --- a/apps/web-tdesign/src/api/core/user.ts +++ b/apps/web-tdesign/src/api/core/user.ts @@ -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 { - const data = await requestClient.get( - '/admin/v1/system/auth/info', + const data = await requestClient.post( + '/api/service/admin/system/auth/info', ); return { avatar: '', desc: '', - homePath: '/system/admin', + homePath: '/system/role', realName: data.nickname ?? data.username, roles: data.roles ?? [], token: '', diff --git a/apps/web-tdesign/src/api/house.ts b/apps/web-tdesign/src/api/house.ts new file mode 100644 index 0000000..e0f3af4 --- /dev/null +++ b/apps/web-tdesign/src/api/house.ts @@ -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( + '/api/service/admin/house/community/list', + data, + ); +} + +export function createCommunity(data: Partial) { + return requestClient.post<{ id: number }>( + '/api/service/admin/house/community/create', + data, + ); +} + +export function updateCommunity(id: number, data: Partial) { + 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( + '/api/service/admin/house/listing/list', + data, + ); +} + +export function createListing(data: Partial) { + return requestClient.post<{ id: number }>( + '/api/service/admin/house/listing/create', + data, + ); +} + +export function updateListing(id: number, data: Partial) { + 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( + '/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', + ); +} diff --git a/apps/web-tdesign/src/api/log.ts b/apps/web-tdesign/src/api/log.ts index 92f3ef5..f0b283a 100644 --- a/apps/web-tdesign/src/api/log.ts +++ b/apps/web-tdesign/src/api/log.ts @@ -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, + ); } diff --git a/apps/web-tdesign/src/api/system.ts b/apps/web-tdesign/src/api/system.ts index 185f27e..c80e71c 100644 --- a/apps/web-tdesign/src/api/system.ts +++ b/apps/web-tdesign/src/api/system.ts @@ -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('/admin/v1/admin', { params }); + return requestClient.post( + '/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('/admin/v1/system/role', { params }); + return requestClient.post( + '/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) { - 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) { - 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('/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( + '/api/service/admin/system/login-log', + data, + ); } diff --git a/apps/web-tdesign/src/bootstrap.ts b/apps/web-tdesign/src/bootstrap.ts index ad7fef6..9b24a7b 100644 --- a/apps/web-tdesign/src/bootstrap.ts +++ b/apps/web-tdesign/src/bootstrap.ts @@ -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表示不注册这个指令 diff --git a/apps/web-tdesign/src/router/access.ts b/apps/web-tdesign/src/router/access.ts index 08cb4b0..973332c 100644 --- a/apps/web-tdesign/src/router/access.ts +++ b/apps/web-tdesign/src/router/access.ts @@ -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 = { diff --git a/apps/web-tdesign/src/views/_core/authentication/login.vue b/apps/web-tdesign/src/views/_core/authentication/login.vue index 89af0c2..62d04ae 100644 --- a/apps/web-tdesign/src/views/_core/authentication/login.vue +++ b/apps/web-tdesign/src/views/_core/authentication/login.vue @@ -1,6 +1,5 @@ + + diff --git a/apps/web-tdesign/src/views/house/dashboard/index.vue b/apps/web-tdesign/src/views/house/dashboard/index.vue new file mode 100644 index 0000000..bed114c --- /dev/null +++ b/apps/web-tdesign/src/views/house/dashboard/index.vue @@ -0,0 +1,132 @@ + + + diff --git a/apps/web-tdesign/src/views/house/data/index.vue b/apps/web-tdesign/src/views/house/data/index.vue new file mode 100644 index 0000000..9666fd8 --- /dev/null +++ b/apps/web-tdesign/src/views/house/data/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/web-tdesign/src/views/house/listing/index.vue b/apps/web-tdesign/src/views/house/listing/index.vue new file mode 100644 index 0000000..fc352d5 --- /dev/null +++ b/apps/web-tdesign/src/views/house/listing/index.vue @@ -0,0 +1,291 @@ + + + diff --git a/apps/web-tdesign/src/views/system/admin/index.vue b/apps/web-tdesign/src/views/system/admin/index.vue index e9fdab7..28b8d74 100644 --- a/apps/web-tdesign/src/views/system/admin/index.vue +++ b/apps/web-tdesign/src/views/system/admin/index.vue @@ -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(() => { 查询 - 新增管理员 + 新增人员 @@ -179,7 +179,7 @@ onMounted(() => {