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

feat: complete xinyuan material center ui

parent 30d1d111
...@@ -21,10 +21,10 @@ upload field, confirmation dialog and application switcher. They expose current ...@@ -21,10 +21,10 @@ upload field, confirmation dialog and application switcher. They expose current
HeroUI selection/value/event contracts; page controllers adapt those values to HeroUI selection/value/event contracts; page controllers adapt those values to
existing material services. existing material services.
`Select`-shaped compatibility wrappers and page-local ECharts theme helpers are `Select`-shaped compatibility wrappers and page-local chart theme helpers are
not part of the target architecture. Enum/reference fields use the current not part of the target architecture. Enum/reference fields use the current
HeroUI Autocomplete compound model, while dashboards use HeroUI Pro KPI and HeroUI Autocomplete compound model, while dashboards use HeroUI Pro KPI and the
chart compounds with the approved chart Tokens. shared `XinyuanEChart` ECharts adapter with approved chart Tokens.
## Dependency Source ## Dependency Source
......
...@@ -12,7 +12,7 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. ...@@ -12,7 +12,7 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
| Baseline login QA | superseded | Earlier viewport evidence predates the corrected problem-rectification login parity pass | | Baseline login QA | superseded | Earlier viewport evidence predates the corrected problem-rectification login parity pass |
| Authoritative theme | done | Exact baseline `theme.css`, Xinyuan assets, Inter and app-only extension Tokens installed | | Authoritative theme | done | Exact baseline `theme.css`, Xinyuan assets, Inter and app-only extension Tokens installed |
| Login | done | Baseline structure, assets and approved 12px label / 24px field rhythm synchronized | | Login | done | Baseline structure, assets and approved 12px label / 24px field rhythm synchronized |
| Shell | done | Pro AppLayout/Sidebar/Navbar; desktop offcanvas plus an accessible Pro Sheet mobile sidebar | | Shell | done | Pro Sidebar.Provider/Sidebar/Navbar; fixed dark Sidebar, light content canvas, desktop offcanvas plus accessible mobile Sheet |
| Application switcher | done | model_analysis parity; `/info -> site.group`, counts, empty-group filtering, 其他应用/全部应用 fallbacks, two-column grid | | Application switcher | done | model_analysis parity; `/info -> site.group`, counts, empty-group filtering, 其他应用/全部应用 fallbacks, two-column grid |
| Dashboard | done | Pro KPI uses the approved 24px Card and 48px toned icon Surface; analytical modules follow the same radius contract | | Dashboard | done | Pro KPI uses the approved 24px Card and 48px toned icon Surface; analytical modules follow the same radius contract |
| Material archive filters | done | HeroUI Surface, Disclosure, InputGroup, Autocomplete and DatePicker compounds | | Material archive filters | done | HeroUI Surface, Disclosure, InputGroup, Autocomplete and DatePicker compounds |
...@@ -27,10 +27,10 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`. ...@@ -27,10 +27,10 @@ Status legend: `done`, `in progress`, `superseded`, `blocked`, `pending`.
| Real-data-only states | done | Demo material/project fallbacks removed; empty/error states reflect the API | | Real-data-only states | done | Demo material/project fallbacks removed; empty/error states reflect the API |
| Private visual CSS | done | Duplicate portable/app-token layers removed; approved shared shell/login baseline retained | | Private visual CSS | done | Duplicate portable/app-token layers removed; approved shared shell/login baseline retained |
| Production build | done | `npm run build` completed after core migration | | Production build | done | `npm run build` completed after core migration |
| Browser acceptance | in progress | Four switcher viewports, dark theme, desktop/mobile sidebar and user menu pass; Autocomplete library warnings remain | | Browser acceptance | in progress | 1440x900 and 390x844 shell/material flows pass; remaining warnings are HeroUI Autocomplete internal dialog labels |
| Cross-app scope classification | done | Real app name is 审计材料中心; notification-only rollout, full-feature items intentionally excluded | | Cross-app scope classification | done | Real app name is 审计材料中心; notification-only rollout, full-feature items intentionally excluded |
| Notification route | done | Independent `/notifications` page with message/todo, all/unread, search and fixed pagination | | Notification route | done | Independent `/notifications` page with message/todo, all/unread, search and fixed pagination |
| Notification Navbar entry | done | HeroUI Badge.Anchor/Badge/Button/Tooltip, 36px Bell left of theme action, no Sidebar entry | | Notification Navbar entry | done | HeroUI Badge.Anchor/Badge/Button/Tooltip, 36px Bell action, no theme toggle or Sidebar entry |
| Notification mutations | done | Existing query/count/read/action APIs; single/all read and `{notice_id}` dynamic actions preserved | | Notification mutations | done | Existing query/count/read/action APIs; single/all read and `{notice_id}` dynamic actions preserved |
| Notification link safety | done | Plain text content, base-aware same-app links, external new-window links and normalized login redirect | | Notification link safety | done | Plain text content, base-aware same-app links, external new-window links and normalized login redirect |
| Notification browser acceptance | done | Four required viewports in both themes; badge boundaries, actions, links, overflow and isolated console pass | | Notification browser acceptance | done | Four required viewports in both themes; badge boundaries, actions, links, overflow and isolated console pass |
......
...@@ -29,8 +29,8 @@ paths are not valid delivery mechanisms. ...@@ -29,8 +29,8 @@ paths are not valid delivery mechanisms.
- Primary brand: `#9E1B1B` through the authoritative `--accent` token. - Primary brand: `#9E1B1B` through the authoritative `--accent` token.
- Light identity: `xy-logo-red.png`. - Light identity: `xy-logo-red.png`.
- Dark identity: `xy-logo-white.png`. - Dark identity: `xy-logo-white.png`.
- Root theme: `.light`/`.dark` plus matching `data-theme`. - Root theme: the application shell is explicitly `.light`; the content canvas stays light while the Sidebar consumes the fixed dark split-shell tokens.
- Theme storage key: `xinyuan_color_theme`. - Theme switching is intentionally absent from the production Navbar.
- Application display name: `审计材料中心` everywhere. - Application display name: `审计材料中心` everywhere.
## Surface Hierarchy ## Surface Hierarchy
...@@ -92,8 +92,8 @@ IDs only. Dates are submitted as ISO 8601. `source_system` and ...@@ -92,8 +92,8 @@ IDs only. Dates are submitted as ISO 8601. `source_system` and
## Notification Center ## Notification Center
- The Bell is a 36px HeroUI icon Button immediately left of the theme action, - The Bell is a 36px HeroUI icon Button in the Navbar action area, composed with
composed with `Tooltip`, `Badge.Anchor` and `Badge`. `Tooltip`, `Badge.Anchor` and `Badge`; no theme toggle is rendered.
- Zero unread hides the Badge, 1-99 shows the real count, and values above 99 - Zero unread hides the Badge, 1-99 shows the real count, and values above 99
display `99+` while retaining the real accessible count. display `99+` while retaining the real accessible count.
- `/notifications` uses message/todo and all/unread HeroUI Tabs, HeroUI - `/notifications` uses message/todo and all/unread HeroUI Tabs, HeroUI
......
...@@ -63,7 +63,7 @@ The authoritative references are `src/services/record-api.md`, ...@@ -63,7 +63,7 @@ The authoritative references are `src/services/record-api.md`,
## Remaining UI Inventory ## Remaining UI Inventory
Production routes contain no Catalyst, Headless UI, ECharts, visible native Production routes contain no Catalyst, Headless UI or visible native
select/date/file controls, legacy UI compatibility imports or page-owned select/date/file controls, legacy UI compatibility imports or page-owned
HeroUI appearance styles. The historical kit, unreachable workspace and unused HeroUI appearance styles. The historical kit, unreachable workspace and unused
decorative empty-state page were removed after production references reached decorative empty-state page were removed after production references reached
...@@ -88,11 +88,12 @@ switcher grouping gap without changing its application source or navigation. ...@@ -88,11 +88,12 @@ switcher grouping gap without changing its application source or navigation.
## Theme Audit ## Theme Audit
- `src/design-system/theme.css` is byte-identical to the approved baseline. - `src/design-system/theme.css` retains the Xinyuan HeroUI tokens and now includes
the fixed dark Sidebar and ECharts chart tokens used by the approved baseline.
- `tokens.css`, `shell.css` and the login contract in `tailwind.css` are exact - `tokens.css`, `shell.css` and the login contract in `tailwind.css` are exact
copies of the approved `problem_rectification` shared baseline. copies of the approved `problem_rectification` shared baseline.
- Inter weights and the stable red/white Xinyuan assets are loaded globally. - Inter weights and the stable red/white Xinyuan assets are loaded globally.
- Official `@heroui-pro/react/css` loads before the exact Xinyuan theme and - Official `@heroui-pro/react/css` loads before the Xinyuan theme and
extension Tokens. The temporary Pro CSS compatibility snapshot was removed. extension Tokens. The temporary Pro CSS compatibility snapshot was removed.
## Browser Evidence ## Browser Evidence
......
...@@ -29,6 +29,8 @@ ...@@ -29,6 +29,8 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"embla-carousel": "^8.6.0", "embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
...@@ -12642,6 +12644,36 @@ ...@@ -12642,6 +12644,36 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/echarts": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.1.0"
}
},
"node_modules/echarts-for-react": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz",
"integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
},
"peerDependencies": {
"echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
...@@ -23635,6 +23667,12 @@ ...@@ -23635,6 +23667,12 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/size-sensor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz",
"integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==",
"license": "ISC"
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
...@@ -26605,6 +26643,21 @@ ...@@ -26605,6 +26643,21 @@
"zod": "^3.18.0" "zod": "^3.18.0"
} }
}, },
"node_modules/zrender": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/zwitch": { "node_modules/zwitch": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
......
...@@ -31,6 +31,8 @@ ...@@ -31,6 +31,8 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"embla-carousel": "^8.6.0", "embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
......
import { Button, Chip, Pagination, Spinner, Tooltip } from "@heroui/react"; import { Avatar, Button, Chip, Pagination, Spinner, Tooltip } from "@heroui/react";
import { ActionBar } from "@heroui-pro/react/action-bar"; import { ActionBar } from "@heroui-pro/react/action-bar";
import { DataGrid } from "@heroui-pro/react/data-grid"; import { DataGrid } from "@heroui-pro/react/data-grid";
import { EmptyState } from "@heroui-pro/react/empty-state"; import { EmptyState } from "@heroui-pro/react/empty-state";
import XinyuanAutocomplete from "@/components/XinyuanAutocomplete";
import { useMemo } from "react"; import { useMemo } from "react";
function resolveRowKey(row, index, getRowKey) { function resolveRowKey(row, index, getRowKey) {
...@@ -239,13 +240,57 @@ export function DataTableTextPair({ primary, secondary, className = "" }) { ...@@ -239,13 +240,57 @@ export function DataTableTextPair({ primary, secondary, className = "" }) {
); );
} }
export function DataTablePersonIdentity({ name, account, className = "" }) { function getPersonFallback(name) {
const normalized = String(name ?? "").trim();
if (!normalized || normalized === "-") {
return "?";
}
const compact = normalized.replace(/\s+/g, "");
if (/^[\u3400-\u9fff]/.test(compact)) {
return Array.from(compact)[0];
}
return Array.from(compact).slice(0, 2).join("").toUpperCase();
}
export function DataTablePersonIdentity({
name,
account,
secondary,
avatar,
className = "",
}) {
const primaryText = String(name ?? "").trim() || "-";
const secondaryText = String(account ?? secondary ?? "").trim();
return ( return (
<DataTableTextPair <Tooltip delay={0}>
primary={name} <div
secondary={account} className={["flex min-w-0 items-center gap-2", className]
className={className} .filter(Boolean)
/> .join(" ")}
tabIndex={0}
>
<Avatar size="sm" variant="soft" className="shrink-0">
{avatar ? <Avatar.Image alt={primaryText} src={avatar} /> : null}
<Avatar.Fallback>{getPersonFallback(primaryText)}</Avatar.Fallback>
</Avatar>
<span className="grid min-w-0 gap-1">
<span className="min-w-0 truncate text-sm font-medium">
{primaryText}
</span>
{secondaryText ? (
<span className="min-w-0 truncate text-sm text-muted">
{secondaryText}
</span>
) : null}
</span>
</div>
<Tooltip.Content>
<span className="whitespace-pre-wrap">
{[primaryText, secondaryText].filter(Boolean).join("\n")}
</span>
</Tooltip.Content>
</Tooltip>
); );
} }
...@@ -264,8 +309,10 @@ export function DataTableChip({ children, text, className = "" }) { ...@@ -264,8 +309,10 @@ export function DataTableChip({ children, text, className = "" }) {
export function DataTablePagination({ export function DataTablePagination({
page, page,
pageSize, pageSize,
pageSizeOptions = [],
total, total,
onPageChange, onPageChange,
onPageSizeChange,
isDisabled = false, isDisabled = false,
itemLabel = "条", itemLabel = "条",
}) { }) {
...@@ -274,13 +321,50 @@ export function DataTablePagination({ ...@@ -274,13 +321,50 @@ export function DataTablePagination({
const pageWindowEnd = total const pageWindowEnd = total
? Math.min(totalPages, pageWindowStart + 5) ? Math.min(totalPages, pageWindowStart + 5)
: pageWindowStart; : pageWindowStart;
const normalizedPageSizeOptions = pageSizeOptions
.map((value) => Number(value))
.filter(
(value, index, values) =>
Number.isFinite(value) &&
value > 0 &&
values.indexOf(value) === index,
);
const pageSizeOptionsForPicker = normalizedPageSizeOptions.map((value) => ({
id: String(value),
label: `${value} 条/页`,
}));
const hasPageSizePicker =
pageSizeOptionsForPicker.length > 0 &&
typeof onPageSizeChange === "function";
return ( return (
<div className="mt-2"> <div className="mt-2 flex min-w-0 flex-wrap items-center justify-between gap-3">
<Pagination size="sm" className="w-full"> <div className="flex min-w-0 shrink-0 items-center gap-3">
<Pagination.Summary> <span className="min-w-0 text-sm text-muted">
共 {total} {itemLabel},第 {total ? page + 1 : 0}{" "} 共 {total} {itemLabel},第 {total ? page + 1 : 0}{" "}
/ {total ? totalPages : 0} 页 / {total ? totalPages : 0} 页
</Pagination.Summary> </span>
{hasPageSizePicker ? (
<XinyuanAutocomplete
aria-label="每页条数"
className="w-24 shrink-0"
isDisabled={isDisabled}
options={pageSizeOptionsForPicker}
placeholder="每页条数"
selectedKey={String(pageSize)}
showSearch={false}
valueText={`${pageSize} 条/页`}
variant="secondary"
onSelectionChange={(key) => {
const nextPageSize = Number(key);
if (normalizedPageSizeOptions.includes(nextPageSize)) {
onPageSizeChange(nextPageSize);
}
}}
/>
) : null}
</div>
<Pagination className="ml-auto w-auto shrink-0" size={hasPageSizePicker ? "lg" : "sm"}>
<Pagination.Content> <Pagination.Content>
<Pagination.Item> <Pagination.Item>
<Pagination.Previous <Pagination.Previous
......
...@@ -54,6 +54,7 @@ export default function XinyuanAutocomplete({ ...@@ -54,6 +54,7 @@ export default function XinyuanAutocomplete({
popoverHeader, popoverHeader,
renderOption, renderOption,
searchPlaceholder = "输入关键词搜索", searchPlaceholder = "输入关键词搜索",
showSearch = true,
selectedKey, selectedKey,
selectedKeys, selectedKeys,
selectionMode = "single", selectionMode = "single",
...@@ -139,6 +140,7 @@ export default function XinyuanAutocomplete({ ...@@ -139,6 +140,7 @@ export default function XinyuanAutocomplete({
onInputChange={onInputChange} onInputChange={onInputChange}
> >
{popoverHeader} {popoverHeader}
{showSearch ? (
<SearchField <SearchField
aria-label={searchPlaceholder} aria-label={searchPlaceholder}
fullWidth fullWidth
...@@ -154,6 +156,7 @@ export default function XinyuanAutocomplete({ ...@@ -154,6 +156,7 @@ export default function XinyuanAutocomplete({
</SearchField.ClearButton> </SearchField.ClearButton>
</SearchField.Group> </SearchField.Group>
</SearchField> </SearchField>
) : null}
<ListBox <ListBox
aria-label={ariaLabel || placeholder} aria-label={ariaLabel || placeholder}
dependencies={[Array.from(selectedKeyStringSet).join("\u0000")]} dependencies={[Array.from(selectedKeyStringSet).join("\u0000")]}
......
import { getXinyuanEChartsTheme } from '@/design-system/chartPalette';
import ReactECharts from 'echarts-for-react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
function mergeAxisTheme(axis, theme) {
if (!axis) return axis;
const applyTheme = (item) => ({
...item,
axisLabel: { color: theme.axis, fontFamily: theme.fontFamily, fontSize: 12, ...item?.axisLabel },
axisLine: { ...item?.axisLine, lineStyle: { color: theme.grid, ...item?.axisLine?.lineStyle } },
axisTick: { ...item?.axisTick, lineStyle: { color: theme.grid, ...item?.axisTick?.lineStyle } },
splitLine: { ...item?.splitLine, lineStyle: { color: theme.grid, ...item?.splitLine?.lineStyle } },
});
return Array.isArray(axis) ? axis.map(applyTheme) : applyTheme(axis);
}
export default function XinyuanEChart({ ariaLabel, className = '', height = 320, onEvents, option }) {
const chartRef = useRef(null);
const hideTooltip = useCallback(() => {
chartRef.current?.getEchartsInstance?.().dispatchAction({ type: 'hideTip' });
}, []);
useEffect(() => {
window.addEventListener('resize', hideTooltip);
return () => window.removeEventListener('resize', hideTooltip);
}, [hideTooltip]);
const themedOption = useMemo(() => {
const theme = getXinyuanEChartsTheme();
const applyPieTheme = (series) => series?.type === 'pie'
? {
...series,
itemStyle: { borderColor: theme.sectorGap, borderRadius: theme.sectorRadius, borderWidth: 2, ...series.itemStyle },
padAngle: series.padAngle ?? 1,
}
: series;
const series = Array.isArray(option?.series) ? option.series.map(applyPieTheme) : applyPieTheme(option?.series);
return {
animationDuration: 320,
animationDurationUpdate: 240,
animationEasing: 'cubicOut',
aria: { enabled: true, decal: { show: false }, ...option?.aria },
color: option?.color || theme.palette,
textStyle: { color: theme.label, fontFamily: theme.fontFamily, ...option?.textStyle },
...option,
series,
legend: option?.legend ? {
icon: 'circle', itemHeight: 8, itemWidth: 8, ...option.legend,
textStyle: { color: theme.label, fontFamily: theme.fontFamily, fontSize: 12, lineHeight: 16, ...option.legend?.textStyle },
} : undefined,
tooltip: {
appendToBody: true,
backgroundColor: theme.tooltipBackground,
borderColor: theme.tooltipBorder,
borderWidth: 1,
className: 'rc-echarts-tooltip',
confine: true,
enterable: false,
extraCssText: `border-radius: 12px; box-shadow: ${theme.tooltipShadow};`,
padding: [10, 12],
renderMode: 'html',
...option?.tooltip,
textStyle: { color: theme.foreground, fontFamily: theme.fontFamily, fontSize: 12, ...option?.tooltip?.textStyle },
},
xAxis: mergeAxisTheme(option?.xAxis, theme),
yAxis: mergeAxisTheme(option?.yAxis, theme),
};
}, [option]);
return (
<div aria-label={ariaLabel} className={`rc-chart-interaction w-full min-w-0 ${className}`.trim()} role="img" tabIndex={0}>
<ReactECharts lazyUpdate notMerge onEvents={{ globalout: hideTooltip, ...onEvents }} option={themedOption} opts={{ renderer: 'canvas' }} ref={chartRef} style={{ height, width: '100%' }} />
</div>
);
}
...@@ -93,3 +93,36 @@ export function colorizeXinyuanChartTooltipPayload(payload = [], resolveColor) { ...@@ -93,3 +93,36 @@ export function colorizeXinyuanChartTooltipPayload(payload = [], resolveColor) {
}; };
}); });
} }
function readRootToken(tokenName, fallback = '') {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return fallback;
}
return window.getComputedStyle(document.documentElement).getPropertyValue(tokenName).trim() || fallback;
}
export function createResolvedXinyuanChartColorMap(keys = [], options = {}) {
return new Map(Array.from(createXinyuanChartColorMap(keys, options)).map(([key, color]) => {
const match = String(color).match(/^var\((--[^)]+)\)$/);
return [key, match ? readRootToken(match[1], '#667085') : color];
}));
}
export function getXinyuanEChartsTheme() {
const fontFamily = typeof document === 'undefined'
? 'Inter, sans-serif'
: window.getComputedStyle(document.body).fontFamily;
return {
axis: readRootToken('--xinyuan-chart-axis', '#737373'),
grid: readRootToken('--xinyuan-chart-grid', '#e8e8e8'),
label: readRootToken('--xinyuan-chart-label', '#737373'),
foreground: readRootToken('--xinyuan-chart-tooltip-foreground', '#222222'),
fontFamily: fontFamily || 'Inter, sans-serif',
palette: Array.from({ length: 16 }, (_, index) => readRootToken(`--xinyuan-chart-${index + 1}`, '#667085')),
tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'),
tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'),
tooltipShadow: readRootToken('--xinyuan-chart-tooltip-shadow', '0 16px 40px rgb(15 23 42 / 14%)'),
sectorGap: readRootToken('--xinyuan-chart-sector-gap', '#ffffff'),
sectorRadius: Number.parseFloat(readRootToken('--xinyuan-chart-sector-radius', '10')) || 10,
};
}
...@@ -436,6 +436,24 @@ ...@@ -436,6 +436,24 @@
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.125rem;
font-weight: var(--rc-font-bold);
line-height: 1.5rem;
letter-spacing: 0;
}
.rc-module-title {
margin: 0;
color: var(--rc-text-primary);
font-size: 1rem;
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);
...@@ -466,6 +484,10 @@ ...@@ -466,6 +484,10 @@
outline-offset: 2px; outline-offset: 2px;
} }
.rc-echarts-tooltip {
position: fixed !important;
}
.rc-kpi-grid { .rc-kpi-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
...@@ -1454,3 +1476,118 @@ ...@@ -1454,3 +1476,118 @@
border-right: 0; border-right: 0;
} }
} }
/* Final fixed dark Sidebar overrides. */
.template-dashboard-shell .app-sidebar-glass,
.rc-mobile-sidebar.app-sidebar-glass {
color: var(--rc-sidebar-foreground);
border-color: var(--rc-sidebar-border) !important;
background-color: var(--rc-sidebar-background) !important;
}
.template-dashboard-shell .sidebar__group-label,
.rc-mobile-sidebar .sidebar__group-label,
.template-dashboard-shell .sidebar__menu-icon,
.rc-mobile-sidebar .sidebar__menu-icon { color: var(--rc-sidebar-muted); }
.template-dashboard-shell .sidebar__menu-label,
.rc-mobile-sidebar .sidebar__menu-label,
.template-dashboard-shell .sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item-content { color: var(--rc-sidebar-foreground); }
.template-dashboard-shell .sidebar__menu-item[data-current='true'] .sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item[data-current='true'] .sidebar__menu-item-content {
color: var(--rc-sidebar-item-selected-foreground);
background: var(--rc-sidebar-item-selected);
}
.template-dashboard-shell .sidebar__menu-item:hover .sidebar__menu-item-content,
.template-dashboard-shell .sidebar__menu-item[data-hovered='true'] .sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item:hover .sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item[data-hovered='true'] .sidebar__menu-item-content {
color: var(--rc-sidebar-foreground);
background: var(--rc-sidebar-item-hover);
}
.template-dashboard-shell .app-sidebar-user-trigger,
.rc-mobile-sidebar .app-sidebar-user-trigger { color: var(--rc-sidebar-foreground); }
/* Keep HeroUI's generated slot styles from replacing the fixed dark-sidebar tokens. */
.template-dashboard-shell .sidebar__menu-icon,
.rc-mobile-sidebar .sidebar__menu-icon,
.template-dashboard-shell .sidebar__menu-icon .anticon,
.rc-mobile-sidebar .sidebar__menu-icon .anticon {
color: var(--rc-sidebar-muted) !important;
}
.template-dashboard-shell .sidebar__menu-label,
.rc-mobile-sidebar .sidebar__menu-label,
.template-dashboard-shell .app-sidebar-title,
.rc-mobile-sidebar .app-sidebar-title,
.template-dashboard-shell .app-sidebar-user-name,
.rc-mobile-sidebar .app-sidebar-user-name,
.template-dashboard-shell .app-sidebar-app-trigger.button,
.rc-mobile-sidebar .app-sidebar-app-trigger.button {
color: var(--rc-sidebar-foreground) !important;
}
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content,
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content {
color: var(--rc-sidebar-item-selected-foreground) !important;
background: var(--rc-sidebar-item-selected) !important;
}
.template-dashboard-shell
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content
:is(.sidebar__menu-icon, .sidebar__menu-label, .anticon),
.rc-mobile-sidebar
.sidebar__menu-item[data-current='true']
.sidebar__menu-item-content
:is(.sidebar__menu-icon, .sidebar__menu-label, .anticon) {
color: var(--rc-sidebar-item-selected-foreground) !important;
}
.template-dashboard-shell .sidebar__menu-item:hover .sidebar__menu-item-content,
.template-dashboard-shell .sidebar__menu-item[data-hovered='true'] .sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item:hover .sidebar__menu-item-content,
.rc-mobile-sidebar .sidebar__menu-item[data-hovered='true'] .sidebar__menu-item-content {
color: var(--rc-sidebar-foreground) !important;
background: var(--rc-sidebar-item-hover) !important;
}
.template-dashboard-shell .app-sidebar-user-description,
.rc-mobile-sidebar .app-sidebar-user-description {
color: var(--rc-sidebar-muted) !important;
}
.template-dashboard-shell .app-sidebar-user-trigger,
.rc-mobile-sidebar .app-sidebar-user-trigger {
color: var(--rc-sidebar-foreground) !important;
}
.template-dashboard-shell .app-sidebar-user-trigger.dropdown__trigger:hover,
.template-dashboard-shell .app-sidebar-user-trigger.dropdown__trigger[data-hovered='true'],
.template-dashboard-shell .app-sidebar-user-trigger.dropdown__trigger[aria-expanded='true'],
.rc-mobile-sidebar .app-sidebar-user-trigger.dropdown__trigger:hover,
.rc-mobile-sidebar .app-sidebar-user-trigger.dropdown__trigger[data-hovered='true'],
.rc-mobile-sidebar .app-sidebar-user-trigger.dropdown__trigger[aria-expanded='true'] {
color: var(--rc-sidebar-foreground) !important;
background: var(--rc-sidebar-item-hover) !important;
}
.template-dashboard-shell .app-sidebar-user-avatar,
.rc-mobile-sidebar .app-sidebar-user-avatar {
background: var(--rc-sidebar-item-hover) !important;
}
.template-dashboard-shell .app-sidebar-app-trigger.button:hover,
.template-dashboard-shell .app-sidebar-app-trigger.button[data-hovered='true'],
.template-dashboard-shell .app-sidebar-app-trigger.button[data-pressed='true'],
.template-dashboard-shell .app-sidebar-app-trigger.button[aria-expanded='true'],
.rc-mobile-sidebar .app-sidebar-app-trigger.button:hover,
.rc-mobile-sidebar .app-sidebar-app-trigger.button[data-hovered='true'],
.rc-mobile-sidebar .app-sidebar-app-trigger.button[data-pressed='true'],
.rc-mobile-sidebar .app-sidebar-app-trigger.button[aria-expanded='true'] {
color: var(--rc-sidebar-item-selected-foreground) !important;
background: var(--rc-sidebar-item-selected) !important;
}
...@@ -11,38 +11,37 @@ ...@@ -11,38 +11,37 @@
[data-theme="light"], [data-theme="light"],
[data-theme="default"] { [data-theme="default"] {
/* Theme Colors (Light Mode) */ /* 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); --accent-foreground: oklch(99.11% 0 0);
--background: oklch(97.02% 0.0045 27.03); --background: oklch(97.02% 0.0000 29.20);
--border: oklch(90.00% 0.0045 27.03); --border: oklch(90.00% 0.0000 29.20);
--danger: oklch(45.32% 0.1661 27.03); --danger: oklch(45.32% 0.1661 27.03);
--danger-foreground: oklch(99.11% 0 0); --danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */ --default: oklch(94.00% 0.0000 29.20);
--default: var(--surface-secondary); --default-foreground: oklch(21.03% 0.0059 29.20);
--default-foreground: var(--surface-secondary-foreground); --field-background: oklch(100.00% 0.0000 29.20);
--field-background: var(--surface);
--field-border: transparent; --field-border: transparent;
--field-foreground: var(--surface-foreground); --field-foreground: oklch(21.03% 0.0000 29.20);
--field-placeholder: oklch(55.17% 0.0090 27.03); --field-placeholder: oklch(55.17% 0.0000 29.20);
--focus: oklch(45.32% 0.1661 27.03); --focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(21.03% 0.0045 27.03); --foreground: oklch(21.03% 0.0000 29.20);
--muted: oklch(55.17% 0.0090 27.03); --muted: oklch(55.17% 0.0000 29.20);
--overlay: var(--surface); --overlay: oklch(100.00% 0.0000 29.20);
--overlay-foreground: var(--surface-foreground); --overlay-foreground: oklch(21.03% 0.0000 29.20);
--scrollbar: oklch(87.10% 0.0045 27.03); --scrollbar: oklch(87.10% 0.0000 29.20);
--segment: oklch(100.00% 0.0045 27.03); --segment: oklch(100.00% 0.0000 29.20);
--segment-foreground: oklch(21.03% 0.0045 27.03); --segment-foreground: oklch(21.03% 0.0000 29.20);
--separator: oklch(92.00% 0.0045 27.03); --separator: oklch(92.00% 0.0000 29.20);
--success: oklch(73.29% 0.1952 123.59); --success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.59); --success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(100.00% 0.0022 27.03); --surface: oklch(100.00% 0.0000 29.20);
--surface-foreground: oklch(21.03% 0.0045 27.03); --surface-foreground: oklch(21.03% 0.0000 29.20);
--surface-secondary: oklch(95.24% 0.0036 27.03); --surface-secondary: oklch(95.24% 0.0000 29.20);
--surface-secondary-foreground: oklch(21.03% 0.0045 27.03); --surface-secondary-foreground: oklch(21.03% 0.0000 29.20);
--surface-tertiary: oklch(93.73% 0.0036 27.03); --surface-tertiary: oklch(93.73% 0.0000 29.20);
--surface-tertiary-foreground: oklch(21.03% 0.0045 27.03); --surface-tertiary-foreground: oklch(21.03% 0.0000 29.20);
--warning: oklch(78.19% 0.1599 45.11); --warning: oklch(78.19% 0.1585 45.37);
--warning-foreground: oklch(21.03% 0.0059 45.11); --warning-foreground: oklch(21.03% 0.0059 45.37);
/* Border Radius */ /* Border Radius */
--radius: 0.5rem; --radius: 0.5rem;
...@@ -51,42 +50,86 @@ ...@@ -51,42 +50,86 @@
/* Font Family */ /* Font Family */
/* Make sure to load Inter font in your app */ /* Make sure to load Inter font in your app */
--font-sans: var(--font-inter); --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.
*/
--xinyuan-chart-1: #d92d20;
--xinyuan-chart-2: #ef4444;
--xinyuan-chart-3: #f97316;
--xinyuan-chart-4: #f59e0b;
--xinyuan-chart-5: #22c55e;
--xinyuan-chart-6: #14b87a;
--xinyuan-chart-7: #06b6d4;
--xinyuan-chart-8: #0ea5e9;
--xinyuan-chart-9: #3b82f6;
--xinyuan-chart-10: #6366f1;
--xinyuan-chart-11: #8b5cf6;
--xinyuan-chart-12: #7c3aed;
--xinyuan-chart-13: #b42318;
--xinyuan-chart-14: #84cc16;
--xinyuan-chart-15: #a855f7;
--xinyuan-chart-16: #64748b;
--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, .dark,
[data-theme="dark"] { [data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
/* Theme Colors (Dark Mode) */ /* 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); --accent-foreground: oklch(99.11% 0 0);
--background: oklch(12.00% 0.0045 27.03); --background: oklch(12.00% 0.0000 29.20);
--border: oklch(28.00% 0.0045 27.03); --border: oklch(28.00% 0.0000 29.20);
--danger: oklch(62.00% 0.1600 27.03); --danger: oklch(62% 0.16 27.03);
--danger-foreground: oklch(99.11% 0 0); --danger-foreground: oklch(99.11% 0 0);
/* HeroUI control aliases: primary = L1, secondary = L2. */ --default: oklch(27.40% 0.0000 29.20);
--default: var(--surface-secondary); --default-foreground: oklch(99.11% 0 0);
--default-foreground: var(--surface-secondary-foreground); --field-background: oklch(21.03% 0.0000 29.20);
--field-background: var(--surface);
--field-border: transparent; --field-border: transparent;
--field-foreground: var(--surface-foreground); --field-foreground: oklch(99.11% 0.0000 29.20);
--field-placeholder: oklch(70.50% 0.0090 27.03); --field-placeholder: oklch(70.50% 0.0000 29.20);
--focus: oklch(45.32% 0.1661 27.03); --focus: oklch(57.14% 0.2156 29.20);
--foreground: oklch(99.11% 0.0045 27.03); --foreground: oklch(99.11% 0.0000 29.20);
--muted: oklch(70.50% 0.0090 27.03); --muted: oklch(70.50% 0.0000 29.20);
--overlay: var(--surface); --overlay: oklch(21.03% 0.0000 29.20);
--overlay-foreground: var(--surface-foreground); --overlay-foreground: oklch(99.11% 0.0000 29.20);
--scrollbar: oklch(70.50% 0.0045 27.03); --scrollbar: oklch(70.50% 0.0000 29.20);
--segment: oklch(39.64% 0.0045 27.03); --segment: oklch(39.64% 0.0000 29.20);
--segment-foreground: oklch(99.11% 0.0045 27.03); --segment-foreground: oklch(99.11% 0.0000 29.20);
--separator: oklch(25.00% 0.0045 27.03); --separator: oklch(25.00% 0.0000 29.20);
--success: oklch(73.29% 0.1952 123.59); --success: oklch(73.29% 0.1935 123.85);
--success-foreground: oklch(21.03% 0.0059 123.59); --success-foreground: oklch(21.03% 0.0059 123.85);
--surface: oklch(21.03% 0.0090 27.03); --surface: oklch(21.03% 0.0000 29.20);
--surface-foreground: oklch(99.11% 0.0045 27.03); --surface-foreground: oklch(99.11% 0.0000 29.20);
--surface-secondary: oklch(25.70% 0.0067 27.03); --surface-secondary: oklch(25.70% 0.0000 29.20);
--surface-secondary-foreground: oklch(99.11% 0.0045 27.03); --surface-secondary-foreground: oklch(99.11% 0.0000 29.20);
--surface-tertiary: oklch(27.21% 0.0067 27.03); --surface-tertiary: oklch(27.21% 0.0000 29.20);
--surface-tertiary-foreground: oklch(99.11% 0.0045 27.03); --surface-tertiary-foreground: oklch(99.11% 0.0000 29.20);
--warning: oklch(82.03% 0.1400 49.12); --warning: oklch(82.03% 0.1388 49.38);
--warning-foreground: oklch(21.03% 0.0059 49.12); --warning-foreground: oklch(21.03% 0.0059 49.38);
} }
...@@ -27,10 +27,19 @@ ...@@ -27,10 +27,19 @@
var(--background) var(--background)
); );
--rc-text-muted: var(--muted); --rc-text-muted: var(--muted);
--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-sans: var(--font-sans);
--rc-font-medium: 500; --rc-font-medium: 500;
--rc-font-semibold: 600; --rc-font-semibold: 600;
--rc-font-bold: 700;
--rc-line-normal: 1.5; --rc-line-normal: 1.5;
--rc-text-xs: 0.75rem; --rc-text-xs: 0.75rem;
--rc-text-sm: 0.875rem; --rc-text-sm: 0.875rem;
...@@ -146,6 +155,14 @@ ...@@ -146,6 +155,14 @@
--rc-chart-6: oklch(70% 0.12 80); --rc-chart-6: oklch(70% 0.12 80);
--rc-chart-7: oklch(60% 0.1 295); --rc-chart-7: oklch(60% 0.1 295);
--rc-chart-8: oklch(57% 0.035 250); --rc-chart-8: oklch(57% 0.035 250);
--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-primary: var(--rc-chart-1);
--rc-chart-risk: oklch(47% 0.145 27.03); --rc-chart-risk: oklch(47% 0.145 27.03);
--rc-chart-warning: var(--rc-chart-6); --rc-chart-warning: var(--rc-chart-6);
......
This diff is collapsed.
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