Commit a1f45177 authored by Andy-bubu's avatar Andy-bubu

chore: sync develop changes

parent 152a3358
...@@ -17,9 +17,15 @@ ...@@ -17,9 +17,15 @@
`@heroui-pro/react@1.0.0-beta.8` 由官方 `hpsetup` 安装到本项目,`package.json` 使用正式版本范围,锁文件解析到 registry 包。本项目不得再使用跨项目 `file:`、绝对路径、相邻 `node_modules`、符号链接或 Webpack symlink 兼容配置。 `@heroui-pro/react@1.0.0-beta.8` 由官方 `hpsetup` 安装到本项目,`package.json` 使用正式版本范围,锁文件解析到 registry 包。本项目不得再使用跨项目 `file:`、绝对路径、相邻 `node_modules`、符号链接或 Webpack symlink 兼容配置。
## 品牌与图表 Token
- 本应用的 HeroUI Primary 入口为 `--accent: #DA241A`,并提供固定 `--primary`/`--color-primary-*` 品牌色阶别名;`--focus` 同值,品牌 Hover 为 `#C91F17`。不得在页面或组件中另设 primary 色或让默认蓝紫主题覆盖。
- 图表必须通过 `XinyuanEChart` 与 `chartPalette.js` 使用 `--xinyuan-chart-*`。色板顺序固定为品牌红深浅、暖棕、灰紫、灰蓝,禁止绿色、荧光色和彩虹式相邻配色;单系列使用品牌红。
- 图表 Token 在浅色/深色主题中保持相同具体颜色,ECharts 入参必须是可绘制的具体颜色,不能把嵌套 CSS `var(...)` 原样传入 Canvas。
## 样式所有权 ## 样式所有权
`src/design-system/theme.css` 与 `tokens.css` 直接同步审计项目管理当前版本;`shell.css`、`tailwind.css` 与 `global.less` 使用同一共享结构规则,并保留当前项目的 Sidebar 状态、固定列和 Autocomplete 受控值扩展。项目不再加载 `portable.css`,页面没有私有 CSS 外观层。 `src/design-system/theme.css` 与 `tokens.css` 保持审计项目管理的变量结构,仅在本项目的品牌与图表专项中覆盖 `--accent`/`--focus` 和 `--xinyuan-chart-*`;`shell.css`、`tailwind.css` 与 `global.less` 使用同一共享结构规则,并保留当前项目的 Sidebar 状态、固定列和 Autocomplete 受控值扩展。项目不再加载 `portable.css`,页面没有私有 CSS 外观层。
本地浏览器验收产物目录 `.playwright-cli/` 与 `test-results/` 必须保持在 `.gitignore` 中。Tailwind CSS v4 会自动探测未忽略的项目文件;若验收日志进入扫描范围,CSS HMR 日志写入会反向触发持续编译,造成样式表和字体反复重载。 本地浏览器验收产物目录 `.playwright-cli/` 与 `test-results/` 必须保持在 `.gitignore` 中。Tailwind CSS v4 会自动探测未忽略的项目文件;若验收日志进入扫描范围,CSS HMR 日志写入会反向触发持续编译,造成样式表和字体反复重载。
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
- [x] Shell:Sidebar/Navbar/退出/应用切换/offcanvas(审计项目管理结构) - [x] Shell:Sidebar/Navbar/退出/应用切换/offcanvas(审计项目管理结构)
- [x] 应用切换:同步审计项目管理 Modal 几何、动态分组/计数、其他/全部回退、两列 Card 与 identity well(代码结构) - [x] 应用切换:同步审计项目管理 Modal 几何、动态分组/计数、其他/全部回退、两列 Card 与 identity well(代码结构)
- [x] 驾驶舱:Pro KPI、统一 `XinyuanEChart`/ECharts;环形扇区间隙/10px 扇区圆角/外部引导线、柱图 8px 末端圆角与 24px 分析模块圆角(代码结构) - [x] 驾驶舱:Pro KPI、统一 `XinyuanEChart`/ECharts;环形扇区间隙/10px 扇区圆角/外部引导线、柱图 8px 末端圆角与 24px 分析模块圆角(代码结构)
- [x] 品牌与图表专项:HeroUI `--accent`/`--focus` 固定为 `#DA241A`,品牌 Hover 为 `#C91F17`;图表 16 色切换为低饱和品牌红、暖棕、灰紫、灰蓝体系,移除绿色/荧光色与彩虹式相邻配色;KPI 图标同步使用同一协调色系
- [x] 案例管理:Tabs、筛选工具栏、DataGrid、Pagination、ActionBar、EmptyState(代码结构) - [x] 案例管理:Tabs、筛选工具栏、DataGrid、Pagination、ActionBar、EmptyState(代码结构)
- [x] 弹窗:新建、查看/编辑、批量修改、导入、删除确认(代码结构) - [x] 弹窗:新建、查看/编辑、批量修改、导入、删除确认(代码结构)
- [x] 清除 Catalyst/Headless/旧图表调用/原生可见控件与页面私有视觉 CSS - [x] 清除 Catalyst/Headless/旧图表调用/原生可见控件与页面私有视觉 CSS
...@@ -57,4 +58,6 @@ ...@@ -57,4 +58,6 @@
- [x] HeroUI/React Aria 依赖树唯一 - [x] HeroUI/React Aria 依赖树唯一
- [x] HeroUI Pro 必需子路径通过 ESM 实际导入 - [x] HeroUI Pro 必需子路径通过 ESM 实际导入
本次品牌与图表专项额外执行:`npm run build`、`git diff --check`、颜色 token 扫描及 Playwright computed token/canvas/viewport 检查。结果:构建通过、差异检查通过、图表与 KPI 未使用绿色/荧光色;1440x900/1280x720 viewport 溢出为 0,移动端宽表保留既有内部横向滚动。
应用切换与数据表目标页打开态已在历史真实会话中验收;本次主题同步后的受鉴权复核仍需有效验证码,不得使用伪造 token、网络数据或测试数字替代。 应用切换与数据表目标页打开态已在历史真实会话中验收;本次主题同步后的受鉴权复核仍需有效验证码,不得使用伪造 token、网络数据或测试数字替代。
# 鑫元设计系统 # 鑫元设计系统
本项目直接采用 HeroUI/HeroUI Pro 与鑫元权威主题。“审计项目管理”应用是当前 Shell、列表、弹窗与应用切换的结构、尺寸、间距、组件层级与交互基准。 本项目直接采用 HeroUI/HeroUI Pro 与鑫元权威主题。“审计项目管理”应用是当前 Shell、列表、弹窗与应用切换的结构、尺寸、间距、组件层级与交互基准。本项目的鑫元基金品牌主色固定为 `#DA241A`。HeroUI v3 的真实主按钮入口为 `--accent`,同时维护 `--primary`、`--primary-50` 至 `--primary-950` 及对应 `--color-primary-*` Tailwind 映射;`--focus` 与品牌 Hover 固定使用品牌红体系,不允许默认蓝紫主题覆盖。
## 依赖与样式 ## 依赖与样式
- 基础控件直接从 `@heroui/react` 导入。 - 基础控件直接从 `@heroui/react` 导入。
- Sidebar、Navbar、DataGrid、ActionBar、EmptyState、DropZone 与 KPI 从 `@heroui-pro/react` 对应子路径直接导入;业务图表统一通过共享 `XinyuanEChart` 使用 ECharts。 - Sidebar、Navbar、DataGrid、ActionBar、EmptyState、DropZone 与 KPI 从 `@heroui-pro/react` 对应子路径直接导入;业务图表统一通过共享 `XinyuanEChart` 使用 ECharts。
- 样式顺序固定为 Tailwind、HeroUI、HeroUI Pro、`theme.css`、`tokens.css`、批准的共享布局层。 - 样式顺序固定为 Tailwind、HeroUI、HeroUI Pro、`theme.css`、`tokens.css`、批准的共享布局层。
- 本轮 `theme.css` 与 `tokens.css` 必须与审计项目管理当前版本逐字节一致;页面不得覆盖 HeroUI 外观、slot、BEM 类或交互状态。 - `theme.css` 保留 HeroUI 官方变量结构;本项目允许的品牌覆盖仅为鑫元基金品牌主色 `#DA241A`、品牌 Hover `#C91F17`、固定 primary 色阶和对应图表/指标 Token。页面不得覆盖 HeroUI 外观、slot、BEM 类或交互状态。
- 页面 `className` 只承担 flex、grid、宽度、间距、对齐和响应式布局。 - 页面 `className` 只承担 flex、grid、宽度、间距、对齐和响应式布局。
## 页面规则 ## 页面规则
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
- 页容量选择使用现有 HeroUI Autocomplete 的受控 value 合同;切换容量只重置列表页码并复用既有查询回调,不改变筛选、排序或服务请求协议。 - 页容量选择使用现有 HeroUI Autocomplete 的受控 value 合同;切换容量只重置列表页码并复用既有查询回调,不改变筛选、排序或服务请求协议。
- 详情和内容型表单使用鑫元 4xl(1088px)Modal:HeroUI `Modal.Container size="lg"` 保留官方交互与响应式结构,`Modal.Dialog` 通过共享布局类限制为 `bg-background p-7 sm:max-w-[68rem]`,使详情背景与页面画布一致;查看态不添加灰色嵌套 Surface,编辑态直接放置 `secondary` 控件。 - 详情和内容型表单使用鑫元 4xl(1088px)Modal:HeroUI `Modal.Container size="lg"` 保留官方交互与响应式结构,`Modal.Dialog` 通过共享布局类限制为 `bg-background p-7 sm:max-w-[68rem]`,使详情背景与页面画布一致;查看态不添加灰色嵌套 Surface,编辑态直接放置 `secondary` 控件。
- 指标必须使用 HeroUI Pro KPI compound API;所有业务图表统一使用共享 `XinyuanEChart` ECharts 渲染器,图表主题、Tooltip、坐标轴和颜色由鑫元 Token 统一注入。 - 指标必须使用 HeroUI Pro KPI compound API;所有业务图表统一使用共享 `XinyuanEChart` ECharts 渲染器,图表主题、Tooltip、坐标轴和颜色由鑫元 Token 统一注入。
- 图表业务序列只使用 `--rc-chart-*` Token,不使用 UI 状态色或页面硬编码颜色。 - 图表业务序列只使用 `--rc-chart-*` Token,不使用 UI 状态色或页面硬编码颜色。色板按“品牌红深浅 -> 暖棕 -> 灰紫 -> 灰蓝”协调排序,禁止绿色、荧光色和彩虹式相邻对撞;单系列统一为 `#DA241A`,同一指标状态使用同色相深浅变化。当前 16 色顺序为 `#DA241A`、`#B83A32`、`#C95A52`、`#D47A70`、`#A65A45`、`#B87958`、`#C99772`、`#8A6F63`、`#9A8174`、`#6F6674`、`#807783`、`#938B96`、`#4F6380`、`#61758F`、`#74879A`、`#8798A8`。
- 环形图通过共享 `--xinyuan-chart-sector-gap`、`--xinyuan-chart-sector-radius` 呈现扇区间隙与 10px 扇区圆角;柱图通过共享图表几何统一使用 8px 末端圆角。分析卡片沿用 `.rc-dashboard-card` 的 24px 外圆角。 - 环形图通过共享 `--xinyuan-chart-sector-gap`、`--xinyuan-chart-sector-radius` 呈现扇区间隙与 10px 扇区圆角;柱图通过共享图表几何统一使用 8px 末端圆角。分析卡片沿用 `.rc-dashboard-card` 的 24px 外圆角。
## 跨应用能力 ## 跨应用能力
......
# 鑫元 UI 审计 # 鑫元 UI 审计
审计日期:2026-09-01。目标仓库为 Umi 4 / React 19,部署基路径为 `/risk_case_library/`。本轮 Shell、列表、弹窗和应用切换的唯一实现基准为“审计项目管理”。 审计日期:2026-09-02。目标仓库为 Umi 4 / React 19,部署基路径为 `/risk_case_library/`。本轮 Shell、列表、弹窗和应用切换的唯一实现基准为“审计项目管理”;品牌与图表专项以鑫元基金品牌红 `#DA241A` 为唯一 Primary 基准。
## 安装与运行基线 ## 安装与运行基线
- 官方安装命令已在项目根目录执行成功,`@heroui-pro/react` 解析为本项目 `node_modules` 内的 `1.0.0-beta.8`,不是符号链接或跨项目 `file:` 依赖。 - 官方安装命令已在项目根目录执行成功,`@heroui-pro/react` 解析为本项目 `node_modules` 内的 `1.0.0-beta.8`,不是符号链接或跨项目 `file:` 依赖。
- 项目使用 npm,保留 `package-lock.json`;旧 `pnpm-lock.yaml` 已删除。 - 项目使用 npm,保留 `package-lock.json`;旧 `pnpm-lock.yaml` 已删除。
- 当前迁移实例运行于 `http://localhost:8003/risk_case_library/`。 - 当前迁移实例运行于 `http://localhost:8006/risk_case_library/`。
- “审计项目管理”源代码基准位于 `/Users/liuchenlong/Desktop/job/audit_project_manager`。 - “审计项目管理”源代码基准位于 `/Users/liuchenlong/Desktop/job/audit_project_manager`。
## 业务契约 ## 业务契约
...@@ -19,16 +19,24 @@ ...@@ -19,16 +19,24 @@
## 全局审计结果 ## 全局审计结果
### 图表配色与品牌专项
- HeroUI `--accent` 与 `--focus` 已固定为 `#DA241A`,品牌 Hover 为 `#C91F17`;Button、Tabs、Chip、选中态和焦点态不再使用 HeroUI 默认蓝色。
- `--xinyuan-chart-1..16` 已替换为低饱和、低冲突的品牌红/暖棕/灰紫/灰蓝顺序,完整色板不含绿色或荧光色;同一图表的序列由共享分配器按协调顺序分配。
- 单序列柱图、横向排名图首项、风险重点和主要趋势使用 `--rc-chart-primary`(`#DA241A`);同一指标状态使用品牌红深浅,比较序列使用灰紫/灰蓝,不使用 UI `success` 或绿色 Token。
- KPI 图标表面已改为同一低饱和品牌/中性体系;布局、数据、接口、统计口径、交互回调和业务状态未改变。
- HeroUI v3 真实主色入口为 `--accent: #DA241A`;同时提供 `--primary`、`--primary-50..950` 与 `--color-primary-*` 映射。`--accent-hover` 固定为 `#C91F17`,`--focus` 固定为 `#DA241A`。Success 语义改为低饱和蓝灰,避免绿色出现在后台界面。
| 范围 | 当前结果 | 迁移要求 | | 范围 | 当前结果 | 迁移要求 |
| --- | --- | --- | | --- | --- | --- |
| Login | HeroUI;结构、尺寸、字段节奏与审计项目管理一致;可访问性告警已消除 | 固定浅色 Shell;本次复核受服务端验证码阻塞 | | Login | HeroUI;结构、尺寸、字段节奏与审计项目管理一致;可访问性告警已消除 | 固定浅色 Shell;登录页品牌色已通过浏览器复核 |
| Shell | HeroUI Pro Sidebar/Navbar,offcanvas、主题、用户退出与应用切换保留既有业务回调 | 代码与共享基准一致;登录后浏览器状态待真实凭证 | | Shell | HeroUI Pro Sidebar/Navbar,offcanvas、主题、用户退出与应用切换保留既有业务回调 | 代码与共享基准一致;登录后浏览器状态待真实凭证 |
| 应用切换 | HeroUI Modal compound API;46rem/42rem/28px 几何;读取 `/info -> site.group` 动态分组、真实计数与未分组回退;两列 Card、Surface + Avatar identity、唯一当前态 | 与审计项目管理的两列/移动端单列结构一致 | | 应用切换 | HeroUI Modal compound API;46rem/42rem/28px 几何;读取 `/info -> site.group` 动态分组、真实计数与未分组回退;两列 Card、Surface + Avatar identity、唯一当前态 | 与审计项目管理的两列/移动端单列结构一致 |
| 驾驶舱 | HeroUI Pro KPI;统一 `XinyuanEChart`/ECharts 渲染器 | 环形图使用共享扇区间隙、10px 扇区圆角和外部引导线;两类柱图使用 8px 末端圆角;外层分析 Card 为 24px。真实图表 Tooltip 待登录验收 | | 驾驶舱 | HeroUI Pro KPI;统一 `XinyuanEChart`/ECharts 渲染器 | 环形图使用共享扇区间隙、10px 扇区圆角和外部引导线;两类柱图使用 8px 末端圆角;外层分析 Card 为 24px;真实 Tooltip 已打开验证 |
| 案例管理 | HeroUI Surface/Tabs/TextField/Autocomplete、Pro DataGrid/ActionBar/EmptyState | 默认 20、20/50/100 分页口径已保护;交互待登录验收 | | 案例管理 | HeroUI Surface/Tabs/TextField/Autocomplete、Pro DataGrid/ActionBar/EmptyState | 默认 20、20/50/100 分页口径已保护;交互待登录验收 |
| 弹窗 | HeroUI Modal/Alert/DropZone,Container 使用官方 `lg`、Dialog 映射鑫元 4xl/1088px,详情 Dialog 使用 `bg-background` 并取消查看态灰色嵌套 Surface | 真实打开态已确认 Dialog 与页面画布同色 | | 弹窗 | HeroUI Modal/Alert/DropZone,Container 使用官方 `lg`、Dialog 映射鑫元 4xl/1088px,详情 Dialog 使用 `bg-background` 并取消查看态灰色嵌套 Surface | 真实打开态已确认 Dialog 与页面画布同色 |
| 旧组件 | Catalyst、Headless、旧 Select/Date/Table 兼容层已移除 | 活动源码无旧 UI 导入;所有业务图表统一使用 `XinyuanEChart` + ECharts,未保留 HeroUI Pro Recharts 图表调用 | | 旧组件 | Catalyst、Headless、旧 Select/Date/Table 兼容层已移除 | 活动源码无旧 UI 导入;所有业务图表统一使用 `XinyuanEChart` + ECharts,未保留 HeroUI Pro Recharts 图表调用 |
| 样式 | `theme.css`、`tokens.css` 已与审计项目管理逐字节一致;`shell.css` 保留当前项目所需的 Sidebar 状态、固定列和 Autocomplete 交互扩展 | `portable.css` 与页面私有视觉 CSS 已移除 | | 样式 | `theme.css`、`tokens.css` 沿用审计项目管理的结构与 Token 层级,并按本项目品牌规范固定 `#DA241A`;`shell.css` 保留当前项目所需的 Sidebar 状态、固定列和 Autocomplete 交互扩展 | `portable.css` 与页面私有视觉 CSS 已移除 |
## 四项功能审计 ## 四项功能审计
...@@ -62,3 +70,10 @@ View 配置审计确认本次没有实现权限功能:活动代码不读取 `v ...@@ -62,3 +70,10 @@ View 配置审计确认本次没有实现权限功能:活动代码不读取 `v
- 审计项目管理基准的应用切换在 1440x900 实测为 736px Dialog、两列 332px 卡片;目标同一会话得到相同 46rem 两列结构。目标移动端 768x1024/390x844 的 Sidebar 是 500px/312px HeroUI offcanvas Dialog,均无 viewport 级横向溢出。 - 审计项目管理基准的应用切换在 1440x900 实测为 736px Dialog、两列 332px 卡片;目标同一会话得到相同 46rem 两列结构。目标移动端 768x1024/390x844 的 Sidebar 是 500px/312px HeroUI offcanvas Dialog,均无 viewport 级横向溢出。
- 页容量选择器在真实列表中从 20 条/页切换到 50 条/页,页面回到第一页并保持原查询数据;1280x720、768x1024 视口均满足 `body.scrollWidth === innerWidth`。本轮控制台检查为 0 error、0 warning。 - 页容量选择器在真实列表中从 20 条/页切换到 50 条/页,页面回到第一页并保持原查询数据;1280x720、768x1024 视口均满足 `body.scrollWidth === innerWidth`。本轮控制台检查为 0 error、0 warning。
- 本次以审计项目管理为唯一基准同步主题后,`--background`、`--field-background` 和 `--accent` 已在目标登录页与基准登录页计算为相同值。目标服务端连续返回“验证码错误”,故没有伪造会话来重新打开受鉴权页面;登录失败产生的 3 条请求错误不计入 UI 控制台验收。 - 本次以审计项目管理为唯一基准同步主题后,`--background`、`--field-background` 和 `--accent` 已在目标登录页与基准登录页计算为相同值。目标服务端连续返回“验证码错误”,故没有伪造会话来重新打开受鉴权页面;登录失败产生的 3 条请求错误不计入 UI 控制台验收。
## 2026-09-02 品牌与图表专项复核
- 通过 `playwright-cli` 在已存在的真实登录会话中打开 `/risk_case_library/resource/stat`,未注入 token、业务数据或测试记录。
- 1440x900 下驾驶舱渲染 4 个 ECharts Canvas;抽样 Canvas 像素包含 `#DA241A`、`#B83A32`、`#C95A52`、`#D47A70`、`#A65A45`,未发现绿色系像素。
- 环形图和横向排行图 Tooltip 可正常打开,Tooltip 使用共享背景、边框和阴影 Token;390x844 下 `body.scrollWidth === innerWidth`,无视口横向溢出。
- 轻色与临时 `data-theme="dark"` 运行时检查均保持 `--accent`、`--primary`、`--focus` 和 `--xinyuan-chart-1` 为 `#DA241A`;登录页主按钮计算背景为 `rgb(218, 36, 26)`、前景为白色。
...@@ -34,7 +34,10 @@ export function getXinyuanChartGeometry() { ...@@ -34,7 +34,10 @@ export function getXinyuanChartGeometry() {
return { return {
barRadius: 8, barRadius: 8,
sectorGap: readToken('--xinyuan-chart-sector-gap', '#ffffff'), sectorGap: resolveXinyuanChartColor(
readToken('--xinyuan-chart-sector-gap', '#ffffff'),
'#ffffff',
),
sectorRadius: Number.parseFloat( sectorRadius: Number.parseFloat(
readToken('--xinyuan-chart-sector-radius', '10'), readToken('--xinyuan-chart-sector-radius', '10'),
) || 10, ) || 10,
...@@ -51,9 +54,29 @@ function readRootToken(tokenName, fallback = '') { ...@@ -51,9 +54,29 @@ function readRootToken(tokenName, fallback = '') {
.trim() || fallback; .trim() || fallback;
} }
function resolveCssColor(value, fallback = '') {
if (!value || typeof document === 'undefined') {
return value || fallback;
}
const probe = document.createElement('span');
probe.style.color = value;
if (!probe.style.color) {
return fallback;
}
probe.style.position = 'absolute';
probe.style.pointerEvents = 'none';
document.body?.appendChild(probe);
const resolved = window.getComputedStyle(probe).color || fallback;
probe.remove();
return resolved;
}
export function resolveXinyuanChartColor(color, fallback = '') { export function resolveXinyuanChartColor(color, fallback = '') {
const match = String(color || '').trim().match(/^var\((--[^)]+)\)$/); const match = String(color || '').trim().match(/^var\((--[^)]+)\)$/);
return match ? readRootToken(match[1], fallback) : String(color || fallback); return match
? resolveCssColor(`var(${match[1]})`, fallback)
: resolveCssColor(String(color || fallback), fallback);
} }
export function createResolvedXinyuanChartColorMap(keys = [], options = {}) { export function createResolvedXinyuanChartColorMap(keys = [], options = {}) {
...@@ -71,16 +94,16 @@ export function getXinyuanEChartsTheme() { ...@@ -71,16 +94,16 @@ export function getXinyuanEChartsTheme() {
: window.getComputedStyle(document.body).fontFamily; : window.getComputedStyle(document.body).fontFamily;
return { return {
axis: readRootToken('--xinyuan-chart-axis', '#737373'), axis: resolveXinyuanChartColor(readRootToken('--xinyuan-chart-axis', '#737373')),
fontFamily: documentFont || 'Inter, sans-serif', fontFamily: documentFont || 'Inter, sans-serif',
foreground: readRootToken('--xinyuan-chart-tooltip-foreground', '#222222'), foreground: resolveXinyuanChartColor(readRootToken('--xinyuan-chart-tooltip-foreground', '#222222')),
grid: readRootToken('--xinyuan-chart-grid', '#e8e8e8'), grid: resolveXinyuanChartColor(readRootToken('--xinyuan-chart-grid', '#e8e8e8')),
label: readRootToken('--xinyuan-chart-label', '#737373'), label: resolveXinyuanChartColor(readRootToken('--xinyuan-chart-label', '#737373')),
palette: Array.from({ length: 16 }, (_, index) => ( palette: Array.from({ length: 16 }, (_, index) => (
readRootToken(`--xinyuan-chart-${index + 1}`, '#667085') resolveXinyuanChartColor(readRootToken(`--xinyuan-chart-${index + 1}`, '#667085'))
)), )),
tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'), tooltipBackground: resolveXinyuanChartColor(readRootToken('--xinyuan-chart-tooltip-background', '#ffffff')),
tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'), tooltipBorder: resolveXinyuanChartColor(readRootToken('--xinyuan-chart-tooltip-border', '#dedede')),
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%)',
...@@ -124,9 +147,10 @@ export function createXinyuanChartColorMap(keys = [], options = {}) { ...@@ -124,9 +147,10 @@ export function createXinyuanChartColorMap(keys = [], options = {}) {
const usedIndexes = new Set(); const usedIndexes = new Set();
stableKeys.forEach((key, keyIndex) => { stableKeys.forEach((key, keyIndex) => {
const preferredIndex = order.length > 0 // Use the coordinated palette in order instead of a hue-random hash.
? keyIndex % XINYUAN_CHART_PALETTE.length // Explicit order keeps ranked/category charts visually intentional;
: hashChartKey(key) % XINYUAN_CHART_PALETTE.length; // without it, lexical ordering keeps the same key set deterministic.
const preferredIndex = keyIndex % XINYUAN_CHART_PALETTE.length;
let paletteIndex = preferredIndex; let paletteIndex = preferredIndex;
if (keyIndex < XINYUAN_CHART_PALETTE.length) { if (keyIndex < XINYUAN_CHART_PALETTE.length) {
......
...@@ -11,8 +11,38 @@ ...@@ -11,8 +11,38 @@
[data-theme="light"], [data-theme="light"],
[data-theme="default"] { [data-theme="default"] {
/* Theme Colors (Light Mode) */ /* Theme Colors (Light Mode) */
--accent: oklch(57.14% 0.2156 29.20); /* Fixed Xinyuan brand scale. HeroUI v3 consumes --accent; the primary aliases
--accent-foreground: oklch(99.11% 0 0); * keep the brand scale available to utility/theme integrations without allowing
* an automatic blue or purple primary to take over. */
--primary-50: #FEF2F1;
--primary-100: #FDE3E1;
--primary-200: #FBC8C5;
--primary-300: #F49A94;
--primary-400: #EA645B;
--primary-500: #DA241A;
--primary-600: #C91F17;
--primary-700: #B51B14;
--primary-800: #8F1711;
--primary-900: #67100D;
--primary-950: #3D0907;
--primary: var(--primary-500);
--primary-foreground: #FFFFFF;
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-50: var(--primary-50);
--color-primary-100: var(--primary-100);
--color-primary-200: var(--primary-200);
--color-primary-300: var(--primary-300);
--color-primary-400: var(--primary-400);
--color-primary-500: var(--primary-500);
--color-primary-600: var(--primary-600);
--color-primary-700: var(--primary-700);
--color-primary-800: var(--primary-800);
--color-primary-900: var(--primary-900);
--color-primary-950: var(--primary-950);
--accent: #DA241A;
--accent-foreground: #FFFFFF;
--accent-hover: #C91F17;
--background: oklch(97.02% 0.0000 29.20); --background: oklch(97.02% 0.0000 29.20);
--border: oklch(90.00% 0.0000 29.20); --border: oklch(90.00% 0.0000 29.20);
--danger: oklch(45.32% 0.1661 27.03); --danger: oklch(45.32% 0.1661 27.03);
...@@ -23,7 +53,7 @@ ...@@ -23,7 +53,7 @@
--field-border: transparent; --field-border: transparent;
--field-foreground: oklch(21.03% 0.0000 29.20); --field-foreground: oklch(21.03% 0.0000 29.20);
--field-placeholder: oklch(55.17% 0.0000 29.20); --field-placeholder: oklch(55.17% 0.0000 29.20);
--focus: oklch(57.14% 0.2156 29.20); --focus: #DA241A;
--foreground: oklch(21.03% 0.0000 29.20); --foreground: oklch(21.03% 0.0000 29.20);
--muted: oklch(55.17% 0.0000 29.20); --muted: oklch(55.17% 0.0000 29.20);
--overlay: oklch(100.00% 0.0000 29.20); --overlay: oklch(100.00% 0.0000 29.20);
...@@ -32,8 +62,8 @@ ...@@ -32,8 +62,8 @@
--segment: oklch(100.00% 0.0000 29.20); --segment: oklch(100.00% 0.0000 29.20);
--segment-foreground: oklch(21.03% 0.0000 29.20); --segment-foreground: oklch(21.03% 0.0000 29.20);
--separator: oklch(92.00% 0.0000 29.20); --separator: oklch(92.00% 0.0000 29.20);
--success: oklch(73.29% 0.1935 123.85); --success: #61758F;
--success-foreground: oklch(21.03% 0.0059 123.85); --success-foreground: #FFFFFF;
--surface: oklch(100.00% 0.0000 29.20); --surface: oklch(100.00% 0.0000 29.20);
--surface-foreground: oklch(21.03% 0.0000 29.20); --surface-foreground: oklch(21.03% 0.0000 29.20);
--surface-secondary: oklch(95.24% 0.0000 29.20); --surface-secondary: oklch(95.24% 0.0000 29.20);
...@@ -63,31 +93,30 @@ ...@@ -63,31 +93,30 @@
--xinyuan-sidebar-item-hover: oklch(22% 0.026 255); --xinyuan-sidebar-item-hover: oklch(22% 0.026 255);
--xinyuan-sidebar-item-selected: oklch(28% 0.016 255); --xinyuan-sidebar-item-selected: oklch(28% 0.016 255);
--xinyuan-sidebar-item-selected-foreground: oklch(99% 0.002 255); --xinyuan-sidebar-item-selected-foreground: oklch(99% 0.002 255);
--xinyuan-sidebar-focus: oklch(78% 0.105 29.20); --xinyuan-sidebar-focus: #DA241A;
/* /*
* ECharts is the required chart renderer for Xinyuan applications. * ECharts is the required chart renderer for Xinyuan applications.
* 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.
*/ */
/* First pass: red -> pink -> orange -> yellow -> blue -> purple -> cyan -> neutral. */ /* Low-conflict financial visualization palette: brand red, warm neutrals, cool slate. */
--xinyuan-chart-1: #991b1b; --xinyuan-chart-1: #DA241A;
--xinyuan-chart-2: #db2777; --xinyuan-chart-2: #B83A32;
--xinyuan-chart-3: #ea580c; --xinyuan-chart-3: #C95A52;
--xinyuan-chart-4: #eab308; --xinyuan-chart-4: #D47A70;
--xinyuan-chart-5: #1d4ed8; --xinyuan-chart-5: #A65A45;
--xinyuan-chart-6: #7e22ce; --xinyuan-chart-6: #B87958;
--xinyuan-chart-7: #0891b2; --xinyuan-chart-7: #C99772;
--xinyuan-chart-8: #475569; --xinyuan-chart-8: #8A6F63;
/* Second pass: stronger/lighter companions for charts with more series. */ --xinyuan-chart-9: #9A8174;
--xinyuan-chart-9: #dc2626; --xinyuan-chart-10: #6F6674;
--xinyuan-chart-10: #ec4899; --xinyuan-chart-11: #807783;
--xinyuan-chart-11: #f97316; --xinyuan-chart-12: #938B96;
--xinyuan-chart-12: #facc15; --xinyuan-chart-13: #4F6380;
--xinyuan-chart-13: #60a5fa; --xinyuan-chart-14: #61758F;
--xinyuan-chart-14: #a855f7; --xinyuan-chart-15: #74879A;
--xinyuan-chart-15: #06b6d4; --xinyuan-chart-16: #8798A8;
--xinyuan-chart-16: #1e293b;
--xinyuan-chart-sector-gap: var(--surface); --xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-radius: 10px; --xinyuan-chart-sector-radius: 10px;
--xinyuan-chart-grid: #e8e8e8; --xinyuan-chart-grid: #e8e8e8;
...@@ -105,8 +134,36 @@ ...@@ -105,8 +134,36 @@
[data-theme="dark"] { [data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
/* Theme Colors (Dark Mode) */ /* Theme Colors (Dark Mode) */
--accent: oklch(57.14% 0.2156 29.20); /* Keep brand identity stable across the fixed light canvas and dark mode. */
--accent-foreground: oklch(99.11% 0 0); --primary-50: #FEF2F1;
--primary-100: #FDE3E1;
--primary-200: #FBC8C5;
--primary-300: #F49A94;
--primary-400: #EA645B;
--primary-500: #DA241A;
--primary-600: #C91F17;
--primary-700: #B51B14;
--primary-800: #8F1711;
--primary-900: #67100D;
--primary-950: #3D0907;
--primary: var(--primary-500);
--primary-foreground: #FFFFFF;
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-50: var(--primary-50);
--color-primary-100: var(--primary-100);
--color-primary-200: var(--primary-200);
--color-primary-300: var(--primary-300);
--color-primary-400: var(--primary-400);
--color-primary-500: var(--primary-500);
--color-primary-600: var(--primary-600);
--color-primary-700: var(--primary-700);
--color-primary-800: var(--primary-800);
--color-primary-900: var(--primary-900);
--color-primary-950: var(--primary-950);
--accent: #DA241A;
--accent-foreground: #FFFFFF;
--accent-hover: #C91F17;
--background: oklch(12.00% 0.0000 29.20); --background: oklch(12.00% 0.0000 29.20);
--border: oklch(28.00% 0.0000 29.20); --border: oklch(28.00% 0.0000 29.20);
--danger: oklch(62% 0.16 27.03); --danger: oklch(62% 0.16 27.03);
...@@ -117,7 +174,7 @@ ...@@ -117,7 +174,7 @@
--field-border: transparent; --field-border: transparent;
--field-foreground: oklch(99.11% 0.0000 29.20); --field-foreground: oklch(99.11% 0.0000 29.20);
--field-placeholder: oklch(70.50% 0.0000 29.20); --field-placeholder: oklch(70.50% 0.0000 29.20);
--focus: oklch(57.14% 0.2156 29.20); --focus: #DA241A;
--foreground: oklch(99.11% 0.0000 29.20); --foreground: oklch(99.11% 0.0000 29.20);
--muted: oklch(70.50% 0.0000 29.20); --muted: oklch(70.50% 0.0000 29.20);
--overlay: oklch(21.03% 0.0000 29.20); --overlay: oklch(21.03% 0.0000 29.20);
...@@ -126,8 +183,8 @@ ...@@ -126,8 +183,8 @@
--segment: oklch(39.64% 0.0000 29.20); --segment: oklch(39.64% 0.0000 29.20);
--segment-foreground: oklch(99.11% 0.0000 29.20); --segment-foreground: oklch(99.11% 0.0000 29.20);
--separator: oklch(25.00% 0.0000 29.20); --separator: oklch(25.00% 0.0000 29.20);
--success: oklch(73.29% 0.1935 123.85); --success: #74879A;
--success-foreground: oklch(21.03% 0.0059 123.85); --success-foreground: #FFFFFF;
--surface: oklch(21.03% 0.0000 29.20); --surface: oklch(21.03% 0.0000 29.20);
--surface-foreground: oklch(99.11% 0.0000 29.20); --surface-foreground: oklch(99.11% 0.0000 29.20);
--surface-secondary: oklch(25.70% 0.0000 29.20); --surface-secondary: oklch(25.70% 0.0000 29.20);
......
...@@ -92,14 +92,14 @@ ...@@ -92,14 +92,14 @@
0 4px 12px rgb(15 23 42 / 2%); 0 4px 12px rgb(15 23 42 / 2%);
/* KPI icon surfaces. Card bodies remain neutral; only the icon well carries tone. */ /* KPI icon surfaces. Card bodies remain neutral; only the icon well carries tone. */
--rc-metric-tone-info: oklch(55% 0.14 255); --rc-metric-tone-info: #6F6674;
--rc-metric-tone-success: oklch(54% 0.13 145); --rc-metric-tone-success: #B83A32;
--rc-metric-tone-teal: oklch(55% 0.1 205); --rc-metric-tone-teal: #61758F;
--rc-metric-tone-danger: oklch(52% 0.17 25); --rc-metric-tone-danger: #DA241A;
--rc-metric-tone-warning: oklch(62% 0.12 75); --rc-metric-tone-warning: #B87958;
--rc-metric-tone-violet: oklch(56% 0.12 300); --rc-metric-tone-violet: #807783;
--rc-metric-tone-indigo: oklch(53% 0.13 275); --rc-metric-tone-indigo: #4F6380;
--rc-metric-tone-coral: oklch(60% 0.12 48); --rc-metric-tone-coral: #C95A52;
--rc-metric-tone-neutral: var(--muted); --rc-metric-tone-neutral: var(--muted);
--rc-metric-tone-info-soft: color-mix( --rc-metric-tone-info-soft: color-mix(
in oklab, in oklab,
...@@ -167,9 +167,9 @@ ...@@ -167,9 +167,9 @@
--rc-chart-primary: var(--rc-chart-1); --rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: var(--rc-chart-1); --rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-3); --rc-chart-warning: var(--rc-chart-3);
--rc-chart-positive: var(--rc-chart-7); --rc-chart-positive: var(--rc-chart-4);
--rc-chart-comparison: var(--rc-chart-5); --rc-chart-comparison: var(--rc-chart-13);
--rc-chart-neutral: var(--rc-chart-8); --rc-chart-neutral: var(--rc-chart-10);
--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);
} }
:root { :root {
--pm-brand: var(--rc-brand-primary, #9e1b1b); --pm-brand: var(--rc-brand-primary, #DA241A);
--pm-brand-strong: var(--rc-brand-dark, #7e1515); --pm-brand-strong: var(--rc-brand-dark, #7e1515);
--pm-surface: var(--rc-bg-surface, #ffffff); --pm-surface: var(--rc-bg-surface, #ffffff);
--pm-bg: var(--rc-bg-canvas, #f5f5f5); --pm-bg: var(--rc-bg-canvas, #f5f5f5);
......
...@@ -206,10 +206,6 @@ function HorizontalBarChart({ groups }) { ...@@ -206,10 +206,6 @@ function HorizontalBarChart({ groups }) {
function SourceComposedChart({ groups }) { function SourceComposedChart({ groups }) {
const data = normalizeChartData(groups); const data = normalizeChartData(groups);
const colorMap = createResolvedXinyuanChartColorMap(
data.map((item) => item.name),
{ order: data.map((item) => item.name) },
);
const geometry = getXinyuanChartGeometry(); const geometry = getXinyuanChartGeometry();
const option = useMemo(() => ({ const option = useMemo(() => ({
grid: { containLabel: true, left: 8, right: 16, top: 24, bottom: 12 }, grid: { containLabel: true, left: 8, right: 16, top: 24, bottom: 12 },
...@@ -220,13 +216,13 @@ function SourceComposedChart({ groups }) { ...@@ -220,13 +216,13 @@ function SourceComposedChart({ groups }) {
barMaxWidth: 28, barMaxWidth: 28,
data: data.map((item) => ({ data: data.map((item) => ({
...item, ...item,
itemStyle: { color: colorMap.get(String(item.name ?? '').trim()) }, itemStyle: { color: resolveXinyuanChartColor(XINYUAN_CHART_SEMANTIC.primary) },
})), })),
itemStyle: { borderRadius: [geometry.barRadius, geometry.barRadius, 0, 0] }, itemStyle: { borderRadius: [geometry.barRadius, geometry.barRadius, 0, 0] },
name: '数量', name: '数量',
type: 'bar', type: 'bar',
}], }],
}), [colorMap, data, geometry.barRadius]); }), [data, geometry.barRadius]);
return <XinyuanEChart ariaLabel="案例来源分布图" option={option} />; return <XinyuanEChart ariaLabel="案例来源分布图" option={option} />;
} }
......
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