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

feat: finalize Xinyuan UI alignment

parent 2078e013
This diff is collapsed.
This diff is collapsed.
......@@ -8,7 +8,8 @@
- UI 原语与 compound component 在页面或业务中立共享组合中直接导入 HeroUI/HeroUI Pro。
- 共享组合只可封装重复布局、可访问性与当前 HeroUI value/selection/event 模型,不得复刻 Catalyst props、DOM event 或旧 CSS hook。
- DataGrid 组合映射列定义并转发原生 selection/fixed-column/ActionBar/Pagination;案例列表分页容量固定为 20,不展示容量选择器。
- DataGrid 组合映射列定义并转发原生 selection/fixed-column/ActionBar/Pagination;宽表以共享 `.data-table-scroll` 为唯一滚动容器并固定首业务列和操作列,案例列表分页默认 20 条并提供 20/50/100 条数选择。
- `DataTablePagination` 的页容量选择只将下一个数值传给页面既有 `onPageSizeChange`;列表页面负责将页码置零并使用原 `loadCaseList` 请求,组件不持有业务查询状态。
- 文件上传采用 HeroUI Pro DropZone 的 `File[]` 协议;页面边界继续只取既有单文件值。
- 应用切换直接组合 HeroUI Modal/Card/Surface/Avatar 与 HeroUI Pro EmptyState;分组只读取现有 `SiteContext` 的 `/info -> site.group`,应用仍合并 `/application/info` 与当前用户权限数据,不引入硬编码分组或应用。
......@@ -18,7 +19,7 @@
## 样式所有权
`src/design-system/theme.css` 是不可变主题权威;`tokens.css`、`shell.css`、`tailwind.css` 与 `global.less` 使用“问题整改”实际副本。项目不再加载 `portable.css`,页面没有私有 CSS 外观层。
`src/design-system/theme.css` 与 `tokens.css` 直接同步审计项目管理当前版本;`shell.css`、`tailwind.css` 与 `global.less` 使用同一共享结构规则,并保留当前项目的 Sidebar 状态、固定列和 Autocomplete 受控值扩展。项目不再加载 `portable.css`,页面没有私有 CSS 外观层。
本地浏览器验收产物目录 `.playwright-cli/` 与 `test-results/` 必须保持在 `.gitignore` 中。Tailwind CSS v4 会自动探测未忽略的项目文件;若验收日志进入扫描范围,CSS HMR 日志写入会反向触发持续编译,造成样式表和字体反复重载。
......
......@@ -10,10 +10,10 @@
## 页面迁移
- [x] Login:问题整改几何、字段节奏、验证码、filled/autofill、浅/深色
- [x] Shell:Sidebar/Navbar/退出/应用切换/主题/offcanvas(代码结构)
- [x] 应用切换:同步 `model_analysis` Modal 几何、动态分组/计数、其他/全部回退、两列 Card 与 identity well(代码结构)
- [x] 驾驶舱:Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltip(代码结构)
- [x] Login:审计项目管理几何、字段节奏、验证码和 filled/autofill
- [x] Shell:Sidebar/Navbar/退出/应用切换/offcanvas(审计项目管理结构)
- [x] 应用切换:同步审计项目管理 Modal 几何、动态分组/计数、其他/全部回退、两列 Card 与 identity well(代码结构)
- [x] 驾驶舱:Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltip;环形扇区间隙/10px 扇区圆角、柱图 8px 末端圆角与 24px 分析模块圆角(代码结构)
- [x] 案例管理:Tabs、筛选工具栏、DataGrid、Pagination、ActionBar、EmptyState(代码结构)
- [x] 弹窗:新建、查看/编辑、批量修改、导入、删除确认(代码结构)
- [x] 清除 Catalyst/Headless/ECharts/原生可见控件与页面私有视觉 CSS
......@@ -34,16 +34,17 @@
- [x] Login 768x1024 light/dark
- [x] Login 390x844 light/dark
- [x] 应用切换 1440x900、1280x720、768x1024、390x844 light/dark:尺寸、两列/单列、动态分组、全部应用回退、唯一当前态、Body 滚动、Escape 与焦点回归
- [ ] 打开 Sidebar、用户退出菜单、应用切换真实动态分组、Dropdown/ListBox/DatePicker
- [x] 打开 Sidebar、用户退出菜单、应用切换真实动态分组、分页 ListBox
- [ ] 验证 KPI 动态状态与图表 Tooltip
- [x] 目标与问题整改在 1440x900、1280x720、768x1024、390x844 完成浅/深色成对截图,页面无 viewport 级横向溢出
- [x] 历史目标验收在 1440x900、1280x720、768x1024、390x844 完成截图,页面无 viewport 级横向溢出
- [x] 真实验证 Tabs、三项折叠筛选、Checkbox、DataGrid、Pagination、ActionBar、详情查看/编辑/取消和移动端通知空态
- [x] 目标与基准最终控制台 0 error / 0 warning
- [x] 宽表外层滚动、首业务列/操作列固定、分页 20/50/100 条数选择、详情画布同色与导入默认 Surface
- [x] 目标最终控制台 0 error / 0 warning
- [ ] 验证 shortTitle true/false、单/多值附件、材料选择、本地上传、取消、失败与重试
- [ ] 验证通知未读 0/1/99/100、单条/全部已读、动态操作与站内外跳转
未勾选原因:真实 View 接口返回空数组,Subject 没有 file 字段,通知接口返回 0 条;未伪造后台配置、通知或业务数据。
- [x] Login 控制台 0 runtime/slot/provider/accessibility 错误或警告
- [ ] 本次主题同步后重新验证受鉴权页面:当前被服务端验证码拒绝,未伪造登录会话
## 最终门槛
......@@ -56,4 +57,4 @@
- [x] HeroUI/React Aria 依赖树唯一
- [x] HeroUI Pro 必需子路径通过 ESM 实际导入
应用切换目标页打开态已验收;`model_analysis` 隔离会话未登录,无法对其打开态做同屏像素测量。其余未勾选项仍需使用真实业务状态验收,不得使用伪造 token、网络数据或测试数字替代。
应用切换与数据表目标页打开态已在历史真实会话中验收;本次主题同步后的受鉴权复核仍需有效验证码,不得使用伪造 token、网络数据或测试数字替代。
# 鑫元设计系统
本项目直接采用 HeroUI/HeroUI Pro 与鑫元权威主题。“问题整改”应用是结构、尺寸、间距、组件层级与交互的唯一实现基准。
本项目直接采用 HeroUI/HeroUI Pro 与鑫元权威主题。“审计项目管理”应用是当前 Shell、列表、弹窗与应用切换的结构、尺寸、间距、组件层级与交互基准。
## 依赖与样式
- 基础控件直接从 `@heroui/react` 导入。
- Sidebar、Navbar、DataGrid、ActionBar、EmptyState、DropZone、KPI 与图表从 `@heroui-pro/react` 对应子路径直接导入。
- 样式顺序固定为 Tailwind、HeroUI、HeroUI Pro、`theme.css`、`tokens.css`、批准的共享布局层。
- `theme.css` 必须与 Skill 权威副本逐字节一致;页面不得覆盖 HeroUI 外观、slot、BEM 类或交互状态。
- 本轮 `theme.css` 与 `tokens.css` 必须与审计项目管理当前版本逐字节一致;页面不得覆盖 HeroUI 外观、slot、BEM 类或交互状态。
- 页面 `className` 只承担 flex、grid、宽度、间距、对齐和响应式布局。
## 页面规则
- Shell 使用 HeroUI Pro offcanvas Sidebar 与 Navbar;页面和 Navbar 使用 48/32/16px 对称 gutter。
- 应用切换严格使用 `model_analysis` 的 HeroUI Modal 基准:46rem 宽、42rem 最大高度、28px inset、Body 内滚动,小屏单列且 `sm` 及以上固定两列;分组读取 `/info -> site.group`,按真实配置顺序展示分组与数量,未分组应用进入“其他应用”,无分组配置时使用“全部应用”。
- 应用切换严格使用审计项目管理的 HeroUI Modal 基准:46rem 宽、42rem 最大高度、28px inset、Body 内滚动,小屏单列且 `sm` 及以上固定两列;分组读取 `/info -> site.group`,按真实配置顺序展示分组与数量,未分组应用进入“其他应用”,无分组配置时使用“全部应用”。
- 结构化筛选使用 HeroUI Surface,内部控件使用 `secondary`;关键词搜索位于列表工具栏左侧。
- 列表使用 HeroUI Pro DataGrid 原生选择、排序、固定列、ActionBar、EmptyState 与 compound Pagination。
- 详情和内容型表单使用鑫元 4xl(1088px)Modal:HeroUI `Modal.Container size="lg"` 保留官方交互与响应式结构,`Modal.Dialog` 通过共享布局类限制为 `sm:max-w-[68rem]`;不得把 HeroUI 不支持的 `4xl` 直接传给 Container。查看态只有一个 `Surface secondary` 字段组,编辑态直接放置 `secondary` 控件。
- 列表使用 HeroUI Pro DataGrid 原生选择、排序、固定列、ActionBar、EmptyState 与 Pagination;宽表由共享 `.data-table-scroll` 承担横纵向滚动,首业务列及操作列固定,分页保留真实 20/50/100 条数选择。
- 页容量选择使用现有 HeroUI Autocomplete 的受控 value 合同;切换容量只重置列表页码并复用既有查询回调,不改变筛选、排序或服务请求协议。
- 详情和内容型表单使用鑫元 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;图表必须使用 Pro PieChart、BarChart、ComposedChart 与 ChartTooltipContent。
- 图表业务序列只使用 `--rc-chart-*` Token,不使用 UI 状态色或页面硬编码颜色。
- 环形图通过共享 `--xinyuan-chart-sector-gap`、`--xinyuan-chart-sector-radius` 呈现扇区间隙与 10px 扇区圆角;柱图通过共享图表几何统一使用 8px 末端圆角。分析卡片沿用 `.rc-dashboard-card` 的 24px 外圆角。
## 跨应用能力
......
# 鑫元 UI 审计
审计日期:2026-08-27。目标仓库为 Umi 4 / React 19,部署基路径为 `/risk_case_library/`。整体实现基准为“问题整改”,应用切换专项基准为 `model_analysis` 当前 HeroUI 实现。
审计日期:2026-09-01。目标仓库为 Umi 4 / React 19,部署基路径为 `/risk_case_library/`。本轮 Shell、列表、弹窗和应用切换的唯一实现基准为“审计项目管理”。
## 安装与运行基线
- 官方安装命令已在项目根目录执行成功,`@heroui-pro/react` 解析为本项目 `node_modules` 内的 `1.0.0-beta.8`,不是符号链接或跨项目 `file:` 依赖。
- 项目使用 npm,保留 `package-lock.json`;旧 `pnpm-lock.yaml` 已删除。
- 当前迁移实例运行于 `http://localhost:8000/risk_case_library/`。
- “问题整改”基准运行于 `http://localhost:8001/problem_rectification_new/`。
- 应用切换专项基准 `model_analysis` 运行于 `http://localhost:8002/model_analysis/`;该隔离浏览器会话停留在登录页,因此没有把未登录状态冒充为打开态对照。
- 当前迁移实例运行于 `http://localhost:8003/risk_case_library/`。
- “审计项目管理”源代码基准位于 `/Users/liuchenlong/Desktop/job/audit_project_manager`。
## 业务契约
......@@ -22,14 +21,14 @@
| 范围 | 当前结果 | 迁移要求 |
| --- | --- | --- |
| Login | HeroUI;结构、尺寸、字段节奏与问题整改一致;可访问性告警已消除 | 四视口浅/深色对照通过 |
| Login | HeroUI;结构、尺寸、字段节奏与审计项目管理一致;可访问性告警已消除 | 固定浅色 Shell;本次复核受服务端验证码阻塞 |
| Shell | HeroUI Pro Sidebar/Navbar,offcanvas、主题、用户退出与应用切换保留既有业务回调 | 代码与共享基准一致;登录后浏览器状态待真实凭证 |
| 应用切换 | HeroUI Modal compound API;46rem/42rem/28px 几何;读取 `/info -> site.group` 动态分组、真实计数与未分组回退;两列 Card、Surface + Avatar identity、唯一当前态 | 目标应用已在四视口浅/深色真实打开验收;动态分组和无配置回退均由真实会话覆盖,专项基准打开态因未登录仍不可复测 |
| 驾驶舱 | HeroUI Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltipContent | 构建通过;真实图表 Tooltip 待登录验收 |
| 应用切换 | HeroUI Modal compound API;46rem/42rem/28px 几何;读取 `/info -> site.group` 动态分组、真实计数与未分组回退;两列 Card、Surface + Avatar identity、唯一当前态 | 与审计项目管理的两列/移动端单列结构一致 |
| 驾驶舱 | HeroUI Pro KPI、PieChart、BarChart、ComposedChart、ChartTooltipContent | 环形图使用共享扇区间隙与 10px 圆角;两类柱图使用 8px 末端圆角;外层分析 Card 为 24px。真实图表 Tooltip 待登录验收 |
| 案例管理 | HeroUI Surface/Tabs/TextField/Autocomplete、Pro DataGrid/ActionBar/EmptyState | 默认 20、20/50/100 分页口径已保护;交互待登录验收 |
| 弹窗 | HeroUI Modal/Alert/DropZone,Container 使用官方 `lg`、Dialog 映射鑫元 4xl/1088px,查看态使用单一 secondary Surface | 新建弹窗宽度已按问题整改基准修正;其他打开态待登录验收 |
| 旧组件 | Catalyst、Headless、ECharts、旧 Select/Date/Table 兼容层已移除 | 活动源码扫描为零 |
| 样式 | `theme.css` 匹配 Skill;`tokens.css`、`shell.css`、`tailwind.css`、`global.less` 匹配问题整改 | `portable.css` 与页面私有视觉 CSS 已移除 |
| 弹窗 | HeroUI Modal/Alert/DropZone,Container 使用官方 `lg`、Dialog 映射鑫元 4xl/1088px,详情 Dialog 使用 `bg-background` 并取消查看态灰色嵌套 Surface | 真实打开态已确认 Dialog 与页面画布同色 |
| 旧组件 | Catalyst、Headless、旧 Select/Date/Table 兼容层已移除 | 活动源码无旧 UI 导入;图表沿用审计项目管理的 HeroUI Pro + Recharts primitive 组合 |
| 样式 | `theme.css`、`tokens.css` 已与审计项目管理逐字节一致;`shell.css` 保留当前项目所需的 Sidebar 状态、固定列和 Autocomplete 交互扩展 | `portable.css` 与页面私有视觉 CSS 已移除 |
## 四项功能审计
......@@ -48,13 +47,18 @@ View 配置审计确认本次没有实现权限功能:活动代码不读取 `v
- 不在源码或浏览器中加入测试记录、伪造 token 或统计数字。
- 应用切换已使用现有真实登录会话完成浅/深色四视口验收;未注入 token、应用、分组或业务数据。其他模块的既有未验收项仍按迁移清单保留,不能据此标记全量应用迁移完成。
## 2026-08-27 浏览器验收
## 2026-09-01 浏览器验收
- 使用账号 Andy 经真实验证码登录目标应用和“问题整改”,未注入 token、通知、View、附件或测试记录。
- 历史验收使用账号 Andy 经真实验证码登录目标应用,未注入 token、通知、View、附件或测试记录。
- 在 1440x900、1280x720、768x1024、390x844 分别采集两应用浅色和深色截图。各视口目标页面 `body.scrollWidth === innerWidth`,宽表滚动限制在 DataGrid 内;桌面 Sidebar/Navbar 为 240px/64px,移动端 Main 与 Navbar 均占满视口。
- 对照确认两应用在相同视口下的 Sidebar、Navbar、Trigger、Bell、主题按钮、页面内容边界、Tabs、筛选 Surface 与工具栏层级一致。
- 对照确认目标与当时的基准在相同视口下的 Sidebar、Navbar、Trigger、Bell、页面内容边界、Tabs、筛选 Surface 与工具栏层级一致。
- 目标真实验证:筛选默认三项与 Disclosure、卡片/表格 Tabs、DataGrid 固定操作列、Checkbox、ActionBar、固定 20 条 Pagination、详情 4xl Modal、查看态单一 secondary Surface、编辑 dirty、取消恢复且无 PUT、移动端通知空态、移动端 Sidebar、应用切换分组/计数/当前态。
- 目标与基准最终控制台均为 0 error、0 warning(忽略开发模式 React DevTools info)。
- 当前真实接口 `GET /subject/risk_case_library/view` 返回空数组,因此没有 shortTitle true/false 配置可触发;代码路径与配置失败完整字段回退已审计,但未把 Mock 当作浏览器通过。
- 当前 Subject 的 11 个字段均非 `file`,无法在真实页面触发单值/多值、材料选择、本地上传、取消、失败与重试。
- 当前通知列表和未读数均为 0,可验证空态与 Badge 隐藏;1/99/100、单条/全部已读、动态 actions 和站内外目标没有真实通知数据可操作。
- 宽表在 1440x900 下通过真实 `scrollLeft=700` 验证:外层 `.data-table-scroll` 为唯一横向滚动容器(1096px viewport / 2056px content),首业务列和右侧操作列保持固定且固定单元格背景不透明。
- 详情 Modal 与页面主画布曾计算为同一背景;Excel 导入初始上传区为单一 `Surface default`,未出现灰色嵌套预览 Surface。
- 审计项目管理基准的应用切换在 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。
- 本次以审计项目管理为唯一基准同步主题后,`--background`、`--field-background` 和 `--accent` 已在目标登录页与基准登录页计算为相同值。目标服务端连续返回“验证码错误”,故没有伪造会话来重新打开受鉴权页面;登录失败产生的 3 条请求错误不计入 UI 控制台验收。
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -30,6 +30,7 @@ const toSelectionKey = (value) => String(value ?? '');
export default function XinyuanAutocomplete({
'aria-label': ariaLabel,
allowsEmptyCollection = false,
className,
emptyText = '暂无可选项',
errorText = '',
......@@ -37,23 +38,28 @@ export default function XinyuanAutocomplete({
fullWidth = true,
inputValue,
isDisabled = false,
isClearable = false,
isLoading = false,
isOpen,
isRequired = false,
name,
onInputChange,
onClear,
onOpenChange,
onSelectionChange,
options = [],
placeholder = '请选择',
placement = 'bottom start',
popoverHeader,
renderOption,
searchPlaceholder = '输入关键词搜索',
showSearch = true,
selectedKey,
selectedKeys,
selectionMode = 'single',
shouldFilter = true,
valueText,
variant = 'secondary',
variant,
}) {
const selectedKeySet = getSelectedKeySet(selectionMode, selectedKey, selectedKeys);
const selectedKeyStringSet = new Set(
......@@ -87,12 +93,14 @@ export default function XinyuanAutocomplete({
<Autocomplete
{...rootSelectionProps}
aria-label={ariaLabel || placeholder}
allowsEmptyCollection={allowsEmptyCollection}
className={className}
fullWidth={fullWidth}
isDisabled={isDisabled}
isOpen={isOpen}
isRequired={isRequired}
name={name}
onClear={onClear}
selectionMode={selectionMode}
variant={variant}
onOpenChange={onOpenChange}
......@@ -104,23 +112,29 @@ export default function XinyuanAutocomplete({
{resolvedValue}
</span>
</Autocomplete.Value>
{isClearable ? (
<Autocomplete.ClearButton aria-label={`清除${ariaLabel || placeholder}`} />
) : null}
<Autocomplete.Indicator>
<ChevronDown size={16} style={FIELD_SUFFIX_ICON_STYLE} />
</Autocomplete.Indicator>
</Autocomplete.Trigger>
<Autocomplete.Popover placement="bottom start">
<Autocomplete.Popover placement={placement}>
<Autocomplete.Filter
filter={shouldFilter ? filter || defaultFilter : () => true}
inputValue={inputValue}
onInputChange={onInputChange}
>
<SearchField aria-label={searchPlaceholder} fullWidth variant="secondary">
{popoverHeader}
{showSearch ? (
<SearchField aria-label={searchPlaceholder} fullWidth variant={variant}>
<SearchField.Group>
<SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon>
<SearchField.Input autoFocus placeholder={searchPlaceholder} />
<SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton>
</SearchField.Group>
</SearchField>
) : null}
<ListBox
aria-label={ariaLabel || placeholder}
items={visibleOptions}
......
......@@ -18,6 +18,21 @@ export const XINYUAN_CHART_SEMANTIC = Object.freeze({
neutral: 'var(--rc-chart-neutral)',
});
export function getXinyuanChartGeometry() {
const rootStyles = typeof document === 'undefined'
? null
: window.getComputedStyle(document.documentElement);
const readToken = (token, fallback) => rootStyles?.getPropertyValue(token).trim() || fallback;
return {
barRadius: 8,
sectorGap: readToken('--xinyuan-chart-sector-gap', '#ffffff'),
sectorRadius: Number.parseFloat(
readToken('--xinyuan-chart-sector-radius', '10'),
) || 10,
};
}
function normalizeChartKey(key) {
return String(key ?? '').trim();
}
......
This diff is collapsed.
......@@ -11,38 +11,37 @@
[data-theme="light"],
[data-theme="default"] {
/* Theme Colors (Light Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent: oklch(57.14% 0.2156 29.20);
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(97.02% 0.0045 27.03);
--border: oklch(90.00% 0.0045 27.03);
--background: oklch(97.02% 0.0000 29.20);
--border: oklch(90.00% 0.0000 29.20);
--danger: oklch(45.32% 0.1661 27.03);
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--default: oklch(94.00% 0.0000 29.20);
--default-foreground: oklch(21.03% 0.0059 29.20);
--field-background: oklch(100.00% 0.0000 29.20);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(55.17% 0.0090 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(21.03% 0.0045 27.03);
--muted: oklch(55.17% 0.0090 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(87.10% 0.0045 27.03);
--segment: oklch(100.00% 0.0045 27.03);
--segment-foreground: oklch(21.03% 0.0045 27.03);
--separator: oklch(92.00% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(100.00% 0.0022 27.03);
--surface-foreground: oklch(21.03% 0.0045 27.03);
--surface-secondary: oklch(95.24% 0.0036 27.03);
--surface-secondary-foreground: oklch(21.03% 0.0045 27.03);
--surface-tertiary: oklch(93.73% 0.0036 27.03);
--surface-tertiary-foreground: oklch(21.03% 0.0045 27.03);
--warning: oklch(78.19% 0.1599 45.11);
--warning-foreground: oklch(21.03% 0.0059 45.11);
--field-foreground: oklch(21.03% 0.0000 29.20);
--field-placeholder: oklch(55.17% 0.0000 29.20);
--focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(21.03% 0.0000 29.20);
--muted: oklch(55.17% 0.0000 29.20);
--overlay: oklch(100.00% 0.0000 29.20);
--overlay-foreground: oklch(21.03% 0.0000 29.20);
--scrollbar: oklch(87.10% 0.0000 29.20);
--segment: oklch(100.00% 0.0000 29.20);
--segment-foreground: oklch(21.03% 0.0000 29.20);
--separator: oklch(92.00% 0.0000 29.20);
--success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(100.00% 0.0000 29.20);
--surface-foreground: oklch(21.03% 0.0000 29.20);
--surface-secondary: oklch(95.24% 0.0000 29.20);
--surface-secondary-foreground: oklch(21.03% 0.0000 29.20);
--surface-tertiary: oklch(93.73% 0.0000 29.20);
--surface-tertiary-foreground: oklch(21.03% 0.0000 29.20);
--warning: oklch(78.19% 0.1585 45.37);
--warning-foreground: oklch(21.03% 0.0059 45.37);
/* Border Radius */
--radius: 0.5rem;
......@@ -51,42 +50,90 @@
/* Font Family */
/* Make sure to load Inter font in your app */
--font-sans: var(--font-inter);
/*
* Xinyuan fixed split-shell contract.
* The application canvas always uses this light theme. The Sidebar is the
* only persistent dark plane and must consume these semantic tokens.
*/
--xinyuan-sidebar-background: oklch(16% 0.020 255);
--xinyuan-sidebar-border: oklch(26% 0.022 255);
--xinyuan-sidebar-foreground: oklch(97% 0.004 255);
--xinyuan-sidebar-muted: oklch(76% 0.018 255);
--xinyuan-sidebar-item-hover: oklch(22% 0.026 255);
--xinyuan-sidebar-item-selected: oklch(28% 0.016 255);
--xinyuan-sidebar-item-selected-foreground: oklch(99% 0.002 255);
--xinyuan-sidebar-focus: oklch(78% 0.105 29.20);
/*
* ECharts is the required chart renderer for Xinyuan applications.
* Ant Design remains the icon source. Resolve these CSS variables to
* concrete colors before assigning them to an ECharts option object.
*/
/* First pass: red -> pink -> orange -> yellow -> blue -> purple -> cyan -> neutral. */
--xinyuan-chart-1: #991b1b;
--xinyuan-chart-2: #db2777;
--xinyuan-chart-3: #ea580c;
--xinyuan-chart-4: #eab308;
--xinyuan-chart-5: #1d4ed8;
--xinyuan-chart-6: #7e22ce;
--xinyuan-chart-7: #0891b2;
--xinyuan-chart-8: #475569;
/* Second pass: stronger/lighter companions for charts with more series. */
--xinyuan-chart-9: #dc2626;
--xinyuan-chart-10: #ec4899;
--xinyuan-chart-11: #f97316;
--xinyuan-chart-12: #facc15;
--xinyuan-chart-13: #60a5fa;
--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-axis: #737373;
--xinyuan-chart-label: #737373;
--xinyuan-chart-tooltip-background: #ffffff;
--xinyuan-chart-tooltip-border: #dedede;
--xinyuan-chart-tooltip-foreground: #222222;
--xinyuan-chart-tooltip-shadow: 0 16px 40px rgb(15 23 42 / 14%);
--xinyuan-chart-legend-font-size: 0.75rem;
--xinyuan-chart-legend-line-height: 1rem;
}
.dark,
[data-theme="dark"] {
color-scheme: dark;
/* Theme Colors (Dark Mode) */
--accent: oklch(45.32% 0.1661 27.03);
--accent: oklch(57.14% 0.2156 29.20);
--accent-foreground: oklch(99.11% 0 0);
--background: oklch(12.00% 0.0045 27.03);
--border: oklch(28.00% 0.0045 27.03);
--danger: oklch(62.00% 0.1600 27.03);
--background: oklch(12.00% 0.0000 29.20);
--border: oklch(28.00% 0.0000 29.20);
--danger: oklch(62% 0.16 27.03);
--danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default: var(--surface-secondary);
--default-foreground: var(--surface-secondary-foreground);
--field-background: var(--surface);
--default: oklch(27.40% 0.0000 29.20);
--default-foreground: oklch(99.11% 0 0);
--field-background: oklch(21.03% 0.0000 29.20);
--field-border: transparent;
--field-foreground: var(--surface-foreground);
--field-placeholder: oklch(70.50% 0.0090 27.03);
--focus: oklch(45.32% 0.1661 27.03);
--foreground: oklch(99.11% 0.0045 27.03);
--muted: oklch(70.50% 0.0090 27.03);
--overlay: var(--surface);
--overlay-foreground: var(--surface-foreground);
--scrollbar: oklch(70.50% 0.0045 27.03);
--segment: oklch(39.64% 0.0045 27.03);
--segment-foreground: oklch(99.11% 0.0045 27.03);
--separator: oklch(25.00% 0.0045 27.03);
--success: oklch(73.29% 0.1952 123.59);
--success-foreground: oklch(21.03% 0.0059 123.59);
--surface: oklch(21.03% 0.0090 27.03);
--surface-foreground: oklch(99.11% 0.0045 27.03);
--surface-secondary: oklch(25.70% 0.0067 27.03);
--surface-secondary-foreground: oklch(99.11% 0.0045 27.03);
--surface-tertiary: oklch(27.21% 0.0067 27.03);
--surface-tertiary-foreground: oklch(99.11% 0.0045 27.03);
--warning: oklch(82.03% 0.1400 49.12);
--warning-foreground: oklch(21.03% 0.0059 49.12);
--field-foreground: oklch(99.11% 0.0000 29.20);
--field-placeholder: oklch(70.50% 0.0000 29.20);
--focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(99.11% 0.0000 29.20);
--muted: oklch(70.50% 0.0000 29.20);
--overlay: oklch(21.03% 0.0000 29.20);
--overlay-foreground: oklch(99.11% 0.0000 29.20);
--scrollbar: oklch(70.50% 0.0000 29.20);
--segment: oklch(39.64% 0.0000 29.20);
--segment-foreground: oklch(99.11% 0.0000 29.20);
--separator: oklch(25.00% 0.0000 29.20);
--success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(21.03% 0.0000 29.20);
--surface-foreground: oklch(99.11% 0.0000 29.20);
--surface-secondary: oklch(25.70% 0.0000 29.20);
--surface-secondary-foreground: oklch(99.11% 0.0000 29.20);
--surface-tertiary: oklch(27.21% 0.0000 29.20);
--surface-tertiary-foreground: oklch(99.11% 0.0000 29.20);
--warning: oklch(82.03% 0.1388 49.38);
--warning-foreground: oklch(21.03% 0.0059 49.38);
}
......@@ -2,9 +2,7 @@
.light,
.default,
[data-theme='light'],
[data-theme='default'],
.dark,
[data-theme='dark'] {
[data-theme='default'] {
/* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */
--rc-brand-primary: var(--accent);
--rc-brand-light: color-mix(in oklab, var(--accent) 84%, white);
......@@ -27,6 +25,17 @@
var(--background)
);
--rc-text-muted: var(--muted);
--rc-text-empty: color-mix(in oklab, var(--muted) 72%, var(--background));
/* Fixed dark Sidebar aliases. The content canvas remains light. */
--rc-sidebar-background: var(--xinyuan-sidebar-background);
--rc-sidebar-border: var(--xinyuan-sidebar-border);
--rc-sidebar-foreground: var(--xinyuan-sidebar-foreground);
--rc-sidebar-muted: var(--xinyuan-sidebar-muted);
--rc-sidebar-item-hover: var(--xinyuan-sidebar-item-hover);
--rc-sidebar-item-selected: var(--xinyuan-sidebar-item-selected);
--rc-sidebar-item-selected-foreground: var(--xinyuan-sidebar-item-selected-foreground);
--rc-sidebar-focus: var(--xinyuan-sidebar-focus);
--rc-font-sans: var(--font-sans);
--rc-font-medium: 500;
......@@ -45,6 +54,7 @@
/* Page geometry only. Component appearance remains owned by HeroUI. */
--rc-shell-inline-gutter: 3rem;
--rc-sidebar-width: 15rem;
--rc-navbar-inline-padding: var(--rc-shell-inline-gutter);
--rc-page-inline-start: var(--rc-shell-inline-gutter);
--rc-page-inline-end: var(--rc-shell-inline-gutter);
......@@ -138,40 +148,28 @@
);
/* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1: oklch(45.32% 0.1661 27.03);
--rc-chart-2: oklch(56% 0.135 255);
--rc-chart-3: oklch(64% 0.11 220);
--rc-chart-4: oklch(56% 0.115 280);
--rc-chart-5: oklch(60% 0.095 200);
--rc-chart-6: oklch(70% 0.12 80);
--rc-chart-7: oklch(60% 0.1 295);
--rc-chart-8: oklch(57% 0.035 250);
--rc-chart-1: var(--xinyuan-chart-1);
--rc-chart-2: var(--xinyuan-chart-2);
--rc-chart-3: var(--xinyuan-chart-3);
--rc-chart-4: var(--xinyuan-chart-4);
--rc-chart-5: var(--xinyuan-chart-5);
--rc-chart-6: var(--xinyuan-chart-6);
--rc-chart-7: var(--xinyuan-chart-7);
--rc-chart-8: var(--xinyuan-chart-8);
--rc-chart-9: var(--xinyuan-chart-9);
--rc-chart-10: var(--xinyuan-chart-10);
--rc-chart-11: var(--xinyuan-chart-11);
--rc-chart-12: var(--xinyuan-chart-12);
--rc-chart-13: var(--xinyuan-chart-13);
--rc-chart-14: var(--xinyuan-chart-14);
--rc-chart-15: var(--xinyuan-chart-15);
--rc-chart-16: var(--xinyuan-chart-16);
--rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03);
--rc-chart-warning: var(--rc-chart-6);
--rc-chart-positive: var(--rc-chart-2);
--rc-chart-comparison: var(--rc-chart-4);
--rc-chart-risk: var(--rc-chart-1);
--rc-chart-warning: var(--rc-chart-3);
--rc-chart-positive: var(--rc-chart-7);
--rc-chart-comparison: var(--rc-chart-5);
--rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: 0.75rem;
--rc-chart-legend-line-height: 1rem;
}
.dark,
[data-theme='dark'] {
color-scheme: dark;
--rc-text-secondary: color-mix(
in oklab,
var(--foreground) 76%,
var(--background)
);
--rc-button-secondary-border: color-mix(
in oklab,
var(--foreground) 18%,
transparent
);
--rc-shadow-raised: 0 1px 2px rgb(0 0 0 / 24%),
0 8px 24px rgb(0 0 0 / 24%);
--rc-shadow-overlay: 0 16px 40px rgb(0 0 0 / 38%);
--rc-shadow-card: 0 1px 2px rgb(0 0 0 / 20%),
0 4px 12px rgb(0 0 0 / 18%);
--rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size);
--rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height);
}
......@@ -14,10 +14,9 @@ import { Avatar, Badge, Button, Dropdown, Label, Toast, Tooltip } from '@heroui/
import { useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { Heading } from 'react-aria-components/Dialog';
import { history, Outlet, useLocation } from 'umi';
import { AppGrid, Archive, BarChart, Bell, LogOut, Moon, Sun } from './AppIcons';
import { AppGrid, Archive, BarChart, Bell, LogOut } from './AppIcons';
const APP_NAME = APP_DISPLAY_NAME;
const THEME_STORAGE_KEY = 'xinyuan_color_theme';
const menuGroups = [
{
......@@ -29,18 +28,6 @@ const menuGroups = [
},
];
function getInitialTheme() {
if (typeof window === 'undefined') return 'light';
try {
return window.localStorage.getItem(THEME_STORAGE_KEY) === 'dark'
? 'dark'
: 'light';
} catch {
return 'light';
}
}
function getRouteTitle(pathname) {
if (pathname === '/notifications') return '通知消息';
for (const group of menuGroups) {
......@@ -223,13 +210,9 @@ function SidebarContents({ onOpenApplicationSwitcher, pathname, userInfo }) {
function DashboardNavbar({
notificationCount,
onOpenNotifications,
onThemeChange,
theme,
title,
}) {
const { isMobile, isOpen } = useSidebar();
const isDark = theme === 'dark';
const themeLabel = isDark ? '切换至浅色模式' : '切换至深色模式';
const unreadCount = Math.max(0, Number(notificationCount) || 0);
return (
......@@ -262,20 +245,6 @@ function DashboardNavbar({
</Badge.Anchor>
<Tooltip.Content>通知消息</Tooltip.Content>
</Tooltip>
<Tooltip delay={0}>
<Button
isIconOnly
aria-label={themeLabel}
aria-pressed={isDark}
className="app-navbar-icon-button rc-navbar-icon-button"
size="sm"
variant="ghost"
onPress={() => onThemeChange(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun size={16} /> : <Moon size={16} />}
</Button>
<Tooltip.Content>{themeLabel}</Tooltip.Content>
</Tooltip>
</div>
</Navbar.Header>
</Navbar>
......@@ -286,25 +255,10 @@ export default function ProjectManagerLayout() {
const location = useLocation();
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [theme, setTheme] = useState(getInitialTheme);
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext);
useLayoutEffect(() => {
const root = document.documentElement;
root.classList.toggle('dark', theme === 'dark');
root.classList.toggle('light', theme === 'light');
root.dataset.theme = theme;
root.style.colorScheme = theme;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// Theme still applies for this session when storage is unavailable.
}
}, [theme]);
useLayoutEffect(() => {
document.title = APP_NAME;
}, []);
......@@ -336,7 +290,7 @@ export default function ProjectManagerLayout() {
return (
<Sidebar.Provider
className={`template-dashboard-shell rc-app-shell ${theme}`}
className="template-dashboard-shell rc-app-shell light"
collapsible="offcanvas"
navigate={(href) => history.push(href)}
open={sidebarOpen}
......@@ -351,13 +305,10 @@ export default function ProjectManagerLayout() {
<DashboardNavbar
notificationCount={unreadNotificationCount}
onOpenNotifications={() => history.push('/notifications')}
theme={theme}
title={routeTitle}
onThemeChange={setTheme}
/>
<div className="app-shell-content-scroll min-h-0 flex-1 overflow-y-auto">
<div className="rc-app-page">
<h1 className="sr-only">{routeTitle}</h1>
<Outlet />
</div>
</div>
......
......@@ -139,7 +139,7 @@
9. 页面顶部提供 `Segment` 视图切换,可在“卡片视图 / 表格视图”之间切换
10. 卡片视图用于浏览案例摘要;每条案例使用独立一级业务卡片,分类/等级/标签、标题、类型、摘要和时间信息按层级展示,点击卡片后以弹框方式查看并编辑详情
11. 表格视图展示标题、分类、标签、案例类型、关键字、来源、等级、年份、更新时间等字段;宽表在 DataGrid 内部横向滚动,各字段只在所属单元格内单行截断,完整值通过 HeroUI Tooltip 查看,不允许跨列覆盖;首列标题仅用于展示,不承担点击操作,查看详情统一从右侧操作列进入
12. 表格视图按页加载案例,固定每页 20 条,并显示页码、当前页和匹配总数;分页栏不提供每页条数选择器
12. 表格视图按页加载案例,默认每页 20 条,并显示页码、当前页和匹配总数;分页栏提供 20、50、100 条/页选择器,切换后回到第一页并按原查询条件重新加载
13. 表格视图支持单选、全选当前页案例,并显示当前勾选数量
### 4.5 编辑案例
......
......@@ -19,6 +19,7 @@ import {
Button,
Checkbox,
Chip,
CloseButton,
Disclosure,
Input,
Label,
......@@ -77,7 +78,7 @@ import {
const ALL_CATEGORY_KEY = '__all__';
const CARD_VIEW_MODE = 'card';
const TABLE_VIEW_MODE = 'table';
const XINYUAN_4XL_DIALOG_CLASS = 'sm:max-w-[68rem] p-7';
const XINYUAN_4XL_DIALOG_CLASS = 'bg-background p-7 sm:max-w-[68rem]';
const FALLBACK_CASE_FIELDS = [
{ name: 'label', label: '标题', type: 'text' },
{ name: 'category', label: '分类', type: 'text' },
......@@ -273,17 +274,14 @@ function KeywordEditor({
<div className="flex flex-wrap gap-2">
{keywords.map((keyword) => (
<Chip key={keyword} color="accent" size="sm" variant="soft">
<span className="truncate">{keyword}</span>
<Chip.Label className="max-w-48 truncate">{keyword}</Chip.Label>
<Tooltip delay={0}>
<Button
isIconOnly
<CloseButton
aria-label={`删除关键字 ${keyword}`}
size="sm"
variant="tertiary"
onPress={() => removeKeyword(keyword)}
>
<XMarkIcon className="size-3" />
</Button>
<XMarkIcon size={14} />
</CloseButton>
<Tooltip.Content>删除关键字</Tooltip.Content>
</Tooltip>
</Chip>
......@@ -525,7 +523,7 @@ export default function ResourceBookingPage() {
const [activeCaseId, setActiveCaseId] = useState('');
const [selectedCaseIds, setSelectedCaseIds] = useState([]);
const [page, setPage] = useState(0);
const pageSize = DEFAULT_PAGE_SIZE;
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
......@@ -837,6 +835,15 @@ export default function ResourceBookingPage() {
loadCaseList(nextPage, pageSize);
}
function handlePageSizeChange(nextPageSize) {
if (nextPageSize === pageSize) {
return;
}
setPage(0);
setPageSize(nextPageSize);
}
function resetCreateForm() {
setCreateForm(buildEmptyCaseForm());
}
......@@ -1348,7 +1355,6 @@ export default function ResourceBookingPage() {
</div>
) : null}
<h2>案例列表</h2>
<div className="rc-page-toolbar">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<div className="w-full min-w-0 flex-none sm:w-80 lg:w-96">
......@@ -1493,10 +1499,12 @@ export default function ResourceBookingPage() {
)}
<DataTablePagination
pageSizeOptions={[20, 50, 100]}
page={page}
pageSize={pageSize}
total={total}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
isDisabled={loading}
/>
</div>
......@@ -1719,15 +1727,20 @@ export default function ResourceBookingPage() {
<Modal.Dialog className={XINYUAN_4XL_DIALOG_CLASS}>
<Modal.CloseTrigger aria-label="关闭案例详情" />
<Modal.Header>
<Modal.Heading>
<span className="inline-flex min-w-0 items-center gap-2">
<span>案例详情</span>
<div className="flex w-full min-w-0 items-start gap-3 pr-16">
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<Modal.Heading className="min-w-0">
{!hideDetailTitle && activeCase
? activeCase.title || activeCase.label || '案例详情'
: '案例详情'}
</Modal.Heading>
{activeCase ? (
<Tooltip delay={0}>
<Button
isIconOnly
aria-label="复制详情"
className="size-6"
className="size-6 shrink-0"
isDisabled={saving}
size="sm"
variant="tertiary"
......@@ -1738,20 +1751,19 @@ export default function ResourceBookingPage() {
<Tooltip.Content>复制详情</Tooltip.Content>
</Tooltip>
) : null}
</span>
</Modal.Heading>
</div>
{activeCase && !hideDetailTitle ? (
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
<span>创建于 {activeCase.createdAtLabel}</span>
<span>最近更新 {activeCase.updatedAtLabel}</span>
</div>
) : null}
</div>
</div>
</Modal.Header>
<Modal.Body>
{activeCase ? (
<div className="space-y-6">
{!hideDetailTitle ? (
<h2 className="break-words text-lg font-semibold">{activeCase.title || activeCase.label}</h2>
) : null}
<div className="flex flex-wrap gap-x-4 gap-y-2">
<span>创建于 {activeCase.createdAtLabel}</span>
<span>最近更新 {activeCase.updatedAtLabel}</span>
</div>
{visibleDetailViews.length > 1 ? (
<Tabs selectedKey={activeDetailView?.name} onSelectionChange={(key) => setActiveDetailViewName(String(key))}>
<Tabs.ListContainer className="w-fit max-w-full min-w-0">
......@@ -1767,10 +1779,7 @@ export default function ResourceBookingPage() {
) : null}
{!isDetailEditing ? (
<Surface
className="px-5 py-2 sm:px-6 sm:py-3"
variant="secondary"
>
<div className="px-1 py-2 sm:px-2 sm:py-3">
<div className="grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2">
{activeDetailColumns.map((column) => {
const value = getFieldValue(activeCase.rawRecord, column.field);
......@@ -1783,7 +1792,7 @@ export default function ResourceBookingPage() {
);
})}
</div>
</Surface>
</div>
) : (
<div className="grid min-w-0 gap-4 md:grid-cols-2">
{activeDetailColumns.map((column) => {
......@@ -2051,7 +2060,7 @@ export default function ResourceBookingPage() {
<Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} />
<Modal.Backdrop>
<Modal.Container placement="center" size="md">
<Modal.Dialog>
<Modal.Dialog className="bg-background">
<Modal.CloseTrigger aria-label="关闭批量删除确认" />
<Modal.Header>
<Modal.Heading>确认批量删除案例</Modal.Heading>
......@@ -2092,7 +2101,7 @@ export default function ResourceBookingPage() {
<Modal.Trigger aria-hidden="true" className="sr-only" tabIndex={-1} />
<Modal.Backdrop>
<Modal.Container placement="center" size="md">
<Modal.Dialog>
<Modal.Dialog className="bg-background">
<Modal.CloseTrigger aria-label="关闭删除确认" />
<Modal.Header>
<Modal.Heading>确认删除案例</Modal.Heading>
......@@ -2140,7 +2149,7 @@ export default function ResourceBookingPage() {
</Modal.Header>
<Modal.Body>
<div className="space-y-6">
<Surface className="p-4" variant="secondary">
<Surface className="p-4" variant="default">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 flex-1">
<span>{importFile ? importFile.name : '还没有选择 Excel 文件'}</span>
......
......@@ -7,6 +7,7 @@ import {
import {
colorizeXinyuanChartTooltipPayload,
createXinyuanChartColorMap,
getXinyuanChartGeometry,
XINYUAN_CHART_SEMANTIC,
} from '@/design-system/chartPalette';
import { Alert, Card, Spinner } from '@heroui/react';
......@@ -111,6 +112,7 @@ function DonutChart({ groups }) {
const data = normalizeChartData(groups);
const total = data.reduce((sum, item) => sum + item.value, 0);
const colorMap = createXinyuanChartColorMap(data.map((item) => item.name));
const geometry = getXinyuanChartGeometry();
return (
<PieChart className="rc-chart-interaction" height={320}>
......@@ -121,6 +123,9 @@ function DonutChart({ groups }) {
nameKey="name"
outerRadius="72%"
paddingAngle={2}
cornerRadius={geometry.sectorRadius}
stroke={geometry.sectorGap}
strokeWidth={3}
>
{data.map((item) => (
<PieChart.Cell
......@@ -163,6 +168,7 @@ function DonutChart({ groups }) {
function HorizontalBarChart({ groups }) {
const data = normalizeChartData(groups);
const geometry = getXinyuanChartGeometry();
return (
<BarChart
......@@ -187,7 +193,7 @@ function HorizontalBarChart({ groups }) {
fill={XINYUAN_CHART_SEMANTIC.primary}
maxBarSize={28}
name="数量"
radius={[0, 4, 4, 0]}
radius={[0, geometry.barRadius, geometry.barRadius, 0]}
/>
</BarChart>
);
......@@ -196,6 +202,7 @@ function HorizontalBarChart({ groups }) {
function SourceComposedChart({ groups }) {
const data = normalizeChartData(groups);
const colorMap = createXinyuanChartColorMap(data.map((item) => item.name));
const geometry = getXinyuanChartGeometry();
return (
<ComposedChart
......@@ -219,6 +226,7 @@ function SourceComposedChart({ groups }) {
fill={XINYUAN_CHART_SEMANTIC.comparison}
maxBarSize={28}
name="数量"
radius={[geometry.barRadius, geometry.barRadius, 0, 0]}
>
{data.map((item) => (
<Cell key={item.key || item.name} fill={colorMap.get(item.name)} />
......
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