Commit 2d3757b2 authored by David Yang's avatar David Yang

feat: 驾驶舱拆分

parent c536e180
......@@ -8,6 +8,7 @@ function resolvePackageRoot(packageName: string) {
const ROUTE_PATHS = {
login: '/login',
dashboard: '/integrity_risk_table/dashboard',
businessDashboard: '/integrity_risk_table/business_dashboard',
problemRectification: '/problem_rectification',
issueRectificationArchive: '/issue_rectification_archive',
problemRectificationForm: '/problem_rectification_form',
......@@ -41,6 +42,10 @@ export default defineConfig({
path: ROUTE_PATHS.dashboard,
component: './problem_rectification/dashboard/index',
},
{
path: ROUTE_PATHS.businessDashboard,
component: './problem_rectification/businessDashboard/BusinessDashboard',
},
{
path: ROUTE_PATHS.problemRectification,
component: './problem_rectification/list/index',
......
......@@ -5,6 +5,7 @@ import { SiteContext } from '@/wrapper/Site';
import {
ArchiveBoxIcon,
Bars3Icon,
ChartPieIcon,
ClipboardDocumentCheckIcon,
HomeModernIcon,
Squares2X2Icon,
......@@ -25,6 +26,11 @@ const menuGroups = [
path: '/integrity_risk_table/dashboard',
icon: HomeModernIcon,
},
{
name: '业务驾驶舱',
path: '/integrity_risk_table/business_dashboard',
icon: ChartPieIcon,
},
{
name: '问题整改',
path: '/problem_rectification',
......@@ -280,9 +286,6 @@ export default function ProblemRectificationLayout() {
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
{APP_NAME}
</h1>
<div className="mt-1 text-xs text-zinc-400">
替代旧多维表格版问题整改应用
</div>
</div>
{applicationSwitcherButton}
</>
......
import { ProblemRectificationDashboardPage } from '../dashboard';
export default function BusinessDashboard() {
return <ProblemRectificationDashboardPage dashboardMode="business" />;
}
import { Button } from '@/catalyst-ui-kit/javascript/button';
import {
Dialog,
DialogActions,
DialogBody,
DialogDescription,
DialogTitle,
} from '@/catalyst-ui-kit/javascript/dialog';
import { Select } from '@/catalyst-ui-kit/javascript/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/catalyst-ui-kit/javascript/table';
import ReactECharts from 'echarts-for-react';
import clsx from 'clsx';
import dayjs from 'dayjs';
......@@ -11,6 +28,32 @@ const CATEGORY_LEVEL_TABS = [
{ key: 'level3', label: '三级分类' },
{ key: 'level4', label: '四级分类' },
];
const FOCUS_ISSUE_TYPES = [
{
key: 'economicLoss',
label: '经济损失问题',
shortLabel: '经济损失',
description: '造成经济损失且严重程度较高的问题。',
color: '#e11d48',
badgeClass: 'bg-rose-50 text-rose-700',
},
{
key: 'externalImpact',
label: '外部影响问题',
shortLabel: '外部影响',
description: '涉及处罚机构或外部影响的问题。',
color: '#f59e0b',
badgeClass: 'bg-amber-50 text-amber-700',
},
{
key: 'accountability',
label: '员工问责问题',
shortLabel: '员工问责',
description: '已触发员工问责后果的问题。',
color: '#0ea5e9',
badgeClass: 'bg-sky-50 text-sky-700',
},
];
function formatPercent(value) {
const numericValue = Number(value || 0);
......@@ -73,16 +116,6 @@ function StatCard({ label, value, hint, tone = 'default' }) {
);
}
function NumberCard({ label, value, hint }) {
return (
<div className="rounded-[24px] border border-zinc-200 bg-zinc-50/70 p-5">
<div className="text-sm font-medium text-slate-500">{label}</div>
<div className="mt-3 text-3xl font-semibold tracking-tight text-slate-950">{value}</div>
{hint ? <div className="mt-2 text-sm text-slate-500">{hint}</div> : null}
</div>
);
}
function Panel({ title, description, children }) {
return (
<section className="rounded-[30px] border border-zinc-200 bg-white/95 shadow-[0_20px_50px_rgba(15,23,42,0.06)]">
......@@ -103,48 +136,57 @@ function EmptyBlock({ title }) {
);
}
function FocusIssueList({ title, description, items = [], tone }) {
const toneClass =
tone === 'rose'
? 'bg-rose-50 text-rose-700'
: tone === 'amber'
? 'bg-amber-50 text-amber-700'
: 'bg-sky-50 text-sky-700';
function FocusIssueDetailDialog({ issueType, items = [], onClose }) {
return (
<Panel title={title} description={description}>
{items.length > 0 ? (
<div className="space-y-3">
{items.map((item) => (
<article
key={item.id}
className="rounded-[22px] border border-zinc-200 bg-[linear-gradient(180deg,#ffffff,rgba(248,250,252,0.92))] p-4 shadow-sm"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-base font-semibold text-slate-900">{item.title}</div>
<div className="mt-1 text-sm text-slate-500">{item.feedbackUnit}</div>
</div>
<div className={clsx('rounded-full px-3 py-1 text-xs font-medium', toneClass)}>
{item.extraLabel}
</div>
</div>
<div className="mt-3 line-clamp-2 text-sm leading-6 text-slate-600">{item.brief}</div>
<div className="mt-4 flex flex-wrap gap-2">
<span className="rounded-full bg-zinc-100 px-3 py-1 text-xs font-medium text-zinc-700">
严重程度 {item.severityLevel}
</span>
<span className="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700">
截止 {item.dueDateLabel}
</span>
</div>
</article>
))}
</div>
) : (
<EmptyBlock title="当前没有相关重点问题" />
)}
</Panel>
<Dialog open={Boolean(issueType)} onClose={onClose} size="5xl">
<DialogTitle>{issueType?.label || '重点问题详情'}</DialogTitle>
<DialogDescription>
{issueType?.description || '当前重点问题详情'} 共 {items.length} 条
</DialogDescription>
<DialogBody>
{items.length > 0 ? (
<Table dense striped className="[--gutter:--spacing(2)]">
<TableHead>
<TableRow>
<TableHeader>问题</TableHeader>
<TableHeader>反馈单位</TableHeader>
<TableHeader>标记</TableHeader>
<TableHeader>严重程度</TableHeader>
<TableHeader>截止日期</TableHeader>
<TableHeader>问题简述</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => (
<TableRow key={item.id} className="align-top">
<TableCell className="min-w-56 font-medium text-slate-950">
{item.title || '-'}
</TableCell>
<TableCell className="min-w-36 text-slate-600">
{item.feedbackUnit || '-'}
</TableCell>
<TableCell>
<span className={clsx('rounded-full px-2.5 py-1 text-xs font-medium', issueType?.badgeClass)}>
{item.extraLabel || '-'}
</span>
</TableCell>
<TableCell>{item.severityLevel || '-'}</TableCell>
<TableCell>{item.dueDateLabel || '-'}</TableCell>
<TableCell className="min-w-80 max-w-[32rem] whitespace-normal text-slate-600">
{item.brief || '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<EmptyBlock title="当前没有相关重点问题" />
)}
</DialogBody>
<DialogActions>
<Button outline onClick={onClose}>关闭</Button>
</DialogActions>
</Dialog>
);
}
......@@ -354,6 +396,71 @@ function buildHorizontalBarOption(data, color) {
};
}
function buildFocusIssueBarOption(data = []) {
return {
color: data.map((item) => item.color),
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params) => {
const item = params[0];
return `${item?.axisValueLabel || ''}: ${item?.value || 0} 条<br/>点击查看详情列表`;
},
backgroundColor: 'rgba(15,23,42,0.9)',
borderWidth: 0,
textStyle: { color: '#fff' },
},
grid: {
left: 8,
right: 20,
top: 18,
bottom: 8,
containLabel: true,
},
xAxis: {
type: 'value',
minInterval: 1,
splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } },
axisLabel: { color: '#64748b' },
},
yAxis: {
type: 'category',
data: data.map((item) => item.label),
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#334155', fontSize: 12 },
},
series: [
{
type: 'bar',
cursor: 'pointer',
data: data.map((item) => ({
name: item.label,
value: item.value,
issueTypeKey: item.key,
itemStyle: {
color: item.color,
borderRadius: [0, 99, 99, 0],
},
})),
barMaxWidth: 34,
showBackground: true,
backgroundStyle: {
color: 'rgba(148,163,184,0.08)',
borderRadius: 99,
},
label: {
show: true,
position: 'right',
formatter: '{c}',
color: '#0f172a',
fontWeight: 700,
},
},
],
};
}
function buildBubbleMatrixOption(data = []) {
const maxTotal = Math.max(...data.map((item) => item.totalCount), 1);
const maxOverdue = Math.max(...data.map((item) => item.overdueCount), 1);
......@@ -437,79 +544,6 @@ function buildBubbleMatrixOption(data = []) {
};
}
function buildTimelinessScatterOption(data = []) {
const maxValue = Math.max(
...data.flatMap((item) => [item.value?.[0] || 0, item.value?.[1] || 0]),
1,
);
return {
tooltip: {
formatter: (params) => {
const item = params.data;
return [
`<strong>${item.name}</strong>`,
`预计整改时长:${item.value[0]} 天`,
`实际整改时长:${item.value[1]} 天`,
`严重程度:${item.value[2]}`,
item.isDelayed ? '判定:已逾期' : '判定:按期或提前完成',
].join('<br/>');
},
backgroundColor: 'rgba(15,23,42,0.9)',
borderWidth: 0,
textStyle: { color: '#fff' },
},
grid: {
left: 8,
right: 16,
top: 16,
bottom: 10,
containLabel: true,
},
xAxis: {
type: 'value',
name: '预计整改时长(天)',
nameTextStyle: { color: '#64748b' },
splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } },
axisLabel: { color: '#64748b' },
},
yAxis: {
type: 'value',
name: '实际整改时长(天)',
nameTextStyle: { color: '#64748b' },
splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } },
axisLabel: { color: '#64748b' },
},
series: [
{
type: 'scatter',
data: data.map((item) => ({
...item,
symbolSize: item.value[2] === '高' ? 18 : item.value[2] === '中' ? 14 : 10,
itemStyle: {
color: item.isDelayed ? '#e11d48' : '#0ea5e9',
opacity: 0.82,
},
})),
},
{
type: 'line',
data: [
[0, 0],
[maxValue, maxValue],
],
silent: true,
symbol: 'none',
lineStyle: {
color: '#94a3b8',
type: 'dashed',
width: 1.5,
},
},
],
};
}
function buildQualityRatingOption(data) {
return {
color: ['#22c55e', '#f59e0b', '#e11d48'],
......@@ -552,12 +586,16 @@ function buildQualityRatingOption(data) {
};
}
export default function ProblemRectificationDashboardPage() {
export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } = {}) {
const [activeGroupKey, setActiveGroupKey] = useState('all');
const [activeCategoryLevel, setActiveCategoryLevel] = useState('level1');
const [activeYear, setActiveYear] = useState('all');
const [activeFocusIssueTypeKey, setActiveFocusIssueTypeKey] = useState('');
const [dashboardData, setDashboardData] = useState({
groups: [],
summaries: {},
yearOptions: [],
yearScopes: {},
error: '',
isEmpty: false,
});
......@@ -582,7 +620,15 @@ export default function ProblemRectificationDashboardPage() {
}, []);
const groups = dashboardData.groups || [];
const summary = dashboardData.summaries?.[activeGroupKey] || {
const yearOptions = dashboardData.yearOptions || [];
const activeScope = activeYear === 'all'
? dashboardData
: dashboardData.yearScopes?.[activeYear] || {
summaries: {},
departmentBuckets: {},
isEmpty: true,
};
const summary = activeScope.summaries?.[activeGroupKey] || {
totalCount: 0,
fixedCount: 0,
completionRate: 0,
......@@ -604,7 +650,6 @@ export default function ProblemRectificationDashboardPage() {
},
overdueRanking: [],
departmentMatrix: [],
timelinessScatter: [],
qualityRatingDistribution: [],
};
......@@ -633,16 +678,44 @@ export default function ProblemRectificationDashboardPage() {
() => buildBubbleMatrixOption(summary.departmentMatrix),
[summary.departmentMatrix],
);
const timelinessScatterOption = useMemo(
() => buildTimelinessScatterOption(summary.timelinessScatter),
[summary.timelinessScatter],
);
const qualityRatingOption = useMemo(
() => buildQualityRatingOption(summary.qualityRatingDistribution),
[summary.qualityRatingDistribution],
);
const focusIssueChartData = useMemo(
() => FOCUS_ISSUE_TYPES.map((type) => ({
...type,
value: summary.focusIssueGroups?.[type.key]?.length || 0,
})),
[summary.focusIssueGroups],
);
const focusIssueOption = useMemo(
() => buildFocusIssueBarOption(focusIssueChartData),
[focusIssueChartData],
);
const focusIssueChartEvents = useMemo(
() => ({
click: (params) => {
const issueTypeKey = params?.data?.issueTypeKey;
if (issueTypeKey) {
setActiveFocusIssueTypeKey(issueTypeKey);
}
},
}),
[],
);
const activeGroup = groups.find((item) => item.key === activeGroupKey);
const activeFocusIssueType = FOCUS_ISSUE_TYPES.find((type) => type.key === activeFocusIssueTypeKey) || null;
const activeFocusIssueItems = activeFocusIssueType
? summary.focusIssueGroups?.[activeFocusIssueType.key] || []
: [];
const isBusinessDashboard = dashboardMode === 'business';
const dashboardTitle = isBusinessDashboard ? '业务驾驶舱' : '问题整改驾驶舱';
const dashboardSubtitle = isBusinessDashboard
? '围绕问题风险定位、部门责任追踪和整改质效评估,沉淀业务分析与督办依据。'
: '围绕整改完成进度、问题风险定位、部门责任压实和整改质效评估,统一展示今年问题整改全景。';
const yearLabel = activeYear === 'all' ? '全部年份' : `${activeYear} 年`;
return (
<div className="space-y-8">
......@@ -655,37 +728,55 @@ export default function ProblemRectificationDashboardPage() {
Integrity Risk Dashboard
</div>
<h1 className="mt-3 text-3xl font-semibold tracking-tight md:text-[2.55rem]">
问题整改驾驶舱
{dashboardTitle}
</h1>
<div className="mt-4 max-w-3xl text-sm leading-6 text-white/78">
围绕整改完成进度、问题风险定位、部门责任压实和整改质效评估,统一展示今年问题整改全景。
{dashboardSubtitle}
</div>
</div>
<div className="rounded-[28px] border border-white/15 bg-white/10 px-5 py-4 backdrop-blur">
<div className="text-xs uppercase tracking-[0.22em] text-white/65">当前分组</div>
<div className="mt-2 text-2xl font-semibold">{activeGroup?.label || '全部'}</div>
<div className="mt-1 text-sm text-white/72">{dayjs().format('YYYY 年 MM 月 DD 日')}</div>
<div className="mt-1 text-sm text-white/72">{yearLabel} · {dayjs().format('YYYY 年 MM 月 DD 日')}</div>
</div>
</div>
</section>
<section className="overflow-x-auto">
<div className="inline-flex min-w-full gap-3 rounded-[28px] border border-zinc-200 bg-white/90 p-2 shadow-sm">
{groups.map((group) => (
<button
key={group.key}
type="button"
onClick={() => setActiveGroupKey(group.key)}
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',
)}
<section className="rounded-[28px] border border-zinc-200 bg-white/90 p-2 shadow-sm">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<div className="overflow-x-auto">
<div className="inline-flex min-w-full gap-3">
{groups.map((group) => (
<button
key={group.key}
type="button"
onClick={() => setActiveGroupKey(group.key)}
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',
)}
>
{group.label}
</button>
))}
</div>
</div>
<label className="flex shrink-0 items-center gap-3 rounded-[20px] border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-slate-600">
<span className="font-medium text-slate-700">统计年份</span>
<Select
value={activeYear}
onChange={(event) => setActiveYear(event.target.value)}
>
{group.label}
</button>
))}
<option value="all">全部年份</option>
{yearOptions.map((year) => (
<option key={year} value={String(year)}>
{year} 年
</option>
))}
</Select>
</label>
</div>
</section>
......@@ -697,7 +788,7 @@ export default function ProblemRectificationDashboardPage() {
{loading ? (
<div className="rounded-[26px] border border-sky-200 bg-sky-50 px-5 py-4 text-sm text-sky-800">
正在加载问题整改驾驶舱...
正在加载{dashboardTitle}...
</div>
) : null}
......@@ -708,221 +799,229 @@ export default function ProblemRectificationDashboardPage() {
</div>
) : null}
{!loading && !dashboardData.isEmpty ? (
{!loading && !dashboardData.isEmpty && activeScope.isEmpty ? (
<div className="rounded-[30px] border border-dashed border-zinc-300 bg-white/80 px-6 py-12 text-center">
<div className="text-xl font-semibold text-slate-900">{yearLabel}暂无问题整改数据</div>
<div className="mt-3 text-sm leading-6 text-slate-500">请选择其它年份或切回全部年份查看统计。</div>
</div>
) : null}
{!loading && !dashboardData.isEmpty && !activeScope.isEmpty ? (
<>
<section className="space-y-5">
<SectionHeading
eyebrow="Overview"
title="宏观总览"
description="先看整体整改健康度,快速判断问题总量、闭环情况和逾期风险。"
/>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<StatCard
label="问题总数"
value={formatCount(summary.totalCount)}
hint="当前分组内全部问题记录"
tone="rose"
/>
<StatCard
label="已完成整改数"
value={formatCount(summary.fixedCount)}
hint="整改进度为已整改或已优化"
tone="emerald"
/>
<StatCard
label="整改完成率"
value={formatPercent(summary.completionRate)}
hint="已完成整改数 / 总问题数"
tone="sky"
{!isBusinessDashboard ? (
<section className="space-y-5">
<SectionHeading
eyebrow="Overview"
title="宏观总览"
description="先看整体整改健康度,快速判断问题总量、闭环情况和逾期风险。"
/>
<StatCard
label="逾期未完成数"
value={formatCount(summary.overdueCount)}
hint="截止今天已超期且未闭环"
tone="amber"
/>
<StatCard
label="逾期率"
value={formatPercent(summary.overdueRate)}
hint="逾期未完成数 / 总问题数"
tone="amber"
/>
</div>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<Panel
title="整改进度分布"
description="按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。"
>
{summary.progressDistribution.length > 0 ? (
<ReactECharts option={progressOption} style={{ height: 340 }} />
) : (
<EmptyBlock title="暂无整改进度分布数据" />
)}
</Panel>
<Panel
title="进度口径说明"
description="当前逾期判定规则采用“预计整改完成时间早于今天且尚未闭环”。如果后续上线显式“是否延期”字段,可直接切换为配置化口径。"
>
<div className="grid gap-4 md:grid-cols-2">
<NumberCard
label="已闭环问题"
value={formatCount(summary.fixedCount)}
hint="适合用于经营层快速判断整体闭环能力"
/>
<NumberCard
label="待重点督办"
value={formatCount(summary.overdueCount)}
hint="适合作为督办清单和预警入口"
/>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<StatCard
label="问题总数"
value={formatCount(summary.totalCount)}
hint="当前分组内全部问题记录"
tone="rose"
/>
<StatCard
label="已完成整改数"
value={formatCount(summary.fixedCount)}
hint="整改进度为已整改或已优化"
tone="emerald"
/>
<StatCard
label="整改完成率"
value={formatPercent(summary.completionRate)}
hint="已完成整改数 / 总问题数"
tone="sky"
/>
<StatCard
label="逾期未完成数"
value={formatCount(summary.overdueCount)}
hint="截止今天已超期且未闭环"
tone="amber"
/>
<StatCard
label="逾期率"
value={formatPercent(summary.overdueRate)}
hint="逾期未完成数 / 总问题数"
tone="amber"
/>
</div>
</section>
) : null}
{isBusinessDashboard ? (
<>
<section className="space-y-5">
<SectionHeading
eyebrow="Progress"
title="整改进度"
description="按整改进度统计分布与口径,辅助判断当前积压状态和督办入口。"
/>
<div className="grid gap-6">
<Panel
title="整改进度分布"
description="按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。"
>
{summary.progressDistribution.length > 0 ? (
<ReactECharts option={progressOption} style={{ height: 340 }} />
) : (
<EmptyBlock title="暂无整改进度分布数据" />
)}
</Panel>
</div>
</Panel>
</div>
</section>
<section className="space-y-5">
<SectionHeading
eyebrow="Risk"
title="问题分析与风险定位"
description="把问题集中区域、根因和来源差异拉出来,支撑制度修订和内审效能判断。"
/>
<div className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<Panel
title="问题类型 / 业务领域分布"
description="切换查看问题一级至四级分类数量分布,帮助定位高发领域。"
>
<div className="mb-5 flex flex-wrap gap-2">
{CATEGORY_LEVEL_TABS.map((tab) => {
const isActive = activeCategoryLevel === tab.key;
return (
<button
key={tab.key}
type="button"
aria-pressed={isActive}
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',
)}
onClick={() => setActiveCategoryLevel(tab.key)}
>
{tab.label}
</button>
);
})}
</section>
<section className="space-y-5">
<SectionHeading
eyebrow="Risk"
title="问题分析与风险定位"
description="把问题集中区域、根因和来源差异拉出来,支撑制度修订和内审效能判断。"
/>
<div className="grid gap-6 xl:grid-cols-[1.05fr_0.95fr]">
<Panel
title="问题类型 / 业务领域分布"
description="切换查看问题一级至四级分类数量分布,帮助定位高发领域。"
>
<div className="mb-5 flex flex-wrap gap-2">
{CATEGORY_LEVEL_TABS.map((tab) => {
const isActive = activeCategoryLevel === tab.key;
return (
<button
key={tab.key}
type="button"
aria-pressed={isActive}
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',
)}
onClick={() => setActiveCategoryLevel(tab.key)}
>
{tab.label}
</button>
);
})}
</div>
{activeCategoryData.length > 0 ? (
<ReactECharts option={categoryOption} style={{ height: 360 }} />
) : (
<EmptyBlock title="暂无问题领域统计数据" />
)}
</Panel>
<Panel
title="问题根本原因分析"
description="按问题原因分类统计,便于识别制度、流程或执行层面的共性成因。"
>
{summary.rootCauseDistribution.length > 0 ? (
<ReactECharts option={rootCauseOption} style={{ height: 360 }} />
) : (
<EmptyBlock title="暂无问题原因统计数据" />
)}
</Panel>
</div>
{activeCategoryData.length > 0 ? (
<ReactECharts option={categoryOption} style={{ height: 360 }} />
) : (
<EmptyBlock title="暂无问题领域统计数据" />
)}
</Panel>
<Panel
title="问题根本原因分析"
description="按问题原因分类统计,便于识别制度、流程或执行层面的共性成因。"
>
{summary.rootCauseDistribution.length > 0 ? (
<ReactECharts option={rootCauseOption} style={{ height: 360 }} />
) : (
<EmptyBlock title="暂无问题原因统计数据" />
)}
</Panel>
</div>
<Panel
title="问题来源对比"
description="横轴为问题来源,堆叠展示检查实施单位,观察内外部检查发现问题的结构差异。"
>
{summary.sourceComparison.categories.length > 0 ? (
<ReactECharts option={sourceComparisonOption} style={{ height: 360 }} />
) : (
<EmptyBlock title="暂无问题来源对比数据" />
)}
</Panel>
<div className="grid gap-6 xl:grid-cols-3">
<FocusIssueList
title="经济损失问题"
description="优先展示造成经济损失且严重程度更高的问题。"
items={summary.focusIssueGroups.economicLoss}
tone="rose"
/>
<FocusIssueList
title="外部影响问题"
description="直观看到涉及处罚机构或外部影响的问题。"
items={summary.focusIssueGroups.externalImpact}
tone="amber"
/>
<FocusIssueList
title="员工问责问题"
description="展示已触发员工问责后果的问题。"
items={summary.focusIssueGroups.accountability}
tone="sky"
/>
</div>
</section>
<section className="space-y-5">
<SectionHeading
eyebrow="Execution"
title="问题责任追踪与执行"
description="围绕责任部门看执行难点,快速识别逾期堆积、完成率偏低和预估时长失真的对象。"
/>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<Panel
title="逾期问题排行"
description="按责任部门统计逾期未完成问题数,用于识别长期挂账和重点督办对象。"
>
{summary.overdueRanking.length > 0 ? (
<ReactECharts option={overdueRankingOption} style={{ height: 340 }} />
) : (
<EmptyBlock title="当前没有逾期问题排行数据" />
)}
</Panel>
<Panel
title="部门整改责任矩阵"
description="X 轴为问题总数,Y 轴为整改完成率,气泡大小表示逾期问题数。"
>
{summary.departmentMatrix.length > 0 ? (
<ReactECharts option={departmentMatrixOption} style={{ height: 340 }} />
) : (
<EmptyBlock title="暂无部门责任矩阵数据" />
)}
</Panel>
</div>
<Panel
title="问题来源对比"
description="横轴为问题来源,堆叠展示检查实施单位,观察内外部检查发现问题的结构差异。"
>
{summary.sourceComparison.categories.length > 0 ? (
<ReactECharts option={sourceComparisonOption} style={{ height: 360 }} />
) : (
<EmptyBlock title="暂无问题来源对比数据" />
)}
</Panel>
<Panel
title="整改时效分析"
description="X 轴为预计整改时长,Y 轴为实际整改时长。虚线右上方的点代表实际时长高于预估。"
>
{summary.timelinessScatter.length > 0 ? (
<ReactECharts option={timelinessScatterOption} style={{ height: 380 }} />
) : (
<EmptyBlock title="缺少预计整改时间、实际整改完成时间或检查完成日期,暂无法形成时效散点图" />
)}
</Panel>
</section>
<section className="space-y-5">
<SectionHeading
eyebrow="Quality"
title="审计整改质效"
description="驾驶舱只展示综合整改质效结果,具体评估维度在维护页补录和校验。"
/>
<Panel
title="综合整改质效结果"
description="按高、中、低展示最终整改质效等级;暂按“两个及以上高为高、出现低则低、一个高则中”的初步规则计算,具体维度和规则后续确认。"
>
{summary.qualityRatingDistribution.length > 0 ? (
<ReactECharts option={qualityRatingOption} style={{ height: 380 }} />
) : (
<EmptyBlock title="暂无综合整改质效结果数据" />
)}
</Panel>
</section>
<Panel
title="重点问题分布"
description="按经济损失、外部影响和员工问责统计重点问题数量,点击柱状图查看对应详情列表。"
>
{focusIssueChartData.some((item) => item.value > 0) ? (
<div className="space-y-4">
<ReactECharts
option={focusIssueOption}
onEvents={focusIssueChartEvents}
style={{ height: 320 }}
/>
<div className="flex flex-wrap gap-2">
{focusIssueChartData.map((item) => (
<Button
key={item.key}
outline
onClick={() => setActiveFocusIssueTypeKey(item.key)}
>
{item.shortLabel} {formatCount(item.value)} 条
</Button>
))}
</div>
</div>
) : (
<EmptyBlock title="当前没有相关重点问题" />
)}
</Panel>
</section>
<section className="space-y-5">
<SectionHeading
eyebrow="Execution"
title="问题责任追踪与执行"
description="围绕责任部门看执行难点,快速识别逾期堆积、完成率偏低和预估时长失真的对象。"
/>
<div className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]">
<Panel
title="逾期问题排行"
description="按责任部门统计逾期未完成问题数,用于识别长期挂账和重点督办对象。"
>
{summary.overdueRanking.length > 0 ? (
<ReactECharts option={overdueRankingOption} style={{ height: 340 }} />
) : (
<EmptyBlock title="当前没有逾期问题排行数据" />
)}
</Panel>
<Panel
title="部门整改责任矩阵"
description="X 轴为问题总数,Y 轴为整改完成率,气泡大小表示逾期问题数。"
>
{summary.departmentMatrix.length > 0 ? (
<ReactECharts option={departmentMatrixOption} style={{ height: 340 }} />
) : (
<EmptyBlock title="暂无部门责任矩阵数据" />
)}
</Panel>
</div>
</section>
<section className="space-y-5">
<SectionHeading
eyebrow="Quality"
title="审计整改质效"
description="驾驶舱只展示综合整改质效结果,具体评估维度在维护页补录和校验。"
/>
<Panel
title="综合整改质效结果"
description="按高、中、低展示最终整改质效等级;暂按“两个及以上高为高、出现低则低、一个高则中”的初步规则计算,具体维度和规则后续确认。"
>
{summary.qualityRatingDistribution.length > 0 ? (
<ReactECharts option={qualityRatingOption} style={{ height: 380 }} />
) : (
<EmptyBlock title="暂无综合整改质效结果数据" />
)}
</Panel>
</section>
</>
) : null}
</>
) : null}
<FocusIssueDetailDialog
issueType={activeFocusIssueType}
items={activeFocusIssueItems}
onClose={() => setActiveFocusIssueTypeKey('')}
/>
</div>
);
}
export default ProblemRectificationDashboardPage;
......@@ -38,15 +38,17 @@
## 3. 功能入口
建议在当前项目中保留以下三个一级入口:
建议在当前项目中保留以下四个一级入口:
- 驾驶舱:`/integrity_risk_table/dashboard`
- 业务驾驶舱:`/integrity_risk_table/business_dashboard`
- 问题整改:`/problem_rectification`
- 往期材料归档:`/issue_rectification_archive`
建议交互形态:
- 驾驶舱使用轻量卡片、统计区块、分组标签页和列表卡片
- 驾驶舱仅展示顶部横幅、部门分组切换与宏观总览 KPI 卡片
- 业务驾驶舱承接整改进度分布、问题风险定位、责任追踪与整改质效图表
- 问题整改主列表使用普通数据表格,不再依赖多维表格编辑器
- 新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
- 批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
......@@ -79,20 +81,23 @@
### 3.1 目标页面拆分
为便于当前项目直接重构,建议把模块拆成以下 4 个页面入口:
为便于当前项目直接重构,建议把模块拆成以下 5 个页面入口:
1. `integrity_risk_table/dashboard`
问题整改驾驶舱
2. `problem_rectification`
2. `integrity_risk_table/business_dashboard`
业务驾驶舱
3. `problem_rectification`
问题整改主列表
3. `issue_rectification_archive`
4. `issue_rectification_archive`
归档批次列表
4. `problem_rectification_form`
5. `problem_rectification_form`
外链填写 / 复核 / 最终审核页
对应推荐页面职责:
- 驾驶舱:只做统计、提醒、分组切换,不承接编辑
- 驾驶舱:展示管理层首屏关注的宏观总览,保留部门分组切换,不承接编辑
- 业务驾驶舱:展示后续业务分析图表、责任追踪、重点问题清单与质效分布,保留部门分组切换,不承接编辑
- 主列表:承接查询、筛选、批量操作、详情入口
- 归档页:承接归档批次查看与明细追溯
- 外链流程页:承接填写、复核、最终审核,不与主列表混杂
......@@ -403,7 +408,12 @@
## 9. 驾驶舱展示规则
当前驾驶舱页面以 `problem_rectification` 为唯一数据源,围绕 2026 年整改驾驶舱需求分为四层:
当前驾驶舱能力以 `problem_rectification` 为唯一数据源,并拆分为两个菜单入口:
- `integrity_risk_table/dashboard`:问题整改驾驶舱,仅展示顶部横幅、部门分组切换与宏观总览 KPI
- `integrity_risk_table/business_dashboard`:业务驾驶舱,展示原宏观总览之后的业务分析内容,并继续支持部门分组切换
围绕 2026 年整改驾驶舱需求,整体统计能力分为四层:
1. 宏观总览层
2. 问题分析与风险定位层
......@@ -435,11 +445,24 @@
当前实现约定:
- 顶部继续保留按 `feedback_unit` 分组的轻量 tabs
- tabs 下方分为 KPI 卡片、ECharts 图表区和重点问题清单区
- 驾驶舱仅负责统计展示,不承接编辑动作
- 两个驾驶舱入口顶部均保留按 `feedback_unit` 分组的轻量 tabs
- 两个驾驶舱入口顶部均提供“统计年份”选择器,默认“全部年份”,选择具体年份后仅统计该年份内的问题记录
- 问题整改驾驶舱的 tabs 下方只展示宏观总览 KPI 卡片
- 业务驾驶舱的 tabs 下方展示整改进度分布、业务风险分析、责任追踪和整改质效图表
- 两个驾驶舱均仅负责统计展示,不承接编辑动作
- 如果线上 Subject 后续把新增字段从 `text` 升级为 `select` / `date` / `boolean`,驾驶舱统计口径无需调整,只需复用标准字段值
### 9.1.1 统计年份筛选
年份筛选用于支持管理层按年度查看整改概览和业务分析:
- 默认选项为“全部年份”,不做年度过滤,按当前数据全量统计
- 年份选项从当前已加载的 `problem_rectification` 记录自动提取,按年份倒序展示
- 记录年份优先从检查类日期和业务发生时间推导,依次读取:`inspection_report_date`、`check_the_implementation_time`、`inspection_completed_at`、`the_time_of_rectification`、`actual_rectification_completed_at`
- 如果上述字段都无法识别年份,则回退到记录的 `created_at` / `updated_at`
- 选择年份后,部门分组、宏观 KPI、整改进度、问题风险定位、责任追踪和整改质效统计均基于该年份内记录重新计算
- 如果某年份下没有可展示记录,页面展示空状态,并提示切换其它年份或全部年份
### 9.2 宏观总览层
当前 KPI 统计口径:
......@@ -454,6 +477,7 @@
- 以 `rectification_progress` 为维度统计数量和占比
- 当前页面使用环形图表达,不再沿用旧版折线趋势表达
- 业务驾驶舱仅保留整改进度分布图,不再展示单独的“进度口径说明”卡片
### 9.3 问题分析与风险定位层
......@@ -466,7 +490,8 @@
- 经济损失问题:`has_economic_loss` + `economic_loss_amount`
- 外部影响问题:`has_external_impact` + `penalty_organization`
- 员工问责问题:`has_employee_accountability` + `accountability_person_count`
- 重点问题清单中同时展示 `problem_severity_level`
- 页面以柱状图展示三类重点问题数量;点击柱状图或下方按钮后,通过弹框展示对应详情列表
- 重点问题详情列表中同时展示 `problem_severity_level`、截止日期、反馈单位、标记信息和问题简述
### 9.4 问题责任追踪与执行层
......@@ -477,10 +502,7 @@
- X 轴:责任部门问题总数
- Y 轴:整改完成率
- 气泡大小:逾期问题数
- 整改时效分析散点图:
- X 轴:`the_time_of_rectification - inspection_completed_at`
- Y 轴:`actual_rectification_completed_at - inspection_completed_at`
- 仅统计同时填写了预计整改时间、实际整改完成时间、实际检查完成日期且天数非负的问题
- 业务驾驶舱不再展示单独的“整改时效分析”散点图
当前逾期 / 延期判定说明:
......@@ -701,6 +723,7 @@ const result = await queryUsers(0, 20, {
- `src/pages/problem_rectification/Layout.jsx`
- `src/pages/problem_rectification/dashboard/index.jsx`
- `src/pages/problem_rectification/businessDashboard/BusinessDashboard.jsx`
- `src/pages/problem_rectification/list/index.jsx`
- `src/pages/problem_rectification/archive/index.jsx`
- `src/pages/problem_rectification/form/index.jsx`
......
......@@ -35,6 +35,13 @@ const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']);
const POSITIVE_FLAGS = new Set(['是', '有', '涉及', 'TRUE', '1', 'Y', 'YES']);
const SEVERITY_LEVELS = ['低', '中', '高'];
const RATING_LEVELS = ['低', '中', '高'];
const YEAR_SOURCE_FIELDS = [
'inspection_report_date',
'check_the_implementation_time',
'inspection_completed_at',
'the_time_of_rectification',
'actual_rectification_completed_at',
];
function normalizeText(value) {
return String(value ?? '').trim();
......@@ -81,6 +88,36 @@ function formatDateText(value) {
return /^\d{4}-\d{2}-\d{2}/.test(text) ? text.slice(0, 10) : text;
}
function extractYear(value) {
const text = normalizeText(value);
if (!text) {
return null;
}
const parsed = parseDate(text);
if (parsed) {
return parsed.year();
}
const yearMatch = text.match(/(?:19|20)\d{2}/);
if (!yearMatch) {
return null;
}
return Number(yearMatch[0]);
}
function extractRecordYear(record, metadata) {
for (const field of YEAR_SOURCE_FIELDS) {
const year = extractYear(metadata[field]);
if (year) {
return year;
}
}
return extractYear(record?.created_at) || extractYear(record?.updated_at);
}
function isAffirmative(value) {
return POSITIVE_FLAGS.has(normalizeText(value).toUpperCase());
}
......@@ -161,14 +198,6 @@ function sortByCountDesc(items = []) {
return [...items].sort((left, right) => right.value - left.value);
}
function diffDays(later, earlier) {
if (!later || !earlier) {
return null;
}
return later.startOf('day').diff(earlier.startOf('day'), 'day');
}
function buildDistributionMap(records, getKey) {
const map = new Map();
......@@ -274,19 +303,16 @@ function buildFocusIssueGroups(records) {
economicLoss: records
.filter((record) => record.hasEconomicLoss)
.sort(sortBySeverityAndDueDate)
.slice(0, 6)
.map((record) =>
formatItem(record, record.economicLossAmount ? `损失 ${record.economicLossAmount}` : '已标记经济损失'),
),
externalImpact: records
.filter((record) => record.hasExternalImpact)
.sort(sortBySeverityAndDueDate)
.slice(0, 6)
.map((record) => formatItem(record, record.penaltyOrganization || '已标记外部影响')),
accountability: records
.filter((record) => record.hasEmployeeAccountability)
.sort(sortBySeverityAndDueDate)
.slice(0, 6)
.map((record) =>
formatItem(
record,
......@@ -359,34 +385,6 @@ function buildOverdueRanking(departmentStats) {
).slice(0, 10);
}
function buildTimelinessScatter(records) {
return records
.map((record) => {
const plannedDays = diffDays(record.dueDate, record.inspectionCompletedAt);
const actualDays = diffDays(
record.actualRectificationCompletedAt,
record.inspectionCompletedAt,
);
if (
plannedDays === null ||
actualDays === null ||
plannedDays < 0 ||
actualDays < 0
) {
return null;
}
return {
name: record.title,
value: [plannedDays, actualDays, record.severityLevel || '未评定'],
isDelayed: actualDays > plannedDays,
};
})
.filter(Boolean)
.slice(0, 120);
}
function buildQualityRatingDistribution(records) {
const counts = {
高: 0,
......@@ -429,7 +427,6 @@ function summarizeRecords(records) {
focusIssueGroups: buildFocusIssueGroups(records),
overdueRanking: buildOverdueRanking(departmentStats),
departmentMatrix: buildDepartmentMatrix(departmentStats),
timelinessScatter: buildTimelinessScatter(records),
qualityRatingDistribution: buildQualityRatingDistribution(records),
};
}
......@@ -454,6 +451,7 @@ function normalizeProblemRecord(record) {
const normalizedRecord = {
id: record?._id || record?.name || '',
title: normalizeText(record?.title) || normalizeText(metadata.project_name) || '-',
recordYear: extractRecordYear(record, metadata),
projectName: normalizeText(metadata.project_name),
inspectUnit: normalizeText(metadata.inspect_the_implementing_unit),
feedbackUnit: normalizeText(metadata.feedback_unit),
......@@ -527,6 +525,32 @@ function buildDepartmentBuckets(records) {
return buckets;
}
function buildDashboardScope(records) {
const departmentBuckets = buildDepartmentBuckets(records);
const summaries = Object.fromEntries(
DEPARTMENT_GROUPS.map((group) => [
group.key,
summarizeRecords(departmentBuckets[group.key] || []),
]),
);
return {
departmentBuckets,
summaries,
isEmpty: records.length === 0,
};
}
function buildYearOptions(records) {
return Array.from(
new Set(
records
.map((record) => record.recordYear)
.filter((year) => Number.isInteger(year)),
),
).sort((left, right) => right - left);
}
async function fetchAllProblemRectificationRecords() {
const firstPage = await queryRecords({
subject: PROBLEM_RECTIFICATION_SUBJECT,
......@@ -558,27 +582,31 @@ export async function loadProblemRectificationDashboardData() {
try {
const rawRecords = await fetchAllProblemRectificationRecords();
const records = rawRecords.map(normalizeProblemRecord);
const departmentBuckets = buildDepartmentBuckets(records);
const summaries = Object.fromEntries(
DEPARTMENT_GROUPS.map((group) => [
group.key,
summarizeRecords(departmentBuckets[group.key] || []),
const yearOptions = buildYearOptions(records);
const allScope = buildDashboardScope(records);
const yearScopes = Object.fromEntries(
yearOptions.map((year) => [
String(year),
buildDashboardScope(records.filter((record) => record.recordYear === year)),
]),
);
return {
groups: DEPARTMENT_GROUPS,
records,
departmentBuckets,
summaries,
yearOptions,
yearScopes,
departmentBuckets: allScope.departmentBuckets,
summaries: allScope.summaries,
error: '',
isEmpty: records.length === 0,
isEmpty: allScope.isEmpty,
};
} catch (error) {
return {
groups: DEPARTMENT_GROUPS,
records: [],
yearOptions: [],
yearScopes: {},
departmentBuckets: {},
summaries: {},
error: error?.message || '驾驶舱数据加载失败,请稍后重试。',
......
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