Commit 190e6b24 authored by Andy-bubu's avatar Andy-bubu

feat: refine dashboard chart palette and headings

parent b9f397bf
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
## 0. 唯一视觉基准 ## 0. 唯一视觉基准
以已完成并验收的“审计项目管理”应用和“问题整改”应用的真实代码为唯一实现基准: 以已完成并验收的“审计项目管理”应用真实代码为唯一实现基准:
- 基准目录固定为 `/Users/liuchenlong/Desktop/job/audit_project_manager`;只读取该项目的真实实现和文档作为视觉、交互、组件结构与验收基准。
- 读取审计项目管理的 docs/xinyuan-design-system.md、docs/ui-architecture.md、docs/ui-migration-checklist.md、src/design-system/theme.css、src/design-system/tokens.css、src/design-system/shell.css,以及 AppShell、DashboardSidebar、DashboardNavbar、ApplicationSwitcherDialog、DataTable、XinyuanEChart 等真实实现。 - 读取审计项目管理的 docs/xinyuan-design-system.md、docs/ui-architecture.md、docs/ui-migration-checklist.md、src/design-system/theme.css、src/design-system/tokens.css、src/design-system/shell.css,以及 AppShell、DashboardSidebar、DashboardNavbar、ApplicationSwitcherDialog、DataTable、XinyuanEChart 等真实实现。
- 问题整改基准目录为 /Users/liuchenlong/Desktop/job/problem_rectification。先读取最近提交记录和对应模块的真实实现,不得只按文字描述自行重写。
- 这不是重新设计。登录页、深色 Sidebar、Navbar、应用切换 Modal、页面边距、Surface 层级、Tabs、筛选、DataGrid、分页、弹窗、KPI、图表、空状态和交互必须与基准的结构、尺寸、间距、层级和行为一致;目标应用只替换自己的业务数据、应用标识、路由、Subject/View 和权限来源。 - 这不是重新设计。登录页、深色 Sidebar、Navbar、应用切换 Modal、页面边距、Surface 层级、Tabs、筛选、DataGrid、分页、弹窗、KPI、图表、空状态和交互必须与基准的结构、尺寸、间距、层级和行为一致;目标应用只替换自己的业务数据、应用标识、路由、Subject/View 和权限来源。
## 1. 开始前硬门槛 ## 1. 开始前硬门槛
...@@ -25,15 +25,22 @@ git status --short ...@@ -25,15 +25,22 @@ git status --short
git branch --show-current git branch --show-current
~~~ ~~~
当前项目已安装 HeroUI Pro: HeroUI Pro 必须在当前目标项目中独立正式安装后才能开始 UI 修改。发现安装缺失、版本不一致、导出方式差异或校验命令不兼容时,优先在当前项目内修复并重新验证,不要因为可修复的环境问题停止整个迁移。
- 不要执行 hpsetup。 - 先只读取当前项目自己的 package.json、锁文件和 node_modules,检查是否缺失、版本是否一致。
- 不要询问、搜索、输出或保存 HeroUI Pro 授权值。 - 如果缺失或 package.json 与 node_modules 实际版本不一致,先在当前项目根目录执行本次用户提供的正式安装命令:
- 只读取当前项目自己的 package.json、锁文件和 node_modules。 `pnpm dlx hpsetup@latest <本次用户提供的授权值>`。
- `<本次用户提供的授权值>` 只能来自用户当前消息或当前任务上下文;禁止从其他项目、Shell 历史、文档、环境文件或 Git 中搜索。
- 不得把授权值写入源码、文档、环境文件、package.json、锁文件或 Git 提交;命令输出中也不要回显授权值。
- 如果 pnpm 不存在,先在当前环境解决 pnpm,再重新执行同一条正式安装命令;网络、缓存、锁文件或安装器提示可修复时,完成修复后继续重试。
- 安装命令执行后,若 package.json 与 node_modules 仍不一致,检查包管理器锁文件、重复安装位置和当前 Node 解析路径,在当前项目内统一到声明版本,再重新安装/验证。
- CommonJS `require.resolve('@heroui-pro/react')` 因 package exports 只提供 ESM 而失败时,不得判定安装失败;改用 `node --input-type=module` 的 `import.meta.resolve('@heroui-pro/react')` 验证。
- `@heroui-pro/react/css` 被 Node 直接动态 import 而出现 `ERR_UNKNOWN_FILE_EXTENSION` 时,不得判定安装失败;用 `import.meta.resolve`、package exports 和 CSS 文件存在性验证,然后通过项目构建验证 bundler 可加载。
- 已知的版本、解析器、锁文件、缓存、pnpm、CSS loader、ESM/CJS 兼容问题必须先修复并继续迁移。只有在当前项目内反复修复仍无法安装/解析,或发现包来源是跨项目依赖时,才暂停并报告具体阻塞。
- 禁止相邻项目 node_modules、跨项目 file: 依赖、绝对 /Users/... 依赖、软链接、本机 vendor 包。 - 禁止相邻项目 node_modules、跨项目 file: 依赖、绝对 /Users/... 依赖、软链接、本机 vendor 包。
- 不得修改 HeroUI/HeroUI Pro 的版本或安装方式。 - 不得擅自修改 HeroUI/HeroUI Pro 版本或安装方式;版本变化只能由正式安装命令产生。
在任何 UI 修改前验证并报告: 在任何 UI 修改前,先完成安装(如需要),再验证并报告:
~~~ ~~~
node -p "require('./node_modules/@heroui-pro/react/package.json').version" node -p "require('./node_modules/@heroui-pro/react/package.json').version"
...@@ -55,7 +62,11 @@ for (const spec of [ ...@@ -55,7 +62,11 @@ for (const spec of [
NODE NODE
~~~ ~~~
@heroui-pro/react/css 是 CSS style/default 导出,不是 Node 可执行 JS;不要把 Node 的 ERR_UNKNOWN_FILE_EXTENSION 误判成包缺失。必须同时确认 package exports 指向 dist/css/index.css,并以项目构建成功证明样式可被 bundler 加载。JS 子路径出现任何导入错误时停止 UI 改造,只报告错误。 @heroui-pro/react/css 是 CSS style/default 导出,不是 Node 可执行 JS;不要把 Node 的 ERR_UNKNOWN_FILE_EXTENSION 误判成包缺失。必须同时确认 package exports 指向 dist/css/index.css,并以项目构建成功证明样式可被 bundler 加载。JS 子路径若出现导入错误,先检查导出版本、解析器和依赖图并修复;修复后仍无法解析才暂停。
### 修复优先原则
后续审计、迁移和验收遇到版本、锁文件、包管理器、ESM/CJS、CSS loader、组件导出、旧组件引用、构建、路由 base、Portal 或浏览器控制台差异时,先判断是否属于当前项目内可修复的问题:读取真实配置和官方 API,按本规范修正,重新执行验证并继续迁移。不要因为第一次命令失败、单个旧引用、样式扫描命中、组件 prop 差异或一次浏览器警告就结束任务。只有在当前工作区不是目标业务项目、缺少用户必须提供的授权/外部权限、涉及无法确定的业务协议,或完成修复和重试后仍存在真实阻塞时,才暂停并报告;暂停时必须列出已尝试的修复和剩余阻塞。
## 2. 必须先审计再迁移 ## 2. 必须先审计再迁移
...@@ -81,16 +92,9 @@ NODE ...@@ -81,16 +92,9 @@ NODE
先从当前后端配置和 user.applications[] 读取真实应用名称、稳定标识、授权名称和 basePath,不得猜测或复制名称。 先从当前后端配置和 user.applications[] 读取真实应用名称、稳定标识、授权名称和 basePath,不得猜测或复制名称。
完整实施功能 1–5 的应用只有: 项目既定的六应用白名单继续决定功能范围:属于该白名单的目标应用实施完整功能 1–5;不属于该白名单的目标应用只实施功能 4“消息通知”。白名单属于业务范围配置,不是视觉基准;不得从任何其他项目复制应用名单、权限或业务代码。若目标项目文档未明确其归属,先报告“功能范围未确认”,不要擅自加入 shortTitle、视图权限或材料中心文件功能。
1. 问题整改
2. 审计项目管理
3. 警示案例库
4. 廉洁从业管理
5. 廉洁风险防控
6. 党风廉政建设
其余鑫元应用只实施功能 4“消息通知”,禁止顺带加入 shortTitle、视图权限或材料中心文件功能。每个应用必须有自己的稳定 APPLICATION_NAME、展示名称、授权名称、basePath 和 source_app;禁止复制 problem_rectification_new、audit_project_manager_new 或任何其他应用标识。 每个应用必须有自己的稳定 APPLICATION_NAME、展示名称、授权名称、basePath 和 source_app;禁止复制 audit_project_manager_new 或任何其他应用标识。
## 4. 依赖和架构边界 ## 4. 依赖和架构边界
...@@ -153,7 +157,8 @@ NODE ...@@ -153,7 +157,8 @@ NODE
### 5.7 ECharts ### 5.7 ECharts
- 所有图表只使用 echarts + echarts-for-react + 共享 XinyuanEChart;HeroUI Pro 只负责外围 Card、布局和 KPI。 - 所有图表只使用 echarts + echarts-for-react + 共享 XinyuanEChart;HeroUI Pro 只负责外围 Card、布局和 KPI。
- 图表颜色使用主题中的 16 个鲜艳且稳定的 --xinyuan-chart-* Token,和 UI danger/success/warning 分离;严谨业务界面不使用粉色/玫红危险色,红系使用鑫元红。 - 图表颜色使用主题中的 16 个鲜艳且稳定的 --xinyuan-chart-* Token,首轮色固定按红→粉→橙→黄→蓝→紫→青→中性灰排列,第 9-16 位只作明暗补充,和 UI danger/success/warning 分离;红色保持鑫元品牌主色,粉色仅作为图表分类色使用。饼图/环图统一使用 `padAngle: 1`、2px Surface 色间隙和 `borderRadius: 10`,间隙色读取 `--xinyuan-chart-sector-gap`,圆角值读取 `--xinyuan-chart-sector-radius`,图表模块外层遵循 24px 圆角。
- 同一图表的相邻分类按业务展示顺序连续使用邻近色相;有显式业务顺序时传入稳定顺序,无显式顺序时按稳定 key 确定性分配。禁止在有业务顺序时使用名称哈希把相邻扇区/图例打散为跳色色系,筛选、排序和 Tab 切换后已有 key 保持原色。
- 共享适配器统一负责轴、网格、图例、ARIA、resize、globalout、HTML Tooltip、appendToBody、confine 和主题字体;页面只提供真实业务数据与 option。 - 共享适配器统一负责轴、网格、图例、ARIA、resize、globalout、HTML Tooltip、appendToBody、confine 和主题字体;页面只提供真实业务数据与 option。
- 饼图/环图外部标签必须使用 ECharts 原生 label/labelLine,标签显示“分类名称 / 真实数量”两行,引导线短而完整,标签锚定到对应引导线终点,不得错位或拉到卡片边缘。 - 饼图/环图外部标签必须使用 ECharts 原生 label/labelLine,标签显示“分类名称 / 真实数量”两行,引导线短而完整,标签锚定到对应引导线终点,不得错位或拉到卡片边缘。
- 同一可见业务区域的同类图表在 1280px 以上成 1fr / 1fr,单图跨满;移动端单列。折线必须对准其业务文案,主文案明显、辅助文案弱化,线条长度适中。 - 同一可见业务区域的同类图表在 1280px 以上成 1fr / 1fr,单图跨满;移动端单列。折线必须对准其业务文案,主文案明显、辅助文案弱化,线条长度适中。
...@@ -177,7 +182,7 @@ settings.shortTitle === true 只隐藏当前 Grid View 的 title 展示列;不 ...@@ -177,7 +182,7 @@ settings.shortTitle === true 只隐藏当前 Grid View 的 title 展示列;不
### 功能 4:消息通知(所有鑫元应用) ### 功能 4:消息通知(所有鑫元应用)
- 使用平台原通知接口和问题整改同等行为,新增独立 /notifications 页面:消息/待办、全部/未读、搜索、分页、单条已读、全部已读、动态 actions 和关联流程跳转。 - 使用平台原通知接口和审计项目管理同等行为,新增独立 /notifications 页面:消息/待办、全部/未读、搜索、分页、单条已读、全部已读、动态 actions 和关联流程跳转。
- Navbar 使用 Badge.Anchor + Badge + Button + Tooltip,36px Bell;未读 0 隐藏,1–99 真实显示,100+ 显示 99+。Sidebar 不加通知入口。 - Navbar 使用 Badge.Anchor + Badge + Button + Tooltip,36px Bell;未读 0 隐藏,1–99 真实显示,100+ 显示 99+。Sidebar 不加通知入口。
- 保留目标应用 basePath。站内链接先去除自身 basePath 交给 Umi;外部链接使用新窗口;通知正文按纯文本渲染,不执行 HTML。 - 保留目标应用 basePath。站内链接先去除自身 basePath 交给 Umi;外部链接使用新窗口;通知正文按纯文本渲染,不执行 HTML。
...@@ -194,7 +199,7 @@ settings.shortTitle === true 只隐藏当前 Grid View 的 title 展示列;不 ...@@ -194,7 +199,7 @@ settings.shortTitle === true 只隐藏当前 Grid View 的 title 展示列;不
## 8. 浏览器验收(必须真实执行) ## 8. 浏览器验收(必须真实执行)
使用 $playwright-cli 启动目标应用,在相同视口逐项对照审计项目管理和问题整改: 使用 $playwright-cli 启动目标应用,在相同视口逐项对照审计项目管理:
- 1440x900、1280x720、768x1024、390x844;浅色内容与固定深色 Sidebar。 - 1440x900、1280x720、768x1024、390x844;浅色内容与固定深色 Sidebar。
- 登录、真实验证码、退出、Sidebar 展开/真 offcanvas、Navbar、应用切换 Modal、通栏搜索、应用卡片和外链。 - 登录、真实验证码、退出、Sidebar 展开/真 offcanvas、Navbar、应用切换 Modal、通栏搜索、应用卡片和外链。
......
...@@ -166,14 +166,24 @@ Inter 与系统回退字体之间切换。 ...@@ -166,14 +166,24 @@ Inter 与系统回退字体之间切换。
- 详情和关联记录中的历史数组字符串必须先解析再展示:数组值使用业务标签以“、”连接;流程图/文档结构等结构化对象展示节点、连线、章节和内容块摘要;无法解析出名称的内部 ID 显示“已关联记录/已关联 N 项”,禁止把 JSON、ObjectId 或存储字段原文直接暴露给业务人员。编辑态 JSON 输入仍保持原数据协议。 - 详情和关联记录中的历史数组字符串必须先解析再展示:数组值使用业务标签以“、”连接;流程图/文档结构等结构化对象展示节点、连线、章节和内容块摘要;无法解析出名称的内部 ID 显示“已关联记录/已关联 N 项”,禁止把 JSON、ObjectId 或存储字段原文直接暴露给业务人员。编辑态 JSON 输入仍保持原数据协议。
- 数字指标使用 Pro KPI;所有图表统一通过共享 `XinyuanEChart` 使用 `echarts` 与 - 数字指标使用 Pro KPI;所有图表统一通过共享 `XinyuanEChart` 使用 `echarts` 与
`echarts-for-react`。系列色、坐标轴、网格、图例、Tooltip Surface/边框/阴影均读取 `echarts-for-react`。系列色、坐标轴、网格、图例、Tooltip Surface/边框/阴影均读取
`theme.css` 的 `--xinyuan-chart-*` Token。当前主题固定提供 16 个鲜艳且稳定的分类色 `theme.css` 的 `--xinyuan-chart-*` Token。首轮 8 个色位固定按红→粉→橙→黄→蓝→紫→青→中性灰排列,
(红、粉、橙、黄、绿、青、蓝、靛、紫、品红及中性灰),页面只提供业务数据和图表 option。图标始终使用 让最多 8 个分类拥有清晰且高级的色相差异;第 9-16 位提供对应的明暗补充,页面只提供业务数据和
图表 option。具体色值依次为 `#991b1b`、`#db2777`、`#ea580c`、`#eab308`、`#1d4ed8`、
`#7e22ce`、`#0891b2`、`#475569`、`#dc2626`、`#ec4899`、`#f97316`、`#facc15`、`#60a5fa`、
`#a855f7`、`#06b6d4`、`#1e293b`。图标始终使用
`AppIcons` 汇出的 Ant Design Icons,ECharts 不作为图标来源。 `AppIcons` 汇出的 Ant Design Icons,ECharts 不作为图标来源。
- 同一图表的分类色按业务展示顺序连续取用相邻色相;有显式业务顺序时必须传入稳定顺序,
无显式顺序时按稳定 key 确定性分配。禁止在有业务顺序时使用名称哈希打散首轮色板,避免相邻
扇区或相邻图例出现无序跳色色系;筛选、排序和 Tab 切换后已有业务 key 保持原色。
- 环形/饼图需要在图表外展示分类内容时,必须使用 ECharts 原生 `label` 与 `labelLine` 引导线, - 环形/饼图需要在图表外展示分类内容时,必须使用 ECharts 原生 `label` 与 `labelLine` 引导线,
使用与标签端点对齐的完整引导线连接对应标签,标签按“分类名称 / 真实数量”分两行显示;分类名称保持 使用与标签端点对齐的完整引导线连接对应标签,标签按“分类名称 / 真实数量”分两行显示;分类名称保持
13px/500,数量使用 14px/600;标签位置必须锚定到对应引导线终点,避免自动避让造成错位。 13px/500,数量使用 14px/600;标签位置必须锚定到对应引导线终点,避免自动避让造成错位。
不得把引导线拉到卡片边缘或只保留 不得把引导线拉到卡片边缘或只保留
底部图例代替内容标注。 底部图例代替内容标注。
- 环形/饼图扇区之间使用统一的紧凑 Surface 色间隙和圆角(`padAngle: 1`、`borderWidth: 2`、
`borderRadius: 10`),形成清晰分隔但不改变数据比例;间隙色必须读取 `--xinyuan-chart-sector-gap`,
圆角值读取 `--xinyuan-chart-sector-radius`,
随主题 Surface 自动适配。图表外层模块继续使用 24px 共享圆角。
- 严谨业务界面不使用粉色、玫红或洋红危险色;HeroUI `danger` / `danger-soft` 必须继承鑫元红系 - 严谨业务界面不使用粉色、玫红或洋红危险色;HeroUI `danger` / `danger-soft` 必须继承鑫元红系
`--danger` Token。图表中的红色序列同样使用红系,不使用粉色替代危险语义。 `--danger` Token。图表中的红色序列同样使用红系,不使用粉色替代危险语义。
......
...@@ -284,10 +284,13 @@ HeroUI Context。`@heroui/styles@3.2.4` 的两个 pnpm peer 虚拟目录仅分 ...@@ -284,10 +284,13 @@ HeroUI Context。`@heroui/styles@3.2.4` 的两个 pnpm peer 虚拟目录仅分
- 驾驶舱状态饼图和阶段柱图已从 HeroUI Pro/Recharts 切换到 ECharts 6.1.0 与 - 驾驶舱状态饼图和阶段柱图已从 HeroUI Pro/Recharts 切换到 ECharts 6.1.0 与
echarts-for-react 3.0.6。共享 `XinyuanEChart` 统一 ARIA、主题系列色、图例、坐标轴、网格和 echarts-for-react 3.0.6。共享 `XinyuanEChart` 统一 ARIA、主题系列色、图例、坐标轴、网格和
body HTML Tooltip;柱宽仍限制为 28px,API、统计口径、项目详情入口和数据结构未改。 body HTML Tooltip;柱宽仍限制为 28px,API、统计口径、项目详情入口和数据结构未改。
- 图表分类色已扩展为 `--xinyuan-chart-1` 至 `--xinyuan-chart-16`,使用鲜艳固定色值; - 图表分类色已扩展为 `--xinyuan-chart-1` 至 `--xinyuan-chart-16`,使用鲜艳固定色值,首轮顺序固定为
通过稳定业务 key 分配颜色,禁止页面写入随机或局部色值。 红→粉→橙→黄→蓝→紫→青→中性灰;
通过稳定业务顺序/业务 key 分配相邻色相,禁止页面写入随机或局部色值;项目状态分布按
计划、执行、归档、整改、待启动、完成的顺序连续取用色板,筛选后 key 颜色保持不变。
- 状态环图已启用 ECharts 外部分类标签和 `labelLine` 引导线,标签显示真实状态与数量, - 状态环图已启用 ECharts 外部分类标签和 `labelLine` 引导线,标签显示真实状态与数量,
使用 `labelLayout` 根据 `labelLinePoints` 将标签锚定到各自折线终点,避免文案与折线错位。 使用 `labelLayout` 根据 `labelLinePoints` 将标签锚定到各自折线终点,避免文案与折线错位。
- 状态环图扇区统一使用 `padAngle: 1`、2px Surface 色间隙和 `borderRadius: 10`,扇区边界紧凑清晰且不改变真实数量比例;图表模块外层使用 24px 圆角。
- 删除、拒绝和危险操作的 `danger-soft` 背景已从默认粉色修正为鑫元红系,活动页面不再出现 - 删除、拒绝和危险操作的 `danger-soft` 背景已从默认粉色修正为鑫元红系,活动页面不再出现
粉色危险操作视觉。 粉色危险操作视觉。
- 列表普通业务单元格已关闭 Hover 文案 Tooltip;操作列仍保留查看、编辑、删除等真实操作按钮 - 列表普通业务单元格已关闭 Hover 文案 Tooltip;操作列仍保留查看、编辑、删除等真实操作按钮
......
...@@ -59,6 +59,23 @@ export default function XinyuanEChart({ ...@@ -59,6 +59,23 @@ export default function XinyuanEChart({
const themedOption = useMemo(() => { const themedOption = useMemo(() => {
const theme = getXinyuanEChartsTheme(); const theme = getXinyuanEChartsTheme();
const applyPieTheme = (series) => {
if (series?.type !== 'pie') return series;
return {
...series,
itemStyle: {
borderColor: theme.sectorGap,
borderRadius: theme.sectorRadius,
borderWidth: 2,
...series.itemStyle,
},
padAngle: series.padAngle ?? 1,
};
};
const themedSeries = Array.isArray(option?.series)
? option.series.map(applyPieTheme)
: applyPieTheme(option?.series);
return { return {
animationDuration: 320, animationDuration: 320,
...@@ -76,6 +93,7 @@ export default function XinyuanEChart({ ...@@ -76,6 +93,7 @@ export default function XinyuanEChart({
...option?.textStyle, ...option?.textStyle,
}, },
...option, ...option,
series: themedSeries,
legend: option?.legend ? { legend: option?.legend ? {
icon: 'circle', icon: 'circle',
itemHeight: 8, itemHeight: 8,
......
...@@ -37,27 +37,59 @@ function hashChartKey(key) { ...@@ -37,27 +37,59 @@ function hashChartKey(key) {
); );
} }
export function createXinyuanChartColorMap(keys = []) { export function createXinyuanChartColorMap(keys = [], options = {}) {
const stableKeys = [...new Set(keys.map(normalizeChartKey).filter(Boolean))] const sourceKeys = Array.isArray(keys) ? keys : [];
.sort(); const order = Array.isArray(options?.order) ? options.order : [];
const orderedKeys = [...new Set(sourceKeys.map(normalizeChartKey).filter(Boolean))];
const preferredOrder = new Map(
order.map(normalizeChartKey).filter(Boolean).map((key, index) => [key, index]),
);
const stableKeys = orderedKeys.sort((left, right) => {
const leftOrder = preferredOrder.has(left)
? preferredOrder.get(left)
: Number.MAX_SAFE_INTEGER;
const rightOrder = preferredOrder.has(right)
? preferredOrder.get(right)
: Number.MAX_SAFE_INTEGER;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
return left.localeCompare(right, 'zh-CN');
});
const unknownPositions = new Map(
stableKeys
.filter((key) => !preferredOrder.has(key))
.map((key, index) => [key, index]),
);
const colorMap = new Map(); const colorMap = new Map();
const usedIndexes = new Set(); const usedIndexes = new Set();
stableKeys.forEach((key, keyIndex) => { stableKeys.forEach((key) => {
const preferredIndex = hashChartKey(key) % XINYUAN_CHART_PALETTE.length; const hasExplicitOrder = preferredOrder.size > 0;
let paletteIndex = preferredIndex; let paletteIndex = hasExplicitOrder && preferredOrder.has(key)
? preferredOrder.get(key) % XINYUAN_CHART_PALETTE.length
: hasExplicitOrder
? preferredOrder.size + unknownPositions.get(key)
: hashChartKey(key) % XINYUAN_CHART_PALETTE.length;
if (keyIndex < XINYUAN_CHART_PALETTE.length) { if (
paletteIndex >= XINYUAN_CHART_PALETTE.length
|| usedIndexes.has(paletteIndex)
) {
const preferredIndex = hashChartKey(key) % XINYUAN_CHART_PALETTE.length;
paletteIndex = preferredIndex;
for (let offset = 0; offset < XINYUAN_CHART_PALETTE.length; offset += 1) { for (let offset = 0; offset < XINYUAN_CHART_PALETTE.length; offset += 1) {
const candidateIndex = (preferredIndex + offset) % XINYUAN_CHART_PALETTE.length; const candidateIndex = (preferredIndex + offset) % XINYUAN_CHART_PALETTE.length;
if (!usedIndexes.has(candidateIndex)) { if (!usedIndexes.has(candidateIndex)) {
paletteIndex = candidateIndex; paletteIndex = candidateIndex;
usedIndexes.add(candidateIndex);
break; break;
} }
} }
} }
usedIndexes.add(paletteIndex);
colorMap.set(key, XINYUAN_CHART_PALETTE[paletteIndex]); colorMap.set(key, XINYUAN_CHART_PALETTE[paletteIndex]);
}); });
...@@ -79,9 +111,9 @@ export function resolveXinyuanChartColor(color, fallback = '') { ...@@ -79,9 +111,9 @@ export function resolveXinyuanChartColor(color, fallback = '') {
return match ? readRootToken(match[1], fallback) : String(color || fallback); return match ? readRootToken(match[1], fallback) : String(color || fallback);
} }
export function createResolvedXinyuanChartColorMap(keys = []) { export function createResolvedXinyuanChartColorMap(keys = [], options = {}) {
return new Map( return new Map(
Array.from(createXinyuanChartColorMap(keys)).map(([key, color]) => [ Array.from(createXinyuanChartColorMap(keys, options)).map(([key, color]) => [
key, key,
resolveXinyuanChartColor(color), resolveXinyuanChartColor(color),
]), ]),
...@@ -104,6 +136,10 @@ export function getXinyuanEChartsTheme() { ...@@ -104,6 +136,10 @@ export function getXinyuanEChartsTheme() {
)), )),
tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'), tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'),
tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'), tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'),
sectorGap: readRootToken('--xinyuan-chart-sector-gap', '#ffffff'),
sectorRadius: Number.parseFloat(
readRootToken('--xinyuan-chart-sector-radius', '10'),
) || 10,
tooltipShadow: readRootToken( tooltipShadow: readRootToken(
'--xinyuan-chart-tooltip-shadow', '--xinyuan-chart-tooltip-shadow',
'0 16px 40px rgb(15 23 42 / 14%)', '0 16px 40px rgb(15 23 42 / 14%)',
......
...@@ -435,6 +435,23 @@ ...@@ -435,6 +435,23 @@
gap: var(--rc-page-module-gap); gap: var(--rc-page-module-gap);
} }
.rc-section-title {
margin: 0;
color: var(--rc-text-primary);
font-size: 1.25rem;
font-weight: var(--rc-font-semibold);
line-height: 1.75rem;
letter-spacing: 0;
}
.rc-module-title {
color: var(--rc-text-primary);
font-size: 1.125rem;
font-weight: var(--rc-font-semibold);
line-height: 1.5rem;
letter-spacing: 0;
}
.rc-page-grid { .rc-page-grid {
min-width: 0; min-width: 0;
gap: var(--rc-page-module-gap); gap: var(--rc-page-module-gap);
......
...@@ -70,22 +70,26 @@ ...@@ -70,22 +70,26 @@
* Ant Design remains the icon source. Resolve these CSS variables to * Ant Design remains the icon source. Resolve these CSS variables to
* concrete colors before assigning them to an ECharts option object. * concrete colors before assigning them to an ECharts option object.
*/ */
--xinyuan-chart-1: #d92d20; /* First pass: red -> pink -> orange -> yellow -> blue -> purple -> cyan -> neutral. */
--xinyuan-chart-2: #ef4444; --xinyuan-chart-1: #991b1b;
--xinyuan-chart-3: #f97316; --xinyuan-chart-2: #db2777;
--xinyuan-chart-4: #f59e0b; --xinyuan-chart-3: #ea580c;
--xinyuan-chart-5: #22c55e; --xinyuan-chart-4: #eab308;
--xinyuan-chart-6: #14b87a; --xinyuan-chart-5: #1d4ed8;
--xinyuan-chart-7: #06b6d4; --xinyuan-chart-6: #7e22ce;
--xinyuan-chart-8: #0ea5e9; --xinyuan-chart-7: #0891b2;
--xinyuan-chart-9: #3b82f6; --xinyuan-chart-8: #475569;
--xinyuan-chart-10: #6366f1; /* Second pass: stronger/lighter companions for charts with more series. */
--xinyuan-chart-11: #8b5cf6; --xinyuan-chart-9: #dc2626;
--xinyuan-chart-12: #7c3aed; --xinyuan-chart-10: #ec4899;
--xinyuan-chart-13: #b42318; --xinyuan-chart-11: #f97316;
--xinyuan-chart-14: #84cc16; --xinyuan-chart-12: #facc15;
--xinyuan-chart-15: #a855f7; --xinyuan-chart-13: #60a5fa;
--xinyuan-chart-16: #64748b; --xinyuan-chart-14: #a855f7;
--xinyuan-chart-15: #06b6d4;
--xinyuan-chart-16: #1e293b;
--xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-radius: 10px;
--xinyuan-chart-grid: #e8e8e8; --xinyuan-chart-grid: #e8e8e8;
--xinyuan-chart-axis: #737373; --xinyuan-chart-axis: #737373;
--xinyuan-chart-label: #737373; --xinyuan-chart-label: #737373;
......
...@@ -165,10 +165,10 @@ ...@@ -165,10 +165,10 @@
--rc-chart-15: var(--xinyuan-chart-15); --rc-chart-15: var(--xinyuan-chart-15);
--rc-chart-16: var(--xinyuan-chart-16); --rc-chart-16: var(--xinyuan-chart-16);
--rc-chart-primary: var(--rc-chart-1); --rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03); --rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-6); --rc-chart-warning: var(--rc-chart-3);
--rc-chart-positive: var(--rc-chart-2); --rc-chart-positive: var(--rc-chart-7);
--rc-chart-comparison: var(--rc-chart-4); --rc-chart-comparison: var(--rc-chart-5);
--rc-chart-neutral: var(--rc-chart-8); --rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size); --rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size);
--rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height); --rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height);
......
...@@ -34,6 +34,13 @@ const STATUS_TONES = { ...@@ -34,6 +34,13 @@ const STATUS_TONES = {
归档: 'secondary', 归档: 'secondary',
}; };
const STATUS_CHART_ORDER = ['计划', '执行', '归档', '整改', '待启动', '完成', '未设置'];
function getStatusChartOrder(status) {
const index = STATUS_CHART_ORDER.indexOf(status);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
}
function MetricCard({ label, value, icon: Icon, tone, onPress }) { function MetricCard({ label, value, icon: Icon, tone, onPress }) {
const numericValue = Number.parseFloat(String(value).replace(/[% ,]/g, '')); const numericValue = Number.parseFloat(String(value).replace(/[% ,]/g, ''));
const content = ( const content = (
...@@ -93,7 +100,7 @@ function ProjectPanel({ title, records, emptyText, onOpen }) { ...@@ -93,7 +100,7 @@ function ProjectPanel({ title, records, emptyText, onOpen }) {
return ( return (
<Card className="rc-dashboard-card"> <Card className="rc-dashboard-card">
<Card.Header className="flex-row items-center gap-3"> <Card.Header className="flex-row items-center gap-3">
<Card.Title className="min-w-0 flex-1">{title}</Card.Title> <Card.Title className="rc-module-title min-w-0 flex-1">{title}</Card.Title>
</Card.Header> </Card.Header>
<Card.Content className="max-h-80 overflow-y-auto"> <Card.Content className="max-h-80 overflow-y-auto">
<ListView <ListView
...@@ -123,6 +130,7 @@ function StatusChart({ data }) { ...@@ -123,6 +130,7 @@ function StatusChart({ data }) {
const theme = getXinyuanEChartsTheme(); const theme = getXinyuanEChartsTheme();
const colorMap = createResolvedXinyuanChartColorMap( const colorMap = createResolvedXinyuanChartColorMap(
data.map((item) => item.name), data.map((item) => item.name),
{ order: STATUS_CHART_ORDER },
); );
return { return {
...@@ -132,7 +140,9 @@ function StatusChart({ data }) { ...@@ -132,7 +140,9 @@ function StatusChart({ data }) {
}, },
series: [{ series: [{
avoidLabelOverlap: true, avoidLabelOverlap: true,
data: data.map((item) => ({ data: [...data].sort(
(left, right) => getStatusChartOrder(left.name) - getStatusChartOrder(right.name),
).map((item) => ({
...item, ...item,
itemStyle: { color: colorMap.get(item.name) }, itemStyle: { color: colorMap.get(item.name) },
})), })),
...@@ -355,21 +365,21 @@ export default function AuditProjectDashboardPage() { ...@@ -355,21 +365,21 @@ export default function AuditProjectDashboardPage() {
</section> </section>
<section className="rc-page-section"> <section className="rc-page-section">
<h2 className="text-base font-medium text-foreground">项目进度分析</h2> <h2 className="rc-section-title">项目进度分析</h2>
<div className="rc-page-grid rc-chart-grid"> <div className="rc-page-grid rc-chart-grid">
<Card className="rc-dashboard-card"> <Card className="rc-dashboard-card">
<Card.Header><Card.Title>项目状态分布</Card.Title></Card.Header> <Card.Header><Card.Title className="rc-module-title">项目状态分布</Card.Title></Card.Header>
<Card.Content>{dashboard.statusData.length ? <StatusChart data={dashboard.statusData} /> : null}</Card.Content> <Card.Content>{dashboard.statusData.length ? <StatusChart data={dashboard.statusData} /> : null}</Card.Content>
</Card> </Card>
<Card className="rc-dashboard-card"> <Card className="rc-dashboard-card">
<Card.Header><Card.Title>开展中项目进度</Card.Title></Card.Header> <Card.Header><Card.Title className="rc-module-title">开展中项目进度</Card.Title></Card.Header>
<Card.Content><StageChart data={dashboard.stageData} /></Card.Content> <Card.Content><StageChart data={dashboard.stageData} /></Card.Content>
</Card> </Card>
</div> </div>
</section> </section>
<section className="rc-page-section"> <section className="rc-page-section">
<h2 className="text-base font-medium text-foreground">项目提醒</h2> <h2 className="rc-section-title">项目提醒</h2>
<div className="grid gap-6 xl:grid-cols-3"> <div className="grid gap-6 xl:grid-cols-3">
<ProjectPanel title="逾期未完成项目" records={dashboard.overdue} emptyText="暂无逾期未完成项目" onOpen={openRecord} /> <ProjectPanel title="逾期未完成项目" records={dashboard.overdue} emptyText="暂无逾期未完成项目" onOpen={openRecord} />
<ProjectPanel title="正在开展项目" records={dashboard.active} emptyText="暂无正在开展项目" onOpen={openRecord} /> <ProjectPanel title="正在开展项目" records={dashboard.active} emptyText="暂无正在开展项目" onOpen={openRecord} />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment