cloud 2 viikkoa sitten
sitoutus
fdf062c5f2
100 muutettua tiedostoa jossa 9409 lisäystä ja 0 poistoa
  1. 13 0
      AGENTS.md
  2. 1301 0
      AI_RULES.md
  3. 21 0
      LICENSE
  4. 583 0
      PROJECT_CONTEXT.md
  5. 99 0
      README.md
  6. 22 0
      saiadmin-artd/.env
  7. 13 0
      saiadmin-artd/.env.development
  8. 10 0
      saiadmin-artd/.env.production
  9. 2 0
      saiadmin-artd/.gitattributes
  10. 11 0
      saiadmin-artd/.gitignore
  11. 1 0
      saiadmin-artd/.husky/commit-msg
  12. 1 0
      saiadmin-artd/.husky/pre-commit
  13. 3 0
      saiadmin-artd/.prettierignore
  14. 20 0
      saiadmin-artd/.prettierrc
  15. 9 0
      saiadmin-artd/.stylelintignore
  16. 82 0
      saiadmin-artd/.stylelintrc.cjs
  17. 10 0
      saiadmin-artd/.vscode/extensions.json
  18. 18 0
      saiadmin-artd/.vscode/settings.json
  19. 21 0
      saiadmin-artd/LICENSE
  20. 330 0
      saiadmin-artd/PROJECT_CONTEXT.md
  21. 31 0
      saiadmin-artd/SAIADMIN.md
  22. 552 0
      saiadmin-artd/alerts-build.log
  23. 97 0
      saiadmin-artd/commitlint.config.cjs
  24. BIN
      saiadmin-artd/content-build.log
  25. 83 0
      saiadmin-artd/eslint.config.mjs
  26. BIN
      saiadmin-artd/finance-build.log
  27. 47 0
      saiadmin-artd/index.html
  28. 552 0
      saiadmin-artd/menu-build.log
  29. BIN
      saiadmin-artd/package-delete-build.log
  30. 126 0
      saiadmin-artd/package.json
  31. BIN
      saiadmin-artd/public/favicon.ico
  32. 838 0
      saiadmin-artd/scripts/clean-dev.ts
  33. 185 0
      saiadmin-artd/scripts/test-water-driving.mjs
  34. 134 0
      saiadmin-artd/scripts/test-water-package-delete.mjs
  35. 47 0
      saiadmin-artd/scripts/test-water-region.mjs
  36. 34 0
      saiadmin-artd/src/App.vue
  37. 213 0
      saiadmin-artd/src/api/agent/agent.ts
  38. 209 0
      saiadmin-artd/src/api/auth.ts
  39. 46 0
      saiadmin-artd/src/api/common.ts
  40. 31 0
      saiadmin-artd/src/api/dashboard.ts
  41. 154 0
      saiadmin-artd/src/api/device/device.ts
  42. 49 0
      saiadmin-artd/src/api/device/distribution-record.ts
  43. 75 0
      saiadmin-artd/src/api/fundManagement/depositRefund.ts
  44. 71 0
      saiadmin-artd/src/api/fundManagement/memberActivationRecord.ts
  45. 71 0
      saiadmin-artd/src/api/fundManagement/rechargeRecord.ts
  46. 265 0
      saiadmin-artd/src/api/merchant/merchant.ts
  47. 59 0
      saiadmin-artd/src/api/miniProgram/alipay.ts
  48. 27 0
      saiadmin-artd/src/api/miniProgram/defaultSettings.ts
  49. 57 0
      saiadmin-artd/src/api/miniProgram/helpCenter.ts
  50. 43 0
      saiadmin-artd/src/api/miniProgram/officialAccount.ts
  51. 29 0
      saiadmin-artd/src/api/miniProgram/platformInfo.ts
  52. 32 0
      saiadmin-artd/src/api/miniProgram/relatedLinks.ts
  53. 65 0
      saiadmin-artd/src/api/miniProgram/settings.ts
  54. 23 0
      saiadmin-artd/src/api/miniProgram/shareSettings.ts
  55. 27 0
      saiadmin-artd/src/api/miniProgram/templateMessage.ts
  56. 61 0
      saiadmin-artd/src/api/miniProgram/version.ts
  57. 66 0
      saiadmin-artd/src/api/miniProgram/wechat.ts
  58. 74 0
      saiadmin-artd/src/api/operationManagement/deviceOperation.ts
  59. 46 0
      saiadmin-artd/src/api/operationManagement/merchantAlert.ts
  60. 100 0
      saiadmin-artd/src/api/order/myOrder.ts
  61. 22 0
      saiadmin-artd/src/api/order/subordinateOrder.ts
  62. 54 0
      saiadmin-artd/src/api/safeguard/attachment.ts
  63. 65 0
      saiadmin-artd/src/api/safeguard/category.ts
  64. 95 0
      saiadmin-artd/src/api/safeguard/database.ts
  65. 102 0
      saiadmin-artd/src/api/safeguard/dict.ts
  66. 30 0
      saiadmin-artd/src/api/safeguard/emailLog.ts
  67. 30 0
      saiadmin-artd/src/api/safeguard/loginLog.ts
  68. 30 0
      saiadmin-artd/src/api/safeguard/operLog.ts
  69. 42 0
      saiadmin-artd/src/api/safeguard/server.ts
  70. 126 0
      saiadmin-artd/src/api/system/config.ts
  71. 75 0
      saiadmin-artd/src/api/system/dept.ts
  72. 76 0
      saiadmin-artd/src/api/system/menu.ts
  73. 75 0
      saiadmin-artd/src/api/system/post.ts
  74. 123 0
      saiadmin-artd/src/api/system/role.ts
  75. 101 0
      saiadmin-artd/src/api/system/user.ts
  76. 101 0
      saiadmin-artd/src/api/tool/crontab.ts
  77. 126 0
      saiadmin-artd/src/api/tool/generate.ts
  78. 124 0
      saiadmin-artd/src/api/userManagement/rentalUser.ts
  79. 102 0
      saiadmin-artd/src/api/userManagement/userComplaint.ts
  80. 110 0
      saiadmin-artd/src/api/userManagement/userFeedback.ts
  81. 62 0
      saiadmin-artd/src/api/water/agents.ts
  82. 90 0
      saiadmin-artd/src/api/water/alerts.ts
  83. 76 0
      saiadmin-artd/src/api/water/content.ts
  84. 65 0
      saiadmin-artd/src/api/water/devices.ts
  85. 93 0
      saiadmin-artd/src/api/water/enterprises.ts
  86. 71 0
      saiadmin-artd/src/api/water/finance.ts
  87. 51 0
      saiadmin-artd/src/api/water/index.ts
  88. 57 0
      saiadmin-artd/src/api/water/miniIdentity.ts
  89. 46 0
      saiadmin-artd/src/api/water/promotion.ts
  90. BIN
      saiadmin-artd/src/assets/images/avatar/avatar.webp
  91. BIN
      saiadmin-artd/src/assets/images/avatar/avatar1.webp
  92. BIN
      saiadmin-artd/src/assets/images/avatar/avatar10.webp
  93. BIN
      saiadmin-artd/src/assets/images/avatar/avatar2.webp
  94. BIN
      saiadmin-artd/src/assets/images/avatar/avatar3.webp
  95. BIN
      saiadmin-artd/src/assets/images/avatar/avatar4.webp
  96. BIN
      saiadmin-artd/src/assets/images/avatar/avatar5.webp
  97. BIN
      saiadmin-artd/src/assets/images/avatar/avatar6.webp
  98. BIN
      saiadmin-artd/src/assets/images/avatar/avatar7.webp
  99. BIN
      saiadmin-artd/src/assets/images/avatar/avatar8.webp
  100. BIN
      saiadmin-artd/src/assets/images/avatar/avatar9.webp

+ 13 - 0
AGENTS.md

@@ -0,0 +1,13 @@
+# 项目开发约定
+
+以下是用户明确、反复要求的代码标准,后续开发必须持续遵守。
+
+- 优先保证代码简洁、可读、可维护。使用有意义的变量名;不把多条语句挤在一行。PHP 按现有格式化配置整理后交付。
+- 控制器只处理 HTTP 输入和输出、调用服务,不写业务规则和数据库查询。
+- 服务层组织业务、权限、状态、事务;所有数据读写使用 Eloquent 模型及模型关联。不得以 Db::table、裸 SQL、PDO 绕过模型(数据库迁移的 DDL 除外)。
+- 公共逻辑集中复用,尤其是鉴权、校验、分页、响应、事务、幂等、审计和公共返回字段。
+- 在实际变化点使用设计模式:外部登录采用适配器接口;司机动作采用命令接口与简单工厂;共用状态前置条件采用模板方法。不要以模式名称为由引入无业务作用的 Repository 或多层转发。
+- 新动作扩展独立处理器,避免把控制器或服务不断堆成大型条件分支。
+- Webman 进程常驻,不在可复用控制器的属性或静态变量中保存本次请求的用户、身份或业务资源。
+- 用户于2026-09-19明确开始建设 uni-app 小程序前端,独立目录为 `water-mini`(Vue 3 + TypeScript,微信小程序及 H5 预览)。UI 来源为 `D:\水后台UI\mobile`;前端运行说明见 `water-mini/README.md`,接口边界、业务差异、配置和测试命令见 `server/docs/WATER_MINI_API.md`。
+- 不覆盖其他未提交的改动。业务回归使用可回滚数据;未配置的真实支付、佣金、硬件功能不得伪造成功。

+ 1301 - 0
AI_RULES.md

@@ -0,0 +1,1301 @@
+# 93. AI 执行模式
+
+本项目采用“**任务分级 + 风险分级 + 自动选择执行模式**”的 AI 开发机制。
+
+AI 不得对所有任务使用相同的处理方式。
+
+必须根据任务类型、影响范围和风险等级,选择对应执行模式。
+
+---
+
+# 94. 执行模式总览
+
+本项目定义以下 7 种 AI 执行模式:
+
+| 模式     | 名称   | 适用场景        | 是否直接修改 |
+| ------ | ---- | ----------- | ------ |
+| MODE-1 | 快速修复 | 简单 Bug、明显错误 | 可以     |
+| MODE-2 | 标准开发 | 普通新功能       | 可以     |
+| MODE-3 | 深度开发 | 复杂业务、跨模块功能  | 先分析后修改 |
+| MODE-4 | 安全模式 | 支付、权限、认证、安全 | 默认先确认  |
+| MODE-5 | 数据模式 | 数据库、迁移、批量数据 | 高风险需确认 |
+| MODE-6 | 重构模式 | 大规模重构、架构调整  | 先方案后执行 |
+| MODE-7 | 发布模式 | 构建、部署、生产发布  | 默认需要确认 |
+
+AI 必须自动判断使用哪一种模式。
+
+---
+
+# 95. MODE-1:快速修复模式
+
+## 适用
+
+以下问题可以使用快速修复:
+
+* PHP 语法错误
+* 明确的变量错误
+* 明确的类型错误
+* 明显的拼写错误
+* 明确的前端 JS 错误
+* 明确的 API 参数错误
+* 已知函数调用错误
+* 明确的空值错误
+
+例如:
+
+```text
+Undefined variable
+Call to undefined method
+Class not found
+Syntax error
+TypeError
+```
+
+---
+
+## 执行流程
+
+```text
+定位错误
+ ↓
+读取相关代码
+ ↓
+确认根因
+ ↓
+最小修改
+ ↓
+测试
+ ↓
+报告结果
+```
+
+不需要为了简单 Bug 输出长篇方案。
+
+---
+
+## 限制
+
+快速修复模式不得:
+
+* 修改数据库结构
+* 修改支付
+* 修改权限
+* 修改认证
+* 升级依赖
+* 大规模重构
+* 修改生产环境
+
+如果发现涉及高风险内容:
+
+**自动切换到对应高风险模式。**
+
+---
+
+# 96. MODE-2:标准开发模式
+
+## 适用
+
+普通功能开发:
+
+* 新增 API
+* 新增页面
+* 新增 CRUD
+* 新增业务 Service
+* 新增 Vue 页面
+* 新增 Uni-app 页面
+* 新增后台管理功能
+* 新增普通第三方接口
+
+---
+
+## 执行流程
+
+```text
+需求分析
+ ↓
+搜索现有实现
+ ↓
+确定修改文件
+ ↓
+设计数据流
+ ↓
+后端开发
+ ↓
+前端开发
+ ↓
+接口联调
+ ↓
+测试
+ ↓
+代码审查
+```
+
+---
+
+## 默认行为
+
+如果风险较低:
+
+**AI 可以直接执行。**
+
+但必须:
+
+* 遵守 AI_RULES.md
+* 遵守 PROJECT_RULES.md
+* 尽量最小化修改
+* 测试相关功能
+* 汇报修改结果
+
+---
+
+# 97. MODE-3:深度开发模式
+
+## 适用
+
+以下情况必须进入深度开发模式:
+
+* 跨多个模块
+* 涉及前后端
+* 涉及多个数据库表
+* 涉及 Redis
+* 涉及队列
+* 涉及 MQTT
+* 涉及 WebSocket
+* 涉及复杂业务状态
+* 涉及订单
+* 涉及库存
+* 涉及用户权限
+* 涉及多个第三方 API
+
+---
+
+## 执行流程
+
+```text
+1. 项目扫描
+        ↓
+2. 业务链路分析
+        ↓
+3. 数据流分析
+        ↓
+4. 依赖分析
+        ↓
+5. 风险分析
+        ↓
+6. 制定实施方案
+        ↓
+7. 修改代码
+        ↓
+8. 测试
+        ↓
+9. 回归测试
+        ↓
+10. 输出变更报告
+```
+
+---
+
+## 必须分析
+
+至少说明:
+
+```text
+入口
+ ↓
+Controller
+ ↓
+Service
+ ↓
+Model
+ ↓
+Database
+ ↓
+Redis / Queue
+ ↓
+第三方服务
+ ↓
+前端
+```
+
+如果某一层不存在:
+
+按照实际项目结构处理。
+
+---
+
+# 98. MODE-4:安全模式
+
+以下功能必须进入安全模式:
+
+* 登录
+* 注册
+* Token
+* Session
+* 权限
+* RBAC
+* 支付
+* 钱包
+* 余额
+* 退款
+* 优惠券
+* 文件上传
+* 管理员
+* API Key
+* Webhook
+* OAuth
+* 第三方登录
+* 加密
+* 签名
+* 敏感数据
+
+---
+
+## 安全模式原则
+
+默认:
+
+**先分析,后修改。**
+
+必须检查:
+
+```text
+身份认证
+权限
+输入验证
+数据权限
+SQL
+XSS
+CSRF
+SSRF
+文件安全
+敏感信息
+日志
+接口重放
+幂等
+并发
+```
+
+---
+
+## 支付相关
+
+涉及支付时必须额外检查:
+
+```text
+订单号
+金额
+签名
+回调
+订单状态
+重复回调
+退款
+并发
+幂等
+```
+
+禁止为了修复支付问题而降低安全等级。
+
+---
+
+## 安全模式确认点
+
+如果修改可能导致:
+
+* 用户权限变化
+* 支付金额变化
+* 账户余额变化
+* 身份认证变化
+* 敏感数据暴露
+
+必须在修改前明确说明风险。
+
+---
+
+# 99. MODE-5:数据库模式
+
+## 适用
+
+以下操作进入数据库模式:
+
+* 新增表
+* 删除表
+* 修改字段
+* 修改索引
+* 修改数据类型
+* 数据迁移
+* 批量更新
+* 批量删除
+* 数据清理
+* 数据修复
+
+---
+
+## 数据库模式原则
+
+先:
+
+```text
+读取表结构
+ ↓
+检查数据量
+ ↓
+检查索引
+ ↓
+检查依赖
+ ↓
+设计迁移
+ ↓
+考虑回滚
+```
+
+---
+
+## 高风险操作
+
+以下操作默认禁止直接执行:
+
+```sql
+DROP DATABASE
+DROP TABLE
+TRUNCATE
+DELETE FROM table
+UPDATE table SET ...
+```
+
+尤其是生产环境。
+
+必须:
+
+1. 明确目标。
+2. 明确影响范围。
+3. 确认备份。
+4. 尽可能使用条件限制。
+5. 先 SELECT 验证目标数据。
+6. 再执行修改。
+
+---
+
+## 批量数据修改
+
+推荐:
+
+```text
+SELECT
+ ↓
+确认数据数量
+ ↓
+备份
+ ↓
+分批处理
+ ↓
+验证
+```
+
+不要直接对百万级数据执行没有评估的 UPDATE。
+
+---
+
+# 100. MODE-6:重构模式
+
+## 适用
+
+* 大规模重构
+* Controller 拆 Service
+* Service 拆分
+* 数据库重构
+* API 重构
+* 前端架构调整
+* Vue 组件重构
+* 目录结构重构
+* Laravel / ThinkPHP 架构调整
+* 技术栈升级
+
+---
+
+## 重构原则
+
+核心原则:
+
+> **行为优先于代码漂亮。**
+
+默认:
+
+```text
+旧功能正常
+ ↓
+增加测试
+ ↓
+逐步重构
+ ↓
+保持兼容
+ ↓
+验证
+ ↓
+删除旧代码
+```
+
+---
+
+## 禁止一次性大改
+
+不要一次修改:
+
+```text
+100+ 文件
+```
+
+而不建立阶段性验证。
+
+推荐:
+
+```text
+阶段 1
+ ↓
+测试
+ ↓
+阶段 2
+ ↓
+测试
+ ↓
+阶段 3
+ ↓
+测试
+```
+
+---
+
+## API 重构
+
+优先:
+
+```text
+旧 API
+ +
+新 API
+```
+
+而不是:
+
+```text
+直接删除旧 API
+```
+
+除非明确确认不再兼容。
+
+---
+
+# 101. MODE-7:发布模式
+
+## 适用
+
+* Git 发布
+* 构建
+* 生产部署
+* 数据库迁移
+* 服务重启
+* Supervisor
+* Docker
+* Nginx
+* PHP-FPM
+* Webman Worker
+* Queue
+* Cron
+* SSL
+* DNS
+
+---
+
+## 发布流程
+
+```text
+代码检查
+ ↓
+git status
+ ↓
+git diff
+ ↓
+测试
+ ↓
+构建
+ ↓
+确认环境
+ ↓
+备份
+ ↓
+部署
+ ↓
+数据库迁移
+ ↓
+清理缓存
+ ↓
+重启必要服务
+ ↓
+健康检查
+ ↓
+日志检查
+```
+
+---
+
+## 生产发布
+
+默认:
+
+**不允许 AI 自行进行不可逆的生产操作。**
+
+例如:
+
+```text
+DROP
+TRUNCATE
+DELETE
+force push
+reset --hard
+清空 Redis
+删除生产文件
+停止核心服务
+```
+
+除非用户明确授权。
+
+---
+
+# 102. 自动模式选择
+
+AI 接收到任务后必须自动判断:
+
+```text
+如果只是明显 Bug
+→ MODE-1
+
+如果是普通功能
+→ MODE-2
+
+如果跨多个模块
+→ MODE-3
+
+如果涉及安全 / 权限 / 支付
+→ MODE-4
+
+如果涉及数据库结构或批量数据
+→ MODE-5
+
+如果涉及大规模重构
+→ MODE-6
+
+如果涉及生产部署
+→ MODE-7
+```
+
+如果一个任务同时符合多个模式:
+
+**选择风险最高的模式。**
+
+例如:
+
+```text
+新增支付接口
+```
+
+同时属于:
+
+```text
+MODE-2
+MODE-4
+```
+
+最终使用:
+
+```text
+MODE-4
+```
+
+---
+
+# 103. 模式可以组合
+
+复杂任务允许组合模式:
+
+```text
+MODE-3 + MODE-4 + MODE-5
+```
+
+例如:
+
+```text
+新增订单支付系统
+```
+
+需要:
+
+```text
+深度开发
++
+安全模式
++
+数据库模式
+```
+
+---
+
+# 104. 风险等级
+
+所有任务分为:
+
+```text
+L0:无风险
+L1:低风险
+L2:中风险
+L3:高风险
+L4:生产高风险
+```
+
+---
+
+## L0
+
+例如:
+
+```text
+修改注释
+修改文档
+格式化单个文件
+```
+
+AI 可以直接执行。
+
+---
+
+## L1
+
+例如:
+
+```text
+简单 Bug
+普通 UI
+普通 API
+```
+
+AI 可以直接执行。
+
+---
+
+## L2
+
+例如:
+
+```text
+新增业务
+新增数据库字段
+Redis
+队列
+第三方 API
+```
+
+AI 可以执行,但必须测试并报告影响。
+
+---
+
+## L3
+
+例如:
+
+```text
+支付
+权限
+认证
+余额
+库存
+数据库批量修改
+大规模重构
+```
+
+必须先进行风险分析。
+
+---
+
+## L4
+
+例如:
+
+```text
+生产数据库
+生产支付
+生产服务器
+生产 Redis
+生产部署
+删除生产数据
+```
+
+必须明确确认后才能执行高风险操作。
+
+---
+
+# 105. AI 修改前的“变更计划”
+
+当任务达到 L2 及以上时,AI 应先生成:
+
+```text
+## Change Plan
+
+### 目标
+
+xxx
+
+### 当前实现
+
+xxx
+
+### 问题
+
+xxx
+
+### 修改文件
+
+1. xxx
+2. xxx
+3. xxx
+
+### 数据库
+
+无 / 新增字段 / 新增表 / 修改数据
+
+### API
+
+无 / 新增 / 修改
+
+### 前端
+
+无 / 修改页面 / 新增组件
+
+### Redis
+
+无 / 新增 Key / 修改缓存
+
+### 风险
+
+低 / 中 / 高
+
+### 测试
+
+xxx
+
+### 回滚
+
+xxx
+```
+
+---
+
+# 106. AI 的“只读分析模式”
+
+如果用户说:
+
+```text
+帮我看看
+分析一下
+检查一下
+为什么
+排查
+审计
+有没有问题
+```
+
+默认进入:
+
+```text
+READ-ONLY MODE
+```
+
+此模式下:
+
+**只分析,不修改代码。**
+
+除非用户明确要求修改。
+
+---
+
+# 107. AI 的“计划模式”
+
+如果用户说:
+
+```text
+设计一下
+怎么实现
+给我方案
+架构怎么做
+```
+
+进入:
+
+```text
+PLAN MODE
+```
+
+输出:
+
+```text
+需求分析
+ ↓
+架构方案
+ ↓
+文件变更计划
+ ↓
+数据库方案
+ ↓
+API 方案
+ ↓
+前端方案
+ ↓
+测试方案
+ ↓
+部署方案
+```
+
+不直接修改代码。
+
+---
+
+# 108. AI 的“实现模式”
+
+如果用户说:
+
+```text
+帮我开发
+实现
+写代码
+修复
+修改
+增加
+删除
+```
+
+进入:
+
+```text
+IMPLEMENT MODE
+```
+
+根据风险等级自动选择 MODE-1 ~ MODE-7。
+
+---
+
+# 109. AI 的“审查模式”
+
+如果用户说:
+
+```text
+帮我 Code Review
+检查代码
+安全审计
+检查有没有问题
+```
+
+进入:
+
+```text
+REVIEW MODE
+```
+
+只检查:
+
+```text
+Bug
+安全
+性能
+架构
+代码质量
+兼容性
+```
+
+默认不修改。
+
+输出:
+
+```text
+严重程度:
+
+CRITICAL
+HIGH
+MEDIUM
+LOW
+INFO
+```
+
+注意:
+
+**审查等级不是代码质量评分。**
+
+不得使用:
+
+```text
+9/10
+A+
+优秀
+差
+```
+
+等无实际依据的评分。
+
+---
+
+# 110. AI 的“测试模式”
+
+如果用户要求:
+
+```text
+测试
+验证
+跑一下
+检查是否正常
+```
+
+进入:
+
+```text
+TEST MODE
+```
+
+优先执行:
+
+```text
+语法检查
+ ↓
+单元测试
+ ↓
+集成测试
+ ↓
+API 测试
+ ↓
+前端构建
+ ↓
+关键业务测试
+```
+
+没有执行的测试:
+
+必须明确标记:
+
+```text
+NOT RUN
+```
+
+不能声称通过。
+
+---
+
+# 111. AI 的“部署模式”
+
+用户要求:
+
+```text
+部署
+上线
+发布
+更新服务器
+```
+
+进入:
+
+```text
+DEPLOY MODE
+```
+
+必须先检查:
+
+```text
+当前分支
+当前 Commit
+工作区
+构建状态
+环境
+数据库迁移
+配置
+备份
+```
+
+生产环境操作必须特别谨慎。
+
+---
+
+# 112. 用户没有明确要求时的默认行为
+
+默认规则:
+
+```text
+查看
+→ 只读
+
+分析
+→ 只读
+
+设计
+→ Plan
+
+开发
+→ Implement
+
+修 Bug
+→ 根据风险自动选择
+
+部署
+→ Deploy
+
+审查
+→ Review
+```
+
+---
+
+# 113. AI 的暂停条件
+
+遇到以下情况必须暂停并询问:
+
+## 需求冲突
+
+例如:
+
+```text
+用户要求保持兼容
+但又要求删除旧 API
+```
+
+---
+
+## 数据风险
+
+例如:
+
+```text
+需要删除大量数据
+但没有明确条件
+```
+
+---
+
+## 生产风险
+
+例如:
+
+```text
+需要修改生产数据库
+但没有备份信息
+```
+
+---
+
+## 安全风险
+
+例如:
+
+```text
+需要关闭权限验证
+需要关闭 SSL 验证
+需要暴露 API Key
+```
+
+不得为了完成需求而执行。
+
+---
+
+## 架构不确定
+
+例如:
+
+```text
+不知道项目使用 Laravel 还是 ThinkPHP
+不知道数据库表结构
+不知道接口规范
+```
+
+先读取项目,而不是猜。
+
+---
+
+# 114. AI 的自动降级原则
+
+如果发现:
+
+```text
+当前任务风险高于最初判断
+```
+
+必须自动切换到更高风险模式。
+
+例如:
+
+```text
+原任务:
+修复一个 API Bug
+
+发现:
+涉及支付金额
+
+自动切换:
+MODE-4 安全模式
+```
+
+---
+
+# 115. AI 的自动升级原则
+
+如果发现:
+
+```text
+简单修改
+```
+
+但实际涉及:
+
+```text
+数据库
+支付
+权限
+生产环境
+```
+
+必须自动升级模式。
+
+不能继续按照普通 Bug 处理。
+
+---
+
+# 116. AI 的最终执行报告
+
+任务完成后必须报告:
+
+```text
+## Execution Report
+
+执行模式:
+MODE-X
+
+风险等级:
+L0 / L1 / L2 / L3 / L4
+
+### 修改文件
+
+- xxx
+- xxx
+
+### 修改内容
+
+xxx
+
+### 数据库
+
+无修改
+
+或者:
+
+新增:
+xxx
+
+### API
+
+无变化
+
+或者:
+
+新增:
+xxx
+
+### Redis
+
+无变化
+
+或者:
+
+新增:
+xxx
+
+### 测试
+
+执行:
+
+xxx
+
+结果:
+
+PASS / FAIL / NOT RUN
+
+### 部署
+
+未部署
+
+或者:
+
+已部署到测试环境
+
+### 风险
+
+xxx
+
+### 回滚
+
+xxx
+```
+
+---
+
+# 117. AI 执行优先级
+
+当多个规则发生冲突时,优先级:
+
+```text
+安全
+ >
+用户明确要求
+ >
+项目规范
+ >
+现有业务兼容性
+ >
+框架规范
+ >
+通用最佳实践
+ >
+AI 自己的偏好
+```
+
+AI 自己的“代码风格偏好”优先级最低。
+
+---
+
+# 118. 最终执行哲学
+
+本项目不追求:
+
+```text
+AI 写得最多
+```
+
+也不追求:
+
+```text
+AI 改得最快
+```
+
+而追求:
+
+```text
+正确
++
+安全
++
+可维护
++
+可验证
++
+可回滚
+```
+
+最终原则:
+
+> **简单问题快速解决,复杂问题先分析,高风险问题先确认,生产问题可回滚。**
+
+> **AI 可以主动发现问题,但不能擅自扩大修改范围。**
+
+> **AI 可以提出更好的方案,但不能未经授权改变核心业务规则。**
+
+> **AI 可以自动修复低风险问题,但涉及数据、权限、支付和生产环境时必须提高执行等级。**

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 saithink
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 583 - 0
PROJECT_CONTEXT.md

@@ -0,0 +1,583 @@
+# Project Context
+
+## 2026-10-19 企业邀请推广赠水第一版
+- 用户选择先实现推广奖励;按企业邀请企业、首购后签收、财务人工核实后赠水权益实现。奖励桶数/规格可配置,默认关闭,无现金发放、无自动送水。
+- 新增 `server/plugin/saiadmin/app/service/promotion/`,三张 `sa_water_promotion_*` 表、本机已执行可复跑的增量迁移;没有回填历史奖励。平台规则在绑定时快照;禁止自邀、共用管理员及循环绑定,首购前绑定一次。购水共用入口 `WaterPurchaseReceipt` 生成待审核奖励,组合结算按整笔金额判断,首购前已有配送不能领奖。审核发放与权益、流水同事务,重复请求不重复发奖。
+- 后台入口:财务结算中心 → 推广赠水奖励,沿用 finance:read/write;小程序“我的 → 奖励与推广”支持邀请码、复制/微信页面分享、显式绑定和真实分页记录,移除旧测试奖励作为实际返回。个人中心显示累计赠水桶数,权益明细增加推广赠水;小程序不披露客户付款凭据、金额或内部审核说明。
+- OpenAPI 69个Schema,51个操作(含HEAD);文档及类型已同步。使用、迁移、API、约束和回退见 `server/docs/WATER_PROMOTION.md`;本轮执行结果见 `server/docs/WATER_PROMOTION_RESULTS.md`。
+- 验证:9组后端共1445项通过(新增推广56);小程序25项通过(新增页面4),两端类型检查、PHP语法/格式、前端格式/ESLint/Stylelint及后台/H5/微信构建通过。业务夹具全部回滚。后台4173及H5预览已更新,HTML及入口资源哈希与构建一致;实际本机登录读取rewards得到water/initialized=true/enabled=false/demo=false/0桶,随后退出测试会话。无真实奖励发放、未改活动启用状态、未上传微信或发布生产。
+- 已有 `water_mini_api_test.php:331`、`mini_identity_management_test.php:133` 仍假设同一账号能同时绑定多个业务身份,实际 IdentityManagement.php:72 已禁止,两个旧测试失败;本轮未修改认证规则或掩盖失败。浏览器点击/截图、微信真机分享和完整端到端验收未执行。
+- 正式退款/已使用赠水扣回未实现;首购后的对应规格签收不能证明组合订单所有权益已全部配送,必须人工核实后审核。关闭活动停止新绑定,已有承诺按快照审核;不能通过删除表或回滚代码撤销已生效权益。
+
+## 2026-10-09 后台 P0/P1 待补做收尾
+- 完成四项真待补做,删残留 TODO、清理误导性 catch、实装注册端点、实装会员开通/续费。
+- **T1 删 TODO**:`views/system/role/modules/permission-dialog.vue:141` 的 `// TODO: 调用保存权限接口` 已删除。后端 `SystemRoleController::menuPermission` 早已实现,仅拖调一次 `api.menuPermission` 的注释残留。
+- **T2 整批清理 catch**:17 个 dialog 文件中的 `console.log('表单验证失败:', error)` 改为留空 catch + 注释(说明 ElForm 自带校验提示 + axios 拦截器自动 `ElMessage.error`);`register/index.vue` catch 保留但改文案;`login/index.vue` 注释化的 `console.log(error.code)` 删除。本轮未触动含 `loading.value = false` 的重置代码。
+- **T3 注册 API**:`POST /core/register` 新增,免登录;`RegisterController` 继承 `OpenController`,`RegisterValidate` 含 username(3-20 字母数字唯一)/password(6-16)/confirmPassword(确认);密码走 `password_hash($password, PASSWORD_DEFAULT)`,默认 `is_super=0 status=1 dashboard=work`,不分配任何角色。前端 `api/auth.ts` 新增 `fetchRegister`,`register/index.vue` 替换 `setTimeout` 模拟为真实调用 + catch 留 `loading.value = false` finally。
+- **T4 会员开通/续费**:`POST /core/user-management/users/member-activation`,权限 `core:user:list:memberActivation`;`MemberActivationValidate` 校验 card_type∈{0,1,2,3}、pay_amount≥0、expire_time>now;`RentalUserLogic::activateMember` 事务内 INSERT `sa_member_activation_record`(source=3,manage_agent_id=operator) + UPDATE `sa_rental_user.member_expire_at/member_card_type`;复用 `accessibleManagerIds()` + `assertUserAccessible()` 鉴权。SQL `20261009_add_rental_user_member_fields.sql` 加字段 + 从历史 activation 记录回填最新有效到期(仅 status=1 且未过期);`20261009_add_member_activation_button_permission.sql` 在 UserList 菜单下挂按钮并自动赋给有 UserList 权限的角色;`scripts/migrate_rental_user_member_fields.php` 等效合并两个 SQL 的可执行脚本,`--apply` 跑。前端 `api/userManagement/rentalUser.ts` 新增 `activateMember` + `MemberActivationPayload/Result` + `MEMBER_CARD_TYPE_OPTIONS` + `RentalUser` 加 `member_expire_at/member_card_type/member_card_type_name/member_active`;`user-list/index.vue` 新增「会员状态」列、`openMemberActivation` 改按钮 click、`MemberActivationDialog` 含卡类型/金额/到期/备注字段(默认到期 +30 天,过往日期禁用);表格「储值卡赠额」列后插入会员状态列。
+- **验证**:`php -l` 6 个后端文件全部通过;`vue-tsc --noEmit` exit=0 0 错误;ESLint 检查 23 个改动文件 0 错误(`--fix` 处理 6 处 prettier/格式问题);原 `showPendingAction` 占位函数已删除(无引用);注册端点 + 会员开通端点的冒烟脚本 `tests/register_and_member_activation_test.php` 已写好(本次未在 webman 容器中运行,留待下次启动后跑)。
+- **未做**:退款/撤销会员流程(等下一期);多种会员等级(沿用 card_type 0-3);自动续费/到期提醒;`MemberActivationRecordLogic` 中 1-4 月/季/半年/年卡的常量与 `MemberActivationValidate::card_type` 0-3 一致(原系统 DEVICE_NAMES 已包含设备类型 100+ 的命名,与卡类型无关,无需调整)。
+
+## 2026-10-09 昨日一致性修复接续
+- 当前后台4173、小程序API及H5预览8787可访问;本机配置为企业余额支付/测试手机号登录开启,微信登录/真实微信支付关闭。正式企业余额充值退款、岗位权限和会计口径仍缺业务定义,不根据旧租赁钱包推断。
+- 后端订单快照、套餐删除、小程序业务、财务、预警、企业、调度、履约、契约和架构10组共1589项通过,业务夹具回滚。小程序21项测试、两端类型检查、后台道路/套餐删除脚本通过。
+- 后台和H5的入口HTML及2/3个入口JS/CSS资源SHA256与现有构建一致;本轮未重构建、发布、迁移、重启或修改业务数据。
+- 修正 `water-mini/README.md` 与子项目上下文中的余额未开放、可开票、按名称猜分类、Schema数量和旧验证状态。浏览器工具因 `windows sandbox failed: orchestrator_helper_exit_nonzero` 无法启动,点击/截图/真机验收仍未完成;详细记录见 `server/docs/ADMIN_MINI_REPAIR_RESULTS.md`。
+
+## 2026-10-08 后台与小程序一致性修复
+- CheckoutService 按持久化订单快照结算,WaterPurchaseReceipt 共用收款/权益逻辑;扣款、流水、权益和支付状态同事务。套餐修改/下架/删除不影响 30 分钟内的原订单,原 SKU 或企业停用仍拒绝支付。创建时锁全部套餐再锁 SKU;支付统一 SKU → 企业 → 余额/权益。quote_revision 校验价格、规格和桶数变化,新小程序提交,旧客户端可省略。
+- 财务列表/导出通过实际余额扣款流水与已支付 checkout 明细识别企业余额购水;小程序用户由 MiniAudit/MiniBalanceLedger 追踪,供水记录 created_by=0。未迁移、未自动修补历史差异。
+- 小程序每次进入套餐页刷新,购物车使用最新报价、失败禁止提交、金额保留分;去除无实现的独立卡/到期扣减/电子开票承诺和名称猜分类。多地址接口文档/地址类型已补齐。后台预警、权益、企业统计按企业+SKU+有效任务关联,兼容新旧 active_key。
+- waterAmap 同页请求共享 1100ms 间隔和最多 3 次 QPS 退避;定向成本缓存 60 秒、256 条。不是跨用户或跨服务的 Key 总量限流。H5 本地发布先复制资源再原子切换 HTML,保留旧哈希资产。
+- 已通过后端 1653 项、小程序 21 项、后台删除/道路脚本、类型/语法/格式与后台/H5/微信生产构建;后台 4173 和小程序 8787/water-mini/index.html 的入口 HTML 与资源哈希已核对为新构建。业务夹具回滚,未上传微信或验证真机点击。
+- 企业余额正式入账与退款、权限岗位细分、套餐分类/设备/有效期配置、财务会计口径仍待业务定义;不可把旧租赁钱包直接当供水企业余额。详细记录:server/docs/ADMIN_MINI_REPAIR_RESULTS.md。
+
+## 2026-09-19 uni-app 小程序第一版
+- 用户明确开始制作移动前端,覆盖此前“只做接口”范围;根 AGENTS.md 已更新。独立 `water-mini` 使用官方 uni-app Vue3模板对应的 DCloud 3.0.0-5020420260813003、Vue3.4.21、TypeScript,依赖锁定 package-lock.json。对照 `D:\水后台UI\mobile\M01–M18.png` 实现28个页面,复用组件/会话/请求/分页/命令服务,59个TS接口类型由既有OpenAPI生成。
+- 企业端:首页/真实套餐/组合报价/待支付意向及取消/地址版本编辑/权益流水与预约/配送跟踪/私有照片/设备用量;司机端:工作台、任务/路线、出发、GCJ02打卡、SN归属核对、拍照、草稿、签收及异常;代理端:客户筛选与真实收款统计。微信登录与身份选择共用现有独立Token,首次登录等管理员绑定。未实现的手机号登录、支付、佣金提现、推广、硬件绑定/GPS明确关闭,无模拟账户或伪造业务数据。
+- 登录状态与请求解耦;401/退出/身份切换清空私有页面数据,旧响应不能覆盖新身份。写请求先持久化用户+路径下的完整payload和request_key,双击锁定,网络/5xx重试沿用原请求;未知结果期间修改内容会拒绝并提供重试按钮。照片用带Token的下载请求,不在URL泄露令牌;上传压缩后前端限制约600KB以适应微信单键存储重试,签收选择1–4张照片;临时照片退出/页面销毁清理。
+- 已通过类型检查、12项关键逻辑测试(受控uni网络替身,不写业务数据库)、Prettier、微信与H5生产构建;28个微信WXML均存在,编译包约203KB。本机H5发布在 `server/public/water-mini`,地址 `http://127.0.0.1:8787/water-mini/index.html`;页面/主资源HTTP200,公开套餐接口实际返回3条。后端未修改、无迁移、无Redis变更、无服务重启或微信平台上传。
+- 运行/维护/边界见 `water-mini/README.md`。微信开发者工具导入 `water-mini/dist/build/mp-weixin`;实际AppID需填写 `src/manifest.json` 的 mp-weixin.appid,API域名由 `.env.local` 的 VITE_API_BASE_URL 配置,真机需HTTPS及合法域名。微信后端凭据仍未配置,真实登录/三端授权业务端到端、拍照/位置实机、浏览器点击验收未完成;浏览器工具仍报 unsupported Codex auth method: apikey。H5仅预览公开页面/登录提示,不提供模拟登录或手填Token入口。
+
+## 2026-09-19 小程序接口文档网页版
+- 新增 `server/public/docs/mini/index.html`,本机访问 `http://127.0.0.1:8787/docs/mini/index.html`。Swagger UI 5.33.0 静态资源来自官方 npm 包,下载后验证 SHA-512,保留 LICENSE/NOTICE 和来源记录;页面支持45个操作的分类、筛选、Schema、示例、Bearer授权和在线调试。手机号登录明确标注仍为设计,未新增对应业务接口。
+- `server/scripts/build_mini_docs.php` 将唯一维护源 `docs/openapi/water-mini-v1.json` 同步到公开目录,`--check` 检查一致性。页面强制使用当前来源的 `/api/mini/v1`,不持久化Token、不使用外部CDN或第三方校验器;配置状态实时读取 `/config`。
+- `MINI_ALLOWED_ORIGINS` 支持逗号分隔精确来源,默认仅本机8787的127.0.0.1/localhost;部署需覆盖为实际HTTPS来源。业务鉴权不变;接入文档、维护说明和.env.example同步更新。无数据库/Redis/业务接口变更,无手工重启或生产部署。
+- 验证:页面/脚本/样式/OpenAPI/Swagger资源HTTP200,框架含HTTP64项、接口契约770项(45操作)通过,PHP/JS语法与文档同步检查通过;实际带本机Origin的GET200、OPTIONS204、空登录POST42201,未登录会话40101,未知来源40301。浏览器工具持续报 unsupported Codex auth method: apikey,未做浏览器渲染/点击验收。PowerShell首次读取异常响应体为空导致断言失败,改用curl确认实际错误码正确。
+
+## 2026-09-18 小程序规则整改与前端接口文档
+- 用户明确授权按规则修改代码并给前端接口文档;按照 AI_RULES 的 IMPLEMENT / MODE-3+MODE-4、L3 执行前置方案和分阶段验证。AI_RULES 仅93–118节,项目内未找到其引用的 PROJECT_RULES.md;实际架构要求见根 AGENTS.md。
+- 地址修改:锁定企业后,WaterDelivery 模型 lockForUpdate 当前读检查 active_key,修复 REPEATABLE READ 授权快照漏掉并发后台预约的问题。新双连接测试在企业锁前交错提交真实预约,证实普通读仍看不到任务而新逻辑返回40902。
+- 新 PhoneNumber 公共格式策略经 Fields::phone/WaterPresenter 复用;接受7–15位国内/国际电话、去除格式符号,拒绝纯符号/掩码/非法格式;格式不明的历史号码统一****,不再原样暴露。
+- AgentQuery::customers 用现有 orders/devices 关联 withSum/withMax/withExists/withCount,消除每客户4次查询,保持返回字段及排除未来收款的口径;测试确认增加客户行数不会增加查询次数。
+- 前端交付:server/docs/WATER_MINI_FRONTEND.md(登录、身份、逐接口目录、字段语义、调用顺序、幂等、照片、错误码);server/docs/openapi/water-mini-v1.json(OpenAPI3.0.3,45个操作含2个HEAD,59个Schema)。维护说明 WATER_MINI_API.md 更新;变更验证记录 WATER_MINI_REVIEW_FIXES.md。
+- 实测:小程序业务161(含32组返回Schema)、地址双连接12、架构97、框架含HTTP64、真实HTTP112;原业务64/调度42/履约59/代理59/企业64通过。契约检查实际路由/鉴权/引用/示例全部通过;PHP lint57文件、格式化dry-run64文件0差异。OpenAPI未在Apifox界面实际导入;项目测试校验器只覆盖本项目使用的Schema子集。
+- 普通业务夹具回滚,HTTP临时身份清理;并发测试需提交独立企业/SKU库存/权益/授权夹具供第二连接读取,finally按ID清理。无迁移、无持久业务种子、未重启或部署。微信凭据未配置,真实外部登录未联调;支付/佣金/提现等尚未开放的能力保持关闭。
+
+## 2026-09-18 小程序 UI 对应基础接口与分层重构
+- 用户要求仅做接口,UI 依据 `D:\水后台UI\mobile\M01.png`–`M18.png`。用户反复强调简洁、可读、可维护、公共逻辑集中、设计模式、控制器/服务分层、模型查询;已写入根 `AGENTS.md`,后续持续遵守。
+- `server/app/miniapi` 由基础 health/session 扩展为系统、认证、套餐、企业、订单、司机、代理、照片独立控制器;公共 BaseController/Access/Input/Fields/Pagination/WaterPresenter/Commands 复用。控制器不查询数据库;业务读写使用 Eloquent 模型,复用 WaterBusiness/WaterFulfillment 的库存与权益事务。套餐和配送列表使用模型关联预加载。
+- 登录采用 IdentityProvider→WechatIdentityProvider 适配器;司机动作采用 TaskAction 命令接口+TaskActionFactory 简单工厂;InTransitAction 模板方法复用状态校验,SaveFulfillment 共用暂存/签收验证。服务不保存跨请求身份。96项架构约束检查防止控制器查库、服务依赖HTTP或绕过模型。
+- 微信 code 登录只创建无授权小程序用户;独立随机令牌只存摘要,每请求校验过期/撤销/停用及当前角色。超级管理员使用 `/core/water/mini-users`、`/core/water/mini-grants` 绑定/撤销企业管理员/成员、司机、代理身份,与后台 JWT/管理员ID隔离。单机多进程登录限流有文件锁,多机需共享限流。
+- 企业接口涵盖真实首页人群、启用套餐、服务器报价、待支付意向、地址版本校验修改、汇总权益/流水、预约/取消、配送跟踪、设备用量;司机涵盖任务/路线、出发、打卡、照片、暂存、实际签收、异常记录、已绑定SN核对;代理仅查询授权自身/有效下级的客户与真实收款统计。写操作统一事务、幂等、撤权复核及独立小程序审计。
+- 已执行 `server/scripts/migrate_water_mini_api.php --apply`:8张 sa_water_mini_* 表,不植入身份/支付/财务数据。照片JPEG重编码及时间/上报位置水印,专用私有表按任务归属读取。真实支付、分卡有效期、多站点、佣金提现、邀请奖励、实时GPS、硬件绑定/联网检测仍未实现,接口明确能力关闭;没有把UI样例比例写成业务规则。
+- 当前 `/api/mini/v1/config` 报告微信登录未配置;需补充 `.env` 的 MINI_WECHAT_APPID/SECRET 等,未做微信外部联调。原收款登记仍是实际权益入口,下单意向不产生权益。代理业绩按企业当前归属,不能当佣金账本。
+- 已通过 PHP 语法61文件、格式化检查、业务接口93项、架构96项、含HTTP基础64项、真实HTTP112项;旧业务64/调度42/履约59/代理59/内容72项回归通过。业务夹具事务回滚,HTTP临时无授权用户finally清理。无前端改动。
+- 接口说明、M01–M18映射、请求示例、模式扩展及限制见 `server/docs/WATER_MINI_API.md`;格式化配置 `.php-cs-fixer.mini.php`,本机工具 runtime/tools/php-cs-fixer.phar(3.88.2)。本次实际HTTP已加载新接口;Windows不要用重复启动windows.php替代重载。
+
+## 2026-09-17 菜单管理导航入口
+- 用户要求把菜单操作加到导航;按添加“菜单管理”入口实现。原系统菜单id8及core:menu:index/read/save/update/destroy早已存在,供水mergeWaterMenus原先隐藏全部旧系统导航,导致入口不可见。
+- router/modules/water.ts从服务端已授权菜单树提取/system/menu,保留原Menu路由名称、组件、可见标记与权限,放在角色与权限后、操作审计前,移除原树重复节点;其他旧菜单仍隐藏但保留路由。无授权不创建入口,不更改数据库菜单、角色授权或业务数据。
+- views/index/index.vue让/system/menu沿用供水外观。复用现有system/menu页面及CRUD接口,原新增/编辑/删除按钮权限不变;补货阈值策略现有权限仍是water:alerts:write(菜单名称“管理补货预警”)。
+- Prettier/ESLint/vue-tsc通过;导航运行验证覆盖排序、原始树不变、未授权不补发、无重复、无供水菜单回退;真实HTTP确认/core/system/menu返回唯一可见Menu页面,/core/menu/index包含全部既有菜单操作权限。浏览器仍有已知apikey认证阻塞,未做点击验收。
+- Vite3335模块生产构建通过,dist更新;4173首页和包含导航修改的主JS资源SHA256一致。
+
+## 2026-09-17 平台阈值策略编辑
+- 用户进一步明确保留“阈值策略”入口,用于编辑平台统一参数;不支持逐企业覆盖。已恢复AlertPolicyDialog:补货预警桶数alert_qty(默认5,严格小于)、每人每桶天数days_per_barrel(默认3)、紧急桶数urgent_qty(默认2,不高于)。紧急阈值须小于补货阈值,天数范围1–365,整数校验。
+- 已执行并幂等复跑 scripts/migrate_water_alert_setting.php --apply,新增InnoDB单行sa_water_alert_setting;默认5/3/2,保存不重置。AlertSettings命令服务以行锁+revision防止覆盖过期修改,策略与platform:alerts审计同事务。首次发现本机新表默认MyISAM,已转换为InnoDB并精确恢复测试参数,最终快照回滚验证通过。
+- GET alerts/policy要求alerts:read;POST alerts action=policy要求alerts:write,仅允许平台参数,旧id/threshold_qty等账户覆盖拒绝。保存后前端刷新列表、统计、规则和预计用水。SQL/逐行计算、企业补货状态、账户预测共用每次查询的策略快照;无跨请求缓存,后台保存后后续查询立即生效。
+- 公司人数employee_count沿用本轮已新增可空字段及企业编辑/详情。日均升数=规格升数×人数÷配置天数,预计天数=满桶×配置天数÷人数。未登记人数仅按桶数预警;多规格分别估算。旧账户threshold_qty/daily_liters不参与算法,不改写历史值。
+- 不按预计用量扣库存、不自动建单,现场桶数仍由盘点/换桶/签收维护。配送沿用原冻结、权限及批次回滚。当前默认29账户/28预警(24紧急、4普通),17已有任务,无业务数据变更。
+- 新平台策略43项+预警118项+业务64项+企业68项共293项通过(含HTTP);所有夹具及策略/业务快照回滚。PHP lint、前端Prettier/ESLint/Stylelint/vue-tsc通过。详细规则与迁移见server/docs/WATER_ALERTS.md。浏览器仍有已知apikey认证故障,未做点击/截图验收。
+- 生产构建成功(3335模块),dist已更新;4173首页与含“保存并生效”的策略资源SHA256一致。仅保留既有system/menu导入警告。
+
+## 2026-09-17 首页内容配置独立页面
+- `/water-content` 按新参考图实现左侧轮播列表/筛选/8条分页/全量上线数量与右侧手机预览;新建/编辑独立表单支持图片及活动链接、人群、区域、起止时间、排序、启停和草稿实时预览。顶部可模拟购水状态、管理员身份与区域,查看有效轮播和真实启用套餐,图片失败回退标题。
+- `WaterContentDashboard` 应用查询服务 + `content/ContentInput/ContentPolicy` 校验及状态策略,Eloquent访问原内容/套餐模型。新增GET content/dashboard与content/preview(content:read),写入复用POST content(content:write)和WaterBusiness事务/审计;原通用内容列表同用状态策略。
+- 状态按上海时间及精确秒包含边界;全量上线计数不受筛选分页影响。区域按完整名称匹配(留空/全国为全域);模拟管理员身份与购水状态可组合。实际移动端接口/登录人群推导尚未接入,不能把后台预览参数当成真实授权;代理权限仍由角色管理。
+- 无数据库迁移,无持久种子数据;当前内容库0条,显示空状态,不填截图样例。参考图已保存到 `server/docs/references/water-content-reference-20260917.png`,契约见 `server/docs/WATER_CONTENT.md`。
+- 内容75项(含HTTP)及原业务69项通过,所有夹具和审计回滚;相关PHP lint及前端Prettier/ESLint/Stylelint/vue-tsc通过。浏览器认证故障仍限制实际点击/截图验收。
+- Vite生产构建3331模块通过,dist更新;4173首页及内容页资源SHA256与本次构建一致,可访问 `http://127.0.0.1:4173/#/water-content`。
+
+## 2026-09-17 待审核提现测试数据
+- 用户要求增加测试申请,已执行并复跑 `server/scripts/migrate_water_withdrawal_demo.php --apply` 与 `seed_water_withdrawal_demo.php --apply --http`,固定批次 `TEST-WATER-WITHDRAWAL-20260917`,ID1–4。星澜商务5760元、青禾推广用户860元、云帆企业服务3280元、森屿商贸2460元,共4笔12360元,名称均带【提现测试】,日期固定09-17。
+- 新增独立 `sa_water_withdrawal_demo` 表,仅存展示夹具,不是真实提现账本;Eloquent `WaterWithdrawalDemo` + `finance/WithdrawalDemo` 返回看板独立 `withdrawal_demo` 字段。真实待提现金额仍null、capabilities.withdrawals仍false,审核/打款未启用;原收款、权益、库存、配送和代理快照不变,仅追加种子审计。
+- 前端待审核区域显示申请方、模拟渠道、金额、日期和待审核(测试);卡片显示明确标记的测试金额/笔数。按所选月份和all/demo范围显示,regular排除;流水关键词和分页不影响申请,不生成退款或资金流水。
+- PHP语法、首次写入/幂等复跑、范围/月界/时间/原数据校验、4组真实HTTP核对及98项财务回归通过;前端ESLint/Prettier/Stylelint/vue-tsc和Vite构建通过,dist更新。浏览器认证仍是既有限制。
+
+## 2026-09-17 财务结算页面接续完成
+- `/water-finance` 已从通用表格改为参考图布局:四卡、资金趋势/月汇总、未开通提现区、流水搜索/8条分页/详情/全量对账导出,支持月份与测试范围切换、错误重试和窄屏单列。
+- 完成 `src/api/water/finance.ts`、`finance/index.vue`、`ReceiptDialog.vue`、`finance.scss`;复用共用卡片/图表/下载工具。登记收款沿用 finance purchase 凭据幂等与权益事务,远程选择企业/套餐、预览金额桶数并核对后提交,失败保留表单。
+- 后端昨日草稿已验证,补上月份格式严格检查,防空字节等非法日期;范围、时间、统计口径见 `server/docs/WATER_FINANCE.md`。实际路由文件是 `server/plugin/saiadmin/config/route.php`。无数据库迁移,无新增持久业务数据。
+- 财务98项(含真实HTTP/完整XLSX/50,000条上限/收款幂等)、原供水69项通过,全部测试夹具回滚;PHP lint、前端vue-tsc/ESLint/Prettier/Stylelint和Vite3323模块构建通过,dist已更新。
+- 本地 `http://127.0.0.1:4173/#/water-finance` 预览已提供本次产物:首页内容及财务资源SHA256与dist一致。
+- 退款、佣金、奖励与提现的规则仍未确定,本次按昨天记录保持未开通,金额为null/“—”,不生成财务账本或启用审核。
+- 浏览器工具复测仍报 `unsupported Codex auth method: apikey`,本次未完成点击、截图及窄屏视觉验收;这是唯一尚未执行的页面验收环节,不应误记为项目登录问题。
+
+## 2026-09-16 财务结算页面(历史交接,已于09-17接续)
+- 最新需求:按用户提供的“财务结算中心”参考图调整 `/water-finance`,延续薄控制器、业务服务和值对象/策略分层,数据经Eloquent模型处理。参考图保存在 `server/docs/references/water-finance-reference-20260916.png`:四张收款/退款/提现/佣金卡片、左侧资金趋势及月汇总、右侧提现审核、底部最近资金流水,顶部对账下载与批量审核。
+- 已核对现状:供水财务仅有 `WaterBusiness::purchase()` 的线下收款登记与套餐权益闭环,`WaterOrder` 记录订单;旧 `DepositRefundLogic` 属于租赁押金,不是供水退款,不应混入。供水退款、推广奖励、佣金与提现尚无账本或规则。
+- 已向用户提出可选澄清:先完成收款/趋势/对账,其他区域显示暂未开通,或补充佣金比例、退款规则及提现审核后的打款方式;截至暂停未收到该问题答复。当前实现方向为先接入已有收款,缺失金额返回null/显示“—”,禁用无业务支撑的提现审核,不伪造截图数值或财务流程。
+- **已落盘但尚未验证的后端代码**:新增 `server/plugin/saiadmin/app/service/finance/FinanceFilters.php`、`service/WaterFinanceDashboard.php`;`model/water/WaterOrder.php` 添加 financeSource/financePeriod/financeDetails 查询scope;`controller/water/WaterController.php` 添加 financeDashboard/financeExport;`config/route.php` 添加 GET `/core/water/finance/dashboard` 和 POST `/core/water/finance/export`,均校验 `water:finance:read`。未新增数据库结构,未写财务数据。
+- 后端草稿契约:month默认为上海时间本月(2000年至本月),source支持all/regular/demo,keyword仅筛流水,page/limit分页;今日统计固定当天,月统计和补零日趋势按所选月份/数据范围,全量统计不受关键词与分页影响。排除未来记录,日期截至当前时间。测试识别暂以凭据 `TEST-` 前缀或套餐名称含“测试”为准,regular仅表示排除已标记测试,不能声称已银行对账。未开通退款/佣金/提现返回null及capabilities=false。XLSX全量匹配导出上限50000条并防公式注入。
+- **前端尚未落盘**:整批patch因对 `finance/index.vue` 同时Delete/Add而校验失败,实查 `src/api/water/finance.ts`、`views/water/finance/ReceiptDialog.vue`、`finance.scss` 均不存在,`finance/index.vue` 仍是原 `<WaterWorkspace module="finance" />`。明天应使用Update修改现有文件,不要认为之前前端草稿已应用。
+- 下一步:先审查并验证后端草稿(PHP语法、月份边界、今日/月汇总、测试范围、关键词、分页、权限、XLSX全量及特殊文本),再创建独立前端看板、月份切换、数据范围和流水搜索、详情/分页/导出、收款弹窗;复用 `WaterMetricCards.vue`、`WaterChart.vue`、`@/utils/waterDownload`,收款沿用POST finance action=purchase及原凭据幂等机制。保留错误/加载/空状态及窄屏布局。退款/净收率显示未开通/不可计算,不当成0。
+- **验证状态**:本轮财务新增代码尚未运行PHP lint、业务测试、HTTP、前端检查或构建,不能标记完成。需完成相关测试及vue-tsc/ESLint/Stylelint/Vite后更新记录;不要混用此前代理模块通过的检查结果。用户要求暂停后只更新交接记录,不继续实现。
+- 浏览器环境:最近复测仍为 `unsupported Codex auth method: apikey`;备用node_repl因 `codex-windows-sandbox-setup.exe` 启动路径解析失败,实际组件已确认存在,重置未恢复。用户知道需处理登录并重启,但未确认已完成;明天需要UI验收时先复测,不将工具认证故障当作项目登录故障。
+
+## 2026-09-16 代理业绩排行测试数据
+- 用户要求增加排行测试,执行并复跑 `server/scripts/seed_water_agent_ranking_demo.php --apply --http`,固定批次 `TEST-WATER-AGENT-RANKING-20260916`;新增订单32–36,复用5家启用的 `【代理测试0916】` 客户。本月排行:李策划2000元、西瓜商贸1500元、测试商贸600元。
+- 通过WaterBusiness购水流程增加4100元模拟订单和205桶权益;测试SKU15及5个套餐完成后停用,流水明确无实际收款。旧记录快照不变,没有库存入库或配送任务。PHP语法、事务内金额/权益/旧数据校验、HTTP排行及幂等复跑通过;下月不自动生成业绩。详见 `server/docs/WATER_AGENTS.md`。
+
+## 2026-09-16 代理商管理独立页面
+- `saiadmin-artd/src/views/water/agents` 替代通用表格:统计卡、代理/手机/等级/授权筛选、本月业绩排行、管理抽屉、代理档案/地图辖区编辑、超级管理员账号开通。真实供水订单按客户当前代理归属统计,未配置佣金显示空值;不混用旧租赁分润。
+- `WaterAgentDashboard` 查询服务、`WaterAgentManagement` 命令服务和 `agent/AgentFilters/AgentHierarchyPolicy/AgentAccessPolicy` 值对象/策略,均通过Eloquent模型处理数据。新增 dashboard/detail/open-account 接口;原 agents save 委托管理服务。
+- 已执行并复跑 `server/scripts/migrate_water_agent_hierarchy.php --apply`,仅增可空parent_id及索引;NULL未设置、0一级,支持最多三级,拒绝循环及子树超深。旧等级不推断回填。
+- 账号开通仅ID=1可执行,事务建立关联,不自动授予角色、部门或全量权限;无默认密码。已授权取真实有效供水权限,档案启用不等于授权。WaterController逐请求拒绝合作停用的关联代理账号;不修改旧系统角色及非供水权限。辖区/团队关系不是代理数据隔离;当前仍是平台入口。
+- 代理63项(含HTTP)、企业68项和原业务69项检查通过,夹具全部回滚;前端vue-tsc/ESLint/Stylelint及Vite生产构建通过,dist已更新。浏览器认证工具错误仍阻止视觉验收。详见 `server/docs/WATER_AGENTS.md`。
+
+## 2026-09-16 代理客户测试档案
+- 执行 `server/scripts/seed_water_agent_customers_demo.php --apply`,固定批次 `TEST-WATER-AGENT-CUSTOMERS-20260916`;新增企业79–84,名称前缀 `【代理测试0916】`。李策划(id=1)、西瓜商贸(id=6)、测试商贸(id=7)各关联2家;5家启用、远航服务停用。地址/电话明确为测试内容,无配送坐标。
+- 仅新增客户档案与审计,不添加收款、套餐权益、库存、设备或配送任务;旧客户及非目标业务模型数据快照校验不变。列表、代理/状态筛选、详情关联验证通过,复跑 `replayed=true` 不重复插入也不覆盖后续用户修改。
+
+## 2026-09-16 企业客户管理独立页面
+- `saiadmin-artd/src/views/water/enterprises` 替代该页通用 WaterWorkspace,按参考图提供四张统计卡、客户筛选/分页、权益与库存/设备/最近订单/补货状态、增长与转化图、详情、编辑及批量启停。编辑保留地图选点;代理归属复用既有 agent_id,无迁移。
+- WaterController 为薄控制器,新增 dashboard/detail/export/batch-status 入口;`WaterEnterpriseDashboard` 应用服务配合 `enterprise/EnterpriseFilters` 值对象和 `EnterpriseReplenishmentPolicy` 策略,数据通过 Eloquent 模型访问。多规格/设备先聚合避免重复累计;批量启停事务化且追加审计,不变更权益或配送。
+- 全量统计独立于列表筛选:活跃取真实用水或换桶记录;复购按本月购水企业中历史累计至少两单的比例;漏斗只取本月新增企业这一批次,暂无线索阶段,不伪造数据。业务口径及接口见 `server/docs/WATER_ENTERPRISES.md`。
+- 企业68项(含本地HTTP)和原业务69项检查通过,测试事务回滚;前端vue-tsc/ESLint/Stylelint及Vite生产构建通过,dist已更新。浏览器工具当前阻塞为 `unsupported Codex auth method: apikey`,未做截图级验收。
+
+## 2026-09-11 代理商服务区域地图选择
+- 代理商新增/编辑的 `region` 字段改为 `region-map` 类型,通过 `WaterRegionField` 打开高德地图,搜索或点选后反向识别省/市/区县,确认回填区域名称,保存代理商后写入。取消保留旧值,兼容原区域文本,无数据库迁移。
+- 原配送选点提取为共用 `WaterLocationPicker`,支持point/region两种模式;`DispatchLocationPicker`保留原open/selected接口。`waterAmap.findMapRegion`共用现有高德加载器,处理直辖市空city/重复省市、识别失败和配额错误;过期请求不会覆盖新选择。
+- 服务区域仍是行政区名称,不是多区域集合或地图多边形范围;地图确认不直接写库。区域解析脚本与既有道路规划回归、ESLint、vue-tsc、Vite构建验证;实际高德浏览器交互仍受既有工具启动故障限制。
+
+## 2026-09-11 导航偶发白色修复
+- 根因:`.water-layout` 导航样式原位于部分业务页按需导入的 `views/water/modules/water.scss`;独立库存/设备页没有该依赖,冷启动直达会显示默认白色,先访问总览等页面后才恢复深色。
+- 将导航和主区域背景规则原样拆至 `saiadmin-artd/src/views/index/water-layout.scss`,由公共布局 `views/index/index.vue` 直接导入。页面样式仍留在原water.scss;保留原 `/water-` 范围判断。
+- Prettier、ESLint、vue-tsc与Vite构建3291模块通过,构建清单验证布局静态依赖包含导航CSS,不依赖业务页懒加载。浏览器启动工具故障仍是既有限制,本轮未声称视觉验收。
+
+- 2026-09-11 工具排查更正:“Node运行时缺失”尚无证据。已确认 Codex cua_node 的 b58ca2eaa616c2da / e7fe122ad3cbcd58 两个版本 node.exe 均存在且可运行(v24.20.0);重置 CUA 会话后仍报 `failed to start Node runtime ... os error 3`,具体失效启动路径未定位。此故障不能归因于项目依赖或Node文件被删除。
+
+## 2026-09-11 配送异常及返仓闭环
+
+- 用户要求兼顾可用性、数据维护和设计模式,新增 `WaterFulfillment` 应用服务、`fulfillment/DeliveryState` 状态策略、`DeliverySettlement` 纯账务计算及 Eloquent `WaterDeliveryEvent` 追加事件。原 complete 结算统一委托新服务,事件保存请求哈希、操作人和快照,禁止覆盖已登记结果。
+- 后台支持实收/部分签收/拒收 → 退回核对(完好入仓、破损隔离)→ 关联重送;实收才扣权益,未送达余量退回后释放冻结。回收空桶先记车载,路线返仓时分规格核对实交及短少,结算后才释放配送员。库存页增加隔离满桶及累计空桶损失,任务详情增加履约记录和重送关系。
+- 已执行并复跑 `server/scripts/migrate_water_fulfillment.php --apply`,仅增量字段/表及唯一重送索引;旧在途任务10/11补充 LEGACY-FULFILL 路线关联及迁移事件,原状态、库存/权益/收款不变。新返回 returning/returned 已纳入跨日待办和总览,已结算时间及全范围返仓待办有独立入口。
+- 新增 dispatch action receive / return_receive / redeliver / route_settle;仓库退回及路线结算额外要求 water:inventory:write。GET dispatch/history提供履约时间线。测试实际数据仍17单103桶,未推进保留演示任务。
+- 后端59项新增闭环、原69业务/42调度/30流水/30设备/24策略模型共254项通过;另19项本地HTTP完整路径核对通过,临时夹具清理,正常操作审计保留。前端验证及工具限制详见 `server/docs/WATER_FULFILLMENT.md`;浏览器工具Node运行时缺失,本轮未做视觉验收。
+- 当前闭环是后台人员登记;移动端接单/客户签字照片、GPS、自动设备核验、支付退款仍未实现。破损转可用/报废和已签收冲正需后续可审计补偿流程。新状态投入使用后不能直接回退旧代码或删除字段。
+- 最终前端vue-tsc、ESLint/Prettier及Vite构建通过(3268 modules),已有道路规划脚本检查通过;4173预览读取最新构建,8787已实测新API。最终数据仍4片区17单103桶,9条正式未结算路线(旧任务10/11的归档路线35/36);未操作保留演示任务的签收或结算。
+
+## 2026-09-11 罗湖、福田、宝安配送测试
+
+- 用户要求增加三个片区数据,已执行 `server/scripts/seed_water_district_dispatch_demo.php --apply`,固定批次 `TEST-WATER-DISTRICTS-20260911`;幂等复跑 `replayed=true`,不覆盖用户后续操作。
+- 新增6个【测试0911】企业及任务77–82:罗湖2单10桶(路线17)、福田2单12桶(路线18)、宝安2单14桶(路线19),每区一位已排班测试配送员、各一条两站已派单路线。共同起点为南油第四工业区4栋;送达点为各区内人工模拟坐标,地址明确虚构企业、模拟坐标、请勿实际配送,未声称核实门牌。
+- 新增36桶模拟入库和权益,无收款;原测试SKU可用库存和在途不变、锁定增加36。事务内校验原任务/账户/收款订单不变,新路线每条2站、片区/日期/冻结权益一致。
+- PHP lint、实际写入校验、三区看板筛选及幂等复跑通过。当前看板4个片区、17单103桶(南山11单67桶,罗湖2单10桶,福田2单12桶,宝安2单14桶),状态待派单3、已派单11、配送中3;本轮未做浏览器或高德道路返回验收。
+
+## 2026-09-11 今日配送测试数据
+
+- 用户要求新增今日任务供测试,已执行 `server/scripts/seed_water_today_dispatch_demo.php --apply`,固定批次 `TEST-WATER-TODAY-20260911`;首次仅允许当天执行,复跑返回 `replayed=true`,不覆盖后续操作。
+- 新增6个【测试0911】企业及今日任务71–76,共33桶:晨光办公4桶、青禾研发6桶待派单;星海科技5桶、云帆商务7桶、绿洲创意3桶组成路线15(三站已派单);远景服务8桶为路线16(配送中)。新增3位已排班测试配送员,其中“合单体验配送员”空闲可用于待派单合单测试。
+- 沿用南油第四工业区4栋起点及此前高德确认的中洲控股中心、科技园、生态园坐标;园区位置明确标为园区级,企业为虚构测试名称、地址含请勿实际配送。三站设置不同送达时限,方便测试路线优化。
+- 仅增加33桶模拟仓库入库及33桶模拟权益,无新增收款;原测试SKU可用库存不变,锁定增加25、在途增加8。事务内确认原任务、原账户和收款订单全部不变,新增状态、日期、权益冻结一致。
+- PHP语法检查、实际写入校验及幂等复跑通过。今日看板服务返回11单67桶(待派单3、已派单5、配送中3),其中包含此前5条未完成任务;本轮未重新进行浏览器验收。
+
+## Identity
+
+- Name: SaiAdmin 6.x / 当前前端品牌名为 `云泉智配`
+- Purpose: 基于 Webman 的后台管理系统,包含权限管理、系统配置、代码生成、日志/缓存/服务器管理,以及当前定制的商户、设备、订单、资金、运营和小程序业务。
+- Root: `D:\phpstudy_pro\WWW\saiadmin6.x`
+- Layout: 前后端同仓库,前端在 `saiadmin-artd`,PHP 后端在 `server`。
+- Status: 开发中;工作树存在大量用户已有修改和未跟踪业务文件,处理任务时不要 reset、checkout 或覆盖这些改动。
+
+## 当前供水业务合并(2026-09-09)
+
+- 用户以 `D:\水后台UI\desktop\P01.png`–`P09.png` 为新后台参考,授权合并。新主线为企业购水套餐、权益、实物库存、补货和配送。下文旧项目映射仅作为旧模块参考,冲突时以新供水规则为准。
+- 新后端:`server/plugin/saiadmin/app/service/WaterBusiness.php`、`app/controller/water/WaterController.php`(均在 saiadmin 插件内),接口 `/core/water/*`。前端:`saiadmin-artd/src/views/water`。入口 `/#/water-overview`。
+- 迁移:`php scripts/migrate_water_business.php --apply`,已执行并复跑,新增 9 张 `sa_water_*` 表及 12 个菜单,未转换或覆盖旧业务数据。
+- 新菜单存在时旧前端一级菜单隐藏,但旧路由继续注册。新供水权限显式授权,不自动继承给旧代理。代理、角色与审计新入口先复用旧页面。
+- 已落地基础闭环:登记已核实线下套餐收款 → 发放权益 → 建单冻结权益/库存 → 派单出库 → 签收扣权益/增加现场满桶 → 回收空桶;未出库可取消释放。权益与实物分别记录,收款凭据和有效配送任务防重复。
+- 当前一企业一个供水地址;没有实际遥测数据时不显示预测或虚构在线状态。尚未完成地图合单、设备饮水协议、线上支付退款、代理自动佣金/提现及移动端内容投放。完整边界见 `server/docs/WATER_MERGE.md`。
+- 验证:`php tests/water_business_test.php` 36 项检查通过,测试数据事务回滚;相关 PHP 语法、前端 vue-tsc、ESLint、Vite production build 通过(3245 modules)。浏览器已确认新总览、企业、库存、内容手机预览及企业必填校验,旧审计页面可从新菜单访问。
+
+## Stack and commands
+
+### 废弃表清理(2026-09-10)
+
+- 用户确认后仅删除water库中的 `sa_device_operation_profile`(0行)、`sa_article`(8行)、`sa_article_banner`(3行)、`sa_article_category`(4行);63表减为59表,非目标表行数全部不变,供水测试数据保留。
+- 完整备份63表且4张表经过临时表恢复/逐行比对;备份目录见 `server/docs/unused-tables-backup-location.txt`,完整结果及恢复命令见 `server/docs/DATABASE_CLEANUP_REVIEW.md`。备份文件未放入网站公开目录或仓库。
+- `server/scripts/cleanup_unused_water_tables.php` 固定4表白名单,清理前检查数据库名、备份SHA256、数据变化和代码/数据库对象依赖;默认检查,`--apply`删除,`--restore --apply`只恢复缺失表且拒绝覆盖。
+- 69项供水、30项设备接入、56项旧设备运营检查全部通过并回滚。15条预警、10+5分页、2595元测试收款核对通过。
+
+### 补货搜索核对(2026-09-10)
+
+- 按用户要求再添加10条不同名称测试预警:星河科技、青禾设计、远航物流、锦程制造、晨曦教育、百川商贸、蓝湾酒店、瑞丰农业、华庭物业、博雅文化,名称均以“(测试)”结尾;分布东/西/南/北城区,使用山泉水/纯净水两种测试规格。脚本 `server/scripts/seed_water_alert_search_demo.php --apply` 使用固定批次标记防重复,现场库存经盘点入口建立账户,未增加收款或权益。
+- 当前预警15条,补货页默认每页10条(其他共享页面仍20条),第一页10条、第二页5条;已验证搜索星河1条、东城区3条、山泉水5条,重跑不重复增加数据,今日收款保持2595元。新增账户权益为0,仅用于预警、搜索和分页;建单仍受权益/仓库库存校验。
+- 本次页大小调整通过vue-tsc、ESLint、Prettier及Vite构建(3247 modules);浏览器验证第一页10条、下一页5条,从第二页搜索“星河”返回第一页1条,重置恢复15条,已停留在第一页供用户测试。
+
+- 搜索已接入 `GET /core/water/alerts?keyword=...`,后端模糊匹配企业名称、区域和商品规格。新增本轮10条数据前,原5条演示企业名均含0910、规格含18.9,搜索“1”会全部命中,并非查询未执行;浏览器搜索“云泉科技”返回1条。
+- `WaterWorkspace.vue` 已修正补货页搜索提示,明确搜索范围及点击查询/回车触发;增加重置(清空关键词、恢复仅预警账户及第1页)、120字限制和无匹配说明。前端搜索去除首尾空格,保持后端原有筛选口径。
+- 验证:vue-tsc、相关 ESLint、Prettier、Vite构建通过(3247 modules)。4173新版浏览器中,带空格的“云泉科技”查询返回1条且去掉空格;回车查询“不存在的企业”返回0条并显示无匹配提示;重置恢复5条。
+
+### 设备协议接入预留(2026-09-10)
+
+- 按用户要求先预留,等有设备后再提供协议。新增 `POST /iot/water/report`,以及原 `mqtt.message` 的供水适配事件;`config/water_device.php` 两个开关默认关闭,`adapter=null`,未启用外部连接或修改现有 MQTT Topic。
+- `app/service/water/DeviceProtocolAdapter.php` 保留 HTTP/MQTT 协议转换接口;`DeviceIngress.php` 检查开关、HTTP 独立 Bearer 凭据及报文大小。具体厂商适配器尚未实现,因此不能宣称已完成真实设备接入。
+- `DeviceReport.php` 仅更新已有供水设备的 `last_seen` 和当日累计用量快照;最近5分钟的报告时间作为心跳,重复/较旧心跳不刷新。用量同日取累计最大值、跨日重置,库存权益及账户日均不变。不支持直接接收增量用量。
+- `php tests/water_device_ingress_test.php` 30项通过,PHP lint通过,所有设备测试回滚;接口格式、未来厂商鉴权/重放/Topic绑定要求见 `server/docs/WATER_DEVICE_INTEGRATION.md`。
+- 原有69项供水业务检查回归通过;路由清单已确认,HTTP子进程已重新加载,实际POST设备入口返回503(保持关闭)。MQTT进程未启用,未来联调时需重启加载新事件配置。
+
+### 总览收款测试数据(2026-09-10)
+
+- 后续按用户要求新增配送/预警演示:`php scripts/seed_water_dispatch_demo.php --apply`,固定批次 `TEST-WATER-DISPATCH-20260910`。任务 ID 9/10/11 分别待派单8桶、已派单6桶、配送中10桶,复用云泉科技/智配办公/清泉产业园;均为模拟任务。
+- 测试仓库入库100桶后为可用76、锁定14、在途10;新增“【测试0910】待补货体验中心”(满桶1/阈值2,紧急)、“【测试0910】待补货商务楼”(满桶3/阈值5,普通),各有10桶可用权益且尚未建单。总预警5条,其中3条已关联有效配送;无模拟遥测。
+- 两个新测试企业各登记299元模拟套餐,因此当日总览测试收款合计从1997增至2595元。脚本整批事务,以 `demo_seed` 流水作为已完成标记,复跑不会重置用户后续操作或重复入库/建单/收款;已复跑核对上述数量和金额不变。
+
+- 用户要求保留少量测试数据,已执行 `server/scripts/seed_water_receipts_demo.php --apply`。新增 3 个 `【测试0910】` 企业、1 个 18.9L 测试规格、3 个套餐和 3 笔模拟收款;订单 ID 3/4/5,金额 699/299/999 元,共 1997 元、90 桶权益。
+- 固定收款凭据 `TEST-WATER-RECEIPTS-20260910-A/B/C`,流水备注明确“未发生真实收款”,地址标明“请勿配送”。未生成配送单、实物库存或设备遥测。
+- 当日总览收款从 0 增至 1997 元;重复执行返回原订单,不重复入账、不修改登记日期。此批为保留的测试业务数据,不能作为实际经营收款依据。
+
+### 补货待办接续(2026-09-10)
+
+- 已补齐账户数量阈值编辑和补货批量建单。入口 `/#/water-alerts`,“全部启用账户 / 策略维护”可查看未预警账户;停用企业/规格不参与补货列表。
+- `POST /core/water/alerts` 的 `action=policy` 仅更新 `threshold_qty`,权限 `water:alerts:write`,记录修改前后阈值及操作人,不修改日均、库存或权益。天数规则仍为既有 5 天预警、3 天紧急。
+- `POST /core/water/dispatch` 的 `action=batch_create` 接收 `items:[{account_id,quantity}]`(1–50 条),权限 `water:dispatch:write`。服务端根据账户解析企业和规格,复用单笔冻结规则,按规格/企业排序加锁;任一条失败整批回滚,已有有效任务禁止重复创建。
+- 前端 `BatchDeliveryDialog.vue` 逐条输入桶数并确认企业、规格及地址;只勾选当前页有可用权益、未建单的预警账户,换页/查询清空勾选。失败保留填写内容供核对。
+- 验证:69 项数据库回滚检查通过,包含阈值即时生效、非法值、合计库存不足、后续行失败回滚、停用企业、重复建单和权限拒绝。PHP lint、vue-tsc、相关 ESLint、Vite build 通过(3247 modules)。浏览器确认新按钮、预警桶数列和范围切换入口,当前零业务数据,未通过浏览器生成实际配送单。
+- 已重启本项目 HTTP 子进程并确认 8787 返回 HTTP 200;前端 4173 预览加载本次构建。无需新增数据库迁移。
+- 下一步建议:配送异常/拒收退回流程;地图合单、设备饮水协议、线上支付退款、佣金和真实移动端投放仍未实现,详见 `server/docs/WATER_MERGE.md`。
+
+### Frontend (`saiadmin-artd`)
+
+- Vue 3 + TypeScript + Vite 7 + Element Plus。
+- Pinia、Vue Router 4、VueUse、Vue I18n、Axios。
+- Tailwind CSS 4、Sass、ECharts、XLSX、WangEditor 等业务组件。
+- Package manager: pnpm;版本要求见 `saiadmin-artd/package.json`:Node `>=20.19.0`、pnpm `>=8.8.0`。
+- 当前环境 Node `v22.21.0` 可用,但 `pnpm` 未加入 PATH;项目 `node_modules` 已存在,可直接使用 `node_modules/.bin` 中的工具。
+- Development: `pnpm dev`;端口由 `.env` 的 `VITE_PORT` 配置为 `3006`。
+- Build: `pnpm build`(`vue-tsc --noEmit && vite build`)。
+- Lint: `pnpm lint`;格式化/样式检查脚本见 `package.json`。
+- Development API: `.env.development` 使用 `VITE_API_URL=/api`,Vite 代理到 `http://127.0.0.1:8787`。
+- Production API: 当前本地部署使用 `VITE_API_URL=http://127.0.0.1:8787` 直连 Webman;如果部署到正式域名,应由反向代理把 API 前缀转发到 Webman,而不能继续指向 `D:\phpstudy_pro\WWW\api` 等其他项目。
+
+### Backend (`server`)
+
+- PHP `>=8.1`,当前 CLI PHP `8.2.29`;Composer 当前 `2.10.2`。
+- Webman/Workerman,`workerman/webman-framework ^2.1`;Think ORM、SaiAdmin 6.x、SaiPackage 6.x。
+- 数据库默认 MySQL,配置入口为 `server/config/database.php`,值来自 `server/.env`。
+- 缓存支持 file/Redis;当前 `server/.env` 为 `CACHE_MODE=file`。
+- JWT、Session、文件存储、Webman 插件机制;当前增加了 `workerman/mqtt ^2.2`。
+- Development server: `server/windows.php`(Windows 入口);Webman HTTP 进程配置在 `server/config/process.php`,监听 `http://0.0.0.0:8787`。
+- Unix-style entry: `server/start.php`。
+- PHP syntax check: `php -l <file>`;本次已核对启动、进程、Bootstrap、MQTT subscriber、登录中间件等关键文件,均无语法错误。
+
+## Structure and entry points
+
+### Frontend
+
+- `saiadmin-artd/src/main.ts`: 创建 Vue 应用,初始化 Pinia、Router、全局指令、错误处理和国际化。
+- `saiadmin-artd/src/App.vue`: Element Plus 配置、主题初始化、存储兼容性检查和系统升级检查。
+- `saiadmin-artd/src/router/index.ts`: Hash 路由;静态路由在 `src/router/routes/staticRoutes.ts`,登录后动态路由来自 `src/router/routes/asyncRoutes.ts`。
+- `saiadmin-artd/src/router/guards/beforeEach.ts`: 登录状态、用户信息、字典、后端菜单和动态路由初始化。
+- `saiadmin-artd/src/router/core/MenuProcessor.ts`: 前端/后端菜单模式选择、角色过滤、空菜单过滤和路径规范化;后端模式通过 `/core/system/menu` 获取菜单。
+- `saiadmin-artd/src/utils/http/index.ts`: Axios 统一封装;从 Pinia 读取 access token,添加 Bearer Authorization,处理统一响应和 401。
+- `saiadmin-artd/src/api`: 按功能组织 API;目前约 31 个 TypeScript API 文件。
+- `saiadmin-artd/src/views`: 约 132 个视图文件,包含系统、安全维护、工具和当前定制业务模块。
+- `saiadmin-artd/SAIADMIN.md`: 前端功能目录约定,新增业务通常按模块/业务/表页面组织。
+
+### Backend
+
+- `server/start.php` / `server/windows.php`: Webman 启动入口。
+- `server/config/process.php`: HTTP、MQTT subscriber、文件监控进程配置。
+- `server/app`: 项目层控制器、模型、服务和进程;当前包含 `app/process/MqttSubscriber.php`、`app/service/MqttPublisher.php`、`app/service/MqttTopic.php`。
+- `server/plugin/saiadmin`: SaiAdmin 核心插件;按 controller、logic、model、validate、middleware、utils、config 分层。
+- `server/plugin/saiadmin/config/route.php`: `/core` 和 `/tool` 路由,`fastRoute()` 为控制器常用 CRUD 路由生成器。
+- `server/plugin/saiadmin/app/middleware`: 登录检查、权限检查、跨域和系统日志中间件。
+- `server/plugin/saipackage`: 在线插件/安装相关功能。
+- `server/config/plugin`: tinywan JWT/storage、Webman channel/console/event 等插件配置。
+
+## Architecture and integrations
+
+- 浏览器先加载前端静态应用;前端通过 Axios 请求 `/api`,开发时由 Vite 代理到 Webman `8787`。
+- 生产构建当前直连 `http://127.0.0.1:8787`;后端 SaiAdmin 插件已启用 `CrossDomain` 中间件以支持前端站点跨端口请求。
+- 登录页调用 `GET /core/captcha`,登录调用 `POST /core/login`;成功后保存 access/refresh token,再获取用户信息、字典和菜单。
+- 前端动态菜单支持 backend 模式和 frontend 模式。当前 `.env` 的 `VITE_ACCESS_MODE=backend`,因此以服务端菜单为主;`MenuProcessor` 会补齐缺失的内置 `MiniProgram` 和 `Merchant` 一级菜单。
+- 后端 SaiAdmin 插件通过控制器属性/反射缓存实现免登录接口和权限标记;登录与验证码接口在 `LoginController::$noNeedLogin` 中放行。
+- MQTT 由 `app\process\MqttSubscriber` 作为独立 Webman 进程运行,业务发布入口位于 `MqttPublisher` 和 `/tool/mqtt/publish`。
+- 前端路由模块目前包括 dashboard、system、miniProgram、agent、merchant、device、operations、result、exception;定制业务视图包括 agent、device、fund-management、management、merchant、mini-program、operation-management、order、user-management。
+
+### 旧业务项目参照映射(后续开发必须复用)
+
+- 业务规则权威参考项目为 `D:\phpstudy_pro\WWW\api`(ThinkPHP 5)。后续实现设备、商户、代理、订单、资金等旧业务页面时,应先追踪该项目的 controller → model/service → 写入/查询表链路,再将业务语义翻译到 SaiAdmin;不直接让新前端调用旧项目接口。
+- 铺货记录旧入口为 `api/application/agentapi/controller/Merchant.php::bind_device_records()`,分页查询位于 `api/application/common/model/MarketZuoRelyRecord.php::queryPageList()`。旧逻辑关联 `market_zuo_rely_record`、`market_goods`、`store` 和 `admin_user_tier`,按记录时间倒序并限制当前代理本人/下级可见范围。
+- 铺货流水字段映射:旧 `market_zuo_rely_record` → 新 `sa_merchant_device_binding_record`;`market_good_id` → `device_id`;`goods_sn` → `device_sn`;`rely_store_id` → `merchant_id`;`distribute_type` → `action_type`(1 铺货、2 解绑);`add_date` → `action_time`;`depend_type` 保留为设备类型码。
+- 关联表映射:旧 `market_goods` → `sa_device`,旧 `device_id`/二维码搜索 → 新 `device_code`,设备 SN 使用 `goods_sn`;旧 `store.store_name` → `sa_merchant.name`;操作人使用 `sa_system_user` 的 `realname`/`username` 和 `phone`。
+- 铺货记录筛选映射:旧 `distribute_type`、`search_store_name`、`search_device_id`、`search_goods_sn`、`search_depend_type` → 新 `action_type`、`merchant`、`qr_code`、`device_sn`、`goods_type`。旧分页 `start` 是从 0 开始的页号,新接口统一使用 `page`(从 1 开始)和 `limit`。
+- 当前项目尚未完整迁移旧 `admin_user_tier` 代理层级树,因此设备类数据权限暂统一沿用 `sa_device.manage_agent_id`:管理员 ID 1 查看全部,其他账号只查看其当前管理设备及对应流水。未来迁移代理层级后,再按旧项目“本人 + 合法下级”规则扩展。
+- 设备分配旧链路位于 `api/application/agentapi/controller/Sttuf.php`:`distribute_agent_devices()` 查询当前代理管理库存,`save_distribute_agent_devices()` 执行下发,`my_agent_stock_devices()` 查询下级未铺货库存,`save_recover_agent_devices()` 执行回收,`agent_distribute_log()` 查询双方分配/回收流水;旧路由分别为 `/agentapi/distribute_agent_devices`、`/agentapi/save_distribute_agent_devices`、`/agentapi/my_agent_stock_devices`、`/agentapi/save_recover_agent_devices`、`/agentapi/agent_distribute_log`。
+- 设备管理权映射:旧 `market_goods.manage_aid` → 新 `sa_device.manage_agent_id`,旧 `market_goods.belong_aid` → 新 `sa_device.belong_agent_id`(分配只转移管理权,不改变所有权)。旧 `device_manager_record` → 新 `sa_device_manager_record`;`goods_id/goods_sn/goods_type` → `device_id/device_sn/goods_type`,`last_aid/now_aid` → `last_agent_id/now_agent_id`。新 `action_type=1` 表示分配、`action_type=2` 表示回收,对应旧 `flow_type=0/1`。
+- 分配安全规则沿用旧语义并收紧为库存操作:目标必须是当前账号有权访问且启用的下级代理;设备必须存在、当前由操作者管理、且尚未铺货到商户;单次最多 100 台。成功后在同一事务内更新 `sa_device.manage_agent_id` 并写 `sa_device_manager_record`,不存在、非当前管理、已在目标代理、已铺货分别返回独立结果集合。回收执行相反的管理权转移,同样禁止回收已铺货设备。
+- 代理修改旧链路位于 `api/application/agentapi/controller/Sttuf.php::edit_agent()` / `save_edit_agent()`。新系统保留 `sa_system_user.realname/phone` 作为账号基础资料,并使用一对一 `sa_agent_profile` 保存运营模式、产品、分润、业务权限、运营区域和公司地址;开通类型继续由部门层级推导且编辑时只读,登录账号和密码均不在此页面修改。
+
+## Conventions and constraints
+
+- 2026-09-08 用户确认的维护要求:可配置的业务类型、产品编码及其映射优先由数据库统一维护,前后端读取同一来源,不在多个类或页面重复写死映射数组。固定协议值与业务配置须区分,不能把可变业务目录当作代码常量。
+- 业务代码按数据查询、规则计算和页面展示拆分职责,使用明确的方法名;注释说明业务来源、公式和边界条件,避免大方法中混杂查询、计算及兼容判断。实现旧业务前复用已验证的项目记录,不另创手工流程代替缺失的数据关联。
+- 新增旧业务功能前,先查询上述 `D:\phpstudy_pro\WWW\api` 对应控制器、模型/服务及数据写入路径;`PROJECT_CONTEXT.md` 已记录过的映射直接复用,只分析尚未覆盖的业务差异。
+- 前端优先遵循 `SAIADMIN.md` 的模块目录结构,并使用已有 API、类型、组件和 store 模式。
+- 路由组件路径使用项目现有的 `AppRouteRecord` 结构及 `@views`/`@utils`/`@stores`/`@styles` 等别名。
+- 后端新增 SaiAdmin 业务通常配套 controller、logic、model、validate 和 `config/route.php` 路由。
+- 修改前先检查当前工作树;截至本次分析,前后端均有未提交变更,不能把这些变更当作基线或擅自清理。
+- 不在项目记忆中保存 `.env` 的密码、Token、JWT 密钥或其他凭据,只记录变量名、用途和非敏感默认值。
+
+## 设备运营实现与旧业务映射(2026-09-08)
+
+- 用户明确要求:设备运营是截图中的只读统计列表,不另建手工运营参数流程;业务类型及产品映射从数据库读取,代码要按职责拆分、便于维护。之前的参数表方案已撤下,不能恢复“待配置+手填分成”作为业务迁移替代方案。
+- 权威旧链路:api/application/agentapi/controller/Statistics.php::business_device_data() → common/model/MarketGoods.php::getMarketGoodsDetailList() / common/service/Statistics.php::getDeviceStatData()。原表头“归属代理”实际显示设备当前管理代理,继续按管理权筛选。
+- 新实现分工:DeviceOperationLogic 负责范围校验、批量订单聚合、排序分页;DeviceProductCatalog 读取数据库产品/类型字典;DeviceOperationShares 计算既有代理、商户分成;DeviceOperationMetrics 计算客单价、盈利、增长率、回本和评级。不得在这些类及页面另写设备编号→产品编码的固定数组。
+- 数据字典:系统管理/数据字典中 device_product(设备产品:标签为名称,键值为 sa_agent_profile.profit_rates 的产品编码);device_type_product(设备类型产品映射:标签为产品编码,键值为 sa_device.goods_type 编号)。支持多个设备类型归同一产品,按状态和软删除过滤;重复编号或重复产品编码报错,禁止静默覆盖。接口每次请求读取字典,修改立即生效,前端类别选项和默认选中项由接口返回。
+- 成本直接读取 sa_device.cost_price(新增可空字段,来源旧 market_goods.cost_price)。比例读取 sa_agent_profile.profit_rates 和 sa_merchant.profit_method/profit_rate;代理为占总交易额比例,商户为相对直属代理比例。例如代理80%、商户相对25%,商户有效20%、直属管理人净60%。多级管理人扣除当前商户分支第一个下级的比例;部门同层多人等无法唯一确定关系时保持未知,不猜分成代理。运营统计不修改支付分账。
+- sa_device_operation_profile 不再参与任何查询,参数写入接口和页面入口已移除;该空表已于2026-09-10经用户确认、备份后删除,core:operation:device:update 权限已停用并软删除。仅保留 GET /core/operation-management/device-operations/index(core:operation:device:index),响应附带 categories/category;类别使用数据库产品编码,不再使用旧页面别名 k-battery/bluetooth 等。
+- 本人与下级范围互斥,本人只查当前管理人,下级沿用部门后代(管理员1查询其他账号);设备与订单同时限制管理范围。净交易额为实付减退款且不小于零,扣款失败无收入;总订单数包含未支付订单,客单价除以有效已付订单数。今日/昨日优先 return_at,缺失依次 update_time/rent_at/create_time;每日收入累加。
+- 盈利按商户有效分成;旧“盈利率”是净交易额÷设备成本;今日表现按昨日交易额÷投放期间日均交易额及旧阈值评级,无交易默认为“最差”。回本按当前铺货时间后的管理人有效收入累计,取首次达到成本的日期;昨日金额0时增长率为 NULL;未知成本/分成的相关指标为 NULL,前端显示“--”,但交易额0时收益可确定为0。
+- 前端:saiadmin-artd/src/views/operation-management/device-operations/index.vue,API:src/api/operationManagement/deviceOperation.ts。保留截图20列、条纹行、设备码换行、居中分页,默认10条;金额、订单数、客单价和盈利率在数据库聚合后排序分页;过滤快速切换时忽略过期响应。没有操作列、运营参数表单或手工比例保存。
+- 新迁移:server/plugin/saiadmin/db/20260908_use_device_operation_sources.sql、20260908_add_device_product_dictionary.sql。运行 server 目录下 php scripts/migrate_device_operation_sources.php --apply --legacy-schema=api;不带 --apply 为检查模式,--legacy-schema 可省略。成本只回填唯一 SN、设备类型及已有 legacy_id 一致且新库成本为空的设备,不覆盖现有成本;运行时报表不访问旧库。
+- 本地数据现状:新库为15条测试设备,与 api.xs_market_goods 匹配SN为0,故没有迁入旧设备成本;现有代理档案也不完整。真实台账及档案未迁入时不能宣称经营指标数据齐全,更不能复制截图数值充当真实数据。
+- 验证:上述迁移重复执行成功;php server/tests/device_operation_test.php 通过56项检查(所有数据库测试事务回滚),包括退款、每日累计、回本、全局排序分页、管理范围、档案修改即时计算、多级分成、新增数据库类型驱动类别与分成、禁用/重复字典映射。PHP lint、vue-tsc、ESLint、Prettier、Stylelint、Vite构建通过;浏览器验证真实列表、10条分页、数据库类别切换,无JavaScript错误。Vite保留既有系统菜单动态/静态导入警告。
+- 最终页面验收(2026-09-08):在 1920×900 桌面视口下,统计表与表头宽度均为 1649px,截图对应的20列完整显示,分页居中;无运营参数按钮或操作列。验收结束后已恢复浏览器原始视口。当前代码修正和本地构建已完成;真实设备成本及缺失代理档案的迁入仍按上述数据现状跟进。
+
+## Known issues and follow-ups
+
+- **Next session (2026-09-05):** 退押管理的真实分页与 XLSX 导出已经完成,明天从“正式退押任务写入链路”继续:先追踪新版订单完结及微信/支付宝退款回调,在生成退押任务时写入 `sa_deposit_refund_record`,退款结果回调时更新 `status`、`refund_time` 和异常备注。当前新系统尚无完整支付退款执行链路,不得用 `sa_rental_order.refund_amount` 猜测押金、计划退款时间或退款路径。可复用本地 `20260904_seed_deposit_refund_test_data.sql` 的 3 条 `TEST-ORDER-*` 数据做页面与导出回归。
+
+1. **登录验证码图片 MIME 和生产 API 缓存/路由问题已修复。**
+   - 前端登录页在 `saiadmin-artd/src/views/auth/login/index.vue` 使用 `img :src="captcha"`,接口为 `GET /core/captcha`。
+   - 本次直接请求本地后端得到 HTTP 200、业务 `code=200`,返回了 `uuid` 和图片数据;图片数据前缀为 `data:image/png;base64,/9j/...`。
+   - `/9j/` 表示实际内容是 JPEG,而 `server/plugin/saiadmin/utils/Captcha.php` 使用的 `Webman\Captcha\CaptchaBuilder::get()` 在当前 vendor 实现中通过 `imagejpeg()` 输出。已将返回值改为 `data:image/jpeg;base64,`,并重启 Webman 进程使修改生效。
+   - 前端 `refreshCaptcha()` 已增加异常捕获,接口失败时会在控制台记录 `[Login] Failed to load captcha`。
+   - 当前 `server/.env` 为 `CAPTCHA_MODE=cache`、`CACHE_MODE=file`,本次接口请求成功,缓存配置不是当前“接口完全不可用”的首要证据。修复前应保留现有用户改动并先确认浏览器 Network/Console。
+   - 生产环境曾请求 `/prod/core/captcha` 并收到 `304 Not Modified`;实测该路径没有进入本项目 Webman:端口 `8787` 下 `/prod/core/captcha` 为 404,端口 80 下则被转到另一个 `D:\phpstudy_pro\WWW\api` 项目并出现 PHP 语法错误。已改为生产构建直连 `http://127.0.0.1:8787`,并给验证码 GET 增加时间戳参数防止缓存复用。
+
+2. **登录跨域预检问题已修复。**
+   - 直连 `127.0.0.1:8787` 后,JSON 登录会先发送 `OPTIONS /core/login`;原先返回 404,浏览器因此阻止后续 POST。
+   - 已增加全局 `CrossDomain` 中间件、通用 OPTIONS 路由,并保留 SaiAdmin 业务路由上的跨域中间件,确保预检及登录成功/失败响应都带跨域头。
+   - 复测结果:OPTIONS HTTP 200;POST `/core/login` 在无效验证码下正常返回业务错误且包含 `Access-Control-Allow-Origin`,说明登录请求链路已打通。
+
+3. **工作树不是干净基线。**
+   - 已修改文件包括前端锁文件、品牌配置、中文文案、菜单处理/路由,以及后端 Composer、缓存、ORM、进程和环境示例配置。
+   - 未跟踪内容包括前端商户/小程序 API 与页面、代理/设备/订单/资金等业务页面,以及后端 MQTT、服务层、插件和 Composer lock。处理后续任务时应区分用户已有改动和新改动。
+
+4. **需要审查配置中的敏感材料。**
+   - `server/config/plugin/tinywan/jwt/app.php` 存在看起来像密钥配置的内容;记忆不保存具体值。若这些值用于真实环境,应移出代码库、改用环境变量并轮换。
+
+5. **编码显示异常。**
+   - 多个源码注释和 README 在当前 PowerShell 输出中显示为乱码,可能是终端编码问题,也可能是部分文件编码不一致;修改中文内容前应确认文件实际编码,避免无意重写整文件。
+
+6. **商户统计页面直链问题已修复。**
+   - `merchant.ts` 路由模块已注册商户列表、添加商户和数据统计页面。
+   - 后端菜单模式缺少商户路由时,`MenuProcessor` 会补充并合并内置商户路由;已验证 `/merchant/statistics?id=73` 正常渲染。
+
+7. **商户列表的营业时间已接入并完成迁移,免费店员仍待业务定义。**
+   - 设备绑定、提现更新、删除商户、密码重置、商户统计和营业时间已有前后端接口。
+   - 营业时间使用旧系统兼容字段 `business_time_des`,迁移脚本为 `server/plugin/saiadmin/db/20260826_add_merchant_business_time.sql`;2026-08-27 已执行并确认字段存在。
+   - “免费店员”在当前新系统没有对应数据表、用户关联或明确业务规则,暂未猜测实现,前端仍保留提示入口。
+
+8. **Windows 后端启动脚本不解析 `status`、`restart` 等参数。**
+   - `server/windows.php` 每次执行都会再启动一整套 Workerman 进程;不要用 `php windows.php restart -d`,否则多个进程会通过端口复用同时监听 8787,新增路由可能随机命中旧进程并返回 404。
+   - 2026-08-26 已清理本项目重复进程树并只保留一套 8787 服务;后续重载应先精确停止属于 `saiadmin6.x/server/windows.php` 的进程树,再启动一次。
+
+9. **车载设备供电/断电依赖独立硬件指令服务。**
+   - 新接口沿用旧系统 `POST /cdy/tasks` 协议,发送 `uid`、`task_type=switch` 与 `value=1/0`;实际网关地址通过 `DEVICE_COMMAND_ENDPOINT` 配置。
+   - 配置为空时接口会拒绝执行;不要用本地字段更新或模拟成功替代真实网关响应。二维码扫描内容可选配 `DEVICE_QR_BASE_URL`,为空时仅编码设备机器码。
+
+## Decisions and change notes
+
+- 2026-08-25: 建立本项目独立记忆文件;不回退当前工作树。
+- 2026-08-25: 记录当前定制方向为 WaterAdmin + 商户/设备/订单/资金/运营/小程序业务,并包含 MQTT 集成。
+- 2026-08-25: 修复登录验证码 JPEG/PNG MIME 不一致问题,重启后端并复测 `/core/captcha` 返回 `data:image/jpeg`,前端重新构建。
+- 2026-08-25: 修复生产 `/prod` API 指向错误项目和验证码缓存问题;生产构建改用 Webman `8787`,启用跨域中间件并为验证码请求增加防缓存参数。
+- 2026-08-25: 修复直连后端导致的登录 OPTIONS 预检 404;增加全局跨域支持和通用 OPTIONS 路由,复测登录 POST 已能正常到达后端。
+- 2026-08-26: 按旧系统语义新增商户营业时间读写接口、数据库迁移和前端编辑弹窗;免费店员功能因缺少业务规则暂缓。
+- 2026-08-26: 核对商户数据统计页面已具备日期查询、汇总、趋势图、支付方式、每日明细和 Excel 导出全链路;执行现有统计演示数据脚本,写入 8 个演示商户共 480 条最近 60 天统计记录,并通过 8787 HTTP 接口验证查询和导出。
+- 2026-08-26: 完成添加商户前后端链路;前端表单通过 `/core/merchant/save` 提交,后端执行字段校验、直属代理/分店归属校验、手机号唯一校验和密码哈希,详情接口不返回密码;添加页补充可编辑的 0-100 分润比例字段。
+- 2026-08-26: 完成“我的设备”真实列表、后端分页搜索/分类/状态汇总和 Excel 导出;数据按 `sa_device.manage_agent_id` 隔离,超级管理员 ID 1 可查看全部,缺少真实业务字段时使用 `--` 或 `0`,不生成虚假订单/金额数据。
+- 2026-08-26: 用户指定后续旧业务逻辑统一参照 `D:\phpstudy_pro\WWW\api`。本次完成铺货记录 controller/model/表字段和权限范围映射并沉淀到项目记忆,后续不重复分析已确认部分。
+- 2026-08-26: “去铺货”沿用旧系统“先选择商户、再绑定设备”的交互语义,并按用户截图直接复用商户经营数据宽表;提交端统一复用 `MerchantLogic::bindDevices()`,不新增第二套绑定事务。
+- 2026-08-27: 完成代理设备管理权分配/回收链路;分配只转移 `sa_device.manage_agent_id`,不改变 `belong_agent_id`,已铺货设备继续禁止分配或回收,操作与流水写入保持同一事务。
+- 2026-08-27: 执行商户营业时间幂等迁移,`sa_merchant.business_time_des` 已落库;“免费店员”仍因缺少数据模型和明确业务规则而保持待定义状态。
+- 2026-08-27: 完成代理“修改信息”页及 `GET /core/agent/read`、`PUT /core/agent/update`;产品分润动态联动,修改范围受直属层级权限约束,手机号变更时检查冲突。列表“更多设置”中的合同管理已接入真实页面与读写接口。
+- 2026-08-27: 代理列表“更多设置 → 删除代理”已改为二次确认后调用 `POST /core/agent/destroy`。按旧系统语义删除仅禁用账号而不物理删除历史;后端会校验操作者可见范围,并拒绝仍有关联设备、商户或下级代理的账号。权限迁移 `20260827_add_agent_destroy_permission.sql` 已执行并确认新增 `core:agent:destroy` POST 权限。
+- 2026-08-27: 代理列表的“广告权限”占位入口已移除;“商户管理”改为跳转 `/merchant/subordinate-merchant?agent_id=<代理ID>`。下级商户页会同步路由中的代理 ID 到筛选条件,后端继续按当前账号可见范围校验该参数。
+- 2026-08-27: 代理列表“更多设置 → 设备绑定”已接入精简编号输入弹窗,交互按参考图使用英文逗号分隔设备编号;提交复用既有 `POST /core/agent/device/assign`,因此沿用代理可见范围、当前管理权、未铺货及单次 100 台的后端限制。相关文件已通过 ESLint、`vue-tsc --noEmit` 和 Vite 生产构建(3183 modules;仅保留既有系统菜单动态/静态导入警告)。
+
+## Latest change notes
+
+- 2026-09-02: 完成“用户管理 → 用户投诉 / 用户反馈”真实业务链路。用户投诉复用 `sa_wechat_mini_program` 的微信支付配置,通过 API v3 提供配置选择、最近 31 天分页查询、详情、敏感手机号解密、协商记录、回复和完结;配置错误会明确返回,不伪造投诉数据。用户反馈新增 `sa_user_feedback`、`sa_user_feedback_reply`,支持组合筛选、状态数量、详情、回复流水、后台备注、状态处理和 XLSX 导出;普通管理员按本人及递归下级代理范围查询,超管查看全部。迁移 `server/plugin/saiadmin/db/20260902_add_user_complaint_feedback.sql` 已连续执行两次并清除菜单缓存,11 个接口权限已落库。
+
+- 2026-09-02: 完成“用户管理 → 用户列表 → 余额充值”管理端代充链路。`sa_rental_user` 已有 `wallet_balance` 列无需加列;新建资金账本表 `sa_rental_user_wallet_log`(为 card/card_gift 预留 `balance_type`,本轮仅支持 wallet)。新增 `POST /core/user-management/users/recharge`,由 `RentalUserController::recharge` → `RentalUserLogic::recharge` 在事务里锁定用户行 → 增量更新 `wallet_balance` → 写流水;操作人 ID/姓名来自 `BaseController::$this->adminId/adminInfo`。扣减时禁止减为负数(余额不足则抛 `ApiException`),amount=0/缺字段均抛业务错误。前端 `user-list/index.vue` 弹窗**保持原状**(仅金额输入框),`confirmBalanceRecharge` 改为真实 `await rentalUserApi.recharge(...)`,成功后刷新列表;`showPendingAction` 仍保留给“设置会员”和“储值卡充值”按钮。后端接口允许 `balance_type` 缺省(默认 wallet)和 `remark` 缺省(默认空串)——前端不暴露任何新 UI 字段。可见范围复用现有 `accessibleManagerIds()` + `assertUserAccessible()`,谁能看谁就能充。新增菜单 `RentalUserRecharge`(slug `core:user:list:recharge`,method POST,sort 100),按 `UserList` 角色传播;当前尚无角色持有 `UserList`,故本次新增菜单未挂在普通角色上,超管 ID 1 不受影响。迁移脚本 `server/plugin/saiadmin/db/20260902_add_rental_user_wallet_log.sql` 已执行两次(幂等)。
+
+- 2026-09-04: 完成“用户列表 → 储值卡充值”。按旧系统 `Business::invest_user_accout(type=0)` 语义,管理端只调整储值卡赠额:旧 `wash_gift_balance` 映射到新 `card_gift_balance`;`card_balance` 代表用户实付本金,不开放人工修改。前端新增显示目标用户、当前赠额、正负金额和备注的真实弹窗,调用现有 `POST /core/user-management/users/recharge` 并传 `balance_type=card_gift`。后端用显式白名单将 `wallet/card_gift` 映射到余额列,保留事务、行锁、可见范围和操作人流水,并新增金额最多两位小数、DECIMAL 上限、0 元拒绝、超额扣减拒绝和备注长度校验。新项目未发现可复用的 NFC 卡同步服务;若恢复实体卡绑定,应新增事务提交后的异步同步,不在数据库事务内调用外部厂商。
+
+- 2026-09-04: 完成“资金管理 → 退押管理”真实列表与 XLSX 导出。旧系统语义来自 `agentapi/Order::withdaw_deposit_orders()`、`common/model/UserOrder::queryDepositOrders()` 和 `common/service/Task::addDepositOrderTask()`:退押是独立任务,退款额为押金减消费,计划退款时间、退款路径和任务状态不能从普通订单退款字段猜测。新系统新增 `sa_deposit_refund_record`(迁移 `20260904_add_deposit_refund_record.sql`),新增 `GET /core/fund-management/refund-management/index` 与 `POST /core/fund-management/refund-management/export`,支持状态、来源、退款类型、订单号、用户 ID 和申请日期筛选,并按“本人 + 递归下级管理代理”隔离数据。导出最多 50000 条,文本字段做 Excel 公式注入防护。原静态页面已替换为真实分页、汇总和下载。权限迁移 `20260904_add_deposit_refund_permissions.sql` 已重复执行验证幂等,菜单 ID 227/228 已落库并清理菜单/接口权限缓存。测试数据脚本 `20260904_seed_deposit_refund_test_data.sql` 仅关联既有 `TEST-ORDER-20260901-*` 订单;正式订单完结/支付回调后续必须按该表字段契约写入退押任务,本轮未伪造尚不存在的支付退款执行链路。
+
+- 2026-09-04: 完成“资金管理 → 充值记录”真实列表与 XLSX 导出。新增 `GET /core/fund-management/recharge-records/index` 和 `POST /core/fund-management/recharge-records/export`,列表与导出共用充值类型、变动方向、用户 ID、昵称及日期范围筛选,并复用用户列表的“本人 + 递归下级”订单用户可见范围。导出不受分页限制,超过 50000 条要求缩小筛选,文本字段做 Excel 公式注入防护。页面已移除静态演示数据,展示增加、扣减、净变动汇总、变动前后余额、操作人和备注。当前新库只有已提交的后台余额流水 `sa_rental_user_wallet_log`,且无新版用户支付单表,因此本页暂不展示微信/支付宝未支付充值单;待迁移实付充值下单与回调链路后再合并。权限迁移 `20260904_add_recharge_record_api_permissions.sql` 已重复执行验证幂等,菜单 ID 224/225 已落库并清理菜单/接口权限缓存;当前普通角色未持有充值记录页权限,故角色继承关联为 0。
+
+- 2026-09-01: 用户决定暂缓商户登录体系设计。当前添加商户会把手机号和密码哈希保存到 `sa_merchant`,但 `/core/login` 只认证 `sa_system_user`,因此商户暂时没有登录入口;在明确“独立商户端”或“共用管理后台”前不要同步创建后台账号,也不要新增临时登录接口。
+- 2026-09-01: “用户管理 → 用户列表”正在由静态演示改为真实数据链路,旧业务参照 `api/application/agentapi/controller/Search.php::query_users()` 和 `common/model/User.php::getPageUserList()`:用户资料独立存储,租借次数与消费金额从订单汇总。已新增 `sa_rental_user` 模型/逻辑/控制器、`GET /core/user-management/users/index`、`GET /core/user-management/users/orders`、前端 API 与真实分页页面;支持微信/支付宝、用户 ID、昵称、手机号筛选及用户订单记录。非超级管理员范围按“本人 + 递归下级”的 `manage_agent_id` 订单关系限制。资金充值和会员设置涉及资金账本、支付日志与商户会员规则,本轮未伪造实现,原静态操作入口已从用户列表移除。
+- 2026-09-02: “用户管理 → 用户列表”数据库与运行时验证已完成。已在本地数据库依次执行并重复验证 `20260901_add_rental_user.sql`、`20260901_add_rental_user_permissions.sql`、`20260901_seed_rental_users_from_test_orders.sql`;`sa_rental_user` 共 19 列,30 个测试订单用户全部同步,重复执行未产生重复数据或权限。使用管理员令牌真实请求用户分页、用户订单以及来源、用户 ID、昵称、手机号筛选均返回业务 `code=200`;微信/支付宝各 15 人。`UserList` 当前没有普通角色关联,因此新增订单权限也没有可继承的角色关联;超级管理员可正常使用,给普通角色开放页面时需同时授权 `core:user:list:index` 与 `core:user:list:orders`。前端 `vue-tsc --noEmit` 与 Vite 生产构建通过(3193 modules),仅保留既有 system menu 动静态导入提示。
+
+- 2026-09-01: “下级订单”已由 `<OrderList empty />` 静态占位改为真实分页页面,新增 `GET /core/order/subordinate/index` 与 `/read`。权限范围与下级设备保持一致:超级管理员查询所有非自身管理人订单;普通账号根据 `sa_system_dept.level` 查询递归后代部门账号,并以 `sa_rental_order.manage_agent_id` 限定列表和详情。页面支持设备分类、状态汇总、代理 ID、订单、用户、商户、设备、交易号、来源、支付和日期筛选,并展示管理代理信息。权限迁移 `20260901_add_subordinate_order_api_permissions.sql` 已在本地 `water` 库执行;30 条测试订单当前分布为管理人 ID 1 共 6 条、ID 3 共 12 条、ID 10 共 12 条,便于验证本人/下级隔离。相关 PHP 文件通过语法检查,前端通过 Prettier、`vue-tsc --noEmit`,生产构建产物已更新。
+
+- 2026-09-01: 完成“订单管理 → 我的订单”真实数据基础链路。旧业务参照 `api/application/agentapi/controller/OrderSearch.php::query_orders_new()` 与 `Order.php::user_order_detail()`;新系统只查询 `sa_rental_order`,不调用旧项目接口。新增 `GET /core/order/my/index` 和 `/read`,支持设备分类、状态、订单号、用户、商户、设备、交易单号、来源、支付和日期筛选,并按 `manage_agent_id` 隔离(管理员 ID 1 查看全部)。迁移 `20260901_add_rental_order.sql`、`20260901_add_my_order_api_permissions.sql` 已执行于本地 `water` 库并确认订单表存在。测试脚本 `20260901_seed_rental_order_test_data.sql` 已执行,写入 6 个页面分类各 5 条、共 30 条可重复清理的 `TEST-ORDER-20260901-` 前缀订单;不包含真实订单数据。
+
+- 2026-08-31: 本轮设备模块完成记录:下级设备页已由静态演示改为按部门后代范围的真实列表,完成归属详情、车载供电/断电、二维码预览/下载、解绑以及单台/批量回收。下级设备接口为 `/core/device/subordinate/index`、`/read`、`/power`、`/unbind`;解绑权限迁移已在本地 `water` 库执行。回收复用既有 `/core/agent/device/recover`,只回收未绑定商户设备。已写入本地测试数据脚本 `20260831_seed_subordinate_device_test_data.sql`,前缀为 `TEST-SUBDEV-`;初始可回收设备为 ID 13、15,已绑定解绑测试设备为 ID 14。我的设备工具栏新增“设备二维码”,二维码页面已接入当前账号管理的真实设备,支持筛选、预览及单个/批量 SVG 下载;二维码内容遵循 `DEVICE_QR_BASE_URL + device_code`(未配置时直接使用机器码)。
+
+- 2026-08-31: 完成“下级设备”真实数据链路:新增 `GET /core/device/subordinate/index`、`GET /core/device/subordinate/read` 和 `POST /core/device/subordinate/power`。可见范围通过 `sa_system_dept.level` 的后代部门计算,并仅返回这些代理当前由 `sa_device.manage_agent_id` 管理的设备;设备归属 `belong_agent_id` 只用于展示,不作为越权依据。页面已移除静态演示数据,归属弹窗展示归属代理、当前管理者与商户绑定信息;供电/断电仅对 `goods_type=107` 车载设备显示,沿用既有 `/cdy/tasks` 指令协议和 `sa_device_command_log` 审计记录。权限迁移位于 `server/plugin/saiadmin/db/20260831_add_subordinate_device_operations.sql`,需在目标数据库执行后非超管角色才能调用新接口。
+- 2026-08-31: 已在本地 `water` 数据库执行 `20260831_add_subordinate_device_operations.sql`,并复核三条下级设备接口权限均已写入。当前 `SubordinateDevice` 菜单没有角色关联,因此没有可继承的非超管角色授权;超级管理员不受该角色映射影响。
+- 2026-08-31: 下级设备页补充二维码与解绑:二维码复用 `subordinate/read` 返回的 `qr_content` 并用 `qrcode.vue` 渲染;解绑使用新接口 `POST /core/device/subordinate/unbind`,按下级设备范围校验后复用原有解绑事务、解绑流水和商户设备数同步。已在本地 `water` 库执行补充权限迁移 `20260831_add_subordinate_device_unbind_permission.sql`;当前已绑定测试设备为 `TEST-SUBDEV-POWER-001`(ID 14),可用于解绑验证,重新执行测试数据脚本即可恢复该绑定。
+- 2026-08-31: 下级设备页补充单台/批量回收,前端复用既有 `POST /core/agent/device/recover` 和 `AgentLogic::recoverDevices()`,按设备当前管理代理分组回收。回收只转移 `manage_agent_id` 回当前账号并写 `sa_device_manager_record`;已绑定商户的设备被前端排除且后端仍会强制拒绝。无需新建回收事务或迁移;接口权限 `core:agent:device:recover` 已由既有代理设备分配迁移提供。测试设备 ID 13、15 可回收,ID 14 因已绑定不能回收。
+- 2026-08-31: 完成“我的设备 → 设备二维码”真实页面:`/device/qrcode` 复用 `GET /core/device/index`,继承当前管理权范围和设备类型/绑定/二维码/SN 筛选;列表显示真实机器码、商户和二维码内容,并支持预览、单个及批量 SVG 下载。`DeviceLogic::formatDevice()` 统一返回 `qr_content`,规则与设备详情一致:配置 `DEVICE_QR_BASE_URL` 时拼接机器码,未配置时直接使用机器码。我的设备工具栏已增加“设备二维码”入口;无新增数据库迁移。
+
+- 2026-08-25: Added `saiadmin-artd/src/router/modules/merchant.ts` with routes for `/merchant/my-merchant`, `/merchant/subordinate-merchant`, `/merchant/add-merchant`, and `/merchant/statistics`.
+- 2026-08-25: Registered the merchant route module and made backend-menu mode supplement/merge these routes when the server menu has not been synchronized yet. This fixes direct navigation to `/merchant/statistics?id=73` being redirected to the dashboard.
+- 2026-08-25: Verified `http://localhost:4173/#/merchant/statistics?id=73` remains on the requested URL and renders the `数据统计` page with merchant data.
+- 2026-08-25: 商户列表接口新增 `status`、`profit_method` 筛选和代理类型 `summary` 统计;下级商户页面已接入代理 ID、状态、分成方式搜索,分类切换和关键词搜索均复测成功。
+- 2026-08-25: 重启 8787 Webman 进程加载商户列表后端改动;浏览器实测下级商户分类显示全部 65、城市合伙人 22、代理商 22、业务员 21,名称搜索可缩小到 1 条结果。
+- 2026-08-26: 商户添加/编辑页地图由 OpenStreetMap iframe 替换为高德 Web JS API;支持地址搜索定位、地图点击逆地理编码、地址/地区回填,以及编辑时按已保存地址回显。高德凭据仅放在被 Git 忽略的本地 `.env.local` 中。
+- 2026-08-26: 修复高德地图加载失败:高德脚本 URL 补充 `AMap.Geocoder` 插件,避免初始化地理编码器时报错。
+- 2026-08-26: 新增 `/core/device/index`、`/core/device/export`、设备 API 封装和 `views/device/my-devices/index.vue` 真实数据页面;执行设备测试数据和权限 SQL,并通过商户绑定接口将 `TEST-VEHICLE-0001` 绑定到演示商户,用于验证商户搜索及绑定状态筛选。
+- 2026-08-26: 参照旧项目 `bind_device_records` 完成铺货记录页:新增 `/core/device/distribution-record/index`、`/core/device/distribution-record/export`、独立前端 API/页面、设备菜单及导出权限;“我的设备”和“下级设备”的铺货记录按钮改为打开真实页面。
+- 2026-08-26: 完成“去铺货”页面 `/device/distribute`。旧入口 `GET agentapi/add_bind_store`、`POST agentapi/save_bind_store`、`Store::bind_device_store()` / `save_bind_store()` 与 `Store::bindZuo2Store()`,在新项目分别映射为商户经营数据列表、`DeviceBindingDialog`、`POST /core/merchant/device/bind` 和 `MerchantLogic::bindDevices()`。页面按旧系统截图展示门头照、商户名称/地址、金额、订单量、设备数、使用率、客单价、上级代理、分润人、分成方式及右侧操作区;点击“设备绑定”输入或扫码 SN 完成铺货。
+- 2026-08-26: 去铺货入口已接入“我的设备”和“下级设备”。从“我的设备”勾选未绑定设备进入时通过查询参数预填 SN;不勾选时可在商户行点击“设备绑定”后手工录入。商户候选复用 `GET /core/merchant/index`,当前绑定列表复用 `GET /core/merchant/devices`,绑定结果继续区分成功、已绑定当前商户、已绑定其他商户、设备不存在和无管理权。
+- 2026-08-26: 新增并执行幂等菜单迁移 `server/plugin/saiadmin/db/20260826_add_device_distribution_page.sql`,`DistributeDevice` 菜单唯一;新增前端设备路由模块,并在后端菜单尚未同步时由 `MenuProcessor` 合并设备子路由。
+- 2026-08-27: 接续完成代理设备分配半成品:`AgentLogic`/`AgentController` 提供代理列表、导出、可分配库存、分配、回收和流水接口;代理列表改用真实 API,接入 `DeviceAssignmentDialog` 与 `AssignmentRecordsDialog`;新增 `/device/assign` 页面,并让“我的设备 → 去分配”携带已勾选未铺货设备 SN。
+- 2026-08-27: 已执行幂等迁移 `server/plugin/saiadmin/db/20260826_add_agent_device_assignment.sql`,数据库现有 `sa_device_manager_record`、唯一 `AssignDevice` 页面菜单及 6 个代理相关接口权限。
+- 2026-08-27: 新增并执行两次幂等迁移 `server/plugin/saiadmin/db/20260827_add_agent_profile.sql`,数据库已有 `sa_agent_profile`、隐藏 `AgentEdit` 页面及 `core:agent:read/update` 权限。前端新增 `/agent/edit-agent?id=...`,后端菜单模式会合并代理内置路由。
+- 2026-08-27: 完成代理合同管理迁移:新增 `/agent/contract?id=...` 隐藏页、`GET/PUT /core/agent/contract`、`sa_agent_contract` 一对一附件表及幂等权限迁移 `server/plugin/saiadmin/db/20260827_add_agent_contract.sql`。三组图片字段分别为 `contract_urls`、`identity_urls`、`extra_urls`,每组最多 20 张;读写均复用代理可见范围校验。
+
+## Verification
+
+- 2026-09-04: 退押管理通过 PHP 语法和 Webman 路由加载检查;本地库 3 条测试任务覆盖待退、已退、退款异常,汇总应退 43.90、已退 12.00、未退 31.90,已退状态筛选返回 1 条,管理员 ID 3 的自身范围返回 2 条。导出生成 5331-byte XLSX,文件签名 `PK 03 04` 正确且包含“订单号”“退款金额”表头,临时导出文件已清理。前端文件通过 Prettier、ESLint、Stylelint、`vue-tsc --noEmit` 和 Vite production build(3197 modules,仅既有 system menu 动静态导入提示)。
+
+- 2026-09-04: 充值记录列表/导出通过 PHP 语法和 Webman 路由加载检查;真实库 7 条流水查询为 wallet 7、card_gift 0,汇总为增加 160.00、扣减 20.00、净变动 140.00,最新记录的用户、操作人和前后余额映射正确;按 wallet 筛选生成 5437-byte XLSX,文件签名 `PK 03 04` 正确,临时导出文件已清理。前端文件通过 Prettier、ESLint、Stylelint、`vue-tsc --noEmit` 和 Vite production build(3197 modules,仅既有 system menu 动静态导入提示)。
+
+- 2026-09-04: 储值卡赠额充值改动通过 3 个 PHP 文件 `php -l`,后端 `php webman route:list` 确认 `POST /core/user-management/users/recharge` 正常加载;前端相关文件通过 Prettier、ESLint、Stylelint 和 `vue-tsc --noEmit`,Vite production build 通过(3196 modules,仅保留既有 system menu 动静态导入提示)。本次未对真实用户执行入账,未产生测试资金流水。
+
+- 2026-09-02: 投诉/反馈新增 PHP 文件及路由全部通过 `php -l`;前端相关文件通过 Prettier、ESLint、`vue-tsc --noEmit` 和 Vite production build(3196 modules)。真实 HTTP 回归:反馈筛选列表、详情、状态更新、回复均返回 200,回复流水写入且状态一致;导出返回 HTTP 200、XLSX MIME、4835 bytes;测试数据和回复已清理。投诉配置接口返回 200,当前测试占位私钥查询投诉时返回明确的“微信支付商户私钥格式错误”,验证配置校验和错误透传生效。
+
+- 2026-09-02: 余额充值回归测试通过:表 `sa_rental_user_wallet_log` 12 列 5 索引落库,菜单 `RentalUserRecharge`(slug `core:user:list:recharge`,method POST,sort 100,id 212)落库,迁移两次幂等。`RentalUserController` / `RentalUserLogic` / `WalletRechargeValidate` 通过 `php -l`;前端 `vue-tsc --noEmit` 通过;Vite production build 通过(39.08s,仅保留既有 system menu 动静态导入提示)。真实 HTTP 自测:测试用户 900005(balance 100)→ POST +50 → 150 → POST -20 → 130 → POST +50 → 180(首轮带 remark 字段)→ 回滚后再 POST 不带 balance_type/remark +30 → 210 → POST 带 balance_type=wallet+remark +10 → 220;列表查询 `wallet_balance=220` 复核;流水表 5 行 direction 1/-1/1/1/1、before/after 全程一致、operator_id=1/operator_name=admin、source=admin_recharge;负数超额扣款返回“余额不足,无法扣减”,余额未变;amount=0 返回“充值金额不能为 0”;balance_type=card 返回“账户类型不支持”;user_id=0 返回“请选择有效用户”;缺 user_id 返回“请选择用户”;**回滚后不带 balance_type 仍 200**,证明 balance_type 已从必填改为可选。前端 user-list/index.vue 弹窗模板回滚到原状,仅金额输入框,未增加任何字段。Webman 清理 supervisor (PID 29800 / 28492 / 3832) 后启动单实例(PID 15804)加载新代码(Windows 不启用文件监控);自测过程未污染正式用户密码或凭据,仅复用现有 admin 令牌。
+
+- 2026-09-02: 用户列表三份幂等迁移已执行两次并通过结构、权限和数据数量复核;真实 HTTP 验证用户总数 30、首页 10 条、首个用户订单明细 1 条,用户 ID/昵称/手机号筛选均精确返回 1 条,来源筛选微信/支付宝各 15 条且行数据来源一致。相关 PHP 文件与路由通过 `php -l`;前端 `vue-tsc --noEmit` 和 Vite production build 通过(3193 modules transformed)。
+- 2026-09-02: 用户列表页面按参考截图完成视觉调整:筛选栏新增“用户画像”,用户 ID 与昵称同行显示,移除截图中不存在的状态列,金额统一为押金两位小数及其他金额带 `¥` 的格式;操作区恢复“订单记录/设置会员/余额充值/储值卡充值”四个按钮并按两列排列。订单记录沿用真实接口,用户画像跳转 `/user-management/avatars`,会员与充值按钮暂提示尚未接入,避免虚构资金业务;页面已通过 Prettier、ESLint、`vue-tsc --noEmit` 与 Vite production build(3193 modules transformed)。
+- 2026-08-31: 下级设备改动已通过 `php -l`(DeviceLogic、DeviceController、route.php)、项目内 `vue-tsc --noEmit` 和 Vite 生产构建(3188 modules transformed)。Vite 仅报告既有 system menu 编辑弹窗的动静态导入提示;未直接执行数据库迁移或向真实设备网关下发指令。
+- 2026-08-31: 二维码/解绑增量已再次通过相关 PHP 文件 `php -l` 与项目内 `vue-tsc --noEmit`;本地数据库已确认解绑权限菜单 ID 208 和测试设备 ID 14 的有效商户绑定。
+- 2026-08-31: 回收功能前端已通过项目内 `vue-tsc --noEmit` 与 Vite 生产构建;既有 `AgentLogic.php` 再次通过 `php -l`。
+- 2026-08-31: 二维码页改动已通过 `DeviceLogic.php` 的 `php -l` 和项目内 `vue-tsc --noEmit`;Vite 构建已启动并完成模块转换,继续保留项目既有 system menu 动静态导入提示。
+
+- Last checked: 2026-09-02
+- Frontend `vue-tsc --noEmit`: passed via the project-local binary.
+- Frontend Vite production build: passed; 3152 modules transformed.
+- Merchant PHP controller/logic: `php -l` passed after list summary and filter changes.
+- Browser verification: `/merchant/subordinate-merchant` loaded without console errors; agent-type tab switching and merchant-name search returned backend-filtered results.
+- Backend selected PHP files: `php -l` passed.
+- 2026-08-26: 营业时间相关 PHP 控制器、逻辑、验证器和路由 `php -l` 均通过;前端使用项目内 `vue-tsc` Node 入口执行类型检查无输出错误。由于当前 8787 未监听,新增接口尚未进行运行时请求验证;数据库迁移也尚未执行。
+- 2026-08-26: 数据统计接口验证成功:`GET /core/merchant/statistics?id=2&start_date=2026-08-01&end_date=2026-08-03` 返回业务 `code=200`、3 天明细、交易额 `375.90`、收益 `67.66`、订单 48;`POST /core/merchant/statistics/export` 返回 HTTP 200,导出内容 5064 bytes。
+- Local `GET http://127.0.0.1:8787/core/captcha`: HTTP 200, business code 200, image payload returned with matching JPEG MIME/signature after the fix.
+- Local cross-origin captcha request with an `Origin` header: HTTP 200 with `Access-Control-Allow-Origin` and JPEG data.
+- Local login preflight: OPTIONS HTTP 200; invalid-code POST HTTP 200 with the expected business error and CORS header.
+- `pnpm` command: not available on PATH, so package scripts were not invoked directly.
+- 2026-08-26: Real HTTP self-test passed: POST `/core/merchant/save` created temporary merchant ID 74; GET `/core/merchant/read` returned submitted fields without `password`; name-filtered GET `/core/merchant/index` found 1 row; duplicate phone was rejected; DELETE `/core/merchant/destroy` removed the temporary record and the follow-up list query returned 0 rows.
+- 2026-08-26: Frontend `vue-tsc --noEmit` passed after adding the profit-rate input.
+- 2026-08-26: Frontend Vite production build passed after the add-merchant form update; 3155 modules transformed. Vite reported the existing static/dynamic import warning for the system menu edit dialog.
+- 2026-08-26: AMap map integration passed `vue-tsc --noEmit` and Vite production build (3155 modules). Browser automation verification was unavailable because the local Node runtime is v22.21.0 while the browser tool requires v22.22.0 or newer.
+- 2026-08-26: Confirmed local `.env.local` contains both AMap variables without recording their values; `vue-tsc --noEmit` passed and Vite build completed successfully after the Geocoder plugin fix.
+- 2026-08-26: “我的设备”PHP 控制器、逻辑和路由均通过 `php -l`;前端项目本地 `vue-tsc --noEmit` 和 Vite production build 均通过(3156 modules,保留既有 system menu 动静态导入警告)。
+- 2026-08-26: 真实 HTTP 自测通过:全量 12 条、车载 2 条、充电设备 10 条、在线 7 条、离线 5 条、SN/二维码/类型搜索均命中预期;绑定 1 台测试设备后,已绑定 1 条、未绑定 11 条、商户名称搜索 1 条。`POST /core/device/export` 返回 HTTP 200、正确 XLSX MIME 和 5984 bytes 文件。
+- 2026-08-26: 铺货记录 PHP 控制器、逻辑、路由通过 `php -l`,前端 `vue-tsc --noEmit` 和 Vite production build 通过(3160 modules,只有既有 system menu 动静态导入警告)。
+- 2026-08-26: 铺货记录真实 HTTP 自测通过:测试流水共 4 条(铺货 3、解绑 1),操作类型筛选分别返回 3/1 条;第二家商户、`PB000001` 二维码、`TEST-POWER-0001` SN、设备类型 101 均精确返回 1 条;分页和空结果正确。解绑筛选导出返回 HTTP 200、XLSX MIME、4941 bytes。当前 12 台测试设备中 2 台已绑定、10 台未绑定。
+- 2026-08-26: 去铺货真实 HTTP 自测通过:候选商户和未绑定设备列表可用;将 `TEST-PASSBT-0002` 成功铺货到商户 ID 75 后,重复提交归入 `has_binded_sn`,改投其他商户归入 `binded_other_store_sn`,不存在 SN 归入 `not_find_sn`;通过直接调用同一业务逻辑并注入代理 ID 2 验证无管理权设备归入 `not_owned_sn`。成功后未绑定候选从 10 降为 9,商户 `device_count` 从 0 增至 1、`distributed=1`,且新增一条 `action_type=1` 流水。当前 12 台测试设备中 3 台已绑定、9 台未绑定。
+- 2026-08-26: 去铺货页面改为用户截图对应的商户经营数据宽表后,前端 `vue-tsc --noEmit` 通过,Vite production build 通过(3163 modules);仅保留既有 system menu 动静态导入警告。浏览器自动化仍因本机 Node v22.21.0 低于工具要求的 v22.22.0 未执行。
+- Files examined include root README, frontend package/env/router/login/http/config files, backend composer/env/start/config/process/session/captcha/login/route files, and current Git status/diff summaries.
+- 2026-08-27: 代理相关 PHP 控制器、逻辑、验证器和路由均通过 `php -l`;代理分配相关前端文件通过 ESLint,`vue-tsc --noEmit` 通过;Vite production build 通过(3173 modules),仅保留既有 system menu 动静态导入警告。
+- 2026-08-27: 真实 HTTP 自测通过:代理列表 7 条、初始可分配库存 9 台;1 台测试设备分配成功,额外不存在 SN 正确归入 `not_find_sn`,分配流水可查询;随后回收成功且设备重新回到当前账号的可分配库存。数据库最终仍有 12 台设备由管理员 ID 1 管理;自测保留 1 条分配和 1 条回收审计流水。
+- 2026-08-27: 自测使用的临时 Webman 8787 进程已停止,验证结束后端口未监听。
+- 2026-08-27: 执行 `20260826_add_merchant_business_time.sql` 后通过 `information_schema.columns` 复核 `sa_merchant.business_time_des` 唯一存在。
+- 2026-08-27: 代理修改相关 PHP 逻辑、控制器、验证器和路由通过 `php -l`;相关前端文件通过 Prettier、ESLint、`vue-tsc --noEmit`,Vite production build 通过(3177 modules,仅保留既有 system menu 动静态导入警告)。
+- 2026-08-27: 代理修改真实 HTTP 自测通过:读取代理 101,临时将运营模式改为 employee 并写入测试公司地址,二次读取确认持久化,再恢复为原始 agent/空地址;读取当前登录人 ID 1 返回“代理不存在或无权操作”。当前 Webman 版本对 PUT JSON 存在 `post()` 为空的运行时情况,`AgentController::update()` 已增加 `rawBody()` JSON 兼容读取。
+- 2026-08-27: 合同管理相关 PHP 控制器、逻辑、验证器和路由通过 `php -l`;前端相关文件通过 Prettier、ESLint、`vue-tsc --noEmit`,Vite production build 通过(3180 modules,仅保留既有 system menu 动静态导入警告)。迁移已执行并确认 `sa_agent_contract`、隐藏页和两个权限菜单存在;浏览器实际加载 `/agent/contract?id=101` 成功,显示代理信息、三组上传区和提交按钮,无页面错误。
+- 2026-08-28: 参照 `https://5.catpay.com.cn/equipment/index` 的“我的设备”操作区,完成设备管理、车载设备供电/断电、已铺货设备解绑和二维码:详情展示归属代理、当前管理者及商户绑定信息;供断电仅对 `goods_type=107` 显示并按旧 `/cdy/tasks` switch 协议下发;解绑保留绑定流水并同步商户设备数;二维码由项目已有 `qrcode.vue` 按真实机器码/扫描前缀生成。迁移 `server/plugin/saiadmin/db/20260828_add_device_operations.sql` 已执行,创建 `sa_device_command_log` 并补齐 `core:device:read`、`core:device:power`、`core:device:unbind` 权限。
+
+## 2026-09-11 设备测试批次
+- 用户要求添加设备测试数据,执行 `server/scripts/seed_water_device_dashboard_demo.php --apply`;固定批次 `TEST-WATER-DEVICES-20260911`,已复跑确认不重复写入、不刷新测试心跳或覆盖编辑。
+- 新增12台【测试0911】设备(ID26–37),南山/罗湖/福田/宝安各3台,复用既有标记测试企业及SKU。写入时6在线、2离线(其中1台超过24小时)、2告警、2未接入,版本1.1.0-test/1.2.0-test/1.3.0-test及未登记。
+- 新增298条明确归属测试设备的模拟日用量,覆盖30天;包含零用量和无当日数据,可验证8+4分页、组合筛选、趋势及异常类型。不是实际设备遥测;测试心跳会按15分钟规则自然转离线,不后台自动续期。
+- 脚本事务内确认原设备、库存、权益、配送和收款不变,并核对状态/区域/分页/历史数量。PHP语法、实际写入校验及幂等复跑通过。
+
+## 2026-09-11 智能设备页改版
+- `/water-devices` 按参考图改为独立看板:统计、SN/企业/状态/区域/版本组合筛选、8条分页卡片、设备详情/维护、版本分布、30日真实用水趋势和当前异常表。组件、查询组合函数和状态/筛选后端策略分层,格式化代码。
+- `WaterDeviceDashboard` + `device/DeviceFilters` + `DeviceStatusPolicy` 统一读取和导出,`GET devices/dashboard`、`POST devices/export` 需设备读取权限;写入复用原 WaterBusiness。故障优先,15分钟未上报离线,从未上报未接入;全量统计不随分页/筛选变化。
+- 已执行并复跑 `migrate_water_device_usage.php --apply`,新增按设备/日期唯一用水汇总;复制已有明确日期快照0条。DeviceReport原事务/设备锁内保存日累计最大值,跨日留存、重复不累加;无报告日期为null。没有虚构历史、OTA或故障工单,固件入口只管理版本档案。
+- 45项设备看板(含真实HTTP)及原30项接入检查通过,夹具全部回滚;PHP语法、Prettier/ESLint/vue-tsc、Vite3290模块构建通过。浏览器工具仍报os error3,本次未完成视觉验收。详见 `server/docs/WATER_DEVICE_DASHBOARD.md`。
+
+## 2026-09-11 库存页参考图改版
+- 库存入口改为独立看板:四项全量统计、分规格表格、七日出入库趋势、库存构成环图,保留查询分页、规格维护、入库、盘点、现场换桶和流水;不使用参考图示例数据。
+- `WaterInventory` 读模型新增 dashboard / export 接口,均需库存读取权限;全量统计不受搜索/分页影响,导出包含所有匹配规格。写入复用 WaterBusiness 事务和凭据去重。
+- 空桶含仓库、企业现场、车载待交接;总满桶库存为可用+锁定+在途。预警按启用规格 available<=safety_stock,趋势入库含完好退回、出库按发车,不混入盘点。
+- 50项库存验证(含真实HTTP)和69项业务回归通过,测试数据回滚;PHP语法、前端ESLint/类型/构建检查通过。浏览器工具启动报 os error 3,视觉验收未完成。细节见 `server/docs/WATER_INVENTORY.md`。
+
+## 2026-09-10 库存流水查询与导出
+- 库存管理的“查看库存流水”使用独立组件 `WaterLedgerDrawer.vue`,不再限制最近 100 条;默认每页 10 条,可选 20/50/100 条,支持页码跳转、关键词、操作类型、起止日期、重置和空结果提示。
+- `WaterLedger` 服务统一列表/导出筛选口径:企业名、商品名、关联凭据、说明,日期包含结束日全天;操作类型及中文名称由后端提供。保留原库存/配送九种流水,档案、收款等其他审计不混入。
+- `GET /core/water/ledger` 返回分页及 `kinds`,`POST /core/water/ledger/export` 接收相同筛选,导出当前已查询条件的全部 XLSX(忽略 page/limit),超过 50,000 条明确拒绝。两接口均要求库存读取权限;临时文件位于系统临时目录,响应生成后删除。
+- 验证:PHP lint、ESLint、vue-tsc、Vite build(3250 modules)通过;`php tests/water_ledger_test.php` 30 项、原 `water_business_test.php` 69 项通过,测试数据全部回滚。浏览器现有 20 条流水分成两页;第二页搜索星河返回第一页 1 条,下载 XLSX 解析确认 1 条星河数据加表头,空结果禁用导出,重置恢复 20 条。
+- 2026-09-10:库存流水开始日期、结束日期改为两个独立的单日期选择框,可只填一端或选择跨日范围;查询前提示开始晚于结束,列表与导出继续复用 start_date/end_date。
+- 日期拆分验证:ESLint、vue-tsc、Vite build 通过;浏览器分别选择 2026-09-01 和 2026-09-10 后查询成功,两个日期保持独立。
+
+## 2026-09-10 配送调度中心
+- `/water-dispatch` 已从 WaterWorkspace 普通列表换为独立调度看板:真实片区任务池、日期/关键词/片区筛选、配送员状态、站点地图、路线摘要/完整路线、任务状态分页及当前页 Excel 导出。原企业/库存/预警页面保持共享组件,调度详情跳转带入关键词。
+- 后端新增 `WaterDispatch.php`:看板、配送员档案、每日排班、车辆容量、待派单任务日期/时限/位置、区域和时限优先合单预览及原子确认。任务、库存、权益仍复用 WaterBusiness;完整路线单独查询,避免关键词/日期过滤漏站。
+- 已执行可重入增量迁移 `php scripts/migrate_water_dispatch.php --apply`:新增 driver/driver_shift/route 三张表,企业经纬度和配送计划/路线字段。旧任务姓名电话关联到 2 位既有测试配送员,未推断排班,当前容量使用可编辑默认 100 桶;3 单 24 桶、待派单 1/配送中 2 的状态未改变。
+- `GET /core/water/dispatch/board`、`GET /core/water/dispatch/route` 需要配送读取权限,`GET /core/water/dispatch/plan` 和 POST dispatch 新 action 需要管理权限。合单按容量及相同片区校验,原始自由文本 HTTP 派单不再使用,单笔派单也生成经校验的路线。
+- 地图使用现有高德配置,仅展示登记过的坐标和站序连线。当前模拟企业无坐标,显示真实空状态;没有道路优化、预计到达时间、GPS轨迹或配送员在线心跳。之前“地图合单未实现”的描述由本条取代:区域/容量/时限合单和坐标地图已实现,道路导航优化仍未接入。
+- 后端41项调度测试、69项供水业务、30项库存流水、30项设备接入共170项通过,新增测试记录全部回滚;ESLint、vue-tsc、Vite构建通过。浏览器确认3单24桶,搜索智配办公后任务池/路线/明细同为1单6桶,排班档案和新增表单可用,无候选方案禁用合单确认。细节与回退见 `server/docs/WATER_DISPATCH.md`。
+- 最终浏览器补充验证:配送员新增表单包含姓名、电话、片区、车辆容量;任务详情跳转补货预警自动带入智配办公,显示1条账户且关联已有配送。最终页面保持原供水侧栏样式,显示当日未排班2人和当前页Excel导出入口。
+
+## 2026-09-10 高德地图实际接入
+- 配送地图不再因无任务坐标提前退出:始终初始化真实高德底图,无坐标默认全国视野,有坐标按路线展示编号站点及顺序连线,含比例尺、缩放、查看全部站点、点选任务和失败重试。
+- `src/utils/waterAmap.ts` 统一供水地图和选点弹窗的脚本/插件加载,复用既有高德脚本标识与本地 VITE_AMAP_KEY/VITE_AMAP_SECURITY_CODE,配置值不记入文档。
+- `DispatchLocationPicker.vue` 为配送计划增加地址搜索及地图选点,确认回填后保存才写入配送任务坐标;保存后切换到对应任务组展示。后端沿用已有 schedule 校验,企业地址和库存/权益不受影响。
+- 浏览器实际确认全国高德底图、比例尺/缩放正常;上海市浦东新区世纪大道1号地址解析和标记正常,坐标回填成功;验证后取消计划,未把验证坐标保存进模拟任务。不存在道路导航或实时GPS轨迹接入。
+- 最终 ESLint、vue-tsc 和 Vite production build 均通过;已刷新4173预览加载高德地图版本。
+
+## 2026-09-10 深圳地图测试坐标
+- 用户要求以南油第四工业区4栋出发、送往中洲大厦;目的地按南山区海德一道中洲控股中心处理(已询问具体大厦,当前按此默认)。两个地址经项目高德 Geocoder 查询:出发 GCJ-02 113.924140,22.522806;送达 113.935124,22.516070。
+- 已执行 `php scripts/seed_water_shenzhen_coordinates.php --apply`,仅给任务9/10/11及其对应3个【测试0910】企业更新地址、深圳南山区片区和坐标;任务新增 origin_address/origin_longitude/origin_latitude 记录出发点,结构由原 dispatch 增量迁移扩展。
+- 三条测试任务使用同一出发点及目的地,保留原8/6/10桶、待派单/配送中/配送中状态,未额外建单或修改库存、权益、收款。脚本事务内校验业务表和非位置字段保持一致,固定批次标记复跑不覆盖后续用户修改;原测试行快照位于 server/runtime/water-shenzhen-map-before.json。
+- 地图新增橙色“起”标记,单任务起点至送达点及同路线按站序绘制示意连线;路线卡和详情显示出发地址。未调用道路导航服务。
+- 脚本 PHP lint 和41项配送回归检查通过,脚本已复跑确认 replayed=true。
+- 深圳坐标最终验证:ESLint、vue-tsc、Vite构建通过;浏览器确认深圳南山区3单24桶、橙色起点与蓝色送达点均显示,起止示意连线及出发地址正常。
+
+## 2026-09-10 高德驾车线路规划
+- 已替换 DispatchMap 的直线示意:单任务或同一真实路线具有唯一出发点及完整送达坐标时,通过 AMap.Driving 按既有站序绘制实际道路,显示预计公里/分钟(不含配送停留),支持重新规划。此前“未接入道路导航”的记录由本条更新;尚无实时GPS及各站承诺到达时间。
+- waterAmap.ts 独立加载驾车插件,每次最多16个途经点,超长路线分段并汇总,保留全部站序;连续重复位置去重,部分失败不展示不完整线路。缺坐标、多起点、未成路线的多任务组明确提示;切换时丢弃旧结果,真实路线等待完整站点接口后才规划。
+- 验证:ESLint、vue-tsc、Vite build 通过;node scripts/test-water-driving.mjs 覆盖分段不漏站、汇总、重复坐标、旧请求及服务失败。浏览器实测南油第四工业区4栋至中洲控股中心道路约2.0公里,初次11分钟、重新规划10分钟,用时随路况变化。本次未更改订单或库存等业务数据。
+
+## 2026-09-10 新增两条深圳测试路线
+- 执行 server/scripts/seed_water_shenzhen_routes.php --apply,固定批次 TEST-WATER-SZ-ROUTES-20260910;重跑 replayed=true,不覆盖用户后续变更。新增任务69/路线13(科技园4桶)、任务70/路线14(生态园10栋506室6桶),状态已派单,分别关联独立测试企业和已排班测试配送员。
+- 同一起点为南油第四工业区4栋 GCJ-02 113.924140,22.522806。高德 Geocoder 查询科技园113.946040,22.544610;生态园113.952743,22.530351为园区级返回值,地址明确标注暂定位园区,未声称精确到10栋或506室。
+- 仅新增10桶测试权益,无收款订单;调用原业务创建配送与路线提交,测试SKU可用库存减10、锁定加10,测试账户权益同步冻结。事务校验原任务/收款不变、路线及权益一致。当前看板5单34桶(待派单1/已派单2/配送中2)。PHP lint、实际写入校验及幂等复跑通过。
+- 浏览器验证两条独立路线均返回实际道路:科技园约5.3公里/28分钟,生态园园区约4.8公里/17分钟(当次路况估计);关键词搜索对应1单,已恢复全部列表。
+
+## 2026-09-10 配送优化策略与Eloquent模型
+- 用户明确要求最短距离、最快送达、时限优先,以及模型数据访问。本次供水业务整体切换Eloquent:model/water独立模型及关联,WaterBusiness/WaterDispatch/WaterLedger/WaterRoutePlanning、供水权限与选项读取、设备报告均不再使用Think ORM或Db::table;框架原登录和其他模块不迁移。测试及调用业务服务的种子脚本统一Eloquent事务连接。
+- service/routing 的 RouteStrategy 与三个具体策略,以有向高德道路矩阵计算固定批次站序;最多8站穷举,固定共同出发点、不返仓。最短距离按总里程,最快送达按总完成用时,时限策略按超时站数→超时总秒数→总用时。含可配置每站配送停留,无时限时按用时比较,不代表跨车队全局最优或未来路况预测。
+- 配送页面新增“线路优化”,合单预览各批新增“选择策略并优化”;可查看优化前后指标、每站到达/完成时间、超时和道路地图。仅尚未出库的已派单路线可应用;待派单批次在合单确认时应用。配送计划补齐出发地址与坐标选点。
+- POST dispatch新增 optimize / optimize_apply,plan_commit各组支持optimization_token。后端重新读取模型校验坐标并执行策略,预览绑定操作员、任务指纹,10分钟过期;保存时再次校验状态、站序、日期,行锁及原库存/权益逻辑保持一致,重复提交幂等。路线optimization保存策略及估计,地图以保存策略重新调用高德。
+- 已执行 scripts/migrate_water_routing.php --apply,仅新增sa_water_route.optimization和sa_water_route_preview。道路成本由浏览器JS API提供,后端校验格式与任务匹配,不独立核验高德来源;仅作为调度估计,不驱动扣款或库存数量。
+- 本机PHP需原生mbstring。Windows supervisor已用项目脚本重启并启用扩展;scripts/start_water.ps1及windows.php向子进程传递config/php/mbstring.ini,不修改全局php.ini。CLI回归使用php -d extension=mbstring tests/文件名。
+- 194项后端回归通过(业务69、调度41、流水30、设备30、策略/模型24),测试数据全部回滚;前端ESLint、vue-tsc、道路矩阵与取消/配额错误测试通过。浏览器实测三种策略并给生态园测试路线14保存时限优先,仍5单34桶,状态不变。现有测试路线为单站,多站不同排序由三站集成测试覆盖。详细契约及限制见server/docs/WATER_ROUTING_STRATEGIES.md。
+- 最终Vite构建通过,浏览器刷新后显示路线14已保存“时限优先”,仍5单34桶;Windows原windows.php启动也已补充项目级mbstring子进程配置。
+
+## 2026-09-19 uni-app 底部导航修复
+- water-mini/src/components/BottomNav.vue 统一控制固定定位:移除 app.css 对 bottom-nav 的 H5 位移覆盖,使用左右贴边、max-width:480px 和自动外边距居中,解决 scoped left/right 与全局 transform 冲突。统一四等分按钮、图标和文字间距,页面底部预留安全区。
+- 企业导航按设计恢复首页/订单/水卡/我的,水卡仍展示真实汇总权益;司机为工作台/任务/地图/我的,地图入口沿用配送路线及站点地图;代理为工作台/用户/订单/佣金,佣金页补导航,账户入口移至代理工作台,账户详情不显示无选中项的代理导航。
+- 点击当前菜单不再 reLaunch;切换进行中忽略重复点击,失败提示可重试。未改变 API、数据库、Redis、身份认证或佣金业务。
+- vue-tsc、现有12项测试、微信小程序及H5构建通过;已更新本地 http://127.0.0.1:8787/water-mini/index.html,入口和新资源 HTTP 200。编译后的 H5/微信 CSS 已核对;浏览器工具因 unsupported Codex auth method: apikey 不可用,视觉截图及实际点击验收 NOT RUN。
+
+## 2026-09-19 本机测试手机号登录及微信导入修正
+- 用户明确指定 13012345678 / 66666 测试功能。新增 POST /api/mini/v1/auth/test-phone,经 TestPhoneLogin 服务和 TestPhoneIdentityProvider 适配器验证,仅 APP_ENV=local 且 MINI_TEST_PHONE_LOGIN=true 并且 TCP 来源回环地址可登录。默认生产环境关闭;不验证真实手机号所有权,不与微信账号合并。SessionVerifier 在开关关闭时拒绝既有测试会话。
+- 本机 server/.env 已开启测试,Windows monitor 自动重新加载,实际 config 已返回 test_phone_login=true。通过幂等 CLI seed_mini_test_phone.php 初始化用户5,并绑定仅三个既有测试资源:企业49【测试0910】科技园配送测试、司机13【测试】地图配送员-KJY、代理7测试商贸。未创建业务库存、收款、订单;脚本不恢复已停用账号/授权。
+- water-mini 登录页新增手机号/验证码表单与本地测试标记,H5和微信编译端共用真实会话及身份选择。正式短信登录仍待接入。源目录 project.config.json 指向 dist/build/mp-weixin,修复直接导入源码找不到 app.json 的问题;此本机入口使用 touristappid、urlCheck=false,生产或真实微信登录须配置真实AppID并启用域名校验。
+- 小程序接口默认地址在 water-mini/src/api/client.ts,可用 water-mini/.env.local 的 VITE_API_BASE_URL 覆盖并重新编译;后端8787监听在 server/config/process.php。测试手机号仅支持电脑本机网页/开发者工具;手机真机的127.0.0.1不是电脑。
+- 验证:18项测试登录、161项业务、786项契约、100项架构、47项框架检查通过,测试数据回滚;前端类型和12项测试、MP/H5构建通过。实际HTTP验证10个身份/业务读取接口、错误验证码401、跨企业403、注销后401。API文档已同步46项操作,H5预览更新并验证资源200。UI实际点击验收受浏览器工具认证错误限制,NOT RUN。

+ 99 - 0
README.md

@@ -0,0 +1,99 @@
+<p align="center">
+  <img src="https://saithink.top/images/logo.png" width="120" />
+</p>
+<p align="center">
+  <img src="https://svg.hamm.cn/badge.svg?key=License&value=MIT" />
+  <img src="https://svg.hamm.cn/badge.svg?key=Version&value=6.x" />
+</p>
+
+<div style="padding:18px;max-width: 1024px;margin:0 auto;">
+<h1>SaiAdmin 6.x</h1>
+
+## 项目简介
+
+SaiAdmin 是一个基于 [Webman](https://www.workerman.net/webman) 的高性能后台管理系统插件。它提供了完整的权限管理、系统配置、代码生成等功能,帮助开发者快速构建企业级应用。
+
+---
+
+## ✨ 核心特性
+
+- **🚀 高性能** - 基于 Webman 常驻内存框架,性能优异
+- **🔐 完整权限系统** - RBAC 权限模型,支持用户、角色、部门、岗位管理
+- **📝 代码生成器** - 一键生成 CRUD 代码,提升开发效率
+- **⚡ 双 ORM 支持** - 同时支持 ThinkORM 和 Eloquent ORM
+- **🔧 插件化架构** - 支持插件扩展,便于功能模块化
+- **📊 系统监控** - 内置服务器监控、缓存管理功能
+- **📋 日志系统** - 完整的登录日志和操作日志记录
+
+## 🛠️ 功能模块
+
+### 系统管理
+
+| 模块       | 说明                             |
+| ---------- | -------------------------------- |
+| 用户管理   | 用户增删改查、密码管理、缓存清理 |
+| 角色管理   | 角色 CRUD、菜单权限分配          |
+| 部门管理   | 组织架构管理、树形结构           |
+| 岗位管理   | 岗位信息维护、Excel 模板导入导出 |
+| 菜单管理   | 菜单配置、按钮权限               |
+| 字典管理   | 字典类型与字典数据维护           |
+| 附件管理   | 文件上传、分类管理、资源移动     |
+| 系统配置   | 分组配置、邮件设置、动态参数     |
+| 日志管理   | 登录日志、操作日志查询与清理     |
+| 服务监控   | 服务器状态、缓存信息、一键清理   |
+| 数据表维护 | 数据表结构、表优化、碎片整理     |
+
+### 开发工具
+
+| 模块     | 说明                         |
+| -------- | ---------------------------- |
+| 代码生成 | 根据数据表自动生成 CRUD 代码 |
+| 定时任务 | Crontab 任务管理、执行日志   |
+
+<h1>学习</h1>
+
+<ul>
+  <li>
+    <a href="https://saithink.top" target="_blank">主页 / Home page</a>
+  </li>
+  <li>
+    <a href="https://saithink.top/documents/v6/" target="_blank">文档 / Document</a>
+  </li>
+</ul>
+
+
+<h1>演示地址</h1>
+<p>演示地址: <a href="http://v6.saithink.top" target="_blank">http://v6.saithink.top</a></p>
+<p>演示账号:admin</p>
+<p>演示密码:123456</p>
+
+<h1>共同交流</h1>
+
+<table>
+  <tbody>
+    <tr>
+      <td align="center" valign="middle">
+        <img src="https://saithink.top/images/me.png" class="no-zoom" width="180px">
+        <p>saiadmin交流群(添加我微信备注"saiadmin")</p>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<h1>支持项目</h1>
+
+如果您正在使用这个项目并感觉良好,或者是想支持我继续开发,您可以通过如下`任意`方式支持我:
+
+谢谢! ❤️
+
+
+|                                       微信                                       |                                      支付宝                                      |
+| :------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: |
+| <img src="https://saithink.top/images/wechat.png" alt="Wechat QRcode" width=180> | <img src="https://saithink.top/images/alipay.png" alt="Alipay QRcode" width=180> |
+
+<div style="clear: both">
+<h1>LICENSE</h1>
+This project is open-sourced software licensed under the MIT.
+</div>
+
+</div>

+ 22 - 0
saiadmin-artd/.env

@@ -0,0 +1,22 @@
+# 【通用】环境变量
+
+#  版本号
+VITE_VERSION = 3.0.2
+
+# 端口号
+VITE_PORT = 3006
+
+# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
+VITE_BASE_URL = /
+
+# 权限模式【 frontend 前端模式 / backend 后端模式 】
+VITE_ACCESS_MODE = backend
+
+# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
+VITE_WITH_CREDENTIALS = false
+
+# 是否打开路由信息
+VITE_OPEN_ROUTE_INFO = false
+
+# 锁屏加密密钥
+VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro

+ 13 - 0
saiadmin-artd/.env.development

@@ -0,0 +1,13 @@
+# 【开发】环境变量
+
+# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
+VITE_BASE_URL = /
+
+# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址)
+VITE_API_URL = /api
+
+# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
+VITE_API_PROXY_URL = http://127.0.0.1:8787
+
+# Delete console
+VITE_DROP_CONSOLE = false

+ 10 - 0
saiadmin-artd/.env.production

@@ -0,0 +1,10 @@
+# 【生产】环境变量
+
+# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
+VITE_BASE_URL = /
+
+# API 地址前缀
+VITE_API_URL = http://127.0.0.1:8787
+
+# Delete console
+VITE_DROP_CONSOLE = true

+ 2 - 0
saiadmin-artd/.gitattributes

@@ -0,0 +1,2 @@
+*.html linguist-detectable=false
+*.vue linguist-detectable=true

+ 11 - 0
saiadmin-artd/.gitignore

@@ -0,0 +1,11 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+.cursorrules
+
+# Auto-generated files
+src/types/import/auto-imports.d.ts
+src/types/import/components.d.ts
+.auto-import.json

+ 1 - 0
saiadmin-artd/.husky/commit-msg

@@ -0,0 +1 @@
+pnpm dlx commitlint --edit $1

+ 1 - 0
saiadmin-artd/.husky/pre-commit

@@ -0,0 +1 @@
+pnpm run lint:lint-staged

+ 3 - 0
saiadmin-artd/.prettierignore

@@ -0,0 +1,3 @@
+/node_modules/*
+/dist/*
+/src/main.ts

+ 20 - 0
saiadmin-artd/.prettierrc

@@ -0,0 +1,20 @@
+{
+  "printWidth": 100,
+  "tabWidth": 2,
+  "useTabs": false,
+  "semi": false,
+  "vueIndentScriptAndStyle": true,
+  "singleQuote": true,
+  "quoteProps": "as-needed",
+  "bracketSpacing": true,
+  "trailingComma": "none",
+  "bracketSameLine": false,
+  "jsxSingleQuote": false,
+  "arrowParens": "always",
+  "insertPragma": false,
+  "requirePragma": false,
+  "proseWrap": "never",
+  "htmlWhitespaceSensitivity": "strict",
+  "endOfLine": "auto",
+  "rangeStart": 0
+}

+ 9 - 0
saiadmin-artd/.stylelintignore

@@ -0,0 +1,9 @@
+dist
+node_modules
+public
+.husky
+.vscode
+
+src/components/Layout/MenuLeft/index.vue
+src/assets
+stats.html

+ 82 - 0
saiadmin-artd/.stylelintrc.cjs

@@ -0,0 +1,82 @@
+module.exports = {
+  // 继承推荐规范配置
+  extends: [
+    'stylelint-config-standard',
+    'stylelint-config-recommended-scss',
+    'stylelint-config-recommended-vue/scss',
+    'stylelint-config-html/vue',
+    'stylelint-config-recess-order'
+  ],
+  // 指定不同文件对应的解析器
+  overrides: [
+    {
+      files: ['**/*.{vue,html}'],
+      customSyntax: 'postcss-html'
+    },
+    {
+      files: ['**/*.{css,scss}'],
+      customSyntax: 'postcss-scss'
+    }
+  ],
+  // 自定义规则
+  rules: {
+    'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url")
+    'selector-class-pattern': null, // 选择器类名命名规则
+    'custom-property-pattern': null, // 自定义属性命名规则
+    'keyframes-name-pattern': null, // 动画帧节点样式命名规则
+    'no-descending-specificity': null, // 允许无降序特异性
+    'no-empty-source': null, // 允许空样式
+    'property-no-vendor-prefix': null, // 允许属性前缀
+    // 允许 global 、export 、deep伪类
+    'selector-pseudo-class-no-unknown': [
+      true,
+      {
+        ignorePseudoClasses: ['global', 'export', 'deep']
+      }
+    ],
+    // 允许未知属性
+    'property-no-unknown': [
+      true,
+      {
+        ignoreProperties: []
+      }
+    ],
+    // 允许未知规则
+    'at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: [
+          'apply',
+          'use',
+          'mixin',
+          'include',
+          'extend',
+          'each',
+          'if',
+          'else',
+          'for',
+          'while',
+          'reference'
+        ]
+      }
+    ],
+    'scss/at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: [
+          'apply',
+          'use',
+          'mixin',
+          'include',
+          'extend',
+          'each',
+          'if',
+          'else',
+          'for',
+          'while',
+          'reference'
+        ]
+      }
+    ]
+  }
+}

+ 10 - 0
saiadmin-artd/.vscode/extensions.json

@@ -0,0 +1,10 @@
+{
+  "recommendations": [
+    "vue.volar",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode",
+    "stylelint.vscode-stylelint",
+    "lokalise.i18n-ally",
+    "bradlc.vscode-tailwindcss"
+  ]
+}

+ 18 - 0
saiadmin-artd/.vscode/settings.json

@@ -0,0 +1,18 @@
+{
+  "volar.inlayHints.eventArgumentInInlineHandlers": true,
+  "css.lint.unknownAtRules": "ignore",
+  "i18n-ally.localesPaths": ["src/locales/langs", "src/locales"],
+  "i18n-ally.enabledParsers": ["json"],
+  "i18n-ally.sourceLanguage": "en",
+  "i18n-ally.displayLanguage": "zh",
+  "i18n-ally.enabledFrameworks": ["vue", "react"],
+  "i18n-ally.keystyle": "nested",
+  "i18n-ally.sortKeys": true,
+  "i18n-ally.namespace": true,
+  "editor.formatOnSave": true,
+  "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "prettier.requireConfig": true,
+  "[vue]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  }
+}

+ 21 - 0
saiadmin-artd/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 SuperManTT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 330 - 0
saiadmin-artd/PROJECT_CONTEXT.md

@@ -0,0 +1,330 @@
+# saiadmin-artd 项目记忆
+
+## 2026-09-17 菜单管理导航入口
+- 用户要求把菜单操作加到导航;按添加“菜单管理”入口实现。原系统菜单id8及core:menu:index/read/save/update/destroy早已存在,供水mergeWaterMenus原先隐藏全部旧系统导航,导致入口不可见。
+- router/modules/water.ts从服务端已授权菜单树提取/system/menu,保留原Menu路由名称、组件、可见标记与权限,放在角色与权限后、操作审计前,移除原树重复节点;其他旧菜单仍隐藏但保留路由。无授权不创建入口,不更改数据库菜单、角色授权或业务数据。
+- views/index/index.vue让/system/menu沿用供水外观。复用现有system/menu页面及CRUD接口,原新增/编辑/删除按钮权限不变;补货阈值策略现有权限仍是water:alerts:write(菜单名称“管理补货预警”)。
+- Prettier/ESLint/vue-tsc通过;导航运行验证覆盖排序、原始树不变、未授权不补发、无重复、无供水菜单回退;真实HTTP确认/core/system/menu返回唯一可见Menu页面,/core/menu/index包含全部既有菜单操作权限。浏览器仍有已知apikey认证阻塞,未做点击验收。
+- Vite3335模块生产构建通过,dist更新;4173首页和包含导航修改的主JS资源SHA256一致。
+
+## 2026-09-17 平台阈值策略编辑
+- 恢复预警页AlertPolicyDialog入口,由alerts:write控制;编辑平台统一alert_qty、days_per_barrel、urgent_qty,默认5/3/2。打开时GET alerts/policy读取持久值,提交带revision,保存成功刷新看板。不存在逐企业阈值编辑,默认值不是写死的算法常量。
+- 列表理由、统计卡片及规则说明使用当前接口policy;企业编辑/详情和共享模块去掉固定5桶/3天说明,统一引用平台策略。企业人数可空,未知人数只计算数量型预警,预计用量不扣实物库存。
+- 全局策略43项、预警118项、业务64项、企业68项共293项后端检查通过;前端Prettier/ESLint/Stylelint/vue-tsc通过,详情见父项目server/docs/WATER_ALERTS.md。浏览器仍有既有apikey认证故障,未做点击/截图验收。
+- 生产构建成功(3335模块),dist已更新;4173首页与含“保存并生效”的策略资源SHA256一致。仅保留既有system/menu导入警告。
+
+## 2026-09-17 首页内容配置独立页面
+- `views/water/content` 改为参考图列表+手机双栏布局,分离 `ContentEditor/ContentPhone/ContentPreviewDialog`,接口类型在 `api/water/content.ts`。搜索状态筛选、8条分页、全量上线统计、选中素材/草稿预览、按身份区域模拟预览均接入后端。
+- 新建/编辑保存原POST content,人群/区域/有效期/排序/启停、HTTP(S)图片和活动链接可配置。手机套餐为真实启用商品,空态不伪造;后台快捷入口仅供展示,实际小程序/App未接入。
+- 状态和受众判断以服务端为准,权限校验content:read/write。源码检查已通过;完整契约及测试见父项目 `server/docs/WATER_CONTENT.md`。浏览器仍受既有工具认证故障限制。
+- Vite3331模块构建通过,dist已更新,4173预览首页及内容页资源与本次产物一致。
+
+## 2026-09-17 待审核提现测试展示
+- 财务API新增独立 `withdrawal_demo`(list/total/amount_cents),待审核区显示4条持久化测试申请,测试卡片合计12360元;名称/状态/说明明确模拟,无真实余额或打款。按月份、all/demo显示,regular隐藏;正式提现权限能力及按钮仍未开通。
+- 测试表、种子及接口验证详见父项目 `server/docs/WATER_FINANCE.md`;原收款汇总、流水与导出不混入测试提现。
+
+## 2026-09-17 财务结算独立页面
+- `/water-finance` 使用 `api/water/finance.ts` 与 `views/water/finance/{index.vue,ReceiptDialog.vue,finance.scss}`,已完成参考图四卡/趋势/月汇总/提现占位/流水布局。月份、测试范围、搜索、8条分页、详情、全量匹配XLSX导出接入真实供水接口。
+- 收款复用 `saveWater('finance',{action:'purchase',...})`,企业和套餐远程选项、金额权益预览、凭据校验与核对勾选、提交锁和失败保留输入。后端凭据幂等,不重复发放权益。按钮校验 finance:write。
+- 今日卡片固定今天,月趋势与汇总不受流水关键词和分页影响。未知退款/佣金/提现为“—/未开通”,审核按钮禁用;不引用旧租赁财务。测试范围与对账语义见父项目 `server/docs/WATER_FINANCE.md`。
+- vue-tsc、相关ESLint/Prettier/Stylelint、Vite3323模块构建通过,dist更新。浏览器仍因工具 `unsupported Codex auth method: apikey` 不能点击或截图验收;接口及后端测试已通过。
+
+## 2026-09-11 代理商区域选图
+- `catalog.ts`代理商region使用新`region-map`字段类型,`WaterWorkspace`交由`WaterRegionField`显示只读区域、地图选择、清空;已有值原样保留,选图确认后回填,代理商保存才提交。
+- 共享`WaterLocationPicker.vue`支持点坐标和行政区名称模式;原`DispatchLocationPicker`为兼容包装,配送调用不变。`findMapRegion`使用高德逆地理编码,去重直辖市省/市并兼容空city;没有识别结果或失败时禁止确认。
+- `node scripts/test-water-region.mjs`覆盖行政区、直辖市、缺区县、空结果和配额错误;原道路规划脚本、ESLint/vue-tsc及Vite构建验证。未修改既有代理商数据和数据库结构,浏览器实际选图验收仍未完成。
+
+## 2026-09-11 公共导航样式归属
+- 供水导航深色样式改由 `src/views/index/index.vue` 导入同目录 `water-layout.scss`。此前依赖业务页懒加载,直接刷新独立设备/库存页会显示默认白色,访问总览后才恢复。
+- `src/views/water/modules/water.scss` 仅保留业务页规则;以后独立页面无需为导航导入该文件。布局仍按原 `/water-` 路由范围应用品牌主题。
+- Prettier/ESLint/vue-tsc及Vite3291模块构建通过,构建清单确认导航CSS包含在布局静态依赖中,与业务页面访问顺序无关。
+
+## 2026-09-11 配送履约闭环
+
+- 配送页新增独立 `FulfillmentDialog.vue`(实收/部分签收/拒收、退回核对、关联重送、返仓结算)及 `DeliveryHistory.vue` 履约记录。任务展示实收数量、异常、父子重送编号和结算时间,新增待退回/已退回筛选;总览同步异常统计。
+- 顶部“返仓结算”展示所选日期及以前全部未结算路线,不受任务池关键词/片区过滤。仓库动作同时检查 dispatch:write 和 inventory:write;表单失败保留输入,阻止重复提交,实物数量和差异需明确核对。对话框开启时暂停焦点刷新。
+- 库存列表增加破损隔离满桶及累计空桶损失。具体状态、账务及迁移合同见父项目 `server/docs/WATER_FULFILLMENT.md`。当前为后台人工确认,未接入配送员移动端或客户电子签名。本轮浏览器工具运行时启动失败,未做视觉验收。
+- 最终vue-tsc、相关ESLint/Prettier和Vite构建通过(3268 modules),4173预览已提供最新产物;实际HTTP闭环由父项目测试完成。任务池支持当天完成签收/退回的旧任务,避免核对后无法找到重送入口。
+
+> 项目:`D:\phpstudy_pro\WWW\saiadmin6.x\saiadmin-artd`
+> 
+> 记录日期:2026-08-25
+> 
+> 本文件只记录该前端子项目。父项目 `saiadmin6.x` 的记忆仍单独保存在父目录,不要混用。
+
+## 1. 项目定位
+
+这是 SaiAdmin 6.x 配套的管理后台前端,当前与父项目中的 Webman 后端配套使用。项目包含登录、动态菜单权限、系统管理、商户管理、设备、订单、资金、运营管理、小程序和后台维护等页面。
+
+当前重点业务已于 2026-09-09 调整为云泉智配企业供水:企业档案、套餐权益、库存、补货与配送。旧商户管理页面及数据保留。
+
+### 2026-09-09 供水业务合并
+
+- 新页面 `src/views/water`,共享表单/列表 `modules/WaterWorkspace.vue`,总览单独实现;API `src/api/water`。
+- 后端动态菜单新增 12 个入口,`src/router/modules/water.ts` 根据已授权菜单调整显示顺序并隐藏旧一级导航,不主动补发新业务权限。`src/store/modules/menu.ts` 优先将已授权 `/water-overview` 设为首页。
+- 页面使用真实接口和空数据状态,无设计图示例经营数据。详情/盘点/签收表单接入后端基础事务;代理、角色、审计入口复用现有页面。
+- 新配色限于 `.water-layout` 和 `.water-page`,品牌名为“云泉智配”,保留用户其他页面设置。
+- 本轮尚未包含地图智能合单、设备饮水遥测、线上支付退款、自动佣金提现、移动端内容投放;功能边界见父项目 `server/docs/WATER_MERGE.md`。
+- 验证:vue-tsc、相关 ESLint、Vite build 通过(3245 modules);浏览器已验证总览真实零数据、12 个新菜单、企业表单必填校验、库存及旧审计页面。
+
+## 2. 技术栈与运行
+
+### 2026-09-10 补货预警接续
+
+- 为验证15条演示预警分页,`WaterWorkspace.vue` 将补货模块页大小及API的limit统一为10,其他模块保持20。新增10家不同名称企业及东/西/南/北区域、山泉水/纯净水规格供筛选;数据脚本见父项目 `server/scripts/seed_water_alert_search_demo.php`。
+
+- 补货搜索实际按企业名称/区域/规格模糊查询。用户反馈无效时已复现名称查询返回1条;搜索“1”会命中全部0910/18.9演示数据。页面已校正提示、增加重置及无匹配文案,并限制120字、去除首尾空格。
+- 搜索调整通过vue-tsc、ESLint及Vite构建;浏览器验证名称查询1条、回车无匹配0条、重置恢复5条。
+
+- `WaterWorkspace.vue` 增加仅预警/全部启用账户切换、数量阈值编辑、预警桶数列及当前页多选;切换查询清空选择,已建单或无可用权益账户不能批量选择。
+- `modules/BatchDeliveryDialog.vue` 独立维护逐条配送数量和确认过程,列出地址、权益与现场满桶;未填数量不能提交,错误时保留表单,提交期间阻止重复提交或关闭。
+- 策略保存使用 `saveWater('alerts',{action:'policy',id,threshold_qty})`;批量保存使用 `saveWater('dispatch',{action:'batch_create',items:[{account_id,quantity}]})`。按钮分别检查已有 alerts/dispatch write 权限。
+- vue-tsc、相关 ESLint、Prettier 和 Vite production build 通过(3247 modules);4173 浏览器确认入口、列和零数据布局。后端 69 项事务回滚检查通过;未在浏览器创建实际业务数据,非空弹窗仍待实际数据验收。
+
+- Vue 3 + TypeScript + Vite 7
+- Element Plus、Pinia、Vue Router 4、Axios、VueUse、Vue I18n
+- ECharts 6、Tailwind CSS 4、WangEditor、XLSX、FileSaver
+- 包管理器:pnpm
+- Node 要求:`>=20.19.0`;已知本机 Node:`v22.21.0`
+- 项目依赖目录 `node_modules` 已存在;如果 pnpm 未加入 PATH,可直接使用 `node_modules/.bin` 中的工具
+
+常用命令:
+
+```bash
+pnpm dev       # 开发服务器,默认端口 3006
+pnpm build     # vue-tsc --noEmit 后执行 vite build
+pnpm lint      # ESLint
+pnpm serve     # 预览构建结果
+```
+
+已知验证结果:
+
+- `vue-tsc --noEmit` 已通过
+- Vite 生产构建已通过,约 3152 个模块转换成功
+
+## 3. 环境变量与后端连接
+
+环境文件:`.env`、`.env.development`、`.env.production`。不要把密钥值写入项目记忆。
+
+关键变量:
+
+- `VITE_ACCESS_MODE=backend`:当前使用后端菜单模式
+- `VITE_API_URL`:Axios 的 baseURL;开发环境为 `/api`
+- `VITE_API_PROXY_URL`:开发代理目标,当前为 `http://127.0.0.1:8787`
+- `VITE_BASE_URL`:当前为 `/`
+- `VITE_PORT`:当前开发端口为 `3006`
+- `VITE_WITH_CREDENTIALS`:是否携带 Cookie
+- `VITE_VERSION`、`VITE_DROP_CONSOLE`、`VITE_OPEN_ROUTE_INFO`
+- `VITE_LOCK_ENCRYPT_KEY`:锁屏相关密钥,仅使用环境文件中的值,不复制到记忆
+
+`vite.config.ts` 将 `/api` 代理到 Webman 后端 `127.0.0.1:8787`,并去除 `/api` 前缀。生产环境默认直接访问 `http://127.0.0.1:8787`,部署时需按实际后端地址调整环境配置。
+
+## 4. 入口和目录结构
+
+- `src/main.ts`:创建 App,初始化 Pinia、Router、语言、指令和错误处理
+- `src/App.vue`:Element Plus 配置、主题、存储兼容性和系统升级检查
+- `src/api/`:按业务拆分的 Axios API 封装(当前约 31 个文件)
+- `src/views/`:页面组件(当前约 132 个文件)
+- `src/components/core/`:通用后台布局、表单、表格、图表和基础组件
+- `src/components/sai/`:项目业务组件,例如 `SaSearchBar`、`SaDict`、上传和导入导出组件
+- `src/store/`:Pinia store
+- `src/router/`:静态路由、动态路由、守卫和路由转换核心
+- `src/locales/`:多语言资源
+- `src/config/`:系统名称、主题、布局和头部配置
+- `src/types/`:API、路由、组件、配置和 store 类型
+- `src/assets/`:图片、SVG 和样式
+
+页面通常按 `src/views/<业务>/<页面>/index.vue` 组织,复杂页面的子组件放在同级 `modules/` 目录。API 按业务放在 `src/api/<业务>/`,例如 `src/api/merchant/merchant.ts`。
+
+Vite 别名:`@` 指向 `src`,另有 `@views`、`@imgs`、`@icons`、`@utils`、`@stores`、`@styles`。
+
+## 5. 路由、菜单和权限
+
+- `src/router/index.ts` 使用 `createWebHashHistory()`,因此访问地址形如 `/#/merchant/statistics?id=73`
+- `src/router/routes/staticRoutes.ts` 注册登录、注册、忘记密码、403、404、500 和 iframe 等静态路由
+- `src/router/routes/asyncRoutes.ts` 汇总前端模块路由
+- `src/router/modules/` 保存 dashboard、system、merchant、mini-program、operations 等模块路由
+- 登录后,`src/router/guards/beforeEach.ts` 并行获取用户信息、字典和菜单,然后注册动态路由
+- `src/router/core/MenuProcessor.ts` 根据 `VITE_ACCESS_MODE` 在前端菜单和后端菜单之间切换;当前是 backend 模式,通过 `/core/system/menu` 获取菜单
+- 后端菜单路径会被规范化为完整路径,并补充/合并前端维护的 MiniProgram、Merchant、Device、Agent 菜单项(用于兼容后端菜单尚未同步的情况)
+- `RouteRegistry` 负责注册/移除动态路由;`RouteTransformer` 负责 Layout、iframe、全屏页和普通组件转换
+- `ComponentLoader` 使用 `import.meta.glob('../../views/**/*.vue')` 按需懒加载页面,组件字段通常写成 `/merchant/my-merchant` 这类视图路径
+- `RoutePermissionValidator` 负责路径、动态参数和菜单前缀权限匹配
+- 登出或 401 后会清理动态路由、菜单和 iframe 路由缓存,再回到登录页
+
+修改后端菜单时要特别注意:后端返回的 path、name、component、children、meta 必须符合 `AppRouteRecord` 和 `RouteValidator` 规则;二级及以下菜单不要错误使用 Layout 作为 component。
+
+## 6. HTTP 请求和认证约定
+
+统一请求封装在 `src/utils/http/index.ts`:
+
+- Axios 默认超时 15 秒,baseURL 读取 `VITE_API_URL`
+- 请求会从 `useUserStore().accessToken` 读取令牌,并添加 `Authorization: Bearer <token>`
+- 非 FormData 请求会自动设置 JSON 请求头并序列化 data
+- POST/PUT 如果调用方只传 `params` 且没有 `data`,封装层会把 params 转成请求体
+- 后端成功响应必须是 `{ code: 200, message: string, data: T }`;封装层最终返回 `data`
+- 非 200 业务 code 会转成 `HttpError`;401 会防抖提示、清理登录状态并跳转登录
+- 当前 `MAX_RETRIES=0`,虽然保留了可配置重试逻辑,但默认不会自动重试
+- `responseType: 'blob'` 时直接返回二进制响应,用于导出文件
+
+认证 API 在 `src/api/auth.ts`:
+
+- 验证码:`GET /core/captcha`
+- 登录:`POST /core/login`
+- 用户信息:`GET /core/system/user`
+- 字典:`GET /core/system/dictAll`
+- 后端菜单:`GET /core/system/menu`
+
+登录页 `src/views/auth/login/index.vue` 在挂载和登录失败后刷新验证码,保存 `uuid` 和 `image`;验证码接口需要避免浏览器缓存时应在请求层增加时间戳参数或正确的缓存响应头。
+
+## 7. 状态管理和持久化
+
+`src/store/index.ts` 创建 Pinia,并通过 `pinia-plugin-persistedstate` 使用 localStorage 持久化。主要 store:
+
+- `user`:登录状态、用户信息、access/refresh token、语言、锁屏和搜索历史
+- `menu`:后端菜单、首页路径、动态路由移除函数
+- `dict`:全局字典缓存
+- `setting`:菜单布局、主题、工作台、界面开关等
+- `worktab`:工作台标签和 keep-alive 状态
+- `table`:表格相关状态
+
+持久化 key 由 `StorageKeyManager` 版本化管理,修改字段或版本时要考虑旧 localStorage 数据兼容。登出逻辑会清理 token、用户信息、动态路由和 iframe 缓存,但按设计保留部分工作台标签,下一次登录会根据用户 ID判断是否清理。
+
+## 8. 商户模块现状
+
+路由定义在 `src/router/modules/merchant.ts`,当前包括:
+
+- `/merchant/my-merchant`:我的商户
+- `/merchant/subordinate-merchant`:下级商户,复用 `my-merchant/index.vue`,通过 `subordinate` prop 切换查询范围
+- `/merchant/add-merchant`:新增商户
+- `/merchant/statistics`:商户数据统计,通常通过 query `id` 进入
+
+API 在 `src/api/merchant/merchant.ts`,主要接口:
+
+- `GET /core/merchant/index`:商户分页列表;`scope` 支持 `own` / `subordinate`,并支持名称、电话、分配状态、状态、分润方式、设备类别、代理类型等筛选
+- `GET /core/merchant/read`
+- `POST /core/merchant/save`
+- `PUT /core/merchant/update`
+- `DELETE /core/merchant/destroy`
+- `POST /core/merchant/password`
+- `GET /core/merchant/devices`
+- `POST /core/merchant/device/bind`
+- `POST /core/merchant/device/unbind`
+- `GET/PUT /core/merchant/withdrawal-setting`
+- `GET /core/merchant/statistics`
+- `POST /core/merchant/statistics/export`:Blob 导出
+
+商户列表返回分页结构 `current_page/data/per_page/total`,并包含分类统计 `summary`:`all`、`city-partner`、`agent`、`salesperson`。统计接口返回商户信息、日期范围、汇总、支付方式和每日数据。
+
+已验证的业务结果:
+
+- `/merchant/statistics?id=73` 可以正常加载统计页面并与后端交互
+- `/merchant/subordinate-merchant` 可以加载后端列表
+- 下级商户分类示例:全部 65、城市合伙人 22、代理商 22、业务员 21
+- 商户名称搜索可以过滤到单条结果
+
+近期新增/修改的商户相关文件包括:
+
+- `src/router/modules/merchant.ts`
+- `src/router/modules/index.ts`
+- `src/router/core/MenuProcessor.ts`
+- `src/api/merchant/merchant.ts`
+- `src/views/merchant/my-merchant/index.vue`
+- `src/views/merchant/subordinate-merchant/index.vue`
+- `src/views/merchant/statistics/index.vue`
+
+## 9. 代理与设备分配模块现状
+
+- `src/api/agent/agent.ts` 提供代理分页、导出、当前账号可分配设备、分配、回收和分配流水 API。
+- `src/views/agent/components/AgentList.vue` 已从静态演示数据切换为真实 API,支持筛选、后端分页、导出,并接入设备分配和分配记录弹窗。
+- `DeviceAssignmentDialog.vue` 支持库存勾选与手工/扫码 SN 合并,单次最多 100 台,并分别展示成功、已在目标、非当前管理、已铺货和未找到结果。
+- `AssignmentRecordsDialog.vue` 支持按操作、设备类型和 SN 查询流水,并可从历史分配记录发起回收;后端仍会复核设备当前管理人和铺货状态。
+- `/device/assign` 是隐藏的去分配页面;“我的设备”勾选未铺货设备后进入该页面会通过 `sns` query 预填待分配 SN。
+- 2026-08-27 验证:代理分配相关文件通过 ESLint,`vue-tsc --noEmit` 通过;Vite production build 通过(3173 modules),仅有既有 system menu 动静态导入警告;真实 HTTP 分配、流水查询和回收闭环通过。
+- `src/views/agent/edit-agent/index.vue` 是真实代理修改页,通过 `/agent/edit-agent?id=...` 进入;运营产品控制对应分润输入,开通类型只读,重置恢复接口加载快照,页面不修改密码或登录账号。
+- `src/api/agent/agent.ts` 已增加 `AgentProfile` / `AgentUpdatePayload` 类型,以及 `GET /core/agent/read`、`PUT /core/agent/update` 封装。
+- 代理列表“更多设置”使用点击下拉菜单展示合同管理、设备绑定、删除代理;合同管理已跳转 `/agent/contract?id=...` 并接入真实读写,设备绑定和删除代理仍显示未接入提示,不执行危险操作。
+- 2026-08-27 验证:代理修改及更多设置相关文件通过 Prettier、ESLint、`vue-tsc --noEmit`;Vite production build 通过(3177 modules),只有既有 system menu 动静态导入警告;真实 HTTP 修改、读取和恢复闭环通过。
+- `src/views/agent/contract/index.vue` 使用 `SaImageUpload` 维护合同照片、身份信息和其他信息,每组最多 20 张;API 为 `GET/PUT /core/agent/contract`,后端持久化到 `sa_agent_contract`。
+- 2026-08-27 验证:合同页相关文件通过 Prettier、ESLint、`vue-tsc --noEmit`;Vite production build 通过(3180 modules)。浏览器实际加载 `/agent/contract?id=101`,代理信息、三组上传区和提交按钮渲染正常。
+
+## 10. 资金管理会员开通记录
+
+- `src/views/fund-management/member-activation-records/index.vue` 已从占位页切换为真实列表,支持来源、卡类型、设备类型、用户、开通日期筛选,以及汇总、分页、重置和 Excel 导出。
+- 前端 API 位于 `src/api/fundManagement/memberActivationRecord.ts`,列表和导出接口分别为 `GET /core/fund-management/member-activation-records/index`、`POST /core/fund-management/member-activation-records/export`。
+- 后端使用 `sa_member_activation_record` 保存会员开通/续费业务快照,不从普通租借订单推测历史会员数据;数据按当前管理员及递归下级范围隔离。
+- 2026-09-05 验证:ESLint、`vue-tsc --noEmit`、PHP 语法检查和 Vite production build 通过;本地数据库、API、筛选、重置和 Excel 导出闭环通过,测试数据为 3 条。
+
+## 11. 当前注意事项和后续检查
+
+- 工作树中有大量业务新增和修改文件,均视为用户现有工作;不要 reset、checkout、清理未跟踪文件或覆盖无关改动
+- `pnpm build` 已通过,但修改路由、API 类型或页面后仍应至少重新执行 `pnpm build`
+- 开发环境前端端口是 3006;用户之前使用的 `http://localhost:4173` 属于其他预览/开发实例,排查时先确认当前实际启动端口和 Vite 进程
+- 验证码问题要同时检查 `/api/core/captcha` 的代理、后端 `/core/captcha` 响应格式、浏览器缓存以及 `image` 是否为完整 data URL
+- 若出现 304、无法登录或页面跳 404,优先检查 Network 中请求 URL、响应 `{ code, message, data }` 结构、Authorization、动态菜单初始化和路由 component 路径
+- 商户页面必须使用真实 API 请求,不要用静态数组替代后端数据;筛选、分页、统计日期和导出都要保持接口参数与后端一致
+
+## 12. 记忆更新规则
+
+后续修改该子项目后,在本文件追加或更新:变更文件、接口/路由约定、已验证命令、已知问题和未完成事项。只记录已经从代码、接口或实际运行结果确认的信息;敏感环境变量值、token、密码和用户隐私不要写入。
+
+## 2026-09-10 库存流水查询与导出
+- 库存管理的“查看库存流水”使用独立组件 `WaterLedgerDrawer.vue`,不再限制最近 100 条;默认每页 10 条,可选 20/50/100 条,支持页码跳转、关键词、操作类型、起止日期、重置和空结果提示。
+- `WaterLedger` 服务统一列表/导出筛选口径:企业名、商品名、关联凭据、说明,日期包含结束日全天;操作类型及中文名称由后端提供。保留原库存/配送九种流水,档案、收款等其他审计不混入。
+- `GET /core/water/ledger` 返回分页及 `kinds`,`POST /core/water/ledger/export` 接收相同筛选,导出当前已查询条件的全部 XLSX(忽略 page/limit),超过 50,000 条明确拒绝。两接口均要求库存读取权限;临时文件位于系统临时目录,响应生成后删除。
+- 验证:PHP lint、ESLint、vue-tsc、Vite build(3250 modules)通过;`php tests/water_ledger_test.php` 30 项、原 `water_business_test.php` 69 项通过,测试数据全部回滚。浏览器现有 20 条流水分成两页;第二页搜索星河返回第一页 1 条,下载 XLSX 解析确认 1 条星河数据加表头,空结果禁用导出,重置恢复 20 条。
+- 2026-09-10:库存流水开始日期、结束日期改为两个独立的单日期选择框,可只填一端或选择跨日范围;查询前提示开始晚于结束,列表与导出继续复用 start_date/end_date。
+- 日期拆分验证:ESLint、vue-tsc、Vite build 通过;浏览器分别选择 2026-09-01 和 2026-09-10 后查询成功,两个日期保持独立。
+
+## 2026-09-10 配送调度中心
+- `/water-dispatch` 已从 WaterWorkspace 普通列表换为独立调度看板:真实片区任务池、日期/关键词/片区筛选、配送员状态、站点地图、路线摘要/完整路线、任务状态分页及当前页 Excel 导出。原企业/库存/预警页面保持共享组件,调度详情跳转带入关键词。
+- 后端新增 `WaterDispatch.php`:看板、配送员档案、每日排班、车辆容量、待派单任务日期/时限/位置、区域和时限优先合单预览及原子确认。任务、库存、权益仍复用 WaterBusiness;完整路线单独查询,避免关键词/日期过滤漏站。
+- 已执行可重入增量迁移 `php scripts/migrate_water_dispatch.php --apply`:新增 driver/driver_shift/route 三张表,企业经纬度和配送计划/路线字段。旧任务姓名电话关联到 2 位既有测试配送员,未推断排班,当前容量使用可编辑默认 100 桶;3 单 24 桶、待派单 1/配送中 2 的状态未改变。
+- `GET /core/water/dispatch/board`、`GET /core/water/dispatch/route` 需要配送读取权限,`GET /core/water/dispatch/plan` 和 POST dispatch 新 action 需要管理权限。合单按容量及相同片区校验,原始自由文本 HTTP 派单不再使用,单笔派单也生成经校验的路线。
+- 地图使用现有高德配置,仅展示登记过的坐标和站序连线。当前模拟企业无坐标,显示真实空状态;没有道路优化、预计到达时间、GPS轨迹或配送员在线心跳。之前“地图合单未实现”的描述由本条取代:区域/容量/时限合单和坐标地图已实现,道路导航优化仍未接入。
+- 后端41项调度测试、69项供水业务、30项库存流水、30项设备接入共170项通过,新增测试记录全部回滚;ESLint、vue-tsc、Vite构建通过。浏览器确认3单24桶,搜索智配办公后任务池/路线/明细同为1单6桶,排班档案和新增表单可用,无候选方案禁用合单确认。细节与回退见 `server/docs/WATER_DISPATCH.md`。
+- 最终浏览器补充验证:配送员新增表单包含姓名、电话、片区、车辆容量;任务详情跳转补货预警自动带入智配办公,显示1条账户且关联已有配送。最终页面保持原供水侧栏样式,显示当日未排班2人和当前页Excel导出入口。
+
+## 2026-09-10 高德地图实际接入
+- 配送地图不再因无任务坐标提前退出:始终初始化真实高德底图,无坐标默认全国视野,有坐标按路线展示编号站点及顺序连线,含比例尺、缩放、查看全部站点、点选任务和失败重试。
+- `src/utils/waterAmap.ts` 统一供水地图和选点弹窗的脚本/插件加载,复用既有高德脚本标识与本地 VITE_AMAP_KEY/VITE_AMAP_SECURITY_CODE,配置值不记入文档。
+- `DispatchLocationPicker.vue` 为配送计划增加地址搜索及地图选点,确认回填后保存才写入配送任务坐标;保存后切换到对应任务组展示。后端沿用已有 schedule 校验,企业地址和库存/权益不受影响。
+- 浏览器实际确认全国高德底图、比例尺/缩放正常;上海市浦东新区世纪大道1号地址解析和标记正常,坐标回填成功;验证后取消计划,未把验证坐标保存进模拟任务。不存在道路导航或实时GPS轨迹接入。
+- 最终 ESLint、vue-tsc 和 Vite production build 均通过;已刷新4173预览加载高德地图版本。
+
+## 2026-09-10 深圳地图测试坐标
+- 用户要求以南油第四工业区4栋出发、送往中洲大厦;目的地按南山区海德一道中洲控股中心处理(已询问具体大厦,当前按此默认)。两个地址经项目高德 Geocoder 查询:出发 GCJ-02 113.924140,22.522806;送达 113.935124,22.516070。
+- 已执行 `php scripts/seed_water_shenzhen_coordinates.php --apply`,仅给任务9/10/11及其对应3个【测试0910】企业更新地址、深圳南山区片区和坐标;任务新增 origin_address/origin_longitude/origin_latitude 记录出发点,结构由原 dispatch 增量迁移扩展。
+- 三条测试任务使用同一出发点及目的地,保留原8/6/10桶、待派单/配送中/配送中状态,未额外建单或修改库存、权益、收款。脚本事务内校验业务表和非位置字段保持一致,固定批次标记复跑不覆盖后续用户修改;原测试行快照位于 server/runtime/water-shenzhen-map-before.json。
+- 地图新增橙色“起”标记,单任务起点至送达点及同路线按站序绘制示意连线;路线卡和详情显示出发地址。未调用道路导航服务。
+- 脚本 PHP lint 和41项配送回归检查通过,脚本已复跑确认 replayed=true。
+- 深圳坐标最终验证:ESLint、vue-tsc、Vite构建通过;浏览器确认深圳南山区3单24桶、橙色起点与蓝色送达点均显示,起止示意连线及出发地址正常。
+
+## 2026-09-10 高德驾车线路规划
+- 已替换 DispatchMap 的直线示意:单任务或同一真实路线具有唯一出发点及完整送达坐标时,通过 AMap.Driving 按既有站序绘制实际道路,显示预计公里/分钟(不含配送停留),支持重新规划。此前“未接入道路导航”的记录由本条更新;尚无实时GPS及各站承诺到达时间。
+- waterAmap.ts 独立加载驾车插件,每次最多16个途经点,超长路线分段并汇总,保留全部站序;连续重复位置去重,部分失败不展示不完整线路。缺坐标、多起点、未成路线的多任务组明确提示;切换时丢弃旧结果,真实路线等待完整站点接口后才规划。
+- 验证:ESLint、vue-tsc、Vite build 通过;node scripts/test-water-driving.mjs 覆盖分段不漏站、汇总、重复坐标、旧请求及服务失败。浏览器实测南油第四工业区4栋至中洲控股中心道路约2.0公里,初次11分钟、重新规划10分钟,用时随路况变化。本次未更改订单或库存等业务数据。
+
+## 2026-09-11 智能设备看板
+- 设备测试数据已添加:`seed_water_device_dashboard_demo.php --apply`固定批次12台【测试0911】设备(ID26–37)、四区各3台、298条模拟用水日记录。写入时6在线/2离线/2告警/2未接入,每页8条分两页;版本三种测试版本及未登记。心跳15分钟后自然离线,脚本复跑不覆盖用户修改。原库存、权益、订单和配送未变。
+- `src/views/water/devices/index.vue` 按用户图片重做,拆分卡片、编辑弹窗、版本弹窗、分析图表、展示函数和 `useDeviceDashboard`;接口类型在 `src/api/water/devices.ts`。
+- 新增可复用 `WaterMetricCards.vue`、`useWaterOptions.ts`、`utils/waterDownload.ts`;远程选项保留原选择、防旧请求覆盖,导出校验XLSX和错误响应。设备编辑继续走原写入接口/权限。
+- 四种互斥状态由后端统一,卡片显示真实用量/上报;查询只影响8条分页列表及全量导出,统计和图表保持全量。未接入不算离线,缺失用量显示—。
+- 固件入口为版本分布与档案维护,无OTA;趋势依赖新日累计表,无历史数据显示空状态,异常表显示当前实际故障/离线而非虚构24小时统计。
+- 45项新看板及30项接入检查通过、全部夹具回滚;前端Prettier/ESLint/vue-tsc与Vite3290模块构建通过。浏览器工具启动os error3,视觉验收未完成。详见 `../server/docs/WATER_DEVICE_DASHBOARD.md`。
+
+## 2026-09-11 库存看板
+- `src/views/water/inventory/index.vue` 替换共享工作台入口,布局按用户参考图:四张卡、左侧规格表/七日趋势、右侧库存构成;新增 `InventoryForm.vue` 和页面限定 `inventory.scss`,保留 `WaterLedgerDrawer`。
+- `getWaterInventory` / `exportWaterInventory` 接真实后端读模型;统计全量,表格搜索分页,导出全部匹配规格。路由 keyword 变化同步查询,窗口回焦刷新但避开正在操作的弹窗,失败显示明确错误。
+- 规格详情保留破损、空桶损失;入库/盘点走原写接口,盘点对象变更清空实际数量,提交中禁用编辑。读写权限入口沿用库存权限。
+- 库存50项(含HTTP)及原业务69项验证通过,事务测试数据回滚;ESLint、vue-tsc、Vite构建通过。浏览器工具启动报 os error 3,本次未做视觉验收;详见 `../server/docs/WATER_INVENTORY.md`。
+
+## 2026-09-10 配送优化策略与Eloquent模型
+- 用户明确要求最短距离、最快送达、时限优先,以及模型数据访问。本次供水业务整体切换Eloquent:model/water独立模型及关联,WaterBusiness/WaterDispatch/WaterLedger/WaterRoutePlanning、供水权限与选项读取、设备报告均不再使用Think ORM或Db::table;框架原登录和其他模块不迁移。测试及调用业务服务的种子脚本统一Eloquent事务连接。
+- service/routing 的 RouteStrategy 与三个具体策略,以有向高德道路矩阵计算固定批次站序;最多8站穷举,固定共同出发点、不返仓。最短距离按总里程,最快送达按总完成用时,时限策略按超时站数→超时总秒数→总用时。含可配置每站配送停留,无时限时按用时比较,不代表跨车队全局最优或未来路况预测。
+- 配送页面新增“线路优化”,合单预览各批新增“选择策略并优化”;可查看优化前后指标、每站到达/完成时间、超时和道路地图。仅尚未出库的已派单路线可应用;待派单批次在合单确认时应用。配送计划补齐出发地址与坐标选点。
+- POST dispatch新增 optimize / optimize_apply,plan_commit各组支持optimization_token。后端重新读取模型校验坐标并执行策略,预览绑定操作员、任务指纹,10分钟过期;保存时再次校验状态、站序、日期,行锁及原库存/权益逻辑保持一致,重复提交幂等。路线optimization保存策略及估计,地图以保存策略重新调用高德。
+- 已执行 scripts/migrate_water_routing.php --apply,仅新增sa_water_route.optimization和sa_water_route_preview。道路成本由浏览器JS API提供,后端校验格式与任务匹配,不独立核验高德来源;仅作为调度估计,不驱动扣款或库存数量。
+- 本机PHP需原生mbstring。Windows supervisor已用项目脚本重启并启用扩展;scripts/start_water.ps1及windows.php向子进程传递config/php/mbstring.ini,不修改全局php.ini。CLI回归使用php -d extension=mbstring tests/文件名。
+- 194项后端回归通过(业务69、调度41、流水30、设备30、策略/模型24),测试数据全部回滚;前端ESLint、vue-tsc、道路矩阵与取消/配额错误测试通过。浏览器实测三种策略并给生态园测试路线14保存时限优先,仍5单34桶,状态不变。现有测试路线为单站,多站不同排序由三站集成测试覆盖。详细契约及限制见server/docs/WATER_ROUTING_STRATEGIES.md。
+- 最终Vite构建通过,浏览器刷新后显示路线14已保存“时限优先”,仍5单34桶;策略选择框宽度及优化预览地图高度已调整。

+ 31 - 0
saiadmin-artd/SAIADMIN.md

@@ -0,0 +1,31 @@
+## 前端开发
+
+[module] 代表一个应用模块
+
+[business] 代表一个功能模块分组
+
+[table] 代表一个具体的功能
+
+### 前端开发规范
+
+```
+src/views/plugin/[module]/
+├── api
+│   ├── [business]/                    # 功能模块接口分组
+|   │   ├── [table].ts                 # 具体功能接口
+├── [business]/                        # 功能模块分组
+│   ├── [table]/                       # 具体功能目录
+│   │   ├── index.vue                  # 功能主页面(列表页)
+│   │   └── modules/                   # 子组件目录
+│   │       ├── table-search.vue       # 搜索表单组件
+│   │       └── edit-dialog.vue        # 编辑弹窗组件
+```
+
+### 组件说明
+
+| 组件文件 | 说明 |
+|---------|------|
+| `table.ts`  | 具体功能接口
+| `index.vue` | 功能主页面,包含列表展示、操作按钮等 |
+| `modules/table-search.vue` | 搜索表单组件,用于筛选列表数据 |
+| `modules/edit-dialog.vue` | 编辑弹窗组件,用于新增/编辑数据 |

+ 552 - 0
saiadmin-artd/alerts-build.log

@@ -0,0 +1,552 @@
+
+> art-design-pro@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+🚀 API_URL = http://127.0.0.1:8787
+🚀 VERSION = 3.0.2
+vite v7.1.7 building for production...
+transforming...
+✓ 3335 modules transformed.
+rendering chunks...
+[plugin vite:reporter] 
+(!) D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue is dynamically imported by D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/router/core/ComponentLoader.ts, D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue?vue&type=script&setup=true&lang.ts but also statically imported by D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/views/system/menu/index.vue?vue&type=script&setup=true&lang.ts, dynamic import will not move module into another chunk.
+
+computing gzip size...
+dist/index.html                                                                         1.34 kB │ gzip:   0.74 kB
+dist/assets/favicon-D4tL44Sk.ico                                                        3.55 kB
+dist/assets/login_icon-C4TVlUS8.svg                                                     4.23 kB │ gzip:   1.48 kB
+dist/assets/403-BdWuHcJA.svg                                                            4.61 kB │ gzip:   1.55 kB
+dist/assets/yd-BrGqJ6Cs.png                                                             4.63 kB
+dist/assets/sd-C0PQtrty.png                                                             4.75 kB
+dist/assets/500-DANzffU6.svg                                                            4.93 kB │ gzip:   1.76 kB
+dist/assets/404-BzxNMzaO.svg                                                            5.12 kB │ gzip:   1.94 kB
+dist/assets/draw1-Ce1WF34i.png                                                         11.32 kB
+dist/assets/logo-5EOo6F45.png                                                          18.79 kB
+dist/assets/bg_light-WsVMpqKz.webp                                                     67.25 kB
+dist/assets/bg_dark-BoZNsetB.webp                                                      70.59 kB
+dist/assets/user-bg-E5ODUodR.jpg                                                       88.58 kB
+dist/assets/el-form-item-tn0RQdqM.css                                                   0.00 kB │ gzip:   0.02 kB
+dist/assets/index-BGJDsef7.css                                                          0.04 kB │ gzip:   0.06 kB
+dist/assets/el-checkbox-group-D_6SYB2i.css                                              0.05 kB │ gzip:   0.07 kB
+dist/assets/FeaturePlaceholder-Ds3Gn_8D.css                                             0.05 kB │ gzip:   0.07 kB
+dist/assets/index-BqEhlTYo.css                                                          0.06 kB │ gzip:   0.08 kB
+dist/assets/index-DbqREa7s.css                                                          0.07 kB │ gzip:   0.09 kB
+dist/assets/DispatchForm-D7VxPRne.css                                                   0.07 kB │ gzip:   0.09 kB
+dist/assets/index-BbywE3f6.css                                                          0.09 kB │ gzip:   0.10 kB
+dist/assets/index-rl4V_FNU.css                                                          0.09 kB │ gzip:   0.10 kB
+dist/assets/DeviceBindingDialog-DwcxV8mx.css                                            0.09 kB │ gzip:   0.11 kB
+dist/assets/index-DpUm9fYV.css                                                          0.11 kB │ gzip:   0.09 kB
+dist/assets/index-BVDfV0xn.css                                                          0.11 kB │ gzip:   0.09 kB
+dist/assets/el-space-3oFudasq.css                                                       0.16 kB │ gzip:   0.12 kB
+dist/assets/el-overlay-Db7iXMEX.css                                                     0.16 kB │ gzip:   0.14 kB
+dist/assets/new-user-CrtQcHjJ.css                                                       0.17 kB │ gzip:   0.14 kB
+dist/assets/WaterLedgerDrawer-7ZPVAbw8.css                                              0.17 kB │ gzip:   0.14 kB
+dist/assets/WaterRegionField-CvMzoDDG.css                                               0.18 kB │ gzip:   0.15 kB
+dist/assets/index-bO8ksejU.css                                                          0.19 kB │ gzip:   0.12 kB
+dist/assets/AssignmentRecordsDialog-DWZuwG1L.css                                        0.19 kB │ gzip:   0.14 kB
+dist/assets/EnterpriseEditor-C4oAWjYP.css                                               0.19 kB │ gzip:   0.14 kB
+dist/assets/index-BwGJNQbf.css                                                          0.22 kB │ gzip:   0.14 kB
+dist/assets/WaterLocationPicker-CszocEzb.css                                            0.23 kB │ gzip:   0.17 kB
+dist/assets/device-binding-dialog-Ds5kxLpc.css                                          0.24 kB │ gzip:   0.18 kB
+dist/assets/index-DamTWgjx.css                                                          0.25 kB │ gzip:   0.16 kB
+dist/assets/WaterLocationField-DUiNRTRC.css                                             0.25 kB │ gzip:   0.18 kB
+dist/assets/AgentEditor-B_l1wBnz.css                                                    0.29 kB │ gzip:   0.20 kB
+dist/assets/RouteOptimization-Cw7oqFEl.css                                              0.30 kB │ gzip:   0.18 kB
+dist/assets/index-0nXq2V_I.css                                                          0.32 kB │ gzip:   0.19 kB
+dist/assets/terminal-CdVYeqnz.css                                                       0.34 kB │ gzip:   0.21 kB
+dist/assets/index-N-ubbNWb.css                                                          0.35 kB │ gzip:   0.20 kB
+dist/assets/index-CkrWr1R3.css                                                          0.35 kB │ gzip:   0.21 kB
+dist/assets/AgentDetail-9HZMX5ja.css                                                    0.42 kB │ gzip:   0.22 kB
+dist/assets/EnterpriseDetail-CmFAfq9z.css                                               0.43 kB │ gzip:   0.22 kB
+dist/assets/withdrawal-update-dialog-Bl21aWO0.css                                       0.44 kB │ gzip:   0.21 kB
+dist/assets/business-time-dialog-C2PgOXRw.css                                           0.47 kB │ gzip:   0.24 kB
+dist/assets/index-CGPX_m9B.css                                                          0.49 kB │ gzip:   0.29 kB
+dist/assets/el-row-C6BJsxyy.css                                                         0.49 kB │ gzip:   0.20 kB
+dist/assets/list-page-RHUR0sFn.css                                                      0.49 kB │ gzip:   0.26 kB
+dist/assets/list-page-DHsWYbEt.css                                                      0.51 kB │ gzip:   0.28 kB
+dist/assets/DeviceAssignmentDialog-BnOrWHVP.css                                         0.55 kB │ gzip:   0.27 kB
+dist/assets/el-image-CtVnJP8l.css                                                       0.56 kB │ gzip:   0.29 kB
+dist/assets/el-tree-select-Csemd6fU.css                                                 0.64 kB │ gzip:   0.27 kB
+dist/assets/el-divider-BUtF_RGI.css                                                     0.71 kB │ gzip:   0.31 kB
+dist/assets/index-P0GJfxvp.css                                                          0.72 kB │ gzip:   0.33 kB
+dist/assets/index-DFm-5RmY.css                                                          0.81 kB │ gzip:   0.37 kB
+dist/assets/index-BNmfVIYZ.css                                                          0.82 kB │ gzip:   0.42 kB
+dist/assets/index-BYbfSlpm.css                                                          0.85 kB │ gzip:   0.34 kB
+dist/assets/el-card-fwQOLwdi.css                                                        0.86 kB │ gzip:   0.29 kB
+dist/assets/index-B-xVoxk3.css                                                          0.87 kB │ gzip:   0.33 kB
+dist/assets/el-avatar-BmRr_O8d.css                                                      0.88 kB │ gzip:   0.34 kB
+dist/assets/withdraw-settings-BrXchJ9M.css                                              0.88 kB │ gzip:   0.38 kB
+dist/assets/WaterMetricCards-B1cgX8Z2.css                                               0.96 kB │ gzip:   0.44 kB
+dist/assets/index-Bl6Al3OB.css                                                          0.97 kB │ gzip:   0.37 kB
+dist/assets/index-DM0lwQug.css                                                          1.03 kB │ gzip:   0.41 kB
+dist/assets/index-Cttr28uV.css                                                          1.03 kB │ gzip:   0.45 kB
+dist/assets/edit-page-D3LQA3MF.css                                                      1.04 kB │ gzip:   0.46 kB
+dist/assets/DeviceAnalytics-s5xg7Mbo.css                                                1.04 kB │ gzip:   0.43 kB
+dist/assets/index-B5OaoFxa.css                                                          1.06 kB │ gzip:   0.44 kB
+dist/assets/index-FRyKr3zj.css                                                          1.12 kB │ gzip:   0.43 kB
+dist/assets/DispatchMap-BNjK_hgc.css                                                    1.15 kB │ gzip:   0.49 kB
+dist/assets/index-Bh17lM2j.css                                                          1.16 kB │ gzip:   0.46 kB
+dist/assets/edit-page-CfgtyH7r.css                                                      1.18 kB │ gzip:   0.51 kB
+dist/assets/index-Cq75mf1I.css                                                          1.19 kB │ gzip:   0.43 kB
+dist/assets/el-empty-D4ZqTl4F.css                                                       1.20 kB │ gzip:   0.47 kB
+dist/assets/edit-dialog-BnkFiCgh.css                                                    1.25 kB │ gzip:   0.41 kB
+dist/assets/el-scrollbar-BWxh-h6K.css                                                   1.33 kB │ gzip:   0.44 kB
+dist/assets/el-popover-Cktl5fHm.css                                                     1.35 kB │ gzip:   0.42 kB
+dist/assets/index-CRr3K0tv.css                                                          1.37 kB │ gzip:   0.43 kB
+dist/assets/AgentList-BHrX5aMK.css                                                      1.46 kB │ gzip:   0.49 kB
+dist/assets/preview--ha0OGK5.css                                                        1.49 kB │ gzip:   0.72 kB
+dist/assets/el-skeleton-item-BLY1jEuR.css                                               1.58 kB │ gzip:   0.54 kB
+dist/assets/index-D1DKg2E6.css                                                          1.61 kB │ gzip:   0.61 kB
+dist/assets/index-D74n0A8b.css                                                          1.69 kB │ gzip:   0.60 kB
+dist/assets/index-D2i4cPFx.css                                                          1.70 kB │ gzip:   0.56 kB
+dist/assets/index-C2nd4I3k.css                                                          1.70 kB │ gzip:   0.56 kB
+dist/assets/index-BfYUDy4T.css                                                          1.74 kB │ gzip:   0.56 kB
+dist/assets/index-TNn7A1lq.css                                                          1.75 kB │ gzip:   0.56 kB
+dist/assets/index-DlPiOlT_.css                                                          1.77 kB │ gzip:   0.57 kB
+dist/assets/index-CtE9tzvE.css                                                          1.79 kB │ gzip:   0.57 kB
+dist/assets/index-Bs3ZykK5.css                                                          1.81 kB │ gzip:   0.63 kB
+dist/assets/index-RgLuEWmr.css                                                          1.81 kB │ gzip:   0.55 kB
+dist/assets/index-DZ2luEux.css                                                          1.86 kB │ gzip:   0.63 kB
+dist/assets/index-DHLEmKR9.css                                                          1.89 kB │ gzip:   0.63 kB
+dist/assets/index-D9W-twQd.css                                                          1.89 kB │ gzip:   0.72 kB
+dist/assets/index-bMPyyuLZ.css                                                          1.89 kB │ gzip:   0.72 kB
+dist/assets/index-5Rlezp3_.css                                                          1.89 kB │ gzip:   0.67 kB
+dist/assets/index-BLKB_GTv.css                                                          1.90 kB │ gzip:   0.58 kB
+dist/assets/index-OEuMJp6Y.css                                                          1.95 kB │ gzip:   0.60 kB
+dist/assets/index-C1B1bfOu.css                                                          1.95 kB │ gzip:   0.74 kB
+dist/assets/index-DHSHLZkD.css                                                          2.00 kB │ gzip:   0.66 kB
+dist/assets/index-BShYJ36M.css                                                          2.01 kB │ gzip:   0.55 kB
+dist/assets/el-image-viewer-Ba-UrN8P.css                                                2.07 kB │ gzip:   0.65 kB
+dist/assets/index-p5mPO7rG.css                                                          2.07 kB │ gzip:   0.63 kB
+dist/assets/index-BgdAqc-4.css                                                          2.18 kB │ gzip:   0.70 kB
+dist/assets/DeviceCard-C9e8X45O.css                                                     2.19 kB │ gzip:   0.79 kB
+dist/assets/el-popper-D1i0e6ba.css                                                      2.21 kB │ gzip:   0.52 kB
+dist/assets/index-CYp7puhs.css                                                          2.26 kB │ gzip:   0.64 kB
+dist/assets/el-link-B58a4a3I.css                                                        2.38 kB │ gzip:   0.54 kB
+dist/assets/index-DYMtodT8.css                                                          2.48 kB │ gzip:   0.76 kB
+dist/assets/index-D40mQk8Y.css                                                          2.51 kB │ gzip:   0.63 kB
+dist/assets/OrderList-D6z6HKmL.css                                                      2.58 kB │ gzip:   0.70 kB
+dist/assets/el-progress-Dw9yTa91.css                                                    2.59 kB │ gzip:   0.76 kB
+dist/assets/el-timeline-item-BvbJTz1y.css                                               2.60 kB │ gzip:   0.64 kB
+dist/assets/index-3sJod6Us.css                                                          2.77 kB │ gzip:   0.67 kB
+dist/assets/index-DXk_0VAd.css                                                          2.77 kB │ gzip:   0.76 kB
+dist/assets/index-Be9jjVp7.css                                                          2.82 kB │ gzip:   0.78 kB
+dist/assets/index-J57XdXk3.css                                                          3.17 kB │ gzip:   0.87 kB
+dist/assets/el-radio-button-CSkroacn.css                                                3.25 kB │ gzip:   0.77 kB
+dist/assets/index-Cx18UXIl.css                                                          3.28 kB │ gzip:   0.88 kB
+dist/assets/edit-dialog-Cz4SMjkt.css                                                    3.29 kB │ gzip:   0.84 kB
+dist/assets/el-descriptions-item-o9ObloqJ.css                                           3.42 kB │ gzip:   0.60 kB
+dist/assets/el-alert-G57rL0jl.css                                                       3.46 kB │ gzip:   0.75 kB
+dist/assets/el-drawer-BhCnIJJ3.css                                                      3.51 kB │ gzip:   0.88 kB
+dist/assets/el-dialog-DyK7vRzj.css                                                      3.53 kB │ gzip:   0.98 kB
+dist/assets/index-BeNm5kmV.css                                                          3.82 kB │ gzip:   1.15 kB
+dist/assets/index-E8JFeY_4.css                                                          3.89 kB │ gzip:   0.93 kB
+dist/assets/el-switch-B5lTGWdM.css                                                      3.94 kB │ gzip:   0.96 kB
+dist/assets/index-y_y2pIUi.css                                                          3.95 kB │ gzip:   1.19 kB
+dist/assets/index-CtbAGBMy.css                                                          4.21 kB │ gzip:   0.90 kB
+dist/assets/el-input-number-D6iOyBgb.css                                                4.34 kB │ gzip:   0.91 kB
+dist/assets/el-radio-group-LltH1qQS.css                                                 4.39 kB │ gzip:   0.94 kB
+dist/assets/index-kDym549C.css                                                          4.57 kB │ gzip:   1.28 kB
+dist/assets/el-collapse-item-D4LG0FJ0.css                                               4.64 kB │ gzip:   0.98 kB
+dist/assets/message-CIxGxpAv.css                                                        4.69 kB │ gzip:   1.06 kB
+dist/assets/el-form-BWkJzdQ_.css                                                        4.75 kB │ gzip:   0.89 kB
+dist/assets/index-CDqzzWQr.css                                                          4.78 kB │ gzip:   1.18 kB
+dist/assets/index-ChIdo08L.css                                                          4.80 kB │ gzip:   1.16 kB
+dist/assets/index-W5cVOjuB.css                                                          5.04 kB │ gzip:   1.36 kB
+dist/assets/el-tag-DljBBxJR.css                                                         5.52 kB │ gzip:   0.87 kB
+dist/assets/water-B1ijNFo5.css                                                          5.91 kB │ gzip:   1.51 kB
+dist/assets/el-text-BYEPGzFo.css                                                        5.96 kB │ gzip:   1.34 kB
+dist/assets/index-DY1CsFsK.css                                                          5.96 kB │ gzip:   1.48 kB
+dist/assets/index-th0PFgZG.css                                                          6.05 kB │ gzip:   1.68 kB
+dist/assets/el-checkbox-DIj50LEB.css                                                    6.53 kB │ gzip:   1.21 kB
+dist/assets/el-pagination-BNQcHhjS.css                                                  6.58 kB │ gzip:   1.17 kB
+dist/assets/dict-edit-dialog-zG3lrnKK.css                                               7.05 kB │ gzip:   1.54 kB
+dist/assets/el-dropdown-item-11ZCvSOX.css                                               7.19 kB │ gzip:   1.24 kB
+dist/assets/index-BCwvCwf0.css                                                          7.62 kB │ gzip:   1.70 kB
+dist/assets/el-select-DC6_bRTH.css                                                      9.25 kB │ gzip:   1.94 kB
+dist/assets/editInfo-BmnVI_jW.css                                                       9.75 kB │ gzip:   1.77 kB
+dist/assets/el-input-tPmZxDKr.css                                                      10.83 kB │ gzip:   1.74 kB
+dist/assets/el-upload-q8uObtwj.css                                                     11.46 kB │ gzip:   2.07 kB
+dist/assets/LoginLeftView-C-JORtQp.css                                                 11.61 kB │ gzip:   2.02 kB
+dist/assets/index-BHtaHxM4.css                                                         14.76 kB │ gzip:   2.83 kB
+dist/assets/el-tab-pane-BLUvaGkK.css                                                   14.83 kB │ gzip:   2.25 kB
+dist/assets/el-button-CDqfIFiK.css                                                     15.58 kB │ gzip:   1.93 kB
+dist/assets/el-table-column-CKoPG0Y8.css                                               18.06 kB │ gzip:   2.87 kB
+dist/assets/el-date-picker-panel-BhfPqR_w.css                                          28.61 kB │ gzip:   4.15 kB
+dist/assets/overlay-C8XuAnme.css                                                       31.12 kB │ gzip:   4.31 kB
+dist/assets/index-BYGh7F6e.css                                                         36.60 kB │ gzip:   6.09 kB
+dist/assets/el-col-DD1Vn-Yu.css                                                        38.68 kB │ gzip:   3.75 kB
+dist/assets/index-CXqLAW6P.css                                                        105.08 kB │ gzip:  19.39 kB
+dist/assets/refs-Cw5r5QN8.js                                                            0.06 kB │ gzip:   0.08 kB
+dist/assets/token-DWNpOE8r.js                                                           0.08 kB │ gzip:   0.08 kB
+dist/assets/event-BwRzfsZt.js                                                           0.08 kB │ gzip:   0.09 kB
+dist/assets/validator-D-DVcHPp.js                                                       0.09 kB │ gzip:   0.10 kB
+dist/assets/cloneDeep-BFdvYfpH.js                                                       0.09 kB │ gzip:   0.10 kB
+dist/assets/_plugin-vue_export-helper-BCo6x5W8.js                                       0.09 kB │ gzip:   0.10 kB
+dist/assets/castArray-D32WP8L8.js                                                       0.14 kB │ gzip:   0.14 kB
+dist/assets/strings-P8RchPHz.js                                                         0.15 kB │ gzip:   0.15 kB
+dist/assets/error-D_Dr4eZ1.js                                                           0.15 kB │ gzip:   0.14 kB
+dist/assets/ContentPhone-B2Cmk9Ce.js                                                    0.16 kB │ gzip:   0.15 kB
+dist/assets/index-BZ_qj9jv.js                                                           0.17 kB │ gzip:   0.17 kB
+dist/assets/WaterChart-DoAUnob_.js                                                      0.18 kB │ gzip:   0.16 kB
+dist/assets/about-project-vs7PyASd.js                                                   0.23 kB │ gzip:   0.17 kB
+dist/assets/server-BOuSC9IS.js                                                          0.23 kB │ gzip:   0.16 kB
+dist/assets/active-user-CbYGxtns.js                                                     0.24 kB │ gzip:   0.19 kB
+dist/assets/dashboard-C4vGUz8H.js                                                       0.25 kB │ gzip:   0.16 kB
+dist/assets/EnterpriseAnalytics-CKlLXoxQ.js                                             0.25 kB │ gzip:   0.19 kB
+dist/assets/dynamic-stats-B4PWk6o-.js                                                   0.26 kB │ gzip:   0.20 kB
+dist/assets/attachment-LOHgX2SS.js                                                      0.28 kB │ gzip:   0.17 kB
+dist/assets/index-BI3bDMMU.js                                                           0.29 kB │ gzip:   0.24 kB
+dist/assets/sales-overview-CWXFM_Ql.js                                                  0.31 kB │ gzip:   0.21 kB
+dist/assets/card-list-BSgrUvK9.js                                                       0.31 kB │ gzip:   0.21 kB
+dist/assets/category-OWABAsyI.js                                                        0.33 kB │ gzip:   0.19 kB
+dist/assets/dept-M0_71AHN.js                                                            0.36 kB │ gzip:   0.20 kB
+dist/assets/post-DZDFWlrL.js                                                            0.36 kB │ gzip:   0.20 kB
+dist/assets/menu-C94HayRU.js                                                            0.37 kB │ gzip:   0.20 kB
+dist/assets/index-1VOXdbyF.js                                                           0.38 kB │ gzip:   0.28 kB
+dist/assets/alipay-BxexvSrN.js                                                          0.39 kB │ gzip:   0.20 kB
+dist/assets/wechat-DXxLCIIZ.js                                                          0.39 kB │ gzip:   0.20 kB
+dist/assets/index-vXSkHMHY.js                                                           0.40 kB │ gzip:   0.33 kB
+dist/assets/DispatchLocationPicker.vue_vue_type_script_setup_true_lang-WA-9_1hu.js      0.40 kB │ gzip:   0.29 kB
+dist/assets/index-wNsP-YhN.js                                                           0.42 kB │ gzip:   0.30 kB
+dist/assets/todo-list-BGwWufNX.js                                                       0.45 kB │ gzip:   0.28 kB
+dist/assets/DeliveryHistory-xn-DoK60.js                                                 0.46 kB │ gzip:   0.28 kB
+dist/assets/user-efF9-jns.js                                                            0.50 kB │ gzip:   0.23 kB
+dist/assets/crontab-Sig7gcMz.js                                                         0.51 kB │ gzip:   0.22 kB
+dist/assets/dict-BXpQWhwT.js                                                            0.51 kB │ gzip:   0.21 kB
+dist/assets/toNumber-DsyGKxn8.js                                                        0.54 kB │ gzip:   0.36 kB
+dist/assets/raf-CnVU7Egs.js                                                             0.54 kB │ gzip:   0.35 kB
+dist/assets/agentPresentation-BQCnbZ9m.js                                               0.54 kB │ gzip:   0.41 kB
+dist/assets/index-DBe6Ufrr.js                                                           0.54 kB │ gzip:   0.29 kB
+dist/assets/database-D6Xs6LRV.js                                                        0.56 kB │ gzip:   0.23 kB
+dist/assets/WaterMetricCards-Df36SF_u.js                                                0.59 kB │ gzip:   0.39 kB
+dist/assets/index-CobdRvho.js                                                           0.60 kB │ gzip:   0.34 kB
+dist/assets/role-C7GEhAtX.js                                                            0.62 kB │ gzip:   0.25 kB
+dist/assets/waterDownload-Cu-N2cSH.js                                                   0.64 kB │ gzip:   0.46 kB
+dist/assets/use-form-common-props-6LOd5EIR.js                                           0.64 kB │ gzip:   0.35 kB
+dist/assets/index-BXZSw8O4.js                                                           0.65 kB │ gzip:   0.47 kB
+dist/assets/config-D9LltFYW.js                                                          0.65 kB │ gzip:   0.24 kB
+dist/assets/generate-B-uMVy5r.js                                                        0.66 kB │ gzip:   0.26 kB
+dist/assets/FeaturePlaceholder-B4dOcZNS.js                                              0.68 kB │ gzip:   0.45 kB
+dist/assets/agent-D1ig2k2x.js                                                           0.79 kB │ gzip:   0.29 kB
+dist/assets/index-3eRhwTFy.js                                                           0.80 kB │ gzip:   0.50 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-Br-BhdWF.js                       0.83 kB │ gzip:   0.50 kB
+dist/assets/install-box-Ay4uMrKD.js                                                     0.92 kB │ gzip:   0.46 kB
+dist/assets/table-search-QE1_8NXz.js                                                    0.93 kB │ gzip:   0.46 kB
+dist/assets/table-search-DJwx6FUa.js                                                    0.93 kB │ gzip:   0.46 kB
+dist/assets/sales-overview.vue_vue_type_script_setup_true_lang-XGEOj_W-.js              0.93 kB │ gzip:   0.60 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-C5oRgut9.js                       0.93 kB │ gzip:   0.57 kB
+dist/assets/index-CblNsMeh.js                                                           0.94 kB │ gzip:   0.52 kB
+dist/assets/useSaiAdmin-BkvGgjJx.js                                                     0.96 kB │ gzip:   0.57 kB
+dist/assets/DispatchLocationPicker-BtAZEmOy.js                                          0.99 kB │ gzip:   0.50 kB
+dist/assets/edit-dialog-CHc-U6h7.js                                                     1.00 kB │ gzip:   0.49 kB
+dist/assets/use-form-item-BKwxC-gI.js                                                   1.02 kB │ gzip:   0.56 kB
+dist/assets/group-edit-dialog-DT4sVQGb.js                                               1.03 kB │ gzip:   0.50 kB
+dist/assets/business-time-dialog-CpVqb9BF.js                                            1.03 kB │ gzip:   0.50 kB
+dist/assets/Iframe-DDnKRRdd.js                                                          1.03 kB │ gzip:   0.61 kB
+dist/assets/debounce-DYpJu5yI.js                                                        1.04 kB │ gzip:   0.56 kB
+dist/assets/permission-dialog-B4jeGkr4.js                                               1.08 kB │ gzip:   0.52 kB
+dist/assets/_baseIteratee-Bk5tC3tu.js                                                   1.13 kB │ gzip:   0.60 kB
+dist/assets/vnode-BhMb0PFG.js                                                           1.14 kB │ gzip:   0.61 kB
+dist/assets/index-7eJiGS4s.js                                                           1.15 kB │ gzip:   0.63 kB
+dist/assets/index-irrXZYpM.js                                                           1.15 kB │ gzip:   0.63 kB
+dist/assets/AlertPolicyDialog-CVvti-x_.js                                               1.15 kB │ gzip:   0.54 kB
+dist/assets/index-CwQGc9BJ.js                                                           1.16 kB │ gzip:   0.63 kB
+dist/assets/_flatRest-BiEhXM81.js                                                       1.16 kB │ gzip:   0.63 kB
+dist/assets/merchant-DLeuTZfG.js                                                        1.16 kB │ gzip:   0.40 kB
+dist/assets/type-edit-dialog-BbyAnnw3.js                                                1.17 kB │ gzip:   0.53 kB
+dist/assets/ArtException.vue_vue_type_script_setup_true_lang-CaSPmyj4.js                1.19 kB │ gzip:   0.71 kB
+dist/assets/table-dialog-BUQ9WV5m.js                                                    1.19 kB │ gzip:   0.57 kB
+dist/assets/scroll-CVx4ve6c.js                                                          1.20 kB │ gzip:   0.62 kB
+dist/assets/el-divider-DKxN7x3m.js                                                      1.21 kB │ gzip:   0.67 kB
+dist/assets/index-DnC5Y_bW.js                                                           1.21 kB │ gzip:   0.83 kB
+dist/assets/FirmwareDialog-BbBKDvz6.js                                                  1.22 kB │ gzip:   0.58 kB
+dist/assets/edit-dialog-Bm4NaX-b.js                                                     1.23 kB │ gzip:   0.55 kB
+dist/assets/edit-dialog-BuLCwNV2.js                                                     1.23 kB │ gzip:   0.55 kB
+dist/assets/table-search-BV4Nkuyv.js                                                    1.27 kB │ gzip:   0.58 kB
+dist/assets/index-Zy9aY0HA.js                                                           1.29 kB │ gzip:   0.60 kB
+dist/assets/el-row-C_r2Wsp_.js                                                          1.31 kB │ gzip:   0.73 kB
+dist/assets/aria-Dle50h-X.js                                                            1.37 kB │ gzip:   0.73 kB
+dist/assets/index-nuA1aUzE.js                                                           1.38 kB │ gzip:   0.71 kB
+dist/assets/dict-edit-dialog-D4LUKxGo.js                                                1.38 kB │ gzip:   0.60 kB
+dist/assets/todo-list.vue_vue_type_script_setup_true_lang-DgLaZ64t.js                   1.38 kB │ gzip:   0.77 kB
+dist/assets/ContentPreviewDialog-BIUf-86G.js                                            1.39 kB │ gzip:   0.62 kB
+dist/assets/_initCloneObject-D_DAmRLL.js                                                1.40 kB │ gzip:   0.71 kB
+dist/assets/withdrawal-update-dialog-BmVektjN.js                                        1.41 kB │ gzip:   0.64 kB
+dist/assets/work-dialog-AQqlTQMv.js                                                     1.44 kB │ gzip:   0.64 kB
+dist/assets/BatchDeliveryDialog-C33fA6Bs.js                                             1.44 kB │ gzip:   0.64 kB
+dist/assets/table-search-nzAt7yg7.js                                                    1.44 kB │ gzip:   0.64 kB
+dist/assets/DeviceEditor-CjCXA4_l.js                                                    1.45 kB │ gzip:   0.63 kB
+dist/assets/dict-search-PxXKc_TD.js                                                     1.47 kB │ gzip:   0.65 kB
+dist/assets/settingComponent-DCVrW0zn.js                                                1.47 kB │ gzip:   0.65 kB
+dist/assets/about-project.vue_vue_type_script_setup_true_lang-BNLASxPU.js               1.47 kB │ gzip:   0.96 kB
+dist/assets/table-search-D9wEqEsy.js                                                    1.47 kB │ gzip:   0.65 kB
+dist/assets/table-search-DlVx_Gfp.js                                                    1.47 kB │ gzip:   0.65 kB
+dist/assets/table-search-BdIk5EQV.js                                                    1.47 kB │ gzip:   0.65 kB
+dist/assets/table-search-TU8dCqGm.js                                                    1.47 kB │ gzip:   0.65 kB
+dist/assets/table-search-XS5zyhlF.js                                                    1.47 kB │ gzip:   0.65 kB
+dist/assets/table-search-D6mgIiHA.js                                                    1.47 kB │ gzip:   0.65 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BgRwEE8u.js                1.48 kB │ gzip:   0.83 kB
+dist/assets/ReceiptDialog-Cm-Pv-rB.js                                                   1.49 kB │ gzip:   0.65 kB
+dist/assets/index-YdqvTHjT.js                                                           1.49 kB │ gzip:   0.81 kB
+dist/assets/el-card-BWmT5WXk.js                                                         1.51 kB │ gzip:   0.74 kB
+dist/assets/table-search-DvfcPmsa.js                                                    1.51 kB │ gzip:   0.67 kB
+dist/assets/table-search-BigOyNvw.js                                                    1.51 kB │ gzip:   0.67 kB
+dist/assets/dynamic-stats.vue_vue_type_script_setup_true_lang-D6jK-hCj.js               1.52 kB │ gzip:   0.85 kB
+dist/assets/table-edit-SuFESznG.js                                                      1.52 kB │ gzip:   0.66 kB
+dist/assets/move-dialog-BTtEXttz.js                                                     1.53 kB │ gzip:   0.68 kB
+dist/assets/ArtResultPage.vue_vue_type_script_setup_true_lang-BCXGuiCX.js               1.53 kB │ gzip:   0.85 kB
+dist/assets/InventoryForm-8ekXoKPv.js                                                   1.56 kB │ gzip:   0.68 kB
+dist/assets/FulfillmentDialog-CjRXWj84.js                                               1.58 kB │ gzip:   0.68 kB
+dist/assets/category-dialog-BkOBCv9Q.js                                                 1.62 kB │ gzip:   0.70 kB
+dist/assets/index-C2BCy7Ks.js                                                           1.63 kB │ gzip:   0.97 kB
+dist/assets/config-edit-dialog-CDlP25Hm.js                                              1.65 kB │ gzip:   0.70 kB
+dist/assets/el-avatar-CEOoolxR.js                                                       1.66 kB │ gzip:   0.91 kB
+dist/assets/ContentEditor-C1eqqAUM.js                                                   1.67 kB │ gzip:   0.72 kB
+dist/assets/index-vb0vWSiC.js                                                           1.71 kB │ gzip:   0.97 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-D22vh8YK.js                1.72 kB │ gzip:   0.86 kB
+dist/assets/index-DWxgywsN.js                                                           1.72 kB │ gzip:   0.91 kB
+dist/assets/edit-dialog-rwRz4_mD.js                                                     1.73 kB │ gzip:   0.72 kB
+dist/assets/table-dialog.vue_vue_type_script_setup_true_lang-DR3oRJH1.js                1.76 kB │ gzip:   0.96 kB
+dist/assets/el-col-DZq76RAf.js                                                          1.79 kB │ gzip:   0.85 kB
+dist/assets/loadTable-Dt3m4DtY.js                                                       1.86 kB │ gzip:   0.80 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-8_Ya7CcB.js                       1.86 kB │ gzip:   0.94 kB
+dist/assets/index-2JZjp-Ks.js                                                           1.87 kB │ gzip:   0.96 kB
+dist/assets/DeviceCard-CDclno-C.js                                                      1.87 kB │ gzip:   1.05 kB
+dist/assets/recycle-list-CKtchwtN.js                                                    1.89 kB │ gzip:   0.81 kB
+dist/assets/log-list-BRViXhX1.js                                                        1.96 kB │ gzip:   0.84 kB
+dist/assets/edit-dialog-D3BCN99U.js                                                     2.00 kB │ gzip:   0.83 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-D54IpsTS.js                2.00 kB │ gzip:   0.95 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-CuU_qyUz.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-D6vbisQO.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-DyOJBGY8.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-D9WUvJqC.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-Dtr2zh4q.js                2.02 kB │ gzip:   0.95 kB
+dist/assets/dict-search.vue_vue_type_script_setup_true_lang-B1z4O02u.js                 2.02 kB │ gzip:   0.94 kB
+dist/assets/FirmwareDialog.vue_vue_type_script_setup_true_lang-BivQvPKA.js              2.03 kB │ gzip:   1.16 kB
+dist/assets/index-BK52trxG.js                                                           2.05 kB │ gzip:   0.76 kB
+dist/assets/WaterRegionField-DtB9_ESG.js                                                2.07 kB │ gzip:   1.11 kB
+dist/assets/useHeaderBar-BIciI3wq.js                                                    2.08 kB │ gzip:   0.97 kB
+dist/assets/el-link-BsYl1Hbw.js                                                         2.15 kB │ gzip:   1.07 kB
+dist/assets/index-CF0sutbC.js                                                           2.16 kB │ gzip:   0.96 kB
+dist/assets/index-BsUTLvcL.js                                                           2.16 kB │ gzip:   0.96 kB
+dist/assets/edit-dialog-CasQuNaQ.js                                                     2.16 kB │ gzip:   0.89 kB
+dist/assets/editInfo-D7SN1YTk.js                                                        2.17 kB │ gzip:   0.89 kB
+dist/assets/el-skeleton-item-Dwm2GrJ6.js                                                2.21 kB │ gzip:   1.11 kB
+dist/assets/el-timeline-item-C-S9BBqg.js                                                2.22 kB │ gzip:   1.00 kB
+dist/assets/index-C4jF0Cld.js                                                           2.29 kB │ gzip:   1.05 kB
+dist/assets/WaterWorkspace-DZdxzxfw.js                                                  2.30 kB │ gzip:   0.96 kB
+dist/assets/config-list-CTn96z3h.js                                                     2.31 kB │ gzip:   0.93 kB
+dist/assets/index-DqMif3-z.js                                                           2.34 kB │ gzip:   1.02 kB
+dist/assets/index-BaSWHAyV.js                                                           2.35 kB │ gzip:   1.03 kB
+dist/assets/EnterpriseAnalytics.vue_vue_type_script_setup_true_lang-CeBcqIpW.js         2.37 kB │ gzip:   1.28 kB
+dist/assets/WaterLocationField-ftehRYa4.js                                              2.38 kB │ gzip:   1.28 kB
+dist/assets/index-i1o_KCyT.js                                                           2.40 kB │ gzip:   0.95 kB
+dist/assets/index-DMS0HJ5p.js                                                           2.42 kB │ gzip:   1.12 kB
+dist/assets/index-BWnswm8I.js                                                           2.45 kB │ gzip:   1.13 kB
+dist/assets/FileSaver.min-B5AjALdM.js                                                   2.49 kB │ gzip:   1.19 kB
+dist/assets/DeliveryHistory.vue_vue_type_script_setup_true_lang-QbFG5m0c.js             2.50 kB │ gzip:   1.43 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BKRqNY-Y.js                2.52 kB │ gzip:   1.03 kB
+dist/assets/index-06D4rUDe.js                                                           2.53 kB │ gzip:   1.21 kB
+dist/assets/el-space-DBn8H5Ke.js                                                        2.54 kB │ gzip:   1.19 kB
+dist/assets/el-alert-CTNsigqm.js                                                        2.55 kB │ gzip:   1.19 kB
+dist/assets/work-dialog.vue_vue_type_script_setup_true_lang-CuL5XKuj.js                 2.56 kB │ gzip:   1.34 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-CtpQDIyO.js                       2.57 kB │ gzip:   1.11 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-B1D8iwuY.js                2.57 kB │ gzip:   1.14 kB
+dist/assets/index-BCLlDChW.js                                                           2.64 kB │ gzip:   1.35 kB
+dist/assets/index-B5wM5S3H.js                                                           2.65 kB │ gzip:   1.36 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-BlZ4JWvz.js                 2.65 kB │ gzip:   1.39 kB
+dist/assets/business-time-dialog.vue_vue_type_style_index_0_lang-BlhB8bMZ.js            2.68 kB │ gzip:   1.35 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-C3INRSlm.js                2.74 kB │ gzip:   1.22 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BswZCeae.js                2.75 kB │ gzip:   1.23 kB
+dist/assets/dropdown-Bonkan7A.js                                                        2.78 kB │ gzip:   1.25 kB
+dist/assets/index-glw9nfLY.js                                                           2.84 kB │ gzip:   1.25 kB
+dist/assets/index-BDwGcf99.js                                                           2.95 kB │ gzip:   1.09 kB
+dist/assets/_baseClone-Dg4Na3eK.js                                                      3.02 kB │ gzip:   1.27 kB
+dist/assets/index-BeNM7BX8.js                                                           3.07 kB │ gzip:   1.52 kB
+dist/assets/index-C2EeKa3A.js                                                           3.07 kB │ gzip:   1.77 kB
+dist/assets/permission-dialog.vue_vue_type_script_setup_true_lang-BRGO1_eq.js           3.08 kB │ gzip:   1.62 kB
+dist/assets/index-Dm7I-WjQ.js                                                           3.15 kB │ gzip:   1.49 kB
+dist/assets/dict-search-WP_6pYUI.js                                                     3.15 kB │ gzip:   1.42 kB
+dist/assets/group-edit-dialog.vue_vue_type_script_setup_true_lang-BTRliuOl.js           3.15 kB │ gzip:   1.48 kB
+dist/assets/index-ewuWYOXS.js                                                           3.15 kB │ gzip:   1.41 kB
+dist/assets/DeviceBindingDialog-CZ0BDam9.js                                             3.16 kB │ gzip:   1.67 kB
+dist/assets/move-dialog.vue_vue_type_script_setup_true_lang-DNVq11ZG.js                 3.16 kB │ gzip:   1.62 kB
+dist/assets/table-search-BZKOTpW0.js                                                    3.18 kB │ gzip:   1.44 kB
+dist/assets/withdrawal-update-dialog.vue_vue_type_style_index_0_lang--TiEdsfF.js        3.23 kB │ gzip:   1.45 kB
+dist/assets/ContentPhone.vue_vue_type_script_setup_true_lang-DxxcqFIE.js                3.31 kB │ gzip:   1.75 kB
+dist/assets/preview-DxiIjNqC.js                                                         3.31 kB │ gzip:   1.73 kB
+dist/assets/DeviceAnalytics-D6AhNITj.js                                                 3.37 kB │ gzip:   1.87 kB
+dist/assets/type-edit-dialog.vue_vue_type_script_setup_true_lang-D5cBbSRf.js            3.40 kB │ gzip:   1.57 kB
+dist/assets/index-BvyQbp4-.js                                                           3.48 kB │ gzip:   1.44 kB
+dist/assets/index-degh2CL_.js                                                           3.50 kB │ gzip:   1.52 kB
+dist/assets/category-dialog.vue_vue_type_script_setup_true_lang-DGG0gHwF.js             3.63 kB │ gzip:   1.71 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-Bn-jKXTP.js                 3.69 kB │ gzip:   1.65 kB
+dist/assets/index-BD11asmI.js                                                           3.70 kB │ gzip:   1.75 kB
+dist/assets/index-DMFkAu0N.js                                                           3.71 kB │ gzip:   1.57 kB
+dist/assets/BatchDeliveryDialog.vue_vue_type_script_setup_true_lang-CQXSs9BV.js         3.75 kB │ gzip:   1.93 kB
+dist/assets/active-user.vue_vue_type_script_setup_true_lang-D0lG8KAv.js                 3.85 kB │ gzip:   1.71 kB
+dist/assets/index-By7p3ExI.js                                                           3.89 kB │ gzip:   1.82 kB
+dist/assets/new-user-DBCLizir.js                                                        3.95 kB │ gzip:   1.90 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-DzjRVbPq.js                       3.97 kB │ gzip:   1.96 kB
+dist/assets/recycle-list.vue_vue_type_script_setup_true_lang-CCbAWwi2.js                3.98 kB │ gzip:   1.93 kB
+dist/assets/config-list.vue_vue_type_script_setup_true_lang-DTdEUPSc.js                 3.98 kB │ gzip:   1.91 kB
+dist/assets/ContentPreviewDialog.vue_vue_type_script_setup_true_lang-B5gD1WtT.js        4.01 kB │ gzip:   2.06 kB
+dist/assets/index-xrfy-5o4.js                                                           4.07 kB │ gzip:   2.15 kB
+dist/assets/list-page-90BaWh8E.js                                                       4.07 kB │ gzip:   1.92 kB
+dist/assets/index.vue_vue_type_style_index_0_lang-B_O9AHVj.js                           4.08 kB │ gzip:   2.01 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-k_0TrWzm.js                 4.09 kB │ gzip:   1.83 kB
+dist/assets/index-CjXgwI27.js                                                           4.10 kB │ gzip:   1.62 kB
+dist/assets/settingComponent.vue_vue_type_script_setup_true_lang-CuWUmGFF.js            4.12 kB │ gzip:   1.67 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-Ckt54kqz.js                       4.16 kB │ gzip:   1.84 kB
+dist/assets/install-box.vue_vue_type_script_setup_true_lang-C_aEkyf0.js                 4.17 kB │ gzip:   2.00 kB
+dist/assets/list-page-CbqUY7rt.js                                                       4.23 kB │ gzip:   1.90 kB
+dist/assets/index-BaM76Pdh.js                                                           4.45 kB │ gzip:   2.23 kB
+dist/assets/el-overlay-DpEcrDxw.js                                                      4.66 kB │ gzip:   1.83 kB
+dist/assets/index-B6AIW6qm.js                                                           4.68 kB │ gzip:   2.36 kB
+dist/assets/log-list.vue_vue_type_script_setup_true_lang-BUoPMYg5.js                    4.69 kB │ gzip:   2.30 kB
+dist/assets/el-progress-BEPbNSyW.js                                                     4.73 kB │ gzip:   1.98 kB
+dist/assets/AlertPolicyDialog.vue_vue_type_script_setup_true_lang-9QrpJe8s.js           4.77 kB │ gzip:   2.32 kB
+dist/assets/el-empty-PSR2X2hN.js                                                        4.82 kB │ gzip:   1.68 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-Dmswx6mh.js                       4.82 kB │ gzip:   1.94 kB
+dist/assets/loadTable.vue_vue_type_script_setup_true_lang-B7a6zu0i.js                   4.86 kB │ gzip:   2.36 kB
+dist/assets/el-image-Bt8CSIxv.js                                                        4.99 kB │ gzip:   2.24 kB
+dist/assets/index-BcjnP1_n.js                                                           5.00 kB │ gzip:   2.18 kB
+dist/assets/el-collapse-item-BvAkP6Nm.js                                                5.01 kB │ gzip:   2.19 kB
+dist/assets/el-drawer-DcKqY9Z0.js                                                       5.09 kB │ gzip:   2.32 kB
+dist/assets/waterAmap-DZ8_MN8P.js                                                       5.13 kB │ gzip:   2.30 kB
+dist/assets/el-switch-DyFpkZxs.js                                                       5.17 kB │ gzip:   1.93 kB
+dist/assets/el-dialog-CW3sE6H2.js                                                       5.17 kB │ gzip:   2.24 kB
+dist/assets/index-x2hsAo3u.js                                                           5.24 kB │ gzip:   2.29 kB
+dist/assets/el-descriptions-item-BSFmo9-V.js                                            5.27 kB │ gzip:   2.09 kB
+dist/assets/index-DETm19Jw.js                                                           5.32 kB │ gzip:   2.34 kB
+dist/assets/config-edit-dialog.vue_vue_type_script_setup_true_lang-CzuifF73.js          5.43 kB │ gzip:   2.22 kB
+dist/assets/card-list.vue_vue_type_script_setup_true_lang-BHaxNW8D.js                   5.45 kB │ gzip:   1.94 kB
+dist/assets/LoginLeftView-CcLPO_t2.js                                                   5.48 kB │ gzip:   2.19 kB
+dist/assets/index-DzylpyUX.js                                                           5.49 kB │ gzip:   2.50 kB
+dist/assets/index-BDNwX9Ht.js                                                           5.49 kB │ gzip:   2.25 kB
+dist/assets/index-Bok8PIQd.js                                                           5.51 kB │ gzip:   2.49 kB
+dist/assets/withdraw-settings-B_UlVP7J.js                                               5.55 kB │ gzip:   1.82 kB
+dist/assets/DeviceEditor.vue_vue_type_script_setup_true_lang-DrFoJiOl.js                5.63 kB │ gzip:   2.43 kB
+dist/assets/WaterLocationPicker-BmNFGYKW.js                                             5.67 kB │ gzip:   2.71 kB
+dist/assets/index-Bm1x2q9M.js                                                           5.71 kB │ gzip:   2.29 kB
+dist/assets/index-C6J5IjFQ.js                                                           5.74 kB │ gzip:   2.32 kB
+dist/assets/index-Bv-PIk6X.js                                                           5.77 kB │ gzip:   2.35 kB
+dist/assets/index-CzbMhTHz.js                                                           5.81 kB │ gzip:   2.58 kB
+dist/assets/el-radio-group-QTgMggQS.js                                                  5.88 kB │ gzip:   2.32 kB
+dist/assets/EnterpriseDetail-D_w94L5I.js                                                5.90 kB │ gzip:   2.75 kB
+dist/assets/AgentDetail-CzfQdazc.js                                                     5.94 kB │ gzip:   2.62 kB
+dist/assets/focus-trap-Bgff6Za9.js                                                      5.97 kB │ gzip:   2.41 kB
+dist/assets/index-BOFR4-6U.js                                                           6.03 kB │ gzip:   2.53 kB
+dist/assets/edit-page-BX-XhXRu.js                                                       6.05 kB │ gzip:   2.56 kB
+dist/assets/AssignmentRecordsDialog-BfhYw9qG.js                                         6.12 kB │ gzip:   2.73 kB
+dist/assets/index-BK_bRAkM.js                                                           6.13 kB │ gzip:   2.81 kB
+dist/assets/device-binding-dialog-BTrqAyjH.js                                           6.16 kB │ gzip:   2.96 kB
+dist/assets/index-BXex_wXz.js                                                           6.20 kB │ gzip:   2.87 kB
+dist/assets/index-DSiZe3xT.js                                                           6.20 kB │ gzip:   2.66 kB
+dist/assets/index-mfoB6_eV.js                                                           6.22 kB │ gzip:   2.18 kB
+dist/assets/table-edit.vue_vue_type_script_setup_true_lang-BFUb8EyJ.js                  6.24 kB │ gzip:   2.38 kB
+dist/assets/index-CL8TnkN2.js                                                           6.31 kB │ gzip:   2.92 kB
+dist/assets/DispatchMap-DEoHG5ac.js                                                     6.37 kB │ gzip:   3.18 kB
+dist/assets/index-CL5ZWPf1.js                                                           6.44 kB │ gzip:   2.78 kB
+dist/assets/ReceiptDialog.vue_vue_type_script_setup_true_lang-DXRqIm6K.js               6.45 kB │ gzip:   2.92 kB
+dist/assets/index-DKGwiLl2.js                                                           6.56 kB │ gzip:   2.86 kB
+dist/assets/el-tree-select-DbLoLH1n.js                                                  6.68 kB │ gzip:   2.67 kB
+dist/assets/index-C0GdcjQ6.js                                                           6.73 kB │ gzip:   2.84 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-Bh_urzn1.js                 6.92 kB │ gzip:   2.54 kB
+dist/assets/useChart-Cfgzd1EO.js                                                        6.96 kB │ gzip:   3.14 kB
+dist/assets/index-CDyTLDXj.js                                                           7.00 kB │ gzip:   2.89 kB
+dist/assets/index-UouodZYy.js                                                           7.13 kB │ gzip:   2.98 kB
+dist/assets/EnterpriseEditor-CeBqydqU.js                                                7.20 kB │ gzip:   3.10 kB
+dist/assets/index-qSoc0u5m.js                                                           7.25 kB │ gzip:   3.06 kB
+dist/assets/el-input-number-BRDEYhQ9.js                                                 7.26 kB │ gzip:   2.94 kB
+dist/assets/index-Dvz0MKZb.js                                                           7.30 kB │ gzip:   2.03 kB
+dist/assets/ContentEditor.vue_vue_type_script_setup_true_lang-Luy0QIjS.js               7.30 kB │ gzip:   3.05 kB
+dist/assets/index-CVpgC3ov.js                                                           7.36 kB │ gzip:   2.76 kB
+dist/assets/el-image-viewer-CZ0gXo-S.js                                                 7.45 kB │ gzip:   3.03 kB
+dist/assets/WaterLedgerDrawer-BaNFYWuG.js                                               7.48 kB │ gzip:   3.40 kB
+dist/assets/index-Dca6f1hj.js                                                           7.50 kB │ gzip:   4.51 kB
+dist/assets/index-DrQ90acv.js                                                           7.51 kB │ gzip:   2.80 kB
+dist/assets/catalog-CKg_I-5f.js                                                         7.51 kB │ gzip:   2.68 kB
+dist/assets/InventoryForm.vue_vue_type_script_setup_true_lang-CiHqItrp.js               7.63 kB │ gzip:   3.00 kB
+dist/assets/el-scrollbar-Ji4ngQJ0.js                                                    7.69 kB │ gzip:   3.09 kB
+dist/assets/index-jmIbt0y0.js                                                           7.79 kB │ gzip:   3.76 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-DrsVgj4T.js                 7.86 kB │ gzip:   2.73 kB
+dist/assets/DeviceAssignmentDialog-BlZiIgSc.js                                          8.27 kB │ gzip:   3.76 kB
+dist/assets/AgentEditor-BL4nzEb7.js                                                     8.31 kB │ gzip:   3.69 kB
+dist/assets/index-5UUuwEso.js                                                           8.42 kB │ gzip:   3.43 kB
+dist/assets/edit-page-DR0dRgE6.js                                                       8.47 kB │ gzip:   3.13 kB
+dist/assets/FulfillmentDialog.vue_vue_type_script_setup_true_lang-BBcNByAk.js           8.52 kB │ gzip:   3.50 kB
+dist/assets/index-VemnSsCh.js                                                           8.54 kB │ gzip:   3.55 kB
+dist/assets/index-D_Um3Ae0.js                                                           8.66 kB │ gzip:   3.62 kB
+dist/assets/index-UzaMhOP8.js                                                           8.70 kB │ gzip:   3.72 kB
+dist/assets/index-BBx0X8th.js                                                           9.00 kB │ gzip:   3.79 kB
+dist/assets/RouteOptimization-DyaSoNmK.js                                               9.19 kB │ gzip:   4.23 kB
+dist/assets/index-BH_lQ3Gp.js                                                           9.25 kB │ gzip:   3.76 kB
+dist/assets/index-BhzbovL8.js                                                           9.27 kB │ gzip:   4.07 kB
+dist/assets/index-CZEBw7jh.js                                                           9.32 kB │ gzip:   4.89 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-jTZBBH3Z.js                 9.34 kB │ gzip:   3.80 kB
+dist/assets/index-CUWb_7Dh.js                                                           9.47 kB │ gzip:   3.74 kB
+dist/assets/index-DoZuKjWd.js                                                           9.74 kB │ gzip:   4.06 kB
+dist/assets/index-CoU_EVUV.js                                                           9.84 kB │ gzip:   3.88 kB
+dist/assets/el-checkbox-CpxmtTUs.js                                                    10.06 kB │ gzip:   3.50 kB
+dist/assets/index-CSI-kjrS.js                                                          10.08 kB │ gzip:   4.04 kB
+dist/assets/index-tpWj3NUX.js                                                          10.16 kB │ gzip:   3.71 kB
+dist/assets/index-Ce4DG3yI.js                                                          10.22 kB │ gzip:   4.63 kB
+dist/assets/index-DPjvFq7R.js                                                          10.38 kB │ gzip:   3.96 kB
+dist/assets/index-25IZg10C.js                                                          10.59 kB │ gzip:   4.06 kB
+dist/assets/index-CWRWz5Nd.js                                                          11.10 kB │ gzip:   4.16 kB
+dist/assets/avatar6-6Evj8BB9.js                                                        11.25 kB │ gzip:   8.36 kB
+dist/assets/AgentList-BO9NC3e5.js                                                      11.26 kB │ gzip:   4.58 kB
+dist/assets/terminal-yGaU0CEN.js                                                       11.33 kB │ gzip:   4.37 kB
+dist/assets/DispatchForm-DsfxsJiM.js                                                   11.39 kB │ gzip:   4.19 kB
+dist/assets/index-B5sHdZ4P.js                                                          11.44 kB │ gzip:   4.74 kB
+dist/assets/overlay-Cz2RaNZ7.js                                                        11.61 kB │ gzip:   4.31 kB
+dist/assets/index-6S7LHzGs.js                                                          11.74 kB │ gzip:   5.04 kB
+dist/assets/OrderList-CPuHIwLE.js                                                      11.98 kB │ gzip:   4.07 kB
+dist/assets/index-DKu_ZKwb.js                                                          12.05 kB │ gzip:   4.46 kB
+dist/assets/el-pagination-DC5A4USz.js                                                  12.16 kB │ gzip:   4.12 kB
+dist/assets/index-DMOx5rIY.js                                                          12.43 kB │ gzip:   4.47 kB
+dist/assets/index-CTBQQ0oE.js                                                          12.44 kB │ gzip:   4.23 kB
+dist/assets/index-Bq7FGciW.js                                                          12.55 kB │ gzip:   4.01 kB
+dist/assets/index-CIrRaBEQ.js                                                          12.64 kB │ gzip:   5.02 kB
+dist/assets/el-tab-pane-vy9ZUJbc.js                                                    12.71 kB │ gzip:   5.33 kB
+dist/assets/index-BGgECCbM.js                                                          12.73 kB │ gzip:   5.42 kB
+dist/assets/index-DeCKbN1M.js                                                          13.23 kB │ gzip:   4.74 kB
+dist/assets/index-CjjoEshN.js                                                          13.32 kB │ gzip:   4.46 kB
+dist/assets/el-upload-DGvQPGOM.js                                                      13.78 kB │ gzip:   4.97 kB
+dist/assets/index-Bv2VpEWX.js                                                          13.87 kB │ gzip:   5.71 kB
+dist/assets/index-DavOImTt.js                                                          14.24 kB │ gzip:   5.11 kB
+dist/assets/el-dropdown-item-cegLQ7Ba.js                                               14.28 kB │ gzip:   5.02 kB
+dist/assets/index-QbepMekZ.js                                                          14.33 kB │ gzip:   5.97 kB
+dist/assets/index-Bcp9kH3F.js                                                          14.76 kB │ gzip:   5.45 kB
+dist/assets/index-O919jR6T.js                                                          14.96 kB │ gzip:   5.38 kB
+dist/assets/index-DXGFM0h_.js                                                          15.32 kB │ gzip:   5.48 kB
+dist/assets/index-Bdr-2OzD.js                                                          17.08 kB │ gzip:   6.85 kB
+dist/assets/useTable-BoG9sSSW.js                                                       18.11 kB │ gzip:   7.66 kB
+dist/assets/device-DdhPWvMM.js                                                         18.12 kB │ gzip:   6.57 kB
+dist/assets/iconify-bwtPFjU3.js                                                        18.54 kB │ gzip:   7.06 kB
+dist/assets/index-D9m71ku-.js                                                          19.11 kB │ gzip:   7.36 kB
+dist/assets/el-button-B7zhhpJL.js                                                      19.98 kB │ gzip:   6.95 kB
+dist/assets/dict-edit-dialog.vue_vue_type_script_setup_true_lang-C-o80Lio.js           21.42 kB │ gzip:   7.44 kB
+dist/assets/index-XXd0FACZ.js                                                          22.02 kB │ gzip:   7.65 kB
+dist/assets/WaterWorkspace.vue_vue_type_script_setup_true_lang-XUgF3DiZ.js             22.81 kB │ gzip:   8.70 kB
+dist/assets/index-C1ypkm2D.js                                                          23.86 kB │ gzip:   8.56 kB
+dist/assets/el-form-item-CKU5hrD2.js                                                   27.20 kB │ gzip:   9.13 kB
+dist/assets/el-select-AdIRFI9d.js                                                      28.77 kB │ gzip:   9.56 kB
+dist/assets/el-text-bKJ44zTh.js                                                        29.02 kB │ gzip:   9.34 kB
+dist/assets/index-DNWwvkXh.js                                                          29.73 kB │ gzip:  10.14 kB
+dist/assets/index-BqcAgXxG.js                                                          39.02 kB │ gzip:  13.58 kB
+dist/assets/index-DBzMhHKH.js                                                          48.20 kB │ gzip:  16.60 kB
+dist/assets/editInfo.vue_vue_type_script_setup_true_lang-H_6kbjjE.js                   50.54 kB │ gzip:  15.71 kB
+dist/assets/index-Bzp1GAS3.js                                                          64.84 kB │ gzip:  21.63 kB
+dist/assets/el-table-column-D4JQW8hi.js                                                75.06 kB │ gzip:  25.00 kB
+dist/assets/index-DlsQkZ6f.js                                                          77.67 kB │ gzip:  31.88 kB
+dist/assets/el-date-picker-panel-D0yDjHjO.js                                           96.34 kB │ gzip:  26.89 kB
+dist/assets/edit-dialog-CDDA4N7j.js                                                   145.93 kB │ gzip:  33.71 kB
+dist/assets/WaterChart.vue_vue_type_script_setup_true_lang-BNXW1hLF.js                348.53 kB │ gzip: 114.37 kB
+dist/assets/xlsx-ByDo_lG2.js                                                          417.70 kB │ gzip: 138.85 kB
+dist/assets/installCanvasRenderer-D-xUkWdX.js                                         764.88 kB │ gzip: 251.02 kB
+dist/assets/index.esm-CYRXWfbl.js                                                     810.82 kB │ gzip: 274.31 kB
+dist/assets/index-Cyw1sGFm.js                                                       1,426.39 kB │ gzip: 463.79 kB
+✓ built in 44.73s

+ 97 - 0
saiadmin-artd/commitlint.config.cjs

@@ -0,0 +1,97 @@
+/**
+ * commitlint 配置文件
+ * 文档
+ * https://commitlint.js.org/#/reference-rules
+ * https://cz-git.qbb.sh/zh/guide/
+ */
+
+module.exports = {
+  // 继承的规则
+  extends: ['@commitlint/config-conventional'],
+  // 自定义规则
+  rules: {
+    // 提交类型枚举,git提交type必须是以下类型
+    'type-enum': [
+      2,
+      'always',
+      [
+        'feat', // 新增功能
+        'fix', // 修复缺陷
+        'docs', // 文档变更
+        'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
+        'refactor', // 代码重构(不包括 bug 修复、功能新增)
+        'perf', // 性能优化
+        'test', // 添加疏漏测试或已有测试改动
+        'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
+        'ci', // 修改 CI 配置、脚本
+        'revert', // 回滚 commit
+        'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
+        'wip' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
+      ]
+    ],
+    'subject-case': [0] // subject大小写不做校验
+  },
+
+  prompt: {
+    messages: {
+      type: '选择你要提交的类型 :',
+      scope: '选择一个提交范围(可选):',
+      customScope: '请输入自定义的提交范围 :',
+      subject: '填写简短精炼的变更描述 :\n',
+      body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
+      breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
+      footerPrefixesSelect: '选择关联issue前缀(可选):',
+      customFooterPrefix: '输入自定义issue前缀 :',
+      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
+      generatingByAI: '正在通过 AI 生成你的提交简短描述...',
+      generatedSelectByAI: '选择一个 AI 生成的简短描述:',
+      confirmCommit: '是否提交或修改commit ?'
+    },
+    // prettier-ignore
+    types: [
+      { value: "feat",     name: "feat:     新增功能" },
+      { value: "fix",      name: "fix:      修复缺陷" },
+      { value: "docs",     name: "docs:     文档变更" },
+      { value: "style",    name: "style:    代码格式(不影响功能,例如空格、分号等格式修正)" },
+      { value: "refactor", name: "refactor: 代码重构(不包括 bug 修复、功能新增)" },
+      { value: "perf",     name: "perf:     性能优化" },
+      { value: "test",     name: "test:     添加疏漏测试或已有测试改动" },
+      { value: "build",    name: "build:    构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)" },
+      { value: "ci",       name: "ci:       修改 CI 配置、脚本" },
+      { value: "revert",   name: "revert:   回滚 commit" },
+      { value: "chore",    name: "chore:    对构建过程或辅助工具和库的更改(不影响源文件、测试用例)" },
+    ],
+    useEmoji: true,
+    emojiAlign: 'center',
+    useAI: false,
+    aiNumber: 1,
+    themeColorCode: '',
+    scopes: [],
+    allowCustomScopes: true,
+    allowEmptyScopes: true,
+    customScopesAlign: 'bottom',
+    customScopesAlias: 'custom',
+    emptyScopesAlias: 'empty',
+    upperCaseSubject: false,
+    markBreakingChangeMode: false,
+    allowBreakingChanges: ['feat', 'fix'],
+    breaklineNumber: 100,
+    breaklineChar: '|',
+    skipQuestions: ['breaking', 'footerPrefix', 'footer'], // 跳过的步骤
+    issuePrefixes: [{ value: 'closed', name: 'closed:   ISSUES has been processed' }],
+    customIssuePrefixAlign: 'top',
+    emptyIssuePrefixAlias: 'skip',
+    customIssuePrefixAlias: 'custom',
+    allowCustomIssuePrefix: true,
+    allowEmptyIssuePrefix: true,
+    confirmColorize: true,
+    maxHeaderLength: Infinity,
+    maxSubjectLength: Infinity,
+    minSubjectLength: 0,
+    scopeOverrides: undefined,
+    defaultBody: '',
+    defaultIssues: '',
+    defaultScope: '',
+    defaultSubject: ''
+  }
+}

BIN
saiadmin-artd/content-build.log


+ 83 - 0
saiadmin-artd/eslint.config.mjs

@@ -0,0 +1,83 @@
+// 从 URL 和路径模块中导入必要的功能
+import fs from 'fs'
+import path, { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+// 从 ESLint 插件中导入推荐配置
+import pluginJs from '@eslint/js'
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
+import pluginVue from 'eslint-plugin-vue'
+import globals from 'globals'
+import tseslint from 'typescript-eslint'
+
+// 使用 import.meta.url 获取当前模块的路径
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+
+// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象
+const autoImportConfig = JSON.parse(
+  fs.readFileSync(path.resolve(__dirname, '.auto-import.json'), 'utf-8')
+)
+
+export default [
+  // 指定文件匹配规则
+  {
+    files: ['**/*.{js,mjs,cjs,ts,vue}']
+  },
+  // 指定全局变量和环境
+  {
+    languageOptions: {
+      globals: {
+        ...globals.browser,
+        ...globals.node
+      }
+    }
+  },
+  // 扩展配置
+  pluginJs.configs.recommended,
+  ...tseslint.configs.recommended,
+  ...pluginVue.configs['flat/essential'],
+  // 自定义规则
+  {
+    // 针对所有 JavaScript、TypeScript 和 Vue 文件应用以下配置
+    files: ['**/*.{js,mjs,cjs,ts,vue}'],
+
+    languageOptions: {
+      globals: {
+        // 合并从 autoImportConfig 中读取的全局变量配置
+        ...autoImportConfig.globals,
+        // TypeScript 全局命名空间
+        Api: 'readonly'
+      }
+    },
+    rules: {
+      quotes: ['error', 'single'], // 使用单引号
+      semi: ['error', 'never'], // 语句末尾不加分号
+      'no-var': 'error', // 要求使用 let 或 const 而不是 var
+      '@typescript-eslint/no-explicit-any': 'off', // 禁用 any 检查
+      'vue/multi-word-component-names': 'off', // 禁用对 Vue 组件名称的多词要求检查
+      'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
+      'no-unexpected-multiline': 'error' // 禁止空余的多行
+    }
+  },
+  // vue 规则
+  {
+    files: ['**/*.vue'],
+    languageOptions: {
+      parserOptions: { parser: tseslint.parser }
+    }
+  },
+  // 忽略文件
+  {
+    ignores: [
+      'node_modules',
+      'dist',
+      'public',
+      '.vscode/**',
+      'src/assets/**',
+      'src/utils/console.ts'
+    ]
+  },
+  // prettier 配置
+  eslintPluginPrettierRecommended
+]

BIN
saiadmin-artd/finance-build.log


+ 47 - 0
saiadmin-artd/index.html

@@ -0,0 +1,47 @@
+<!doctype html>
+<html>
+  <head>
+    <title>SaiAdmin</title>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta
+      name="description"
+      content="SaiAdmin - A modern admin dashboard system built with Vue 3, Element Plus, and Webman."
+    />
+    <link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
+
+    <style>
+      /* 防止页面刷新时白屏的初始样式 */
+      html {
+        background-color: #fafbfc;
+      }
+
+      html.dark {
+        background-color: #070707;
+      }
+    </style>
+
+    <script>
+      // 初始化 html class 主题属性
+      ;(function () {
+        try {
+          if (typeof Storage === 'undefined' || !window.localStorage) {
+            return
+          }
+
+          const themeType = localStorage.getItem('sys-theme')
+          if (themeType === 'dark') {
+            document.documentElement.classList.add('dark')
+          }
+        } catch (e) {
+          console.warn('Failed to apply initial theme:', e)
+        }
+      })()
+    </script>
+  </head>
+
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 552 - 0
saiadmin-artd/menu-build.log

@@ -0,0 +1,552 @@
+
+> art-design-pro@0.0.0 build
+> vue-tsc --noEmit && vite build
+
+🚀 API_URL = http://127.0.0.1:8787
+🚀 VERSION = 3.0.2
+vite v7.1.7 building for production...
+transforming...
+✓ 3335 modules transformed.
+rendering chunks...
+[plugin vite:reporter] 
+(!) D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue is dynamically imported by D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/router/core/ComponentLoader.ts, D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue?vue&type=script&setup=true&lang.ts but also statically imported by D:/phpstudy_pro/WWW/saiadmin6.x/saiadmin-artd/src/views/system/menu/index.vue?vue&type=script&setup=true&lang.ts, dynamic import will not move module into another chunk.
+
+computing gzip size...
+dist/index.html                                                                         1.34 kB │ gzip:   0.74 kB
+dist/assets/favicon-D4tL44Sk.ico                                                        3.55 kB
+dist/assets/login_icon-C4TVlUS8.svg                                                     4.23 kB │ gzip:   1.48 kB
+dist/assets/403-BdWuHcJA.svg                                                            4.61 kB │ gzip:   1.55 kB
+dist/assets/yd-BrGqJ6Cs.png                                                             4.63 kB
+dist/assets/sd-C0PQtrty.png                                                             4.75 kB
+dist/assets/500-DANzffU6.svg                                                            4.93 kB │ gzip:   1.76 kB
+dist/assets/404-BzxNMzaO.svg                                                            5.12 kB │ gzip:   1.94 kB
+dist/assets/draw1-Ce1WF34i.png                                                         11.32 kB
+dist/assets/logo-5EOo6F45.png                                                          18.79 kB
+dist/assets/bg_light-WsVMpqKz.webp                                                     67.25 kB
+dist/assets/bg_dark-BoZNsetB.webp                                                      70.59 kB
+dist/assets/user-bg-E5ODUodR.jpg                                                       88.58 kB
+dist/assets/el-form-item-tn0RQdqM.css                                                   0.00 kB │ gzip:   0.02 kB
+dist/assets/index-BGJDsef7.css                                                          0.04 kB │ gzip:   0.06 kB
+dist/assets/el-checkbox-group-D_6SYB2i.css                                              0.05 kB │ gzip:   0.07 kB
+dist/assets/FeaturePlaceholder-Ds3Gn_8D.css                                             0.05 kB │ gzip:   0.07 kB
+dist/assets/index-BqEhlTYo.css                                                          0.06 kB │ gzip:   0.08 kB
+dist/assets/index-DbqREa7s.css                                                          0.07 kB │ gzip:   0.09 kB
+dist/assets/DispatchForm-D7VxPRne.css                                                   0.07 kB │ gzip:   0.09 kB
+dist/assets/index-BbywE3f6.css                                                          0.09 kB │ gzip:   0.10 kB
+dist/assets/index-rl4V_FNU.css                                                          0.09 kB │ gzip:   0.10 kB
+dist/assets/DeviceBindingDialog-DwcxV8mx.css                                            0.09 kB │ gzip:   0.11 kB
+dist/assets/index-DpUm9fYV.css                                                          0.11 kB │ gzip:   0.09 kB
+dist/assets/index-BVDfV0xn.css                                                          0.11 kB │ gzip:   0.09 kB
+dist/assets/el-space-3oFudasq.css                                                       0.16 kB │ gzip:   0.12 kB
+dist/assets/el-overlay-Db7iXMEX.css                                                     0.16 kB │ gzip:   0.14 kB
+dist/assets/new-user-CrtQcHjJ.css                                                       0.17 kB │ gzip:   0.14 kB
+dist/assets/WaterLedgerDrawer-7ZPVAbw8.css                                              0.17 kB │ gzip:   0.14 kB
+dist/assets/WaterRegionField-CvMzoDDG.css                                               0.18 kB │ gzip:   0.15 kB
+dist/assets/index-bO8ksejU.css                                                          0.19 kB │ gzip:   0.12 kB
+dist/assets/AssignmentRecordsDialog-DWZuwG1L.css                                        0.19 kB │ gzip:   0.14 kB
+dist/assets/EnterpriseEditor-C4oAWjYP.css                                               0.19 kB │ gzip:   0.14 kB
+dist/assets/index-BwGJNQbf.css                                                          0.22 kB │ gzip:   0.14 kB
+dist/assets/WaterLocationPicker-CszocEzb.css                                            0.23 kB │ gzip:   0.17 kB
+dist/assets/device-binding-dialog-Ds5kxLpc.css                                          0.24 kB │ gzip:   0.18 kB
+dist/assets/index-DamTWgjx.css                                                          0.25 kB │ gzip:   0.16 kB
+dist/assets/WaterLocationField-DUiNRTRC.css                                             0.25 kB │ gzip:   0.18 kB
+dist/assets/AgentEditor-B_l1wBnz.css                                                    0.29 kB │ gzip:   0.20 kB
+dist/assets/RouteOptimization-Cw7oqFEl.css                                              0.30 kB │ gzip:   0.18 kB
+dist/assets/index-0nXq2V_I.css                                                          0.32 kB │ gzip:   0.19 kB
+dist/assets/terminal-CdVYeqnz.css                                                       0.34 kB │ gzip:   0.21 kB
+dist/assets/index-N-ubbNWb.css                                                          0.35 kB │ gzip:   0.20 kB
+dist/assets/index-CkrWr1R3.css                                                          0.35 kB │ gzip:   0.21 kB
+dist/assets/AgentDetail-9HZMX5ja.css                                                    0.42 kB │ gzip:   0.22 kB
+dist/assets/EnterpriseDetail-CmFAfq9z.css                                               0.43 kB │ gzip:   0.22 kB
+dist/assets/withdrawal-update-dialog-Bl21aWO0.css                                       0.44 kB │ gzip:   0.21 kB
+dist/assets/business-time-dialog-C2PgOXRw.css                                           0.47 kB │ gzip:   0.24 kB
+dist/assets/index-CGPX_m9B.css                                                          0.49 kB │ gzip:   0.29 kB
+dist/assets/el-row-C6BJsxyy.css                                                         0.49 kB │ gzip:   0.20 kB
+dist/assets/list-page-RHUR0sFn.css                                                      0.49 kB │ gzip:   0.26 kB
+dist/assets/list-page-DHsWYbEt.css                                                      0.51 kB │ gzip:   0.28 kB
+dist/assets/DeviceAssignmentDialog-BnOrWHVP.css                                         0.55 kB │ gzip:   0.27 kB
+dist/assets/el-image-CtVnJP8l.css                                                       0.56 kB │ gzip:   0.29 kB
+dist/assets/el-tree-select-Csemd6fU.css                                                 0.64 kB │ gzip:   0.27 kB
+dist/assets/el-divider-BUtF_RGI.css                                                     0.71 kB │ gzip:   0.31 kB
+dist/assets/index-P0GJfxvp.css                                                          0.72 kB │ gzip:   0.33 kB
+dist/assets/index-DFm-5RmY.css                                                          0.81 kB │ gzip:   0.37 kB
+dist/assets/index-BNmfVIYZ.css                                                          0.82 kB │ gzip:   0.42 kB
+dist/assets/index-BYbfSlpm.css                                                          0.85 kB │ gzip:   0.34 kB
+dist/assets/el-card-fwQOLwdi.css                                                        0.86 kB │ gzip:   0.29 kB
+dist/assets/index-B-xVoxk3.css                                                          0.87 kB │ gzip:   0.33 kB
+dist/assets/el-avatar-BmRr_O8d.css                                                      0.88 kB │ gzip:   0.34 kB
+dist/assets/withdraw-settings-BrXchJ9M.css                                              0.88 kB │ gzip:   0.38 kB
+dist/assets/WaterMetricCards-B1cgX8Z2.css                                               0.96 kB │ gzip:   0.44 kB
+dist/assets/index-Bl6Al3OB.css                                                          0.97 kB │ gzip:   0.37 kB
+dist/assets/index-DM0lwQug.css                                                          1.03 kB │ gzip:   0.41 kB
+dist/assets/index-Cttr28uV.css                                                          1.03 kB │ gzip:   0.45 kB
+dist/assets/edit-page-D3LQA3MF.css                                                      1.04 kB │ gzip:   0.46 kB
+dist/assets/DeviceAnalytics-s5xg7Mbo.css                                                1.04 kB │ gzip:   0.43 kB
+dist/assets/index-B5OaoFxa.css                                                          1.06 kB │ gzip:   0.44 kB
+dist/assets/index-FRyKr3zj.css                                                          1.12 kB │ gzip:   0.43 kB
+dist/assets/DispatchMap-BNjK_hgc.css                                                    1.15 kB │ gzip:   0.49 kB
+dist/assets/index-Bh17lM2j.css                                                          1.16 kB │ gzip:   0.46 kB
+dist/assets/edit-page-CfgtyH7r.css                                                      1.18 kB │ gzip:   0.51 kB
+dist/assets/index-Cq75mf1I.css                                                          1.19 kB │ gzip:   0.43 kB
+dist/assets/el-empty-D4ZqTl4F.css                                                       1.20 kB │ gzip:   0.47 kB
+dist/assets/edit-dialog-BnkFiCgh.css                                                    1.25 kB │ gzip:   0.41 kB
+dist/assets/el-scrollbar-BWxh-h6K.css                                                   1.33 kB │ gzip:   0.44 kB
+dist/assets/el-popover-Cktl5fHm.css                                                     1.35 kB │ gzip:   0.42 kB
+dist/assets/index-CRr3K0tv.css                                                          1.37 kB │ gzip:   0.43 kB
+dist/assets/AgentList-BHrX5aMK.css                                                      1.46 kB │ gzip:   0.49 kB
+dist/assets/preview--ha0OGK5.css                                                        1.49 kB │ gzip:   0.72 kB
+dist/assets/el-skeleton-item-BLY1jEuR.css                                               1.58 kB │ gzip:   0.54 kB
+dist/assets/index-D1DKg2E6.css                                                          1.61 kB │ gzip:   0.61 kB
+dist/assets/index-D74n0A8b.css                                                          1.69 kB │ gzip:   0.60 kB
+dist/assets/index-D2i4cPFx.css                                                          1.70 kB │ gzip:   0.56 kB
+dist/assets/index-C2nd4I3k.css                                                          1.70 kB │ gzip:   0.56 kB
+dist/assets/index-BfYUDy4T.css                                                          1.74 kB │ gzip:   0.56 kB
+dist/assets/index-TNn7A1lq.css                                                          1.75 kB │ gzip:   0.56 kB
+dist/assets/index-DlPiOlT_.css                                                          1.77 kB │ gzip:   0.57 kB
+dist/assets/index-CtE9tzvE.css                                                          1.79 kB │ gzip:   0.57 kB
+dist/assets/index-Bs3ZykK5.css                                                          1.81 kB │ gzip:   0.63 kB
+dist/assets/index-RgLuEWmr.css                                                          1.81 kB │ gzip:   0.55 kB
+dist/assets/index-DZ2luEux.css                                                          1.86 kB │ gzip:   0.63 kB
+dist/assets/index-DHLEmKR9.css                                                          1.89 kB │ gzip:   0.63 kB
+dist/assets/index-D9W-twQd.css                                                          1.89 kB │ gzip:   0.72 kB
+dist/assets/index-bMPyyuLZ.css                                                          1.89 kB │ gzip:   0.72 kB
+dist/assets/index-5Rlezp3_.css                                                          1.89 kB │ gzip:   0.67 kB
+dist/assets/index-BLKB_GTv.css                                                          1.90 kB │ gzip:   0.58 kB
+dist/assets/index-OEuMJp6Y.css                                                          1.95 kB │ gzip:   0.60 kB
+dist/assets/index-C1B1bfOu.css                                                          1.95 kB │ gzip:   0.74 kB
+dist/assets/index-DHSHLZkD.css                                                          2.00 kB │ gzip:   0.66 kB
+dist/assets/index-BShYJ36M.css                                                          2.01 kB │ gzip:   0.55 kB
+dist/assets/el-image-viewer-Ba-UrN8P.css                                                2.07 kB │ gzip:   0.65 kB
+dist/assets/index-p5mPO7rG.css                                                          2.07 kB │ gzip:   0.63 kB
+dist/assets/index-BgdAqc-4.css                                                          2.18 kB │ gzip:   0.70 kB
+dist/assets/DeviceCard-C9e8X45O.css                                                     2.19 kB │ gzip:   0.79 kB
+dist/assets/el-popper-D1i0e6ba.css                                                      2.21 kB │ gzip:   0.52 kB
+dist/assets/index-CYp7puhs.css                                                          2.26 kB │ gzip:   0.64 kB
+dist/assets/el-link-B58a4a3I.css                                                        2.38 kB │ gzip:   0.54 kB
+dist/assets/index-DYMtodT8.css                                                          2.48 kB │ gzip:   0.76 kB
+dist/assets/index-D40mQk8Y.css                                                          2.51 kB │ gzip:   0.63 kB
+dist/assets/OrderList-D6z6HKmL.css                                                      2.58 kB │ gzip:   0.70 kB
+dist/assets/el-progress-Dw9yTa91.css                                                    2.59 kB │ gzip:   0.76 kB
+dist/assets/el-timeline-item-BvbJTz1y.css                                               2.60 kB │ gzip:   0.64 kB
+dist/assets/index-3sJod6Us.css                                                          2.77 kB │ gzip:   0.67 kB
+dist/assets/index-DXk_0VAd.css                                                          2.77 kB │ gzip:   0.76 kB
+dist/assets/index-Be9jjVp7.css                                                          2.82 kB │ gzip:   0.78 kB
+dist/assets/index-J57XdXk3.css                                                          3.17 kB │ gzip:   0.87 kB
+dist/assets/el-radio-button-CSkroacn.css                                                3.25 kB │ gzip:   0.77 kB
+dist/assets/index-Cx18UXIl.css                                                          3.28 kB │ gzip:   0.88 kB
+dist/assets/edit-dialog-Cz4SMjkt.css                                                    3.29 kB │ gzip:   0.84 kB
+dist/assets/el-descriptions-item-o9ObloqJ.css                                           3.42 kB │ gzip:   0.60 kB
+dist/assets/el-alert-G57rL0jl.css                                                       3.46 kB │ gzip:   0.75 kB
+dist/assets/el-drawer-BhCnIJJ3.css                                                      3.51 kB │ gzip:   0.88 kB
+dist/assets/el-dialog-DyK7vRzj.css                                                      3.53 kB │ gzip:   0.98 kB
+dist/assets/index-BeNm5kmV.css                                                          3.82 kB │ gzip:   1.15 kB
+dist/assets/index-E8JFeY_4.css                                                          3.89 kB │ gzip:   0.93 kB
+dist/assets/el-switch-B5lTGWdM.css                                                      3.94 kB │ gzip:   0.96 kB
+dist/assets/index-y_y2pIUi.css                                                          3.95 kB │ gzip:   1.19 kB
+dist/assets/index-CtbAGBMy.css                                                          4.21 kB │ gzip:   0.90 kB
+dist/assets/el-input-number-D6iOyBgb.css                                                4.34 kB │ gzip:   0.91 kB
+dist/assets/el-radio-group-LltH1qQS.css                                                 4.39 kB │ gzip:   0.94 kB
+dist/assets/index-kDym549C.css                                                          4.57 kB │ gzip:   1.28 kB
+dist/assets/el-collapse-item-D4LG0FJ0.css                                               4.64 kB │ gzip:   0.98 kB
+dist/assets/message-CIxGxpAv.css                                                        4.69 kB │ gzip:   1.06 kB
+dist/assets/el-form-BWkJzdQ_.css                                                        4.75 kB │ gzip:   0.89 kB
+dist/assets/index-CDqzzWQr.css                                                          4.78 kB │ gzip:   1.18 kB
+dist/assets/index-ChIdo08L.css                                                          4.80 kB │ gzip:   1.16 kB
+dist/assets/index-W5cVOjuB.css                                                          5.04 kB │ gzip:   1.36 kB
+dist/assets/el-tag-DljBBxJR.css                                                         5.52 kB │ gzip:   0.87 kB
+dist/assets/water-B1ijNFo5.css                                                          5.91 kB │ gzip:   1.51 kB
+dist/assets/el-text-BYEPGzFo.css                                                        5.96 kB │ gzip:   1.34 kB
+dist/assets/index-DY1CsFsK.css                                                          5.96 kB │ gzip:   1.48 kB
+dist/assets/index-th0PFgZG.css                                                          6.05 kB │ gzip:   1.68 kB
+dist/assets/el-checkbox-DIj50LEB.css                                                    6.53 kB │ gzip:   1.21 kB
+dist/assets/el-pagination-BNQcHhjS.css                                                  6.58 kB │ gzip:   1.17 kB
+dist/assets/dict-edit-dialog-zG3lrnKK.css                                               7.05 kB │ gzip:   1.54 kB
+dist/assets/el-dropdown-item-11ZCvSOX.css                                               7.19 kB │ gzip:   1.24 kB
+dist/assets/index-BCwvCwf0.css                                                          7.62 kB │ gzip:   1.70 kB
+dist/assets/el-select-DC6_bRTH.css                                                      9.25 kB │ gzip:   1.94 kB
+dist/assets/editInfo-BmnVI_jW.css                                                       9.75 kB │ gzip:   1.77 kB
+dist/assets/el-input-tPmZxDKr.css                                                      10.83 kB │ gzip:   1.74 kB
+dist/assets/el-upload-q8uObtwj.css                                                     11.46 kB │ gzip:   2.07 kB
+dist/assets/LoginLeftView-C-JORtQp.css                                                 11.61 kB │ gzip:   2.02 kB
+dist/assets/index-BHtaHxM4.css                                                         14.76 kB │ gzip:   2.83 kB
+dist/assets/el-tab-pane-BLUvaGkK.css                                                   14.83 kB │ gzip:   2.25 kB
+dist/assets/el-button-CDqfIFiK.css                                                     15.58 kB │ gzip:   1.93 kB
+dist/assets/el-table-column-CKoPG0Y8.css                                               18.06 kB │ gzip:   2.87 kB
+dist/assets/el-date-picker-panel-BhfPqR_w.css                                          28.61 kB │ gzip:   4.15 kB
+dist/assets/overlay-C8XuAnme.css                                                       31.12 kB │ gzip:   4.31 kB
+dist/assets/index-B94BL0qh.css                                                         36.60 kB │ gzip:   6.09 kB
+dist/assets/el-col-DD1Vn-Yu.css                                                        38.68 kB │ gzip:   3.75 kB
+dist/assets/index-CXqLAW6P.css                                                        105.08 kB │ gzip:  19.39 kB
+dist/assets/refs-Cw5r5QN8.js                                                            0.06 kB │ gzip:   0.08 kB
+dist/assets/token-DWNpOE8r.js                                                           0.08 kB │ gzip:   0.08 kB
+dist/assets/event-BwRzfsZt.js                                                           0.08 kB │ gzip:   0.09 kB
+dist/assets/validator-DDvlS4RE.js                                                       0.09 kB │ gzip:   0.10 kB
+dist/assets/cloneDeep-iGUezF1a.js                                                       0.09 kB │ gzip:   0.10 kB
+dist/assets/_plugin-vue_export-helper-BCo6x5W8.js                                       0.09 kB │ gzip:   0.10 kB
+dist/assets/castArray-C3dsxDoX.js                                                       0.14 kB │ gzip:   0.14 kB
+dist/assets/strings-qYjWOt6M.js                                                         0.15 kB │ gzip:   0.15 kB
+dist/assets/error-D_Dr4eZ1.js                                                           0.15 kB │ gzip:   0.14 kB
+dist/assets/ContentPhone-DQRhrtc6.js                                                    0.16 kB │ gzip:   0.15 kB
+dist/assets/index-BOf8taOp.js                                                           0.17 kB │ gzip:   0.17 kB
+dist/assets/WaterChart-DiIyuxp-.js                                                      0.18 kB │ gzip:   0.16 kB
+dist/assets/about-project-D5Mt4uO-.js                                                   0.23 kB │ gzip:   0.17 kB
+dist/assets/server-o8DvhhqK.js                                                          0.23 kB │ gzip:   0.15 kB
+dist/assets/active-user-B_w0MJQE.js                                                     0.24 kB │ gzip:   0.19 kB
+dist/assets/dashboard-DlOJJnOo.js                                                       0.25 kB │ gzip:   0.16 kB
+dist/assets/EnterpriseAnalytics-DngCx-CI.js                                             0.25 kB │ gzip:   0.19 kB
+dist/assets/dynamic-stats-BFvNF0Oe.js                                                   0.26 kB │ gzip:   0.20 kB
+dist/assets/attachment-BIGzO27J.js                                                      0.28 kB │ gzip:   0.17 kB
+dist/assets/index-Dr413WVR.js                                                           0.29 kB │ gzip:   0.24 kB
+dist/assets/sales-overview-Dlb9zECx.js                                                  0.31 kB │ gzip:   0.21 kB
+dist/assets/card-list-1P8X3mr0.js                                                       0.31 kB │ gzip:   0.21 kB
+dist/assets/category-C0cbRRb1.js                                                        0.33 kB │ gzip:   0.19 kB
+dist/assets/dept-B9pSrdxc.js                                                            0.36 kB │ gzip:   0.20 kB
+dist/assets/post-DCLGkyl5.js                                                            0.36 kB │ gzip:   0.20 kB
+dist/assets/menu-DCiWDi31.js                                                            0.37 kB │ gzip:   0.20 kB
+dist/assets/index-DSKl7imw.js                                                           0.38 kB │ gzip:   0.28 kB
+dist/assets/alipay-C73oUWgb.js                                                          0.39 kB │ gzip:   0.19 kB
+dist/assets/wechat-CeDPDAiM.js                                                          0.39 kB │ gzip:   0.20 kB
+dist/assets/index-9kDaRBnj.js                                                           0.40 kB │ gzip:   0.33 kB
+dist/assets/DispatchLocationPicker.vue_vue_type_script_setup_true_lang-BPUErVkF.js      0.40 kB │ gzip:   0.29 kB
+dist/assets/index-CKTGqcu6.js                                                           0.42 kB │ gzip:   0.30 kB
+dist/assets/todo-list-Bqz-IGgY.js                                                       0.45 kB │ gzip:   0.28 kB
+dist/assets/DeliveryHistory-BE82fo9n.js                                                 0.46 kB │ gzip:   0.27 kB
+dist/assets/user-CIh8G139.js                                                            0.50 kB │ gzip:   0.23 kB
+dist/assets/crontab-D8nnPwYK.js                                                         0.51 kB │ gzip:   0.22 kB
+dist/assets/dict-B82VB-Y4.js                                                            0.51 kB │ gzip:   0.21 kB
+dist/assets/toNumber-Dum0pWK1.js                                                        0.54 kB │ gzip:   0.36 kB
+dist/assets/raf-hEtcbFU-.js                                                             0.54 kB │ gzip:   0.35 kB
+dist/assets/agentPresentation-BZPs3RG4.js                                               0.54 kB │ gzip:   0.41 kB
+dist/assets/index-B84wasJh.js                                                           0.54 kB │ gzip:   0.29 kB
+dist/assets/database-B5KBVUXU.js                                                        0.56 kB │ gzip:   0.23 kB
+dist/assets/WaterMetricCards-BuSmDvgP.js                                                0.59 kB │ gzip:   0.39 kB
+dist/assets/index-CMRlobix.js                                                           0.60 kB │ gzip:   0.34 kB
+dist/assets/role-_iPgf6Ev.js                                                            0.62 kB │ gzip:   0.25 kB
+dist/assets/waterDownload-Cu-N2cSH.js                                                   0.64 kB │ gzip:   0.46 kB
+dist/assets/use-form-common-props-BKYurvLx.js                                           0.64 kB │ gzip:   0.35 kB
+dist/assets/index-BfkANJQ8.js                                                           0.65 kB │ gzip:   0.46 kB
+dist/assets/config-CGG4AlMv.js                                                          0.65 kB │ gzip:   0.24 kB
+dist/assets/generate-DCxml7kE.js                                                        0.66 kB │ gzip:   0.27 kB
+dist/assets/FeaturePlaceholder-DBu7mzhv.js                                              0.68 kB │ gzip:   0.45 kB
+dist/assets/agent-CWnCAtzC.js                                                           0.79 kB │ gzip:   0.29 kB
+dist/assets/index-CYD8IuVS.js                                                           0.80 kB │ gzip:   0.50 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-txcHMNNh.js                       0.83 kB │ gzip:   0.50 kB
+dist/assets/install-box-BB-i3Cs5.js                                                     0.92 kB │ gzip:   0.46 kB
+dist/assets/table-search-m9mURzAT.js                                                    0.93 kB │ gzip:   0.45 kB
+dist/assets/table-search-CjAmZe9x.js                                                    0.93 kB │ gzip:   0.45 kB
+dist/assets/sales-overview.vue_vue_type_script_setup_true_lang-CeFCmzic.js              0.93 kB │ gzip:   0.59 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-KbBcK65m.js                       0.93 kB │ gzip:   0.57 kB
+dist/assets/index-CrKHXTa_.js                                                           0.94 kB │ gzip:   0.52 kB
+dist/assets/useSaiAdmin-DYcQAUw9.js                                                     0.96 kB │ gzip:   0.56 kB
+dist/assets/DispatchLocationPicker-CZyAK62t.js                                          0.99 kB │ gzip:   0.49 kB
+dist/assets/edit-dialog-lkKe6b1w.js                                                     1.00 kB │ gzip:   0.49 kB
+dist/assets/use-form-item-Bxf0Z8nC.js                                                   1.02 kB │ gzip:   0.56 kB
+dist/assets/group-edit-dialog-OFmZmNLM.js                                               1.03 kB │ gzip:   0.49 kB
+dist/assets/business-time-dialog-BS-qLI4x.js                                            1.03 kB │ gzip:   0.49 kB
+dist/assets/Iframe-CTTxu95J.js                                                          1.03 kB │ gzip:   0.61 kB
+dist/assets/debounce-BAWNjOkb.js                                                        1.04 kB │ gzip:   0.55 kB
+dist/assets/permission-dialog-DTQwfAru.js                                               1.08 kB │ gzip:   0.51 kB
+dist/assets/_baseIteratee-ChOFfiJS.js                                                   1.13 kB │ gzip:   0.60 kB
+dist/assets/vnode-CdyD2JLF.js                                                           1.14 kB │ gzip:   0.61 kB
+dist/assets/index-u8pvGGAD.js                                                           1.15 kB │ gzip:   0.63 kB
+dist/assets/index-Djn1Qr9t.js                                                           1.15 kB │ gzip:   0.63 kB
+dist/assets/AlertPolicyDialog-DC1Gsg51.js                                               1.15 kB │ gzip:   0.53 kB
+dist/assets/index-Dsju5mFB.js                                                           1.16 kB │ gzip:   0.63 kB
+dist/assets/_flatRest-BLj6NPB_.js                                                       1.16 kB │ gzip:   0.63 kB
+dist/assets/merchant-CZxQxaxy.js                                                        1.16 kB │ gzip:   0.40 kB
+dist/assets/type-edit-dialog-37G--RQo.js                                                1.17 kB │ gzip:   0.52 kB
+dist/assets/ArtException.vue_vue_type_script_setup_true_lang-Bjwh1ufS.js                1.19 kB │ gzip:   0.70 kB
+dist/assets/table-dialog-DMAKclY7.js                                                    1.19 kB │ gzip:   0.56 kB
+dist/assets/scroll-0YXLJbIl.js                                                          1.20 kB │ gzip:   0.62 kB
+dist/assets/el-divider-BKVKLXJr.js                                                      1.21 kB │ gzip:   0.67 kB
+dist/assets/index-Ceccgqow.js                                                           1.21 kB │ gzip:   0.82 kB
+dist/assets/FirmwareDialog-CGcAjT_d.js                                                  1.22 kB │ gzip:   0.57 kB
+dist/assets/edit-dialog-DvU-EFo9.js                                                     1.23 kB │ gzip:   0.54 kB
+dist/assets/edit-dialog-B3jGpLd0.js                                                     1.23 kB │ gzip:   0.54 kB
+dist/assets/table-search-B6VASAXZ.js                                                    1.27 kB │ gzip:   0.57 kB
+dist/assets/index-B8YuBxoC.js                                                           1.29 kB │ gzip:   0.60 kB
+dist/assets/el-row-oRH_KoiN.js                                                          1.31 kB │ gzip:   0.73 kB
+dist/assets/aria-Dle50h-X.js                                                            1.37 kB │ gzip:   0.73 kB
+dist/assets/index-CPbZAme_.js                                                           1.38 kB │ gzip:   0.71 kB
+dist/assets/dict-edit-dialog-CPx0wGae.js                                                1.38 kB │ gzip:   0.60 kB
+dist/assets/todo-list.vue_vue_type_script_setup_true_lang-TjvwKPUn.js                   1.38 kB │ gzip:   0.78 kB
+dist/assets/ContentPreviewDialog-doiT3TlJ.js                                            1.39 kB │ gzip:   0.61 kB
+dist/assets/_initCloneObject-Cs5gNg43.js                                                1.40 kB │ gzip:   0.71 kB
+dist/assets/withdrawal-update-dialog-CzVarW82.js                                        1.41 kB │ gzip:   0.63 kB
+dist/assets/work-dialog-Drb5TFWT.js                                                     1.44 kB │ gzip:   0.63 kB
+dist/assets/BatchDeliveryDialog-BpkCVIHc.js                                             1.44 kB │ gzip:   0.63 kB
+dist/assets/table-search-CEVzEpIW.js                                                    1.44 kB │ gzip:   0.63 kB
+dist/assets/DeviceEditor-B6bEpN-_.js                                                    1.45 kB │ gzip:   0.63 kB
+dist/assets/dict-search-BcB7LAuY.js                                                     1.47 kB │ gzip:   0.64 kB
+dist/assets/settingComponent-B2bDe4Gq.js                                                1.47 kB │ gzip:   0.65 kB
+dist/assets/about-project.vue_vue_type_script_setup_true_lang-DRBbvFBx.js               1.47 kB │ gzip:   0.96 kB
+dist/assets/table-search-Dd3GbG5k.js                                                    1.47 kB │ gzip:   0.64 kB
+dist/assets/table-search-C6qwW9gj.js                                                    1.47 kB │ gzip:   0.64 kB
+dist/assets/table-search-CT6tX4ZY.js                                                    1.47 kB │ gzip:   0.64 kB
+dist/assets/table-search-D3g4toTI.js                                                    1.47 kB │ gzip:   0.64 kB
+dist/assets/table-search-Bapx7pqM.js                                                    1.47 kB │ gzip:   0.64 kB
+dist/assets/table-search-Cuvfb9nV.js                                                    1.47 kB │ gzip:   0.64 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BFWABRet.js                1.48 kB │ gzip:   0.84 kB
+dist/assets/ReceiptDialog-jJ0Bo7ra.js                                                   1.49 kB │ gzip:   0.64 kB
+dist/assets/index-DZe25p66.js                                                           1.49 kB │ gzip:   0.81 kB
+dist/assets/el-card-COIynvu0.js                                                         1.51 kB │ gzip:   0.74 kB
+dist/assets/table-search-gFM4WAP0.js                                                    1.51 kB │ gzip:   0.66 kB
+dist/assets/table-search-CNpt__XS.js                                                    1.51 kB │ gzip:   0.66 kB
+dist/assets/dynamic-stats.vue_vue_type_script_setup_true_lang-D_uYAP3l.js               1.52 kB │ gzip:   0.85 kB
+dist/assets/table-edit-DD8-aoLX.js                                                      1.52 kB │ gzip:   0.66 kB
+dist/assets/move-dialog-BMTxeU3d.js                                                     1.53 kB │ gzip:   0.67 kB
+dist/assets/ArtResultPage.vue_vue_type_script_setup_true_lang-YDyPtgnm.js               1.53 kB │ gzip:   0.85 kB
+dist/assets/InventoryForm-DK5vZzN8.js                                                   1.56 kB │ gzip:   0.67 kB
+dist/assets/FulfillmentDialog-DF3-D5TO.js                                               1.58 kB │ gzip:   0.67 kB
+dist/assets/category-dialog-CEDQvaQn.js                                                 1.62 kB │ gzip:   0.69 kB
+dist/assets/index-DEudtceW.js                                                           1.63 kB │ gzip:   0.97 kB
+dist/assets/config-edit-dialog-Cfitfrsp.js                                              1.65 kB │ gzip:   0.69 kB
+dist/assets/el-avatar-BtczA8wP.js                                                       1.66 kB │ gzip:   0.91 kB
+dist/assets/ContentEditor-D__WQ35H.js                                                   1.67 kB │ gzip:   0.71 kB
+dist/assets/index-BKjgdxZ5.js                                                           1.71 kB │ gzip:   0.97 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-Dl8guTgO.js                1.72 kB │ gzip:   0.86 kB
+dist/assets/index-BL_SMsoa.js                                                           1.72 kB │ gzip:   0.90 kB
+dist/assets/edit-dialog-BLQZEALO.js                                                     1.73 kB │ gzip:   0.71 kB
+dist/assets/table-dialog.vue_vue_type_script_setup_true_lang-DYPnqO8G.js                1.76 kB │ gzip:   0.96 kB
+dist/assets/el-col-iWrVk-H9.js                                                          1.79 kB │ gzip:   0.85 kB
+dist/assets/loadTable-BluzV6x5.js                                                       1.86 kB │ gzip:   0.79 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-z5r28NjP.js                       1.86 kB │ gzip:   0.94 kB
+dist/assets/index-DH9qjwQE.js                                                           1.87 kB │ gzip:   0.96 kB
+dist/assets/DeviceCard-C2Eu55kA.js                                                      1.87 kB │ gzip:   1.05 kB
+dist/assets/recycle-list-sXMDPvUK.js                                                    1.89 kB │ gzip:   0.81 kB
+dist/assets/log-list-BLqDdgAj.js                                                        1.96 kB │ gzip:   0.83 kB
+dist/assets/edit-dialog-C25IrKlF.js                                                     2.00 kB │ gzip:   0.81 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-QMk69L-u.js                2.00 kB │ gzip:   0.95 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BQF_OYw8.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BN4CRJHv.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-8xBRgCaw.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-B572ey-v.js                2.02 kB │ gzip:   0.94 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-BBy84Dhr.js                2.02 kB │ gzip:   0.95 kB
+dist/assets/dict-search.vue_vue_type_script_setup_true_lang-BO9VSKfS.js                 2.02 kB │ gzip:   0.94 kB
+dist/assets/FirmwareDialog.vue_vue_type_script_setup_true_lang-BJMZTLRi.js              2.03 kB │ gzip:   1.16 kB
+dist/assets/index-Bcuq6Tvj.js                                                           2.05 kB │ gzip:   0.76 kB
+dist/assets/WaterRegionField-CS6YjOah.js                                                2.07 kB │ gzip:   1.10 kB
+dist/assets/useHeaderBar-CxrDsC_6.js                                                    2.08 kB │ gzip:   0.97 kB
+dist/assets/el-link-BzY-6Gw0.js                                                         2.15 kB │ gzip:   1.07 kB
+dist/assets/index-C3xr82Cm.js                                                           2.16 kB │ gzip:   0.95 kB
+dist/assets/index-_xnB4JqH.js                                                           2.16 kB │ gzip:   0.95 kB
+dist/assets/edit-dialog-CMTj-M_3.js                                                     2.16 kB │ gzip:   0.89 kB
+dist/assets/editInfo-DIi07765.js                                                        2.17 kB │ gzip:   0.88 kB
+dist/assets/el-skeleton-item-CJPR0-5b.js                                                2.21 kB │ gzip:   1.11 kB
+dist/assets/el-timeline-item-Cr8ydgzC.js                                                2.22 kB │ gzip:   1.00 kB
+dist/assets/index-ClC-19dZ.js                                                           2.29 kB │ gzip:   1.05 kB
+dist/assets/WaterWorkspace-BHRmRTxn.js                                                  2.30 kB │ gzip:   0.94 kB
+dist/assets/config-list-BePRe1JK.js                                                     2.31 kB │ gzip:   0.93 kB
+dist/assets/index-B7U6CemV.js                                                           2.34 kB │ gzip:   1.01 kB
+dist/assets/index-V84kc5Yk.js                                                           2.35 kB │ gzip:   1.02 kB
+dist/assets/EnterpriseAnalytics.vue_vue_type_script_setup_true_lang-C8noZTjK.js         2.37 kB │ gzip:   1.28 kB
+dist/assets/WaterLocationField-BNU443IF.js                                              2.38 kB │ gzip:   1.27 kB
+dist/assets/index-Cscs0r-N.js                                                           2.40 kB │ gzip:   0.94 kB
+dist/assets/index-Dx_SS-AT.js                                                           2.42 kB │ gzip:   1.12 kB
+dist/assets/index-BS4gyibB.js                                                           2.45 kB │ gzip:   1.13 kB
+dist/assets/FileSaver.min-BR7op60A.js                                                   2.49 kB │ gzip:   1.19 kB
+dist/assets/DeliveryHistory.vue_vue_type_script_setup_true_lang-DS-cGbEp.js             2.50 kB │ gzip:   1.42 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-CdpcXuLR.js                2.52 kB │ gzip:   1.03 kB
+dist/assets/index-B-k-ZTsd.js                                                           2.53 kB │ gzip:   1.21 kB
+dist/assets/el-space-BvryZYrX.js                                                        2.54 kB │ gzip:   1.20 kB
+dist/assets/el-alert-Caczv7-u.js                                                        2.55 kB │ gzip:   1.19 kB
+dist/assets/work-dialog.vue_vue_type_script_setup_true_lang-C0SMLAYi.js                 2.56 kB │ gzip:   1.33 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-B5Lam6aq.js                       2.57 kB │ gzip:   1.12 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-C_kOHg40.js                2.57 kB │ gzip:   1.14 kB
+dist/assets/index-Dcan-X7t.js                                                           2.64 kB │ gzip:   1.34 kB
+dist/assets/index-Bk1VCMnl.js                                                           2.65 kB │ gzip:   1.35 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-CSz3h3lM.js                 2.65 kB │ gzip:   1.39 kB
+dist/assets/business-time-dialog.vue_vue_type_style_index_0_lang-AaLC7GVi.js            2.68 kB │ gzip:   1.35 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-V4d7qJmQ.js                2.74 kB │ gzip:   1.22 kB
+dist/assets/table-search.vue_vue_type_script_setup_true_lang-DyyZERuR.js                2.75 kB │ gzip:   1.23 kB
+dist/assets/dropdown-pV6lcOOV.js                                                        2.78 kB │ gzip:   1.25 kB
+dist/assets/index-B4xEEAAI.js                                                           2.84 kB │ gzip:   1.24 kB
+dist/assets/index-C_6lrBNw.js                                                           2.95 kB │ gzip:   1.08 kB
+dist/assets/_baseClone-Drg9RIqB.js                                                      3.02 kB │ gzip:   1.27 kB
+dist/assets/index-B_W4LOBX.js                                                           3.07 kB │ gzip:   1.51 kB
+dist/assets/index-DlT7w5QV.js                                                           3.07 kB │ gzip:   1.77 kB
+dist/assets/permission-dialog.vue_vue_type_script_setup_true_lang-CDGQsGgY.js           3.08 kB │ gzip:   1.63 kB
+dist/assets/index-Cz0mr9fL.js                                                           3.15 kB │ gzip:   1.49 kB
+dist/assets/dict-search-BMU7IPD_.js                                                     3.15 kB │ gzip:   1.42 kB
+dist/assets/group-edit-dialog.vue_vue_type_script_setup_true_lang-D2dPCsBj.js           3.15 kB │ gzip:   1.47 kB
+dist/assets/index-sgBtpkM6.js                                                           3.15 kB │ gzip:   1.41 kB
+dist/assets/DeviceBindingDialog-CWWPE-1_.js                                             3.16 kB │ gzip:   1.66 kB
+dist/assets/move-dialog.vue_vue_type_script_setup_true_lang-DrsZ4Z4R.js                 3.16 kB │ gzip:   1.62 kB
+dist/assets/table-search-DLI8-hNr.js                                                    3.18 kB │ gzip:   1.43 kB
+dist/assets/withdrawal-update-dialog.vue_vue_type_style_index_0_lang-KqKR29ng.js        3.23 kB │ gzip:   1.44 kB
+dist/assets/ContentPhone.vue_vue_type_script_setup_true_lang-CX9uVQ8s.js                3.31 kB │ gzip:   1.75 kB
+dist/assets/preview-DhhD8bEx.js                                                         3.31 kB │ gzip:   1.73 kB
+dist/assets/DeviceAnalytics-Lqh0UzEc.js                                                 3.37 kB │ gzip:   1.86 kB
+dist/assets/type-edit-dialog.vue_vue_type_script_setup_true_lang-X9oobZyw.js            3.40 kB │ gzip:   1.57 kB
+dist/assets/index-BoPuujG6.js                                                           3.48 kB │ gzip:   1.44 kB
+dist/assets/index-DaO4fkL3.js                                                           3.50 kB │ gzip:   1.52 kB
+dist/assets/category-dialog.vue_vue_type_script_setup_true_lang-DCnk6Kb8.js             3.63 kB │ gzip:   1.70 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-CMIpQCvG.js                 3.69 kB │ gzip:   1.65 kB
+dist/assets/index-CxqidK38.js                                                           3.70 kB │ gzip:   1.75 kB
+dist/assets/index-BxgZS-2d.js                                                           3.71 kB │ gzip:   1.56 kB
+dist/assets/BatchDeliveryDialog.vue_vue_type_script_setup_true_lang-DbQEDd3r.js         3.75 kB │ gzip:   1.93 kB
+dist/assets/active-user.vue_vue_type_script_setup_true_lang-BBhbjZLA.js                 3.85 kB │ gzip:   1.71 kB
+dist/assets/index-DOph9Dz_.js                                                           3.89 kB │ gzip:   1.81 kB
+dist/assets/new-user-Bx9wVGGA.js                                                        3.95 kB │ gzip:   1.89 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-0J7E0nTi.js                       3.97 kB │ gzip:   1.96 kB
+dist/assets/recycle-list.vue_vue_type_script_setup_true_lang-By_Km97Q.js                3.98 kB │ gzip:   1.93 kB
+dist/assets/config-list.vue_vue_type_script_setup_true_lang-CAm2BSNy.js                 3.98 kB │ gzip:   1.91 kB
+dist/assets/ContentPreviewDialog.vue_vue_type_script_setup_true_lang-C8F-wZfy.js        4.01 kB │ gzip:   2.05 kB
+dist/assets/index-KvuTdFCx.js                                                           4.07 kB │ gzip:   2.15 kB
+dist/assets/list-page-60EW0xi-.js                                                       4.07 kB │ gzip:   1.91 kB
+dist/assets/index.vue_vue_type_style_index_0_lang-SrAG63Cq.js                           4.08 kB │ gzip:   2.01 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-BuHJ67v5.js                 4.09 kB │ gzip:   1.82 kB
+dist/assets/index-BdmiTw2R.js                                                           4.10 kB │ gzip:   1.62 kB
+dist/assets/settingComponent.vue_vue_type_script_setup_true_lang-IfUGTO6p.js            4.12 kB │ gzip:   1.67 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-BP5PNFzv.js                       4.16 kB │ gzip:   1.84 kB
+dist/assets/install-box.vue_vue_type_script_setup_true_lang-B3TxE0I7.js                 4.17 kB │ gzip:   2.00 kB
+dist/assets/list-page-C24x9JWa.js                                                       4.23 kB │ gzip:   1.89 kB
+dist/assets/index-DuVeViOH.js                                                           4.45 kB │ gzip:   2.22 kB
+dist/assets/el-overlay-Dp_luZTF.js                                                      4.66 kB │ gzip:   1.83 kB
+dist/assets/index-Bq2oBa8K.js                                                           4.68 kB │ gzip:   2.35 kB
+dist/assets/log-list.vue_vue_type_script_setup_true_lang-CEVVpY5y.js                    4.69 kB │ gzip:   2.30 kB
+dist/assets/el-progress-C0IUiTPQ.js                                                     4.73 kB │ gzip:   1.98 kB
+dist/assets/AlertPolicyDialog.vue_vue_type_script_setup_true_lang-BRZiYqis.js           4.77 kB │ gzip:   2.31 kB
+dist/assets/el-empty-DujfTtxA.js                                                        4.82 kB │ gzip:   1.68 kB
+dist/assets/index.vue_vue_type_script_setup_true_lang-DLurgPyd.js                       4.82 kB │ gzip:   1.94 kB
+dist/assets/loadTable.vue_vue_type_script_setup_true_lang-B8HlL5ES.js                   4.86 kB │ gzip:   2.36 kB
+dist/assets/el-image-B6YDa6BK.js                                                        4.99 kB │ gzip:   2.24 kB
+dist/assets/index-CA83Vrk7.js                                                           5.00 kB │ gzip:   2.18 kB
+dist/assets/el-collapse-item-CEq51zTt.js                                                5.01 kB │ gzip:   2.19 kB
+dist/assets/el-drawer-M9Arf7-Y.js                                                       5.09 kB │ gzip:   2.32 kB
+dist/assets/waterAmap-DZ8_MN8P.js                                                       5.13 kB │ gzip:   2.30 kB
+dist/assets/el-switch-B1xwBnYs.js                                                       5.17 kB │ gzip:   1.92 kB
+dist/assets/el-dialog-BuuOQiR8.js                                                       5.17 kB │ gzip:   2.24 kB
+dist/assets/index-4jtktL9N.js                                                           5.24 kB │ gzip:   2.29 kB
+dist/assets/el-descriptions-item-DdL9kvpE.js                                            5.27 kB │ gzip:   2.09 kB
+dist/assets/index-Dfirqoxe.js                                                           5.32 kB │ gzip:   2.33 kB
+dist/assets/config-edit-dialog.vue_vue_type_script_setup_true_lang-DCBBU8ks.js          5.43 kB │ gzip:   2.22 kB
+dist/assets/card-list.vue_vue_type_script_setup_true_lang-I9GsngvT.js                   5.45 kB │ gzip:   1.94 kB
+dist/assets/LoginLeftView-BNuulhHu.js                                                   5.48 kB │ gzip:   2.19 kB
+dist/assets/index-C-drnk6m.js                                                           5.49 kB │ gzip:   2.49 kB
+dist/assets/index-DgBfbIB-.js                                                           5.49 kB │ gzip:   2.25 kB
+dist/assets/index-Di6X_TAw.js                                                           5.51 kB │ gzip:   2.48 kB
+dist/assets/withdraw-settings-GekuL1YB.js                                               5.55 kB │ gzip:   1.82 kB
+dist/assets/DeviceEditor.vue_vue_type_script_setup_true_lang-M7ONU9OB.js                5.63 kB │ gzip:   2.43 kB
+dist/assets/WaterLocationPicker-D2ecgLOj.js                                             5.67 kB │ gzip:   2.70 kB
+dist/assets/index-CN-4GMXZ.js                                                           5.71 kB │ gzip:   2.28 kB
+dist/assets/index-Dnz5sn8Z.js                                                           5.74 kB │ gzip:   2.32 kB
+dist/assets/index-DPH3kO2l.js                                                           5.77 kB │ gzip:   2.34 kB
+dist/assets/index-CKceIeVp.js                                                           5.81 kB │ gzip:   2.56 kB
+dist/assets/el-radio-group-D0vYz6xZ.js                                                  5.88 kB │ gzip:   2.31 kB
+dist/assets/EnterpriseDetail-CDVgmCdU.js                                                5.90 kB │ gzip:   2.75 kB
+dist/assets/AgentDetail-Cb3yB4HH.js                                                     5.94 kB │ gzip:   2.62 kB
+dist/assets/focus-trap-Db6G_DfR.js                                                      5.97 kB │ gzip:   2.41 kB
+dist/assets/index-O0EWoe1B.js                                                           6.03 kB │ gzip:   2.52 kB
+dist/assets/edit-page-DP4a2xg6.js                                                       6.05 kB │ gzip:   2.55 kB
+dist/assets/AssignmentRecordsDialog-BMAiWBeX.js                                         6.12 kB │ gzip:   2.72 kB
+dist/assets/index-BAxlnhXx.js                                                           6.13 kB │ gzip:   2.80 kB
+dist/assets/device-binding-dialog-DDnDbICc.js                                           6.16 kB │ gzip:   2.96 kB
+dist/assets/index-zA0PfuYj.js                                                           6.20 kB │ gzip:   2.87 kB
+dist/assets/index-fDQjyalh.js                                                           6.20 kB │ gzip:   2.64 kB
+dist/assets/index-DUBbSFV7.js                                                           6.22 kB │ gzip:   2.18 kB
+dist/assets/table-edit.vue_vue_type_script_setup_true_lang-CoHNPRhr.js                  6.24 kB │ gzip:   2.38 kB
+dist/assets/index-DhEyo2By.js                                                           6.31 kB │ gzip:   2.91 kB
+dist/assets/DispatchMap-aL0euslj.js                                                     6.37 kB │ gzip:   3.18 kB
+dist/assets/index-Bo-Fo0aG.js                                                           6.44 kB │ gzip:   2.77 kB
+dist/assets/ReceiptDialog.vue_vue_type_script_setup_true_lang-CUp2sp13.js               6.45 kB │ gzip:   2.92 kB
+dist/assets/index-BjTPs-bh.js                                                           6.56 kB │ gzip:   2.85 kB
+dist/assets/el-tree-select-C39nFczi.js                                                  6.68 kB │ gzip:   2.68 kB
+dist/assets/index-CaZFi8De.js                                                           6.73 kB │ gzip:   2.83 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-BjhU1q_d.js                 6.92 kB │ gzip:   2.54 kB
+dist/assets/useChart-CgyCrnMc.js                                                        6.96 kB │ gzip:   3.14 kB
+dist/assets/index-DSQEf030.js                                                           7.00 kB │ gzip:   2.88 kB
+dist/assets/index-Cxxq8Gqy.js                                                           7.13 kB │ gzip:   2.97 kB
+dist/assets/EnterpriseEditor-DTHmpyMu.js                                                7.20 kB │ gzip:   3.09 kB
+dist/assets/index-RSfMiP98.js                                                           7.25 kB │ gzip:   3.05 kB
+dist/assets/el-input-number-BnDqS78H.js                                                 7.26 kB │ gzip:   2.94 kB
+dist/assets/index-H8o_d-CN.js                                                           7.30 kB │ gzip:   2.03 kB
+dist/assets/ContentEditor.vue_vue_type_script_setup_true_lang-DqejFpbz.js               7.30 kB │ gzip:   3.05 kB
+dist/assets/index-ClWtZ1AQ.js                                                           7.36 kB │ gzip:   2.75 kB
+dist/assets/el-image-viewer-BR2d0zUK.js                                                 7.45 kB │ gzip:   3.03 kB
+dist/assets/WaterLedgerDrawer-Zyxabad2.js                                               7.48 kB │ gzip:   3.39 kB
+dist/assets/index-W4a1zGvr.js                                                           7.50 kB │ gzip:   4.51 kB
+dist/assets/index-DTilINY9.js                                                           7.51 kB │ gzip:   2.78 kB
+dist/assets/catalog-CKg_I-5f.js                                                         7.51 kB │ gzip:   2.68 kB
+dist/assets/InventoryForm.vue_vue_type_script_setup_true_lang-DQFbHd4h.js               7.63 kB │ gzip:   3.00 kB
+dist/assets/el-scrollbar-D59ngoVk.js                                                    7.69 kB │ gzip:   3.09 kB
+dist/assets/index-BY52tATz.js                                                           7.79 kB │ gzip:   3.75 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-DpgDk1po.js                 7.86 kB │ gzip:   2.73 kB
+dist/assets/DeviceAssignmentDialog-CsP-vyc4.js                                          8.27 kB │ gzip:   3.75 kB
+dist/assets/AgentEditor-C-X4T0pB.js                                                     8.31 kB │ gzip:   3.69 kB
+dist/assets/index-Cab3DACI.js                                                           8.42 kB │ gzip:   3.41 kB
+dist/assets/edit-page-Bc48opB2.js                                                       8.47 kB │ gzip:   3.13 kB
+dist/assets/FulfillmentDialog.vue_vue_type_script_setup_true_lang-BqhIYjph.js           8.52 kB │ gzip:   3.50 kB
+dist/assets/index-T2z8-cod.js                                                           8.54 kB │ gzip:   3.54 kB
+dist/assets/index-CsyJTWWd.js                                                           8.66 kB │ gzip:   3.61 kB
+dist/assets/index-BNEqQBvq.js                                                           8.70 kB │ gzip:   3.72 kB
+dist/assets/index-CAfXNVjw.js                                                           9.00 kB │ gzip:   3.78 kB
+dist/assets/RouteOptimization-CDC56iiV.js                                               9.19 kB │ gzip:   4.22 kB
+dist/assets/index-D1i5ySVc.js                                                           9.25 kB │ gzip:   3.75 kB
+dist/assets/index-CRYb-kki.js                                                           9.27 kB │ gzip:   4.07 kB
+dist/assets/index-DC2uOXSM.js                                                           9.32 kB │ gzip:   4.89 kB
+dist/assets/edit-dialog.vue_vue_type_script_setup_true_lang-BU_d3IQ8.js                 9.34 kB │ gzip:   3.79 kB
+dist/assets/index-i04WmTRV.js                                                           9.47 kB │ gzip:   3.74 kB
+dist/assets/index-DvVREqqW.js                                                           9.74 kB │ gzip:   4.05 kB
+dist/assets/index-CJXfRCnI.js                                                           9.84 kB │ gzip:   3.88 kB
+dist/assets/el-checkbox-8WPskwOs.js                                                    10.06 kB │ gzip:   3.50 kB
+dist/assets/index-Y80xVsIZ.js                                                          10.08 kB │ gzip:   4.04 kB
+dist/assets/index-wyfVpEEj.js                                                          10.16 kB │ gzip:   3.70 kB
+dist/assets/index-CsmMvmU5.js                                                          10.22 kB │ gzip:   4.62 kB
+dist/assets/index-Tua3gxz3.js                                                          10.38 kB │ gzip:   3.96 kB
+dist/assets/index-BPxEJ5Hl.js                                                          10.59 kB │ gzip:   4.06 kB
+dist/assets/index-Dm6CluJw.js                                                          11.10 kB │ gzip:   4.16 kB
+dist/assets/avatar6-6Evj8BB9.js                                                        11.25 kB │ gzip:   8.36 kB
+dist/assets/AgentList-DVT7PZNu.js                                                      11.26 kB │ gzip:   4.57 kB
+dist/assets/terminal-kc_A2SrU.js                                                       11.33 kB │ gzip:   4.36 kB
+dist/assets/DispatchForm-mWDI090w.js                                                   11.39 kB │ gzip:   4.19 kB
+dist/assets/index-BMTiN11E.js                                                          11.44 kB │ gzip:   4.74 kB
+dist/assets/overlay-CHkflwq0.js                                                        11.61 kB │ gzip:   4.30 kB
+dist/assets/index-CN0f8AtB.js                                                          11.74 kB │ gzip:   5.04 kB
+dist/assets/OrderList-BI1Lp1qY.js                                                      11.98 kB │ gzip:   4.07 kB
+dist/assets/index-Bf-gPpq0.js                                                          12.05 kB │ gzip:   4.46 kB
+dist/assets/el-pagination-D183zjrB.js                                                  12.16 kB │ gzip:   4.11 kB
+dist/assets/index-CZf8CJmL.js                                                          12.43 kB │ gzip:   4.46 kB
+dist/assets/index-CQObHKLo.js                                                          12.44 kB │ gzip:   4.22 kB
+dist/assets/index-CE_74kYI.js                                                          12.55 kB │ gzip:   4.00 kB
+dist/assets/index-D8Wjw5xm.js                                                          12.64 kB │ gzip:   5.00 kB
+dist/assets/el-tab-pane-DvtkZaio.js                                                    12.71 kB │ gzip:   5.34 kB
+dist/assets/index-fnNUDtOQ.js                                                          12.73 kB │ gzip:   5.42 kB
+dist/assets/index-GLOJrxyS.js                                                          13.23 kB │ gzip:   4.73 kB
+dist/assets/index-C0YZuBSz.js                                                          13.32 kB │ gzip:   4.45 kB
+dist/assets/el-upload-B6kD9xFR.js                                                      13.78 kB │ gzip:   4.97 kB
+dist/assets/index-C5Dz1VF0.js                                                          13.87 kB │ gzip:   5.70 kB
+dist/assets/index-KKukStqE.js                                                          14.24 kB │ gzip:   5.10 kB
+dist/assets/el-dropdown-item-GlrRW81G.js                                               14.28 kB │ gzip:   5.02 kB
+dist/assets/index-B9cejfeV.js                                                          14.33 kB │ gzip:   5.96 kB
+dist/assets/index-D0TYPyiy.js                                                          14.76 kB │ gzip:   5.44 kB
+dist/assets/index-g6zJUAbR.js                                                          14.96 kB │ gzip:   5.37 kB
+dist/assets/index-BaH9H28b.js                                                          15.32 kB │ gzip:   5.47 kB
+dist/assets/index-DmfrBXlA.js                                                          17.08 kB │ gzip:   6.84 kB
+dist/assets/useTable-VD6D0SuE.js                                                       18.11 kB │ gzip:   7.66 kB
+dist/assets/device-4qn9GDRb.js                                                         18.12 kB │ gzip:   6.57 kB
+dist/assets/iconify-9209hsOe.js                                                        18.54 kB │ gzip:   7.06 kB
+dist/assets/index-Cmf5AHP8.js                                                          19.11 kB │ gzip:   7.36 kB
+dist/assets/el-button-D6xSIxbs.js                                                      19.98 kB │ gzip:   6.95 kB
+dist/assets/dict-edit-dialog.vue_vue_type_script_setup_true_lang-vx0meXP5.js           21.42 kB │ gzip:   7.44 kB
+dist/assets/index-CdZsDqCi.js                                                          22.02 kB │ gzip:   7.65 kB
+dist/assets/WaterWorkspace.vue_vue_type_script_setup_true_lang-BUFquwpn.js             22.81 kB │ gzip:   8.69 kB
+dist/assets/index-VcdrIQH_.js                                                          23.86 kB │ gzip:   8.56 kB
+dist/assets/el-form-item-I9ajA9k3.js                                                   27.20 kB │ gzip:   9.12 kB
+dist/assets/el-select-CnfQsXC2.js                                                      28.77 kB │ gzip:   9.55 kB
+dist/assets/el-text-Bmvk54d7.js                                                        29.02 kB │ gzip:   9.34 kB
+dist/assets/index-BbnA-KSe.js                                                          29.73 kB │ gzip:  10.13 kB
+dist/assets/index-C5KTt-5Q.js                                                          39.02 kB │ gzip:  13.58 kB
+dist/assets/index-D1WqPH0A.js                                                          48.20 kB │ gzip:  16.61 kB
+dist/assets/editInfo.vue_vue_type_script_setup_true_lang-C-0DO34E.js                   50.54 kB │ gzip:  15.72 kB
+dist/assets/index-BgBFlSNx.js                                                          64.87 kB │ gzip:  21.64 kB
+dist/assets/el-table-column-DYT3kyxm.js                                                75.06 kB │ gzip:  25.00 kB
+dist/assets/index-Bi5JEEBm.js                                                          77.67 kB │ gzip:  31.88 kB
+dist/assets/el-date-picker-panel-BVnHrxDR.js                                           96.34 kB │ gzip:  26.89 kB
+dist/assets/edit-dialog-BwQ3EUdI.js                                                   145.93 kB │ gzip:  33.67 kB
+dist/assets/WaterChart.vue_vue_type_script_setup_true_lang-UxQAIEIF.js                348.53 kB │ gzip: 114.37 kB
+dist/assets/xlsx-ByDo_lG2.js                                                          417.70 kB │ gzip: 138.85 kB
+dist/assets/installCanvasRenderer-D-xUkWdX.js                                         764.88 kB │ gzip: 251.02 kB
+dist/assets/index.esm-NA4Tgubb.js                                                     810.82 kB │ gzip: 274.31 kB
+dist/assets/index-D1lEDoZA.js                                                       1,426.76 kB │ gzip: 463.87 kB
+✓ built in 55.20s

BIN
saiadmin-artd/package-delete-build.log


+ 126 - 0
saiadmin-artd/package.json

@@ -0,0 +1,126 @@
+{
+  "name": "art-design-pro",
+  "version": "0.0.0",
+  "type": "module",
+  "engines": {
+    "node": ">=20.19.0",
+    "pnpm": ">=8.8.0"
+  },
+  "scripts": {
+    "dev": "vite --open",
+    "build": "vue-tsc --noEmit && vite build",
+    "serve": "vite preview",
+    "lint": "eslint",
+    "fix": "eslint --fix",
+    "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
+    "lint:stylelint": "stylelint  \"**/*.{css,scss,vue}\" --fix",
+    "lint:lint-staged": "lint-staged",
+    "prepare": "husky",
+    "commit": "git-cz",
+    "clean:dev": "tsx scripts/clean-dev.ts"
+  },
+  "config": {
+    "commitizen": {
+      "path": "node_modules/cz-git"
+    }
+  },
+  "lint-staged": {
+    "*.{js,ts,mjs,mts,tsx}": [
+      "eslint --fix",
+      "prettier --write"
+    ],
+    "*.{cjs,json,jsonc}": [
+      "prettier --write"
+    ],
+    "*.vue": [
+      "eslint --fix",
+      "stylelint --fix --allow-empty-input",
+      "prettier --write"
+    ],
+    "*.{html,htm}": [
+      "prettier --write"
+    ],
+    "*.{scss,css,less}": [
+      "stylelint --fix --allow-empty-input",
+      "prettier --write"
+    ],
+    "*.{md,mdx}": [
+      "prettier --write"
+    ],
+    "*.{yaml,yml}": [
+      "prettier --write"
+    ]
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.2",
+    "@iconify/vue": "^5.0.0",
+    "@tailwindcss/vite": "^4.1.14",
+    "@vue/reactivity": "^3.5.21",
+    "@vueuse/core": "^13.9.0",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "next",
+    "axios": "^1.12.2",
+    "crypto-js": "^4.2.0",
+    "echarts": "^6.0.0",
+    "element-plus": "^2.11.2",
+    "file-saver": "^2.0.5",
+    "highlight.js": "^11.10.0",
+    "mitt": "^3.0.1",
+    "md-editor-v3": "^6.3.1",
+    "nprogress": "^0.2.0",
+    "ohash": "^2.0.11",
+    "pinia": "^3.0.3",
+    "pinia-plugin-persistedstate": "^4.3.0",
+    "qrcode.vue": "^3.6.0",
+    "spark-md5": "^3.0.2",
+    "tailwindcss": "^4.1.14",
+    "vue": "^3.5.21",
+    "vue-draggable-plus": "^0.6.0",
+    "vue-i18n": "^9.14.0",
+    "vue-router": "^4.5.1",
+    "xgplayer": "^3.0.20",
+    "xlsx": "^0.18.5"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "^19.4.1",
+    "@commitlint/config-conventional": "^19.4.1",
+    "@eslint/js": "^9.9.1",
+    "@types/node": "^24.0.5",
+    "@types/spark-md5": "^3.0.5",
+    "@typescript-eslint/eslint-plugin": "^8.3.0",
+    "@typescript-eslint/parser": "^8.3.0",
+    "@vitejs/plugin-vue": "^6.0.1",
+    "@vue/compiler-sfc": "^3.0.5",
+    "commitizen": "^4.3.0",
+    "cz-git": "^1.11.1",
+    "eslint": "^9.9.1",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-prettier": "^5.2.1",
+    "eslint-plugin-vue": "^9.27.0",
+    "globals": "^15.9.0",
+    "husky": "^9.1.5",
+    "lint-staged": "^15.5.2",
+    "prettier": "^3.5.3",
+    "rollup-plugin-visualizer": "^5.12.0",
+    "sass": "^1.81.0",
+    "stylelint": "^16.20.0",
+    "stylelint-config-html": "^1.1.0",
+    "stylelint-config-recess-order": "^4.6.0",
+    "stylelint-config-recommended-scss": "^14.1.0",
+    "stylelint-config-recommended-vue": "^1.5.0",
+    "stylelint-config-standard": "^36.0.1",
+    "terser": "^5.36.0",
+    "tsx": "^4.20.3",
+    "typescript": "~5.6.3",
+    "typescript-eslint": "^8.9.0",
+    "unplugin-auto-import": "^20.2.0",
+    "unplugin-element-plus": "^0.10.0",
+    "unplugin-vue-components": "^29.1.0",
+    "vite": "^7.1.5",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-vue-devtools": "^7.7.6",
+    "vue-demi": "^0.14.9",
+    "vue-img-cutter": "^3.0.5",
+    "vue-tsc": "~2.1.6"
+  }
+}

BIN
saiadmin-artd/public/favicon.ico


+ 838 - 0
saiadmin-artd/scripts/clean-dev.ts

@@ -0,0 +1,838 @@
+// scripts/clean-dev.ts
+import fs from 'fs/promises'
+import path from 'path'
+
+// 现代化颜色主题
+const theme = {
+  // 基础颜色
+  reset: '\x1b[0m',
+  bold: '\x1b[1m',
+  dim: '\x1b[2m',
+
+  // 前景色
+  primary: '\x1b[38;5;75m', // 亮蓝色
+  success: '\x1b[38;5;82m', // 亮绿色
+  warning: '\x1b[38;5;220m', // 亮黄色
+  error: '\x1b[38;5;196m', // 亮红色
+  info: '\x1b[38;5;159m', // 青色
+  purple: '\x1b[38;5;141m', // 紫色
+  orange: '\x1b[38;5;208m', // 橙色
+  gray: '\x1b[38;5;245m', // 灰色
+  white: '\x1b[38;5;255m', // 白色
+
+  // 背景色
+  bgDark: '\x1b[48;5;235m', // 深灰背景
+  bgBlue: '\x1b[48;5;24m', // 蓝色背景
+  bgGreen: '\x1b[48;5;22m', // 绿色背景
+  bgRed: '\x1b[48;5;52m' // 红色背景
+}
+
+// 现代化图标集
+const icons = {
+  rocket: '🚀',
+  fire: '🔥',
+  star: '⭐',
+  gem: '💎',
+  crown: '👑',
+  magic: '✨',
+  warning: '⚠️',
+  success: '✅',
+  error: '❌',
+  info: 'ℹ️',
+  folder: '📁',
+  file: '📄',
+  image: '🖼️',
+  code: '💻',
+  data: '📊',
+  globe: '🌐',
+  map: '🗺️',
+  chat: '💬',
+  bolt: '⚡',
+  shield: '🛡️',
+  key: '🔑',
+  link: '🔗',
+  clean: '🧹',
+  trash: '🗑️',
+  check: '✓',
+  cross: '✗',
+  arrow: '→',
+  loading: '⏳'
+}
+
+// 格式化工具
+const fmt = {
+  title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`,
+  subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`,
+  success: (text: string) => `${theme.success}${text}${theme.reset}`,
+  error: (text: string) => `${theme.error}${text}${theme.reset}`,
+  warning: (text: string) => `${theme.warning}${text}${theme.reset}`,
+  info: (text: string) => `${theme.info}${text}${theme.reset}`,
+  highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`,
+  dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`,
+  orange: (text: string) => `${theme.orange}${text}${theme.reset}`,
+
+  // 带背景的文本
+  badge: (text: string, bg: string = theme.bgBlue) =>
+    `${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`,
+
+  // 渐变效果模拟
+  gradient: (text: string) => {
+    const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m']
+    const chars = text.split('')
+    return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset
+  }
+}
+
+// 创建现代化标题横幅
+function createModernBanner() {
+  console.log()
+  console.log(
+    fmt.gradient('  ╔══════════════════════════════════════════════════════════════════╗')
+  )
+  console.log(
+    fmt.gradient('  ║                                                                  ║')
+  )
+  console.log(
+    `  ║               ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic}                ║`
+  )
+  console.log(
+    `  ║               ${fmt.dim('为项目移除演示数据,快速切换至开发模式')}             ║`
+  )
+  console.log(
+    fmt.gradient('  ║                                                                  ║')
+  )
+  console.log(
+    fmt.gradient('  ╚══════════════════════════════════════════════════════════════════╝')
+  )
+  console.log()
+}
+
+// 创建分割线
+function createDivider(char = '─', color = theme.primary) {
+  console.log(`${color}${'  ' + char.repeat(66)}${theme.reset}`)
+}
+
+// 创建卡片样式容器
+function createCard(title: string, content: string[]) {
+  console.log(`  ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`)
+  console.log()
+  content.forEach((line) => {
+    console.log(`     ${line}`)
+  })
+  console.log()
+}
+
+// 进度条动画
+function createProgressBar(current: number, total: number, text: string, width = 40) {
+  const percentage = Math.round((current / total) * 100)
+  const filled = Math.round((current / total) * width)
+  const empty = width - filled
+
+  const filledBar = '█'.repeat(filled)
+  const emptyBar = '░'.repeat(empty)
+
+  process.stdout.write(
+    `\r  ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}`
+  )
+
+  if (current === total) {
+    console.log()
+  }
+}
+
+// 统计信息
+const stats = {
+  deletedFiles: 0,
+  deletedPaths: 0,
+  failedPaths: 0,
+  startTime: Date.now(),
+  totalFiles: 0
+}
+
+// 清理目标
+const targets = [
+  'README.md',
+  'README.zh-CN.md',
+  'CHANGELOG.md',
+  'CHANGELOG.zh-CN.md',
+  'src/views/change',
+  'src/views/safeguard',
+  'src/views/article',
+  'src/views/examples',
+  'src/views/system/nested',
+  'src/views/widgets',
+  'src/views/template',
+  'src/views/dashboard/analysis',
+  'src/views/dashboard/ecommerce',
+  'src/mock/json',
+  'src/mock/temp/articleList.ts',
+  'src/mock/temp/commentDetail.ts',
+  'src/mock/temp/commentList.ts',
+  'src/assets/images/cover',
+  'src/assets/images/safeguard',
+  'src/assets/images/3d',
+  'src/components/core/charts/art-map-chart',
+  'src/components/business/comment-widget'
+]
+
+// 递归统计文件数量
+async function countFiles(targetPath: string): Promise<number> {
+  const fullPath = path.resolve(process.cwd(), targetPath)
+
+  try {
+    const stat = await fs.stat(fullPath)
+
+    if (stat.isFile()) {
+      return 1
+    } else if (stat.isDirectory()) {
+      const entries = await fs.readdir(fullPath)
+      let count = 0
+
+      for (const entry of entries) {
+        const entryPath = path.join(targetPath, entry)
+        count += await countFiles(entryPath)
+      }
+
+      return count
+    }
+  } catch {
+    return 0
+  }
+
+  return 0
+}
+
+// 统计所有目标的文件数量
+async function countAllFiles(): Promise<number> {
+  let totalCount = 0
+
+  for (const target of targets) {
+    const count = await countFiles(target)
+    totalCount += count
+  }
+
+  return totalCount
+}
+
+// 删除文件和目录
+async function remove(targetPath: string, index: number) {
+  const fullPath = path.resolve(process.cwd(), targetPath)
+
+  createProgressBar(index + 1, targets.length, targetPath)
+
+  try {
+    const fileCount = await countFiles(targetPath)
+    await fs.rm(fullPath, { recursive: true, force: true })
+    stats.deletedFiles += fileCount
+    stats.deletedPaths++
+    await new Promise((resolve) => setTimeout(resolve, 50))
+  } catch (err) {
+    stats.failedPaths++
+    console.log()
+    console.log(`     ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`)
+    console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+  }
+}
+
+// 清理路由模块
+async function cleanRouteModules() {
+  const modulesPath = path.resolve(process.cwd(), 'src/router/modules')
+
+  try {
+    // 删除演示相关的路由模块
+    const modulesToRemove = [
+      'template.ts',
+      'widgets.ts',
+      'examples.ts',
+      'article.ts',
+      'safeguard.ts',
+      'help.ts'
+    ]
+
+    for (const module of modulesToRemove) {
+      const modulePath = path.join(modulesPath, module)
+      try {
+        await fs.rm(modulePath, { force: true })
+      } catch {
+        // 文件不存在时忽略错误
+      }
+    }
+
+    // 重写 dashboard.ts - 只保留 console
+    const dashboardContent = `import { AppRouteRecord } from '@/types/router'
+
+export const dashboardRoutes: AppRouteRecord = {
+  name: 'Dashboard',
+  path: '/dashboard',
+  component: '/index/index',
+  meta: {
+    title: 'menus.dashboard.title',
+    icon: 'ri:pie-chart-line',
+    roles: ['R_SUPER', 'R_ADMIN']
+  },
+  children: [
+    {
+      path: 'console',
+      name: 'Console',
+      component: '/dashboard/console',
+      meta: {
+        title: 'menus.dashboard.console',
+        keepAlive: false,
+        fixedTab: true
+      }
+    }
+  ]
+}
+`
+    await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8')
+
+    // 重写 system.ts - 移除 nested 嵌套菜单
+    const systemContent = `import { AppRouteRecord } from '@/types/router'
+
+export const systemRoutes: AppRouteRecord = {
+  path: '/system',
+  name: 'System',
+  component: '/index/index',
+  meta: {
+    title: 'menus.system.title',
+    icon: 'ri:user-3-line',
+    roles: ['R_SUPER', 'R_ADMIN']
+  },
+  children: [
+    {
+      path: 'user',
+      name: 'User',
+      component: '/system/user',
+      meta: {
+        title: 'menus.system.user',
+        keepAlive: true,
+        roles: ['R_SUPER', 'R_ADMIN']
+      }
+    },
+    {
+      path: 'role',
+      name: 'Role',
+      component: '/system/role',
+      meta: {
+        title: 'menus.system.role',
+        keepAlive: true,
+        roles: ['R_SUPER']
+      }
+    },
+    {
+      path: 'user-center',
+      name: 'UserCenter',
+      component: '/system/user-center',
+      meta: {
+        title: 'menus.system.userCenter',
+        isHide: true,
+        keepAlive: true,
+        isHideTab: true
+      }
+    },
+    {
+      path: 'menu',
+      name: 'Menus',
+      component: '/system/menu',
+      meta: {
+        title: 'menus.system.menu',
+        keepAlive: true,
+        roles: ['R_SUPER'],
+        authList: [
+          { title: '新增', authMark: 'add' },
+          { title: '编辑', authMark: 'edit' },
+          { title: '删除', authMark: 'delete' }
+        ]
+      }
+    }
+  ]
+}
+`
+    await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8')
+
+    // 重写 index.ts - 只导入保留的模块
+    const indexContent = `import { AppRouteRecord } from '@/types/router'
+import { dashboardRoutes } from './dashboard'
+import { systemRoutes } from './system'
+import { resultRoutes } from './result'
+import { exceptionRoutes } from './exception'
+
+/**
+ * 导出所有模块化路由
+ */
+export const routeModules: AppRouteRecord[] = [
+  dashboardRoutes,
+  systemRoutes,
+  resultRoutes,
+  exceptionRoutes
+]
+`
+    await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8')
+
+    console.log(`     ${icons.success} ${fmt.success('清理路由模块完成')}`)
+  } catch (err) {
+    console.log(`     ${icons.error} ${fmt.error('清理路由模块失败')}`)
+    console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+  }
+}
+
+// 清理路由别名
+async function cleanRoutesAlias() {
+  const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts')
+
+  try {
+    const cleanedAlias = `/**
+ * 公共路由别名
+ # 存放系统级公共路由路径,如布局容器、登录页等   
+ */
+export enum RoutesAlias {
+  Layout = '/index/index', // 布局容器
+  Login = '/auth/login' // 登录页
+}
+`
+
+    await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8')
+    console.log(`     ${icons.success} ${fmt.success('重写路由别名配置完成')}`)
+  } catch (err) {
+    console.log(`     ${icons.error} ${fmt.error('清理路由别名失败')}`)
+    console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+  }
+}
+
+// 清理变更日志
+async function cleanChangeLog() {
+  const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts')
+
+  try {
+    const cleanedChangeLog = `import { ref } from 'vue'
+
+interface UpgradeLog {
+  version: string // 版本号
+  title: string // 更新标题
+  date: string // 更新日期
+  detail?: string[] // 更新内容
+  requireReLogin?: boolean // 是否需要重新登录
+  remark?: string // 备注
+}
+
+export const upgradeLogList = ref<UpgradeLog[]>([])
+`
+
+    await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8')
+    console.log(`     ${icons.success} ${fmt.success('清空变更日志数据完成')}`)
+  } catch (err) {
+    console.log(`     ${icons.error} ${fmt.error('清理变更日志失败')}`)
+    console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+  }
+}
+
+// 清理语言文件
+async function cleanLanguageFiles() {
+  const languageFiles = [
+    { path: 'src/locales/langs/zh.json', name: '中文语言文件' },
+    { path: 'src/locales/langs/en.json', name: '英文语言文件' }
+  ]
+
+  for (const { path: langPath, name } of languageFiles) {
+    try {
+      const fullPath = path.resolve(process.cwd(), langPath)
+      const content = await fs.readFile(fullPath, 'utf-8')
+      const langData = JSON.parse(content)
+
+      const menusToRemove = [
+        'widgets',
+        'template',
+        'article',
+        'examples',
+        'safeguard',
+        'plan',
+        'help'
+      ]
+
+      if (langData.menus) {
+        menusToRemove.forEach((menuKey) => {
+          if (langData.menus[menuKey]) {
+            delete langData.menus[menuKey]
+          }
+        })
+
+        if (langData.menus.dashboard) {
+          if (langData.menus.dashboard.analysis) {
+            delete langData.menus.dashboard.analysis
+          }
+          if (langData.menus.dashboard.ecommerce) {
+            delete langData.menus.dashboard.ecommerce
+          }
+        }
+
+        if (langData.menus.system) {
+          const systemKeysToRemove = [
+            'nested',
+            'menu1',
+            'menu2',
+            'menu21',
+            'menu3',
+            'menu31',
+            'menu32',
+            'menu321'
+          ]
+          systemKeysToRemove.forEach((key) => {
+            if (langData.menus.system[key]) {
+              delete langData.menus.system[key]
+            }
+          })
+        }
+      }
+
+      await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8')
+      console.log(`     ${icons.success} ${fmt.success(`清理${name}完成`)}`)
+    } catch (err) {
+      console.log(`     ${icons.error} ${fmt.error(`清理${name}失败`)}`)
+      console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+    }
+  }
+}
+
+// 清理快速入口组件
+async function cleanFastEnterComponent() {
+  const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts')
+
+  try {
+    const cleanedFastEnter = `/**
+ * 快速入口配置
+ * 包含:应用列表、快速链接等配置
+ */
+import { WEB_LINKS } from '@/utils/constants'
+import type { FastEnterConfig } from '@/types/config'
+
+const fastEnterConfig: FastEnterConfig = {
+  // 显示条件(屏幕宽度)
+  minWidth: 1200,
+  // 应用列表
+  applications: [
+    {
+      name: '工作台',
+      description: '系统概览与数据统计',
+      icon: 'ri:pie-chart-line',
+      iconColor: '#377dff',
+      enabled: true,
+      order: 1,
+      routeName: 'Console'
+    },
+    {
+      name: '官方文档',
+      description: '使用指南与开发文档',
+      icon: 'ri:bill-line',
+      iconColor: '#ffb100',
+      enabled: true,
+      order: 2,
+      link: WEB_LINKS.DOCS
+    },
+    {
+      name: '技术支持',
+      description: '技术支持与问题反馈',
+      icon: 'ri:user-location-line',
+      iconColor: '#ff6b6b',
+      enabled: true,
+      order: 3,
+      link: WEB_LINKS.COMMUNITY
+    },
+    {
+      name: '哔哩哔哩',
+      description: '技术分享与交流',
+      icon: 'ri:bilibili-line',
+      iconColor: '#FB7299',
+      enabled: true,
+      order: 4,
+      link: WEB_LINKS.BILIBILI
+    }
+  ],
+  // 快速链接
+  quickLinks: [
+    {
+      name: '登录',
+      enabled: true,
+      order: 1,
+      routeName: 'Login'
+    },
+    {
+      name: '注册',
+      enabled: true,
+      order: 2,
+      routeName: 'Register'
+    },
+    {
+      name: '忘记密码',
+      enabled: true,
+      order: 3,
+      routeName: 'ForgetPassword'
+    },
+    {
+      name: '个人中心',
+      enabled: true,
+      order: 4,
+      routeName: 'UserCenter'
+    }
+  ]
+}
+
+export default Object.freeze(fastEnterConfig)
+`
+
+    await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8')
+    console.log(`     ${icons.success} ${fmt.success('清理快速入口配置完成')}`)
+  } catch (err) {
+    console.log(`     ${icons.error} ${fmt.error('清理快速入口配置失败')}`)
+    console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+  }
+}
+
+// 更新菜单接口
+async function updateMenuApi() {
+  const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts')
+
+  try {
+    const content = await fs.readFile(apiPath, 'utf-8')
+    const updatedContent = content.replace(
+      "url: '/api/v3/system/menus'",
+      "url: '/api/v3/system/menus/simple'"
+    )
+
+    await fs.writeFile(apiPath, updatedContent, 'utf-8')
+    console.log(`     ${icons.success} ${fmt.success('更新菜单接口完成')}`)
+  } catch (err) {
+    console.log(`     ${icons.error} ${fmt.error('更新菜单接口失败')}`)
+    console.log(`     ${fmt.dim('错误详情: ' + err)}`)
+  }
+}
+
+// 用户确认函数
+async function getUserConfirmation(): Promise<boolean> {
+  const { createInterface } = await import('readline')
+
+  return new Promise((resolve) => {
+    const rl = createInterface({
+      input: process.stdin,
+      output: process.stdout
+    })
+
+    console.log(
+      `  ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}`
+    )
+    console.log()
+    process.stdout.write(`  ${icons.arrow} `)
+
+    rl.question('', (answer: string) => {
+      rl.close()
+      resolve(answer.toLowerCase().trim() === 'yes')
+    })
+  })
+}
+
+// 显示清理警告
+async function showCleanupWarning() {
+  createCard('安全警告', [
+    `${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`,
+    `${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}`
+  ])
+
+  const cleanupItems = [
+    {
+      icon: icons.image,
+      name: '图片资源',
+      desc: '演示用的封面图片、3D图片、运维图片等',
+      color: theme.orange
+    },
+    {
+      icon: icons.file,
+      name: '演示页面',
+      desc: 'widgets、template、article、examples、safeguard等页面',
+      color: theme.purple
+    },
+    {
+      icon: icons.code,
+      name: '路由模块文件',
+      desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)',
+      color: theme.primary
+    },
+    {
+      icon: icons.link,
+      name: '路由别名',
+      desc: '重写routesAlias.ts,移除演示路由别名',
+      color: theme.info
+    },
+    {
+      icon: icons.data,
+      name: 'Mock数据',
+      desc: '演示用的JSON数据、文章列表、评论数据等',
+      color: theme.success
+    },
+    {
+      icon: icons.globe,
+      name: '多语言文件',
+      desc: '清理中英文语言包中的演示菜单项',
+      color: theme.warning
+    },
+    { icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error },
+    { icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange },
+    {
+      icon: icons.bolt,
+      name: '快速入口',
+      desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目',
+      color: theme.purple
+    }
+  ]
+
+  console.log(`  ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`)
+  console.log()
+
+  cleanupItems.forEach((item, index) => {
+    console.log(`     ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`)
+    console.log(`        ${fmt.dim(item.desc)}`)
+  })
+
+  console.log()
+  console.log(`  ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`)
+  console.log()
+
+  const preservedModules = [
+    { name: 'Dashboard', desc: '工作台页面' },
+    { name: 'System', desc: '系统管理模块' },
+    { name: 'Result', desc: '结果页面' },
+    { name: 'Exception', desc: '异常页面' },
+    { name: 'Auth', desc: '登录注册功能' },
+    { name: 'Core Components', desc: '核心组件库' }
+  ]
+
+  preservedModules.forEach((module) => {
+    console.log(`     ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`)
+  })
+
+  console.log()
+  createDivider()
+  console.log()
+}
+
+// 显示统计信息
+async function showStats() {
+  const duration = Date.now() - stats.startTime
+  const seconds = (duration / 1000).toFixed(2)
+
+  console.log()
+  createCard('清理统计', [
+    `${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`,
+    `${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`,
+    ...(stats.failedPaths > 0
+      ? [
+          `${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径`
+        ]
+      : []),
+    `${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒`
+  ])
+}
+
+// 创建成功横幅
+function createSuccessBanner() {
+  console.log()
+  console.log(
+    fmt.gradient('  ╔══════════════════════════════════════════════════════════════════╗')
+  )
+  console.log(
+    fmt.gradient('  ║                                                                  ║')
+  )
+  console.log(
+    `  ║                  ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket}                  ║`
+  )
+  console.log(
+    `  ║                    ${fmt.dim('现在可以开始您的开发之旅了!')}                  ║`
+  )
+  console.log(
+    fmt.gradient('  ║                                                                  ║')
+  )
+  console.log(
+    fmt.gradient('  ╚══════════════════════════════════════════════════════════════════╝')
+  )
+  console.log()
+}
+
+// 主函数
+async function main() {
+  // 清屏并显示横幅
+  console.clear()
+  createModernBanner()
+
+  // 显示清理警告
+  await showCleanupWarning()
+
+  // 统计文件数量
+  console.log(`  ${fmt.info('正在统计文件数量...')}`)
+  stats.totalFiles = await countAllFiles()
+
+  console.log(`  ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`)
+  console.log(`  ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`)
+  console.log()
+
+  // 用户确认
+  const confirmed = await getUserConfirmation()
+
+  if (!confirmed) {
+    console.log(`  ${fmt.warning('操作已取消,清理中止')}`)
+    console.log()
+    return
+  }
+
+  console.log()
+  console.log(`  ${icons.check} ${fmt.success('确认成功,开始清理...')}`)
+  console.log()
+
+  // 开始清理过程
+  console.log(`  ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`)
+  console.log()
+  for (let i = 0; i < targets.length; i++) {
+    await remove(targets[i], i)
+  }
+  console.log()
+
+  console.log(`  ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`)
+  console.log()
+  await cleanRouteModules()
+  console.log()
+
+  console.log(`  ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`)
+  console.log()
+  await cleanRoutesAlias()
+  console.log()
+
+  console.log(`  ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`)
+  console.log()
+  await cleanChangeLog()
+  console.log()
+
+  console.log(`  ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`)
+  console.log()
+  await cleanLanguageFiles()
+  console.log()
+
+  console.log(`  ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`)
+  console.log()
+  await cleanFastEnterComponent()
+  console.log()
+
+  console.log(`  ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`)
+  console.log()
+  await updateMenuApi()
+
+  // 显示统计信息
+  await showStats()
+
+  // 显示成功横幅
+  createSuccessBanner()
+}
+
+main().catch((err) => {
+  console.log()
+  console.log(`  ${icons.error} ${fmt.error('清理脚本执行出错')}`)
+  console.log(`  ${fmt.dim('错误详情: ' + err)}`)
+  console.log()
+  process.exit(1)
+})

+ 185 - 0
saiadmin-artd/scripts/test-water-driving.mjs

@@ -0,0 +1,185 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+// Isolate the provider contract without accessing a browser, a key or live orders.
+const calls = []
+const policies = []
+let response
+let now = 100000
+const requestedAt = []
+Date.now = () => now
+globalThis.window = {
+  setTimeout: (callback, ms) => {
+    if (ms < 10000) {
+      now += ms
+      return setTimeout(callback, 0)
+    }
+    return setTimeout(callback, ms)
+  },
+  AMap: {
+    plugin: (_plugins, done) => done(),
+    Driving: class {
+      constructor(options) {
+        policies.push(options.policy)
+      }
+      search(start, end, options, done) {
+        calls.push([start, ...options.waypoints, end])
+        requestedAt.push(now)
+        response(done)
+      }
+    }
+  }
+}
+const source = await readFile(new URL('../src/utils/waterAmap.ts', import.meta.url), 'utf8')
+const { outputText } = ts.transpileModule(source, {
+  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext }
+})
+const { planWaterDriving, collectWaterRoadMatrix } = await import(
+  `data:text/javascript;base64,${Buffer.from(outputText).toString('base64')}`
+)
+const success = (done) =>
+  done('complete', {
+    routes: [
+      {
+        distance: 1000,
+        time: 60,
+        steps: [
+          {
+            path: [
+              [113, 22],
+              [114, 23]
+            ]
+          }
+        ]
+      }
+    ]
+  })
+response = success
+const stops = Array.from({ length: 37 }, (_, i) => [113 + i / 1000, 22])
+const result = await planWaterDriving(stops, () => true)
+assert.equal(calls.length, 3)
+assert.ok(calls.every((chunk) => chunk.length <= 18))
+assert.deepEqual([calls[0][0], ...calls.flatMap((chunk) => chunk.slice(1))], stops)
+assert.equal(result.distance, 3000)
+assert.equal(result.time, 180)
+
+calls.length = 0
+await planWaterDriving([stops[0], stops[0], stops[1], stops[1]], () => true)
+assert.deepEqual(calls, [[stops[0], stops[1]]])
+calls.length = 0
+assert.equal((await planWaterDriving([stops[0], stops[0]], () => true)).distance, 0)
+assert.equal(calls.length, 0)
+
+let active = true
+response = (done) => {
+  active = false
+  success(done)
+}
+assert.equal(await planWaterDriving(stops, () => active), undefined)
+assert.equal(calls.length, 1)
+calls.length = 0
+assert.equal(await planWaterDriving(stops, () => false), undefined)
+assert.equal(calls.length, 0)
+
+response = (done) => (calls.length === 1 ? success(done) : done('error', 'OVER_QUOTA'))
+await assert.rejects(
+  planWaterDriving(stops, () => true),
+  /OVER_QUOTA/
+)
+assert.equal(calls.length, 2)
+response = (done) => done('complete', { routes: [{ distance: 1, time: 1, steps: [] }] })
+await assert.rejects(
+  planWaterDriving(stops.slice(0, 2), () => true),
+  /未返回可用/
+)
+console.log(
+  'PASS: ordered chunking, totals, duplicate stops, same location, stale requests, partial failure, invalid paths'
+)
+
+calls.length = 0
+policies.length = 0
+response = success
+let progress
+const matrix = await collectWaterRoadMatrix(
+  stops.slice(0, 3),
+  'shortest',
+  () => true,
+  (done, total) => {
+    progress = [done, total]
+  }
+)
+assert.deepEqual(progress, [6, 6])
+assert.equal(calls.length, 4)
+assert.ok(policies.every((value) => value === 2))
+assert.deepEqual(calls[2], [stops[1], stops[2]])
+assert.deepEqual(calls[3], [stops[2], stops[1]])
+assert.equal(matrix[1][0], null)
+assert.deepEqual(matrix[1][1], { distance: 0, time: 0 })
+calls.length = 0
+await collectWaterRoadMatrix(
+  stops.slice(0, 3),
+  'shortest',
+  () => true,
+  () => {}
+)
+assert.equal(calls.length, 0, 'repeat directed matrix uses fresh shared cost cache')
+now += 60001
+await collectWaterRoadMatrix(
+  stops.slice(0, 3),
+  'shortest',
+  () => true,
+  () => {}
+)
+assert.equal(calls.length, 4, 'expired road costs are refreshed')
+response = (done) => done('error', 'NO_DATA')
+const unreachable = await collectWaterRoadMatrix(
+  stops.slice(0, 2),
+  'fastest',
+  () => true,
+  () => {}
+)
+assert.equal(unreachable[0][1], null)
+response = (done) => done('error', 'OVER_QUOTA')
+await assert.rejects(
+  collectWaterRoadMatrix(
+    stops.slice(0, 2),
+    'deadline',
+    () => true,
+    () => {}
+  ),
+  /OVER_QUOTA/
+)
+console.log(
+  'PASS: directed road matrix, shortest policy, progress, same-coordinate cells, unreachable roads and provider errors'
+)
+requestedAt.length = 0
+let attempts = 0
+response = (done) =>
+  ++attempts < 3 ? done('error', 'CUQPS_HAS_EXCEEDED_THE_LIMIT') : success(done)
+await planWaterDriving(stops.slice(0, 2), () => true)
+assert.equal(attempts, 3)
+assert.ok(requestedAt[1] - requestedAt[0] >= 1500)
+assert.ok(requestedAt[2] - requestedAt[1] >= 3000)
+attempts = 0
+response = (done) => {
+  attempts++
+  done('error', 'CUQPS_HAS_EXCEEDED_THE_LIMIT')
+}
+await assert.rejects(
+  planWaterDriving(stops.slice(0, 2), () => true),
+  /请求过于频繁/
+)
+assert.equal(attempts, 3, 'QPS retry count is bounded')
+requestedAt.length = 0
+response = success
+await Promise.all([
+  planWaterDriving(stops.slice(0, 2), () => true),
+  planWaterDriving(stops.slice(1, 3), () => true)
+])
+assert.ok(requestedAt[1] - requestedAt[0] >= 1100, 'concurrent planners share pacing')
+let current = true
+const cancelled = planWaterDriving(stops.slice(0, 2), () => current)
+current = false
+assert.equal(await cancelled, undefined, 'queued stale request never reaches provider')
+console.log('PASS: shared pacing, TTL cache, QPS backoff, bounded retries and queued cancellation')

+ 134 - 0
saiadmin-artd/scripts/test-water-package-delete.mjs

@@ -0,0 +1,134 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc'
+import * as vue from 'vue'
+import ts from 'typescript'
+
+// Run the real component setup with controlled confirmation and API dependencies.
+const filename = 'WaterWorkspace.vue'
+const source = await readFile(
+  new URL('../src/views/water/modules/WaterWorkspace.vue', import.meta.url),
+  'utf8'
+)
+const { descriptor, errors } = parse(source, { filename })
+assert.deepEqual(errors, [])
+const script = compileScript(descriptor, { id: 'package-delete-test' })
+const template = compileTemplate({
+  source: descriptor.template.content,
+  filename,
+  id: 'package-delete-test',
+  compilerOptions: { bindingMetadata: script.bindings }
+})
+assert.deepEqual(template.errors, [])
+
+const saves = []
+const reads = []
+const messages = []
+const confirmations = []
+let confirm = async () => {}
+let save = async () => ({})
+let listing = async () => ({ data: [{ id: 2, name: '保留套餐' }], total: 1 })
+const user = vue.reactive({ info: { id: 1, buttons: [] } })
+const dependencies = {
+  vue: { ...vue, onMounted: () => {} },
+  'vue-router': { useRoute: () => ({ query: {} }), useRouter: () => ({ push: () => {} }) },
+  'element-plus': {
+    ElMessage: { success: (message) => messages.push(message), warning: () => {} },
+    ElMessageBox: {
+      confirm: (...args) => {
+        confirmations.push(args)
+        return confirm()
+      }
+    }
+  },
+  xlsx: {},
+  '@/store/modules/user': { useUserStore: () => user },
+  '@/api/water': {
+    saveWater: async (...args) => {
+      saves.push(args)
+      return save()
+    },
+    getWater: async (...args) => {
+      reads.push(args)
+      return listing()
+    },
+    waterOptions: async () => []
+  }
+}
+function evaluate(code) {
+  const { outputText } = ts.transpileModule(code, {
+    compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
+  })
+  const exports = {}
+  const require = (name) => {
+    if (name in dependencies) return dependencies[name]
+    if (name.endsWith('.vue') || name.endsWith('.scss')) return {}
+    throw new Error(`Unexpected dependency: ${name}`)
+  }
+  new Function('require', 'exports', outputText)(require, exports)
+  return exports
+}
+dependencies['./catalog'] = evaluate(
+  await readFile(new URL('../src/views/water/modules/catalog.ts', import.meta.url), 'utf8')
+)
+const component = evaluate(script.content).default
+const state = component.setup({ module: 'packages' }, { expose: () => {} })
+const row = { id: '1', name: '待删除套餐' }
+
+confirm = async () => {
+  throw new Error('cancel')
+}
+await state.removePackage(row)
+assert.equal(saves.length, 0)
+assert.equal(reads.length, 0)
+assert.equal(state.removingPackageId.value, undefined)
+assert.match(confirmations[0][0], /待删除套餐.*ID:1/)
+assert.equal(confirmations[0][2].cancelButtonText, '取消')
+
+let resolveConfirmation
+confirm = () => new Promise((resolve) => (resolveConfirmation = resolve))
+state.preview.value = row
+const deleting = state.removePackage(row)
+assert.equal(saves.length, 0, 'no deletion before confirmation')
+const confirmationCount = confirmations.length
+await state.removePackage(row)
+assert.equal(confirmations.length, confirmationCount, 'repeated click does not open another dialog')
+resolveConfirmation()
+await deleting
+assert.deepEqual(saves, [['packages', { id: 1, action: 'delete' }]])
+assert.equal(reads.length, 1)
+assert.equal(state.preview.value, undefined)
+assert.equal(messages.at(-1), '删除成功')
+assert.equal(state.removingPackageId.value, undefined)
+
+confirm = async () => {}
+state.page.value = 2
+listing = async () =>
+  reads.at(-1)[1].page === 2 ? { data: [], total: 20 } : { data: [{ id: 2 }], total: 20 }
+await state.removePackage({ id: 21, name: '第二页最后一条' })
+assert.deepEqual(
+  reads.slice(-2).map(([, query]) => query.page),
+  [2, 1]
+)
+assert.equal(state.page.value, 1)
+assert.equal(state.rows.value[0].id, 2)
+
+const successCount = messages.length
+const readCount = reads.length
+save = async () => {
+  throw new Error('API rejected')
+}
+await state.removePackage(row)
+assert.equal(reads.length, readCount, 'failed deletion does not refresh or remove a row')
+assert.equal(messages.length, successCount, 'failed deletion shows no success message')
+assert.equal(state.removingPackageId.value, undefined)
+
+user.info.id = 2
+const saveCount = saves.length
+const dialogs = confirmations.length
+await state.removePackage(row)
+assert.equal(confirmations.length, dialogs, 'read-only operator cannot initiate deletion')
+assert.equal(saves.length, saveCount)
+console.log(
+  'PASS: component compilation, confirmation, cancellation, duplicate click, ID deletion, pagination, API failure and write permission.'
+)

+ 47 - 0
saiadmin-artd/scripts/test-water-region.mjs

@@ -0,0 +1,47 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import ts from 'typescript'
+
+let providerResponse
+let requestedPoint
+globalThis.window = {
+  setTimeout,
+  AMap: {
+    plugin: (_plugins, done) => done(),
+    Geocoder: class {
+      getAddress(point, done) {
+        requestedPoint = point
+        providerResponse(done)
+      }
+    }
+  }
+}
+
+const source = await readFile(new URL('../src/utils/waterAmap.ts', import.meta.url), 'utf8')
+const { outputText } = ts.transpileModule(source, {
+  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext }
+})
+const { findMapRegion } = await import(
+  `data:text/javascript;base64,${Buffer.from(outputText).toString('base64')}`
+)
+
+const fixtures = [
+  [{ province: '广东省', city: '深圳市', district: '南山区' }, '广东省深圳市南山区'],
+  [{ province: '北京市', city: [], district: '朝阳区' }, '北京市朝阳区'],
+  [{ province: '上海市', city: '上海市', district: '浦东新区' }, '上海市浦东新区'],
+  [{ province: '广东省', city: '东莞市', district: '' }, '广东省东莞市']
+]
+for (const [component, expected] of fixtures) {
+  providerResponse = (done) => done('complete', { regeocode: { addressComponent: component } })
+  assert.equal(await findMapRegion([113.93, 22.53]), expected)
+  assert.deepEqual(requestedPoint, [113.93, 22.53])
+}
+providerResponse = (done) => done('complete', { regeocode: { addressComponent: {} } })
+await assert.rejects(findMapRegion([0, 0]), /未能识别/)
+providerResponse = (done) => done('no_data', { info: 'NO_DATA' })
+await assert.rejects(findMapRegion([0, 0]), /未能识别/)
+providerResponse = (done) => done('error', { info: 'CUQPS_HAS_EXCEEDED_THE_LIMIT' })
+await assert.rejects(findMapRegion([113.93, 22.53]), /CUQPS_HAS_EXCEEDED_THE_LIMIT/)
+console.log(
+  'PASS: region provider checks (districts, municipalities, missing district, empty result and quota errors).'
+)

+ 34 - 0
saiadmin-artd/src/App.vue

@@ -0,0 +1,34 @@
+<template>
+  <ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
+    <RouterView></RouterView>
+  </ElConfigProvider>
+</template>
+
+<script setup lang="ts">
+  import { useUserStore } from './store/modules/user'
+  import zh from 'element-plus/es/locale/lang/zh-cn'
+  import en from 'element-plus/es/locale/lang/en'
+  import { systemUpgrade } from './utils/sys'
+  import { toggleTransition } from './utils/ui/animation'
+  import { checkStorageCompatibility } from './utils/storage'
+  import { initializeTheme } from './hooks/core/useTheme'
+
+  const userStore = useUserStore()
+  const { language } = storeToRefs(userStore)
+
+  const locales = {
+    zh: zh,
+    en: en
+  }
+
+  onBeforeMount(() => {
+    toggleTransition(true)
+    initializeTheme()
+  })
+
+  onMounted(() => {
+    checkStorageCompatibility()
+    toggleTransition(false)
+    systemUpgrade()
+  })
+</script>

+ 213 - 0
saiadmin-artd/src/api/agent/agent.ts

@@ -0,0 +1,213 @@
+import request from '@/utils/http'
+
+export interface AgentItem {
+  id: number
+  name: string
+  username: string
+  phone: string
+  avatar: string
+  agent_type: string
+  agent_type_code: string
+  province: string
+  city: string
+  district: string
+  dept_name: string
+  device_count: number
+  vehicle_count: number
+  charging_count: number
+  subordinate_count: number
+  income: string
+  frozen: string
+  withdrawable: string
+  profit_rate: number
+  status: number
+  status_name: string
+  created_at: string
+}
+
+export interface AgentListParams {
+  page: number
+  limit: number
+  agent_type?: string
+  sort?: string
+  name?: string
+  phone?: string
+  status?: string
+}
+
+export interface AgentCandidateDevice {
+  id: number
+  name: string
+  goods_type: number
+  type_name: string
+  device_sn: string
+  qr_code: string
+  model: string
+  online: boolean
+  created_at: string
+}
+
+export interface AgentDeviceActionResult {
+  message: string
+  success_sn: string[]
+  not_find_sn: string[]
+  not_managed_sn: string[]
+  already_target_sn: string[]
+  distributed_sn: string[]
+  all_action_sn: string[]
+}
+
+export interface AgentDeviceRecord {
+  id: number
+  device_id: number
+  device_sn: string
+  goods_type: number
+  type_name: string
+  last_agent_id: number
+  last_agent_name: string
+  now_agent_id: number
+  now_agent_name: string
+  action_type: number
+  action_name: string
+  reason: string
+  action_time: string
+}
+
+export type AgentProductCode =
+  | 'vehicle'
+  | 'ktv'
+  | 'bluetooth-cable'
+  | 'power-bank'
+  | 'password-cable'
+  | 'password-bluetooth-cable'
+
+export type AgentPermissionCode =
+  | 'self_operated'
+  | 'view_order'
+  | 'self_merchant'
+  | 'finish_order'
+  | 'refund_order'
+  | 'dd'
+
+export interface AgentProfile {
+  id: number
+  opening_type: string
+  opening_type_name: string
+  business_mode: 'agent' | 'employee'
+  name: string
+  phone: string
+  product_codes: AgentProductCode[]
+  profit_rates: Partial<Record<AgentProductCode, number>>
+  permissions: AgentPermissionCode[]
+  province: string
+  city: string
+  district: string
+  company_address: string
+  status: number
+}
+
+export interface AgentContract {
+  id: number
+  agent_name: string
+  phone: string
+  contract_urls: string[]
+  identity_urls: string[]
+  extra_urls: string[]
+}
+
+export type AgentContractUpdatePayload = Pick<
+  AgentContract,
+  'id' | 'contract_urls' | 'identity_urls' | 'extra_urls'
+>
+
+export type AgentUpdatePayload = Pick<
+  AgentProfile,
+  | 'id'
+  | 'business_mode'
+  | 'name'
+  | 'phone'
+  | 'product_codes'
+  | 'profit_rates'
+  | 'permissions'
+  | 'province'
+  | 'city'
+  | 'district'
+  | 'company_address'
+>
+
+export default {
+  list(params: AgentListParams) {
+    return request.get<Api.Common.ApiPage<AgentItem>>({ url: '/core/agent/index', params })
+  },
+
+  read(id: number) {
+    return request.get<AgentProfile>({ url: '/core/agent/read', params: { id } })
+  },
+
+  update(data: AgentUpdatePayload) {
+    return request.put<void>({ url: '/core/agent/update', data })
+  },
+
+  destroy(id: number) {
+    return request.post<void>({ url: '/core/agent/destroy', data: { id } })
+  },
+
+  contract(id: number) {
+    return request.get<AgentContract>({ url: '/core/agent/contract', params: { id } })
+  },
+
+  updateContract(data: AgentContractUpdatePayload) {
+    return request.put<AgentContract>({ url: '/core/agent/contract', data })
+  },
+
+  export(params: Omit<AgentListParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/agent/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  },
+
+  candidates(params: {
+    page: number
+    limit: number
+    category: string
+    goods_type?: string
+    device_sn?: string
+    qr_code?: string
+  }) {
+    return request.get<Api.Common.ApiPage<AgentCandidateDevice>>({
+      url: '/core/agent/device/candidates',
+      params
+    })
+  },
+
+  assign(agentId: number, deviceSns: string[]) {
+    return request.post<AgentDeviceActionResult>({
+      url: '/core/agent/device/assign',
+      data: { agent_id: agentId, device_sns: deviceSns }
+    })
+  },
+
+  recover(agentId: number, deviceSns: string[]) {
+    return request.post<AgentDeviceActionResult>({
+      url: '/core/agent/device/recover',
+      data: { agent_id: agentId, device_sns: deviceSns }
+    })
+  },
+
+  records(params: {
+    page: number
+    limit: number
+    agent_id: number
+    action_type?: string
+    goods_type?: string
+    device_sn?: string
+  }) {
+    return request.get<Api.Common.ApiPage<AgentDeviceRecord>>({
+      url: '/core/agent/device/records',
+      params
+    })
+  }
+}

+ 209 - 0
saiadmin-artd/src/api/auth.ts

@@ -0,0 +1,209 @@
+import request from '@/utils/http'
+import { AppRouteRecord } from '@/types/router'
+
+/**
+ * 获取验证码
+ * @returns 响应
+ */
+export function fetchCaptcha() {
+  return request.get<Api.Auth.CaptchaResponse>({
+    url: '/core/captcha',
+    // 验证码不能复用浏览器或代理缓存
+    params: { _: Date.now() }
+  })
+}
+
+/**
+ * 登录
+ * @param params 登录参数
+ * @returns 登录响应
+ */
+export function fetchLogin(params: Api.Auth.LoginParams) {
+  return request.post<Api.Auth.LoginResponse>({
+    url: '/core/login',
+    params
+  })
+}
+
+/**
+ * 匿名注册
+ * @param params 注册参数(用户名 + 密码 + 确认密码)
+ * @returns 响应
+ */
+export function fetchRegister(params: {
+  username: string
+  password: string
+  confirmPassword: string
+}) {
+  return request.post<any>({
+    url: '/core/register',
+    params
+  })
+}
+
+/**
+ * 获取用户信息
+ * @returns 用户信息
+ */
+export function fetchGetUserInfo() {
+  return request.get<Api.Auth.UserInfo>({
+    url: '/core/system/user'
+  })
+}
+
+/**
+ * 修改资料
+ * @param params 修改资料参数
+ * @returns 响应
+ */
+export function updateUserInfo(params: Record<string, any>) {
+  return request.post<any>({
+    url: '/core/user/updateInfo',
+    params
+  })
+}
+
+/**
+ * 修改密码
+ * @param params 修改密码参数
+ * @returns 响应
+ */
+export function modifyPassword(params: Record<string, any>) {
+  return request.post<any>({
+    url: '/core/user/modifyPassword',
+    params
+  })
+}
+
+/**
+ * 获取登录日志
+ * @returns 登录日志数组
+ */
+export function fetchGetLogin(params: Record<string, any>) {
+  return request.get<Api.Common.ApiPage>({
+    url: '/core/system/getLoginLogList',
+    params
+  })
+}
+
+/**
+ * 获取操作日志
+ * @returns 操作日志数组
+ */
+export function fetchGetOperate(params: Record<string, any>) {
+  return request.get<Api.Common.ApiPage>({
+    url: '/core/system/getOperationLogList',
+    params
+  })
+}
+
+/**
+ * 清理缓存
+ * @returns
+ */
+export function fetchClearCache() {
+  return request.get<any>({
+    url: '/core/system/clearAllCache'
+  })
+}
+
+/**
+ * 获取字典数据
+ * @returns 字典数组
+ */
+export function fetchGetDictList() {
+  return request.get<Api.Auth.DictData>({
+    url: '/core/system/dictAll'
+  })
+}
+
+/**
+ * 获取菜单列表
+ * @returns 菜单数组
+ */
+export function fetchGetMenuList() {
+  return request.get<AppRouteRecord[]>({
+    url: '/core/system/menu'
+  })
+}
+
+/**
+ * 上传图片
+ * @param params
+ * @returns
+ */
+export function uploadImage(params: any) {
+  return request.post<any>({
+    url: '/core/system/uploadImage',
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    },
+    params
+  })
+}
+
+/**
+ * 上传文件
+ * @param params
+ * @returns
+ */
+export function uploadFile(params: any) {
+  return request.post<any>({
+    url: '/core/system/uploadFile',
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    },
+    params
+  })
+}
+
+/**
+ * 切片上传
+ * @param params
+ * @returns
+ */
+export function chunkUpload(params: any) {
+  return request.post<any>({
+    url: '/core/system/chunkUpload',
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    },
+    params
+  })
+}
+
+/**
+ * 资源分类
+ * @param params
+ * @returns
+ */
+export function getResourceCategory(params: any) {
+  return request.get<Api.Common.ApiData[]>({
+    url: '/core/system/getResourceCategory',
+    params
+  })
+}
+
+/**
+ * 图片资源列表
+ * @param params
+ * @returns
+ */
+export function getResourceList(params: any) {
+  return request.get<Api.Common.ApiPage>({
+    url: '/core/system/getResourceList',
+    params
+  })
+}
+
+/**
+ * 用户列表
+ * @param params
+ * @returns
+ */
+export function getUserList(params: any) {
+  return request.get<Api.Common.ApiPage>({
+    url: '/core/system/getUserList',
+    params
+  })
+}

+ 46 - 0
saiadmin-artd/src/api/common.ts

@@ -0,0 +1,46 @@
+import request from '@/utils/http'
+
+/**
+ * 通用API
+ */
+export default {
+  /**
+   * GET请求
+   * @param url 请求URL
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  get(url: string, params?: Record<string, any>) {
+    return request.get<any>({
+      url: url,
+      params
+    })
+  },
+
+  /**
+   * POST请求
+   * @param url 请求URL
+   * @param data 请求参数
+   * @returns 数据列表
+   */
+  post(url: string, data: Record<string, any>) {
+    return request.post<any>({
+      url: url,
+      data
+    })
+  },
+
+  /**
+   * 下载文件
+   * @param url
+   * @returns
+   */
+  download(url: string) {
+    return request.request<any>({
+      url: url,
+      method: 'post',
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 31 - 0
saiadmin-artd/src/api/dashboard.ts

@@ -0,0 +1,31 @@
+import request from '@/utils/http'
+
+/**
+ * 基础数据统计
+ * @returns 响应
+ */
+export function fetchStatistics() {
+  return request.get<any>({
+    url: '/core/system/statistics'
+  })
+}
+
+/**
+ * 登录统计图表数据
+ * @returns 响应
+ */
+export function fetchLoginChart() {
+  return request.get<any>({
+    url: '/core/system/loginChart'
+  })
+}
+
+/**
+ * 登录统计图表数据
+ * @returns 响应
+ */
+export function fetchLoginBarChart() {
+  return request.get<any>({
+    url: '/core/system/loginBarChart'
+  })
+}

+ 154 - 0
saiadmin-artd/src/api/device/device.ts

@@ -0,0 +1,154 @@
+import request from '@/utils/http'
+
+export interface DeviceItem {
+  id: number
+  category: 'vehicle' | 'power-bank'
+  name: string
+  goods_type: number
+  type_name: string
+  qr_code: string
+  qr_content: string
+  device_sn: string
+  borrowable: string
+  imei: string
+  version: string
+  signal: '在线' | '离线'
+  signal_value: string
+  connection_count: number
+  distributed: boolean
+  distributed_at: string
+  merchant_id: number
+  merchant: string
+  merchant_phone: string
+  wechat_orders: number
+  alipay_orders: number
+  amount: string
+  heartbeat: '正常' | '掉线' | '维护' | '异常'
+  status: number
+  status_name: string
+  manage_agent_id: number
+  is_locked: boolean
+  is_active: boolean
+  active_time: string
+  created_at: string
+  owner?: DevicePerson | null
+  manager?: DevicePerson | null
+}
+
+export interface DeviceListParams {
+  page: number
+  limit: number
+  category: 'all' | 'vehicle' | 'power-bank'
+  device_state: '' | 'online' | 'offline' | 'bound' | 'unbound'
+  merchant?: string
+  qr_code?: string
+  device_sn?: string
+  goods_type?: string
+  signal?: '' | 'online' | 'offline'
+}
+
+export interface DeviceSummary {
+  all: number
+  vehicle: number
+  'power-bank': number
+  online: number
+  offline: number
+  bound: number
+  unbound: number
+}
+
+export interface DeviceListResult extends Api.Common.ApiPage<DeviceItem> {
+  summary: DeviceSummary
+}
+
+export interface DevicePerson {
+  id: number
+  name: string
+  username: string
+  phone: string
+}
+
+export interface DeviceDetail {
+  device: DeviceItem & {
+    qr_content: string
+    can_switch_power: boolean
+    can_unbind: boolean
+  }
+  owner: DevicePerson | null
+  manager: DevicePerson | null
+  merchant: {
+    id: number
+    name: string
+    phone: string
+    address: string
+    bind_time: string
+    operator: DevicePerson | null
+  } | null
+}
+
+export default {
+  list(params: DeviceListParams) {
+    return request.get<DeviceListResult>({
+      url: '/core/device/index',
+      params
+    })
+  },
+
+  subordinateList(params: DeviceListParams) {
+    return request.get<DeviceListResult>({
+      url: '/core/device/subordinate/index',
+      params
+    })
+  },
+
+  export(params: Omit<DeviceListParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/device/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  },
+
+  read(id: number) {
+    return request.get<DeviceDetail>({
+      url: '/core/device/read',
+      params: { id }
+    })
+  },
+
+  subordinateRead(id: number) {
+    return request.get<DeviceDetail>({
+      url: '/core/device/subordinate/read',
+      params: { id }
+    })
+  },
+
+  power(id: number, action: 'on' | 'off') {
+    return request.post({
+      url: '/core/device/power',
+      data: { id, action }
+    })
+  },
+
+  subordinatePower(id: number, action: 'on' | 'off') {
+    return request.post({
+      url: '/core/device/subordinate/power',
+      data: { id, action }
+    })
+  },
+
+  subordinateUnbind(id: number) {
+    return request.post({
+      url: '/core/device/subordinate/unbind',
+      data: { id }
+    })
+  },
+
+  unbind(id: number) {
+    return request.post({
+      url: '/core/device/unbind',
+      data: { id }
+    })
+  }
+}

+ 49 - 0
saiadmin-artd/src/api/device/distribution-record.ts

@@ -0,0 +1,49 @@
+import request from '@/utils/http'
+
+export interface DistributionRecordItem {
+  id: number
+  device_id: number
+  device_sn: string
+  qr_code: string
+  goods_name: string
+  goods_type: number
+  type_name: string
+  action_type: 1 | 2
+  action_name: '铺货' | '解绑' | '未知'
+  action_time: string
+  merchant_id: number
+  merchant: string
+  merchant_phone: string
+  operator_id: number
+  operator_name: string
+  operator_username: string
+  operator_phone: string
+}
+
+export interface DistributionRecordParams {
+  page: number
+  limit: number
+  action_type: '' | '1' | '2'
+  merchant?: string
+  qr_code?: string
+  device_sn?: string
+  goods_type?: string
+}
+
+export default {
+  list(params: DistributionRecordParams) {
+    return request.get<Api.Common.ApiPage<DistributionRecordItem>>({
+      url: '/core/device/distribution-record/index',
+      params
+    })
+  },
+
+  export(params: Omit<DistributionRecordParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/device/distribution-record/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 75 - 0
saiadmin-artd/src/api/fundManagement/depositRefund.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/http'
+
+export type DepositRefundStatus = '' | '0' | '1' | '2'
+export type DepositRefundSource = '' | '1' | '2'
+export type DepositRefundType = '' | '0' | '1'
+
+export interface DepositRefundParams {
+  page: number
+  limit: number
+  status: DepositRefundStatus
+  source: DepositRefundSource
+  refund_type: DepositRefundType
+  order_sn?: string
+  user_id?: string
+  start_date?: string
+  end_date?: string
+}
+
+export interface DepositRefundRecord {
+  id: number
+  order_id: number
+  order_sn: string
+  user_id: number
+  nickname: string
+  phone: string
+  avatar: string
+  manage_agent_id: number
+  manager_name: string
+  source: number
+  source_name: string
+  device_type: number
+  device_type_name: string
+  deposit_amount: number
+  consumption_amount: number
+  refund_amount: number
+  apply_time: string
+  scheduled_refund_time: string
+  refund_time: string
+  refund_type: number
+  refund_type_name: string
+  status: number
+  status_name: string
+  remark: string
+}
+
+export interface DepositRefundSummary {
+  record_count: number
+  total_refund_amount: number
+  refunded_amount: number
+  pending_amount: number
+}
+
+export interface DepositRefundPage {
+  data: DepositRefundRecord[]
+  total: number
+  summary: DepositRefundSummary
+}
+
+export default {
+  list(params: DepositRefundParams) {
+    return request.get<DepositRefundPage>({
+      url: '/core/fund-management/refund-management/index',
+      params
+    })
+  },
+
+  export(params: Omit<DepositRefundParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/fund-management/refund-management/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 71 - 0
saiadmin-artd/src/api/fundManagement/memberActivationRecord.ts

@@ -0,0 +1,71 @@
+import request from '@/utils/http'
+
+export type MemberActivationSource = '' | '1' | '2' | '3'
+export type MemberCardType = '' | '0' | '1' | '2' | '3'
+
+export interface MemberActivationParams {
+  page: number
+  limit: number
+  source: MemberActivationSource
+  card_type: MemberCardType
+  device_type: string
+  user_id?: string
+  nickname?: string
+  start_date?: string
+  end_date?: string
+}
+
+export interface MemberActivationRecord {
+  id: number
+  merchant_order_sn: string
+  transaction_sn: string
+  user_id: number
+  nickname: string
+  phone: string
+  avatar: string
+  source: number
+  source_name: string
+  device_type: number
+  device_type_name: string
+  merchant_id: number
+  merchant_name: string
+  pay_amount: number
+  card_type: number
+  card_type_name: string
+  activation_time: string
+  expire_time: string
+  status: number
+  status_name: string
+  remark: string
+}
+
+export interface MemberActivationSummary {
+  record_count: number
+  total_paid_amount: number
+  active_count: number
+  expired_count: number
+}
+
+export interface MemberActivationPage {
+  data: MemberActivationRecord[]
+  total: number
+  summary: MemberActivationSummary
+}
+
+export default {
+  list(params: MemberActivationParams) {
+    return request.get<MemberActivationPage>({
+      url: '/core/fund-management/member-activation-records/index',
+      params
+    })
+  },
+
+  export(params: Omit<MemberActivationParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/fund-management/member-activation-records/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 71 - 0
saiadmin-artd/src/api/fundManagement/rechargeRecord.ts

@@ -0,0 +1,71 @@
+import request from '@/utils/http'
+
+export type RechargeBalanceType = '' | 'wallet' | 'card' | 'card_gift'
+export type RechargeDirection = '' | '1' | '-1'
+
+export interface RechargeRecordParams {
+  page: number
+  limit: number
+  balance_type: RechargeBalanceType
+  direction: RechargeDirection
+  user_id?: string
+  nickname?: string
+  start_date?: string
+  end_date?: string
+}
+
+export interface RechargeRecord {
+  id: number
+  user_id: number
+  nickname: string
+  phone: string
+  avatar: string
+  user_source: number
+  user_source_name: string
+  balance_type: Exclude<RechargeBalanceType, ''>
+  balance_type_name: string
+  direction: 1 | -1
+  direction_name: '增加' | '扣减'
+  amount: number
+  change_amount: number
+  before_balance: number
+  after_balance: number
+  source: string
+  source_name: string
+  remark: string
+  operator_id: number
+  operator_name: string
+  created_at: string
+  status_name: '已完成'
+}
+
+export interface RechargeRecordSummary {
+  record_count: number
+  increase_amount: number
+  decrease_amount: number
+  net_amount: number
+}
+
+export interface RechargeRecordPage {
+  data: RechargeRecord[]
+  total: number
+  summary: RechargeRecordSummary
+}
+
+export default {
+  list(params: RechargeRecordParams) {
+    return request.get<RechargeRecordPage>({
+      url: '/core/fund-management/recharge-records/index',
+      params
+    })
+  },
+
+  export(params: Omit<RechargeRecordParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/fund-management/recharge-records/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 265 - 0
saiadmin-artd/src/api/merchant/merchant.ts

@@ -0,0 +1,265 @@
+import request from '@/utils/http'
+
+export interface MerchantItem {
+  id: number
+  agent_id: number
+  parent_merchant_id?: number | null
+  profit_user_id?: number | null
+  name: string
+  store_phone: string
+  storefront_image: string
+  industry: string
+  region: string
+  address: string
+  description: string
+  products: string[]
+  device_type: string
+  profit_method: string
+  profit_rate: number
+  contact: string
+  phone: string
+  permissions: string[]
+  distributed: number
+  status: number
+  agent_type: string
+  total_amount: string
+  total_income: string
+  withdrawable_amount: string
+  order_count: number
+  device_count: number
+  yesterday_usage_rate: number
+  three_day_usage_rate: number
+  seven_day_usage_rate: number
+  yesterday_average_order: string
+  three_day_average_order: string
+  seven_day_average_order: string
+  agent_name: string
+  agent_phone: string
+  profit_user_name: string
+  profit_user_phone: string
+  created_at: string
+}
+
+export interface MerchantForm {
+  id?: number
+  agent_id?: number
+  parent_merchant_id?: number | null
+  name: string
+  store_phone: string
+  storefront_image: string
+  industry: string
+  region: string
+  address: string
+  description: string
+  products: string[]
+  profit_method: string
+  profit_rate?: number
+  contact: string
+  phone: string
+  password?: string
+  permissions: string[]
+  distributed?: number
+  status?: number
+  agent_type?: string
+}
+
+export interface MerchantDeviceItem {
+  id: number
+  name: string
+  sn: string
+  device_code: string
+  model: string
+  type: number
+  type_name: string
+  status: number
+  status_name: string
+  online_status: number
+  bind_time: string
+}
+
+export interface MerchantDeviceBindResult {
+  message: string
+  bind_success_sn: string[]
+  has_binded_sn: string[]
+  binded_other_store_sn: string[]
+  not_find_sn: string[]
+  not_owned_sn: string[]
+  all_action_sn: string[]
+}
+
+export interface MerchantWithdrawalSetting {
+  merchant_id: number
+  update_mode: 0 | 1
+  update_day: number
+}
+
+export interface MerchantBusinessTime {
+  merchant_id: number
+  business_time_des: string
+}
+
+export interface MerchantStatisticsDaily {
+  date: string
+  amount: string
+  income: string
+  orders: number
+  usage_rate: number
+  device_count: number
+  wechat_amount: string
+  alipay_amount: string
+  balance_amount: string
+  wechat_orders: number
+  alipay_orders: number
+  balance_orders: number
+}
+
+export interface MerchantStatisticsData {
+  merchant: { id: number; name: string }
+  range: { start_date: string; end_date: string; day_count: number }
+  summary: {
+    total_amount: string
+    total_income: string
+    total_orders: number
+    average_usage_rate: number
+  }
+  payment: {
+    wechat: { amount: string; orders: number }
+    alipay: { amount: string; orders: number }
+    balance: { amount: string; orders: number }
+  }
+  daily: MerchantStatisticsDaily[]
+}
+
+export interface MerchantListParams {
+  page: number
+  limit: number
+  scope: 'own' | 'subordinate'
+  agent_id?: string
+  name?: string
+  phone?: string
+  distributed?: string
+  status?: string
+  profit_method?: string
+  device_category?: string
+  agent_type?: string
+  sort?: string
+}
+
+export interface MerchantListSummary {
+  all: number
+  'city-partner': number
+  agent: number
+  salesperson: number
+}
+
+export interface MerchantListResult extends Api.Common.ApiPage<MerchantItem> {
+  summary: MerchantListSummary
+}
+
+export default {
+  list(params: MerchantListParams) {
+    return request.get<MerchantListResult>({
+      url: '/core/merchant/index',
+      params
+    })
+  },
+
+  read(id: number | string) {
+    return request.get<MerchantItem>({
+      url: `/core/merchant/read?id=${id}`
+    })
+  },
+
+  save(data: MerchantForm) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/merchant/save',
+      data
+    })
+  },
+
+  update(data: MerchantForm) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/merchant/update',
+      data
+    })
+  },
+
+  delete(ids: number[]) {
+    return request.del<Api.Common.ApiData>({
+      url: '/core/merchant/destroy',
+      data: { ids }
+    })
+  },
+
+  resetPassword(id: number, password: string) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/merchant/password',
+      data: { id, password }
+    })
+  },
+
+  devices(id: number) {
+    return request.get<MerchantDeviceItem[]>({
+      url: '/core/merchant/devices',
+      params: { id }
+    })
+  },
+
+  bindDevices(merchantId: number, deviceSns: string[]) {
+    return request.post<MerchantDeviceBindResult>({
+      url: '/core/merchant/device/bind',
+      data: { merchant_id: merchantId, device_sns: deviceSns }
+    })
+  },
+
+  unbindDevice(merchantId: number, deviceId: number) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/merchant/device/unbind',
+      data: { merchant_id: merchantId, device_id: deviceId }
+    })
+  },
+
+  withdrawalSetting(id: number) {
+    return request.get<MerchantWithdrawalSetting>({
+      url: '/core/merchant/withdrawal-setting',
+      params: { id }
+    })
+  },
+
+  updateWithdrawalSetting(data: MerchantWithdrawalSetting) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/merchant/withdrawal-setting',
+      data
+    })
+  },
+
+  businessTime(id: number) {
+    return request.get<MerchantBusinessTime>({
+      url: '/core/merchant/business-time',
+      params: { id }
+    })
+  },
+
+  updateBusinessTime(data: MerchantBusinessTime) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/merchant/business-time',
+      data
+    })
+  },
+
+  statistics(params: { id: number; start_date: string; end_date: string }) {
+    return request.get<MerchantStatisticsData>({
+      url: '/core/merchant/statistics',
+      params
+    })
+  },
+
+  exportStatistics(data: { id: number; start_date: string; end_date: string }) {
+    return request.post<Blob>({
+      url: '/core/merchant/statistics/export',
+      data,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 59 - 0
saiadmin-artd/src/api/miniProgram/alipay.ts

@@ -0,0 +1,59 @@
+import request from '@/utils/http'
+
+export interface AlipayMiniProgramSummary {
+  id: number
+  partner_id: number
+  mini_program_name: string
+  appid: string
+  credit_service_id: string
+  create_time?: string
+  update_time?: string
+}
+
+export interface AlipayMiniProgramForm extends AlipayMiniProgramSummary {
+  app_private_key: string
+  alipay_public_key: string
+  aes_key: string
+}
+
+export type AlipayMiniProgramPayload = Omit<
+  AlipayMiniProgramForm,
+  'partner_id' | 'create_time' | 'update_time'
+>
+
+export default {
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage<AlipayMiniProgramSummary>>({
+      url: '/core/miniProgram/alipay/index',
+      params
+    })
+  },
+
+  read(id: number) {
+    return request.get<AlipayMiniProgramForm>({
+      url: '/core/miniProgram/alipay/read',
+      params: { id }
+    })
+  },
+
+  save(data: Omit<AlipayMiniProgramPayload, 'id'>) {
+    return request.post<{ id: number }>({
+      url: '/core/miniProgram/alipay/save',
+      data
+    })
+  },
+
+  update(data: AlipayMiniProgramPayload) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/alipay/update',
+      data
+    })
+  },
+
+  delete(data: { ids: number[] }) {
+    return request.del<Api.Common.ApiData>({
+      url: '/core/miniProgram/alipay/destroy',
+      data
+    })
+  }
+}

+ 27 - 0
saiadmin-artd/src/api/miniProgram/defaultSettings.ts

@@ -0,0 +1,27 @@
+import request from '@/utils/http'
+
+export interface MiniProgramDefaultSetting {
+  id: number
+  device_type: string
+  device_name: string
+  free_minutes: number
+  overdue_deposit: number
+  min_deposit: number
+  max_deposit: number
+  sort: number
+}
+
+export default {
+  list() {
+    return request.get<MiniProgramDefaultSetting[]>({
+      url: '/core/miniProgram/defaultSettings/index'
+    })
+  },
+
+  update(data: MiniProgramDefaultSetting) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/defaultSettings/update',
+      data
+    })
+  }
+}

+ 57 - 0
saiadmin-artd/src/api/miniProgram/helpCenter.ts

@@ -0,0 +1,57 @@
+import request from '@/utils/http'
+
+export type HelpCategory = 'use' | 'pay' | 'return' | 'refund'
+
+export interface MiniProgramHelpArticle {
+  id: number
+  category: HelpCategory
+  title: string
+  description: string
+  sort: number
+  status: number
+  create_time?: string
+  update_time?: string
+}
+
+export interface HelpArticleQuery {
+  category: HelpCategory
+  page: number
+  limit: number
+}
+
+export type HelpArticlePayload = Omit<
+  MiniProgramHelpArticle,
+  'id' | 'create_time' | 'update_time'
+> & {
+  id?: number
+}
+
+export default {
+  list(params: HelpArticleQuery) {
+    return request.get<Api.Common.ApiPage<MiniProgramHelpArticle>>({
+      url: '/core/miniProgram/helpCenter/index',
+      params
+    })
+  },
+
+  save(data: HelpArticlePayload) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/miniProgram/helpCenter/save',
+      data
+    })
+  },
+
+  update(data: HelpArticlePayload) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/helpCenter/update',
+      data
+    })
+  },
+
+  delete(ids: number[]) {
+    return request.del<Api.Common.ApiData>({
+      url: '/core/miniProgram/helpCenter/destroy',
+      data: { ids }
+    })
+  }
+}

+ 43 - 0
saiadmin-artd/src/api/miniProgram/officialAccount.ts

@@ -0,0 +1,43 @@
+import request from '@/utils/http'
+
+export interface WechatOfficialAccountConfig {
+  id: number
+  logo: string
+  account_name: string
+  description: string
+  follow_message: string
+  follow_url: string
+  follow_enabled: number
+  appid: string
+  original_id: string
+  appsecret: string
+  server_url: string
+  token: string
+  encoding_aes_key: string
+  encryption_mode: number
+  free_recharge_times: number
+  follow_free_recharge: number
+  all_merchant_follow: number
+  create_time?: string
+  update_time?: string
+}
+
+export type WechatOfficialAccountPayload = Omit<
+  WechatOfficialAccountConfig,
+  'id' | 'create_time' | 'update_time'
+>
+
+export default {
+  read() {
+    return request.get<WechatOfficialAccountConfig>({
+      url: '/core/miniProgram/officialAccount/read'
+    })
+  },
+
+  update(data: WechatOfficialAccountPayload) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/officialAccount/update',
+      data
+    })
+  }
+}

+ 29 - 0
saiadmin-artd/src/api/miniProgram/platformInfo.ts

@@ -0,0 +1,29 @@
+import request from '@/utils/http'
+
+export interface MiniProgramPlatformInfo {
+  id?: number
+  mini_program_logo: string
+  mini_program_name: string
+  official_account_qrcode: string
+  official_account_name: string
+  customer_service_phone: string
+  customer_service_wechat: string
+  company_name: string
+  company_address: string
+  introduction: string
+}
+
+export default {
+  read() {
+    return request.get<MiniProgramPlatformInfo>({
+      url: '/core/miniProgram/platformInfo/read'
+    })
+  },
+
+  update(data: MiniProgramPlatformInfo) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/platformInfo/update',
+      data
+    })
+  }
+}

+ 32 - 0
saiadmin-artd/src/api/miniProgram/relatedLinks.ts

@@ -0,0 +1,32 @@
+import request from '@/utils/http'
+
+export interface MiniProgramRelatedLink {
+  id: number
+  code: string
+  name: string
+  link: string
+  link_label: string
+  secondary_link: string
+  secondary_label: string
+  sort: number
+}
+
+export interface MiniProgramRelatedLinksConfig {
+  links: MiniProgramRelatedLink[]
+  verification_file: string
+}
+
+export default {
+  read() {
+    return request.get<MiniProgramRelatedLinksConfig>({
+      url: '/core/miniProgram/relatedLinks/read'
+    })
+  },
+
+  updateVerification(verificationFile: string) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/relatedLinks/updateVerification',
+      data: { verification_file: verificationFile }
+    })
+  }
+}

+ 65 - 0
saiadmin-artd/src/api/miniProgram/settings.ts

@@ -0,0 +1,65 @@
+import request from '@/utils/http'
+
+export interface WithdrawTimeRange {
+  start: string
+  end: string
+}
+
+export interface WithdrawProfile {
+  review_amount: number
+  other_fee_mode: 'tax' | 'fee' | 'both'
+  alipay_fee_mode: 'tax' | 'fee' | 'both'
+  tax_rate: number
+  fee: number
+  min_amount: number
+  max_amount: number
+  schedule_type: 'day' | 'week' | 'month'
+  time_ranges: WithdrawTimeRange[]
+}
+
+export interface MiniProgramSettings {
+  deposit_refund: {
+    original_refund: number
+    refund_speed: 'immediate' | 'delayed'
+    delay_hours: number
+    wallet_as_deposit: number
+  }
+  withdraw_methods: string[]
+  agent_withdraw: WithdrawProfile
+  merchant_withdraw: WithdrawProfile
+  user_withdraw: {
+    review_amount: number
+    fee_rate: number
+    min_amount: number
+    max_amount: number
+    deposit_refund_mode: number
+  }
+  user_settings: {
+    wechat_phone_authorization: number
+    alipay_phone_authorization: number
+  }
+  membership: {
+    enabled: number
+    month_price: number
+    quarter_price: number
+    year_price: number
+  }
+  order_profit_detail: number
+  official_account_login: number
+  new_merchant_dd_prefix: number
+}
+
+export default {
+  read() {
+    return request.get<MiniProgramSettings>({
+      url: '/core/miniProgram/settings/read'
+    })
+  },
+
+  update(data: MiniProgramSettings) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/settings/update',
+      data
+    })
+  }
+}

+ 23 - 0
saiadmin-artd/src/api/miniProgram/shareSettings.ts

@@ -0,0 +1,23 @@
+import request from '@/utils/http'
+
+export interface MiniProgramShareSetting {
+  id: number
+  share_name: string
+  share_title: string
+  share_image: string
+}
+
+export default {
+  list() {
+    return request.get<MiniProgramShareSetting[]>({
+      url: '/core/miniProgram/shareSettings/index'
+    })
+  },
+
+  update(data: MiniProgramShareSetting) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/shareSettings/update',
+      data
+    })
+  }
+}

+ 27 - 0
saiadmin-artd/src/api/miniProgram/templateMessage.ts

@@ -0,0 +1,27 @@
+import request from '@/utils/http'
+
+export interface MiniProgramTemplateMessage {
+  id: number
+  code: string
+  title: string
+  usage_detail: string
+  keywords: string
+  template_id: string
+  appid: string
+  type: '一次性订阅' | '长期订阅'
+}
+
+export default {
+  list() {
+    return request.get<MiniProgramTemplateMessage[]>({
+      url: '/core/miniProgram/templateMessage/index'
+    })
+  },
+
+  update(data: MiniProgramTemplateMessage) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/templateMessage/update',
+      data
+    })
+  }
+}

+ 61 - 0
saiadmin-artd/src/api/miniProgram/version.ts

@@ -0,0 +1,61 @@
+import request from '@/utils/http'
+
+/**
+ * 小程序版本管理 API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数 + 分页
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/miniProgram/version/index',
+      params
+    })
+  },
+
+  /**
+   * 读取单条
+   * @param id 记录 ID
+   */
+  read(id: number) {
+    return request.get<Record<string, any>>({
+      url: '/core/miniProgram/version/read',
+      params: { id }
+    })
+  },
+
+  /**
+   * 新增版本
+   * @param params 表单数据
+   */
+  save(params: Record<string, any>) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/miniProgram/version/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新版本
+   * @param params 表单数据(需含 id)
+   */
+  update(params: Record<string, any>) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/version/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除版本(支持批量)
+   * @param params { ids: number[] }
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/miniProgram/version/destroy',
+      data: params
+    })
+  }
+}

+ 66 - 0
saiadmin-artd/src/api/miniProgram/wechat.ts

@@ -0,0 +1,66 @@
+import request from '@/utils/http'
+
+export interface WechatMiniProgramSummary {
+  id: number
+  partner_nickname: string
+  partner_account: string
+  partner_name: string
+  mini_program_name: string
+  appid: string
+  create_time?: string
+  update_time?: string
+}
+
+export interface WechatMiniProgramForm extends WechatMiniProgramSummary {
+  appsecret: string
+  original_id: string
+  mchid: string
+  api_key: string
+  apiv3_key: string
+  apiclient_cert: string
+  apiclient_key: string
+  certificate_serial_no: string
+  service_id: string
+}
+
+export type WechatMiniProgramPayload = Omit<
+  WechatMiniProgramForm,
+  'partner_nickname' | 'partner_account' | 'partner_name' | 'create_time' | 'update_time'
+>
+
+export default {
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/miniProgram/wechat/index',
+      params
+    })
+  },
+
+  read(id: number) {
+    return request.get<WechatMiniProgramForm>({
+      url: '/core/miniProgram/wechat/read',
+      params: { id }
+    })
+  },
+
+  save(data: Omit<WechatMiniProgramPayload, 'id'>) {
+    return request.post<{ id: number }>({
+      url: '/core/miniProgram/wechat/save',
+      data
+    })
+  },
+
+  update(data: WechatMiniProgramPayload) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/miniProgram/wechat/update',
+      data
+    })
+  },
+
+  delete(data: { ids: number[] }) {
+    return request.del<Api.Common.ApiData>({
+      url: '/core/miniProgram/wechat/destroy',
+      data
+    })
+  }
+}

+ 74 - 0
saiadmin-artd/src/api/operationManagement/deviceOperation.ts

@@ -0,0 +1,74 @@
+import request from '@/utils/http'
+
+export type DeviceOperationScope = 'mine' | 'subordinate'
+export type DeviceOperationCategory = string
+export type DeviceOperationSort =
+  | ''
+  | 'total_amount'
+  | 'total_orders'
+  | 'unit_price'
+  | 'profit_rate_value'
+
+export interface DeviceOperationCategoryOption {
+  value: string
+  label: string
+  goods_types: number[]
+}
+
+export interface DeviceOperation {
+  id: number
+  goods_type: number
+  goods_sn: string
+  device_code: string
+  manage_agent_id: number
+  agent_name: string
+  merchant_id: number
+  merchant_name: string | null
+  bind_time: string | null
+  cost_price: number | null
+  merchant_rate: number | null
+  manager_rate: number | null
+  total_amount: number
+  today_amount: number
+  yesterday_amount: number
+  total_orders: number
+  today_orders: number
+  yesterday_orders: number
+  paid_orders: number
+  unit_price: number
+  total_profit: number | null
+  today_profit: number | null
+  yesterday_profit: number | null
+  profit_rate_value: number | null
+  daily_growth: number | null
+  operated_days: number
+  daily_average_amount: number | null
+  is_cost_recovery: boolean | null
+  cost_recovery_date: string | null
+  cost_recovery_days: number | null
+  performance_grade: number | null
+  today_performance: string
+}
+
+export interface DeviceOperationParams {
+  scope: DeviceOperationScope
+  category: DeviceOperationCategory
+  page: number
+  limit: number
+  sort: DeviceOperationSort
+  order: 'asc' | 'desc'
+}
+
+export default {
+  list(params: DeviceOperationParams) {
+    return request.get<
+      Api.Common.ApiPage<DeviceOperation> & {
+        categories: DeviceOperationCategoryOption[]
+        category: string
+      }
+    >({
+      url: '/core/operation-management/device-operations/index',
+      params
+    })
+  }
+}

+ 46 - 0
saiadmin-artd/src/api/operationManagement/merchantAlert.ts

@@ -0,0 +1,46 @@
+import request from '@/utils/http'
+
+export type MerchantAlertType = '' | 'two_days' | 'three_days' | 'never'
+
+export interface MerchantAlertParams {
+  page: number
+  limit: number
+  merchant_name: string
+  alert_type: MerchantAlertType
+}
+
+export interface MerchantAlert {
+  merchant_id: number
+  merchant_name: string
+  created_at: string
+  last_login_at: string
+  last_distribution_at: string
+  last_transaction_date: string
+  alert_type: Exclude<MerchantAlertType, ''>
+  reason: string
+  contact: string
+  phone: string
+}
+
+export interface MerchantAlertPage {
+  data: MerchantAlert[]
+  total: number
+}
+
+export default {
+  list(params: MerchantAlertParams) {
+    return request.get<MerchantAlertPage>({
+      url: '/core/operation-management/merchant-alerts/index',
+      params
+    })
+  },
+
+  export(params: Omit<MerchantAlertParams, 'page' | 'limit'>) {
+    return request.post<Blob>({
+      url: '/core/operation-management/merchant-alerts/export',
+      data: params,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 100 - 0
saiadmin-artd/src/api/order/myOrder.ts

@@ -0,0 +1,100 @@
+import request from '@/utils/http'
+
+export type OrderCategory =
+  | 'all'
+  | 'vehicle'
+  | 'k-battery'
+  | 'bluetooth'
+  | 'power-bank'
+  | 'password'
+  | 'password-bluetooth'
+export type OrderStatus =
+  | 'all'
+  | 'today'
+  | 'pending'
+  | 'using'
+  | 'done'
+  | 'timeout'
+  | 'rent_failed'
+  | 'deduct_failed'
+  | 'refund'
+
+export interface MyOrderParams {
+  page?: number
+  limit?: number
+  category?: OrderCategory
+  status?: OrderStatus
+  order_no?: string
+  phone?: string
+  user_id?: string
+  merchant?: string
+  device_sn?: string
+  transaction_id?: string
+  agent_id?: number
+  source?: '' | '1' | '2'
+  pay_type?: '' | 'deposit' | 'free'
+  start_at?: string
+  end_at?: string
+}
+
+export interface MyOrderSummary {
+  all: number
+  today: number
+  pending: number
+  using: number
+  done: number
+  timeout: number
+  rent_failed: number
+  deduct_failed: number
+  refund: number
+}
+
+export interface MyOrder {
+  id: number
+  order_sn: string
+  sub_order_sn: string
+  transaction_id: string
+  merchant_order_no: string
+  device_type: number
+  device_type_name: string
+  device_name: string
+  device_sn: string
+  device_code: string
+  source: number
+  source_name: string
+  pay_type: string
+  pay_type_name: string
+  user_id: number
+  user_name: string
+  user_phone: string
+  rent_merchant: string
+  return_merchant: string
+  rent_at: string
+  return_at: string
+  status: Exclude<OrderStatus, 'all' | 'today'>
+  status_name: string
+  paid_amount: number
+  income_amount: number
+  refund_amount: number
+  package_description: string
+  remark: string
+  manage_agent_id: number
+  manager_name: string
+  manager_phone: string
+  create_time: string
+}
+
+export interface MyOrderListResult {
+  data: MyOrder[]
+  total: number
+  summary: MyOrderSummary
+}
+
+export default {
+  list(params: MyOrderParams) {
+    return request.get<MyOrderListResult>({ url: '/core/order/my/index', params })
+  },
+  read(id: number) {
+    return request.get<MyOrder>({ url: '/core/order/my/read', params: { id } })
+  }
+}

+ 22 - 0
saiadmin-artd/src/api/order/subordinateOrder.ts

@@ -0,0 +1,22 @@
+import request from '@/utils/http'
+import type { MyOrder, MyOrderListResult, MyOrderParams } from './myOrder'
+
+export type SubordinateOrderParams = MyOrderParams
+export type SubordinateOrder = MyOrder
+export type SubordinateOrderListResult = MyOrderListResult
+
+export default {
+  list(params: SubordinateOrderParams) {
+    return request.get<SubordinateOrderListResult>({
+      url: '/core/order/subordinate/index',
+      params
+    })
+  },
+
+  read(id: number) {
+    return request.get<SubordinateOrder>({
+      url: '/core/order/subordinate/read',
+      params: { id }
+    })
+  }
+}

+ 54 - 0
saiadmin-artd/src/api/safeguard/attachment.ts

@@ -0,0 +1,54 @@
+import request from '@/utils/http'
+
+/**
+ * 附件API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/attachment/index',
+      params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/attachment/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/attachment/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 移动文件到分类
+   * @param params 参数,包含文件ID数组和目标分类ID
+   * @returns 执行结果
+   */
+  move(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/attachment/move',
+      data: params
+    })
+  }
+}

+ 65 - 0
saiadmin-artd/src/api/safeguard/category.ts

@@ -0,0 +1,65 @@
+import request from '@/utils/http'
+
+/**
+ * 附件分类API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/category/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/category/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/category/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/category/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/category/destroy',
+      data: params
+    })
+  }
+}

+ 95 - 0
saiadmin-artd/src/api/safeguard/database.ts

@@ -0,0 +1,95 @@
+import request from '@/utils/http'
+
+/**
+ * 数据表维护API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/database/index',
+      params
+    })
+  },
+
+  /**
+   * 获取数据源
+   * @returns
+   */
+  getDataSource(params: Record<string, any> = {}) {
+    return request.get<any>({
+      url: '/core/database/dataSource',
+      params
+    })
+  },
+
+  /**
+   * 获取表字段列表
+   * @returns
+   */
+  getDetailed(params: Record<string, any> = {}) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/database/detailed',
+      params
+    })
+  },
+
+  /**
+   * 获取回收站数据
+   * @returns
+   */
+  getRecycle(params: Record<string, any> = {}) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/database/recycle',
+      params
+    })
+  },
+
+  /**
+   * 销毁数据
+   * @returns
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/database/delete',
+      data: params
+    })
+  },
+
+  /**
+   * 恢复数据
+   * @returns
+   */
+  recovery(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/database/recovery',
+      data: params
+    })
+  },
+
+  /**
+   * 优化表
+   * @returns
+   */
+  optimize(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/database/optimize',
+      data: params
+    })
+  },
+
+  /**
+   * 清理表碎片
+   * @returns
+   */
+  fragment(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/database/fragment',
+      data: params
+    })
+  }
+}

+ 102 - 0
saiadmin-artd/src/api/safeguard/dict.ts

@@ -0,0 +1,102 @@
+import request from '@/utils/http'
+
+/**
+ * 字典数据API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  typeList(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/dictType/index',
+      params
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/dictType/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/dictType/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/dictType/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 字典项数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  dataList(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/dictData/index',
+      params
+    })
+  },
+
+  /**
+   * 创建字典项数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  dataSave(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/dictData/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新字典项数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  dataUpdate(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/dictData/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  dataDelete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/dictData/destroy',
+      data: params
+    })
+  }
+}

+ 30 - 0
saiadmin-artd/src/api/safeguard/emailLog.ts

@@ -0,0 +1,30 @@
+import request from '@/utils/http'
+
+/**
+ * 邮件日志数据API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/email/index',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param params
+   * @returns
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/email/destroy',
+      data: params
+    })
+  }
+}

+ 30 - 0
saiadmin-artd/src/api/safeguard/loginLog.ts

@@ -0,0 +1,30 @@
+import request from '@/utils/http'
+
+/**
+ * 登录日志数据API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/logs/getLoginLogPageList',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param params
+   * @returns
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/logs/deleteLoginLog',
+      data: params
+    })
+  }
+}

+ 30 - 0
saiadmin-artd/src/api/safeguard/operLog.ts

@@ -0,0 +1,30 @@
+import request from '@/utils/http'
+
+/**
+ * 操作日志数据API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/logs/getOperLogPageList',
+      params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param params
+   * @returns
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/logs/deleteOperLog',
+      data: params
+    })
+  }
+}

+ 42 - 0
saiadmin-artd/src/api/safeguard/server.ts

@@ -0,0 +1,42 @@
+import request from '@/utils/http'
+
+/**
+ * 服务器信息API
+ */
+export default {
+  /**
+   * 服务监控
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  monitor(params: Record<string, any>) {
+    return request.get<any>({
+      url: '/core/server/monitor',
+      params
+    })
+  },
+
+  /**
+   * 缓存列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  cache(params: Record<string, any>) {
+    return request.get<any>({
+      url: '/core/server/cache',
+      params
+    })
+  },
+
+  /**
+   * 清理缓存
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  clear(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/server/clear',
+      params
+    })
+  }
+}

+ 126 - 0
saiadmin-artd/src/api/system/config.ts

@@ -0,0 +1,126 @@
+import request from '@/utils/http'
+
+/**
+ * 系统设置API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  groupList(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/configGroup/index',
+      params
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/configGroup/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/configGroup/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/configGroup/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 系统设置项数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  configList(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/config/index',
+      params
+    })
+  },
+
+  /**
+   * 创建系统设置项数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  configSave(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/config/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新系统设置项数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  configUpdate(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/config/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  configDelete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/config/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 批量修改配置
+   * @param params
+   * @returns
+   */
+  batchUpdate(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/config/batchUpdate',
+      data: params
+    })
+  },
+
+  /**
+   * 邮件测试
+   * @param params
+   * @returns
+   */
+  emailTest(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/configGroup/email',
+      data: params
+    })
+  }
+}

+ 75 - 0
saiadmin-artd/src/api/system/dept.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/http'
+
+/**
+ * 部门API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/dept/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/dept/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/dept/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/dept/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/dept/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 可操作部门
+   * @returns 数据列表
+   */
+  accessDept() {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/dept/accessDept'
+    })
+  }
+}

+ 76 - 0
saiadmin-artd/src/api/system/menu.ts

@@ -0,0 +1,76 @@
+import request from '@/utils/http'
+
+/**
+ * 菜单API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/menu/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/menu/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/menu/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/menu/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/menu/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 可操作角色
+   * @returns 数据列表
+   */
+  accessMenu(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/menu/accessMenu',
+      params
+    })
+  }
+}

+ 75 - 0
saiadmin-artd/src/api/system/post.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/http'
+
+/**
+ * 岗位API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/post/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/post/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/post/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/post/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/post/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 可操作岗位
+   * @returns 数据列表
+   */
+  accessPost() {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/post/accessPost'
+    })
+  }
+}

+ 123 - 0
saiadmin-artd/src/api/system/role.ts

@@ -0,0 +1,123 @@
+import request from '@/utils/http'
+
+/**
+ * 角色API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/role/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/role/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/role/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/role/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/role/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  menuByRole(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/role/getMenuByRole',
+      params
+    })
+  },
+
+  /**
+   * 可操作角色
+   * @returns 数据列表
+   */
+  accessRole() {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/role/accessRole'
+    })
+  },
+
+  /**
+   * 保存菜单权限
+   * @param params
+   * @returns
+   */
+  menuPermission(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/role/menuPermission',
+      data: params
+    })
+  },
+
+  /**
+   * 获取部门数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  deptByRole(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/core/role/getDeptByRole',
+      params
+    })
+  },
+
+  /**
+   * 保存数据权限
+   * @param params
+   * @returns
+   */
+  dataPermission(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/role/dataPermission',
+      data: params
+    })
+  }
+}

+ 101 - 0
saiadmin-artd/src/api/system/user.ts

@@ -0,0 +1,101 @@
+import request from '@/utils/http'
+
+/**
+ * 用户API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/core/user/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/core/user/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/user/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/core/user/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/core/user/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 设置首页
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  setHomePage(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/user/setHomePage',
+      data: params
+    })
+  },
+
+  /**
+   * 修改密码
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  changePassword(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/user/initUserPassword',
+      data: params
+    })
+  },
+
+  /**
+   * 清理缓存
+   * @param params
+   * @returns
+   */
+  clearCache(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/core/user/clearCache',
+      data: params
+    })
+  }
+}

+ 101 - 0
saiadmin-artd/src/api/tool/crontab.ts

@@ -0,0 +1,101 @@
+import request from '@/utils/http'
+
+/**
+ * 定时任务API
+ */
+export default {
+  /**
+   * 获取数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/tool/crontab/index',
+      params
+    })
+  },
+
+  /**
+   * 读取数据
+   * @param id 数据ID
+   * @returns 数据详情
+   */
+  read(id: number | string) {
+    return request.get<Api.Common.ApiData>({
+      url: '/tool/crontab/read?id=' + id
+    })
+  },
+
+  /**
+   * 创建数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  save(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/tool/crontab/save',
+      data: params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/tool/crontab/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/tool/crontab/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 运行
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  run(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/tool/crontab/run',
+      data: params
+    })
+  },
+
+  /**
+   * 获取运行日志列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  logPageList(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/tool/crontab/logPageList',
+      params
+    })
+  },
+
+  /**
+   * 删除运行日志
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  deleteCrontabLog(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/tool/crontab/deleteCrontabLog',
+      data: params
+    })
+  }
+}

+ 126 - 0
saiadmin-artd/src/api/tool/generate.ts

@@ -0,0 +1,126 @@
+import request from '@/utils/http'
+
+/**
+ * 代码生成API
+ */
+export default {
+  /**
+   * 数据列表
+   * @param params 搜索参数
+   * @returns 数据列表
+   */
+  list(params: Record<string, any>) {
+    return request.get<Api.Common.ApiPage>({
+      url: '/tool/code/index',
+      params
+    })
+  },
+
+  /**
+   * 读取表结构
+   * @param params 搜索参数
+   * @returns 表结构
+   */
+  read(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData>({
+      url: '/tool/code/read',
+      params
+    })
+  },
+
+  /**
+   * 更新数据
+   * @param params 数据参数
+   * @returns 执行结果
+   */
+  update(params: Record<string, any>) {
+    return request.put<any>({
+      url: '/tool/code/update',
+      data: params
+    })
+  },
+
+  /**
+   * 删除数据
+   * @param id 数据ID
+   * @returns 执行结果
+   */
+  delete(params: Record<string, any>) {
+    return request.del<any>({
+      url: '/tool/code/destroy',
+      data: params
+    })
+  },
+
+  /**
+   * 获取表字段
+   * @param params 搜索参数
+   * @returns 表字段
+   */
+  getTableColumns(params: Record<string, any>) {
+    return request.get<Api.Common.ApiData[]>({
+      url: '/tool/code/getTableColumns',
+      params
+    })
+  },
+
+  /**
+   * 装载数据表
+   * @param params 搜索参数
+   * @returns 装载结果
+   */
+  loadTable(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/tool/code/loadTable',
+      params
+    })
+  },
+
+  /**
+   * 同步数据表
+   * @param params 搜索参数
+   * @returns 装载结果
+   */
+  async(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/tool/code/sync',
+      params
+    })
+  },
+
+  /**
+   * 预览代码
+   * @param params 搜索参数
+   * @returns 预览结果
+   */
+  preview(params: Record<string, any>) {
+    return request.get<any>({
+      url: '/tool/code/preview',
+      params
+    })
+  },
+
+  /**
+   * 生成代码
+   * @returns
+   */
+  generateCode(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/tool/code/generate',
+      responseType: 'blob',
+      timeout: 20 * 1000,
+      params
+    })
+  },
+
+  /**
+   * 生成到文件
+   * @returns
+   */
+  generateFile(params: Record<string, any>) {
+    return request.post<any>({
+      url: '/tool/code/generateFile',
+      params
+    })
+  }
+}

+ 124 - 0
saiadmin-artd/src/api/userManagement/rentalUser.ts

@@ -0,0 +1,124 @@
+import request from '@/utils/http'
+
+export interface RentalUserParams {
+  page: number
+  limit: number
+  source: '' | '1' | '2'
+  user_id?: string
+  nickname?: string
+  phone?: string
+}
+
+export interface RentalUser {
+  id: number
+  agent_id: number
+  source: number
+  source_name: string
+  nickname: string
+  phone: string
+  avatar: string
+  deposit_amount: number
+  rent_count: number
+  amount: number
+  wallet_balance: number
+  card_balance: number
+  card_gift_balance: number
+  member_expire_at: string
+  member_card_type: number
+  member_card_type_name: string
+  member_active: boolean
+  status: number
+  register_at: string
+}
+
+export interface RentalUserOrder {
+  id: number
+  order_sn: string
+  device_type: number
+  device_name: string
+  device_sn: string
+  merchant_name: string
+  rent_at: string
+  return_at: string
+  status: number
+  status_name: string
+  paid_amount: number
+  refund_amount: number
+  manage_agent_id: number
+}
+
+export interface PageResult<T> {
+  data: T[]
+  total: number
+}
+
+export interface RentalUserRechargePayload {
+  user_id: number
+  balance_type?: 'wallet' | 'card_gift'
+  amount: number
+  remark?: string
+}
+
+export interface RentalUserRechargeResult {
+  user_id: number
+  balance_type: string
+  amount: number
+  before_balance: number
+  after_balance: number
+}
+
+export interface MemberActivationPayload {
+  user_id: number
+  card_type: 0 | 1 | 2 | 3
+  pay_amount: number
+  expire_time: string
+  activation_time?: string
+  remark?: string
+}
+
+export interface MemberActivationResult {
+  user_id: number
+  card_type: number
+  card_type_name: string
+  pay_amount: number
+  activation_time: string
+  expire_time: string
+  merchant_order_sn: string
+}
+
+export const MEMBER_CARD_TYPE_OPTIONS: { value: 0 | 1 | 2 | 3; label: string }[] = [
+  { value: 0, label: '月卡' },
+  { value: 1, label: '季卡' },
+  { value: 2, label: '半年卡' },
+  { value: 3, label: '年卡' }
+]
+
+export default {
+  list(params: RentalUserParams) {
+    return request.get<PageResult<RentalUser>>({
+      url: '/core/user-management/users/index',
+      params
+    })
+  },
+
+  orders(userId: number, page: number, limit: number) {
+    return request.get<PageResult<RentalUserOrder>>({
+      url: '/core/user-management/users/orders',
+      params: { user_id: userId, page, limit }
+    })
+  },
+
+  recharge(payload: RentalUserRechargePayload) {
+    return request.post<RentalUserRechargeResult>({
+      url: '/core/user-management/users/recharge',
+      data: payload
+    })
+  },
+
+  activateMember(payload: MemberActivationPayload) {
+    return request.post<MemberActivationResult>({
+      url: '/core/user-management/users/member-activation',
+      data: payload
+    })
+  }
+}

+ 102 - 0
saiadmin-artd/src/api/userManagement/userComplaint.ts

@@ -0,0 +1,102 @@
+import request from '@/utils/http'
+
+export interface ComplaintConfig {
+  id: number
+  partner_id: number
+  mini_program_name: string
+  appid: string
+  mchid: string
+}
+
+export interface ComplaintOrder {
+  transaction_id?: string
+  out_trade_no?: string
+  amount?: number
+}
+
+export interface ComplaintMedia {
+  media_type?: string
+  media_url?: string
+}
+
+export interface UserComplaint {
+  complaint_id: string
+  complaint_time: string
+  complaint_detail: string
+  complaint_state: string
+  complainted_mchid?: string
+  complaint_order_info: ComplaintOrder[]
+  complaint_media_list?: ComplaintMedia[]
+  complaint_full_refunded?: boolean
+  incoming_user_response?: boolean
+  user_complaint_times?: number
+  payer_phone?: string
+  refund_remark?: string
+}
+
+export interface ComplaintHistory {
+  complaint_id?: string
+  log_id?: string
+  operator?: string
+  operate_time?: string
+  operate_type?: string
+  operate_details?: string
+  complaint_media_list?: ComplaintMedia[]
+}
+
+export interface ComplaintListResult {
+  data: UserComplaint[]
+  total: number
+  current_page: number
+  per_page: number
+  config: Pick<ComplaintConfig, 'id' | 'mini_program_name' | 'appid' | 'mchid'>
+}
+
+export default {
+  configs() {
+    return request.get<ComplaintConfig[]>({
+      url: '/core/user-management/complaints/configs'
+    })
+  },
+
+  list(params: {
+    config_id: number
+    start_date: string
+    end_date: string
+    page: number
+    limit: number
+  }) {
+    return request.get<ComplaintListResult>({
+      url: '/core/user-management/complaints/index',
+      params
+    })
+  },
+
+  detail(configId: number, complaintId: string) {
+    return request.get<UserComplaint>({
+      url: '/core/user-management/complaints/read',
+      params: { config_id: configId, complaint_id: complaintId }
+    })
+  },
+
+  history(configId: number, complaintId: string) {
+    return request.get<ComplaintHistory[]>({
+      url: '/core/user-management/complaints/history',
+      params: { config_id: configId, complaint_id: complaintId }
+    })
+  },
+
+  reply(data: { config_id: number; complaint_id: string; content: string }) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/user-management/complaints/reply',
+      data
+    })
+  },
+
+  complete(data: { config_id: number; complaint_id: string }) {
+    return request.post<Api.Common.ApiData>({
+      url: '/core/user-management/complaints/complete',
+      data
+    })
+  }
+}

+ 110 - 0
saiadmin-artd/src/api/userManagement/userFeedback.ts

@@ -0,0 +1,110 @@
+import request from '@/utils/http'
+
+export type FeedbackStatus = 0 | 1 | 2
+
+export interface FeedbackReply {
+  id: number
+  author_type: number
+  author_name: string
+  content: string
+  images: string[]
+  create_time: string
+}
+
+export interface UserFeedback {
+  id: number
+  agent_id: number
+  user_id: number
+  identity: string
+  nickname: string
+  phone: string
+  type: number
+  type_name: string
+  order_id: number
+  order_sn: string
+  transaction_id: string
+  merchant_id: number
+  merchant_name: string
+  device_id: number
+  device_sn: string
+  device_code: string
+  device_type: number
+  device_type_name: string
+  content: string
+  images: string[]
+  status: FeedbackStatus
+  status_name: string
+  remark: string
+  handled_by: number
+  handled_name: string
+  handled_at: string
+  latest_reply_identity: string
+  latest_reply: string
+  create_time: string
+  update_time: string
+  replies?: FeedbackReply[]
+}
+
+export interface FeedbackSummary {
+  all: number
+  pending: number
+  processing: number
+  done: number
+}
+
+export interface FeedbackListResult extends Api.Common.ApiPage<UserFeedback> {
+  summary: FeedbackSummary
+}
+
+export interface FeedbackListParams {
+  page?: number
+  limit?: number
+  status?: '' | '0' | '1' | '2'
+  type?: string
+  device_type?: string
+  phone?: string
+  merchant?: string
+  device_sn?: string
+  content?: string
+  start_date?: string
+  end_date?: string
+}
+
+export default {
+  list(params: FeedbackListParams) {
+    return request.get<FeedbackListResult>({
+      url: '/core/user-management/feedback/index',
+      params
+    })
+  },
+
+  detail(id: number) {
+    return request.get<UserFeedback>({
+      url: '/core/user-management/feedback/read',
+      params: { id }
+    })
+  },
+
+  update(data: { id: number; status: FeedbackStatus; remark: string }) {
+    return request.put<Api.Common.ApiData>({
+      url: '/core/user-management/feedback/update',
+      data
+    })
+  },
+
+  reply(data: { id: number; content: string }) {
+    return request.post<{ id: number }>({
+      url: '/core/user-management/feedback/reply',
+      data
+    })
+  },
+
+  export(data: FeedbackListParams) {
+    return request.post<Blob>({
+      url: '/core/user-management/feedback/export',
+      data,
+      timeout: 0,
+      responseType: 'blob'
+    })
+  }
+}

+ 62 - 0
saiadmin-artd/src/api/water/agents.ts

@@ -0,0 +1,62 @@
+import request from '@/utils/http'
+import { getWater, type WaterPage } from './index'
+export interface WaterAgent {
+  id: number
+  name: string
+  contact: string
+  phone: string
+  region: string
+  address: string
+  user_id: number | null
+  username: string | null
+  status: number
+  parent_id: number | null
+  parent_name: string | null
+  level: number | null
+  enterprise_count: number
+  sales_cents: number
+  month_cents: number
+  commission_cents: number | null
+  authorization: 'authorized' | 'pending' | 'disabled'
+}
+export interface AgentFilters {
+  keyword: string
+  level: string
+  authorization: string
+}
+export interface AgentDashboard {
+  list: Omit<WaterPage, 'data'> & { data: WaterAgent[] }
+  summary: {
+    total: number
+    new_month: number
+    sales_cents: number
+    platform_share: number | null
+    commission_cents: number | null
+    members: number
+    active_members: number
+  }
+  ranking: WaterAgent[]
+  options: Pick<WaterAgent, 'id' | 'name' | 'status' | 'level'>[]
+  commission: { configured: boolean; message: string }
+  updated_at: string
+}
+export interface AgentDetail {
+  agent: WaterAgent
+  children: WaterAgent[]
+  enterprises: { id: number; name: string; region: string; status: number }[]
+}
+export interface CommissionPolicy {
+  level1_basis_points: number
+  level2_basis_points: number
+  level3_basis_points: number
+  revision: number
+  updated_at: string | null
+}
+export const getAgentDashboard = (params: AgentFilters & { page: number; limit: number }) =>
+  getWater<AgentDashboard>('agents/dashboard', params)
+export const getAgentDetail = (id: number) => getWater<AgentDetail>('agents/detail', { id })
+export const openAgentAccount = (data: Record<string, unknown>) =>
+  request.post<{ id: number; user_id: number }>({ url: '/core/water/agents/open-account', data })
+export const getCommissionPolicy = () => getWater<CommissionPolicy>('agents/commission-policy')
+export const saveCommissionPolicy = (data: Omit<CommissionPolicy, 'updated_at'>) =>
+  request.post<CommissionPolicy>({ url: '/core/water/agents/commission-policy', data })

+ 90 - 0
saiadmin-artd/src/api/water/alerts.ts

@@ -0,0 +1,90 @@
+import { getWater, saveWater, type WaterPage } from './index'
+
+export interface AlertPolicy {
+  alert_qty: number
+  comparison: 'lt'
+  days_per_barrel: number
+  urgent_qty: number
+  revision: number
+  updated_at: string | null
+  accuracy_available: boolean
+}
+export const getAlertPolicy = () => getWater<AlertPolicy>('alerts/policy')
+export const saveAlertPolicy = (
+  data: Pick<AlertPolicy, 'alert_qty' | 'days_per_barrel' | 'urgent_qty' | 'revision'>
+) => saveWater('alerts', { action: 'policy', ...data })
+
+export interface AlertFilters {
+  keyword: string
+  severity: '' | 'urgent' | 'normal' | 'healthy'
+  region: string
+  exhaust_date: string
+  scope: 'alerts' | 'all'
+}
+export interface AlertRow {
+  id: number
+  enterprise_id: number
+  enterprise_name: string
+  address: string
+  region: string
+  sku_id: number
+  sku_name: string
+  liters: number
+  full_qty: number
+  empty_qty: number
+  entitlement: number
+  frozen: number
+  available_entitlement: number
+  warehouse_available: number
+  threshold_qty: number
+  daily_liters: number | null
+  employee_count: number | null
+  estimated_daily_liters: number | null
+  estimated_daily_barrels: number | null
+  days_left: number | null
+  exhausts_at: string | null
+  device_count: number
+  delivery_id: number | null
+  delivery_status: string | null
+  severity: 'urgent' | 'normal' | 'healthy'
+  is_alert: boolean
+  can_create: boolean
+  blocked_reason: string
+  reasons: string[]
+  updated_at: string
+}
+export interface AlertPage extends Omit<WaterPage, 'data'> {
+  data: AlertRow[]
+}
+export interface AlertDashboard {
+  list: AlertPage
+  summary: {
+    accounts: number
+    alerts: number
+    urgent: number
+    normal: number
+    ordered: number
+    created_today: number
+    quantity_today: number
+    accuracy: number | null
+  }
+  regions: string[]
+  trend: { day: string; count: number; quantity: number }[]
+  trend_kind: 'delivery_created'
+  policy: AlertPolicy
+  updated_at: string
+}
+export const getAlertDashboard = (params: AlertFilters & { page: number; limit: number }) =>
+  getWater<AlertDashboard>('alerts/dashboard', params)
+export const getAlertAccounts = (params: { keyword: string; page: number; limit: number }) =>
+  getWater<AlertPage>('alerts', { ...params, scope: 'all' })
+export const alertSeverityLabels = { urgent: '紧急', normal: '普通', healthy: '正常' }
+export const alertDeliveryLabels: Record<string, string> = {
+  pending: '待派单',
+  assigned: '已派单',
+  transit: '配送中',
+  returning: '待退回核对',
+  returned: '已退回',
+  completed: '已签收',
+  cancelled: '已取消'
+}

+ 76 - 0
saiadmin-artd/src/api/water/content.ts

@@ -0,0 +1,76 @@
+import { getWater, type WaterPage } from './index'
+
+export type ContentAudience = 'all' | 'new' | 'purchased' | 'enterprise_admin'
+export type ContentStatus = 'active' | 'scheduled' | 'expired' | 'disabled'
+export interface ContentDraft {
+  id: number
+  title: string
+  image_url: string
+  target_url: string
+  audience: ContentAudience
+  region: string
+  starts_at: string | null
+  ends_at: string | null
+  sort: number
+  status: number
+}
+export interface ContentRow extends ContentDraft {
+  delivery_status: ContentStatus
+  status_label: string
+  created_at: string
+  updated_at: string
+}
+export interface ContentPackage {
+  id: number
+  name: string
+  sku_name: string
+  buckets: number
+  price_cents: number
+}
+export interface ContentFilters {
+  keyword: string
+  status: ContentStatus | ''
+}
+export interface ContentDashboard {
+  list: Omit<WaterPage, 'data'> & { data: ContentRow[] }
+  summary: Record<ContentStatus | 'total', number>
+  packages: ContentPackage[]
+  updated_at: string
+}
+export interface PreviewContext {
+  purchase_status: 'new' | 'purchased'
+  is_enterprise_admin: number
+  region: string
+}
+export interface ContentPreview {
+  banners: ContentRow[]
+  total: number
+  packages: ContentPackage[]
+  context: PreviewContext
+  updated_at: string
+}
+export const contentAudiences: Record<ContentAudience, string> = {
+  all: '全部用户',
+  new: '未首购企业',
+  purchased: '已购套餐企业',
+  enterprise_admin: '企业管理员'
+}
+export const contentStatuses: Record<ContentStatus, string> = {
+  active: '投放中',
+  scheduled: '待上线',
+  expired: '已结束',
+  disabled: '已停用'
+}
+export const contentStatusType = (status: ContentStatus) =>
+  ({ active: 'success', scheduled: 'warning', expired: 'info', disabled: 'info' })[status] as
+    | 'success'
+    | 'warning'
+    | 'info'
+export const contentPeriod = (row: ContentDraft) => {
+  if (!row.starts_at && !row.ends_at) return '长期'
+  return `${row.starts_at?.slice(0, 16) || '不限开始'} — ${row.ends_at?.slice(0, 16) || '长期有效'}`
+}
+export const getContentDashboard = (params: ContentFilters & { page: number; limit: number }) =>
+  getWater<ContentDashboard>('content/dashboard', params)
+export const getContentPreview = (params: PreviewContext) =>
+  getWater<ContentPreview>('content/preview', params)

+ 65 - 0
saiadmin-artd/src/api/water/devices.ts

@@ -0,0 +1,65 @@
+import request from '@/utils/http'
+import { getWater, type WaterPage } from './index'
+
+export type DeviceStatus = 'online' | 'offline' | 'fault' | 'unconnected'
+
+export interface DeviceFilters {
+  keyword: string
+  status: DeviceStatus | ''
+  region: string
+  firmware: string
+}
+
+export interface WaterDevice {
+  id: number
+  name: string
+  sn: string
+  enterprise_id: number
+  enterprise_name: string | null
+  region: string | null
+  sku_id: number
+  sku_name: string | null
+  firmware: string
+  connection_status: DeviceStatus
+  today_liters: number | null
+  last_seen: string | null
+  last_seen_seconds: number | null
+  fault: string
+}
+
+export interface DeviceSummary {
+  total: number
+  online: number
+  offline: number
+  fault: number
+  unconnected: number
+  new_month: number
+  offline_long: number
+  online_rate: number
+}
+
+export interface FirmwareVersion {
+  version: string
+  total: number
+}
+
+export interface DeviceDashboard {
+  summary: DeviceSummary
+  list: Omit<WaterPage, 'data'> & { data: WaterDevice[] }
+  options: { regions: string[]; firmwares: FirmwareVersion[] }
+  trend: { day: string; liters: number | null; reporting_devices: number }[]
+  anomalies: { label: string; total: number; percentage: number }[]
+  updated_at: string
+}
+
+export const getDeviceDashboard = (params: DeviceFilters & { page: number; limit: number }) =>
+  getWater<DeviceDashboard>('devices/dashboard', params)
+
+export const exportDevices = (filters: DeviceFilters) =>
+  request.post<Blob>({
+    url: '/core/water/devices/export',
+    data: filters,
+    responseType: 'blob',
+    timeout: 0,
+    showErrorMessage: false
+  })

+ 93 - 0
saiadmin-artd/src/api/water/enterprises.ts

@@ -0,0 +1,93 @@
+import request from '@/utils/http'
+import { getWater, type WaterPage, type WaterRow } from './index'
+
+export interface EnterpriseFilters {
+  keyword: string
+  status: string
+  agent_id: number | ''
+  balance: string
+}
+export interface Enterprise extends WaterRow {
+  employee_count: number | null
+  id: number
+  name: string
+  contact: string
+  phone: string
+  region: string
+  address: string
+  agent_id: number
+  agent_name: string | null
+  status: number
+  longitude: number | string | null
+  latitude: number | string | null
+  entitlement: number
+  frozen: number
+  available_entitlement: number
+  full_qty: number
+  empty_qty: number
+  device_count: number
+  online_count: number
+  unconnected_count: number
+  latest_order_at: string | null
+  latest_order_amount: number | null
+  replenishment_status: string
+}
+export interface EnterpriseAgent {
+  id: number
+  name: string
+  status: number
+}
+export interface EnterpriseDashboard {
+  list: Omit<WaterPage, 'data'> & { data: Enterprise[] }
+  summary: {
+    total: number
+    new_month: number
+    active: number
+    entitlement: number
+    repurchase_rate: number | null
+    repurchase_buyers: number
+    repeat_buyers: number
+    repurchase_change: number | null
+  }
+  agents: EnterpriseAgent[]
+  trend: { day: string; new: number; total: number }[]
+  funnel: { registered: number; purchased: number; repeated: number }
+  updated_at: string
+}
+export interface EnterpriseDetail {
+  enterprise: Enterprise
+  addresses: EnterpriseAddress[]
+  accounts: WaterRow[]
+  orders: WaterRow[]
+}
+export interface EnterpriseAddress {
+  id: number
+  label: string
+  contact: string
+  phone: string
+  region: string
+  address: string
+  longitude: number | null
+  latitude: number | null
+  is_default: boolean
+  status: number
+  updated_at: string
+}
+export const getEnterpriseDashboard = (
+  params: EnterpriseFilters & { page: number; limit: number }
+) => getWater<EnterpriseDashboard>('enterprises/dashboard', params)
+export const getEnterpriseDetail = (id: number) =>
+  getWater<EnterpriseDetail>('enterprises/detail', { id })
+export const exportEnterprises = (data: EnterpriseFilters) =>
+  request.post<Blob>({
+    url: '/core/water/enterprises/export',
+    data,
+    responseType: 'blob',
+    timeout: 0,
+    showErrorMessage: false
+  })
+export const batchEnterpriseStatus = (ids: number[], status: number) =>
+  request.post<{ changed: number }>({
+    url: '/core/water/enterprises/batch-status',
+    data: { ids, status }
+  })

+ 71 - 0
saiadmin-artd/src/api/water/finance.ts

@@ -0,0 +1,71 @@
+import request from '@/utils/http'
+import { getWater, type WaterPage } from './index'
+
+export interface FinanceFilters {
+  month: string
+  source: 'all' | 'regular' | 'demo'
+  keyword: string
+}
+export interface FinanceReceipt {
+  id: number
+  number: string
+  reference: string
+  enterprise_id: number
+  enterprise_name: string
+  package_name: string
+  buckets: number
+  amount_cents: number
+  created_at: string
+  is_demo: boolean
+  type_label: string
+  status_label: string
+}
+export interface FinanceDashboard {
+  list: Omit<WaterPage, 'data'> & { data: FinanceReceipt[] }
+  summary: {
+    today_cents: number
+    today_count: number
+    month_cents: number
+    month_count: number
+    demo_cents: number
+    demo_count: number
+    today_refund_cents: number | null
+    month_refund_cents: number | null
+    pending_withdrawal_cents: number | null
+    pending_commission_cents: number | null
+    net_rate: number | null
+  }
+  trend: { day: string; receipt_cents: number; count: number }[]
+  capabilities: { refunds: boolean; withdrawals: boolean; commissions: boolean; rewards: boolean }
+  withdrawal_demo: {
+    total: number
+    amount_cents: number
+    list: {
+      id: number
+      reference: string
+      applicant_name: string
+      kind_label: string
+      channel_label: string
+      amount_cents: number
+      created_at: string
+      is_demo: true
+      status_label: string
+    }[]
+  }
+  period: { month: string; start: string; end: string }
+  updated_at: string
+}
+export const financeMoney = (cents: number | null | undefined) =>
+  cents == null
+    ? '—'
+    : `¥${(cents / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
+export const getFinanceDashboard = (params: FinanceFilters & { page: number; limit: number }) =>
+  getWater<FinanceDashboard>('finance/dashboard', params)
+export const exportFinance = (data: FinanceFilters) =>
+  request.post<Blob>({
+    url: '/core/water/finance/export',
+    data,
+    responseType: 'blob',
+    timeout: 0,
+    showErrorMessage: false
+  })

+ 51 - 0
saiadmin-artd/src/api/water/index.ts

@@ -0,0 +1,51 @@
+import request from '@/utils/http'
+
+export type WaterRow = Record<string, any>
+export interface WaterPage {
+  data: WaterRow[]
+  total: number
+  current_page: number
+  per_page: number
+}
+export const getWater = <T = WaterPage>(module: string, params: WaterRow = {}) =>
+  request.get<T>({ url: `/core/water/${module}`, params })
+export const saveWater = (module: string, params: WaterRow) =>
+  request.post<WaterRow>({ url: `/core/water/${module}`, params })
+export const waterOptions = (module: string, type: string, keyword = '') =>
+  request.get<WaterRow[]>({ url: `/core/water/${module}/options`, params: { type, keyword } })
+
+export interface WaterInventoryDashboard {
+  summary: Record<string, number>
+  trend: { day: string; inbound: number; outbound: number }[]
+  list: WaterPage
+  updated_at: string
+}
+export const getWaterInventory = (params: { keyword: string; page: number; limit: number }) =>
+  getWater<WaterInventoryDashboard>('inventory/dashboard', params)
+export const exportWaterInventory = (keyword: string) =>
+  request.post<Blob>({
+    url: '/core/water/inventory/export',
+    data: { keyword },
+    responseType: 'blob',
+    timeout: 0
+  })
+
+export interface WaterLedgerFilters {
+  keyword: string
+  kind: string
+  start_date: string
+  end_date: string
+}
+export interface WaterLedgerPage extends WaterPage {
+  kinds: { value: string; label: string }[]
+}
+export const getWaterLedger = (params: WaterLedgerFilters & { page: number; limit: number }) =>
+  getWater<WaterLedgerPage>('ledger', params)
+export const exportWaterLedger = (data: WaterLedgerFilters) =>
+  request.post<Blob>({
+    url: '/core/water/ledger/export',
+    data,
+    responseType: 'blob',
+    timeout: 0,
+    showErrorMessage: false
+  })

+ 57 - 0
saiadmin-artd/src/api/water/miniIdentity.ts

@@ -0,0 +1,57 @@
+import request from '@/utils/http'
+
+/** 与 server 端 IdentityManagement 对齐;后端路径是 /core/water/* 而非 /api/mini/v1/*。 */
+
+export type WaterMiniRole =
+  | 'enterprise_admin'
+  | 'enterprise_member'
+  | 'driver'
+  | 'agent'
+
+export interface WaterMiniGrant {
+  id: number
+  role: WaterMiniRole
+  resource_id: number
+  status: number
+}
+
+export interface WaterMiniUser {
+  id: number
+  nickname: string
+  status: number
+  created_at: string
+  grants: WaterMiniGrant[]
+}
+
+export interface WaterMiniUserPage {
+  items: WaterMiniUser[]
+  total: number
+  page: number
+  page_size: number
+  has_more: boolean
+}
+
+export interface MiniIdentityFilters {
+  user_id?: number
+  page: number
+  page_size: number
+}
+
+export const listMiniUsers = (params: MiniIdentityFilters) =>
+  request.get<WaterMiniUserPage>({
+    url: '/core/water/mini-users',
+    params
+  })
+
+export interface GrantMiniIdentityInput {
+  user_id: number
+  role: WaterMiniRole
+  resource_id: number
+  status: 0 | 1
+}
+
+export const grantMiniIdentity = (data: GrantMiniIdentityInput) =>
+  request.post<{ id: number; status: number }>({
+    url: '/core/water/mini-grants',
+    data
+  })

+ 46 - 0
saiadmin-artd/src/api/water/promotion.ts

@@ -0,0 +1,46 @@
+import request from '@/utils/http'
+import { getWater } from './index'
+
+export interface PromotionPolicy {
+  initialized: boolean
+  enabled: boolean
+  sku_id: number | null
+  sku_name: string | null
+  quantity: number
+  min_amount_cents: number
+  revision: number
+  rules: string
+}
+export interface PromotionRecord {
+  id: number
+  inviter_name: string
+  enterprise_name: string
+  sku_name: string
+  quantity: number
+  status: 'bound' | 'pending' | 'issued' | 'rejected'
+  status_text: string
+  order_id: number | null
+  reference: string | null
+  amount_cents: number | null
+  created_at: string
+  qualified_at: string | null
+  reviewed_at: string | null
+  review_note: string
+  revision: number
+}
+export interface PromotionDashboard {
+  policy: PromotionPolicy
+  list: { data: PromotionRecord[]; total: number }
+  statuses: Record<string, string>
+}
+export const getPromotion = (params: { page: number; status: string }) =>
+  getWater<PromotionDashboard>('finance/promotion', params)
+export const savePromotionPolicy = (data: PromotionPolicy) =>
+  request.post<PromotionPolicy>({ url: '/core/water/finance/promotion/policy', data })
+export const reviewPromotion = (data: {
+  id: number
+  revision: number
+  decision: 'issued' | 'rejected'
+  receipt_refund_checked: boolean
+  note: string
+}) => request.post({ url: '/core/water/finance/promotion/review', data })

BIN
saiadmin-artd/src/assets/images/avatar/avatar.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar1.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar10.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar2.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar3.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar4.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar5.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar6.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar7.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar8.webp


BIN
saiadmin-artd/src/assets/images/avatar/avatar9.webp


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä