Commit cc0c84626d78cfb224bcff5e306227d147482778

Authored by reporkey
0 parents

chore: 初始化项目并完成 Plan 阶段 (A0~A5)

- A0 项目骨架:CLAUDE.md / docs 模板 / Git 初始化
- A1 范围锁定:项目概述 + 技术栈 + USR 模块 + 4 张 REQ 卡片 + config-vars.yaml
- A2 骨架生成:docs/04 架构规范 + scripts/*.mjs + tokens.css + .gitignore
- A3 DB 设计:docs/03(5 张表,schema SSoT)+ REQ 依赖表回填
- A4 DB 初始化:V1__initial_schema.sql(5 维校验通过,已 apply 到测试库)
- A5 下游文档:docs/02 开发计划 + docs/05 API 契约 + docs/08 模块/FE 清单
- prototype/erp.html 前端原型
.gitignore 0 → 100644
  1 +++ a/.gitignore
  1 +# ==== ERP 插件推荐忽略项(skeleton-gen 追加) ====
  2 +# 注:项目配置(含凭据)统一在 config-vars.yaml,随项目提交(内部 git 传播),不在此忽略。
  3 +
  4 +# Java / Maven
  5 +target/
  6 +*.class
  7 +
  8 +# Node / 前端构建产物
  9 +node_modules/
  10 +dist/
  11 +build/
  12 +coverage/
  13 +
  14 +# IDE
  15 +.idea/
  16 +.vscode/
  17 +*.iml
  18 +
  19 +# OS
  20 +.DS_Store
  21 +Thumbs.db
  22 +
  23 +# 日志
  24 +*.log
  25 +logs/
  26 +
  27 +# 插件运行时临时文件
  28 +.tmp/
  29 +*.raw
  30 +# ==== 结束 ====
... ...
CLAUDE.md 0 → 100644
  1 +++ a/CLAUDE.md
  1 +# CLAUDE.md — ERP项目 Claude Code 主指令文件
  2 +
  3 +> 本文件是 Claude Code 的"操作手册"。Claude Code 启动时会自动读取此文件。
  4 +
  5 +---
  6 +
  7 +## 🎯 项目概述
  8 +
  9 +- **项目名称**: 小羚羊
  10 +- **项目简述**: 测试ERP
  11 +- **目标用户**: 企业内部管理人员
  12 +- **部署方式**: 私有化部署
  13 +
  14 +---
  15 +
  16 +## 📐 编码行为约束
  17 +
  18 +### 你必须做的 ✅
  19 +
  20 +1. **严格遵循** `docs/04-技术规范.md`——命名 / 编码 / 统一响应 / 异常处理 / 数据访问 / 配置与安全 等项目专属技术规约全部在此
  21 +2. **严格遵循** `docs/04-技术规范.md § 1.2 分层结构 / § 2.1 目录约定`——文件放对位置
  22 +3. **每个后端接口** 必须先在 `docs/05-API接口契约.md` 定义,再编码实现
  23 +4. **每个功能可追溯到 `REQ-XXX-NNN`**——commit tag + 代码注释(如 `// REQ-SYS-001: 用户登录`)+ plan/spec 文件名均用此 tag
  24 +5. **遇到跨模块改动**(动到非当前模块的代码)——允许改,但必须在《模块完成报告》记录原因 / 影响评估(留痕)
  25 +
  26 +### 你禁止做的 🚫
  27 +
  28 +1. **主会话直接 `mysql -e` 跑业务 DDL**(只读查询 / 临时本地调试除外)——业务 schema 必须走 `sql/migrations/V_n__*.sql`,详见下方 Schema 演化规约
  29 +
  30 +### Schema 演化规约(Flyway migration)
  31 +
  32 +1. **文件命名**:`sql/migrations/V<n>__<snake_case_desc>.sql`,例:`V5__add_user_email_unique_index.sql`
  33 +2. **版本号分配**:建文件前 `ls sql/migrations/V*.sql` 查当前最大 n,新文件 `n_max + 1`
  34 +3. **Apply 方式**:Spring Boot 启动 / 测试启动时 Flyway 自动 apply(项目必须在 `pom.xml` 声明 `flyway-core` + `flyway-mysql` 依赖)。`scripts/setup-test-db.mjs` 只负责清空库,不做 apply
  35 +4. **已合并的 migration 永不修改**:发现错了写一个补救 migration(如 `V7__fix_V5_index_name.sql`),旧 `V_n.sql`
  36 +5. **临时调试 DDL**:临时在本地试字段/索引可手动 `mysql -e`,但不写 migration;下次 `setup-test-db.mjs` 会 drop+create 清掉
  37 +6. **A4 生成的 V1**:`V1__initial_schema.sql` 是 A 阶段由 `db-init` 从 `docs/03-数据库设计文档.md`(A3 正向设计的 schema SSoT)翻译生成的初始版本;后续 V2/V3/... 由 B 阶段每个 REQ 按需写入,**同时**反向同步更新 docs/03 对应表小节以保持 SSoT 一致
  38 +
  39 +---
  40 +
  41 +## 🗂️ Git 提交规范
  42 +
  43 +每次提交必须遵循以下格式:
  44 +
  45 +```
  46 +<type>(<scope>): <subject>
  47 +```
  48 +
  49 +- `scope`: 模块名,如 `user` / `inventory` / `order`
  50 +- `subject`: 简短描述;业务类(feat / fix / test)必须带 `REQ-XXX-NNN` 后缀
  51 +
  52 +`type` 含义:
  53 +
  54 +| type | 看到它意味着 |
  55 +|-----|-------------|
  56 +| `feat` | **新能力上线**——用户多了一个功能、接口、页面或业务规则 |
  57 +| `fix` | **修 bug**——原来行为错了,这次改对 |
  58 +| `refactor` | **重构**——外部行为不变,只改代码结构 / 命名 / 抽象 |
  59 +| `docs` | **文档改动**——只动 Markdown / 代码注释,不动实现 |
  60 +| `style` | **格式调整**——空白 / 缩进 / import 顺序,逻辑 0 变化 |
  61 +| `test` | **只动测试代码**——补用例 / 修 fixture,不碰实现 |
  62 +| `chore` | **流程维护**——构建 / 依赖 / 工具 / 证据档案 / 里程碑元数据等非业务动作 |
... ...
config-vars.yaml 0 → 100644
  1 +++ a/config-vars.yaml
  1 +# config-vars.yaml — 项目全部配置(含敏感凭据)。随项目提交,内部 git 传播。
  2 +# 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析此文件。
  3 +# 值含 : / # / 空格 / $ / 引号等特殊字符时,用单引号包裹整个值:password: 'p@ss: w0rd#1'
  4 +# 所有配置值(含敏感值)只在本文件,不得散落到 docs / 源码 / 日志。
  5 +# base_package / 命名空间锁定后全项目复用,不各模块各写。
  6 +
  7 +backend:
  8 + base_package: com.xly.erp
  9 + http_port: 5172
  10 +
  11 +frontend:
  12 + pkg_name: xly-erp-web
  13 + dev_port: 5173
  14 +
  15 +database:
  16 + host: 118.178.19.35
  17 + port: 3318
  18 + user: xlyprint
  19 + password: xlyXLYprint2016
  20 + schema: xlyweberp_vibe_erp_test
  21 +
  22 +admin_init:
  23 + username: admin
  24 + password: 666666
  25 +
  26 +secrets:
  27 + jwt_secret: a3b7e8f1c4d6029e5b8f37a1c9d2e4068b5f1d3a7c0e9b2f48d6a1c5e7f9b3d2
  28 + # 项目专属凭据按需取消注释 / 追加,直接填真实值:
  29 + # redis_password: 【人工填写:Redis 密码(用 Redis 时)】
  30 + # oss_access_key_secret: 【人工填写:对象存储密钥】
  31 + # sms_api_secret: 【人工填写:短信网关密钥】
... ...
docs/01-需求清单/USR-用户管理/REQ-USR-001.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-001.md
  1 +### REQ-USR-001 增加用户
  2 +
  3 +**目标**: 用户在后台新建用户账号,指定用户名、密码及角色,账号立即生效可用
  4 +
  5 +- **输入**:
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  11 + | 创建时间 | 日期时间 | — | 系统生成 | — | 页面加载时 | 当前日期 | 保存后自动生成;只读 |
  12 + | 制单人 | 文本 | — | 系统生成 | — | 页面加载时 | 当前登录用户 | 保存后自动生成;只读 |
  13 + | 员工名 | 文本 | 否 | 下拉单选 | `职员表` | 用户操作时 | — | 关联职员(可选) |
  14 + | 用户号 | 文本 | 是 | 手工输入 | — | 用户操作时 | — | 关联职员选择后自动输入员工姓名 |
  15 + | 用户名 | 文本 | 是 | 手工输入 | — | 用户操作时 | — | 关联职员选择后自动输入员工姓名 |
  16 + | 类型 | 文本 | 是 | 下拉单选 | 普通用户/超级管理员 | 页面加载时 | 普通用户 | — |
  17 + | 语言 | 文本 | 是 | 下拉单选 | 中文/英文/繁体 | 页面加载时 | — | — |
  18 + | 单据修改权限 | 布尔 | 否 | 复选框 | — | — | 否 | — |
  19 + | 密码 | 文本 | — | 系统生成 | 不显示 | — | 666666 | 保存后自动设为初始化 |
  20 +
  21 + - **表2** - 权限组:
  22 +
  23 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  24 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  25 + | 复选框 | 布尔 | 否 | 复选框 | — | — | 否 | 是否选择当前行权限 |
  26 + | 权限分类 | 文本 | — | — | — | 页面加载时 | — | — |
  27 +
  28 +
  29 +- **输出**:
  30 +
  31 + - **表1**:
  32 +
  33 + | 字段 | 类型 | 显示来源 |
  34 + | --- | --- | --- |
  35 + | 用户号 | 文本 | — |
  36 +
  37 +- **跨字段规则**: 用户名在系统内全局唯一;角色取值受系统配置约束
  38 +- **边界**: 密码以哈希形式存储
  39 +- **验收**: 提交合法数据后用户记录出现在列表;重复用户名返回错误提示;普通账号无权访问此功能
  40 +- **依赖表**: `usr_user`(写)、`usr_employee`(读,员工名下拉)、`usr_permission` + `usr_user_permission`(权限组授权)
  41 +- **依赖接口**: 无(本 REQ 提供 `POST /api/usr/users`;员工名/权限/类型下拉为基础数据读取,无上游 REQ 接口依赖)
... ...
docs/01-需求清单/USR-用户管理/REQ-USR-002.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-002.md
  1 +### REQ-USR-002 修改用户
  2 +
  3 +**目标**: 用户可更新已有用户的基本信息(姓名、角色、状态等),修改实时生效
  4 +
  5 +- **输入**: 选中目标
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  11 + | 创建时间 | 日期时间 | — | 系统生成 | — | 页面加载时 | 原值 | 保存后自动生成;只读 |
  12 + | 制单人 | 文本 | — | 系统生成 | — | 页面加载时 | 原值 | 保存后自动生成;只读 |
  13 + | 员工名 | 文本 | 否 | 下拉单选 | `职员表` | 页面加载时 | 原值 | 关联职员(可选) |
  14 + | 用户号 | 文本 | 是 | 手工输入 | — | 页面加载时 | 原值 | 关联职员选择后自动输入员工姓名 |
  15 + | 用户名 | 文本 | 是 | 手工输入 | — | 页面加载时 | 原值 | 关联职员选择后自动输入员工姓名 |
  16 + | 类型 | 文本 | 是 | 下拉单选 | 普通用户/超级管理员 | 页面加载时 | 原值 | — |
  17 + | 语言 | 文本 | 是 | 下拉单选 | 中文/英文/繁体 | 页面加载时 | 原值 | — |
  18 + | 单据修改权限 | 布尔 | 否 | 复选框 | — | 页面加载时 | 原值 | — |
  19 + | 密码 | 文本 | — | 系统生成 | 不显示 | 页面加载时 | 原值 | 保存后自动设为初始化 |
  20 +
  21 + - **表2** - 权限组:
  22 +
  23 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  24 + | -------- | ---- | --- | ---- | ----------------- | --------- | --------- | ------------------- |
  25 + | 复选框 | 布尔 | 否 | 复选框 | — | 页面加载时 | 原值 | 是否选择当前行的权限 |
  26 + | 权限分类 | 文本 | — | — | — | 页面加载时 | — | — |
  27 +
  28 +- **输出**:
  29 +
  30 + - **表1**:
  31 +
  32 + | 字段 | 类型 | 显示来源 |
  33 + | --- | --- | --- |
  34 + | 用户 id | 文本 | `职员表` |
  35 +
  36 +- **跨字段规则**: 密码不在该接口修改;角色变更需具备相应权限
  37 +- **边界**: 必须传入有效用户 id;字段格式与新增一致
  38 +- **验收**: 修改角色或状态后立即反映在用户列表;被禁用账号无法登录并收到明确提示
  39 +- **依赖表**: `usr_user`(写)、`usr_employee`(读,员工名下拉)、`usr_permission` + `usr_user_permission`(权限组授权)
  40 +- **依赖接口**: 无(本 REQ 提供 `PUT /api/usr/users/{id}`;编辑前的用户详情可由 REQ-USR-003 查询接口提供,非强依赖)
... ...
docs/01-需求清单/USR-用户管理/REQ-USR-003.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-003.md
  1 +### REQ-USR-003 查询用户
  2 +
  3 +**目标**: 用户可按用户名、角色或状态筛选并分页浏览用户列表
  4 +
  5 +- **输入**:
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | ---- | ---- | --- | ---- | ----------------------------------------------- | ----- | ------- | --------------- |
  11 + | 查询字段 | 文本 | 否 | 下拉单选 | 用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人 | 页面加载时 | 用户名 | — |
  12 + | 匹配方式 | 文本 | 否 | 下拉单选 | 包含/不包含/等于 | 页面加载时 | 包含 | — |
  13 + | 查询值 | 文本 | 否 | 手工输入 | — | — | — | 与「查询字段」配合使用,空为选择全部 |
  14 +
  15 +- **输出**:
  16 +
  17 + - **表1**:
  18 +
  19 + | 字段 | 类型 | 显示来源 |
  20 + | ---- | ---- | ----- |
  21 + | 序号 | 数字 | 系统生成 |
  22 + | 用户名 | 文本 | `用户表` |
  23 + | 员工名 | 文本 | `职员表` |
  24 + | 用户号 | 文本 | `用户表` |
  25 + | 部门 | 文本 | `职员表` |
  26 + | 用户类型 | 文本 | `用户表` |
  27 + | 语言 | 文本 | `用户表` |
  28 + | 作废 | 布尔 | `用户表` |
  29 + | 登录日期 | 日期时间 | `用户表` |
  30 + | 制单人 | 文本 | `用户表` |
  31 + | 制单日期 | 日期时间 | `用户表` |
  32 +
  33 +- **跨字段规则**: -
  34 +- **边界**: 单页最大条数受限(默认 100);密码与敏感字段不返回;查询为只读,不产生写副作用
  35 +- **验收**: 按条件筛选返回正确结果集;无匹配时返回空列表而非报错;分页参数越界时返回最后一页
  36 +- **依赖表**: `usr_user`(读)、`usr_employee`(读,员工名 / 部门关联)
  37 +- **依赖接口**: 无(本 REQ 提供 `GET /api/usr/users`;无上游 REQ 接口依赖)
... ...
docs/01-需求清单/USR-用户管理/REQ-USR-004.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/REQ-USR-004.md
  1 +### REQ-USR-004 登录用户
  2 +
  3 +**目标**: 用户通过用户名+密码完成身份认证,获取 JWT Token 用于后续接口鉴权
  4 +
  5 +- **输入**:
  6 +
  7 + - **表1**:
  8 +
  9 + | 字段 | 类型 | 必填 | 输入方式 | 显示来源 | 预加载 | 默认值 | 业务规则 |
  10 + | --- | ---- | --- | ---- | ------- | ----- | --- | ----------- |
  11 + | 用户名 | 文本 | 是 | 手工输入 | — | — | — | — |
  12 + | 密码 | 文本 | 是 | 手工输入 | — | — | — | 输入显示星号 |
  13 + | 版本 | 文本 | 是 | 下拉单选 | `公司表` | 页面加载时 | — | |
  14 +
  15 +- **输出**: 成功/失败
  16 +
  17 +- **跨字段规则**: 账号密码匹配且用户处于启用状态才允许登录;连续登录失败需有锁定或限流策略;登录成功后签发访问令牌。
  18 +- **边界**: 已禁用或已删除用户禁止登录;密码错误时不区分「账号不存在/密码错误」以防账号枚举;令牌须设置过期时间。
  19 +- **验收**: 正确凭证登录成功并返回令牌;错误凭证返回统一失败提示;禁用用户登录请求被拒绝。
  20 +- **依赖表**: `usr_user`(读,认证)、`usr_company`(读,登录「版本」下拉)
  21 +- **依赖接口**: 无(本 REQ 提供 `POST /api/usr/login`;登录"版本"下拉数据来自 `usr_company` 基础数据读取,无上游 REQ 接口依赖)
... ...
docs/01-需求清单/USR-用户管理/_module.md 0 → 100644
  1 +++ a/docs/01-需求清单/USR-用户管理/_module.md
  1 +# USR-用户管理
  2 +
  3 +- **模块简述**: 用户管理模块,负责系统用户的增加、修改、查询,以及用户登录认证。
  4 +- **依赖模块**: 无(USR 为本项目唯一模块,无跨模块依赖)
  5 +- **涉及表**: `usr_user`、`usr_employee`、`usr_company`、`usr_permission`、`usr_user_permission`
... ...
docs/01-需求清单/index.md 0 → 100644
  1 +++ a/docs/01-需求清单/index.md
  1 +# 需求清单
  2 +
  3 +> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `REQ-XXX-NNN.md`(每张 REQ 卡片一个文件)。下方核心功能点供 CC 拆分出 REQ 编号 + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。
  4 +
  5 +## 模块索引
  6 +
  7 +| 模块代码 | 模块名称 | 核心功能点(简要) |
  8 +| ---- | ---- | ------------------- |
  9 +| USR | 用户管理 | 增加用户,修改用户,查询用户,登录用户 |
  10 +
... ...
docs/02-开发计划.md 0 → 100644
  1 +++ a/docs/02-开发计划.md
  1 +# 02-开发计划
  2 +
  3 +## 一、模块依赖表
  4 +
  5 +| 模块 ID | 模块名 | 依赖模块 | 依赖表 |
  6 +|---|---|---|---|
  7 +| USR | 用户管理 | 无 | usr_user, usr_employee, usr_company, usr_permission, usr_user_permission |
  8 +
  9 +## 二、开发顺序清单(CC 分发权威)
  10 +
  11 +> Coding 阶段按本表行序分发;约束:同一模块所有 REQ 必须连续排列。
  12 +
  13 +| # | REQ | 所属模块 | 选中理由 | 备注 |
  14 +|---|-----|---------|---------|------|
  15 +| 1 | **REQ-USR-001** | USR | 模块基础,建立用户写入能力(建表后第一个落地的写接口,无前置 REQ 依赖) | — |
  16 +| 2 | **REQ-USR-002** | USR | 依赖用户已存在(REQ-USR-001 增加用户) | — |
  17 +| 3 | **REQ-USR-003** | USR | 依赖用户数据(REQ-USR-001),提供列表/详情检索 | — |
  18 +| 4 | **REQ-USR-004** | USR | 依赖用户表与认证数据(REQ-USR-001),实现登录签发 JWT | — |
... ...
docs/03-数据库设计文档.md 0 → 100644
  1 +++ a/docs/03-数据库设计文档.md
  1 +# 03-数据库设计文档
  2 +
  3 +- **Schema**: `xlyweberp_vibe_erp_test`
  4 +- **Migration 清单**: `sql/migrations/V*.sql`(由 Flyway 顺序 apply)
  5 +- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单/<module>/REQ-*.md` REQ 卡片正向设计生成(schema SSoT)。
  6 +
  7 +## 项目标准列约定
  8 +
  9 +下文每张业务表的字段清单都自动包含以下 5 个标准列(匈牙利前缀 `i` int / `s` varchar / `t` datetime)。渲染时由 `docs-03-table-template.md` 模板内置原样输出。
  10 +
  11 +| 列名 | 类型 | 可空 | 主键 | 说明 |
  12 +|---|---|---|---|---|
  13 +| `iIncrement` | int | 否 | 是 | 整数主键 ID(自增方式由实现决定:DB `AUTO_INCREMENT` 或应用 / 触发器分配) |
  14 +| `sId` | varchar(100) | 是 | — | 业务 ID(对外暴露的字符串标识,如 UUID / 人类可读编号) |
  15 +| `sBrandsId` | varchar(100) | 是 | — | 品牌 ID(多租户隔离) |
  16 +| `sSubsidiaryId` | varchar(100) | 是 | — | 子公司 ID(组织层级隔离) |
  17 +| `tCreateDate` | datetime | 否 | — | 记录创建时间 |
  18 +
  19 +字典 / 辅助表如有豁免,在该表业务注记里注明豁免原因。
  20 +
  21 +## ER 关系概览
  22 +
  23 +本库围绕「用户管理(USR)」单模块设计,核心实体为用户表 `usr_user`,辅以 4 张支撑/关联表:
  24 +
  25 +- `usr_user`(用户)—— 核心表。承载登录账号、密码、用户类型、语言、单据修改权限、作废标志、最后登录时间等。
  26 +- `usr_employee`(职员)—— 支撑表。提供员工名 / 员工编号 / 部门;`usr_user.iEmployeeId` 可选外键关联(N:1,一个职员至多对应一个登录用户)。
  27 +- `usr_company`(公司 / 版本)—— 支撑表。登录页「版本」下拉的数据来源;当前仅供登录时选择,不与 `usr_user` 建强外键。
  28 +- `usr_permission`(权限)—— 支撑表。定义可分配的权限项,按「权限分类」组织。
  29 +- `usr_user_permission`(用户权限)—— 关联表。`usr_user` 与 `usr_permission` 的多对多授权关系(对应新增 / 修改用户界面的「权限组」勾选)。
  30 +
  31 +关系:
  32 +
  33 +```
  34 +usr_user N:1 usr_employee (ON DELETE SET NULL)
  35 +usr_user N:M usr_permission 经 usr_user_permission(两侧 ON DELETE CASCADE)
  36 +usr_company 独立支撑表(登录时选择,无外键)
  37 +```
  38 +
  39 +## 表清单
  40 +
  41 +- `usr_user` — 用户表:登录账号与用户属性核心表
  42 +- `usr_employee` — 职员表:员工名 / 部门等支撑信息
  43 +- `usr_company` — 公司表:登录「版本」下拉数据来源
  44 +- `usr_permission` — 权限表:可分配权限项定义
  45 +- `usr_user_permission` — 用户权限关联表:用户↔权限多对多授权
  46 +
  47 +---
  48 +
  49 +## `usr_user` — 用户表:登录账号与用户属性核心表
  50 +
  51 +### 字段
  52 +
  53 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  54 +|---|---|---|---|---|
  55 +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) |
  56 +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) |
  57 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) |
  58 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) |
  59 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列,对应「制单日期」) |
  60 +| `sUserName` | varchar(50) | 否 | — | 用户名,登录账号,系统内全局唯一(3-20 位字母数字下划线) |
  61 +| `sUserNo` | varchar(50) | 是 | — | 用户号,关联职员后可自动带出员工编号 / 姓名 |
  62 +| `sPassword` | varchar(100) | 否 | — | 登录密码,BCrypt 哈希存储(初始密码 666666) |
  63 +| `iEmployeeId` | int | 是 | — | 关联职员 ID(可选),外键 → `usr_employee.iIncrement` |
  64 +| `sUserType` | varchar(20) | 否 | `普通用户` | 用户类型:普通用户 / 超级管理员 |
  65 +| `sLanguage` | varchar(20) | 否 | `中文` | 界面语言:中文 / 英文 / 繁体 【人工填写:需用户审阅】默认值与取值范围待确认 |
  66 +| `iCanModifyBill` | tinyint(1) | 否 | `0` | 单据修改权限:0 否 / 1 是 |
  67 +| `iIsVoid` | tinyint(1) | 否 | `0` | 作废 / 禁用标志:0 正常 / 1 已作废(禁用后不可登录) |
  68 +| `tLastLoginDate` | datetime | 是 | — | 最后登录时间,登录成功时更新 |
  69 +| `sCreator` | varchar(50) | 是 | — | 制单人(创建该用户的操作员) |
  70 +
  71 +### 索引
  72 +
  73 +- `uk_usr_user_username` (UNIQUE): sUserName
  74 +- `idx_usr_user_employee` (INDEX): iEmployeeId
  75 +- `idx_usr_user_type` (INDEX): sUserType
  76 +- `idx_usr_user_tenant` (INDEX): sBrandsId, sSubsidiaryId
  77 +
  78 +### 外键
  79 +
  80 +- `fk_usr_user_employee`: iEmployeeId → usr_employee.iIncrement (SET NULL)
  81 +
  82 +### 业务注记
  83 +
  84 +用户表为本模块核心实体,承载登录认证(用户名 + 密码)与用户类型 / 语言 / 单据修改权限等属性。`sUserName` 全局唯一;`sPassword` 以 BCrypt 哈希存储,初始为 666666;`iIsVoid=1` 表示禁用,禁止登录。可选关联职员(`iEmployeeId`,职员删除时置空)以带出员工名 / 部门。登录令牌 JWT 为无状态,不落库。查询接口(REQ-USR-003)按用户名 / 类型 / 作废 / 登录日期 / 制单人等条件检索,密码字段不返回。
  85 +
  86 +---
  87 +
  88 +## `usr_employee` — 职员表:员工名 / 部门等支撑信息
  89 +
  90 +### 字段
  91 +
  92 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  93 +|---|---|---|---|---|
  94 +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) |
  95 +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) |
  96 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) |
  97 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) |
  98 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  99 +| `sEmployeeName` | varchar(50) | 否 | — | 职员 / 员工姓名(用户「员工名」下拉来源) |
  100 +| `sEmployeeNo` | varchar(50) | 是 | — | 员工编号 |
  101 +| `sDepartment` | varchar(100) | 是 | — | 所属部门(用户查询输出「部门」来源) |
  102 +
  103 +### 索引
  104 +
  105 +- `idx_usr_employee_name` (INDEX): sEmployeeName
  106 +- `idx_usr_employee_tenant` (INDEX): sBrandsId, sSubsidiaryId
  107 +
  108 +### 外键
  109 +
  110 +(无)
  111 +
  112 +### 业务注记
  113 +
  114 +职员表为用户的关联支撑表,提供员工名、员工编号、部门信息。用户新增 / 修改时通过「员工名」下拉选择职员,用户查询 / 展示时按 `usr_user.iEmployeeId` 关联取员工名与部门。可作为字典型支撑数据维护。
  115 +
  116 +---
  117 +
  118 +## `usr_company` — 公司表:登录「版本」下拉数据来源
  119 +
  120 +### 字段
  121 +
  122 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  123 +|---|---|---|---|---|
  124 +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) |
  125 +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) |
  126 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) |
  127 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) |
  128 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  129 +| `sCompanyName` | varchar(100) | 否 | — | 公司名称(登录页「版本」下拉的显示来源) |
  130 +| `sVersion` | varchar(50) | 是 | — | 版本 / 账套标识 【人工填写:需用户审阅】"版本"语义(账套 / 数据版本)待确认 |
  131 +
  132 +### 索引
  133 +
  134 +- `uk_usr_company_name` (UNIQUE): sCompanyName
  135 +
  136 +### 外键
  137 +
  138 +(无)
  139 +
  140 +### 业务注记
  141 +
  142 +公司表为登录页「版本」下拉的数据来源(REQ-USR-004),每行代表一个可登录的公司 / 账套。当前仅用于登录时选择,不与用户表建立强外键关系。
  143 +
  144 +---
  145 +
  146 +## `usr_permission` — 权限表:可分配权限项定义
  147 +
  148 +### 字段
  149 +
  150 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  151 +|---|---|---|---|---|
  152 +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) |
  153 +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列) |
  154 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) |
  155 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) |
  156 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  157 +| `sPermissionName` | varchar(100) | 否 | — | 权限名称 |
  158 +| `sPermissionCode` | varchar(100) | 否 | — | 权限编码(程序判定用,系统内唯一) |
  159 +| `sPermissionCategory` | varchar(100) | 是 | — | 权限分类(新增 / 修改用户界面「权限组」的"权限分类") |
  160 +
  161 +### 索引
  162 +
  163 +- `uk_usr_permission_code` (UNIQUE): sPermissionCode
  164 +- `idx_usr_permission_category` (INDEX): sPermissionCategory
  165 +
  166 +### 外键
  167 +
  168 +(无)
  169 +
  170 +### 业务注记
  171 +
  172 +权限表定义可分配的权限项,按「权限分类」组织(对应新增 / 修改用户界面的「权限组」网格)。`sPermissionCode` 全局唯一供程序判定。【人工填写:需用户审阅】权限粒度(按分类 / 按具体功能点)待确认。
  173 +
  174 +---
  175 +
  176 +## `usr_user_permission` — 用户权限关联表:用户↔权限多对多授权
  177 +
  178 +### 字段
  179 +
  180 +| 字段 | 类型 | Nullable | 默认 | 业务含义 |
  181 +|---|---|---|---|---|
  182 +| `iIncrement` | int | 否 | — | 整数主键 ID(标准列) |
  183 +| `sId` | varchar(100) | 是 | — | 业务 ID(标准列;关联表对外不暴露,可留空) |
  184 +| `sBrandsId` | varchar(100) | 是 | `1111111111` | 品牌 ID,多租户隔离(标准列) |
  185 +| `sSubsidiaryId` | varchar(100) | 是 | `1111111111` | 子公司 ID,组织层级隔离(标准列) |
  186 +| `tCreateDate` | datetime | 否 | 当前时间 | 创建时间(标准列) |
  187 +| `iUserId` | int | 否 | — | 用户 ID,外键 → `usr_user.iIncrement` |
  188 +| `iPermissionId` | int | 否 | — | 权限 ID,外键 → `usr_permission.iIncrement` |
  189 +
  190 +### 索引
  191 +
  192 +- `uk_usr_user_permission` (UNIQUE): iUserId, iPermissionId
  193 +- `idx_usr_user_permission_perm` (INDEX): iPermissionId
  194 +
  195 +### 外键
  196 +
  197 +- `fk_usr_up_user`: iUserId → usr_user.iIncrement (CASCADE)
  198 +- `fk_usr_up_permission`: iPermissionId → usr_permission.iIncrement (CASCADE)
  199 +
  200 +### 业务注记
  201 +
  202 +用户↔权限多对多关联表,记录每个用户被授予的权限(对应「权限组」勾选)。`(iUserId, iPermissionId)` 唯一防重复授权;删除用户或权限时级联清除对应授权记录。该表为纯关联表,`sId` 业务 ID 不对外暴露(标准列仍保留以保持结构一致)。
... ...
docs/04-技术规范.md 0 → 100644
  1 +++ a/docs/04-技术规范.md
  1 +# 04-技术规范
  2 +
  3 +## 零、技术栈总览
  4 +
  5 +| 分层模块 | 技术 | 版本要求 | 说明 |
  6 +|---|---|---|---|
  7 +| 前端基础框架 | React | 18.x | 构建前端应用 |
  8 +| 前端 UI 组件 | Ant Design | 5.x | 页面组件与交互控件 |
  9 +| 前端状态管理 | Redux Toolkit | 最新稳定版 | 管理全局状态 |
  10 +| 前端路由管理 | React Router | v6 | 页面路由与导航 |
  11 +| 前端工程化构建 | Vite | 最新稳定版 | 前端开发与打包构建 |
  12 +| 前端接口通信 | Axios | 最新稳定版 | 调用后端 API |
  13 +| 后端基础框架 | Spring Boot | 3.x | 构建后端服务 |
  14 +| 后端数据访问 | MyBatis-Plus | 最新稳定版 | 数据库访问与 ORM 增强 |
  15 +| 工作流引擎 | Activiti | 6.x | 审批流、流程流转 |
  16 +| 缓存服务 | Redis | 最新稳定版 | 缓存、会话、分布式能力 |
  17 +| 报表打印 | JXLS | 2.8.1 | 基于 Excel 模板生成报表 |
  18 +| Excel 导入导出 | EasyExcel | 4.0.3 | Excel 数据导入导出 |
  19 +| 关系型数据库 | MySQL | 8.x | 核心业务数据存储 |
  20 +| 数据库 schema 迁移 | Flyway (`flyway-core` + `flyway-mysql`) | 10.x / 最新稳定版 | `sql/migrations/V_n__*.sql` 顺序 apply;Spring Boot 启动时自动应用 |
  21 +| 接口风格 | RESTful API | 统一规范 | 前后端接口设计规范 |
  22 +| 权限认证 | Spring Security / JWT | 最新稳定版 | 登录认证、权限控制 |
  23 +| API 文档 | OpenAPI / Swagger | 最新稳定版 | 接口文档与调试 |
  24 +| 项目构建管理 | Maven | 3.9.x | Java 项目依赖与构建 |
  25 +| JDK 运行环境 | Java | 17 / 21 | Spring Boot 3 推荐版本 |
  26 +| 部署容器 | Docker | 最新稳定版 | 容器化部署 |
  27 +| Web 服务器 / 反向代理 | Nginx | 最新稳定版 | 前端托管、反向代理、负载分发 |
  28 +| 日志管理 | Logback | 默认集成 / 最新稳定版 | 应用日志输出 |
  29 +| 对象映射工具 | MapStruct | 最新稳定版 | DTO / VO / Entity 转换 |
  30 +| 工具类库 | Hutool / Apache Commons | 最新稳定版 | 常用工具方法支持 |
  31 +
  32 +> 本表由 scope-lock 锁定。后续所有规范基于此表推导。
  33 +
  34 +### 命令清单
  35 +
  36 +> 由 scope-lock(A1) 锁定。Coding 阶段 `coding.mjs` 的 tdd / test-gate 按 stack 读取以下命令。`无` 表示该栈不提供此类命令。
  37 +
  38 +**后端(Spring Boot 3 / Maven / Java 17)**
  39 +
  40 +| 类别 | 命令 |
  41 +|---|---|
  42 +| build | `mvn -q -B -DskipTests package` |
  43 +| lint | `mvn -q -B checkstyle:check` |
  44 +| unit | `mvn -q -B test` |
  45 +| e2e | 无 |
  46 +
  47 +**前端(React / Vite / npm)**
  48 +
  49 +| 类别 | 命令 |
  50 +|---|---|
  51 +| build | `npm run build` |
  52 +| lint | `npm run lint` |
  53 +| unit | `npm run test:unit` |
  54 +| e2e | `npm run test:e2e` |
  55 +
  56 +---
  57 +
  58 +## 一、后端规范
  59 +
  60 +> 技术栈:Spring Boot 3 + MyBatis-Plus + MySQL 8 + Flyway + Spring Security/JWT,根包 `com.xly.erp`,构建工具 Maven。
  61 +
  62 +### 1.1 规则
  63 +
  64 +- 所有后端代码位于仓库根 `backend/` 子项目;根包统一为 `com.xly.erp`,禁止散落到其他包名。
  65 +- 业务代码按模块组织在 `com.xly.erp.modules.<模块小写代码>` 下(如 `modules.usr`),通用能力在 `com.xly.erp.common` 下,不得跨模块直接引用对方的 `mapper`/`entity`。
  66 +- 每个对外接口必须先在 `docs/05-API接口契约.md` 定义,再编码实现;Controller 只做参数校验 + 调 Service,不写业务逻辑。
  67 +- 业务 schema 变更一律走 `sql/migrations/V_n__*.sql`(Flyway),禁止在代码或会话里手跑业务 DDL。
  68 +- 密码等敏感值只从 `config-vars.yaml` / 环境读取,禁止硬编码进源码或日志。
  69 +
  70 +### 1.2 分层结构
  71 +
  72 +后端为仓库根下的 `backend/` Maven 子项目,目录布局:
  73 +
  74 +```
  75 +backend/
  76 +├── pom.xml # 声明 spring-boot / mybatis-plus / flyway-core / flyway-mysql / spring-security / jjwt / mapstruct / hutool 等依赖
  77 +├── src/main/java/com/xly/erp/
  78 +│ ├── ErpApplication.java # Spring Boot 启动类
  79 +│ ├── common/ # 跨模块通用能力(不属于任何业务模块)
  80 +│ │ ├── response/ # Result<T> 统一响应体、ResultCode 枚举、PageResult<T>
  81 +│ │ ├── exception/ # BusinessException、GlobalExceptionHandler(@RestControllerAdvice)
  82 +│ │ ├── config/ # MybatisPlusConfig / SecurityConfig / SwaggerConfig / CorsConfig
  83 +│ │ ├── security/ # JWT 工具、JwtAuthenticationFilter、UserDetails 适配
  84 +│ │ └── base/ # BaseEntity(id/创建时间/制单人/逻辑删除等公共字段)
  85 +│ └── modules/
  86 +│ └── usr/ # USR 用户管理(每个业务模块一个子包)
  87 +│ ├── controller/ # UsrUserController —— 仅校验 + 委派
  88 +│ ├── service/ # UsrUserService 接口
  89 +│ │ └── impl/ # UsrUserServiceImpl 业务实现
  90 +│ ├── mapper/ # UsrUserMapper(继承 MyBatis-Plus BaseMapper)
  91 +│ ├── entity/ # UsrUser 实体(映射数据库表)
  92 +│ ├── dto/ # 入参对象(CreateUserDTO / UpdateUserDTO / UserQueryDTO / LoginDTO)
  93 +│ └── vo/ # 出参对象(UserVO / LoginVO)
  94 +├── src/main/resources/
  95 +│ ├── application.yml # 端口、数据源、MyBatis-Plus、Flyway locations、JWT 等配置
  96 +│ └── mapper/ # 复杂 SQL 的 MyBatis XML(简单 CRUD 用注解/MP 内置)
  97 +└── src/test/java/com/xly/erp/ # 单元测试 + 集成测试,包结构镜像主代码
  98 +```
  99 +
  100 +- **跨模块判定**:路径 `backend/src/main/java/com/xly/erp/modules/<mod>/**` 归属模块 `<mod>`;`common/**` 为公共区,改动需在《模块完成报告》留痕。
  101 +- **Flyway**:迁移脚本在仓库根 `sql/migrations/`,`application.yml` 配置 `spring.flyway.locations=filesystem:../sql/migrations`(相对 `backend/` 工作目录),Spring Boot 启动时自动 apply。
  102 +
  103 +### 1.3 命名约定
  104 +
  105 +- 类:大驼峰,模块前缀 + 业务名 + 层后缀,如 `UsrUserController` / `UsrUserServiceImpl` / `UsrUserMapper`。
  106 +- 方法:小驼峰动词起头,如 `createUser` / `updateUser` / `pageUsers` / `login`。
  107 +- 表名:`snake_case` 单数或业务习惯命名(详见 docs/03);实体类名大驼峰对应表名。
  108 +- 常量:全大写下划线;DTO/VO 字段小驼峰。
  109 +- REST 路径:`/api/<模块>/<资源>`,小写中划线,如 `/api/usr/users`。
  110 +
  111 +### 1.4 统一响应格式
  112 +
  113 +所有接口返回统一包装 `Result<T>`:
  114 +
  115 +```json
  116 +{ "code": 0, "message": "success", "data": { } }
  117 +```
  118 +
  119 +- `code`:0 成功;非 0 为业务/系统错误码(由 `ResultCode` 枚举集中定义)。
  120 +- 分页返回 `Result<PageResult<T>>`,`PageResult` 含 `records` / `total` / `pageNum` / `pageSize`。
  121 +- 失败响应不抛栈到前端,`message` 给可读提示。
  122 +
  123 +### 1.5 异常处理
  124 +
  125 +- 业务错误统一抛 `BusinessException(ResultCode, msg)`,由 `GlobalExceptionHandler` 捕获转 `Result`。
  126 +- 参数校验用 `jakarta.validation`(`@Valid` + 注解),校验失败由全局处理器转统一错误。
  127 +- 系统异常(未捕获)记录 ERROR 日志并返回通用错误码,不泄露内部细节。
  128 +
  129 +### 1.6 事务
  130 +
  131 +- 写操作(增/改/删,含多表)在 Service 实现方法上加 `@Transactional(rollbackFor = Exception.class)`。
  132 +- 只读查询不开事务;避免在事务方法内做远程调用 / 长耗时操作。
  133 +
  134 +### 1.7 认证
  135 +
  136 +- 采用 Spring Security + JWT 无状态认证。登录成功签发 JWT(密钥取自 `config-vars.yaml` `secrets.jwt_secret`,有过期时间)。
  137 +- 受保护接口经 `JwtAuthenticationFilter` 校验 `Authorization: Bearer <token>`;登录接口 `/api/usr/login` 放行。
  138 +- 密码用 `BCryptPasswordEncoder` 哈希存储与比对,禁止明文。
  139 +
  140 +## 二、前端规范
  141 +
  142 +> 技术栈:React 18 + Ant Design 5 + Redux Toolkit + React Router v6 + Vite + Axios,包名 `xly-erp-web`。
  143 +
  144 +### 2.1 目录约定
  145 +
  146 +前端为仓库根下的 `frontend/` 子项目,目录布局:
  147 +
  148 +```
  149 +frontend/
  150 +├── package.json # name=xly-erp-web;scripts: dev/build/lint/test:unit/test:e2e
  151 +├── vite.config.ts
  152 +├── index.html
  153 +├── src/
  154 +│ ├── main.tsx # 入口:挂载 App + Redux Provider + Router + AntD ConfigProvider
  155 +│ ├── App.tsx
  156 +│ ├── router/ # React Router v6 路由表 + 路由守卫(未登录跳登录页)
  157 +│ ├── store/ # Redux Toolkit:store.ts + slices/(authSlice 等)
  158 +│ ├── api/ # Axios 实例封装(request.ts)+ 各模块 api(usrApi.ts)
  159 +│ ├── pages/
  160 +│ │ └── usr/ # 用户管理页面(用户列表 / 新增 / 编辑 / 登录)
  161 +│ ├── components/ # 跨页面通用组件
  162 +│ ├── styles/ # 引用 Design Tokens(见下)
  163 +│ └── utils/ # 通用工具
  164 +└── tests/ # 单元测试(Vitest)+ e2e(Playwright)
  165 +```
  166 +
  167 +- **Design Tokens SSoT**:色值单一来源在仓库根 `src/styles/tokens.css`(由 skeleton-gen 生成),前端在 `main.tsx` / 全局样式中引入;组件只用 `var(--color-xxx)`,禁止硬编码 hex/rgba。**色值冲突时 `tokens.css` 优先于 `prototype/`**。
  168 +- **UI/交互/布局权威**:项目根 `prototype/`(完整 demo)为前端页面布局与交互的权威参照,A5 据其推导 FE 清单。
  169 +
  170 +### 2.2 状态管理
  171 +
  172 +- 全局状态(登录态、当前用户、token)用 Redux Toolkit `createSlice` 管理;按域拆 slice。
  173 +- 服务端数据优先就近在页面用 hooks 拉取,跨页面共享的才进 store;避免把所有响应塞进全局。
  174 +
  175 +### 2.3 请求封装
  176 +
  177 +- 统一 Axios 实例(`api/request.ts`):baseURL 指向后端 `/api`,请求拦截器注入 `Authorization` 头,响应拦截器拆 `Result`、对非 0 `code` 统一提示。
  178 +- 各模块 API 集中在 `api/<模块>Api.ts`,页面只调封装后的方法,不直接散用 axios。
  179 +
  180 +### 2.4 错误处理
  181 +
  182 +- 响应拦截器统一处理:401 跳登录、业务错误码弹 `message.error`、网络异常兜底提示。
  183 +- 表单提交错误就近在表单展示;列表加载失败展示空态/重试。
  184 +
  185 +## 三、共同约定
  186 +
  187 +### 3.1 Git 提交
  188 +
  189 +`<type>(<scope>): <subject> REQ-XXX-NNN`(详见 `CLAUDE.md § 🗂️ Git 提交规范`)。
  190 +
  191 +### 3.2 分页查询
  192 +
  193 +- 入参统一 `pageNum`(从 1 起)+ `pageSize`(有上限,默认 10/20,最大 100)+ 业务过滤条件。
  194 +- 返回 `PageResult<T>`:`records` / `total` / `pageNum` / `pageSize`。
  195 +- 文本条件模糊匹配,枚举/外键条件精确匹配;空条件返回全量分页。
  196 +
  197 +### 3.3 日期与金额
  198 +
  199 +- 日期时间统一 ISO-8601 字符串传输(`yyyy-MM-dd'T'HH:mm:ss`),后端用 `LocalDateTime`。
  200 +- 金额用整数分或 `BigDecimal`,禁止用 `float/double` 表示金额。
  201 +
  202 +### 3.4 数据访问规约
  203 +
  204 +- 数据访问只走 Mapper(MyBatis-Plus);简单 CRUD 用 MP 内置/`LambdaQueryWrapper`,复杂 SQL 写 XML。
  205 +- 禁止在 Controller 直接操作 Mapper;逻辑删除/审计字段(创建时间、制单人)由 `BaseEntity` + MP 自动填充统一处理。
  206 +- 业务 schema 变更走 Flyway migration,并反向同步更新 `docs/03-数据库设计文档.md` 对应表小节(SSoT 一致)。
... ...
docs/05-API接口契约.md 0 → 100644
  1 +++ a/docs/05-API接口契约.md
  1 +# 05-API接口契约
  2 +
  3 +BasePath: `/api`
  4 +端口: 见 `config-vars.yaml` 的 `backend.http_port`(单一来源,不在此重复填)
  5 +
  6 +## 全局约定
  7 +
  8 +响应格式 / 异常 / 错误码 / 认证 / 分页等全局约定的 SSoT 在 `docs/04`(响应格式见 § 1.4、异常处理见 § 1.5、认证见 § 1.7、分页查询见 § 3.2),此处不重复。各端点专属的请求 / 响应 / 错误码见下方接口清单。
  9 +
  10 +## 接口清单
  11 +(各模块接口段落见下方,由 `downstream-gen` 按 REQ 填入)
  12 +
  13 +### REQ-USR-001 增加用户
  14 +
  15 +- **Method**: POST
  16 +- **Path**: `/api/usr/users`
  17 +- **Auth**: 需要(Bearer JWT,仅管理员/超级管理员可调用)
  18 +- **请求**: JSON body `{ sUserName(必填,3-20位字母数字下划线,全局唯一), sUserNo(可选), iEmployeeId(可选,关联职员), sUserType(必填,普通用户/超级管理员,默认普通用户), sLanguage(必填,中文/英文/繁体), iCanModifyBill(可选,0/1,默认0), permissionIds(可选,number[],权限组勾选), initialPassword(可选,默认 666666) }`。密码以 BCrypt 哈希入库。
  19 +- **响应**: `Result<{ id: number }>`,返回新建用户主键 id(`data.id`)。
  20 +
  21 +#### 错误码
  22 +- `40001` — 参数校验失败(字段格式/必填项不满足)
  23 +- `40901` — 用户名已存在(sUserName 全局唯一冲突)
  24 +- `40301` — 无权限(非管理员调用)
  25 +
  26 +### REQ-USR-002 修改用户
  27 +
  28 +- **Method**: PUT
  29 +- **Path**: `/api/usr/users/{id}`
  30 +- **Auth**: 需要(Bearer JWT,仅管理员/超级管理员可调用)
  31 +- **请求**: 路径参数 `id`(用户主键);JSON body `{ sUserNo, iEmployeeId, sUserType, sLanguage, iCanModifyBill, iIsVoid, permissionIds }`(sUserName 作为唯一标识不可修改;密码不在本接口修改)。
  32 +- **响应**: `Result<{ id: number }>`,返回被修改用户的 id;持久化变更。
  33 +
  34 +#### 错误码
  35 +- `40001` — 参数校验失败
  36 +- `40401` — 用户不存在(id 无对应记录)
  37 +- `40301` — 无权限(非管理员调用)
  38 +
  39 +### REQ-USR-003 查询用户
  40 +
  41 +- **Method**: GET
  42 +- **Path**: `/api/usr/users`
  43 +- **Auth**: 需要(Bearer JWT)
  44 +- **请求**: query 参数 `{ queryField(可选,用户名/员工名/用户号/部门/用户类型/作废/登录日期/制单人), matchType(可选,包含/不包含/等于,默认包含), queryValue(可选), pageNum(默认1), pageSize(默认10,最大100) }`。空条件返回全量分页;密码字段不返回。
  45 +- **响应**: `Result<PageResult<UserVO>>`,`UserVO = { id, sUserName, 员工名, sUserNo, 部门, sUserType, sLanguage, iIsVoid, tLastLoginDate, sCreator, tCreateDate }`;`PageResult = { records, total, pageNum, pageSize }`。
  46 +
  47 +#### 错误码
  48 +- `42201` — 分页参数非法(pageNum<1 或 pageSize 超上限)
  49 +- `40001` — 查询参数校验失败
  50 +
  51 +### REQ-USR-004 登录用户
  52 +
  53 +- **Method**: POST
  54 +- **Path**: `/api/usr/login`
  55 +- **Auth**: 否(登录端点,放行)
  56 +- **请求**: JSON body `{ sUserName(必填), password(必填,提交明文经 HTTPS,服务端 BCrypt 比对), companyId(必填,登录"版本"下拉选中的 usr_company.id) }`。
  57 +- **响应**: `Result<{ token: string, user: { id, sUserName, sUserType, sLanguage } }>`,签发 JWT(有过期时间,无状态);登录成功更新 `tLastLoginDate`。
  58 +
  59 +#### 错误码
  60 +- `40101` — 认证失败(用户名或密码错误;不区分以防账号枚举)
  61 +- `40302` — 账号已禁用(iIsVoid=1,禁止登录)
  62 +- `40001` — 参数校验失败(缺用户名/密码/版本)
... ...
docs/08-模块任务管理.md 0 → 100644
  1 +++ a/docs/08-模块任务管理.md
  1 +# 08-工作流进度
  2 +
  3 +> 全流程进度跟踪。CC 每完成一项产出就勾选一项。
  4 +
  5 +## 一、Plan 阶段(一次性)
  6 +
  7 +- [x] A0 项目初始化 — project-init
  8 + - [x] 依赖检查通过
  9 + - [x] 项目文件骨架已创建(CLAUDE.md + docs/01-需求清单/index.md + docs/04-技术规范.md)
  10 + - [x] Git 已初始化
  11 +
  12 +- [x] A1 范围锁定 — scope-lock
  13 + - [x] 项目概述已填写(CLAUDE.md § 🎯 项目概述)
  14 + - [x] 技术栈已确认(docs/04 § 零)
  15 + - [x] 需求清单索引已填写(docs/01-需求清单/index.md)
  16 + - [x] REQ 卡片骨架已生成(docs/01-需求清单/<module>/REQ-*.md,业务内容留待人工填写)
  17 +
  18 +- [x] A2 骨架生成 — skeleton-gen
  19 + - [x] 架构文档已生成(docs/04 § 一+)
  20 + - [x] 工具脚本已生成(scripts/*.mjs)
  21 + - [x] 样式 token 骨架已生成(src/styles/tokens.css)
  22 + - [x] .gitignore 已配置
  23 +
  24 +- [x] A3 DB 设计 + REQ 回填 — db-design-gen
  25 + - [x] docs/03-数据库设计文档.md 已生成
  26 + - [x] docs/01 各 REQ 卡片"依赖表" + 模块头"涉及表" 已回填
  27 +
  28 +- [x] A4 DB 初始化 — db-init
  29 + - [x] sql/migrations/V1__initial_schema.sql 已生成
  30 + - [x] DDL ↔ docs/03 5 维一致(validate-ddl.mjs)
  31 + - [x] config-vars.yaml DB 凭据 5 项非空校验通过
  32 + - [x] setup-test-db.mjs DROP+CREATE + apply V1 已执行
  33 +
  34 +- [x] A5 下游文档生成 — downstream-gen
  35 + - [x] docs/02 开发计划已生成
  36 + - [x] docs/05 API 契约已生成
  37 + - [x] 下方模块列表已填入
  38 + - [x] REQ 卡片依赖接口已回填
  39 + - [x] FE 清单已推导填入 docs/08 § 三
  40 +
  41 +## 二、Coding 阶段(后端模块循环)
  42 +
  43 +(A5 填入后,每行一个后端模块。每个模块的 `里程碑:` 字段在 `—` 和 `milestone/<id>` 之间变化,完成由本地 `git tag -l 'milestone/<id>'` 判定。功能行 checkbox 只作可视化,真正的功能级 resume 由 `req-done/<REQ>` tag 判定。后端模块全部打里程碑后自动进入 § 三 前端阶段。)
  44 +
  45 +- USR 用户管理
  46 + - 依赖: 无
  47 + - 路径: `backend/src/main/java/com/xly/erp/modules/usr/**`
  48 + - 里程碑: —
  49 + - 功能:
  50 + - [ ] REQ-USR-001 增加用户
  51 + - [ ] REQ-USR-002 修改用户
  52 + - [ ] REQ-USR-003 查询用户
  53 + - [ ] REQ-USR-004 登录用户
  54 +
  55 +## 三、Coding 阶段(前端整体)
  56 +
  57 +(FE 业务功能清单在 Plan 期 A5 `downstream-gen` 由 prototype/ + docs/01 + docs/05 推导后写入下方"功能:"项;Coding 阶段 `coding.mjs` 的 Router 把缺少 `req-done/<FE-NN>` tag 的 FE 聚合为单一 `frontend-phase` 阶段,排在所有后端模块之后。整个前端阶段 1 个里程碑 tag,分支 `frontend-phase`。无前端则此处留空,Router 不产生前端阶段。)
  58 +
  59 +- 整体里程碑: —
  60 +- 功能:
  61 + - [ ] FE-01 登录页(用户名/密码/版本下拉登录,对接 POST /api/usr/login)
  62 + - [ ] FE-02 主页与导航框架(顶栏 + 全部导航总览 + 主页 KPI 看板 + 常用操作;登录后落地页与路由壳)
  63 + - [ ] FE-03 用户列表与查询(工具栏刷新/导出 + 筛选条件 + 用户表格 + 分页,对接 GET /api/usr/users)
  64 + - [ ] FE-04 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 POST /api/usr/users 与 PUT /api/usr/users/{id})
... ...
prototype/erp.html 0 → 100644
  1 +++ a/prototype/erp.html
  1 +<!doctype html>
  2 +<html lang="zh-CN">
  3 +<head>
  4 +<meta charset="utf-8" />
  5 +<title>ERP - 企业业务能力平台</title>
  6 +<meta name="viewport" content="width=device-width, initial-scale=1" />
  7 +<style>
  8 + :root {
  9 + --bg: #f3f4f6;
  10 + --panel: #ffffff;
  11 + --topbar: #1f1f23;
  12 + --topbar-text: #ffffff;
  13 + --primary: #2f7adf;
  14 + --primary-strong: #1f6ed4;
  15 + --link: #1e84e6;
  16 + --text: #333333;
  17 + --text-soft: #555;
  18 + --text-mute: #888;
  19 + --border: #e3e6eb;
  20 + --row-alt: #f7f8fa;
  21 + --header-bg: #f4f5f7;
  22 + --danger: #e34d4d;
  23 + --tab-active: #1e84e6;
  24 + --toolbar-bg: #2c2f36;
  25 + --toolbar-text: #ffffff;
  26 + --label: #f04848;
  27 + --field-bg: #eaf3fe;
  28 + --field-bg-readonly: #f1f3f5;
  29 + }
  30 + *{box-sizing:border-box}
  31 + html,body{margin:0;padding:0;background:var(--bg);color:var(--text);font-family:"Microsoft YaHei","PingFang SC","Helvetica Neue",Helvetica,Arial,"Segoe UI",sans-serif;font-size:13px;}
  32 + button{font-family:inherit;cursor:pointer}
  33 + a{color:inherit;text-decoration:none}
  34 + input,select,textarea{font-family:inherit;font-size:13px}
  35 +
  36 + /* ======= TOP BAR ======= */
  37 + .topbar{display:flex;align-items:stretch;height:44px;background:var(--topbar);color:var(--topbar-text);position:relative;z-index:30;}
  38 + .topbar .logo{width:54px;display:flex;align-items:center;justify-content:center;}
  39 + .topbar .logo svg{width:30px;height:30px}
  40 + .topbar .nav-btn{display:flex;align-items:center;gap:6px;padding:0 18px;color:#fff;cursor:pointer;font-size:14px;border:none;background:transparent;height:100%;}
  41 + .topbar .nav-btn.active{background:var(--primary);}
  42 + .topbar .nav-btn:hover{background:#33363d}
  43 + .topbar .nav-btn.active:hover{background:var(--primary-strong)}
  44 + .topbar .tabs{display:flex;align-items:stretch;flex:1;}
  45 + .topbar .tab{display:flex;align-items:center;gap:8px;padding:0 18px;cursor:pointer;color:#cfd2d8;font-size:14px;height:100%;}
  46 + .topbar .tab .ic{opacity:.85}
  47 + .topbar .tab.active{color:var(--link)}
  48 + .topbar .tab .close{margin-left:6px;width:14px;height:14px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:11px;color:#9aa0a8}
  49 + .topbar .tab .close:hover{background:#3a3d44;color:#fff}
  50 + .topbar .right{display:flex;align-items:center;gap:18px;padding-right:14px;}
  51 + .topbar .right .ic{width:18px;height:18px;opacity:.9;cursor:pointer}
  52 + .topbar .user{display:flex;align-items:center;gap:6px;font-size:14px}
  53 + .topbar .more{font-size:18px;letter-spacing:2px;cursor:pointer;padding:0 4px}
  54 +
  55 + /* ======= APP LAYOUT ======= */
  56 + .app{height:100vh;display:flex;flex-direction:column;overflow:hidden}
  57 + .stage{flex:1;position:relative;overflow:hidden;background:var(--bg)}
  58 + .screen{position:absolute;inset:0;display:none;overflow:auto}
  59 + .screen.active{display:block}
  60 +
  61 + /* ======= MAIN / DASHBOARD ======= */
  62 + .main-wrap{display:grid;grid-template-columns:1fr 280px;gap:10px;padding:10px;min-height:100%;}
  63 + .panel{background:var(--panel);border:1px solid var(--border);border-radius:2px}
  64 + .kpi-head{padding:14px 18px;display:flex;align-items:center;gap:24px;flex-wrap:wrap;}
  65 + .kpi-head .title{font-size:15px;color:#222;font-weight:500;margin-right:6px}
  66 + .kpi-head .stat{color:var(--text-soft)}
  67 + .kpi-head .stat b{color:var(--danger);font-weight:500;margin-left:6px;font-size:14px}
  68 + .kpi-head .stat.blue b{color:var(--link)}
  69 + .kpi-head .sep{color:#cdd0d6}
  70 + .kpi-head .ai-btn{margin-left:auto;background:var(--primary);color:#fff;border:none;padding:7px 14px;border-radius:2px;display:inline-flex;align-items:center;gap:6px;font-size:13px;}
  71 + .kpi-head .ai-btn:hover{background:var(--primary-strong)}
  72 +
  73 + .kpi-body{display:grid;grid-template-columns:200px 90px 1fr 1fr 90px 90px 130px;border-top:1px solid var(--border)}
  74 + .kpi-body > div{border-right:1px solid var(--border);border-bottom:1px solid var(--border);padding:10px 12px;font-size:13px;min-height:38px;display:flex;align-items:center}
  75 + .kpi-body > div:nth-last-child(-n+7){border-bottom:none}
  76 + .kpi-body > div:last-child{border-right:none}
  77 + .kpi-body .h{background:var(--header-bg);font-weight:500;color:#222;padding:9px 12px}
  78 + .kpi-body .row-alt{background:var(--row-alt)}
  79 + .kpi-body .link{color:var(--link);cursor:pointer}
  80 + .kpi-body .link:hover{text-decoration:underline}
  81 + .kpi-body .num-red{color:var(--danger);font-weight:600;justify-content:center}
  82 + .kpi-body .num-zero{color:var(--danger);font-weight:600;justify-content:center}
  83 + .kpi-body .num{justify-content:center}
  84 + .kpi-body .center{justify-content:center}
  85 +
  86 + .nav-tree{padding:6px 0}
  87 + .nav-tree .group{padding:8px 14px;color:#444;font-size:13px;display:flex;align-items:center;gap:6px;cursor:pointer}
  88 + .nav-tree .group .arrow{display:inline-block;width:0;height:0;border-left:4px solid #888;border-top:4px solid transparent;border-bottom:4px solid transparent;transform:rotate(90deg);margin-right:2px}
  89 + .nav-tree .group .ico{color:#e0b96a}
  90 + .nav-tree .item{padding:6px 14px 6px 36px;display:flex;align-items:center;gap:8px;color:#3a3a3a;cursor:pointer;font-size:13px}
  91 + .nav-tree .item:hover{background:#eef3fb}
  92 + .nav-tree .item.active{background:#d8eaff;color:#1166cc}
  93 + .nav-tree .item .ico{color:#e0b96a}
  94 +
  95 + .three-col{display:grid;grid-template-columns:280px 1fr;height:100%;}
  96 + .three-col .left-nav{background:var(--panel);border:1px solid var(--border);overflow:auto}
  97 + .three-col .center{display:flex;flex-direction:column;gap:10px;min-width:0}
  98 +
  99 + .common-ops{padding:14px 18px}
  100 + .common-ops .h{font-size:14px;color:#222;margin-bottom:14px;font-weight:500}
  101 + .common-ops a{display:block;color:var(--link);padding:8px 0;font-size:13px;border-bottom:1px dashed transparent}
  102 + .common-ops a:hover{text-decoration:underline}
  103 +
  104 + /* table sub-process column */
  105 + .subproc{writing-mode:vertical-rl;text-orientation:upright;color:#222;font-weight:500;justify-content:center;min-width:24px;}
  106 + .subproc.estimate{ background:transparent }
  107 +
  108 + footer.foot{
  109 + background:#f3f4f6;border-top:1px solid var(--border);padding:10px 14px;text-align:center;color:#666;font-size:12px;
  110 + }
  111 + footer.foot .pipe{margin:0 8px;color:#bbb}
  112 + footer.foot .police{display:inline-flex;align-items:center;gap:4px;margin-left:6px}
  113 + footer.foot .police svg{width:14px;height:14px}
  114 +
  115 + /* ======= NAV OVERLAY ======= */
  116 + #nav-overlay{position:absolute;inset:0;background:#2b3137;display:none;z-index:20;color:#cfd3da;}
  117 + #nav-overlay.show{display:flex}
  118 + #nav-overlay .side{width:200px;background:#2b3137;padding:8px 0;border-right:1px solid #1e2226}
  119 + #nav-overlay .side .si{display:flex;align-items:center;gap:10px;padding:11px 18px;font-size:14px;color:#d3d6db;cursor:pointer}
  120 + #nav-overlay .side .si:hover{background:#34393f}
  121 + #nav-overlay .side .si.active{color:var(--link);background:#34393f}
  122 + #nav-overlay .side .si svg{width:16px;height:16px;opacity:.85}
  123 + #nav-overlay .grid{flex:1;padding:30px 40px;display:grid;grid-template-columns:repeat(7,1fr);gap:30px 40px;align-content:start}
  124 + #nav-overlay .col h3{font-size:15px;color:#e8eaee;font-weight:500;margin:0 0 18px;border-bottom:1px solid #4a4f57;padding-bottom:10px}
  125 + #nav-overlay .col a{display:flex;align-items:center;gap:6px;padding:7px 0;color:#cfd3da;font-size:14px;cursor:pointer}
  126 + #nav-overlay .col a:hover{color:#fff}
  127 + #nav-overlay .col a .star{color:#f3b526}
  128 +
  129 + /* ======= USER LIST ======= */
  130 + .toolbar{background:var(--toolbar-bg);color:#fff;display:flex;align-items:center;gap:6px;padding:0 8px;height:38px}
  131 + .toolbar .tb-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;color:#e6e7ea;cursor:pointer;font-size:13px;border-radius:2px}
  132 + .toolbar .tb-btn:hover{background:#3a3d44}
  133 + .toolbar .tb-btn .ic{opacity:.9}
  134 + .toolbar .spacer{flex:1}
  135 + .toolbar .gear{padding:6px 8px;cursor:pointer;color:#cfd2d8}
  136 +
  137 + .filterbar{display:flex;align-items:center;gap:8px;padding:10px 12px;background:var(--panel);border-bottom:1px solid var(--border)}
  138 + .filterbar select, .filterbar input{height:30px;border:1px solid #d5d8de;border-radius:2px;padding:0 28px 0 10px;background:#fff;min-width:140px;appearance:none;
  139 + background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3l3 4 3-4z' fill='%23888'/></svg>");
  140 + background-repeat:no-repeat;background-position:right 8px center}
  141 + .filterbar input{background-image:none;padding-right:10px}
  142 + .filterbar .down{width:34px;height:30px;background:#dfe5ee;border:1px solid #d5d8de;display:flex;align-items:center;justify-content:center;border-radius:2px;cursor:pointer;color:#3776c8}
  143 + .filterbar .btn{height:30px;padding:0 14px;border-radius:2px;border:1px solid var(--primary);background:var(--primary);color:#fff;display:inline-flex;align-items:center;gap:5px;font-size:13px;cursor:pointer}
  144 + .filterbar .btn.ghost{background:#fff;color:#444;border-color:#cfd3da}
  145 + .filterbar .btn:hover{filter:brightness(1.05)}
  146 +
  147 + .grid-table{width:100%;border-collapse:collapse;background:#fff;font-size:13px;}
  148 + .grid-table th, .grid-table td{border:1px solid var(--border);padding:7px 10px;text-align:left;white-space:nowrap}
  149 + .grid-table thead th{background:var(--header-bg);font-weight:500;color:#333;position:sticky;top:0;z-index:1}
  150 + .grid-table thead th .h-flex{display:flex;align-items:center;gap:6px;justify-content:space-between}
  151 + .grid-table thead th .h-flex .ic{display:flex;gap:2px;color:#aaa}
  152 + .grid-table tbody tr:nth-child(even){background:var(--row-alt)}
  153 + .grid-table tbody tr:hover{background:#eaf3fe}
  154 + .grid-table .radio-cell{width:32px;text-align:center}
  155 + .radio-dot{width:14px;height:14px;border:1px solid #b8bcc3;border-radius:50%;display:inline-block;vertical-align:middle;background:#fff}
  156 + .grid-table input.cb{margin:0}
  157 +
  158 + .pager{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-top:1px solid var(--border);justify-content:flex-end;font-size:13px;color:#555}
  159 + .pager .pgbtn{width:28px;height:28px;border:1px solid #d5d8de;background:#fff;border-radius:2px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:#666}
  160 + .pager .pgcur{width:28px;height:28px;border:1px solid var(--primary);color:var(--primary);display:inline-flex;align-items:center;justify-content:center;border-radius:2px}
  161 + .pager select{height:28px;border:1px solid #d5d8de;border-radius:2px;padding:0 8px;background:#fff}
  162 +
  163 + /* ======= USER DETAIL ======= */
  164 + .form-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0;background:#fff;padding:10px 14px;border-bottom:1px solid var(--border)}
  165 + .form-cell{display:flex;align-items:center;gap:6px;padding:8px 10px;}
  166 + .form-cell .lbl{min-width:88px;color:#333;font-size:13px;text-align:right}
  167 + .form-cell .lbl.req::before{content:"*";color:var(--label);margin-right:3px}
  168 + .form-cell .lbl.req{color:var(--label)}
  169 + .form-cell input[type=text], .form-cell .field{
  170 + flex:1;height:28px;border:1px solid #d5d8de;border-radius:2px;padding:0 24px 0 10px;background:var(--field-bg);
  171 + appearance:none; min-width:0;
  172 + }
  173 + .form-cell .field.readonly{background:var(--field-bg-readonly);color:#444;display:flex;align-items:center}
  174 + .form-cell .field.with-caret{background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2 3l3 4 3-4z' fill='%23888'/></svg>");background-repeat:no-repeat;background-position:right 8px center;background-color:var(--field-bg)}
  175 + .form-cell .field.with-cal{background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16' fill='none' stroke='%23888' stroke-width='1.4'><rect x='2' y='3' width='12' height='11' rx='1'/><path d='M2 6h12M5 1v3M11 1v3'/></svg>");background-repeat:no-repeat;background-position:right 8px center;background-color:var(--field-bg-readonly)}
  176 + .form-cell .cb{width:14px;height:14px;border:1px solid #b8bcc3;background:#fff;display:inline-block}
  177 +
  178 + .tabs-row{display:flex;background:#fff;border-bottom:1px solid var(--border);padding:0 6px}
  179 + .tabs-row .tb{padding:11px 18px;font-size:14px;color:#444;cursor:pointer;border-bottom:2px solid transparent;margin-right:4px}
  180 + .tabs-row .tb.active{color:var(--tab-active);border-bottom-color:var(--tab-active)}
  181 +
  182 + .perm-list{background:#fff}
  183 + .perm-row{display:flex;align-items:center;gap:14px;padding:10px 14px;border-bottom:1px solid var(--border);font-size:13px;color:#333}
  184 + .perm-row.head{background:var(--header-bg);font-weight:500;color:#222}
  185 + .perm-row .cb{width:14px;height:14px;border:1px solid #b8bcc3;background:#fff;display:inline-block;flex-shrink:0}
  186 + .perm-row.head .ic{margin-left:auto;color:#aaa}
  187 +
  188 + /* ======= LOGIN ======= */
  189 + .login-wrap{position:absolute;inset:0;background:#eaedf2;display:flex;flex-direction:column}
  190 + .login-head{display:flex;align-items:center;gap:12px;padding:18px 36px;background:#eaedf2}
  191 + .login-head .lg{width:42px;height:42px;display:flex;align-items:center;justify-content:center}
  192 + .login-head .name{font-size:24px;font-weight:700;color:#e0a020;letter-spacing:2px}
  193 + .login-head .sub{color:#444;font-size:14px;margin-left:6px}
  194 + .login-hero{flex:1;position:relative;background:
  195 + radial-gradient(ellipse at center, #1a4ea0 0%, #0a1d44 60%, #050d20 100%);
  196 + overflow:hidden}
  197 + .login-hero::before{
  198 + content:"";position:absolute;inset:0;
  199 + background-image:
  200 + linear-gradient(rgba(80,160,255,.18) 1px, transparent 1px),
  201 + linear-gradient(90deg, rgba(80,160,255,.18) 1px, transparent 1px);
  202 + background-size:80px 80px;
  203 + transform:perspective(800px) rotateX(55deg) translateY(20%);
  204 + transform-origin:center;
  205 + opacity:.55;
  206 + }
  207 + .login-hero::after{
  208 + content:"";position:absolute;inset:0;
  209 + background:
  210 + radial-gradient(ellipse 800px 300px at 50% 50%, rgba(140,200,255,.35), transparent 60%),
  211 + radial-gradient(circle 200px at 30% 40%, rgba(255,255,255,.15), transparent 70%),
  212 + radial-gradient(circle 160px at 70% 60%, rgba(255,255,255,.12), transparent 70%);
  213 + }
  214 + .login-text{position:absolute;left:8%;top:35%;color:#fff;z-index:2}
  215 + .login-text .en{font-size:30px;font-weight:300;letter-spacing:1px;color:#cfe1ff;margin-bottom:6px}
  216 + .login-text .zh{font-size:54px;font-weight:700;color:#fff;letter-spacing:4px;margin-bottom:4px}
  217 + .login-text .erp{font-size:90px;font-weight:800;color:#fff;letter-spacing:8px;line-height:.9}
  218 + .login-card{position:absolute;right:8%;top:50%;transform:translateY(-50%);background:#fff;width:380px;padding:36px 32px;border-radius:2px;box-shadow:0 12px 40px rgba(0,0,0,.3);z-index:3}
  219 + .login-card h3{margin:0 0 22px;font-size:18px;color:#333;font-weight:500}
  220 + .login-card .lf{display:flex;align-items:center;border:1px solid #e1e4e8;border-radius:2px;height:42px;margin-bottom:14px;background:#fff;}
  221 + .login-card .lf .ic{width:42px;display:flex;align-items:center;justify-content:center;color:#888}
  222 + .login-card .lf .div{width:1px;height:20px;background:#e1e4e8}
  223 + .login-card .lf input{flex:1;border:none;outline:none;height:100%;padding:0 12px;background:transparent}
  224 + .login-card .lf select{flex:1;border:none;outline:none;height:100%;padding:0 12px;background:transparent;appearance:none}
  225 + .login-card .lf.dropdown{position:relative}
  226 + .login-card .lf.dropdown::after{content:"";position:absolute;right:14px;top:50%;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #888;transform:translateY(-50%)}
  227 + .login-card .lf.dropdown.open .opt{display:block}
  228 + .login-card .lf .opt{display:none;position:absolute;left:-1px;right:-1px;top:42px;background:#fff;border:1px solid #e1e4e8;border-top:none;z-index:5}
  229 + .login-card .lf .opt .o{padding:10px 14px;color:#333;cursor:pointer;background:#eef5ff}
  230 + .login-card .lf .opt .o:hover{background:#dde9fb}
  231 + .login-card .submit{width:100%;height:42px;background:var(--primary);color:#fff;border:none;border-radius:2px;font-size:15px;letter-spacing:8px;cursor:pointer;margin-top:6px}
  232 + .login-card .submit:hover{background:var(--primary-strong)}
  233 + .login-foot{background:#eaedf2;text-align:center;padding:14px 8px;color:#666;font-size:12px;border-top:1px solid #d8dce2}
  234 +
  235 + /* misc */
  236 + .ic{display:inline-flex;align-items:center;justify-content:center}
  237 + .star{color:#f3b526}
  238 + .scrollable-y{overflow-y:auto}
  239 + .table-shell{background:#fff;flex:1;overflow:auto;border:1px solid var(--border);border-top:none}
  240 +
  241 + /* Antler logo color */
  242 + .lg-antler{color:#0e1216}
  243 +</style>
  244 +</head>
  245 +<body>
  246 +<div class="app">
  247 +
  248 + <!-- ======= TOP BAR ======= -->
  249 + <div class="topbar" id="topbar">
  250 + <div class="logo" data-go="main" title="主页">
  251 + <!-- antler/deer logo -->
  252 + <svg viewBox="0 0 64 64" fill="currentColor" class="lg-antler">
  253 + <path d="M14 10c2 4 1 8-1 11 3-1 7 0 10 3 1-4 4-7 8-7-3 3-4 7-3 11l4 1c-1 3 0 6 3 8-3 0-6 1-8 4-1-3-4-5-8-5 2-3 2-7 0-10-3 1-7 0-10-3 3 0 5-2 6-5l-1-8z"/>
  254 + <path d="M48 14c-2 3-2 6-1 9-2-2-5-2-8-1 1 3 1 6-1 9 3 0 5 2 6 5 1-3 4-5 7-5-2-3-2-6 0-9 2 1 5 1 7-1-2 0-4-1-5-3-1-2-3-4-5-4z"/>
  255 + <path d="M28 38c2 3 5 5 9 5 1 4 4 7 8 8-3 2-5 5-5 9-3-2-7-3-11-2 1-3 1-7-1-10-3 0-6-1-8-4 3-1 6-3 8-6z"/>
  256 + </svg>
  257 + </div>
  258 +
  259 + <div class="tabs" id="tabs">
  260 + <button class="nav-btn" id="nav-toggle">
  261 + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="17" x2="20" y2="17"/></svg>
  262 + 全部导航
  263 + </button>
  264 + <div class="tab" data-go="main">
  265 + <span class="ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/></svg></span>
  266 + 主页
  267 + </div>
  268 + <div class="tab" id="tab-userlist" data-go="userlist" style="display:none">
  269 + 用户列表 <span class="close" data-close="userlist">✕</span>
  270 + </div>
  271 + <div class="tab" id="tab-userdetail" data-go="userdetail" style="display:none">
  272 + 用户信息单据 <span class="close" data-close="userdetail">✕</span>
  273 + </div>
  274 + </div>
  275 +
  276 + <div class="right">
  277 + <span class="ic" title="搜索">
  278 + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
  279 + </span>
  280 + <span class="ic" title="通知">
  281 + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 8a6 6 0 0 1 12 0v5l1.5 3h-15L6 13z"/><path d="M10 19a2 2 0 0 0 4 0"/></svg>
  282 + </span>
  283 + <div class="user">
  284 + <span class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 9h18"/></svg></span>
  285 + 朱子纯(超级管理员) <span style="font-size:10px">▾</span>
  286 + </div>
  287 + <span class="more">⋯</span>
  288 + </div>
  289 + </div>
  290 +
  291 + <!-- ======= STAGE ======= -->
  292 + <div class="stage" id="stage">
  293 +
  294 + <!-- NAV OVERLAY -->
  295 + <div id="nav-overlay">
  296 + <div class="side" id="nav-side"></div>
  297 + <div class="grid" id="nav-grid"></div>
  298 + </div>
  299 +
  300 + <!-- ===== MAIN ===== -->
  301 + <section class="screen active" id="screen-main">
  302 + <div class="main-wrap">
  303 + <div style="display:flex;flex-direction:column;gap:10px;min-height:0">
  304 + <!-- KPI head bar -->
  305 + <div class="panel kpi-head">
  306 + <span class="title">KPI监控</span>
  307 + <span class="stat">今日未处理:<b>37428</b></span>
  308 + <span class="sep">|</span>
  309 + <span class="stat blue">未清总数:<b>56433</b></span>
  310 + <button class="ai-btn">
  311 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/></svg>
  312 + 小ai同学,请帮我安排今日工作
  313 + </button>
  314 + </div>
  315 +
  316 + <!-- KPI body grid -->
  317 + <div class="three-col">
  318 + <!-- Left tree -->
  319 + <div class="left-nav nav-tree">
  320 + <div class="group"><span class="arrow"></span><span class="ico">📁</span>按角色</div>
  321 + <div class="item active"><span class="ico">📄</span>所有部门 (37428)</div>
  322 + <div class="item"><span class="ico">📄</span>核价人员 (17)</div>
  323 + <div class="item"><span class="ico">📄</span>销售人员 (0)</div>
  324 + <div class="item"><span class="ico">📄</span>印前 (11)</div>
  325 + <div class="item"><span class="ico">📄</span>客服部 (30127)</div>
  326 + <div class="item"><span class="ico">📄</span>技术研发部 (47)</div>
  327 + <div class="item"><span class="ico">📄</span>车间主管 (316)</div>
  328 + <div class="item"><span class="ico">📄</span>工艺部 (6)</div>
  329 + <div class="item"><span class="ico">📄</span>物控部 (728)</div>
  330 + <div class="item"><span class="ico">📄</span>生产计划部 (225)</div>
  331 + <div class="item"><span class="ico">📄</span>版房 (120)</div>
  332 + <div class="item"><span class="ico">📄</span>生产车间 (596)</div>
  333 + <div class="item"><span class="ico">📄</span>工艺技术部 (0)</div>
  334 + <div class="item"><span class="ico">📄</span>品质管理部 (589)</div>
  335 + <div class="item"><span class="ico">📄</span>储运部 (3496)</div>
  336 + <div class="item"><span class="ico">📄</span>通用 (0)</div>
  337 + <div class="item"><span class="ico">📄</span>外发组 (867)</div>
  338 + <div class="item"><span class="ico">📄</span>材料仓管 (0)</div>
  339 + <div class="item"><span class="ico">📄</span>机修组 (42)</div>
  340 + <div class="item"><span class="ico">📄</span>应收 (30)</div>
  341 + <div class="item"><span class="ico">📄</span>出纳 (211)</div>
  342 + <div class="item"><span class="ico">📄</span>应付 (0)</div>
  343 + <div class="item"><span class="ico">📄</span>客服 (0)</div>
  344 + <div class="group"><span class="arrow"></span><span class="ico">📁</span>按流程</div>
  345 + <div class="item"><span class="ico">📄</span>估价管理流程 (17)</div>
  346 + <div class="item"><span class="ico">📄</span>设计制作流程 (11)</div>
  347 + <div class="item"><span class="ico">📄</span>新品研发流程 (11)</div>
  348 + <div class="item"><span class="ico">📄</span>材料测试流程 (51)</div>
  349 + <div class="item"><span class="ico">📄</span>订单下达流程 (30118)</div>
  350 + </div>
  351 + <div class="center">
  352 + <div class="panel" style="overflow:auto">
  353 + <div class="kpi-body" id="kpi-body"></div>
  354 + </div>
  355 + </div>
  356 + </div>
  357 + </div>
  358 +
  359 + <!-- right side common ops -->
  360 + <div class="panel common-ops" style="height:fit-content">
  361 + <div class="h">常用操作</div>
  362 + <a data-go="userlist">用户列表</a>
  363 + <a>系统功能模块设置</a>
  364 + </div>
  365 + </div>
  366 +
  367 + <footer class="foot">
  368 + <span style="vertical-align:middle">🛠</span>
  369 + ©Copyright Antler Software <span class="pipe">|</span> 印刷智慧工厂 <span class="pipe">|</span> 印刷MES <span class="pipe">|</span> 印刷ERP <span class="pipe">|</span> 印刷电商平台 <span class="pipe">|</span> 文件智能处理 <span class="pipe">|</span> 印前自动化 <span class="pipe">|</span> 400-880-6237
  370 + <span class="police">
  371 + <svg viewBox="0 0 24 24" fill="#3a6cb6"><path d="M12 2l9 4v6c0 5-4 9-9 10-5-1-9-5-9-10V6z"/></svg>
  372 + 沪ICP备14034791号-1
  373 + </span>
  374 + </footer>
  375 + </section>
  376 +
  377 + <!-- ===== USER LIST ===== -->
  378 + <section class="screen" id="screen-userlist">
  379 + <div class="toolbar">
  380 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></svg>刷新</span>
  381 + <span class="tb-btn" id="btn-add" data-add-user="1"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v8M8 12h8"/></svg>新增</span>
  382 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h12l4 4v12H4z"/><path d="M16 4v4h4"/><path d="M8 12h8M8 16h8"/></svg>导出Excel</span>
  383 + <span class="spacer"></span>
  384 + <span class="gear">⚙</span>
  385 + </div>
  386 + <div class="filterbar">
  387 + <select><option>全部用户</option></select>
  388 + <select><option>用户名</option></select>
  389 + <select><option>包含</option></select>
  390 + <input type="text" />
  391 + <span class="down">▾</span>
  392 + <button class="btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>搜索</button>
  393 + <button class="btn ghost">⊗ 清空</button>
  394 + </div>
  395 + <div class="table-shell">
  396 + <table class="grid-table" id="user-table">
  397 + <thead>
  398 + <tr>
  399 + <th style="width:36px"></th>
  400 + <th style="width:60px">序号</th>
  401 + <th>用户名 <span style="color:#aaa">⇅ ⌕</span></th>
  402 + <th>员工名 <span style="color:#aaa">⇅ ⌕</span></th>
  403 + <th>用户号 <span style="color:#aaa">⇅ ⌕</span></th>
  404 + <th>部门 <span style="color:#aaa">⇅ ⌕</span></th>
  405 + <th>用户类型 <span style="color:#aaa">⇅ ⌕</span></th>
  406 + <th>语言 <span style="color:#aaa">⇅ ⌕</span></th>
  407 + <th>作 <span style="color:#aaa">⇅ ⌕</span></th>
  408 + <th>登录日期</th>
  409 + <th>制单人 <span style="color:#aaa">⇅ ⌕</span></th>
  410 + <th>制单日期</th>
  411 + </tr>
  412 + </thead>
  413 + <tbody id="user-tbody"></tbody>
  414 + </table>
  415 + </div>
  416 + <div class="pager">
  417 + <span>当前显示 共37个单据 共37条记录</span>
  418 + <span class="pgbtn">‹</span>
  419 + <span class="pgcur">1</span>
  420 + <span class="pgbtn">›</span>
  421 + <select><option>10000 条/页</option></select>
  422 + </div>
  423 + </section>
  424 +
  425 + <!-- ===== USER DETAIL ===== -->
  426 + <section class="screen" id="screen-userdetail">
  427 + <div class="toolbar">
  428 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v8M8 12h8"/></svg>新增</span>
  429 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 3l7 7-11 11H3v-7z"/></svg>修改</span>
  430 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M8 8l8 8M16 8l-8 8"/></svg>删除</span>
  431 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 4h11l3 3v13H5z"/><rect x="8" y="4" width="8" height="5"/></svg>保存</span>
  432 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/></svg>取消</span>
  433 + <span class="tb-btn"><svg class="ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>功能</span>
  434 + <span class="tb-btn">作废</span>
  435 + <span class="tb-btn">重置密码</span>
  436 + <span class="tb-btn">取消作废</span>
  437 + <span class="spacer"></span>
  438 + <span class="gear">⚙</span>
  439 + </div>
  440 +
  441 + <div class="form-grid">
  442 + <div class="form-cell"><span class="lbl">创建时间:</span><div class="field with-cal readonly" id="f-ctime">2023-10-26 17:02:01</div></div>
  443 + <div class="form-cell"><span class="lbl">制单人:</span><div class="field readonly" id="f-creator">超级管理员</div></div>
  444 + <div class="form-cell"><span class="lbl req">员工名:</span><div class="field with-caret" id="f-empname">管广飞</div></div>
  445 +
  446 + <div class="form-cell"><span class="lbl req">用户名:</span><input type="text" id="f-username" value="管广飞"/></div>
  447 + <div class="form-cell"><span class="lbl req">类型:</span><div class="field with-caret" id="f-type">超级管理员</div></div>
  448 + <div class="form-cell"><span class="lbl req">语言:</span><div class="field with-caret" id="f-lang">英文</div></div>
  449 +
  450 + <div class="form-cell"><span class="lbl req">用户号:</span><input type="text" id="f-userno" value="ggf"/></div>
  451 + <div class="form-cell"></div>
  452 + <div class="form-cell"><span class="lbl">单据修改权限:</span><span class="cb"></span></div>
  453 + </div>
  454 +
  455 + <div class="tabs-row">
  456 + <div class="tb active">权限组</div>
  457 + <div class="tb">客户查看权限</div>
  458 + <div class="tb">供应商查看权限</div>
  459 + <div class="tb">人员查看权限</div>
  460 + <div class="tb">工序查看权限</div>
  461 + <div class="tb">司机查看权限</div>
  462 + </div>
  463 +
  464 + <div class="perm-list" id="perm-list">
  465 + <div class="perm-row head"><span class="cb"></span><span>权限分类</span><span class="ic" style="margin-left:auto;color:#aaa">⇅</span></div>
  466 + </div>
  467 + </section>
  468 +
  469 + <!-- ===== LOGIN ===== -->
  470 + <section class="screen" id="screen-login">
  471 + <div class="login-wrap">
  472 + <div class="login-head">
  473 + <span class="lg">
  474 + <svg viewBox="0 0 64 64" width="42" height="42" fill="#0e1216">
  475 + <path d="M14 10c2 4 1 8-1 11 3-1 7 0 10 3 1-4 4-7 8-7-3 3-4 7-3 11l4 1c-1 3 0 6 3 8-3 0-6 1-8 4-1-3-4-5-8-5 2-3 2-7 0-10-3 1-7 0-10-3 3 0 5-2 6-5l-1-8z"/>
  476 + <path d="M48 14c-2 3-2 6-1 9-2-2-5-2-8-1 1 3 1 6-1 9 3 0 5 2 6 5 1-3 4-5 7-5-2-3-2-6 0-9 2 1 5 1 7-1-2 0-4-1-5-3-1-2-3-4-5-4z"/>
  477 + <path d="M28 38c2 3 5 5 9 5 1 4 4 7 8 8-3 2-5 5-5 9-3-2-7-3-11-2 1-3 1-7-1-10-3 0-6-1-8-4 3-1 6-3 8-6z"/>
  478 + </svg>
  479 + </span>
  480 + <span class="name">Antler ERP</span>
  481 + <span class="sub">欢迎登录EBC平台</span>
  482 + </div>
  483 + <div class="login-hero">
  484 + <div class="login-text">
  485 + <div class="en">Enterprise Business Capability</div>
  486 + <div class="zh">企业业务能力平台</div>
  487 + <div class="erp">ERP</div>
  488 + </div>
  489 + <div class="login-card">
  490 + <h3>用户登录</h3>
  491 + <div class="lf">
  492 + <span class="ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 4-7 8-7s8 3 8 7"/></svg></span>
  493 + <span class="div"></span>
  494 + <input type="text" placeholder="请输入你的用户名" />
  495 + </div>
  496 + <div class="lf">
  497 + <span class="ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="11" width="16" height="10" rx="1"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></span>
  498 + <span class="div"></span>
  499 + <input type="password" placeholder="请输入你的密码" />
  500 + </div>
  501 + <div class="lf dropdown" id="ver-drop">
  502 + <input type="text" value="标准版" readonly style="cursor:pointer"/>
  503 + <div class="opt">
  504 + <div class="o">标准版</div>
  505 + </div>
  506 + <div class="opt">
  507 + <div class="o">标准版1</div>
  508 + </div>
  509 + </div>
  510 + <button class="submit" data-go="main">登 录</button>
  511 + </div>
  512 + </div>
  513 + <div class="login-foot">
  514 + 🛠 ©Copyright Antler Software | 印刷智慧工厂 | 印刷MES | 印刷ERP | 印刷电商平台 | 文件智能处理 | 印前自动化 | 400-880-6237
  515 + <span style="display:inline-flex;align-items:center;gap:4px;margin-left:6px">
  516 + <svg width="14" height="14" viewBox="0 0 24 24" fill="#3a6cb6"><path d="M12 2l9 4v6c0 5-4 9-9 10-5-1-9-5-9-10V6z"/></svg>
  517 + 沪ICP备14034791号-1
  518 + </span>
  519 + </div>
  520 + </div>
  521 + </section>
  522 +
  523 + </div>
  524 +</div>
  525 +
  526 +<script>
  527 +/* ============ KPI ROWS ============ */
  528 +const kpiHeader = ['导航类型','角色','KPI待处理事项(当前行双击进入)','KPI内容描述及处理结果(点击蓝色查看明细)','今日未处理','未清总数','子流程'];
  529 +const kpiRows = [
  530 + // [role, item, desc, today, total, sub, navType?, rowSpanRole?, rowSpanSub?]
  531 + // group 1: 估价管理流程 — 5 rows, role 核价人员 spans 4, 销售人员 1
  532 + {role:'核价人员', item:'01/04【新增】新报价单', desc:'报价单明细', today:'-', total:'-', sub:'估价管理流程', navTypeFirst:true, roleSpan:4, subSpan:5},
  533 + {role:null, item:'02/04 审核后报价单->客户确认价格', desc:'报价单明细', today:'16', total:'16', red:true},
  534 + {role:null, item:'03/04 客户不认可->二次确认', desc:'报价单明细', today:'-', total:'-'},
  535 + {role:null, item:'04/04 报价单->销售订单', desc:'销售订单明细', today:'1', total:'1', red:true},
  536 + {role:'销售人员', item:'04/04 报价单->销售订单(标签)', desc:'销售订单明细(标签)', today:'0', total:'0', red:true},
  537 + // group 2: 设计制作流程 — 印前 (2 rows), span 2
  538 + {role:'印前', item:'1/2 新增设计申请单', desc:'设计申请明细', today:'-', total:'-', sub:'设计制作流程', roleSpan:2, subSpan:2},
  539 + {role:null, item:'2/2 设计申请->设计制作', desc:'根据设计申请单进行设计制作,当日16:00前审核的为今日任务,16:00后(含)顺延至次日', today:'11', total:'11', red:true},
  540 + // group 3: 新品研发流程 — 客服部, 技术研发部, 客服部, 技术研发部
  541 + {role:'客服部', item:'1/1 研发申请->文件制作', desc:'根据研发申请单,制作电子文件,当日16:00前下达的为今日任务,16:00后(含)顺延至次日', today:'0', total:'12', red:true, sub:'新品研发流程', subSpan:5},
  542 + {role:'客服部', item:'1/5 新增研发申请单', desc:'研发申请明细', today:'-', total:'-'},
  543 + {role:'技术研发部', item:'2/5 研发申请>>研发工单', desc:'及时开立研发工单,当日16:00前审核的为今日任务,16:00后(含)顺延至次日', today:'4', total:'4', red:true, roleSpan:2},
  544 + {role:null, item:'3/5 研发工单>>完工处理', desc:'计划人员在交货日期前确认工单完工', today:'7', total:'7', red:true},
  545 + {role:'客服部', item:'4/5 研发工单->客户确认', desc:'工单完工后需在7天内和客户确认样品', today:'-', total:'2703'},
  546 + // 5/5 技术研发部
  547 + {role:'技术研发部', item:'5/5 客户确认->工艺卡', desc:'根据客户已经确认的研发工单,生成产品工艺卡。当日16:00前确认的为今日任务,16:00后(含)顺延至次日', today:'0', total:'1632', red:true, sub:'', subSpan:0},
  548 + // group 4: 材料测试流程 — 车间主管, 技术研发部, 技术研发部
  549 + {role:'车间主管', item:'1/3 工单(测试部门数)->车间反馈', desc:'车间主管在工单完工前对测试材料进行数据反馈', today:'10', total:'115', red:true, sub:'材料测试流程', subSpan:3},
  550 + {role:null, item:'2/3 车间反馈->车间补充(多部门)', desc:'补充新材料测试信息,车间反馈次日16:00前的为当日任务,16:00后(含)顺延一日', today:'8', total:'8', red:true, roleSpan:2},
  551 + {role:'技术研发部', item:'2/3 车间反馈->工程部反馈(单部门)', desc:'工程部对新材料的测试结果进行反馈,车间反馈次日16:00前的为当日任务,16:00后(含)顺延一日', today:'23', total:'23', red:true},
  552 +];
  553 +
  554 +/* Render KPI grid via spans simulated with empty cells (CSS grid) */
  555 +(function renderKpi(){
  556 + const host = document.getElementById('kpi-body');
  557 + const heads = ['导航类型','角色','KPI待处理事项(当前行双击进入)','KPI内容描述及处理结果(点击蓝色查看明细)','今日未处理','未清总数','子流程'];
  558 + heads.forEach(h => { const d = document.createElement('div'); d.className='h'; d.textContent=h; host.appendChild(d); });
  559 +
  560 + // We'll render 7 columns per row.
  561 + // Track active rowspans for col 0(navType), 1(role), 6(sub).
  562 + // We model by emitting blank cells for spanned positions (visually merge by removing borders).
  563 + // Simpler: emit single tall cells via grid-row span.
  564 + let r = 2; // CSS row index (1-based) but auto rows after header row = 1
  565 + // Use explicit grid placement
  566 + let line = 2;
  567 + // First, emit a single big "按角色" cell for col1 spanning all data rows? Original shows rows have nav type only at start.
  568 + // We'll emit "按角色" merged across all rows (24 rows in screenshot share 按角色). Use full span.
  569 + // Emit nav cell once
  570 + const total = kpiRows.length;
  571 + const navCell = document.createElement('div');
  572 + navCell.style.gridColumn = '1';
  573 + navCell.style.gridRow = `2 / span ${total}`;
  574 + navCell.className = 'center';
  575 + navCell.textContent = '按角色';
  576 + host.appendChild(navCell);
  577 +
  578 + let curRow = 2;
  579 + let i = 0;
  580 + while (i < kpiRows.length) {
  581 + const row = kpiRows[i];
  582 + const altClass = (i%2===1)?'row-alt':'';
  583 + // role
  584 + if (row.role) {
  585 + const span = row.roleSpan || 1;
  586 + const c = document.createElement('div');
  587 + c.style.gridColumn = '2';
  588 + c.style.gridRow = `${curRow} / span ${span}`;
  589 + c.className = 'center ' + altClass;
  590 + c.textContent = row.role;
  591 + host.appendChild(c);
  592 + }
  593 + // item
  594 + const item = document.createElement('div');
  595 + item.style.gridColumn = '3';
  596 + item.style.gridRow = `${curRow}`;
  597 + item.className = 'link ' + altClass;
  598 + item.textContent = row.item;
  599 + host.appendChild(item);
  600 + // desc
  601 + const desc = document.createElement('div');
  602 + desc.style.gridColumn = '4';
  603 + desc.style.gridRow = `${curRow}`;
  604 + desc.className = 'link ' + altClass;
  605 + desc.textContent = row.desc;
  606 + host.appendChild(desc);
  607 + // today
  608 + const today = document.createElement('div');
  609 + today.style.gridColumn = '5';
  610 + today.style.gridRow = `${curRow}`;
  611 + today.className = 'num ' + (row.red?'num-red':'') + ' ' + altClass;
  612 + today.textContent = row.today;
  613 + host.appendChild(today);
  614 + // total
  615 + const tot = document.createElement('div');
  616 + tot.style.gridColumn = '6';
  617 + tot.style.gridRow = `${curRow}`;
  618 + tot.className = 'num ' + (row.red?'num-red':'') + ' ' + altClass;
  619 + tot.textContent = row.total;
  620 + host.appendChild(tot);
  621 + // sub
  622 + if (row.sub && row.subSpan) {
  623 + const c = document.createElement('div');
  624 + c.style.gridColumn = '7';
  625 + c.style.gridRow = `${curRow} / span ${row.subSpan}`;
  626 + c.className = 'subproc';
  627 + c.textContent = row.sub;
  628 + host.appendChild(c);
  629 + }
  630 + curRow++;
  631 + i++;
  632 + }
  633 + host.style.gridTemplateRows = `38px repeat(${total}, minmax(38px, auto))`;
  634 +})();
  635 +
  636 +/* ============ USER TABLE ============ */
  637 +const users = [
  638 + ['管广飞','管广飞','ggf','工艺技术','超级管理员','英文','','2026-02-27 17:48:11','超级管理员','2023-10-26 17:02:01'],
  639 + ['李斌','李斌','lib','印前制作','超级管理员','中文','','2026-01-28 16:53:32','超级管理员','2023-10-26 17:02:58'],
  640 + ['系统管理员','','admin','','超级管理员','中文','','2026-05-06 12:28:49','超级管理员','2023-10-26 17:05:58'],
  641 + ['朱财喜','朱财喜','zhucx','印刷车间','超级管理员','中文','','2026-03-23 10:08:29','超级管理员','2023-11-20 10:29:09'],
  642 + ['ljh','ljh','ljh','机修','超级管理员','中文','','2026-05-06 11:14:04','YFZ','2024-10-08 13:48:59'],
  643 + ['wx','汪鑫','wx','工艺技术','超级管理员','中文','','2026-03-23 11:57:13','超级管理员','2023-11-22 13:22:35'],
  644 + ['钱豹','钱豹','qianb','物控部','超级管理员','中文','','2026-04-28 16:49:04','超级管理员','2023-11-27 15:30:11'],
  645 + ['zyf','张寅飞','zyf','印前制作','超级管理员','中文','','2025-09-11 11:42:12','LJH','2024-11-11 15:59:52'],
  646 + ['孟威','孟威','mengw','工艺技术','超级管理员','中文','','2026-05-06 13:56:22','系统管理员','2025-06-03 21:26:07'],
  647 + ['杭仁萍','杭仁萍','hangrp','跟单','超级管理员','中文','','2026-04-30 14:18:28','孟威','2025-06-05 11:11:56'],
  648 + ['李丹','','李丹','','超级管理员','中文','','2026-04-27 13:47:58','杭仁萍','2025-06-11 10:34:29'],
  649 + ['王宽明','王宽明','王宽明','印刷车间','超级管理员','中文','','2026-04-25 16:07:38','李丹','2025-06-11 10:40:22'],
  650 + ['潘茹','潘茹','潘茹','工艺技术','超级管理员','中文','','2025-06-17 09:04:46','李丹','2025-06-11 10:41:07'],
  651 + ['耿广东','耿广东','耿广东','工艺技术','超级管理员','中文','','2025-07-04 14:40:02','李丹','2025-06-11 10:41:37'],
  652 + ['yut','余涛','yut','印刷车间','超级管理员','中文','','2026-04-03 18:39:34','杭仁萍','2025-06-17 14:32:49'],
  653 + ['lzj','廖赵军','lzj','财务部','超级管理员','中文','','','杭仁萍','2025-06-26 10:57:28'],
  654 + ['caojy','caojy','caojy','物控部','超级管理员','中文','','2026-02-02 13:58:14','李明青','2025-07-28 13:59:21'],
  655 + ['陈淑贤','陈淑贤','csx','品质管理部','超级管理员','中文','','2026-04-24 15:05:52','csx','2025-07-29 13:26:58'],
  656 + ['张红英','张红英','zhy','模烫车间','超级管理员','中文','','2025-12-24 16:24:52','系统管理员','2025-08-18 09:34:47'],
  657 + ['lzy','吕政彦','吕政彦','总经理办公室','超级管理员','中文','','2026-04-16 08:54:24','杭仁萍','2025-08-21 11:16:12'],
  658 + ['陈鑫涛','陈鑫涛','cxt','品质管理部','超级管理员','中文','','2026-03-23 10:12:47','陈淑贤','2025-09-01 11:22:00'],
  659 + ['陆鑫','陆鑫','luxin','工艺技术','超级管理员','中文','','2026-05-05 17:56:03','张震','2025-09-04 11:48:44'],
  660 + ['陆鑫-储运部…','陆鑫','ZY0006','工艺技术','普通用户','中文','','2025-11-19 09:11:27','陆鑫','2025-09-05 11:28:37'],
  661 + ['朱咸兵','朱咸兵','zhuxb','工艺技术','超级管理员','中文','','2026-04-27 13:40:15','钱豹','2025-09-08 15:00:29'],
  662 + ['孟臻晟','孟臻晟','mengzs','装订车间','超级管理员','中文','','2026-05-07 09:17:57','系统管理员','2025-09-12 16:24:07'],
  663 + ['pengm','彭敏','pengm','计划管理','超级管理员','中文','','2026-05-06 11:28:33','彭敏','2025-10-16 13:30:32'],
  664 + ['张伟','张伟','zhangw','印刷车间','超级管理员','中文','','2026-03-15 09:22:14','张伟','2025-10-22 10:12:00'],
  665 + ['李娜','李娜','lin','质检部','普通用户','中文','','2026-04-02 14:50:33','李丹','2025-11-04 16:08:21'],
  666 + ['王军','王军','wangj','装订车间','超级管理员','中文','','2026-04-15 17:10:55','系统管理员','2025-11-15 09:30:11'],
  667 + ['赵敏','赵敏','zhaom','财务部','超级管理员','中文','','2026-05-01 08:45:00','赵敏','2025-12-01 11:00:00'],
  668 + ['周强','周强','zhouq','物控部','普通用户','中文','','2026-04-20 10:30:21','钱豹','2025-12-08 14:22:33'],
  669 + ['吴丽','吴丽','wul','人事部','超级管理员','中文','','2026-04-25 15:18:09','吴丽','2026-01-05 09:15:42'],
  670 + ['郑涛','郑涛','zhengt','工艺技术','超级管理员','中文','','2026-05-02 11:40:58','郑涛','2026-01-18 13:55:27'],
  671 + ['冯静','冯静','fengj','客服部','超级管理员','中文','','2026-05-04 16:25:17','冯静','2026-02-02 10:08:14'],
  672 + ['孙磊','孙磊','sunl','装订车间','普通用户','中文','','2026-05-05 09:55:36','系统管理员','2026-02-20 15:32:48'],
  673 + ['马超','马超','mac','机修','超级管理员','中文','','2026-05-06 14:12:25','LJH','2026-03-08 11:48:09'],
  674 + ['朱子纯','朱子纯','zhuzc','总经理办公室','超级管理员','中文','','2026-05-07 13:00:00','超级管理员','2026-03-22 09:00:00'],
  675 +];
  676 +
  677 +(function renderUsers(){
  678 + const tb = document.getElementById('user-tbody');
  679 + users.forEach((u,i)=>{
  680 + const tr = document.createElement('tr');
  681 + tr.innerHTML = `
  682 + <td class="radio-cell"><span class="radio-dot"></span></td>
  683 + <td>${i+1}</td>
  684 + <td>${u[0]}</td>
  685 + <td>${u[1]}</td>
  686 + <td>${u[2]}</td>
  687 + <td>${u[3]}</td>
  688 + <td>${u[4]}</td>
  689 + <td>${u[5]}</td>
  690 + <td><input class="cb" type="checkbox"></td>
  691 + <td>${u[7]}</td>
  692 + <td>${u[8]}</td>
  693 + <td>${u[9]}</td>
  694 + `;
  695 + tr.addEventListener('dblclick', ()=> goTo('userdetail'));
  696 + tb.appendChild(tr);
  697 + });
  698 +})();
  699 +
  700 +/* ============ PERM LIST ============ */
  701 +const perms = ['默认显示(必选)','禁止查看价格','客服跟单','报价组员工','物控部员工','供应链PMC','允许查看订单价格','储运部员工','外部供应商','品质部员工','技术中心员工','机修组员工','生产部计划员工','外发组员工','模烫车间','装订车间','后加工车间','品质部管理','精品车间','人事组','统计组','机修主管','样品开发部员工','设计开发','总经办','审核组','结算组','打样车间','制版组','文控组','行政组','成本组','采购组','OA管理员','开发组','API对接','MES管理员','报表组'];
  702 +(function(){
  703 + const host = document.getElementById('perm-list');
  704 + perms.forEach(p=>{
  705 + const r = document.createElement('div');
  706 + r.className = 'perm-row';
  707 + r.innerHTML = `<span class="cb"></span><span>${p}</span>`;
  708 + host.appendChild(r);
  709 + });
  710 +})();
  711 +
  712 +/* ============ NAV OVERLAY ============ */
  713 +const navSide = [
  714 + {ico:'sales', label:'销售管理'}, {ico:'dcs', label:'DCS系统'}, {ico:'prod', label:'产品管理'},
  715 + {ico:'ops', label:'生产运营'}, {ico:'exec', label:'生产执行'}, {ico:'mold', label:'模具管理'},
  716 + {ico:'cart', label:'采购管理'}, {ico:'mat', label:'材料库存'}, {ico:'fg', label:'成品库存'},
  717 + {ico:'out', label:'外协管理'}, {ico:'logi', label:'物流管理'}, {ico:'qa', label:'质量管理'},
  718 + {ico:'fin', label:'财务管理'}, {ico:'cost1', label:'成本管理(专)'}, {ico:'cost2', label:'成本管理'},
  719 + {ico:'eq', label:'设备管理'}, {ico:'hr', label:'人事行政'}, {ico:'oa', label:'OA系统'},
  720 + {ico:'base', label:'基础设置'}, {ico:'sys', label:'系统设置', active:true},
  721 +];
  722 +const sideIco = {
  723 + sales:'M3 7l3 10h12l3-10M5 7l1-3h12l1 3M9 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z',
  724 + dcs:'M12 2l9 5-9 5-9-5z M3 12l9 5 9-5 M3 17l9 5 9-5',
  725 + prod:'M3 7l9-5 9 5v10l-9 5-9-5z',
  726 + ops:'M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z',
  727 + exec:'M5 4h14v16H5z M5 9h14 M9 4v5',
  728 + mold:'M4 7h16v10H4z M8 7v10 M16 7v10',
  729 + cart:'M5 5h2l3 11h10l2-8H8 M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm9 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z',
  730 + mat:'M4 21V8l8-5 8 5v13z M9 21v-7h6v7',
  731 + fg:'M3 21V9l9-6 9 6v12z',
  732 + out:'M12 12c2 0 4-1 4-4s-2-4-4-4-4 1-4 4 2 4 4 4z M4 21c0-4 4-7 8-7s8 3 8 7',
  733 + logi:'M3 7h11v9H3z M14 10h5l3 3v3h-8z M7 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm10 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z',
  734 + qa:'M12 2l8 4v6c0 5-4 8-8 10-4-2-8-5-8-10V6z M9 12l2 2 4-4',
  735 + fin:'M12 2v20 M7 6h10 M7 10h10',
  736 + cost1:'M4 20V8 M9 20V4 M14 20v-8 M19 20v-6 M2 20h20',
  737 + cost2:'M4 20V8 M9 20V4 M14 20v-8 M19 20v-6 M2 20h20',
  738 + eq:'M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8z M19 12a7 7 0 0 0-.5-2.5l1.5-1.5-2-2-1.5 1.5A7 7 0 0 0 14 7l-.5-2h-3l-.5 2A7 7 0 0 0 7.5 7.5L6 6 4 8l1.5 1.5A7 7 0 0 0 5 12',
  739 + hr:'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M2 21c0-4 3-7 7-7s7 3 7 7 M17 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M22 21c0-3-2-5-5-5',
  740 + oa:'M3 7h18v12H3z M3 11h18 M8 7V4h8v3',
  741 + base:'M4 6h16 M4 12h16 M4 18h16 M8 6v12 M14 6v12',
  742 + sys:'M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8z M19 12a7 7 0 0 0-.5-2.5l1.5-1.5-2-2-1.5 1.5A7 7 0 0 0 14 7l-.5-2h-3l-.5 2A7 7 0 0 0 7.5 7.5L6 6 4 8l1.5 1.5A7 7 0 0 0 5 12',
  743 +};
  744 +const navSideHost = document.getElementById('nav-side');
  745 +navSide.forEach(s=>{
  746 + const d = document.createElement('div');
  747 + d.className = 'si' + (s.active?' active':'');
  748 + d.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="${sideIco[s.ico]||''}"/></svg>${s.label}`;
  749 + navSideHost.appendChild(d);
  750 +});
  751 +
  752 +const navCols = [
  753 + {title:'期初设置', items:['客户期初','供应商期初','材料期初','产品期初','数据导入','离线导出下载']},
  754 + {title:'用户管理', items:[{label:'用户列表',star:true,go:'userlist'},'系统权限','系统权限稽查表','权限组']},
  755 + {title:'系统参数', items:['系统参数','财务结账','系统常量配置']},
  756 + {title:'计算方案', items:['方案列表','计算参数']},
  757 + {title:'日志', items:['个性化模块','操作日志','异常清除KPI任务表','MYSQL监听器']},
  758 + {title:'开发平台', items:['自定义开发范例',{label:'系统功能模块设置',star:true},'EBC流程清单','功能模块界面设置','增删改存业务处理']},
  759 + {title:'API对接管理', items:['调用第三方接口(TOKEN配置)','调用第三方接口(接口定义)','被第三方调用(生成token)','数据同步','被第三方调用(API定义)']},
  760 +];
  761 +const navGridHost = document.getElementById('nav-grid');
  762 +navCols.forEach(c=>{
  763 + const col = document.createElement('div');
  764 + col.className = 'col';
  765 + let html = `<h3>${c.title}</h3>`;
  766 + c.items.forEach(it=>{
  767 + if (typeof it === 'string') html += `<a>${it}</a>`;
  768 + else html += `<a data-go="${it.go||''}">${it.label}${it.star?' <span class="star">★</span>':''}</a>`;
  769 + });
  770 + col.innerHTML = html;
  771 + navGridHost.appendChild(col);
  772 +});
  773 +
  774 +/* ============ NAV / TABS ============ */
  775 +const screens = ['main','userlist','userdetail','login'];
  776 +function goTo(name){
  777 + screens.forEach(s=>document.getElementById('screen-'+s).classList.toggle('active', s===name));
  778 + // hide top bar on login
  779 + document.getElementById('topbar').style.display = (name==='login') ? 'none' : 'flex';
  780 + // tabs visibility
  781 + document.getElementById('tab-userlist').style.display = (['userlist','userdetail'].includes(name) || tabsOpen.userlist) ? 'flex' : 'none';
  782 + document.getElementById('tab-userdetail').style.display = (name==='userdetail' || tabsOpen.userdetail) ? 'flex' : 'none';
  783 + // tab active states
  784 + document.querySelectorAll('.topbar .tab').forEach(t=>t.classList.remove('active'));
  785 + if (name==='main') document.querySelectorAll('.topbar .tab')[0]?.classList.add('active');
  786 + if (name==='userlist') document.getElementById('tab-userlist').classList.add('active');
  787 + if (name==='userdetail') document.getElementById('tab-userdetail').classList.add('active');
  788 + // nav button active when on main with overlay; clear otherwise
  789 + document.getElementById('nav-overlay').classList.remove('show');
  790 + document.getElementById('nav-toggle').classList.remove('active');
  791 + // close login overlay if leaving
  792 +}
  793 +const tabsOpen = {userlist:false, userdetail:false};
  794 +function openTab(name){
  795 + if (name==='userlist'){ tabsOpen.userlist = true; }
  796 + if (name==='userdetail'){ tabsOpen.userlist = true; tabsOpen.userdetail = true; }
  797 + goTo(name);
  798 +}
  799 +
  800 +document.body.addEventListener('click', (e)=>{
  801 + const go = e.target.closest('[data-go]');
  802 + if (go){
  803 + const name = go.dataset.go;
  804 + if (!name) return;
  805 + if (name==='userlist' || name==='userdetail') openTab(name);
  806 + else goTo(name);
  807 + return;
  808 + }
  809 + const close = e.target.closest('[data-close]');
  810 + if (close){
  811 + e.stopPropagation();
  812 + const which = close.dataset.close;
  813 + tabsOpen[which] = false;
  814 + if (which==='userdetail') goTo('userlist');
  815 + else goTo('main');
  816 + if (which==='userlist'){ tabsOpen.userdetail=false; }
  817 + return;
  818 + }
  819 +});
  820 +
  821 +document.getElementById('nav-toggle').addEventListener('click', ()=>{
  822 + const ov = document.getElementById('nav-overlay');
  823 + ov.classList.toggle('show');
  824 + document.getElementById('nav-toggle').classList.toggle('active', ov.classList.contains('show'));
  825 +});
  826 +
  827 +// new-user mode
  828 +function setUserDetailMode(mode){
  829 + const isNew = mode === 'new';
  830 + document.getElementById('f-ctime').textContent = isNew ? '' : '2023-10-26 17:02:01';
  831 + document.getElementById('f-creator').textContent = isNew ? '保存后自动生成' : '超级管理员';
  832 + document.getElementById('f-empname').textContent = isNew ? '' : '管广飞';
  833 + document.getElementById('f-type').textContent = isNew ? '' : '超级管理员';
  834 + document.getElementById('f-lang').textContent = isNew ? '' : '英文';
  835 + document.getElementById('f-username').value = isNew ? '' : '管广飞';
  836 + document.getElementById('f-userno').value = isNew ? '' : 'ggf';
  837 + document.querySelectorAll('#perm-list .perm-row:not(.head) .cb').forEach(cb=>{cb.classList.remove('checked')});
  838 +}
  839 +document.querySelector('[data-add-user]')?.addEventListener('click', ()=>{ setUserDetailMode('new'); openTab('userdetail'); });
  840 +
  841 +// Default initial screen: login
  842 +goTo('login');
  843 +
  844 +// version dropdown demo
  845 +document.getElementById('ver-drop').addEventListener('click', e=>{
  846 + e.currentTarget.classList.toggle('open');
  847 +});
  848 +</script>
  849 +</body>
  850 +</html>
... ...
scripts/setup-test-db.mjs 0 → 100644
  1 +++ a/scripts/setup-test-db.mjs
  1 +#!/usr/bin/env node
  2 +// scripts/setup-test-db.mjs — DROP + CREATE 测试库。
  3 +// 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。
  4 +// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取:schema 经标识符校验后才拼进 SQL(防误删 / 注入,见下方守卫);
  5 +// host / user / password 信任该文件,port 仅校验范围。
  6 +
  7 +import { spawnSync } from 'node:child_process'
  8 +import { existsSync, readFileSync } from 'node:fs'
  9 +import { dirname, join } from 'node:path'
  10 +import { fileURLToPath } from 'node:url'
  11 +
  12 +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
  13 +const CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml')
  14 +
  15 +// 极简 YAML 读取(2 层 map + 标量;与插件 lib/yaml-config.mjs 同规则,内联以免运行时依赖)。
  16 +function parseScalar(raw) {
  17 + let s = String(raw).trim()
  18 + if (s === '' || s[0] === '#') return ''
  19 + const q = s[0]
  20 + if (q === '"' || q === "'") {
  21 + const end = s.indexOf(q, 1)
  22 + if (end !== -1) return s.slice(1, end)
  23 + }
  24 + const hash = s.indexOf(' #')
  25 + if (hash !== -1) s = s.slice(0, hash).trim()
  26 + return s
  27 +}
  28 +function parseYamlConfig(text) {
  29 + const root = {}
  30 + let section = null
  31 + for (const rawLine of text.split('\n')) {
  32 + const line = rawLine.replace(/\r$/, '')
  33 + const trimmed = line.trim()
  34 + if (trimmed === '' || trimmed[0] === '#') continue
  35 + const colon = line.indexOf(':')
  36 + if (colon === -1) continue
  37 + const key = line.slice(0, colon).trim()
  38 + if (key === '') continue
  39 + const indent = line.length - line.replace(/^\s+/, '').length
  40 + const value = parseScalar(line.slice(colon + 1))
  41 + if (indent === 0) {
  42 + if (value === '') {
  43 + section = {}
  44 + root[key] = section
  45 + } else {
  46 + root[key] = value
  47 + section = null
  48 + }
  49 + } else if (section) {
  50 + section[key] = value
  51 + } else {
  52 + root[key] = value
  53 + }
  54 + }
  55 + return root
  56 +}
  57 +
  58 +if (!existsSync(CONFIG_FILE)) {
  59 + console.error(`[setup-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`)
  60 + process.exit(1)
  61 +}
  62 +
  63 +const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {}
  64 +
  65 +const DB_HOST = db.host ?? ''
  66 +const DB_PORT = db.port ?? '3306'
  67 +const DB_USER = db.user ?? ''
  68 +const DB_PASSWORD = db.password ?? ''
  69 +const DB_SCHEMA = db.schema ?? ''
  70 +
  71 +if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) {
  72 + console.error(`[setup-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`)
  73 + process.exit(1)
  74 +}
  75 +
  76 +// schema 是被无条件 DROP + CREATE 的标识符——必须严格校验后才拼进 SQL:
  77 +// · 空值 → 避免 DROP DATABASE `` 这类无意义/误删语句
  78 +// · 「【人工填写】」占位 → 配置尚未填好,不应连库
  79 +// · 含反引号 → 防止 `erp`; DROP DATABASE `prod` 形态的标识符注入(值来自 config-vars.yaml,按 fail-closed 处理)
  80 +// 注:仅接受 ASCII 标识符;非 ASCII schema 名一律拒绝(即便 MySQL / apply-ddl 允许),与推荐的 test/_dev 命名一致
  81 +if (!/^[A-Za-z0-9_$]+$/.test(DB_SCHEMA)) {
  82 + console.error(`[setup-test-db] database.schema 非法或未填: ${JSON.stringify(DB_SCHEMA)}(需为 [A-Za-z0-9_$] 标识符;空值 / 「【人工填写】」占位 / 含反引号均拒绝)`)
  83 + process.exit(1)
  84 +}
  85 +
  86 +console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`)
  87 +
  88 +const sql =
  89 + `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` +
  90 + `CREATE DATABASE \`${DB_SCHEMA}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`
  91 +
  92 +const mysqlArgs = [
  93 + `--host=${DB_HOST}`,
  94 + `--port=${DB_PORT}`,
  95 + `--user=${DB_USER}`,
  96 + `--password=${DB_PASSWORD}`,
  97 + '-e',
  98 + sql,
  99 +]
  100 +const res = spawnSync('mysql', mysqlArgs, { stdio: 'inherit' })
  101 +if (res.error) {
  102 + console.error(`[setup-test-db] FATAL: 无法执行 mysql(请确认其在 PATH 中): ${res.error.message}`)
  103 + process.exit(1)
  104 +}
  105 +if (res.status !== 0) {
  106 + console.error(`[setup-test-db] FAIL: mysql exit=${res.status}`)
  107 + process.exit(res.status === null ? 1 : res.status)
  108 +}
  109 +
  110 +console.log('[setup-test-db] done — schema will be applied by Flyway when Spring Boot starts')
... ...
scripts/test.mjs 0 → 100644
  1 +++ a/scripts/test.mjs
  1 +#!/usr/bin/env node
  2 +// scripts/test.mjs —— 合并到默认分支(main / master)前的测试闸门。
  3 +// 顺序:detect → setup-db → build → lint → unit+integration → e2e
  4 +// (不在尾部 reset:下次跑的 setup-db 会 DROP+CREATE,重复清库无意义)
  5 +// 由 coding.mjs 的 test-gate stage(通过子会话)调用。
  6 +//
  7 +// 跨平台:所有命令经 child_process.spawnSync(cmd, { shell:true }) 执行,
  8 +// 在 Windows 走 cmd.exe,在 *nix 走 /bin/sh,无需 WSL / Git-Bash。
  9 +// 命令字符串来自 docs/04 §零(构建/lint/单测/e2e)——由 skeleton-gen 在 Plan 期填充。
  10 +
  11 +import { spawnSync } from 'node:child_process'
  12 +import { existsSync } from 'node:fs'
  13 +import { dirname, join } from 'node:path'
  14 +import { fileURLToPath } from 'node:url'
  15 +
  16 +const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
  17 +
  18 +// 在指定子目录下跑一条 shell 命令;非零退出码即终止整个闸门并透传该码。
  19 +function run(label, command, cwd = PROJECT_ROOT) {
  20 + console.log(`[test.mjs] ${label}: ${command}`)
  21 + const res = spawnSync(command, { cwd, shell: true, stdio: 'inherit' })
  22 + if (res.error) {
  23 + console.error(`[test.mjs] FATAL: 无法执行 (${label}): ${res.error.message}`)
  24 + process.exit(1)
  25 + }
  26 + if (res.status !== 0) {
  27 + console.error(`[test.mjs] FAIL (${label}) exit=${res.status}`)
  28 + process.exit(res.status === null ? 1 : res.status)
  29 + }
  30 +}
  31 +
  32 +// Stack detection (runtime, mode-agnostic)
  33 +const hasBackend = existsSync(join(PROJECT_ROOT, 'backend'))
  34 +const hasFrontend = existsSync(join(PROJECT_ROOT, 'frontend'))
  35 +if (!hasBackend && !hasFrontend) {
  36 + console.error('[test.mjs] FATAL: neither backend/ nor frontend/ exists')
  37 + process.exit(1)
  38 +}
  39 +
  40 +const backendDir = join(PROJECT_ROOT, 'backend')
  41 +const frontendDir = join(PROJECT_ROOT, 'frontend')
  42 +
  43 +console.log('[test.mjs] 1/5 setup test db')
  44 +run('setup-test-db', `node ${JSON.stringify(join('scripts', 'setup-test-db.mjs'))}`)
  45 +
  46 +console.log('[test.mjs] 2/5 build')
  47 +if (hasBackend) run('backend build', 'mvn -q -B -DskipTests package', backendDir)
  48 +else console.log('[test.mjs] skip backend build')
  49 +if (hasFrontend) run('frontend build', 'npm run build', frontendDir)
  50 +else console.log('[test.mjs] skip frontend build')
  51 +
  52 +console.log('[test.mjs] 3/5 lint')
  53 +if (hasBackend) run('backend lint', 'mvn -q -B checkstyle:check', backendDir)
  54 +else console.log('[test.mjs] skip backend lint')
  55 +if (hasFrontend) run('frontend lint', 'npm run lint', frontendDir)
  56 +else console.log('[test.mjs] skip frontend lint')
  57 +
  58 +console.log('[test.mjs] 4/5 unit + integration')
  59 +if (hasBackend) run('backend test', 'mvn -q -B test', backendDir)
  60 +else console.log('[test.mjs] skip backend test')
  61 +if (hasFrontend) run('frontend test', 'npm run test:unit', frontendDir)
  62 +else console.log('[test.mjs] skip frontend test')
  63 +
  64 +console.log('[test.mjs] 5/5 E2E')
  65 +run('e2e', 'echo "[test.mjs] e2e 略(后端无 e2e;前端 e2e: npm run test:e2e,见 docs/04 §零,前端阶段单独执行)"')
  66 +
  67 +console.log('[test.mjs] GREEN')
... ...
sql/migrations/V1__initial_schema.sql 0 → 100644
  1 +++ a/sql/migrations/V1__initial_schema.sql
  1 +-- Flyway migration V1 — initial schema for 小羚羊
  2 +-- Generated: 2026-06-01T03:43:38Z
  3 +-- Source: 由 A4 db-init 从 docs/03-数据库设计文档.md 翻译生成(schema SSoT 是 docs/03)
  4 +-- This is the FIRST migration; subsequent schema changes must be written as new files sql/migrations/V2__<desc>.sql, V3__... etc.
  5 +-- Apply: Flyway runs this automatically at Spring Boot startup.
  6 +-- Do not hand-edit this file after it is committed; write a new migration instead.
  7 +
  8 +-- ============================================================
  9 +-- CREATE TABLE
  10 +-- ============================================================
  11 +
  12 +CREATE TABLE `usr_user` (
  13 + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  14 + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)',
  15 + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)',
  16 + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)',
  17 + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列,对应制单日期)',
  18 + `sUserName` varchar(50) NOT NULL COMMENT '用户名,登录账号,系统内全局唯一(3-20 位字母数字下划线)',
  19 + `sUserNo` varchar(50) NULL COMMENT '用户号,关联职员后可自动带出员工编号/姓名',
  20 + `sPassword` varchar(100) NOT NULL COMMENT '登录密码,BCrypt 哈希存储(初始密码 666666)',
  21 + `iEmployeeId` int NULL COMMENT '关联职员 ID(可选),外键 -> usr_employee.iIncrement',
  22 + `sUserType` varchar(20) NOT NULL DEFAULT '普通用户' COMMENT '用户类型:普通用户 / 超级管理员',
  23 + `sLanguage` varchar(20) NOT NULL DEFAULT '中文' COMMENT '界面语言:中文 / 英文 / 繁体',
  24 + `iCanModifyBill` tinyint(1) NOT NULL DEFAULT 0 COMMENT '单据修改权限:0 否 / 1 是',
  25 + `iIsVoid` tinyint(1) NOT NULL DEFAULT 0 COMMENT '作废/禁用标志:0 正常 / 1 已作废(禁用后不可登录)',
  26 + `tLastLoginDate` datetime NULL COMMENT '最后登录时间,登录成功时更新',
  27 + `sCreator` varchar(50) NULL COMMENT '制单人(创建该用户的操作员)',
  28 + PRIMARY KEY (`iIncrement`)
  29 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表:登录账号与用户属性核心表';
  30 +
  31 +CREATE TABLE `usr_employee` (
  32 + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  33 + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)',
  34 + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)',
  35 + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)',
  36 + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  37 + `sEmployeeName` varchar(50) NOT NULL COMMENT '职员/员工姓名(用户员工名下拉来源)',
  38 + `sEmployeeNo` varchar(50) NULL COMMENT '员工编号',
  39 + `sDepartment` varchar(100) NULL COMMENT '所属部门(用户查询输出部门来源)',
  40 + PRIMARY KEY (`iIncrement`)
  41 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职员表:员工名/部门等支撑信息';
  42 +
  43 +CREATE TABLE `usr_company` (
  44 + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  45 + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)',
  46 + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)',
  47 + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)',
  48 + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  49 + `sCompanyName` varchar(100) NOT NULL COMMENT '公司名称(登录页版本下拉的显示来源)',
  50 + `sVersion` varchar(50) NULL COMMENT '版本/账套标识',
  51 + PRIMARY KEY (`iIncrement`)
  52 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司表:登录版本下拉数据来源';
  53 +
  54 +CREATE TABLE `usr_permission` (
  55 + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  56 + `sId` varchar(100) NULL COMMENT '业务 ID(标准列)',
  57 + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)',
  58 + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)',
  59 + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  60 + `sPermissionName` varchar(100) NOT NULL COMMENT '权限名称',
  61 + `sPermissionCode` varchar(100) NOT NULL COMMENT '权限编码(程序判定用,系统内唯一)',
  62 + `sPermissionCategory` varchar(100) NULL COMMENT '权限分类(权限组的权限分类)',
  63 + PRIMARY KEY (`iIncrement`)
  64 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表:可分配权限项定义';
  65 +
  66 +CREATE TABLE `usr_user_permission` (
  67 + `iIncrement` int NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  68 + `sId` varchar(100) NULL COMMENT '业务 ID(标准列;关联表对外不暴露,可留空)',
  69 + `sBrandsId` varchar(100) NULL DEFAULT '1111111111' COMMENT '品牌 ID,多租户隔离(标准列)',
  70 + `sSubsidiaryId` varchar(100) NULL DEFAULT '1111111111' COMMENT '子公司 ID,组织层级隔离(标准列)',
  71 + `tCreateDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(标准列)',
  72 + `iUserId` int NOT NULL COMMENT '用户 ID,外键 -> usr_user.iIncrement',
  73 + `iPermissionId` int NOT NULL COMMENT '权限 ID,外键 -> usr_permission.iIncrement',
  74 + PRIMARY KEY (`iIncrement`)
  75 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户权限关联表:用户↔权限多对多授权';
  76 +
  77 +-- ============================================================
  78 +-- CREATE INDEX
  79 +-- ============================================================
  80 +
  81 +CREATE UNIQUE INDEX `uk_usr_user_username` ON `usr_user` (`sUserName`);
  82 +CREATE INDEX `idx_usr_user_employee` ON `usr_user` (`iEmployeeId`);
  83 +CREATE INDEX `idx_usr_user_type` ON `usr_user` (`sUserType`);
  84 +CREATE INDEX `idx_usr_user_tenant` ON `usr_user` (`sBrandsId`, `sSubsidiaryId`);
  85 +
  86 +CREATE INDEX `idx_usr_employee_name` ON `usr_employee` (`sEmployeeName`);
  87 +CREATE INDEX `idx_usr_employee_tenant` ON `usr_employee` (`sBrandsId`, `sSubsidiaryId`);
  88 +
  89 +CREATE UNIQUE INDEX `uk_usr_company_name` ON `usr_company` (`sCompanyName`);
  90 +
  91 +CREATE UNIQUE INDEX `uk_usr_permission_code` ON `usr_permission` (`sPermissionCode`);
  92 +CREATE INDEX `idx_usr_permission_category` ON `usr_permission` (`sPermissionCategory`);
  93 +
  94 +CREATE UNIQUE INDEX `uk_usr_user_permission` ON `usr_user_permission` (`iUserId`, `iPermissionId`);
  95 +CREATE INDEX `idx_usr_user_permission_perm` ON `usr_user_permission` (`iPermissionId`);
  96 +
  97 +-- ============================================================
  98 +-- ADD FOREIGN KEY
  99 +-- ============================================================
  100 +
  101 +ALTER TABLE `usr_user`
  102 + ADD CONSTRAINT `fk_usr_user_employee` FOREIGN KEY (`iEmployeeId`) REFERENCES `usr_employee` (`iIncrement`) ON DELETE SET NULL ON UPDATE CASCADE;
  103 +
  104 +ALTER TABLE `usr_user_permission`
  105 + ADD CONSTRAINT `fk_usr_up_user` FOREIGN KEY (`iUserId`) REFERENCES `usr_user` (`iIncrement`) ON DELETE CASCADE ON UPDATE CASCADE;
  106 +
  107 +ALTER TABLE `usr_user_permission`
  108 + ADD CONSTRAINT `fk_usr_up_permission` FOREIGN KEY (`iPermissionId`) REFERENCES `usr_permission` (`iIncrement`) ON DELETE CASCADE ON UPDATE CASCADE;
... ...
src/styles/tokens.css 0 → 100644
  1 +++ a/src/styles/tokens.css
  1 +/*
  2 + * src/styles/tokens.css — Design Tokens(色值的单一来源 / SSoT)
  3 + *
  4 + * 命名格式:--color-<scope>-<role>-<state>
  5 + * <scope> 组件域:form / table-row / table-header / ...
  6 + * <role> 作用:bg(背景)/ fg(前景/字体)/ border
  7 + * <state> 状态:edit / readonly / hover / selected(无状态时省略)
  8 + *
  9 + * 约束:
  10 + * - 组件样式中只用 var(--color-xxx),禁止硬编码 hex / rgba
  11 + * - 修改色值只改本文件,不允许在组件级覆盖
  12 + * - 新增 token 直接在本文件登记(本文件即单一来源)
  13 + */
  14 +
  15 +:root {
  16 + /* === 1. 全局调色板(与 Ant Design 主题对齐) === */
  17 + --color-primary: #1890ff;
  18 + --color-success: #52c41a;
  19 + --color-warning: #faad14;
  20 + --color-error: #ff4d4f;
  21 + --color-text: rgba(0, 0, 0, 0.85);
  22 + --color-text-secondary: rgba(0, 0, 0, 0.45);
  23 + --color-border: #d9d9d9;
  24 + --color-bg-base: #f0f2f5;
  25 +
  26 + /* === 2. 组件级状态色 === */
  27 +
  28 + /* form:输入框 / 备注框 / 时间框 / 下拉框共用 */
  29 + --color-form-bg-edit: #ffffff;
  30 + --color-form-bg-readonly: #f1f2f8;
  31 + --color-form-bg-hover: #f5f5f5; /* 仅下拉框使用 */
  32 + --color-form-fg: #000000;
  33 +
  34 + /* table */
  35 + --color-table-row-bg-selected: #86d5fb;
  36 + --color-table-row-bg-hover: #fff7e6;
  37 + --color-table-row-bg-readonly: #f1f2f8; /* = rgb(241, 242, 248) */
  38 + --color-table-row-fg: #000000;
  39 + --color-table-header-bg: #f5f5f5;
  40 + --color-table-header-fg: rgba(0, 0, 0, 0.85); /* = #000000D9 */
  41 +}
... ...