Commit cc0c84626d78cfb224bcff5e306227d147482778
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 前端原型
Showing
19 changed files
with
2005 additions
and
0 deletions
.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
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 | +} |