Commit 8d74d2c6 authored by Andy-bubu's avatar Andy-bubu

fix: align pie chart palette with xinyuan baseline

parent 89d9efa3
......@@ -88,7 +88,7 @@ Layout route/path change + 60s timer + notification-state event
- 可见结构使用 HeroUI 原生组合:L1 `Card` 承载分析模块,L2 `Surface secondary` 承载时间轴,L3 `Surface tertiary` 承载刻度和任务行。
- 实际进度与时间进度使用 `ProgressBar` compound API;结果状态使用语义 `Chip`,空范围使用 Pro `EmptyState`。
- 共享 CSS 只控制网格、宽度、间距、对齐与横向滚动;组件颜色、圆角、描边和交互状态由 HeroUI variant 与鑫元共享 Tokens 负责。
- 图表颜色由 `tokens.css` 的共享 `--rc-chart-*` 序列和 `XinyuanEChart` 适配器统一提供,优先使用品牌红深浅、暖色、中性和灰蓝层级;业务页面不得添加绿色、荧光色或页面私有图表色。
- 图表颜色由 `tokens.css` 的共享 `--rc-chart-*` 序列和 `XinyuanEChart` 适配器统一提供,普通 Bar/Line/Ranking 保持该色板;Pie/Donut 独立读取 `--rc-pie-chart-*`,前六个为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,并按稳定分类 key 确定性分配、冲突消解且在筛选/排序/Tab/主题切换后保持不变。业务页面不得添加绿色、青绿色、荧光色、彩虹色或页面私有饼图色。
## 个人日历架构
......
......@@ -43,6 +43,8 @@
- [x] 状态环图、结果/进度柱图和成员负载组合图保持真实业务口径并使用共享图表 Token。
- [x] Chart Tooltip、坐标轴和饼图标签由适配器统一配置。
- [x] 图表共享色系固定为品牌红深浅 -> 暖色 -> 中性 -> 灰蓝;已检查资源统计全部图表,未使用绿色系、荧光色或彩虹式相邻系列色。
- [x] 饼图/环形图同步独立 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 色板(前六色为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`);共享适配器按稳定分类 key 分配并消除冲突,页面级旧颜色不再覆盖,普通 Bar/Line/Ranking 色板保持不变。
- [x] 项目状态环图按 `已完成 / 进行中 / 受阻 / assigned / 草稿` 的稳定业务顺序依次使用专用色板前五色。
- [x] 资源统计、甘特图、个人日历和风险清单模块已移除重复副标题,范围信息保留在控件和数据区域内。
- [x] 灰色 L2 嵌套 Surface 统一使用 `rc-nested-surface` 的 8px 圆角,外层工作台模块仍保持 24px。
- [x] 整个周进度记录模块及其记录项使用 HeroUI `Surface variant="transparent"` + `rc-outline-surface`,去除灰色填充并统一使用灰色描边。
......
......@@ -53,7 +53,7 @@
- ActionBar:选中后位于当前主内容视口下方中央,右侧关闭清除选择。
- Modal:标准 24px 圆角;详情/复杂表单 4xl;Header 中功能标题与记录标题分层展示,正文与 Header 共享内容左右边界;查看态使用无框标签/值布局,不增加灰色填充 Surface,字段从 `md` 起双列、列间距 32px、行间距 8px、字段上下 12px、标签和值间距 6px,标签为 14px/500 muted、值为 14px/400 foreground;可编辑记录的查看态 Footer 只保留右对齐的“编辑”主操作,右上角关闭图标负责退出;编辑态 controls 直接位于同一 Overlay 并使用 `secondary`,已有记录的删除作为 Footer 右侧操作组中的独立危险次操作,位于“取消”之前且不抢占最右主操作,确认仍通过受控 AlertDialog;查看/编辑切换复用同一 4xl Modal 几何,避免双 Modal 进出场造成跳动。
- KPI:HeroUI Pro KPI compound,24px 外卡、16px board gap、实际 data-count 布局。
- 图表:ECharts + `echarts-for-react` 通过共享 `XinyuanEChart` 适配器渲染;应用扩展 `--rc-chart-*` 使用 `#DA241A` 品牌红及低饱和红、暖色、中性、灰蓝层级,禁止绿色系、荧光色和彩虹式相邻对撞。适配器读取扩展色板后再向 ECharts 注入具体颜色;Tooltip、坐标轴和饼图间隙由适配器统一处理。
- 图表:ECharts + `echarts-for-react` 通过共享 `XinyuanEChart` 适配器渲染;普通柱状图、折线图和排名图继续使用 `--xinyuan-chart-*` / `--rc-chart-*`,其序列为品牌红、低饱和暖色、中性和灰蓝层级。饼图/环形图独立使用 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*`,前六个固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,禁止绿色、青绿色、荧光色和彩虹色。适配器按稳定分类 key 确定性分配饼图颜色,过滤、排序、Tab 和主题切换不改变分类颜色;页面不得写入饼图颜色覆盖。Tooltip、坐标轴和饼图间隙由适配器统一处理。
- 模块头部只保留功能标题;图表、甘特图、日历和清单不显示重复性副标题或日期说明,必要的范围和解释由控件、坐标轴或弹窗上下文承载。
- 字段标题旁的持久性规则说明使用小型 HeroUI Tooltip 信息图标承载;以项目状态为例,工作流说明不再占用字段下方垂直空间。
- 个人日历:HeroUI Pro Agenda 月视图;跨日期项目使用 MonthSpanningEvent,单日项目使用 MonthEvent,导航和日期状态使用 Agenda 官方 compound API。
......
......@@ -97,6 +97,7 @@
- `bash /Users/liuchenlong/.codex/skills/xinyuan-design-system/scripts/audit_project.sh /Users/liuchenlong/Desktop/job/project_manager_xy`:退出码 0;权威主题、原生 HeroUI 架构和四份文档检查通过。脚本仍输出 6 类启发式警告,均已逐项复核:Pro 子路径警告源于 Node `require.resolve` 加载包内 CSS;Modal、DataGrid 字体层级、人员双行信息和可点击主字段使用当前 HeroUI compound/页面数据 schema,未沿用旧包装;`--rc-shadow-card` 与 Skill 原始副本逐字一致,脚本的单行正则不能匹配格式化后的多行值。作为对照,同一脚本对问题整改本身也输出 Pro 子路径、DataGrid 命名和 shadow 正则等警告。这些警告不代表活动页面遗留;已用源代码扫描、`exports`、`import.meta.resolve`、生产构建和浏览器计算结果复核。
- 旧组件扫描:Catalyst、Headless UI、compat、旧 DataTable/MetricCard/FormSelect 及 Recharts/Pro Charts 活动引用为 0;图表统一使用 ECharts 适配器。
- 图表色系扫描:资源统计的状态环图、结果/进度柱图、成员负载组合图均只从应用扩展共享 `--rc-chart-*` 序列取色,采用 `#DA241A` 品牌红及低饱和红、暖色、中性、灰蓝层级;页面图表未使用绿色系、荧光色或私有色值。主题与扩展 Token 的运行时计算值已复验一致。
- 饼图专项同步:状态环图改由 `XinyuanEChart` 统一使用 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 专用色板;前六色固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,按稳定分类 key 确定性取色并消除冲突。普通柱图、折线图和排名图仍使用原 `--rc-chart-*`,页面布局、尺寸、数据结构和交互回调未改变。
- 原生控件扫描:可见 `<select>` 与 `type=date|datetime-local|file` 为 0。
- 页面私有视觉扫描:禁用色系/背景/描边/圆角/阴影 utility 为 0;页面 CSS/LESS/SCSS 的 HeroUI slot/BEM/伪元素命中为 0。
- Typography 扫描:Navbar 20px/600、模块标题 16px/600、正文 400、标签 500 均由共享 Token/组件合同提供;侧栏应用名已从 650 修正为 600,未保留非规范字重。
......@@ -128,3 +129,11 @@
- 真实接口当前返回通知空列表和未读 0;浏览器临时路由夹具覆盖未读 1/99/100(100 显示 `99+` 且 aria 保留 100)、转义搜索 `a.*[`、未读服务端状态、两页 Pagination、单条/全部已读、动态 action、站内/站外跳转与恶意 HTML 纯文本渲染。夹具只存在于 Playwright 会话,没有写入源码或业务数据。
- 项目清单与通知列表分页默认每页 10 条,可切换 10/20/50 条/页;页容量选择沿用共享 HeroUI Autocomplete 受控值且隐藏搜索,Pagination 保持 `sm` 字号。
- 最终目标通知页控制台为 0 error、0 warning;登录时人为误读验证码产生的历史请求错误已通过全新浏览器会话隔离,不计入页面最终控制台结果。
## 2026-09-08 饼图 / 环形图配色专项同步
- 本轮仅同步 Pie/Donut 相关 Token、共享 `XinyuanEChart` 适配器和资源统计状态环图调用;普通 Bar/Line/Ranking 色板、品牌主色、页面布局、图表尺寸、接口、统计口径及业务回调均未修改。
- 新增 `--xinyuan-pie-chart-1..16` 与 `--rc-pie-chart-1..16`。前六色固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,扩展色保持灰蓝、灰紫、暖棕和暖灰,禁止绿色、青绿色、荧光色和彩虹色。
- `XinyuanEChart` 对 `type: 'pie'`(含 donut)收集完整分类 key,使用专用色板和冲突消解;页面级旧 `itemStyle.color` 不再覆盖。Tooltip、标签、引导线、间隙、圆角和点击行为保持原实现。
- 项目状态环图使用固定业务顺序 `已完成 -> 进行中 -> 受阻 -> assigned -> 草稿`,直接对应专用色板第 1 至第 5 色;不再因分类名称哈希而跳到扩展色。
- `npm run build`、`git diff --check` 均通过;HeroUI Pro 子路径导入和依赖树检查通过。Playwright 登录页 Token 检查返回 `--xinyuan-pie-chart-1=#DA241A`、`--xinyuan-pie-chart-6=#667080`、`--rc-pie-chart-1=#DA241A`,页面横向溢出为 0,控制台无 error/warning。受保护资源统计数据在未登录会话不可展开,本轮图表填充态标记为待真实数据复验。
import {
createResolvedXinyuanPieChartColorMap,
getXinyuanEChartsTheme,
} from '@/design-system/chartPalette';
import ReactECharts from 'echarts-for-react';
......@@ -41,6 +42,21 @@ function mergeAxisTheme(axis, theme) {
return Array.isArray(axis) ? axis.map(applyTheme) : applyTheme(axis);
}
function forceItemStyle(data, itemStyle) {
if (!Array.isArray(data)) return data;
return data.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
return {
...item,
itemStyle: {
...item.itemStyle,
...itemStyle,
},
};
});
}
export default function XinyuanEChart({
ariaLabel,
className = '',
......@@ -62,14 +78,43 @@ export default function XinyuanEChart({
const applyPieTheme = (series) => {
if (series?.type !== 'pie') return series;
const { pieColorOrder, ...pieSeries } = series;
const pieColorMap = createResolvedXinyuanPieChartColorMap(
Array.isArray(series.data) ? series.data.map((item) => item?.name) : [],
{ order: pieColorOrder },
);
const pieData = Array.isArray(series.data)
? series.data.map((item, index) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
const key = String(item.name ?? '').trim();
return {
...item,
itemStyle: {
...item.itemStyle,
// Dedicated pie palette wins over legacy page-level colors.
color: pieColorMap.get(key) || theme.piePalette[index % theme.piePalette.length],
},
};
})
: series.data;
const seriesItemStyle = { ...series.itemStyle };
delete seriesItemStyle.color;
return {
...series,
...pieSeries,
color: theme.piePalette,
itemStyle: {
...seriesItemStyle,
borderColor: theme.sectorGap,
borderRadius: theme.sectorRadius,
borderWidth: 2,
...series.itemStyle,
},
data: forceItemStyle(pieData, {
borderColor: theme.sectorGap,
borderRadius: theme.sectorRadius,
borderWidth: 2,
}),
padAngle: series.padAngle ?? 1,
};
};
......
......@@ -19,6 +19,12 @@ export const XINYUAN_CHART_PALETTE = Object.freeze([
'var(--rc-chart-16)',
]);
// Pie and donut slices use a dedicated palette so ordinary bar/line/ranking
// colors remain unchanged while adjacent sectors stay distinguishable.
export const XINYUAN_PIE_CHART_PALETTE = Object.freeze(
Array.from({ length: 16 }, (_, index) => `var(--rc-pie-chart-${index + 1})`),
);
export const XINYUAN_CHART_SEMANTIC = Object.freeze({
primary: 'var(--rc-chart-primary)',
risk: 'var(--rc-chart-risk)',
......@@ -32,6 +38,13 @@ function normalizeChartKey(key) {
return String(key ?? '').trim();
}
function hashChartKey(key) {
return Array.from(key).reduce(
(value, character) => ((value * 31) + character.codePointAt(0)) >>> 0,
0,
);
}
export function createXinyuanChartColorMap(keys = [], options = {}) {
const sourceKeys = Array.isArray(keys) ? keys : [];
const order = Array.isArray(options?.order) ? options.order : [];
......@@ -101,9 +114,16 @@ function readRootToken(tokenName, fallback = '') {
.trim() || fallback;
}
export function resolveXinyuanChartColor(color, fallback = '') {
function resolveChartColorValue(color, fallback, seen) {
const match = String(color || '').trim().match(/^var\((--[^)]+)\)$/);
return match ? readRootToken(match[1], fallback) : String(color || fallback);
if (!match) return String(color || fallback);
if (seen.has(match[1])) return fallback;
seen.add(match[1]);
return resolveChartColorValue(readRootToken(match[1], fallback), fallback, seen);
}
export function resolveXinyuanChartColor(color, fallback = '') {
return resolveChartColorValue(color, fallback, new Set());
}
export function createResolvedXinyuanChartColorMap(keys = [], options = {}) {
......@@ -115,6 +135,53 @@ export function createResolvedXinyuanChartColorMap(keys = [], options = {}) {
);
}
export function getXinyuanPieChartColor(index = 0) {
return XINYUAN_PIE_CHART_PALETTE[
Math.max(0, Number(index) || 0) % XINYUAN_PIE_CHART_PALETTE.length
];
}
export function createResolvedXinyuanPieChartColorMap(keys = [], options = {}) {
const preferredOrder = new Map(
(Array.isArray(options?.order) ? options.order : [])
.map(normalizeChartKey)
.filter(Boolean)
.map((key, index) => [key, index]),
);
const stableKeys = [...new Set(keys.map(normalizeChartKey).filter(Boolean))]
.sort((left, right) => {
const leftOrder = preferredOrder.get(left);
const rightOrder = preferredOrder.get(right);
if (leftOrder !== undefined || rightOrder !== undefined) {
if (leftOrder === undefined) return 1;
if (rightOrder === undefined) return -1;
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
}
return left.localeCompare(right, 'zh-CN');
});
const colorMap = new Map();
const usedIndexes = new Set();
stableKeys.forEach((key) => {
const preferredIndex = preferredOrder.has(key)
? preferredOrder.get(key) % XINYUAN_PIE_CHART_PALETTE.length
: hashChartKey(key) % XINYUAN_PIE_CHART_PALETTE.length;
let paletteIndex = preferredIndex;
for (let offset = 0; offset < XINYUAN_PIE_CHART_PALETTE.length; offset += 1) {
const candidate = (preferredIndex + offset) % XINYUAN_PIE_CHART_PALETTE.length;
if (!usedIndexes.has(candidate)) {
paletteIndex = candidate;
usedIndexes.add(candidate);
break;
}
}
colorMap.set(key, resolveXinyuanChartColor(getXinyuanPieChartColor(paletteIndex)));
});
return colorMap;
}
export function getXinyuanEChartsTheme() {
const documentFont = typeof document === 'undefined'
? 'Inter, sans-serif'
......@@ -129,6 +196,9 @@ export function getXinyuanEChartsTheme() {
palette: XINYUAN_CHART_PALETTE.map((color) => (
resolveXinyuanChartColor(color, '#667085')
)),
piePalette: XINYUAN_PIE_CHART_PALETTE.map((color) => (
resolveXinyuanChartColor(color, '#667080')
)),
tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'),
tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'),
sectorGap: readRootToken('--xinyuan-chart-sector-gap', '#ffffff'),
......
......@@ -115,6 +115,23 @@
--xinyuan-chart-14: #61758F;
--xinyuan-chart-15: #74879A;
--xinyuan-chart-16: #8798A8;
/* Pie / donut only: independent higher-hue-separation palette. */
--xinyuan-pie-chart-1: #DA241A;
--xinyuan-pie-chart-2: #3F5F86;
--xinyuan-pie-chart-3: #8A668F;
--xinyuan-pie-chart-4: #C28A3D;
--xinyuan-pie-chart-5: #8A6253;
--xinyuan-pie-chart-6: #667080;
--xinyuan-pie-chart-7: #74879A;
--xinyuan-pie-chart-8: #9A8174;
--xinyuan-pie-chart-9: #6F6674;
--xinyuan-pie-chart-10: #B87958;
--xinyuan-pie-chart-11: #61758F;
--xinyuan-pie-chart-12: #807783;
--xinyuan-pie-chart-13: #B0A4A0;
--xinyuan-pie-chart-14: #AFA5B1;
--xinyuan-pie-chart-15: #897A73;
--xinyuan-pie-chart-16: #556B88;
--xinyuan-chart-grid: #e8e8e8;
--xinyuan-chart-axis: #737373;
--xinyuan-chart-label: #737373;
......
......@@ -174,6 +174,23 @@
--rc-chart-14: #61758F;
--rc-chart-15: #74879A;
--rc-chart-16: #8798A8;
/* Pie / donut series remain independent from bar/line categorical colors. */
--rc-pie-chart-1: var(--xinyuan-pie-chart-1);
--rc-pie-chart-2: var(--xinyuan-pie-chart-2);
--rc-pie-chart-3: var(--xinyuan-pie-chart-3);
--rc-pie-chart-4: var(--xinyuan-pie-chart-4);
--rc-pie-chart-5: var(--xinyuan-pie-chart-5);
--rc-pie-chart-6: var(--xinyuan-pie-chart-6);
--rc-pie-chart-7: var(--xinyuan-pie-chart-7);
--rc-pie-chart-8: var(--xinyuan-pie-chart-8);
--rc-pie-chart-9: var(--xinyuan-pie-chart-9);
--rc-pie-chart-10: var(--xinyuan-pie-chart-10);
--rc-pie-chart-11: var(--xinyuan-pie-chart-11);
--rc-pie-chart-12: var(--xinyuan-pie-chart-12);
--rc-pie-chart-13: var(--xinyuan-pie-chart-13);
--rc-pie-chart-14: var(--xinyuan-pie-chart-14);
--rc-pie-chart-15: var(--xinyuan-pie-chart-15);
--rc-pie-chart-16: var(--xinyuan-pie-chart-16);
--rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-3);
......
......@@ -136,7 +136,7 @@
- 即将到期项目
- 超时项目清单
- 风险清单区位于成员负载明细下方,采用独立全宽区块展示,避免作为右侧纵列造成页面左右高度失衡
- 图表统一使用 ECharts,通过共享 `XinyuanEChart` 适配器处理 Pie、Bar、组合图、Tooltip 和主题色;统计口径不变。系列色按共享 Token 使用品牌红深浅、暖色、中性与灰蓝层级,不使用绿色系、荧光色或页面私有颜色
- 图表统一使用 ECharts,通过共享 `XinyuanEChart` 适配器处理 Pie、Bar、组合图、Tooltip 和主题色;统计口径不变。普通 Bar/Line/Ranking 系列继续使用 `--rc-chart-*`,Pie/Donut 独立使用 `--rc-pie-chart-*`(前六色固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`),按稳定分类 key 确定性分配且不使用绿色、青绿色、荧光色或彩虹色
- 建议补充统计维度:
- 项目状态分布
- 结果状态分布(按时间规则实时派生)
......
......@@ -31,9 +31,9 @@ const PROGRESS_BUCKETS = [
{ label: '100%', min: 100, max: 100 },
];
const STATUS_CHART_ORDER = ['已完成', '进行中', '受阻', 'assigned', '草稿'];
const RESULT_CHART_ORDER = ['已超时', '提前完成', '超时完成', '准时完成'];
const PROGRESS_CHART_ORDER = PROGRESS_BUCKETS.map((bucket) => bucket.label);
const STATUS_CHART_ORDER = ['已完成', '进行中', '受阻', 'assigned', '草稿'];
function formatUserLabel(user) {
return user?.displayName || user?.username || user?.id || '-';
......@@ -88,18 +88,16 @@ function ChartEmptyState() {
function StatusPieChart({ data, total }) {
const option = useMemo(() => {
const theme = getXinyuanEChartsTheme();
const colorMap = createResolvedXinyuanChartColorMap(data.map((item) => item.name), {
order: STATUS_CHART_ORDER,
});
return {
legend: { bottom: 0, left: 'center' },
series: [{
type: 'pie',
name: '项目状态',
pieColorOrder: STATUS_CHART_ORDER,
radius: ['52%', '72%'],
center: ['50%', '44%'],
avoidLabelOverlap: true,
data: data.map((item) => ({ ...item, itemStyle: { color: colorMap.get(item.name) } })),
data,
label: { show: true, formatter: ({ name, value }) => [
`{name|${name}}`,
`{value|${value} 个}`,
......
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