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" />;
}
......@@ -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