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

feat: unify Xinyuan brand and filter visuals

parent 174de5ea
......@@ -13,6 +13,12 @@ Inter 禁止通过 `src/tailwind.css` 导入。Tailwind v4 内容扫描必须排
页面 `className` 仅承担 flex、grid、宽度、间距、对齐、滚动和响应式。背景、颜色、描边、圆角、阴影及交互状态由官方 variant、compound API 和上述共享样式负责。
## 品牌与图表色板
- 鑫元品牌主色固定为 `#DA241A`。HeroUI v3 的 `--accent`、`--accent-hover`、`--focus` 以及共享 `--rc-brand-primary` 均不得被页面或深色主题改写为其他色相;primary 色阶仅作为兼容别名维护。
- 图表统一使用 `--rc-chart-1` 至 `--rc-chart-16`,由 `src/design-system/chartTokens.js` 提供 ECharts 的解析值。顺序为品牌红、低饱和暖色、灰紫中性色和蓝灰色,禁止绿色、荧光色和彩虹式相邻对撞;单系列始终使用 `chart-1`。
- KPI 图标色调复用同一图表色板的暖色/灰紫/蓝灰衍生值,卡片表面保持中性,不以高饱和渐变区分指标。
## 组件合同
- Shell:`@heroui-pro/react/sidebar`、`navbar`;桌面 offcanvas 真折叠,移动端 `Sidebar.Mobile`;导航分组首次进入时默认展开,用户仍可手动收起。
......
......@@ -604,7 +604,7 @@ export function DataTablePagination({
{hasPageSizePicker ? (
<XinyuanAutocomplete
aria-label="每页条数"
className="w-24 shrink-0"
className="w-32 shrink-0"
isDisabled={isDisabled}
options={pageSizeOptionsForPicker}
placeholder="每页条数"
......
import { Close, Eye, More, Reload, Search } from '@/components/AppIcons';
import { ChevronDown, Close, Eye, More, Reload, Search } from '@/components/AppIcons';
import { TableActionButton, TableActionGroup } from '@/components/TableActionButton';
import FilterSelect from '@/components/FilterSelect';
import FormDatePicker from '@/components/FormDatePicker';
......@@ -174,6 +174,7 @@ export default function RecordWorkbench({
filterSchema = EMPTY_ARRAY,
onFiltersChange,
onClearFilters,
onQuery,
title,
listTitle,
columns = EMPTY_ARRAY,
......@@ -373,20 +374,45 @@ export default function RecordWorkbench({
{structuredFilterFields.length > 0 ? (
<Surface className="rc-filter-surface p-6" variant="default">
<Disclosure isExpanded={filtersExpanded} onExpandedChange={setFiltersExpanded}>
<form
onSubmit={(event) => {
event.preventDefault();
onQuery?.();
}}
>
<div className="grid min-w-0 gap-3 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end">
<div className="grid min-w-0 gap-3 sm:grid-cols-2 xl:grid-cols-3">
{primaryFilterFields.map(renderFilterField)}
</div>
<div className="flex min-w-0 items-end justify-end gap-2">
<div className="flex min-w-0 flex-wrap items-end gap-2 md:justify-end xl:flex-nowrap">
{onClearFilters ? (
<Tooltip delay={0}>
<Button isIconOnly aria-label="重置筛选" size="sm" variant="ghost" onPress={onClearFilters}><Reload size={16} /></Button>
<Button
aria-label="重置筛选"
className="rc-filter-reset-button"
isDisabled={Boolean(states.loading)}
isIconOnly
size="sm"
variant="secondary"
type="button"
onPress={onClearFilters}
>
<Reload size={16} />
</Button>
<Tooltip.Content>重置筛选</Tooltip.Content>
</Tooltip>
) : null}
{onQuery ? (
<Button isDisabled={Boolean(states.loading)} size="sm" variant="primary" type="submit">
查询
</Button>
) : null}
{expandedFilterFields.length > 0 ? (
<Disclosure.Trigger className={buttonVariants({ size: 'sm', variant: 'ghost' })}>
<span>{filtersExpanded ? '收起' : '展开'}</span>
<Disclosure.Indicator>
<ChevronDown aria-hidden="true" size={14} />
</Disclosure.Indicator>
</Disclosure.Trigger>
) : null}
</div>
......@@ -398,6 +424,7 @@ export default function RecordWorkbench({
</Disclosure.Content>
) : null}
</div>
</form>
</Disclosure>
</Surface>
) : null}
......
/**
* Xinyuan chart palette in visual order: brand red, warm neutrals, mauve
* neutrals, then restrained blue-greys. Green and fluorescent hues are not
* part of the visualization system.
*/
export const XINYUAN_CHART_COLORS = [
'#DA241A',
'#B83A32',
'#C95A52',
'#D47A70',
'#A65A45',
'#B87958',
'#C99772',
'#6F6674',
'#807783',
'#938B96',
'#4F6380',
'#61758F',
'#74879A',
'#8A6F63',
'#9A8174',
'#B5A69D',
];
export function getXinyuanChartColors() {
if (typeof window === 'undefined') {
return XINYUAN_CHART_COLORS;
}
const styles = getComputedStyle(document.documentElement);
return XINYUAN_CHART_COLORS.map((fallback, index) => (
styles.getPropertyValue(`--rc-chart-${index + 1}`).trim() || fallback
));
}
......@@ -552,6 +552,27 @@
border-radius: var(--rc-radius-overlay);
}
.rc-filter-reset-button.button {
width: 2.25rem;
min-width: 2.25rem;
max-width: 2.25rem;
height: 2.25rem;
flex: 0 0 2.25rem;
padding: 0;
border-radius: 999px;
color: var(--rc-brand-primary);
}
.rc-filter-reset-button.button > span,
.rc-filter-reset-button.button .anticon,
.rc-filter-reset-button.button svg {
display: inline-grid;
width: 1rem;
height: 1rem;
place-items: center;
margin: 0;
}
/* Filter fields paint their HeroUI focus/invalid ring outside the disclosure
panel. The workbench unmounts this content while the filter is collapsed. */
.rc-filter-disclosure-content {
......@@ -729,7 +750,10 @@
max-height: min(60dvh, 42rem);
overflow-x: auto;
overflow-y: auto;
overscroll-behavior: contain;
/* Keep horizontal overflow inside the grid, but let vertical wheel/touch
scrolling continue into the page once the last row is reached. */
overscroll-behavior-x: contain;
overscroll-behavior-y: auto;
}
/* DataGrid adds a non-scrolling resize wrapper. Let sticky headers use the
......
......@@ -11,8 +11,11 @@
[data-theme="light"],
[data-theme="default"] {
/* Theme Colors (Light Mode) */
--accent: oklch(57.14% 0.2156 29.20);
/* Xinyuan brand red. HeroUI's accent token is the v3 primary color. */
--accent: #DA241A;
--accent-foreground: oklch(99.11% 0 0);
--accent-hover: #C91F17;
--accent-pressed: #B51B14;
--background: oklch(97.02% 0.0000 29.20);
--border: oklch(90.00% 0.0000 29.20);
--danger: oklch(45.32% 0.1661 27.03);
......@@ -23,7 +26,7 @@
--field-border: transparent;
--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);
--focus: #DA241A;
--foreground: oklch(21.03% 0.0000 29.20);
--muted: oklch(55.17% 0.0000 29.20);
--overlay: oklch(100.00% 0.0000 29.20);
......@@ -32,8 +35,8 @@
--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);
--success: #61758F;
--success-foreground: #ffffff;
--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);
......@@ -70,24 +73,24 @@
* 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;
/* Coordinated low-conflict chart palette: brand, warm neutrals, mauve
neutrals and restrained blue-greys. */
--xinyuan-chart-1: #DA241A;
--xinyuan-chart-2: #B83A32;
--xinyuan-chart-3: #C95A52;
--xinyuan-chart-4: #D47A70;
--xinyuan-chart-5: #A65A45;
--xinyuan-chart-6: #B87958;
--xinyuan-chart-7: #C99772;
--xinyuan-chart-8: #6F6674;
--xinyuan-chart-9: #807783;
--xinyuan-chart-10: #938B96;
--xinyuan-chart-11: #4F6380;
--xinyuan-chart-12: #61758F;
--xinyuan-chart-13: #74879A;
--xinyuan-chart-14: #8A6F63;
--xinyuan-chart-15: #9A8174;
--xinyuan-chart-16: #B5A69D;
--xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-radius: 10px;
--xinyuan-chart-grid: #e8e8e8;
......@@ -105,8 +108,11 @@
[data-theme="dark"] {
color-scheme: dark;
/* Theme Colors (Dark Mode) */
--accent: oklch(57.14% 0.2156 29.20);
/* Keep the primary brand stable across themes. */
--accent: #DA241A;
--accent-foreground: oklch(99.11% 0 0);
--accent-hover: #C91F17;
--accent-pressed: #B51B14;
--background: oklch(12.00% 0.0000 29.20);
--border: oklch(28.00% 0.0000 29.20);
--danger: oklch(62% 0.16 27.03);
......@@ -117,7 +123,7 @@
--field-border: transparent;
--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);
--focus: #DA241A;
--foreground: oklch(99.11% 0.0000 29.20);
--muted: oklch(70.50% 0.0000 29.20);
--overlay: oklch(21.03% 0.0000 29.20);
......@@ -126,8 +132,8 @@
--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);
--success: #74879A;
--success-foreground: #ffffff;
--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);
......
......@@ -4,10 +4,26 @@
[data-theme='light'],
[data-theme='default'] {
/* Xinyuan aliases and extension tokens. HeroUI core tokens are owned by theme.css. */
--rc-brand-primary: var(--accent);
--rc-brand-primary: #DA241A;
--rc-brand-light: color-mix(in oklab, var(--accent) 84%, white);
--rc-brand-dark: color-mix(in oklab, var(--accent) 82%, black);
/* Compatibility aliases for integrations that still read numbered primary
tokens. HeroUI v3 components consume --accent and --accent-hover. */
--primary: #DA241A;
--primary-foreground: #ffffff;
--primary-50: #FFF1F0;
--primary-100: #FFE0DD;
--primary-200: #FFC2BD;
--primary-300: #FFA39C;
--primary-400: #F26D63;
--primary-500: #DA241A;
--primary-600: #C91F17;
--primary-700: #B51B14;
--primary-800: #8F1711;
--primary-900: #6F120E;
--primary-950: #4B0C09;
--rc-bg-canvas: var(--background);
--rc-bg-surface: var(--surface);
--rc-border-default: var(--border);
......@@ -92,14 +108,14 @@
0 4px 12px rgb(15 23 42 / 2%);
/* 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-success: oklch(54% 0.13 145);
--rc-metric-tone-teal: oklch(55% 0.1 205);
--rc-metric-tone-danger: oklch(52% 0.17 25);
--rc-metric-tone-warning: oklch(62% 0.12 75);
--rc-metric-tone-violet: oklch(56% 0.12 300);
--rc-metric-tone-indigo: oklch(53% 0.13 275);
--rc-metric-tone-coral: oklch(60% 0.12 48);
--rc-metric-tone-info: var(--rc-chart-11);
--rc-metric-tone-success: var(--rc-chart-12);
--rc-metric-tone-teal: var(--rc-chart-14);
--rc-metric-tone-danger: var(--rc-chart-1);
--rc-metric-tone-warning: var(--rc-chart-6);
--rc-metric-tone-violet: var(--rc-chart-9);
--rc-metric-tone-indigo: var(--rc-chart-13);
--rc-metric-tone-coral: var(--rc-chart-3);
--rc-metric-tone-neutral: var(--muted);
--rc-metric-tone-info-soft: color-mix(
in oklab,
......@@ -167,8 +183,8 @@
--rc-chart-primary: var(--rc-chart-1);
--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-positive: var(--rc-chart-12);
--rc-chart-comparison: var(--rc-chart-11);
--rc-chart-neutral: var(--rc-chart-8);
--rc-chart-legend-font-size: var(--xinyuan-chart-legend-font-size);
--rc-chart-legend-line-height: var(--xinyuan-chart-legend-line-height);
......
import { useMemo } from 'react';
import { getXinyuanChartColors } from './chartTokens';
function readToken(name, fallback) {
if (typeof window === 'undefined') return fallback;
......@@ -12,7 +13,7 @@ export default function useXinyuanChartTheme(theme = 'light') {
const muted = readToken('--rc-text-muted', theme === 'dark' ? '#a1a1aa' : '#718096');
const border = readToken('--rc-border-default', theme === 'dark' ? '#303036' : '#e3e8ef');
return {
colors: ['#9e1b1b', '#16845b', '#b46a00', '#0f766e', '#718096'],
colors: getXinyuanChartColors(),
axisLine: { lineStyle: { color: border } },
axisLabel: { color: muted, fontSize: 12 },
splitLine: { lineStyle: { color: border, opacity: 0.7 } },
......
......@@ -325,6 +325,9 @@ export default function SubjectWorkbench() {
setFilters({});
setPage(0);
}}
onQuery={() => {
setRefreshKey((value) => value + 1);
}}
title={subjectConfig.title || subject?.title || subjectName}
listTitle={subjectConfig.listTitle}
description={
......
......@@ -37,7 +37,7 @@ import { saveAs } from 'file-saver';
import JSZip from 'jszip';
import * as XLSX from 'xlsx';
export const DEFAULT_PAGE_SIZE = 10;
export const DEFAULT_PAGE_SIZE = 20;
function normalizeFilterFieldPath(field) {
const value = typeof field === 'string' ? field.trim() : '';
......
......@@ -20,8 +20,9 @@ import clsx from 'clsx';
import dayjs from 'dayjs';
import { useEffect, useMemo, useState } from 'react';
import { loadProblemRectificationDashboardData } from '../problemRectificationDashboardService';
import { XINYUAN_CHART_COLORS } from '@/design-system/chartTokens';
const CHART_COLOR_SET = ['#e11d48', '#fb7185', '#f97316', '#f59e0b', '#22c55e', '#0ea5e9', '#8b5cf6'];
const CHART_COLOR_SET = XINYUAN_CHART_COLORS;
const CATEGORY_LEVEL_TABS = [
{ key: 'level1', label: '一级分类' },
{ key: 'level2', label: '二级分类' },
......@@ -34,24 +35,24 @@ const FOCUS_ISSUE_TYPES = [
label: '经济损失问题',
shortLabel: '经济损失',
description: '造成经济损失且严重程度较高的问题。',
color: '#e11d48',
badgeClass: 'bg-rose-50 text-rose-700',
color: CHART_COLOR_SET[0],
badgeClass: 'bg-[color-mix(in_oklab,var(--rc-chart-1)_12%,var(--surface))] text-[var(--rc-chart-1)]',
},
{
key: 'externalImpact',
label: '外部影响问题',
shortLabel: '外部影响',
description: '涉及处罚机构或外部影响的问题。',
color: '#f59e0b',
badgeClass: 'bg-amber-50 text-amber-700',
color: CHART_COLOR_SET[5],
badgeClass: 'bg-[color-mix(in_oklab,var(--rc-chart-6)_14%,var(--surface))] text-[var(--rc-chart-6)]',
},
{
key: 'accountability',
label: '员工问责问题',
shortLabel: '员工问责',
description: '已触发员工问责后果的问题。',
color: '#0ea5e9',
badgeClass: 'bg-sky-50 text-sky-700',
color: CHART_COLOR_SET[10],
badgeClass: 'bg-[color-mix(in_oklab,var(--rc-chart-11)_12%,var(--surface))] text-[var(--rc-chart-11)]',
},
];
......@@ -72,25 +73,13 @@ function formatCount(value) {
}
function toneCardClass(tone) {
if (tone === 'rose') {
return 'border-rose-200 bg-[linear-gradient(135deg,rgba(255,241,242,0.95),rgba(255,255,255,0.9))]';
}
if (tone === 'amber') {
return 'border-amber-200 bg-[linear-gradient(135deg,rgba(255,251,235,0.95),rgba(255,255,255,0.92))]';
}
if (tone === 'emerald') {
return 'border-emerald-200 bg-[linear-gradient(135deg,rgba(236,253,245,0.95),rgba(255,255,255,0.92))]';
}
if (tone === 'sky') {
return 'border-sky-200 bg-[linear-gradient(135deg,rgba(240,249,255,0.95),rgba(255,255,255,0.92))]';
}
return 'border-zinc-200 bg-[linear-gradient(135deg,rgba(250,250,250,0.95),rgba(255,255,255,0.92))]';
return 'rc-card-surface rc-dashboard-card';
}
function SectionHeading({ eyebrow, title, description }) {
return (
<div className="space-y-2">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-rose-500">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--rc-brand-primary)]">
{eyebrow}
</div>
<div className="text-2xl font-semibold tracking-tight text-slate-950">{title}</div>
......@@ -105,7 +94,7 @@ function StatCard({ label, value, hint, tone = 'default' }) {
return (
<div
className={clsx(
'rounded-[28px] border p-5 shadow-[0_16px_40px_rgba(15,23,42,0.06)]',
'p-5',
toneCardClass(tone),
)}
>
......@@ -233,7 +222,7 @@ function buildDonutOption(data, colors) {
function buildCategoryBarOption(data = []) {
return {
color: ['#e11d48'],
color: [CHART_COLOR_SET[0]],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
......@@ -478,10 +467,10 @@ function buildBubbleMatrixOption(data = []) {
itemStyle: {
color:
item.completionRate >= 75
? '#22c55e'
? CHART_COLOR_SET[10]
: item.completionRate >= 45
? '#f59e0b'
: '#e11d48',
? CHART_COLOR_SET[5]
: CHART_COLOR_SET[0],
opacity: 0.85,
},
label: {
......@@ -582,7 +571,7 @@ function buildBubbleMatrixOption(data = []) {
function buildQualityRatingOption(data) {
return {
color: ['#22c55e', '#f59e0b', '#e11d48'],
color: [CHART_COLOR_SET[10], CHART_COLOR_SET[5], CHART_COLOR_SET[0]],
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(15,23,42,0.9)',
......@@ -707,7 +696,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
[summary.sourceComparison],
);
const overdueRankingOption = useMemo(
() => buildHorizontalBarOption(summary.overdueRanking, '#e11d48'),
() => buildHorizontalBarOption(summary.overdueRanking, CHART_COLOR_SET[0]),
[summary.overdueRanking],
);
const departmentMatrixOption = useMemo(
......@@ -756,7 +745,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
return (
<div className="space-y-8">
<section className="relative overflow-hidden rounded-[34px] border border-white/80 bg-[linear-gradient(135deg,rgba(124,24,24,0.96),rgba(190,24,93,0.92)_54%,rgba(244,63,94,0.9))] px-6 py-7 text-white shadow-[0_28px_80px_rgba(159,18,57,0.24)]">
<section className="relative overflow-hidden rounded-[34px] border border-white/20 bg-[var(--rc-brand-primary)] px-6 py-7 text-white shadow-[0_28px_80px_rgba(218,36,26,0.24)]">
<div className="absolute inset-y-0 right-0 w-[36%] bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_45%)]" />
<div className="absolute -bottom-16 left-1/3 h-40 w-40 rounded-full bg-white/10 blur-3xl" />
<div className="relative flex flex-wrap items-end justify-between gap-6">
......@@ -791,8 +780,8 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
className={clsx(
'rounded-[20px] px-4 py-3 text-sm font-medium transition-all duration-200',
activeGroupKey === group.key
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg'
: 'text-slate-600 hover:bg-rose-50 hover:text-rose-700',
? 'bg-[var(--rc-brand-primary)] text-white shadow-lg'
: 'text-slate-600 hover:bg-[color-mix(in_oklab,var(--rc-brand-primary)_8%,var(--surface))] hover:text-[var(--rc-brand-primary)]',
)}
>
{group.label}
......@@ -864,7 +853,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
label="已完成整改数"
value={formatCount(summary.fixedCount)}
hint="整改进度为已整改或已优化"
tone="emerald"
tone="neutral"
/>
<StatCard
label="整改完成率"
......@@ -932,8 +921,8 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
className={clsx(
'min-h-11 rounded-full border px-4 text-sm font-medium transition',
isActive
? 'border-rose-500 bg-rose-500 text-white shadow-sm'
: 'border-zinc-200 bg-white text-slate-600 hover:border-rose-200 hover:bg-rose-50 hover:text-rose-700',
? 'border-[var(--rc-brand-primary)] bg-[var(--rc-brand-primary)] text-white shadow-sm'
: 'border-zinc-200 bg-white text-slate-600 hover:border-[var(--rc-brand-primary)] hover:bg-[color-mix(in_oklab,var(--rc-brand-primary)_8%,var(--surface))] hover:text-[var(--rc-brand-primary)]',
)}
onClick={() => setActiveCategoryLevel(tab.key)}
>
......
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