diff --git a/AGENTS.md b/AGENTS.md index 15e5109c..089b9456 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,14 +1,17 @@ # AGENTS.md -适用范围:`D:\AI Code\3danzhuang` 全仓库。 +适用范围:`D:\AI Code\3danzhuang-wuyongjie-mainline` 全仓库。 未单独说明时,先遵循全局规则:`C:\Users\54972\.codex\AGENTS.md`。 这份文件只补充本仓库特有规则,不重复全局通用要求。 +重要边界:`D:\AI Code\3danzhuang` 是历史旧现场;当前正式工作目录以 `D:\AI Code\3danzhuang-wuyongjie-mainline` 为准,除非用户明确要求,不要混用旧目录。 + ## 1. 默认执行口径 - 用户未指定目录时,默认在 `furniture-3d-system/` 执行。 - 当任务发生在 `furniture-3d-system/` 时,默认按当前会话直接开发。 -- 未经用户明确要求,不主动 `commit / push`。 +- 常规任务分支在完成必要验证、确认范围和目标后,可以按团队协作标准流程主动 `commit / push`。 +- `main`、生产发布、破坏性操作、删除数据、改远端历史等高风险动作仍必须由用户明确授权。 - 禁止破坏性操作,除非用户明确授权。 - 没有“规范审查结论”,任务不算完成。 @@ -34,22 +37,47 @@ ## 4. 协作与分支口径 -- `main` 只作为 `WuYongJie` 的主线收口分支,其他协作者默认不能直接推主线。 -- `xuxiake` 的长期包装分支固定为 `codex/xuxiake-packaging`。 -- `xuxiake` 的临时任务分支固定为 `codex/xuxiake-packaging-<任务简称>`。 +- `main` 是最终发布主线,不能随便直接推;生产发布必须另行确认。 +- `origin/codex/xuxiake-mainline` 是当前共享开发主线和默认同步基线。 +- 新功能、优化和修复默认从最新 `origin/codex/xuxiake-mainline` 开任务分支,再按协作流程回流。 - 新分支统一使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。 - 不再使用 `feature/packaging`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。 -- 长期包装分支同步 `main` 时固定使用 `merge main`。 -- 临时个人任务分支同步 `main` 时才使用 `rebase main`。 +- 长期共享分支同步当前共享开发主线时固定使用 `merge`。 +- 临时个人任务分支只有在确认无人共用时才使用 `rebase`。 -## 5. Gemini 前端协作口径 +## 5. 白曜石工坊视觉口径 -- 如项目已指定 `Gemini(前端专家)` 负责前端,则前端代码、UI 视觉、排版动效和 `3D(立体)` 表现层默认由 Gemini 主导。 -- 我不主动改 Gemini 已定稿的前端样式;我默认负责底层逻辑、接口、数据、数据库、部署和非前端部分。 -- 与 Gemini 联调时,必须优先遵守 Gemini 已确定的数据结构和类型口径,避免因为字段或格式不一致导致前端渲染异常。 -- `docs/前端UI改动清单.md` 只作为当前轮次的轻量对接板使用,不作为长期规范来源、历史总账或唯一协作入口。 +- `furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md` 是当前新平台第一视觉标准。 +- 新平台主链页面,以及 `/admin`、`/users` 等后台管理页面,默认都要向白曜石工坊口径统一。 +- 白曜石工坊默认是浅色工作台;不再提供深浅色主题切换入口,也不要把后台页面继续做成默认黑底。 +- 页面应保持工作台感、低噪音、克制、层级清楚,不能退回普通 SaaS 卡片墙或营销式大卡片布局。 +- `Inspector(检视器)`、数据列表、表格、长文本和表单密集区必须优先保证可读性,不能用毛玻璃承载正文。 +- 毛玻璃只允许用于顶栏、HUD、浮动工具条和轻量状态浮层。 +- 板材、五金、组件、包装等资产应按 `Meta Component(元组件)` 表达,而不是普通图片卡片。 +- 如果苹果风格视觉标准与白曜石工坊标准冲突,新平台主链和后台管理页面优先执行白曜石工坊标准;苹果风格标准只作为基础组件、交互细节和可访问性补充参考。 -## 6. 规范审查 +## 6. 前端责任口径 + +- 未明确指定其他协作者时,前端、视觉、交互、`3D(立体)` 表现层和系统联调默认由当前 Codex 全权负责。 +- 前端实现必须遵守白曜石工坊视觉标准、模块边界和最小验证要求。 +- 处理前端页面时,优先用当前代码、实际页面表现、截图结论和设计规范判断,不再套用旧模型时期的保守分工。 +- 复杂任务可以主动拆计划、并行排查和验证,但不能因为模型更强就扩大改动范围或跳过规范审查。 + +## 7. 截图和上下文续接 + +- 工程对话可以正常使用截图,但截图只用于当前判断。 +- 压缩上下文、写会话恢复摘要或给新对话续接时,只保留文字结论、关键决策、当前状态和下一步。 +- 不继承、不复述 `data:image base64(图片编码文本)`,也不要把图片原始内容写进摘要。 +- 如果同一问题需要连续大量截图,优先开短图像对话处理,最后只把文字结论合回主工程对话。 + +## 8. 敏感信息和交接材料 + +- 账号、密码、服务器地址、访问密钥、数据库连接、S3/MinIO 密钥等敏感信息,只能放在本地续接材料里。 +- 含敏感信息的摘要、日志或交接文件不能提交 Git,不能发给同事,不能写进共享文档。 +- 仓库内协作文档只写非敏感入口、流程和职责;凭据类信息必须用 `[REDACTED_SECRET]` 或本地路径说明替代。 +- 发现摘要文件里混有明文凭据时,默认先提示风险,再决定是否拆成本地敏感版和仓库协作版。 + +## 9. 规范审查 规范审查固定只查三件事: @@ -66,10 +94,13 @@ 只要架构违规、视觉违规,或明显是补丁式修复,就必须 `rejected`。 如果这次不是独立复审,必须明确写出“这是自审,不是独立复审”。 -统一清单:`docs/workflow/规范审查清单.md` +统一清单:`furniture-3d-system/docs/workflow/规范审查清单.md` -## 7. 标准来源 +## 10. 标准来源 + +- `furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md` +- `furniture-3d-system/docs/workflow/规范审查清单.md` + +补充参考: -- `furniture-3d-system/.cursor/rules/*.mdc` - `furniture-3d-system/docs/设计规范/苹果风格视觉标准.md` -- `docs/workflow/规范审查清单.md` diff --git a/furniture-3d-system/docs/workflow/主线同步操作手册.md b/furniture-3d-system/docs/workflow/主线同步操作手册.md index 1ce12d50..5a194f44 100644 --- a/furniture-3d-system/docs/workflow/主线同步操作手册.md +++ b/furniture-3d-system/docs/workflow/主线同步操作手册.md @@ -1,18 +1,19 @@ # 主线同步操作手册 适用对象:会用 Codex 协作、但不一定熟悉 Git 的同事。 -目标:当 `main` 持续更新时,你仍然能把自己的分支保持在最新状态,不把代码做旧,不把冲突拖到最后一天。 +目标:当共享开发主线持续更新时,你仍然能把自己的分支保持在最新状态,不把代码做旧,不把冲突拖到最后一天。 --- ## 1. 先记住一句话 你不要直接改 `main`。 -你只在自己的分支里开发,然后经常把最新的 `main` 同步进来。 +你只在自己的分支里开发,然后经常把最新的 `origin/codex/xuxiake-mainline` 同步进来。 当前推荐分工: -- `WuYongJie` 负责主线 `main` +- `WuYongJie` 负责最终发布主线 `main` +- 日常开发默认以 `origin/codex/xuxiake-mainline` 为共享开发主线 - 其他协作者在自己的长期共享分支或临时任务分支开发 分支命名固定规则: @@ -53,25 +54,24 @@ git status --short - 要保留并提交:先提交到你当前分支 - 只是临时改动:先暂存(stash)再继续 -不要在“有一堆未提交改动”时直接 `merge main` 或 `rebase main`。 +不要在“有一堆未提交改动”时直接 `merge origin/codex/xuxiake-mainline` 或 `rebase origin/codex/xuxiake-mainline`。 --- 如果你现在在长期共享分支,就把下面这 5 行命令原样交给 Codex 执行: ```bash -git checkout main -git pull origin main +git fetch origin git checkout <你的长期共享分支> -git merge main +git merge origin/codex/xuxiake-mainline git push origin <你的长期共享分支> ``` 这组命令的白话意思: -1. 先把本地 `main` 拉到最新 +1. 先刷新远端分支信息 2. 切回你的长期共享分支 -3. 把最新主线合进这个共享分支 +3. 把最新共享开发主线合进这个共享分支 4. 再把共享分支推上去 --- @@ -81,10 +81,9 @@ git push origin <你的长期共享分支> 只有在你从长期共享分支再开出个人任务分支时,才用下面这套: ```bash -git checkout main -git pull origin main +git fetch origin git checkout <你的分支名> -git rebase main +git rebase origin/codex/xuxiake-mainline git push --force-with-lease origin <你的分支名> ``` @@ -125,7 +124,7 @@ git push --force-with-lease origin <你的分支名> 可以直接把下面这段话发给 Codex: ```text -我在同步 main 到我的分支时遇到冲突了。 +我在同步 `origin/codex/xuxiake-mainline` 到我的分支时遇到冲突了。 请只帮我处理这几个冲突文件,不要扩范围,不要改无关模块。 优先保留主线最新结构,再把我这个分支的目标改动接进去。 处理后请告诉我还需要执行什么命令。 @@ -139,7 +138,7 @@ git push --force-with-lease origin <你的分支名> - 不要长时间不同步主线 - 不要等“全部做完了”才第一次同步 - 不要把 `dev` 当万能长期分支名 -- 不要对长期共享分支执行 `git rebase main` +- 不要对长期共享分支执行 `git rebase origin/codex/xuxiake-mainline` - 不要看到冲突就随手全选自己的版本 - 不要在没看懂的情况下执行 `git reset --hard` @@ -150,7 +149,7 @@ git push --force-with-lease origin <你的分支名> 当同事分支已经推上来后,`WuYongJie` 固定按下面顺序做: 1. 先看交接说明,确认本次只做什么、没做什么、验证跑了什么 -2. 看分支有没有同步最新 `main` +2. 看分支有没有同步最新 `origin/codex/xuxiake-mainline` 3. 看改动有没有超出任务范围,或者碰到共享接缝 4. 再决定这次是 `merge`、`cherry-pick`,还是手工整合 @@ -174,7 +173,7 @@ git push --force-with-lease origin <你的分支名> 如果你懒得记,只记这一段: ```text -每天开工前,先把 main 拉最新,再把 main merge 进你的长期共享分支。 +每天开工前,先 fetch 远端,再把 `origin/codex/xuxiake-mainline` merge 进你的长期共享分支。 只有你临时开的个人任务分支才用 rebase。 如果冲突了,不要硬处理,先让 Codex 只处理冲突文件。 ``` diff --git a/furniture-3d-system/docs/workflow/同事交接模板.md b/furniture-3d-system/docs/workflow/同事交接模板.md index 218623c1..7bc5c4da 100644 --- a/furniture-3d-system/docs/workflow/同事交接模板.md +++ b/furniture-3d-system/docs/workflow/同事交接模板.md @@ -20,10 +20,10 @@ ```text 任务名称:<名称> -目标分支:<固定写你要推到哪个分支,例如 codex/xuxiake-packaging 或 codex/xuxiake-packaging-<任务简称>> +目标分支:<固定写你要推到哪个分支,例如 codex/<负责人>-<工作域> 或 codex/<负责人>-<工作域>-<任务简称>> 当前状态:<已完成到哪里> 你接手的部分:<只写这次要做的内容> -分支名称:<默认写 codex/xuxiake-packaging;如果是临时小任务,再写 codex/xuxiake-packaging-<任务简称>> +分支名称:<默认写 codex/<负责人>-<工作域>;如果是临时小任务,再写 codex/<负责人>-<工作域>-<任务简称>> 本次只改:<一句话说清本次改动范围> 本次明确没改:<一句话写清这次没碰什么> 是否碰共享接缝:<没有 / 有,改了哪些,为什么非改不可> @@ -89,17 +89,15 @@ - 按需跑相关测试 - 能跑就跑 `npm run build` -如果你当前在长期包装分支上,请先同步主线再继续开发: -- `git checkout main` -- `git pull origin main` -- `git checkout codex/xuxiake-packaging` -- `git merge main` +如果你当前在长期共享分支上,请先同步共享开发主线再继续开发: +- `git fetch origin` +- `git checkout <你的长期共享分支>` +- `git merge origin/codex/xuxiake-mainline` 如果你当前在自己的临时任务分支上,再用: -- `git checkout main` -- `git pull origin main` +- `git fetch origin` - `git checkout <你的分支>` -- `git rebase main` +- `git rebase origin/codex/xuxiake-mainline` 完成后请按下面格式回报: 1. 现在你要做什么 @@ -120,5 +118,5 @@ - 没写“本次明确没改什么” - 没写有没有碰共享接缝 - 没写建议主线怎么收这次分支 -- 没提醒对方先同步最新 `main` +- 没提醒对方先同步最新 `origin/codex/xuxiake-mainline` - 没同步文档,导致下一个人继续靠猜 diff --git a/furniture-3d-system/docs/workflow/同事本地AGENTS使用说明.md b/furniture-3d-system/docs/workflow/同事本地AGENTS使用说明.md index 76a7e48e..3a0d9265 100644 --- a/furniture-3d-system/docs/workflow/同事本地AGENTS使用说明.md +++ b/furniture-3d-system/docs/workflow/同事本地AGENTS使用说明.md @@ -15,9 +15,9 @@ - 仓库里的 `AGENTS.md` 继续保留为团队共享规则 - 你同事在自己电脑上,再单独放一份“本地工作区级”的 `AGENTS.md` -当前这个项目里,真正的团队共享规则入口是上一层工作区的: +当前这个项目里,真正的团队共享规则入口是当前正式仓库根目录的: -- `D:\AI Code\3danzhuang\AGENTS.md` +- `D:\AI Code\3danzhuang-wuyongjie-mainline\AGENTS.md` 它已经明确要求: @@ -129,7 +129,7 @@ furniture-3d-system-codex-workspace/ - 先看哪些文档 - 这位同事当前只负责哪些模块 - 哪些目录绝对不要碰 -- 开工前先同步 `main` +- 开工前先同步 `origin/codex/xuxiake-mainline` - 结果汇报格式要简单 不适合放进去的内容: diff --git a/furniture-3d-system/docs/workflow/同事本地AGENTS模板.md b/furniture-3d-system/docs/workflow/同事本地AGENTS模板.md index 8be7de04..c74ac082 100644 --- a/furniture-3d-system/docs/workflow/同事本地AGENTS模板.md +++ b/furniture-3d-system/docs/workflow/同事本地AGENTS模板.md @@ -61,7 +61,7 @@ ## 5. 开发前的固定动作 -- 开工前先同步最新 `main` +- 开工前先同步最新 `origin/codex/xuxiake-mainline` - 主负责人刚推了重要改动后,再同步一次 - 自己准备提交前,再同步一次 @@ -72,27 +72,25 @@ 固定分支命名: - 统一用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>` -- 如果你是 `xuxiake`,长期分支固定使用 `codex/xuxiake-packaging` -- 只有临时小任务才使用 `codex/xuxiake-packaging-<任务简称>` +- 长期分支使用 `codex/<负责人>-<工作域>` +- 临时小任务使用 `codex/<负责人>-<工作域>-<任务简称>` - 不要使用 `dev` 这种看不出职责的泛分支名 默认同步步骤: ```bash -git checkout main -git pull origin main -git checkout codex/xuxiake-packaging -git merge main -git push origin codex/xuxiake-packaging +git fetch origin +git checkout <你的长期共享分支> +git merge origin/codex/xuxiake-mainline +git push origin <你的长期共享分支> ``` 如果你临时开的只是个人任务分支,再改用: ```bash -git checkout main -git pull origin main +git fetch origin git checkout <你的分支名> -git rebase main +git rebase origin/codex/xuxiake-mainline git push --force-with-lease origin <你的分支名> ``` diff --git a/furniture-3d-system/docs/workflow/团队协作模式.md b/furniture-3d-system/docs/workflow/团队协作模式.md index 843508b5..57021086 100644 --- a/furniture-3d-system/docs/workflow/团队协作模式.md +++ b/furniture-3d-system/docs/workflow/团队协作模式.md @@ -29,7 +29,7 @@ ### 先固定两件事 -- `main` 继续只由 `WuYongJie` 直接 push 和收口。 +- `main` 是最终发布主线,由 `WuYongJie` 收口;日常开发默认同步 `origin/codex/xuxiake-mainline`。 - 这份文档现在只固定 Git 协作方式、主线收口规则和共享接缝处理原则,不再把当前模块职责长期写死在这里。 ### 当前主线入口 @@ -69,9 +69,9 @@ ### 主分支 -- `main` 继续保持受保护状态,且当前只允许 `WuYongJie` 直接 push 和收口。 +- `main` 继续保持受保护状态,只作为最终发布主线收口。 - 其他协作者默认不能直接 push `main`,只能走自己的功能分支。 -- 这样做的目的不是放开主线,而是保证主负责人可以快速收口,同事不会误把半成品直接推上主线。 +- 日常开发基线是 `origin/codex/xuxiake-mainline`,不要把半成品直接推上最终发布主线。 ### 开发分支 @@ -92,13 +92,13 @@ - `codex/wuyongjie-mainline` - `codex/wuyongjie-mainline-asset-center` -- `codex/xuxiake-packaging` -- `codex/xuxiake-intake-warehouse-clean-20260415` +- `codex/xuxiake-costing` +- `codex/xuxiake-intake-clean` - `codex/zhangsan-cad-compat` ### 合并方式 -- 主负责人可以直接在 `main` 收口、修主线、推主线。 +- 主负责人负责把稳定内容从共享开发主线收口到 `main`。 - 其他协作者默认先推自己的功能分支。 - 同事的改动进入主线前,默认先由主负责人检查和收口。 - 协作者不要自己合并到 `main`,也不要自己改远端主线历史。 @@ -106,7 +106,7 @@ 当前固定流程: -1. 协作者先从最新 `main` 同步到自己的长期分支或任务分支。 +1. 协作者先从最新 `origin/codex/xuxiake-mainline` 同步到自己的长期分支或任务分支。 2. 协作者只在自己负责的工作域内开发。 3. 协作者推送自己的分支,并附上交接说明。 4. `WuYongJie` 负责检查范围、决定 merge / cherry-pick / 手工收口。 @@ -116,7 +116,7 @@ `WuYongJie` 收同事分支前,固定先看这 6 项: -1. 这个分支是不是已经同步过最新 `main` +1. 这个分支是不是已经同步过最新 `origin/codex/xuxiake-mainline` 2. 这次改动是不是只落在任务约定范围内 3. 有没有碰共享接缝;如果碰了,交接里有没有写清原因 4. 有没有最小验证结果,而不是只说“应该可以” @@ -131,7 +131,7 @@ - 直接 `merge` - 适用:改动范围干净,只在同事负责域内,没有混入共享接缝或无关改动 - - 前提:已经同步最新 `main`,并且附了最小验证和交接说明 + - 前提:已经同步最新 `origin/codex/xuxiake-mainline`,并且附了最小验证和交接说明 - 优先 `cherry-pick` - 适用:同一个分支里既有可收的目标提交,也混入了暂时不该进主线的提交 @@ -142,16 +142,16 @@ - 常见场景:同一个文件同时碰到主线重构区和同事任务区 - 暂不收口 - - 适用:没同步最新 `main`、没交接说明、没最小验证、范围失控、直接改了敏感共享接缝但没写原因 + - 适用:没同步最新 `origin/codex/xuxiake-mainline`、没交接说明、没最小验证、范围失控、直接改了敏感共享接缝但没写原因 - 处理方式:先退回补说明、补同步、补验证,再继续看 ### 同事和主线怎么保持同步 当前项目默认采用这个口径: -- `WuYongJie` 负责持续更新和收口 `main` +- `WuYongJie` 负责维护共享开发主线,并把稳定内容收口到 `main` - 协作者长期在自己的长期共享分支或当前任务分支开发 -- 同事不能等做完了再一次性同步,而是要高频同步 `main` +- 同事不能等做完了再一次性同步,而是要高频同步 `origin/codex/xuxiake-mainline` 默认同步时机: @@ -161,33 +161,31 @@ 默认推荐方式: -- 长期协作分支固定优先使用 `merge main` -- 只有临时个人任务分支才优先使用 `rebase main` +- 长期协作分支固定优先使用 `merge origin/codex/xuxiake-mainline` +- 只有临时个人任务分支才优先使用 `rebase origin/codex/xuxiake-mainline` - 但不要一边长期不同步,一边继续堆改动 白话规则: -- 长期共享分支保留真实协作历史,所以固定用 `merge main` -- 临时个人任务分支只服务当前任务,为了保持提交线清楚,才用 `rebase main` +- 长期共享分支保留真实协作历史,所以固定用 `merge origin/codex/xuxiake-mainline` +- 临时个人任务分支只服务当前任务,为了保持提交线清楚,才用 `rebase origin/codex/xuxiake-mainline` - 如果你拿不准自己现在是不是长期共享分支,就先按长期共享分支处理,不要擅自改写共享历史 长期共享分支日常同步固定步骤: ```bash -git checkout main -git pull origin main +git fetch origin git checkout <你的长期共享分支> -git merge main +git merge origin/codex/xuxiake-mainline git push origin <你的长期共享分支> ``` 如果你临时开了个人任务分支,再用: ```bash -git checkout main -git pull origin main +git fetch origin git checkout <你的临时任务分支> -git rebase main +git rebase origin/codex/xuxiake-mainline git push --force-with-lease origin <你的临时任务分支> ``` diff --git a/furniture-3d-system/docs/workflow/模块边界说明.md b/furniture-3d-system/docs/workflow/模块边界说明.md index 20f14a47..09ac76c5 100644 --- a/furniture-3d-system/docs/workflow/模块边界说明.md +++ b/furniture-3d-system/docs/workflow/模块边界说明.md @@ -5,7 +5,7 @@ 配套入口: - 先读 `docs/workflow/团队协作模式.md`,确认你属于哪个工作域、该用什么分支名 -- 再读 `docs/workflow/主线同步操作手册.md`,确认你当前分支该用 `merge main` 还是 `rebase main` +- 再读 `docs/workflow/主线同步操作手册.md`,确认你当前分支该用 `merge origin/codex/xuxiake-mainline` 还是 `rebase origin/codex/xuxiake-mainline` - 这份文档只负责讲边界,不额外创造新的角色体系或新的协作入口 - 这份文档只固定系统边界,不固定当前负责人;具体任务归属以当前任务单、交接摘要和已确认协作信息为准 diff --git a/furniture-3d-system/docs/workflow/规范审查清单.md b/furniture-3d-system/docs/workflow/规范审查清单.md index 7a42f10c..41471751 100644 --- a/furniture-3d-system/docs/workflow/规范审查清单.md +++ b/furniture-3d-system/docs/workflow/规范审查清单.md @@ -12,6 +12,8 @@ ### 视觉有没有跑偏 - 有没有明显偏离当前统一视觉标准 +- 新平台主链页面、后台管理页面是否优先符合 `白曜石工坊统一视觉标准.md` +- `苹果风格视觉标准.md` 是否只作为基础组件、交互细节和可访问性补充参考 - 有没有临时拼凑感很强的新界面 - 新页面和现有页面的层级、间距、交互有没有失控 diff --git a/furniture-3d-system/docs/workflow/高频协作五条铁律.md b/furniture-3d-system/docs/workflow/高频协作五条铁律.md index 6765e79b..703bcd69 100644 --- a/furniture-3d-system/docs/workflow/高频协作五条铁律.md +++ b/furniture-3d-system/docs/workflow/高频协作五条铁律.md @@ -19,9 +19,9 @@ ## 2. 五条铁律 -### 铁律 1:开工先同步主线 +### 铁律 1:开工先同步共享开发主线 -- 每天开始开发前,先同步一次最新 `main` +- 每天开始开发前,先同步一次最新 `origin/codex/xuxiake-mainline` - 主负责人刚推了重要改动后,再同步一次 - 自己准备提交前,再同步一次 @@ -47,7 +47,7 @@ ### 铁律 4:冲突先停,不硬解 -- 同步 `main` 出现冲突时,先判断是不是自己负责的模块 +- 同步 `origin/codex/xuxiake-mainline` 出现冲突时,先判断是不是自己负责的模块 - 不是自己负责的模块,先问主负责人 - 是自己负责的模块,再让 Codex 只处理冲突文件 @@ -72,7 +72,7 @@ ```md ## 高频协作五条铁律 -- 开工前、刚有人更新主线后、准备提交前,先同步最新 `main` +- 开工前、刚有人更新共享开发主线后、准备提交前,先同步最新 `origin/codex/xuxiake-mainline` - 每次任务先写清楚:这次只改什么、不改什么、哪些目录不能碰 - 公共模块和主链骨架优先由我收口,不让协作者顺手改成多份真相 - 遇到冲突先停下来判断边界,不要硬解 @@ -88,7 +88,7 @@ ```md ## 高频协作五条铁律 -- 开工前、主负责人刚更新主线后、准备提交前,先同步最新 `main` +- 开工前、主负责人刚更新共享开发主线后、准备提交前,先同步最新 `origin/codex/xuxiake-mainline` - 每次任务先说清楚:这次只改什么、不改什么、哪些目录不能碰 - 主导航、主流程骨架、公共数据结构,不要擅自大改 - 同步主线遇到冲突时,先只处理自己负责的文件,不要硬改别人的模块 diff --git a/furniture-3d-system/docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md b/furniture-3d-system/docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md index 72e04509..6b304fdd 100644 --- a/furniture-3d-system/docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md +++ b/furniture-3d-system/docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md @@ -26,8 +26,9 @@ 固定协作分支口径: -- `xuxiake` 默认长期只在 `codex/xuxiake-packaging` 上继续开发 -- 只有临时小任务才从它分出 `codex/xuxiake-packaging-<任务简称>` +- 日常开发默认先同步 `origin/codex/xuxiake-mainline` +- 长期包装分支使用 `codex/<负责人>-packaging` +- 临时小任务使用 `codex/<负责人>-packaging-<任务简称>` - 不要使用 `dev` 这种泛分支名 - 不要直接 push `main` @@ -356,8 +357,8 @@ 请接手当前仓库里的“新平台包装子工作区”任务,不要扩范围。 当前固定工作分支: -- 长期分支:`codex/xuxiake-packaging` -- 临时任务分支:`codex/xuxiake-packaging-<任务简称>` +- 长期分支:`codex/<负责人>-packaging` +- 临时任务分支:`codex/<负责人>-packaging-<任务简称>` - 不要继续使用 `codex/xuxiake-dev` - 不要使用 `feature/packaging` diff --git a/furniture-3d-system/docs/开发指南/组件使用指南.md b/furniture-3d-system/docs/开发指南/组件使用指南.md index 0f4484e7..0351e7f8 100644 --- a/furniture-3d-system/docs/开发指南/组件使用指南.md +++ b/furniture-3d-system/docs/开发指南/组件使用指南.md @@ -56,7 +56,7 @@ const blueColor = appleColorTokens.system.blue.light **最佳实践:** - 优先使用语义化颜色(如 `apple-bg-primary`) -- 深色模式会自动切换颜色 +- 当前产品固定浅色主题,不再提供深浅色切换 - 使用系统颜色保持一致性 ## 基础组件 diff --git a/furniture-3d-system/docs/设计规范/黑曜石工坊统一视觉标准.md b/furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md similarity index 82% rename from furniture-3d-system/docs/设计规范/黑曜石工坊统一视觉标准.md rename to furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md index c5d3e699..a9dd39f5 100644 --- a/furniture-3d-system/docs/设计规范/黑曜石工坊统一视觉标准.md +++ b/furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md @@ -1,16 +1,16 @@ -# Obsidian Atelier 统一视觉标准 +# White Obsidian Atelier(白曜石工坊)统一视觉标准 适用范围:资产中心、辅助装配、草稿组、正式入库、轻仓库等新平台主链页面。 -目标:统一“黑曜石 / Obsidian Atelier”视觉方向,避免把资产中心做成普通 SaaS 卡片列表,也避免把毛玻璃效果滥用成低可读性的装饰。 +目标:统一“白曜石工坊 / White Obsidian Atelier”视觉方向。默认基调是浅色工作台,不再做深浅色主题切换;同时避免把资产中心做成普通 SaaS 卡片列表,也避免把毛玻璃效果滥用成低可读性的装饰。 --- ## 1. 总体视觉方向 -Obsidian Atelier 的关键词是:深色精密、克制通透、资产工作台、可检视的工业元件。 +White Obsidian Atelier 的关键词是:浅色精密、克制通透、资产工作台、可检视的工业元件。 -- `Obsidian(黑曜石)`:整体基调偏深、安静、低干扰,用深灰、炭黑、冷白、金属中性色建立专业感。 +- `White Obsidian(白曜石)`:整体基调偏浅、安静、低干扰,用冷白、浅灰、石墨灰、金属中性色建立专业感。 - `Atelier(工坊)`:页面不是营销展示页,而是给人长期处理资产、装配、审核、入库的工作空间。 - `Unified(统一)`:资产中心、辅助装配、草稿组、正式入库、轻仓库要像同一套产品,不允许每个页面各自发明一套视觉语言。 - `3D Workbench(3D 工作台)`:新平台的第一感知应是“可操作的资产工作台”,而不是“管理后台卡片墙”。 @@ -19,14 +19,15 @@ Obsidian Atelier 的关键词是:深色精密、克制通透、资产工作台 - 信息层级清楚,主操作区域足够安静。 - 强调 3D、材质、规格、状态和审核进度,而不是堆叠大面积营销式卡片。 -- 避免一屏全是同色深灰,也避免大面积高饱和渐变。 +- 避免一屏全是同色白灰,也避免大面积高饱和渐变。 +- 不再提供深浅色主题切换;旧的深色实现只能作为历史代码兼容,不作为新设计依据。 - 装饰服从功能,任何质感效果都必须服务于空间层级、焦点和可读性。 --- ## 2. 毛玻璃使用边界 -毛玻璃是 Obsidian Atelier 中允许且需要的视觉语言,但它只能用于“浮在工作台上、需要轻量透出背景关系”的界面层。 +毛玻璃是 White Obsidian Atelier 中允许且需要的视觉语言,但它只能用于“浮在工作台上、需要轻量透出背景关系”的界面层。 允许使用毛玻璃的区域: @@ -109,12 +110,6 @@ Obsidian Atelier 的关键词是:深色精密、克制通透、资产工作台 className="bg-white/72 backdrop-blur-xl ring-1 ring-black/5 shadow-sm" ``` -深色场景可用: - -```tsx -className="bg-zinc-950/64 backdrop-blur-2xl ring-1 ring-white/10 shadow-sm" -``` - 顶栏可用: ```tsx @@ -133,24 +128,12 @@ className="bg-white/68 backdrop-blur-2xl ring-1 ring-white/40 shadow-sm" className="bg-white border-l border-zinc-200" ``` -深色 Inspector 主面板: - -```tsx -className="bg-zinc-950 border-l border-white/10" -``` - 数据列表主体应使用稳定、可读的实体层: ```tsx className="bg-white divide-y divide-zinc-100" ``` -深色数据列表主体: - -```tsx -className="bg-zinc-950 divide-y divide-white/10" -``` - 使用限制: - 不要把 `backdrop-blur-xl` 或 `backdrop-blur-2xl` 放进大面积主内容面板。 @@ -158,14 +141,17 @@ className="bg-zinc-950 divide-y divide-white/10" - 不要为了质感叠加多层 `shadow-xl`、高饱和渐变或大面积发光。 - 同一屏毛玻璃层级最多保留在顶栏、HUD、浮动工具条和少量状态浮层中。 - 如果文字可读性下降,以可读性为准,立即改回实体背景。 +- 不新增深色入口,不在导航、登录、注册、后台页保留深浅色切换按钮。 --- ## 6. 协作验收口径 -做 Obsidian Atelier 相关页面时,至少按下面几条自查: +做 White Obsidian Atelier 相关页面时,至少按下面几条自查: - 第一屏是否仍是工作台体验,而不是 SaaS 卡片墙。 +- 页面默认是否为浅色工作台,而不是黑底后台。 +- 是否已经移除深浅色主题切换入口。 - 右侧 Inspector 是否清晰、稳定、可长期阅读。 - 毛玻璃是否只出现在顶栏、HUD、浮动工具条和轻量状态浮层。 - 板材、五金、组件、包装是否按元组件表达,而不是普通展示卡片。 diff --git a/furniture-3d-system/docs/设计规范/苹果风格视觉标准.md b/furniture-3d-system/docs/设计规范/苹果风格视觉标准.md index 10693101..23abd222 100644 --- a/furniture-3d-system/docs/设计规范/苹果风格视觉标准.md +++ b/furniture-3d-system/docs/设计规范/苹果风格视觉标准.md @@ -1,7 +1,8 @@ # 苹果风格视觉设计标准 (Apple-Style Visual Design Standards) > **版本**: 2.0 (2026-01-29) -> **状态**: 现行标准 (Single Source of Truth) +> **状态**: 补充参考标准;当前新平台第一视觉标准以 `白曜石工坊统一视觉标准.md` 为准 +> **优先级**: 如本文件与白曜石工坊标准冲突,优先执行白曜石工坊标准 > **目标**: 打造优雅、统一、沉浸式的用户体验,完全去除"模板化"和"AI味",接近苹果官网的视觉水准。 --- @@ -26,7 +27,7 @@ ### 1.1 内容至上 (Content is King) * **原则**:界面不仅是容器,更是内容的舞台。UI 应该"退后",让 3D 模型、产品图片和关键数据"向前"。 * **执行**: - * 背景色主要使用白色或极淡的灰色(Light Mode),深色模式下使用纯黑。 + * 背景色主要使用白色或极淡的灰色;当前产品不再提供深浅色主题切换。 * **拒绝**无意义的装饰性边框、分割线和阴影。只有在区分层级必要时才使用。 * **拒绝**均等分割的网格布局("AI味"典型特征)。使用非对称布局(如:左侧 2/3 展示 3D 模型,右侧 1/3 展示控制面板)。 @@ -149,7 +150,7 @@ ``` src/ ├── lib/ -│ └── design-tokens/ # 核心视觉配置 (Single Source of Truth) +│ └── design-tokens/ # 核心视觉配置 │ ├── typography.ts # 字体定义 │ ├── colors.ts # 颜色定义 │ ├── spacing.ts # 间距定义 diff --git a/furniture-3d-system/docs/运维指南.md b/furniture-3d-system/docs/运维指南.md index 9dc72e1c..bcc75b2a 100644 --- a/furniture-3d-system/docs/运维指南.md +++ b/furniture-3d-system/docs/运维指南.md @@ -150,7 +150,7 @@ MINIO_DATA_DIR=/path/to/minio-data ./scripts/backup-files.sh **当前版本特性:** -- 顶部导航:返回首页、主题切换 +- 顶部导航:返回首页、固定浅色工作台 - 检查项: - 设计令牌系统 - 字体系统 diff --git a/furniture-3d-system/src/app/__tests__/assisted-assembly-page.test.tsx b/furniture-3d-system/src/app/__tests__/assisted-assembly-page.test.tsx index c8dd1f18..ca0c00d6 100644 --- a/furniture-3d-system/src/app/__tests__/assisted-assembly-page.test.tsx +++ b/furniture-3d-system/src/app/__tests__/assisted-assembly-page.test.tsx @@ -5,11 +5,6 @@ import { prisma } from '@/lib/prisma' import { canViewProduct } from '@/lib/permissions' import { buildManualAssemblyWorkspace } from '@/lib/open-product/assembly-workspace' -jest.mock('@/components/ui/ThemeToggle', () => ({ - __esModule: true, - default: () =>
ThemeToggle
, -})) - jest.mock('@/components/ui/UserMenu', () => ({ __esModule: true, default: () =>
UserMenu
, diff --git a/furniture-3d-system/src/app/__tests__/draft-groups-page.test.tsx b/furniture-3d-system/src/app/__tests__/draft-groups-page.test.tsx index f8b0d7aa..359a8a85 100644 --- a/furniture-3d-system/src/app/__tests__/draft-groups-page.test.tsx +++ b/furniture-3d-system/src/app/__tests__/draft-groups-page.test.tsx @@ -18,9 +18,11 @@ jest.mock('next/link', () => ({ ), })) -jest.mock('@/components/ui/ThemeToggle', () => ({ - __esModule: true, - default: () =>
ThemeToggle
, +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + back: jest.fn(), + push: jest.fn(), + }), })) jest.mock('@/components/ui/UserMenu', () => ({ diff --git a/furniture-3d-system/src/app/__tests__/home-client-delete.test.tsx b/furniture-3d-system/src/app/__tests__/home-client-delete.test.tsx index 0819d0f4..a479225c 100644 --- a/furniture-3d-system/src/app/__tests__/home-client-delete.test.tsx +++ b/furniture-3d-system/src/app/__tests__/home-client-delete.test.tsx @@ -24,10 +24,6 @@ jest.mock('next/link', () => ({ jest.mock('@/components/products/UploadProductModal', () => () => null) jest.mock('@/components/products/AddSKUModal', () => () => null) -jest.mock('@/components/ui/ThemeToggle', () => ({ - __esModule: true, - default: () =>
ThemeToggle
, -})) jest.mock('@/components/ui/UserMenu', () => ({ __esModule: true, default: () =>
UserMenu
, diff --git a/furniture-3d-system/src/app/__tests__/intake-page.test.tsx b/furniture-3d-system/src/app/__tests__/intake-page.test.tsx index 279aca85..9ae74b8d 100644 --- a/furniture-3d-system/src/app/__tests__/intake-page.test.tsx +++ b/furniture-3d-system/src/app/__tests__/intake-page.test.tsx @@ -18,11 +18,6 @@ jest.mock('next/link', () => ({ ), })) -jest.mock('@/components/ui/ThemeToggle', () => ({ - __esModule: true, - default: () =>
ThemeToggle
, -})) - jest.mock('@/components/ui/UserMenu', () => ({ __esModule: true, default: () =>
UserMenu
, @@ -80,7 +75,16 @@ jest.mock('@/server/services', () => ({ seriesTemplateName: '单板桌', }, ]), - getById: jest.fn(), + getById: jest.fn().mockResolvedValue({ + draftGroup: { + id: 'draft-1', + code: 'DG-001', + name: '包装中草稿', + status: 'PACKAGING', + seriesTemplateName: '单板桌', + }, + preSkus: [], + }), }, intakeService: { recommendExistingProducts: jest.fn().mockResolvedValue([]), @@ -88,6 +92,24 @@ jest.mock('@/server/services', () => ({ warehouseService: { listLocations: jest.fn().mockResolvedValue([]), }, + assetCenterService: { + listSubstrates: jest.fn().mockResolvedValue([]), + getSubstrateDictionaryBundle: jest.fn().mockResolvedValue({ + categories: [], + substrateTypes: [], + processTypes: [], + }), + }, + materialService: { + findAll: jest.fn().mockResolvedValue({ items: [] }), + findAllCategories: jest.fn().mockResolvedValue([]), + }, + categoryService: { + listActive: jest.fn().mockResolvedValue([]), + }, + intakeNonIntegratedProductService: { + list: jest.fn().mockResolvedValue([]), + }, })) jest.mock('../intake/IntakeClient', () => ({ diff --git a/furniture-3d-system/src/app/__tests__/page-nav.test.tsx b/furniture-3d-system/src/app/__tests__/page-nav.test.tsx index f4098358..0dd055c4 100644 --- a/furniture-3d-system/src/app/__tests__/page-nav.test.tsx +++ b/furniture-3d-system/src/app/__tests__/page-nav.test.tsx @@ -26,11 +26,6 @@ jest.mock('../HomeClient', () => ({ default: (props: unknown) => mockedHomeClient(props), })) -jest.mock('@/components/ui/ThemeToggle', () => ({ - __esModule: true, - default: () =>
ThemeToggle
, -})) - jest.mock('@/components/ui/UserMenu', () => ({ __esModule: true, default: () =>
UserMenu
, diff --git a/furniture-3d-system/src/app/admin/AdminPortalClient.tsx b/furniture-3d-system/src/app/admin/AdminPortalClient.tsx new file mode 100644 index 00000000..6451d74d --- /dev/null +++ b/furniture-3d-system/src/app/admin/AdminPortalClient.tsx @@ -0,0 +1,119 @@ +'use client' + +import Link from 'next/link' +import { + Grid, + Palette, + ShieldCheck, + SwatchBook, + Users, +} from 'lucide-react' +import { + ObsidianControlShell, + ObsidianDataRow, + ObsidianInspectorSection, + ObsidianRailButton, + ObsidianRailGroup, +} from '@/components/platform/ObsidianControlShell' + +interface AdminPortalClientProps { + role: string +} + +const roleLabels: Record = { + SUPER_ADMIN: '超级管理员', + ADMIN: '管理员', +} + +const adminModules = [ + { + title: '用户权限中心', + subtitle: '权限', + href: '/users', + icon: Users, + metric: '组织 / 角色', + }, + { + title: '主题视觉管理', + subtitle: '主题', + href: '/admin/themes', + icon: Palette, + metric: '配置', + }, + { + title: '材质物理库', + subtitle: '材质', + href: '/asset-center/material-library?from=/admin&fromLabel=后台设置', + icon: SwatchBook, + metric: 'PBR', + }, + { + title: '分类体系定义', + subtitle: '分类', + href: '/admin/categories', + icon: Grid, + metric: '排序', + }, +] + +export function AdminPortalClient({ role }: AdminPortalClientProps) { + const roleLabel = roleLabels[role] ?? role + + return ( + + + {adminModules.map((module) => ( + + ))} + +
+

操作员

+

{roleLabel}

+
+ + } + inspector={ +
+ + + + + + + + +
+ } + > +
+
+ {adminModules.map((module) => ( + +
+ +
+
+

{module.title}

+

{module.subtitle}

+
+ + {module.metric} + + + ))} +
+
+
+ ) +} diff --git a/furniture-3d-system/src/app/admin/categories/page.tsx b/furniture-3d-system/src/app/admin/categories/page.tsx index f18116fc..f714191b 100644 --- a/furniture-3d-system/src/app/admin/categories/page.tsx +++ b/furniture-3d-system/src/app/admin/categories/page.tsx @@ -7,14 +7,20 @@ 'use client' import { useState, useEffect, useCallback, useRef } from 'react' -import { useRouter } from 'next/navigation' -import { Plus, Pencil, Trash2 } from 'lucide-react' +import { Grid, Layers, Plus, Pencil, Trash2 } from 'lucide-react' import Button from '@/components/ui/Button' import Input from '@/components/ui/Input' import Modal from '@/components/ui/Modal' -import Card from '@/components/ui/Card' -import { Title2, Body, Caption1 } from '@/components/ui/Typography' +import { Body, Caption1 } from '@/components/ui/Typography' import { useToast, ToastContainer } from '@/components/ui/Toast' +import { + ObsidianControlShell, + ObsidianDataRow, + ObsidianInspectorSection, + ObsidianPanel, + ObsidianRailButton, + ObsidianRailGroup, +} from '@/components/platform/ObsidianControlShell' import { getApiErrorMessage } from '@/lib/utils' interface Category { @@ -27,7 +33,6 @@ interface Category { } export default function CategoriesPage() { - const router = useRouter() const toast = useToast() const toastError = toast.error @@ -192,99 +197,90 @@ export default function CategoriesPage() { } return ( -
- {/* 顶部导航 */} -
- -
- - {/* 主内容 */} -
- -
- - 产品分类列表 - -
- - {loading ? ( -
- 加载中... -
- ) : categories.length === 0 ? ( -
- 暂无分类,点击右上角添加 -
- ) : ( -
- {categories.map((category) => ( -
-
-
+ + + 添加分类 + + } + rail={ +
+ + + category.productCount > 0).length}`} /> + +
+ } + inspector={ +
+ + + sum + category.productCount, 0)}`} /> + category.productCount === 0).length}`} /> + + + + + +
+ } + > +
+
+

产品分类

+
+ +
+ + {loading ? ( +
加载中...
+ ) : categories.length === 0 ? ( +
暂无分类,点击右上角添加
+ ) : ( +
+ {categories.map((category) => ( +
+
{category.icon || category.name.charAt(0)}
-
- - {category.name} - - {category.description && ( - - {category.description} - - )} +
+

{category.name}

+ {category.description ?

{category.description}

: null} +
+
+ + {category.productCount} 个产品 + +
+
+ +
- - {category.productCount} 个产品 -
- -
- - -
-
- ))} -
- )} - -
+ ))} +
+ )} + + + {/* 添加/编辑弹窗 */}
- + 确定要删除分类 "{deletingCategory?.name}" 吗? {deletingCategory && deletingCategory.productCount > 0 && ( @@ -381,6 +377,6 @@ export default function CategoriesPage() { -
+ ) } diff --git a/furniture-3d-system/src/app/admin/page.tsx b/furniture-3d-system/src/app/admin/page.tsx index 1e7a1eba..146bd3df 100644 --- a/furniture-3d-system/src/app/admin/page.tsx +++ b/furniture-3d-system/src/app/admin/page.tsx @@ -1,60 +1,10 @@ -import Link from 'next/link' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' -import { - Users, Palette, SwatchBook, Grid, ArrowLeft, ArrowRight, - ShieldCheck, Activity, Database, Zap, Clock, - Cpu, Terminal, ShieldAlert, BarChart3 -} from 'lucide-react' -import { cn } from '@/lib/utils' import { verifyToken } from '@/lib/jwt' -import Button from '@/components/ui/Button' - -const adminModules = [ - { - title: '用户权限中心', - subtitle: 'IDENTITY & ACCESS', - description: '管理系统访问权限、组织架构及账号生命周期,确保数据访问的安全合规。', - href: '/users', - icon: Users, - color: 'text-blue-600', - bgColor: 'bg-blue-50', - stats: '1,248 Users Active', - }, - { - title: '主题视觉管理', - subtitle: 'THEME ENGINE', - description: '配置系统全局视觉变量、品牌色、间距及 UI 调性,实现动态化品牌表达。', - href: '/admin/themes', - icon: Palette, - color: 'text-indigo-600', - bgColor: 'bg-indigo-50', - stats: '12 Active Themes', - }, - { - title: '材质物理库', - subtitle: 'PHYSICAL MATERIALS', - description: '维护 3D 渲染核心材质球、PBR 物理参数及纹理资产,打造极致视觉还原。', - href: '/admin/materials', - icon: SwatchBook, - color: 'text-emerald-600', - bgColor: 'bg-emerald-50', - stats: '450+ Materials', - }, - { - title: '分类体系定义', - subtitle: 'TAXONOMY SYSTEM', - description: '定义产品资产的标准化层级、标签规则与归类逻辑,支撑多维检索需求。', - href: '/admin/categories', - icon: Grid, - color: 'text-amber-600', - bgColor: 'bg-amber-50', - stats: '32 Categories', - }, -] +import { AdminPortalClient } from './AdminPortalClient' export const metadata = { - title: '系统总控制台 | Precision Atelier', + title: '系统总控制台 | White Obsidian Atelier', } export default async function AdminPortalPage() { @@ -68,192 +18,5 @@ export default async function AdminPortalPage() { const allowedRoles = ['SUPER_ADMIN', 'ADMIN'] if (!allowedRoles.includes(payload.role)) redirect('/?error=unauthorized') - return ( -
- {/* 沉浸式背景背景 */} -
-
-
-
-
- -
- {/* Header Section */} -
-
-
- - - -
- -
- -
-
-
-
-
- - Precision Master Console - -
-
-

- 系统总控制台 -

-

- 集成式基础设施管理中心 · 实时监控系统运行状态与核心资产分发 -

-
-
- - -
- - - -
-
- -
- {/* Left: Core Modules (8 Columns) */} -
-
- {adminModules.map((module, idx) => ( - -
-
- -
-
- -
-
- -
- - {module.subtitle} - -

- {module.title} -

-

- {module.description} -

-
- - {/* Footer Stats inside card */} -
- {module.stats} - ENTER CENTER -
- - - {module.href.split('/').pop()} - - - ))} -
-
- - {/* Right: System Health & Logs (4 Columns) */} -
-
-
-

System Health

- -
- -
- - - - -
- -
-
- -
- 安全提示 - 发现 2 个未挂载的材质分区 -
-
-
-
- -
-
-

Live Logs

- -
-
- - - - -
-
-
-
- -
-

- Precision Atelier · Built for Next Generation Design -

-
-
-
- ) -} - -function QuickMetric({ icon: Icon, label, value, color }: { icon: any, label: string, value: string, color?: string }) { - return ( -
-
- -
-
- {label} - {value} -
-
- ) -} - -function HealthBar({ label, progress, color = "bg-emerald-500" }: { label: string, progress: number, color?: string }) { - return ( -
-
- {label} - {progress}% -
-
-
-
-
- ) -} - -function LogItem({ time, user, action }: { time: string, user: string, action: string }) { - return ( -
- {time} -
- {user} - {action} -
-
- ) + return } diff --git a/furniture-3d-system/src/app/admin/themes/ThemeManagementClient.tsx b/furniture-3d-system/src/app/admin/themes/ThemeManagementClient.tsx index f1f18e84..0e62daa0 100644 --- a/furniture-3d-system/src/app/admin/themes/ThemeManagementClient.tsx +++ b/furniture-3d-system/src/app/admin/themes/ThemeManagementClient.tsx @@ -9,14 +9,20 @@ */ import { useState, useEffect, useCallback } from 'react' -import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/Button' -import { Card } from '@/components/ui/Card' import { Input } from '@/components/ui/Input' import Modal from '@/components/ui/Modal' import { Badge } from '@/components/ui/Badge' import { useToast } from '@/components/ui/Toast' -import { useAuthStore } from '@/stores/auth.store' +import { useAuth } from '@/hooks/useAuth' +import { + ObsidianControlShell, + ObsidianDataRow, + ObsidianInspectorSection, + ObsidianPanel, + ObsidianRailButton, + ObsidianRailGroup, +} from '@/components/platform/ObsidianControlShell' import { Plus, Edit2, @@ -51,29 +57,26 @@ const initialFormData: ThemeFormData = { loginPanel: null, } +const roleLabels: Record = { + SUPER_ADMIN: '超级管理员', + ADMIN: '管理员', +} + // ============ 主组件 ============ export default function ThemeManagementClient() { - const router = useRouter() const toast = useToast() - const { user } = useAuthStore() + const { user } = useAuth() + const canManageThemes = user?.role === 'SUPER_ADMIN' // 状态 const [themes, setThemes] = useState([]) - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) const [editingTheme, setEditingTheme] = useState(null) const [formData, setFormData] = useState(initialFormData) const [submitting, setSubmitting] = useState(false) - // 权限检查 - useEffect(() => { - if (user && user.role !== 'SUPER_ADMIN') { - toast.error('只有超级管理员可以访问此页面') - router.push('/') - } - }, [user, router, toast]) - const fetchThemes = useCallback(async () => { try { setLoading(true) @@ -94,8 +97,10 @@ export default function ThemeManagementClient() { // 加载主题列表 useEffect(() => { - fetchThemes() - }, [fetchThemes]) + if (canManageThemes) { + fetchThemes() + } + }, [canManageThemes, fetchThemes]) // 打开创建/编辑模态框 const openModal = async (themeId?: string) => { @@ -246,133 +251,155 @@ export default function ThemeManagementClient() { // ============ 渲染 ============ - if (user?.role !== 'SUPER_ADMIN') { - return null + if (!canManageThemes) { + return ( + + + + +
+ } + inspector={ +
+ + + + + +
+ } + > +
+ +
+

当前账号不能管理主题

+

请使用超级管理员账号操作主题。

+
+
+
+ + ) } return ( -
- {/* 顶部导航 */} -
- -
+ openModal()} className="obsidian-control-action rounded-md border"> + + 新建主题 + + } + rail={ +
+ + + theme.isActive).length}`} /> + theme.isDefault).length}`} /> + +
+ } + inspector={ +
+ + + theme.isActive).length}`} tone="success" /> + theme.isDefault).length}`} /> + + + + + + +
+ } + > +
+
+

主题配置

+
- {/* 主内容 */} -
- {loading ? ( -
-
加载中...
-
- ) : themes.length === 0 ? ( - - -

暂无主题配置

- -
- ) : ( -
- {themes.map((theme) => ( - -
-
-
-

- {theme.name} -

- {theme.isDefault && ( - - - 默认 - - )} - {theme.isActive && ( - - - 激活 - - )} - {theme.startDate && theme.endDate && ( - - - 定时 - +
+ + {loading ? ( +
加载中...
+ ) : themes.length === 0 ? ( +
+ +

暂无主题配置

+ +
+ ) : ( +
+ {themes.map((theme) => ( +
+
+
+

{theme.name}

+ {theme.isDefault && ( + + + 默认 + + )} + {theme.isActive && ( + + + 激活 + + )} + {theme.startDate && theme.endDate && ( + + + 定时 + + )} +
+ {theme.description ?

{theme.description}

: null} + {(theme.startDate || theme.endDate) && ( +

+ {theme.startDate && `开始: ${new Date(theme.startDate).toLocaleString('zh-CN')}`} + {theme.startDate && theme.endDate && ' | '} + {theme.endDate && `结束: ${new Date(theme.endDate).toLocaleString('zh-CN')}`} +

)}
- {theme.description && ( -

- {theme.description} -

- )} - {(theme.startDate || theme.endDate) && ( -

- {theme.startDate && `开始: ${new Date(theme.startDate).toLocaleString('zh-CN')}`} - {theme.startDate && theme.endDate && ' | '} - {theme.endDate && `结束: ${new Date(theme.endDate).toLocaleString('zh-CN')}`} -

- )} -
-
- - - {!theme.isDefault && ( - - )} + + {!theme.isDefault && ( + + )} +
-
- - ))} -
- )} -
+ ))} +
+ )} + +
+
{/* 编辑模态框 */} -

+

定时发布(可选)

@@ -421,7 +448,7 @@ export default function ThemeManagementClient() { {/* 首页背景 */}
-

+

首页背景

@@ -457,7 +484,7 @@ export default function ThemeManagementClient() { {/* 登录页面板 */}
-

+

@@ -488,7 +515,7 @@ export default function ThemeManagementClient() {
{/* 操作按钮 */} -
+
@@ -498,6 +525,6 @@ export default function ThemeManagementClient() {
-
+ ) } diff --git a/furniture-3d-system/src/app/admin/themes/page.tsx b/furniture-3d-system/src/app/admin/themes/page.tsx index aee1d181..758734aa 100644 --- a/furniture-3d-system/src/app/admin/themes/page.tsx +++ b/furniture-3d-system/src/app/admin/themes/page.tsx @@ -7,13 +7,21 @@ */ import { Metadata } from 'next' +import { AuthProvider } from '@/hooks/useAuth' +import { RequireAuth } from '@/components/auth/RequireAuth' import ThemeManagementClient from './ThemeManagementClient' export const metadata: Metadata = { - title: '主题管理 - 3D 家具系统', + title: '主题视觉管理 | White Obsidian Atelier', description: '管理系统主题和背景配置', } export default function ThemeManagementPage() { - return + return ( + + + + + + ) } diff --git a/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx b/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx index 74a02444..b4027a19 100644 --- a/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx +++ b/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx @@ -2,7 +2,7 @@ /** * 基础资料库 (Material Library) - * Obsidian Atelier - 元组件资料工作台 + * White Obsidian Atelier - 元组件资料工作台 * 布局标准:资料索引 - 材质/基材工作台 - 实体 Inspector */ diff --git a/furniture-3d-system/src/app/debug-theme/page.tsx b/furniture-3d-system/src/app/debug-theme/page.tsx index eeff1038..df7fdcc7 100644 --- a/furniture-3d-system/src/app/debug-theme/page.tsx +++ b/furniture-3d-system/src/app/debug-theme/page.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react' import Button from '@/components/ui/Button' -import ThemeToggle from '@/components/ui/ThemeToggle' export default function DebugThemePage() { const [themeInfo, setThemeInfo] = useState({ @@ -45,12 +44,8 @@ export default function DebugThemePage() { attributeFilter: ['data-theme', 'class'], }) - // 监听自定义事件 - window.addEventListener('theme-change', updateInfo) - return () => { observer.disconnect() - window.removeEventListener('theme-change', updateInfo) } }, []) @@ -59,9 +54,8 @@ export default function DebugThemePage() {

- 主题系统调试页面 + 浅色主题调试页面

-
{/* 调试信息卡片 */} @@ -174,8 +168,8 @@ export default function DebugThemePage() {

1. 打开浏览器开发者工具(F12)

2. 查看 Console 标签页,应该能看到详细的主题信息

-

3. 切换主题后,信息会自动更新

-

4. 检查 Elements 标签页,查看 <html> 标签的 data-theme 属性

+

3. 当前系统固定为浅色主题,不再提供深浅色切换

+

4. 检查 Elements 标签页,查看 <html> 标签的 data-theme 属性应为 light

diff --git a/furniture-3d-system/src/app/globals.css b/furniture-3d-system/src/app/globals.css index 9834931c..a25222e7 100644 --- a/furniture-3d-system/src/app/globals.css +++ b/furniture-3d-system/src/app/globals.css @@ -514,8 +514,8 @@ } } -/* ===== Obsidian Material Library Theme Bridge ===== - 基础资料库使用黑曜石工作台结构,但必须跟随全局 light/dark 主题。 */ +/* ===== White Obsidian Material Library Theme Bridge ===== + 基础资料库使用白曜石工作台结构;全局产品主题固定浅色。 */ @layer components { :root, [data-theme='light'] { @@ -630,6 +630,288 @@ border-color: var(--ml-border) !important; } + :root, + [data-theme='light'] { + --obs-shell-bg: #eef2f3; + --obs-shell-text: #263033; + --obs-shell-panel-bg: #ffffff; + --obs-shell-content-bg: #f4f7f7; + --obs-shell-header-bg: #f8faf9; + --obs-shell-surface-bg: #ffffff; + --obs-shell-muted-bg: #edf2f2; + --obs-shell-rail-bg: #e5ebeb; + --obs-shell-border: rgba(17, 23, 25, 0.12); + --obs-shell-title: #101719; + --obs-shell-muted: #5f6d70; + --obs-shell-subtle: #7a878a; + --obs-shell-hover-bg: rgba(17, 23, 25, 0.045); + --obs-shell-control-bg: rgba(17, 23, 25, 0.045); + --obs-shell-control-hover-bg: rgba(17, 23, 25, 0.08); + --obs-shell-control-text: #4f5c5f; + --obs-shell-control-hover-text: #101719; + --obs-shell-icon-bg: rgba(17, 23, 25, 0.06); + --obs-shell-icon-text: #2d393c; + --obs-shell-rail-title: #687477; + --obs-shell-rail-text: #3f4a4d; + --obs-shell-rail-hover-bg: rgba(17, 23, 25, 0.06); + --obs-shell-rail-hover-text: #101719; + --obs-shell-rail-active-bg: #101719; + --obs-shell-rail-active-text: #f7f9f9; + --obs-shell-status-text: #566265; + --obs-shell-status-muted: #6d787b; + --obs-shell-status-strong: #101719; + --obs-shell-status-pill-bg: rgba(17, 23, 25, 0.055); + --obs-shell-action-bg: #101719; + --obs-shell-action-text: #f7f9f9; + --obs-shell-action-border: rgba(17, 23, 25, 0.16); + --obs-shell-action-hover-bg: #263033; + --obs-shell-rail-card-bg: rgba(17, 23, 25, 0.045); + --obs-shell-rail-card-text: #101719; + --obs-shell-rail-card-muted: #687477; + --obs-shell-success: #1f7a56; + --obs-shell-warning: #9a6a16; + --obs-shell-danger: #b3453d; + } + + [data-theme='dark'] { + --obs-shell-bg: #111719; + --obs-shell-text: #e8edef; + --obs-shell-panel-bg: #111719; + --obs-shell-content-bg: #0f1416; + --obs-shell-header-bg: #101719; + --obs-shell-surface-bg: #151d1f; + --obs-shell-muted-bg: #202a2d; + --obs-shell-rail-bg: #111719; + --obs-shell-border: rgba(255, 255, 255, 0.10); + --obs-shell-title: #f4f7f6; + --obs-shell-muted: #aeb8bb; + --obs-shell-subtle: #8f9a9d; + --obs-shell-hover-bg: rgba(255, 255, 255, 0.055); + --obs-shell-control-bg: rgba(255, 255, 255, 0.04); + --obs-shell-control-hover-bg: rgba(255, 255, 255, 0.08); + --obs-shell-control-text: #aeb8bb; + --obs-shell-control-hover-text: #ffffff; + --obs-shell-icon-bg: rgba(255, 255, 255, 0.06); + --obs-shell-icon-text: #dfe6e8; + --obs-shell-rail-bg: #171d1f; + --obs-shell-rail-title: #748083; + --obs-shell-rail-text: #c4cccf; + --obs-shell-rail-hover-bg: rgba(255, 255, 255, 0.06); + --obs-shell-rail-hover-text: #ffffff; + --obs-shell-rail-active-bg: #ffffff; + --obs-shell-rail-active-text: #111719; + --obs-shell-status-text: #aeb8bb; + --obs-shell-status-muted: #748083; + --obs-shell-status-strong: #ffffff; + --obs-shell-status-pill-bg: rgba(255, 255, 255, 0.04); + --obs-shell-action-bg: #ffffff; + --obs-shell-action-text: #111719; + --obs-shell-action-border: rgba(255, 255, 255, 0.10); + --obs-shell-action-hover-bg: #e8edef; + --obs-shell-rail-card-bg: rgba(255, 255, 255, 0.04); + --obs-shell-rail-card-text: #ffffff; + --obs-shell-rail-card-muted: #748083; + --obs-shell-success: #9bdcc5; + --obs-shell-warning: #e5bd72; + --obs-shell-danger: #f3a29c; + } + + .obsidian-control-grid { + background-image: var(--obs-shell-grid); + background-size: 28px 28px; + opacity: var(--obs-shell-grid-opacity); + } + + .obsidian-control-vignette { + background: var(--obs-shell-vignette); + } + + .obsidian-control-action { + border-color: var(--obs-shell-action-border) !important; + background: var(--obs-shell-action-bg) !important; + color: var(--obs-shell-action-text) !important; + } + + .obsidian-control-action:hover { + background: var(--obs-shell-action-hover-bg) !important; + } + + .obsidian-control-rail-card { + border-color: var(--obs-shell-border); + background: var(--obs-shell-rail-card-bg); + color: var(--obs-shell-rail-card-text); + } + + .obsidian-control-rail-card-muted { + color: var(--obs-shell-rail-card-muted); + } + + .obsidian-control-shell { + background: var(--obs-shell-bg) !important; + color: var(--obs-shell-text) !important; + } + + .obsidian-control-shell > div > aside:first-of-type { + background: var(--obs-shell-rail-bg) !important; + border-color: var(--obs-shell-border) !important; + } + + .obsidian-control-shell > div > section { + background: var(--obs-shell-panel-bg) !important; + border-color: var(--obs-shell-border) !important; + } + + .obsidian-control-shell > div > aside:last-child { + background: var(--obs-shell-panel-bg) !important; + border-color: var(--obs-shell-border) !important; + } + + .obsidian-control-shell > div > section > header, + .obsidian-control-shell > div > aside > div:first-child, + .obsidian-control-shell [class*="bg-[#101719]"] { + background: var(--obs-shell-header-bg) !important; + border-color: var(--obs-shell-border) !important; + } + + .obsidian-control-shell [class*="bg-[#0d1113]"], + .obsidian-control-shell [class*="bg-[#0f1416]"], + .obsidian-control-shell [class*="bg-[#101517]"] { + background-color: var(--obs-shell-content-bg) !important; + } + + .obsidian-control-shell [class*="bg-[#111719]"] { + background-color: var(--obs-shell-panel-bg) !important; + } + + .obsidian-control-shell [class*="bg-[#151d1f]"] { + background-color: var(--obs-shell-surface-bg) !important; + } + + .obsidian-control-shell [class*="bg-[#202a2d]"], + .obsidian-control-shell [class*="bg-white/[0.03]"], + .obsidian-control-shell [class*="bg-white/[0.04]"], + .obsidian-control-shell [class*="bg-white/[0.06]"] { + background-color: var(--obs-shell-muted-bg) !important; + } + + .obsidian-control-shell [class*="bg-[#e7eeec]"] { + background-color: var(--obs-shell-rail-active-bg) !important; + } + + .obsidian-control-shell [class*="border-white/10"], + .obsidian-control-shell [class*="border-red-400/25"], + .obsidian-control-shell [class*="border-amber-400/30"] { + border-color: var(--obs-shell-border) !important; + } + + .obsidian-control-shell [class*="divide-white/10"] > :not([hidden]) ~ :not([hidden]) { + border-color: var(--obs-shell-border) !important; + } + + .obsidian-control-shell [class*="text-[#f4f7f6]"], + .obsidian-control-shell [class*="text-[#edf4f2]"], + .obsidian-control-shell [class*="text-[#e7eeec]"], + .obsidian-control-shell [class*="text-[#dce7e4]"], + .obsidian-control-shell [class*="text-[#cbd8d5]"] { + color: var(--obs-shell-title) !important; + } + + .obsidian-control-shell [class*="text-[#aeb8bb]"], + .obsidian-control-shell [class*="text-[#c0ccca]"], + .obsidian-control-shell [class*="text-[#9ba8a8]"], + .obsidian-control-shell [class*="text-[#8f9c9d]"], + .obsidian-control-shell [class*="text-[#7f8d8e]"], + .obsidian-control-shell [class*="text-[#788586]"], + .obsidian-control-shell [class*="text-[#748083]"] { + color: var(--obs-shell-muted) !important; + } + + .obsidian-control-shell [class*="text-[#101517]"], + .obsidian-control-shell [class*="text-[#101719]"] { + color: var(--obs-shell-rail-active-text) !important; + } + + .obsidian-control-shell [class*="text-white"] { + color: var(--obs-shell-title) !important; + } + + .obsidian-control-shell [class*="hover:text-white"]:hover { + color: var(--obs-shell-rail-hover-text) !important; + } + + .obsidian-control-shell [class*="text-emerald-200"], + .obsidian-control-shell [class*="text-[#9bdcc5]"] { + color: var(--obs-shell-success) !important; + } + + .obsidian-control-shell [class*="text-amber-200"], + .obsidian-control-shell [class*="text-[#e5bd72]"] { + color: var(--obs-shell-warning) !important; + } + + .obsidian-control-shell [class*="text-red-200"] { + color: var(--obs-shell-danger) !important; + } + + .obsidian-control-shell [class*="hover:bg-white/[0.04]"]:hover, + .obsidian-control-shell [class*="hover:bg-white/[0.06]"]:hover, + .obsidian-control-shell [class*="hover:bg-red-400/10"]:hover, + .obsidian-control-shell [class*="hover:bg-amber-400/10"]:hover { + background-color: var(--obs-shell-hover-bg) !important; + } + + .obsidian-modal-panel label, + .obsidian-modal-panel .text-apple-label-primary, + .obsidian-modal-panel .text-apple-label-secondary, + .obsidian-modal-panel .text-apple-label-tertiary { + color: var(--obs-shell-muted) !important; + } + + .obsidian-modal-panel input, + .obsidian-modal-panel textarea, + .obsidian-modal-panel select { + border-color: var(--obs-shell-border) !important; + background: var(--obs-shell-surface-bg) !important; + color: var(--obs-shell-title) !important; + } + + .obsidian-modal-panel input:focus, + .obsidian-modal-panel textarea:focus, + .obsidian-modal-panel select:focus { + border-color: var(--obs-shell-title) !important; + background: var(--obs-shell-surface-bg) !important; + } + + .obsidian-modal-panel input::placeholder, + .obsidian-modal-panel textarea::placeholder { + color: var(--obs-shell-subtle) !important; + } + + .obsidian-modal-panel [class*="bg-white"], + .obsidian-modal-panel [class*="bg-[#fbfcfc]"], + .obsidian-modal-panel [class*="bg-[#f7f8f8]"], + .obsidian-modal-panel [class*="bg-apple-bg-secondary"], + .obsidian-modal-panel [class*="bg-apple-bg-tertiary"] { + background-color: var(--obs-shell-surface-bg) !important; + } + + .obsidian-modal-panel [class*="border-[#dde3e5]"], + .obsidian-modal-panel [class*="border-apple-separator"] { + border-color: var(--obs-shell-border) !important; + } + + .obsidian-modal-panel [class*="text-[#2d3335]"], + .obsidian-modal-panel [class*="text-apple-label-primary"] { + color: var(--obs-shell-title) !important; + } + + .obsidian-modal-panel [class*="text-[#6b7679]"], + .obsidian-modal-panel [class*="text-[#7a8588]"], + .obsidian-modal-panel [class*="text-apple-label-secondary"], + .obsidian-modal-panel [class*="text-apple-label-tertiary"] { + color: var(--obs-shell-muted) !important; + } + .material-library-obsidian [class*="ring-white/10"], .material-library-obsidian [class*="ring-[#8fb6b0]"], .material-library-obsidian [class*="ring-[#48686b]"] { diff --git a/furniture-3d-system/src/app/layout.tsx b/furniture-3d-system/src/app/layout.tsx index b898b54d..7f8fc0d9 100644 --- a/furniture-3d-system/src/app/layout.tsx +++ b/furniture-3d-system/src/app/layout.tsx @@ -46,16 +46,10 @@ export default function RootLayout({ return ( - {/* 防止主题闪烁 - 必须在页面渲染前执行 */} + {/* 固定浅色主题,避免旧本地偏好把工作台切回深色 */} @@ -64,7 +58,7 @@ export default function RootLayout({ {/* 移动端优化 */} - + diff --git a/furniture-3d-system/src/app/login/page.tsx b/furniture-3d-system/src/app/login/page.tsx index 58fb06ff..a341a7ae 100644 --- a/furniture-3d-system/src/app/login/page.tsx +++ b/furniture-3d-system/src/app/login/page.tsx @@ -17,7 +17,6 @@ import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { LargeTitle, Body, Footnote, Caption2 } from '@/components/ui/Typography'; -import ThemeToggle from '@/components/ui/ThemeToggle'; import { useToast, ToastContainer } from '@/components/ui/Toast'; import { getApiErrorMessage } from '@/lib/utils'; import type { ApiResponse } from '@/lib/api'; @@ -177,11 +176,6 @@ export default function LoginPage() { {/* Toast 容器 - 居中显示,类似系统通知 */} - {/* 主题切换按钮 - 移到右侧面板内或悬浮在顶层 */} -
- -
- {/* 左侧:极简居中品牌卡片 */}
{/* 居中容器 - 响应式宽度 */} diff --git a/furniture-3d-system/src/app/profile/page.tsx b/furniture-3d-system/src/app/profile/page.tsx new file mode 100644 index 00000000..bfa007de --- /dev/null +++ b/furniture-3d-system/src/app/profile/page.tsx @@ -0,0 +1,178 @@ +'use client' + +import Link from 'next/link' +import { Activity, Building2, CalendarClock, Fingerprint, KeyRound, LogOut, ShieldCheck, UserRound, Users } from 'lucide-react' +import { AuthProvider, useAuth } from '@/hooks/useAuth' +import { RequireAuth } from '@/components/auth/RequireAuth' +import Button from '@/components/ui/Button' +import { + ObsidianControlShell, + ObsidianDataRow, + ObsidianInspectorSection, + ObsidianPanel, + ObsidianRailButton, + ObsidianRailGroup, +} from '@/components/platform/ObsidianControlShell' + +const roleLabels: Record = { + SUPER_ADMIN: '超级管理员', + ADMIN: '管理员', + SUPERVISOR: '主管', + SUPERVISOR_DEPARTMENT: '部门主管', + SUPERVISOR_TEAM: '小组主管', + DESIGNER: '设计师', + USER: '普通用户', +} + +const statusLabels: Record = { + ACTIVE: '运行中', + PENDING: '待激活', + INACTIVE: '已离职', + SUSPENDED: '已锁定', +} + +export default function ProfilePage() { + return ( + + + + + + ) +} + +function ProfileContent() { + const { user, logout } = useAuth() + const canManageUsers = Boolean(user && ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM'].includes(user.role)) + const canAccessAdmin = Boolean(user && ['SUPER_ADMIN', 'ADMIN'].includes(user.role)) + const roleLabel = user ? roleLabels[user.role] ?? user.role : '-' + const statusLabel = user ? statusLabels[user.status] ?? user.status : '-' + const lastLoginText = user?.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString('zh-CN') : '暂无记录' + + return ( + void logout()} + className="obsidian-control-action rounded-md border px-4" + > + + 退出登录 + + } + rail={ +
+ + + {canManageUsers ? : null} + {canAccessAdmin ? : null} + + +
+

会话

+

{statusLabel}

+
+
+ } + inspector={ +
+ + + + + + + + + + + + + +
+ } + > +
+
+
+
+

{user?.name || '当前用户'}

+

{user?.email}

+
+
+

角色

+

{roleLabel}

+
+
+
+ +
+
+ +
+ + + +
+
+ + +
+ + + +
+
+
+ +
+ {canManageUsers ? ( + + ) : null} + {canAccessAdmin ? ( + + ) : null} + +
+
+
+
+ ) +} + +function ProfileMetric({ icon: Icon, label, value }: { icon: typeof Fingerprint; label: string; value: string }) { + return ( +
+ +

{label}

+

{value}

+
+ ) +} + +function ProfileLine({ icon: Icon, label, value }: { icon: typeof Building2; label: string; value: string }) { + return ( +
+ +
+

{label}

+

{value}

+
+
+ ) +} + +function ProfileAction({ href, title }: { href: string; title: string }) { + return ( + +

{title}

+ + ) +} diff --git a/furniture-3d-system/src/app/register/page.tsx b/furniture-3d-system/src/app/register/page.tsx index ded023fd..ad1b2c37 100644 --- a/furniture-3d-system/src/app/register/page.tsx +++ b/furniture-3d-system/src/app/register/page.tsx @@ -6,7 +6,6 @@ import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { LargeTitle, Body, Footnote, Caption2 } from '@/components/ui/Typography'; -import ThemeToggle from '@/components/ui/ThemeToggle'; import { useToast, ToastContainer } from '@/components/ui/Toast'; import { getApiErrorMessage } from '@/lib/utils'; import type { ApiResponse } from '@/lib/api'; @@ -92,10 +91,6 @@ export default function RegisterPage() {
-
- -
-
diff --git a/furniture-3d-system/src/app/settings/page.tsx b/furniture-3d-system/src/app/settings/page.tsx new file mode 100644 index 00000000..bd5a5e26 --- /dev/null +++ b/furniture-3d-system/src/app/settings/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function SettingsRouteAlias() { + redirect('/admin') +} diff --git a/furniture-3d-system/src/app/system-health/page.tsx b/furniture-3d-system/src/app/system-health/page.tsx index c9eed550..71c56ce2 100644 --- a/furniture-3d-system/src/app/system-health/page.tsx +++ b/furniture-3d-system/src/app/system-health/page.tsx @@ -3,7 +3,7 @@ * * 验证所有设计系统组件和功能的正常运行 * - * @updated 2025-12-12 - 适配深浅色主题 + * @updated 2026-04-28 - 固定浅色主题 */ 'use client' @@ -16,7 +16,6 @@ import Card from '@/components/ui/Card' import Badge, { type BadgeType } from '@/components/ui/Badge' import ProgressBar from '@/components/ui/ProgressBar' import { useToast, ToastContainer } from '@/components/ui/Toast' -import ThemeToggle from '@/components/ui/ThemeToggle' import Link from 'next/link' interface HealthCheck { @@ -63,7 +62,7 @@ export default function SystemHealthPage() { { name: '动画系统', status: 'loading', message: '检查中...' }, { name: '可访问性支持', status: 'loading', message: '检查中...' }, { name: '组件性能', status: 'loading', message: '检查中...' }, - { name: '主题系统', status: 'loading', message: '检查中...' }, + { name: '浅色主题', status: 'loading', message: '检查中...' }, ]) const [overallProgress, setOverallProgress] = React.useState(0) @@ -159,14 +158,13 @@ export default function SystemHealthPage() { details: ['React.memo ✓', 'useMemo ✓', 'useCallback ✓', '虚拟滚动 ✓'] }, { - name: '主题系统', + name: '浅色主题', test: () => { - const supportsColorScheme = window.matchMedia('(prefers-color-scheme: dark)') - return supportsColorScheme ? 'success' : 'warning' + return document.documentElement.getAttribute('data-theme') === 'light' ? 'success' : 'error' }, - successMessage: '主题系统完全支持', - warningMessage: '主题检测功能受限', - details: ['浅色主题 ✓', '深色主题 ✓', '系统检测 ✓', '平滑过渡 ✓'] + successMessage: '浅色主题已固定启用', + errorMessage: '浅色主题未正确写入', + details: ['data-theme=light ✓', '深浅色切换已移除 ✓', '白曜石工作台基调 ✓'] } ] let successCount = 0 @@ -308,7 +306,6 @@ export default function SystemHealthPage() { > 重新检查 -
diff --git a/furniture-3d-system/src/app/ui-preview/page.tsx b/furniture-3d-system/src/app/ui-preview/page.tsx index c5949ad9..89e586fc 100644 --- a/furniture-3d-system/src/app/ui-preview/page.tsx +++ b/furniture-3d-system/src/app/ui-preview/page.tsx @@ -5,7 +5,7 @@ * 用于测试和验证设计系统的实现 * * @created 2025-12-11 - * @updated 2025-12-12 - 完善深浅色主题适配 + * @updated 2026-04-28 - 固定浅色主题 */ 'use client' @@ -42,7 +42,6 @@ import Spinner from '@/components/ui/Spinner' import { SlideInUp } from '@/components/ui/AnimatedEntry' import { SuccessAlert, ErrorAlert, WarningAlert, InfoAlert } from '@/components/ui/Alert' import { AccessibilityPanel, PreferencesProvider } from '@/lib/preferences' -import ThemeToggle from '@/components/ui/ThemeToggle' export default function UIPreviewPage() { const [progress, setProgress] = React.useState(45) @@ -77,7 +76,6 @@ export default function UIPreviewPage() { 基于苹果 Human Interface Guidelines 2025 的设计系统实现
-
diff --git a/furniture-3d-system/src/app/users/page.tsx b/furniture-3d-system/src/app/users/page.tsx index 4f66aad5..229bf40f 100644 --- a/furniture-3d-system/src/app/users/page.tsx +++ b/furniture-3d-system/src/app/users/page.tsx @@ -1,49 +1,73 @@ -'use client'; +'use client' -/** - * 身份控制中心 (Identity Control Center) - * Precision Atelier - 三栏式管理工作台重构版 - */ - -import { useCallback, useState, useMemo } from 'react'; -import type { Role } from '@prisma/client'; -import type { LucideIcon } from 'lucide-react'; -import { - Search, ShieldCheck, UserPlus, - Fingerprint, Mail, Phone, Briefcase, Calendar, Shield -} from 'lucide-react'; -import { AuthProvider, useAuth } from '@/hooks/useAuth'; -import { RequireAuth } from '@/components/auth/RequireAuth'; -import { PermissionGuard } from '@/components/auth/PermissionGuard'; -import Button from '@/components/ui/Button'; -import { Select } from '@/components/ui/Select'; -import { Alert } from '@/components/ui/Alert'; -import { ActivateUserModal } from '@/components/users/ActivateUserModal'; -import { CreateUserModal } from '@/components/users/CreateUserModal'; -import { EditUserModal } from '@/components/users/EditUserModal'; -import { HandoverModal } from '@/components/users/HandoverModal'; -import { DepartmentAccessTree, type UserTreeItem } from '@/components/users/DepartmentAccessTree'; -import { PlatformSplitLayout } from '@/components/platform/PlatformSplitLayout'; -import { canAssignRoleByHierarchy, getAssignableRolesForRole } from '@/lib/role-scope'; -import { cn } from '@/lib/utils'; -import { useDebounce } from '@/hooks/usePerformance'; +import { useCallback, useMemo, useState } from 'react' +import type { Role } from '@prisma/client' +import type { LucideIcon } from 'lucide-react' +import { + Activity, + Briefcase, + Fingerprint, + KeyRound, + Mail, + Phone, + Search, + ShieldCheck, + UserPlus, + Users, +} from 'lucide-react' +import { AuthProvider, useAuth } from '@/hooks/useAuth' +import { RequireAuth } from '@/components/auth/RequireAuth' +import { PermissionGuard } from '@/components/auth/PermissionGuard' +import Button from '@/components/ui/Button' +import { Alert } from '@/components/ui/Alert' +import { ActivateUserModal } from '@/components/users/ActivateUserModal' +import { CreateUserModal } from '@/components/users/CreateUserModal' +import { EditUserModal } from '@/components/users/EditUserModal' +import { HandoverModal } from '@/components/users/HandoverModal' +import { DepartmentAccessTree, type UserTreeItem } from '@/components/users/DepartmentAccessTree' +import { + ObsidianControlShell, + ObsidianDataRow, + ObsidianInspectorSection, + ObsidianRailButton, + ObsidianRailGroup, +} from '@/components/platform/ObsidianControlShell' +import { canAssignRoleByHierarchy, getAssignableRolesForRole } from '@/lib/role-scope' +import { useDebounce } from '@/hooks/usePerformance' const ROLE_OPTIONS = [ - { value: '', label: '所有权限角色' }, - { value: 'SUPER_ADMIN', label: '超级管理员' }, - { value: 'ADMIN', label: '管理员' }, - { value: 'SUPERVISOR', label: '主管' }, - { value: 'DESIGNER', label: '设计师' }, - { value: 'USER', label: '普通用户' }, -]; + { value: '', label: '所有权限角色', icon: Users }, + { value: 'SUPER_ADMIN', label: '超级管理员', icon: KeyRound }, + { value: 'ADMIN', label: '管理员', icon: ShieldCheck }, + { value: 'SUPERVISOR', label: '主管', icon: Briefcase }, + { value: 'DESIGNER', label: '设计师', icon: Fingerprint }, + { value: 'USER', label: '普通用户', icon: Users }, +] as const const STATUS_OPTIONS = [ { value: '', label: '全状态' }, - { value: 'ACTIVE', label: '运行中 / ACTIVE' }, - { value: 'PENDING', label: '待激活 / PENDING' }, - { value: 'INACTIVE', label: '已离职 / INACTIVE' }, - { value: 'SUSPENDED', label: '已锁定 / LOCKED' }, -]; + { value: 'ACTIVE', label: '运行中' }, + { value: 'PENDING', label: '待激活' }, + { value: 'INACTIVE', label: '已离职' }, + { value: 'SUSPENDED', label: '已锁定' }, +] as const + +const roleLabels: Record = { + SUPER_ADMIN: '超级管理员', + ADMIN: '管理员', + SUPERVISOR: '主管', + SUPERVISOR_DEPARTMENT: '部门主管', + SUPERVISOR_TEAM: '小组主管', + DESIGNER: '设计师', + USER: '普通用户', +} + +const statusLabels: Record = { + ACTIVE: '运行中', + PENDING: '待激活', + INACTIVE: '已离职', + SUSPENDED: '已锁定', +} export default function UsersPage() { return ( @@ -54,283 +78,259 @@ export default function UsersPage() { - ); + ) } function UsersPageContent() { - const { user: currentUser } = useAuth(); - const [searchQuery, setSearchQuery] = useState(''); - const [roleFilter, setRoleFilter] = useState(''); - const [statusFilter, setStatusFilter] = useState(''); - const [refreshToken, setRefreshToken] = useState(0); - const [showCreateModal, setShowCreateModal] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); - const [activationUser, setActivationUser] = useState(null); - const [handoverUser, setHandoverUser] = useState(null); - const [actionError, setActionError] = useState(null); + const { user: currentUser } = useAuth() + const [searchQuery, setSearchQuery] = useState('') + const [roleFilter, setRoleFilter] = useState('') + const [statusFilter, setStatusFilter] = useState('') + const [refreshToken, setRefreshToken] = useState(0) + const [showCreateModal, setShowCreateModal] = useState(false) + const [selectedUser, setSelectedUser] = useState(null) + const [editingUser, setEditingUser] = useState(null) + const [activationUser, setActivationUser] = useState(null) + const [handoverUser, setHandoverUser] = useState(null) + const [actionError, setActionError] = useState(null) - const debouncedSearchQuery = useDebounce(searchQuery, 300); + const debouncedSearchQuery = useDebounce(searchQuery, 300) const assignableRoles = useMemo( () => (currentUser ? (getAssignableRolesForRole(currentUser.role) as Role[]) : []), - [currentUser] - ); + [currentUser], + ) const canEditManagedUser = useCallback( (user: UserTreeItem) => { - if (!currentUser) return false; - if (user.id === currentUser.id) return true; - if (currentUser.role === 'SUPER_ADMIN') return true; - return canAssignRoleByHierarchy(currentUser.role, user.role as Role); + if (!currentUser) return false + if (user.id === currentUser.id) return true + if (currentUser.role === 'SUPER_ADMIN') return true + return canAssignRoleByHierarchy(currentUser.role, user.role as Role) }, - [currentUser] - ); + [currentUser], + ) const canActivatePendingUser = useCallback( (user: UserTreeItem) => { - if (!currentUser) return false; - if (user.id === currentUser.id) return false; - if (!['SUPERVISOR', 'ADMIN', 'SUPER_ADMIN'].includes(currentUser.role)) return false; - if (currentUser.role === 'SUPER_ADMIN') return true; - return canAssignRoleByHierarchy(currentUser.role, user.role as Role); + if (!currentUser) return false + if (user.id === currentUser.id) return false + if (!['SUPERVISOR', 'ADMIN', 'SUPER_ADMIN'].includes(currentUser.role)) return false + if (currentUser.role === 'SUPER_ADMIN') return true + return canAssignRoleByHierarchy(currentUser.role, user.role as Role) }, - [currentUser] - ); + [currentUser], + ) const triggerRefresh = useCallback(() => { - setRefreshToken((current) => current + 1); - }, []); + setRefreshToken((current) => current + 1) + }, []) - const closeCreateModal = useCallback(() => setShowCreateModal(false), []); - const closeEditModal = useCallback(() => setSelectedUser(null), []); - const closeActivateModal = useCallback(() => setActivationUser(null), []); - const closeHandoverModal = useCallback(() => setHandoverUser(null), []); + const closeCreateModal = useCallback(() => setShowCreateModal(false), []) + const closeEditModal = useCallback(() => setEditingUser(null), []) + const closeActivateModal = useCallback(() => setActivationUser(null), []) + const closeHandoverModal = useCallback(() => setHandoverUser(null), []) const handleCreateSuccess = useCallback(() => { - closeCreateModal(); - triggerRefresh(); - }, [closeCreateModal, triggerRefresh]); + closeCreateModal() + triggerRefresh() + }, [closeCreateModal, triggerRefresh]) const handleEditSuccess = useCallback(() => { - closeEditModal(); - triggerRefresh(); - }, [closeEditModal, triggerRefresh]); + closeEditModal() + triggerRefresh() + }, [closeEditModal, triggerRefresh]) const handleActivateSuccess = useCallback(() => { - closeActivateModal(); - triggerRefresh(); - }, [closeActivateModal, triggerRefresh]); + closeActivateModal() + triggerRefresh() + }, [closeActivateModal, triggerRefresh]) const handleHandoverSuccess = useCallback(() => { - closeHandoverModal(); - triggerRefresh(); - }, [closeHandoverModal, triggerRefresh]); + closeHandoverModal() + triggerRefresh() + }, [closeHandoverModal, triggerRefresh]) - const handleEditUser = useCallback((user: UserTreeItem) => { - if (!canEditManagedUser(user)) { - setActionError('当前账号不能编辑同级或更高权限用户'); - return; - } - setActionError(null); - setSelectedUser(user); - }, [canEditManagedUser]); - - const handleDeleteUser = useCallback(async (userId: string) => { - try { - setActionError(null); - if (!window.confirm('确定要移除此访问权限吗?此操作不可撤销。')) return; - const response = await fetch(`/api/users/${userId}`, { method: 'DELETE', credentials: 'include' }); - if (!response.ok) throw new Error('删除用户失败'); - triggerRefresh(); - } catch (error) { - setActionError(error instanceof Error ? error.message : '删除失败'); - } - }, [triggerRefresh]); - - const handleActivateUser = useCallback((user: UserTreeItem) => { - if (!canActivatePendingUser(user)) { - setActionError('当前账号不能激活该用户'); - return; - } - setActionError(null); - setActivationUser(user); - }, [canActivatePendingUser]); - - const selectedUserAllowedRoles = useMemo( - () => { - if (!selectedUser || !currentUser) return []; - if (selectedUser.id === currentUser.id) { - return [selectedUser.role as Role]; + const handleEditUser = useCallback( + (user: UserTreeItem) => { + if (!canEditManagedUser(user)) { + setActionError('当前账号不能编辑同级或更高权限用户') + return } - return assignableRoles; + setActionError(null) + setSelectedUser(user) + setEditingUser(user) }, - [assignableRoles, currentUser, selectedUser] - ); + [canEditManagedUser], + ) + + const handleSelectUser = useCallback((user: UserTreeItem) => { + setActionError(null) + setSelectedUser(user) + }, []) + + const handleDeleteUser = useCallback( + async (userId: string) => { + try { + setActionError(null) + if (!window.confirm('确定要移除此访问权限吗?此操作不可撤销。')) return + const response = await fetch(`/api/users/${userId}`, { method: 'DELETE', credentials: 'include' }) + if (!response.ok) throw new Error('删除用户失败') + if (selectedUser?.id === userId) setSelectedUser(null) + if (editingUser?.id === userId) setEditingUser(null) + triggerRefresh() + } catch (error) { + setActionError(error instanceof Error ? error.message : '删除失败') + } + }, + [editingUser?.id, selectedUser?.id, triggerRefresh], + ) + + const handleActivateUser = useCallback( + (user: UserTreeItem) => { + if (!canActivatePendingUser(user)) { + setActionError('当前账号不能激活该用户') + return + } + setActionError(null) + setActivationUser(user) + }, + [canActivatePendingUser], + ) + + const selectedUserAllowedRoles = useMemo(() => { + if (!editingUser || !currentUser) return [] + if (editingUser.id === currentUser.id) { + return [editingUser.role as Role] + } + return assignableRoles + }, [assignableRoles, currentUser, editingUser]) return ( -
- -
-

Quick Filters

-
- {ROLE_OPTIONS.map(opt => ( - - ))} + setShowCreateModal(true)} + className="obsidian-control-action rounded-md border" + > + + 开设账号 + + } + rail={ +
+ + {ROLE_OPTIONS.map((option) => ( + setRoleFilter(option.value)} + /> + ))} + + + {STATUS_OPTIONS.map((option) => ( + setStatusFilter(option.value)} + /> + ))} + +
+ } + inspector={ +
+ {selectedUser ? ( + <> +
+
+
-
- -
-
- - Security Status -
-
- - - -
-
-
- } - mainContent={ -
- {/* 精密搜索栏 */} -
-
- - setSearchQuery(e.target.value)} - className="h-12 w-full rounded-2xl border-none bg-white/80 pl-14 pr-6 text-apple-sm font-apple-medium text-apple-label-primary outline-none focus:bg-white focus:ring-4 focus:ring-apple-blue/5" - /> -
-
- setSearchQuery(event.target.value)} + className="h-10 w-full rounded-[6px] border border-[#d6dee0] bg-white pl-10 pr-4 text-sm font-semibold text-[#152024] outline-none transition placeholder:text-[#7a878a] focus:border-[#152024] focus:bg-white" + /> +
+ - {showCreateModal && } - {activationUser && ( +
+ {actionError ? ( + + ) : null} + +
+ +
+
+
+ + {showCreateModal ? ( + + ) : null} + {activationUser ? ( - )} - {handoverUser && } - {selectedUser && ( + ) : null} + {handoverUser ? ( + + ) : null} + {editingUser ? ( - )} -
- ); + ) : null} +
+ ) } -function SecurityMetric({ label, value, color }: { label: string, value: string, color?: string }) { +function InspectorItem({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) { return ( -
- {label} - {value} +
+ +
+

{label}

+

{value}

+
) } -function InspectorGroup({ title, children }: { title: string, children: React.ReactNode }) { - return ( -
-

{title}

-
{children}
-
- ) -} - -function InspectorItem({ icon: Icon, label, value }: { icon: LucideIcon, label: string, value: string }) { - return ( -
-
- -
-
- {label} - {value} -
-
- ) +function formatDate(value: string) { + const date = new Date(value) + if (Number.isNaN(date.getTime())) return value + return date.toLocaleString('zh-CN') } diff --git a/furniture-3d-system/src/components/layout/AppShell.tsx b/furniture-3d-system/src/components/layout/AppShell.tsx index c793ca57..0d598033 100644 --- a/furniture-3d-system/src/components/layout/AppShell.tsx +++ b/furniture-3d-system/src/components/layout/AppShell.tsx @@ -8,6 +8,9 @@ import { AppSideNav } from './AppSideNav' // AppShell 只服务新平台主链;旧 /products 兼容链和工具页不进入这套全局壳子 export const APP_SHELL_ROOTS = [ '/', + '/profile', + '/users', + '/admin', '/asset-center', '/assisted-assembly', '/draft-groups', @@ -47,8 +50,14 @@ export function AppShell({ children, user }: AppShellProps) { const isDraftGroupsRoute = pathname === '/draft-groups' || pathname.startsWith('/draft-groups/') const isIntakeRoute = pathname === '/intake' || pathname.startsWith('/intake/') const isWarehouseRoute = pathname === '/warehouse' || pathname.startsWith('/warehouse/') + const isProfileRoute = pathname === '/profile' || pathname.startsWith('/profile/') + const isUsersRoute = pathname === '/users' || pathname.startsWith('/users/') + const isAdminRoute = pathname === '/admin' || pathname.startsWith('/admin/') const isWorkbenchShellRoute = isHomeRoute || + isProfileRoute || + isUsersRoute || + isAdminRoute || isAssetCenterRoute || isAssistedAssemblyRoute || isDraftGroupsRoute || @@ -96,11 +105,12 @@ export function AppShell({ children, user }: AppShellProps) {
{children}
diff --git a/furniture-3d-system/src/components/layout/AppTopNav.tsx b/furniture-3d-system/src/components/layout/AppTopNav.tsx index 944c2749..8dc9cf61 100644 --- a/furniture-3d-system/src/components/layout/AppTopNav.tsx +++ b/furniture-3d-system/src/components/layout/AppTopNav.tsx @@ -12,7 +12,6 @@ import { Search, Warehouse, } from 'lucide-react' -import ThemeToggle from '@/components/ui/ThemeToggle' import UserMenu from '@/components/ui/UserMenu' import { APP_BRAND_LOGO_SRC, APP_BRAND_NAME } from '@/lib/branding' @@ -25,6 +24,12 @@ const WORKBENCH_NAV_ITEMS = [ { href: '/warehouse', icon: Warehouse, label: '仓库管理' }, ] +const AUXILIARY_ROUTE_LABELS = [ + { href: '/profile', label: '个人中心' }, + { href: '/users', label: '用户权限' }, + { href: '/admin', label: '后台设置' }, +] as const + const WORKBENCH_BRAND_NAME = APP_BRAND_NAME === '由壹点「产品开发平台」' ? '壹点产品开发平台' : APP_BRAND_NAME @@ -45,7 +50,9 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) { const currentWorkbenchItem = WORKBENCH_NAV_ITEMS.find((item) => item.href === '/' ? pathname === '/' : pathname.startsWith(item.href), - ) ?? WORKBENCH_NAV_ITEMS[0] + ) ?? + AUXILIARY_ROUTE_LABELS.find((item) => pathname === item.href || pathname.startsWith(item.href + '/')) ?? + WORKBENCH_NAV_ITEMS[0] if (variant === 'workbench') { return ( @@ -93,7 +100,6 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) { -
@@ -152,8 +158,6 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) { - -
diff --git a/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx b/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx new file mode 100644 index 00000000..a7492c90 --- /dev/null +++ b/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx @@ -0,0 +1,242 @@ +'use client' + +import Link from 'next/link' +import type { ComponentType, ReactNode } from 'react' +import { ArrowLeft } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface ObsidianControlShellProps { + eyebrow: string + title: string + description?: string + backHref?: string + backLabel?: string + icon?: ComponentType<{ className?: string }> + actions?: ReactNode + rail?: ReactNode + children: ReactNode + inspector?: ReactNode + statusBar?: ReactNode + className?: string + contentClassName?: string + inspectorClassName?: string +} + +export function ObsidianControlShell({ + eyebrow, + title, + backHref, + backLabel = '返回', + icon: Icon, + actions, + rail, + children, + inspector, + className, + contentClassName, + inspectorClassName, +}: ObsidianControlShellProps) { + return ( +
+
+ {rail ? ( + + ) : null} + +
+
+
+ {Icon ? ( +
+ +
+ ) : null} +
+

{eyebrow}

+

{title}

+
+
+ {actions ?
{actions}
: null} +
+
{children}
+
+ + {inspector ? ( + + ) : null} +
+
+ ) +} + +export function ObsidianRailGroup({ title, children }: { title: string; children: ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ) +} + +export function ObsidianRailButton({ + active, + icon: Icon, + label, + meta, + href, + onClick, +}: { + active?: boolean + icon?: ComponentType<{ className?: string }> + label: string + meta?: string + href?: string + onClick?: () => void +}) { + const className = cn( + 'flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition', + active + ? 'bg-[#e7eeec] text-[#101517]' + : 'text-[#c0ccca] hover:bg-white/[0.06] hover:text-white', + ) + const content = ( + <> + {Icon ? : null} + {label} + {meta ? {meta} : null} + + ) + + if (href) { + return ( + + {content} + + ) + } + + return ( + + ) +} + +export function ObsidianPanel({ + title, + eyebrow, + children, + className, +}: { + title?: string + eyebrow?: string + children: ReactNode + className?: string +}) { + return ( +
+ {title || eyebrow ? ( +
+ {eyebrow ? ( +

{eyebrow}

+ ) : null} + {title ?

{title}

: null} +
+ ) : null} + {children} +
+ ) +} + +export function ObsidianInspectorSection({ + title, + children, +}: { + title: string + children: ReactNode +}) { + return ( +
+

+ {title} +

+
{children}
+
+ ) +} + +export function ObsidianDataRow({ + label, + value, + tone = 'default', +}: { + label: string + value: ReactNode + tone?: 'default' | 'success' | 'warning' +}) { + return ( +
+ {label} + + {value} + +
+ ) +} + +export function ObsidianStatusBar({ + items, +}: { + items: Array<{ label: string; value: string; tone?: 'default' | 'success' | 'warning' }> +}) { + return ( +
+ {items.map((item) => ( +
+ + {item.label} + {item.value} +
+ ))} +
+ ) +} diff --git a/furniture-3d-system/src/components/platform/PlatformHeaderTools.tsx b/furniture-3d-system/src/components/platform/PlatformHeaderTools.tsx index 41521052..c6ddf7ab 100644 --- a/furniture-3d-system/src/components/platform/PlatformHeaderTools.tsx +++ b/furniture-3d-system/src/components/platform/PlatformHeaderTools.tsx @@ -1,4 +1,3 @@ -import ThemeToggle from '@/components/ui/ThemeToggle' import UserMenu from '@/components/ui/UserMenu' interface PlatformHeaderToolsProps { @@ -15,7 +14,6 @@ interface PlatformHeaderToolsProps { export function PlatformHeaderTools({ user }: PlatformHeaderToolsProps) { return (
-
) diff --git a/furniture-3d-system/src/components/platform/ViewportStageShell.tsx b/furniture-3d-system/src/components/platform/ViewportStageShell.tsx index ee2cac7c..0316e1dd 100644 --- a/furniture-3d-system/src/components/platform/ViewportStageShell.tsx +++ b/furniture-3d-system/src/components/platform/ViewportStageShell.tsx @@ -68,7 +68,7 @@ export function ViewportStageShell({ {/* 3D 内容区 */}
{children}
- {/* Obsidian Atelier vertical viewport tools */} + {/* White Obsidian Atelier vertical viewport tools */}
{/* 交互模式 */} {enableTranslateGizmo && ( diff --git a/furniture-3d-system/src/components/ui/Modal.tsx b/furniture-3d-system/src/components/ui/Modal.tsx index c0df19b2..2a1ca3ed 100644 --- a/furniture-3d-system/src/components/ui/Modal.tsx +++ b/furniture-3d-system/src/components/ui/Modal.tsx @@ -1,5 +1,5 @@ /** - * Modal 弹窗组件 - Obsidian Atelier 风格 + * Modal 弹窗组件 - White Obsidian Atelier 风格 * * 表单弹窗使用实体面板保证可读性,只把暗色遮罩作为轻量玻璃层。 * 交互细节继续保留 ESC 关闭、焦点循环和关闭后焦点恢复。 @@ -158,9 +158,10 @@ export default function Modal({ ref={modalRef} onKeyDown={handleKeyDown} data-testid="obsidian-modal-panel" - className={`relative w-full ${sizeClasses[size]} overflow-hidden ${className ?? ''}`} + className={`obsidian-modal-panel relative w-full ${sizeClasses[size]} overflow-hidden ${className ?? ''}`} style={{ - backgroundColor: '#ffffff', + backgroundColor: 'var(--obs-shell-panel-bg)', + border: '1px solid var(--obs-shell-border)', borderRadius: '6px', maxHeight: '92vh', display: 'flex', @@ -173,12 +174,12 @@ export default function Modal({ onClick={(e) => e.stopPropagation()} > {(title || showCloseButton) && ( -
+
{title && ( @@ -188,7 +189,7 @@ export default function Modal({ @@ -197,17 +198,17 @@ export default function Modal({ )}
{children}
{footer && ( -
+
{footer}
)} diff --git a/furniture-3d-system/src/components/ui/ThemeToggle.tsx b/furniture-3d-system/src/components/ui/ThemeToggle.tsx index 01363dc7..521d0b0a 100644 --- a/furniture-3d-system/src/components/ui/ThemeToggle.tsx +++ b/furniture-3d-system/src/components/ui/ThemeToggle.tsx @@ -1,71 +1,6 @@ -/** - * 主题切换按钮组件 - * - * 提供深浅色主题切换功能 - * - * @created 2025-12-12 - */ - 'use client' -import React from 'react' -import { Sun, Moon } from 'lucide-react' -import { useUserPreferences } from '@/lib/preferences' - +// 深浅色切换能力已下线;保留空组件只为兼容旧页面导入。 export default function ThemeToggle() { - const { preferences } = useUserPreferences() - const [mounted, setMounted] = React.useState(false) - - // 避免服务端渲染不匹配 - React.useEffect(() => { - setMounted(true) - }, []) - - const toggleTheme = () => { - const currentTheme = - (document.documentElement.getAttribute('data-theme') as 'light' | 'dark' | null) || - preferences.prefersColorScheme - const newTheme = currentTheme === 'dark' ? 'light' : 'dark' - localStorage.setItem('apple-ui-theme', newTheme) - document.documentElement.setAttribute('data-theme', newTheme) - - // 触发存储事件以更新其他组件 - window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme: newTheme } })) - } - - if (!mounted) { - return ( - - ) - } - - const isDark = preferences.prefersColorScheme === 'dark' - - return ( - - ) + return null } diff --git a/furniture-3d-system/src/components/ui/UserMenu.tsx b/furniture-3d-system/src/components/ui/UserMenu.tsx index 79f59017..7cd7b02f 100644 --- a/furniture-3d-system/src/components/ui/UserMenu.tsx +++ b/furniture-3d-system/src/components/ui/UserMenu.tsx @@ -127,6 +127,8 @@ export default function UserMenu({ user }: UserMenuProps) { DESIGNER: '设计师', USER: '普通用户', } + const isAdmin = resolvedUser.role === 'SUPER_ADMIN' || resolvedUser.role === 'ADMIN' + const canManageUsers = isAdmin || resolvedUser.role === 'SUPERVISOR' return (
@@ -250,53 +252,57 @@ export default function UserMenu({ user }: UserMenuProps) { {/* 菜单项 */}
- + + + 用户权限管理 + + + )} - + + + 后台设置 + + + )}
{/* 登出按钮 */} diff --git a/furniture-3d-system/src/components/ui/__tests__/Modal.test.tsx b/furniture-3d-system/src/components/ui/__tests__/Modal.test.tsx index 5e374131..832c8b28 100644 --- a/furniture-3d-system/src/components/ui/__tests__/Modal.test.tsx +++ b/furniture-3d-system/src/components/ui/__tests__/Modal.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react' import Modal from '../Modal' describe('Modal 组件', () => { - it('应该使用 Obsidian Atelier 弹窗外壳,而不是 Apple 大圆角样式', async () => { + it('应该使用 White Obsidian Atelier 弹窗外壳,而不是 Apple 大圆角样式', async () => { render( 创建}> diff --git a/furniture-3d-system/src/components/ui/__tests__/UserMenu.test.tsx b/furniture-3d-system/src/components/ui/__tests__/UserMenu.test.tsx new file mode 100644 index 00000000..62610694 --- /dev/null +++ b/furniture-3d-system/src/components/ui/__tests__/UserMenu.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import UserMenu from '../UserMenu' + +const pushMock = jest.fn() +const refreshMock = jest.fn() + +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + push: pushMock, + refresh: refreshMock, + }), +})) + +const adminUser = { + id: 'user-1', + name: '李承豪', + email: '222@qq.com', + role: 'ADMIN', + avatar: null, + department: null, +} + +describe('UserMenu', () => { + beforeEach(() => { + pushMock.mockReset() + refreshMock.mockReset() + }) + + it('管理员菜单入口应指向真实后台页面,不再跳转到缺失路由', async () => { + const actor = userEvent.setup() + + render() + + await actor.click(screen.getByRole('button', { name: '李 李承豪' })) + + await actor.click(screen.getByRole('button', { name: '用户权限管理' })) + expect(pushMock).toHaveBeenCalledWith('/users') + expect(pushMock).not.toHaveBeenCalledWith('/profile') + + await actor.click(screen.getByRole('button', { name: '李 李承豪' })) + await actor.click(screen.getByRole('button', { name: '后台设置' })) + expect(pushMock).toHaveBeenCalledWith('/admin') + expect(pushMock).not.toHaveBeenCalledWith('/settings') + }) +}) diff --git a/furniture-3d-system/src/components/users/ActivateUserModal.tsx b/furniture-3d-system/src/components/users/ActivateUserModal.tsx index c734d06b..a88ec5af 100644 --- a/furniture-3d-system/src/components/users/ActivateUserModal.tsx +++ b/furniture-3d-system/src/components/users/ActivateUserModal.tsx @@ -176,16 +176,16 @@ export function ActivateUserModal({ }; return ( - +
{errors.submit && ( -
+

{errors.submit}

)} -
+

@@ -206,7 +206,7 @@ export function ActivateUserModal({ options={roleOptions} required errorText={errors.role} - className="bg-white/50" + className="bg-white" /> ({ value: team.id, label: team.name })), ]} - className="bg-white/50" + className="bg-white" />

@@ -245,7 +245,7 @@ export function ActivateUserModal({ variant="plain" onClick={onClose} disabled={loading} - className="rounded-apple-md px-6 uppercase text-[10px] font-apple-black tracking-widest" + className="rounded-md px-6 uppercase text-[10px] font-apple-black tracking-widest" > 取消 @@ -253,7 +253,7 @@ export function ActivateUserModal({ type="submit" variant="filled" loading={loading} - className="rounded-apple-md px-10 uppercase text-[10px] font-apple-black tracking-widest shadow-apple-blue/20" + className="rounded-md px-10 uppercase text-[10px] font-apple-black tracking-widest" > 激活账号 diff --git a/furniture-3d-system/src/components/users/CreateUserModal.tsx b/furniture-3d-system/src/components/users/CreateUserModal.tsx index f29ce7d1..7dd74f9d 100644 --- a/furniture-3d-system/src/components/users/CreateUserModal.tsx +++ b/furniture-3d-system/src/components/users/CreateUserModal.tsx @@ -140,22 +140,21 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr onClose={onClose} title="新建账号" size="medium" - className="liquid-glass" > {errors.submit && ( -
+

{errors.submit}

)} -
+

基础信息

- setFormData({ ...formData, name: e.target.value })} placeholder="张三" required errorText={errors.name} className="bg-white/50" /> - setFormData({ ...formData, email: e.target.value })} placeholder="user@example.com" required errorText={errors.email} className="bg-white/50" /> - setFormData({ ...formData, phone: e.target.value })} placeholder="13800138000" className="bg-white/50" /> + setFormData({ ...formData, name: e.target.value })} placeholder="张三" required errorText={errors.name} className="bg-white" /> + setFormData({ ...formData, email: e.target.value })} placeholder="user@example.com" required errorText={errors.email} className="bg-white" /> + setFormData({ ...formData, phone: e.target.value })} placeholder="13800138000" className="bg-white" />
@@ -165,15 +164,15 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr value: role, label: ROLE_LABELS[role], })) - } required className="bg-white/50" /> + } required className="bg-white" /> setFormData({ ...formData, phone: e.target.value })} placeholder="13800138000" - className="bg-white/50" + className="bg-white" />
@@ -280,7 +279,7 @@ export function EditUserModal({ options={availableRoleOptions} required disabled={disablePrivilegeFields || user.role === 'SUPER_ADMIN'} - className="bg-white/50" + className="bg-white" />
-
+

@@ -350,7 +349,7 @@ export function EditUserModal({ variant="plain" onClick={onClose} disabled={loading} - className="rounded-apple-md px-6 uppercase text-[10px] font-apple-black tracking-widest" + className="rounded-md px-6 uppercase text-[10px] font-apple-black tracking-widest" > 取消 @@ -358,7 +357,7 @@ export function EditUserModal({ type="submit" variant="filled" loading={loading} - className="rounded-apple-md px-10 shadow-apple-blue/20 uppercase text-[10px] font-apple-black tracking-widest" + className="rounded-md px-10 uppercase text-[10px] font-apple-black tracking-widest" > 保存修改 diff --git a/furniture-3d-system/src/components/users/HandoverModal.tsx b/furniture-3d-system/src/components/users/HandoverModal.tsx index a4421204..14dc699f 100644 --- a/furniture-3d-system/src/components/users/HandoverModal.tsx +++ b/furniture-3d-system/src/components/users/HandoverModal.tsx @@ -320,11 +320,10 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa onClose={onClose} title="离职交接" size="large" - className="liquid-glass" >
{errorMessage && ( -
+

{errorMessage}

@@ -336,7 +335,7 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa
) : checklist ? ( <> -
+

@@ -373,7 +372,7 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa

-
+
待转交 SKU
@@ -381,7 +380,7 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa {checklist.summary.totalSKUs}
-
+
创建产品
@@ -389,7 +388,7 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa {checklist.summary.totalProducts}
-
+
协作产品
@@ -400,7 +399,7 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa
{managementWarnings.length > 0 && ( -
+

@@ -425,24 +424,24 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa value={recipientSearch} onChange={(event) => setRecipientSearch(event.target.value)} placeholder="输入姓名或邮箱筛选" - className="bg-white/50" + className="bg-white" />