Commit 88f229a6 authored by David Yang's avatar David Yang

fix: bugs

parent dcdc9689
No preview for this file type
...@@ -16,6 +16,8 @@ const ROUTE_PATHS = { ...@@ -16,6 +16,8 @@ const ROUTE_PATHS = {
export default defineConfig({ export default defineConfig({
layout: false, layout: false,
fastRefresh: true, fastRefresh: true,
base: '/problem_rectification_new/',
publicPath: '/problem_rectification_new/',
alias: { alias: {
react: resolvePackageRoot('react'), react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'), 'react-dom': resolvePackageRoot('react-dom'),
......
{
"name": "problem_rectification_new",
"version": "1.0.3",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
"author": "yang"
}
This diff is collapsed.
No preview for this file type
This diff is collapsed.
...@@ -15,25 +15,6 @@ import { ...@@ -15,25 +15,6 @@ import {
isViewAccessible, isViewAccessible,
} from '@/utils/subjectView'; } from '@/utils/subjectView';
function formatDateTime(value) {
if (!value) {
return '未记录';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '未记录';
}
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function CloseIcon() { function CloseIcon() {
return ( return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="size-5"> <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="size-5">
...@@ -141,20 +122,6 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -141,20 +122,6 @@ const RecordDetailDrawer = (props, ref) => {
const activeView = detailViews.find((view) => view.name === activeTabKey) || detailViews[0]; const activeView = detailViews.find((view) => view.name === activeTabKey) || detailViews[0];
const drawerWidth = isFullscreen ? '100vw' : typeof width === 'number' ? `${width}px` : width; const drawerWidth = isFullscreen ? '100vw' : typeof width === 'number' ? `${width}px` : width;
const summaryItems = [
{
label: '主题',
value: subject?.title || subject?.name || '未识别',
},
{
label: '当前视图',
value: activeView?.title || activeView?.name || '基础信息',
},
{
label: '最近更新',
value: formatDateTime(editableRecord?.updated_at),
},
];
const closeDrawer = () => { const closeDrawer = () => {
onClose?.(); onClose?.();
...@@ -176,40 +143,32 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -176,40 +143,32 @@ const RecordDetailDrawer = (props, ref) => {
className="pointer-events-auto h-full transform overflow-hidden border-l border-white/70 bg-[linear-gradient(180deg,#fffdf8_0%,#f8fafc_38%,#f4f4f5_100%)] shadow-[0_24px_80px_rgba(15,23,42,0.24)] ring-1 ring-zinc-950/5 transition duration-300 data-closed:translate-x-full data-enter:ease-out data-leave:ease-in" className="pointer-events-auto h-full transform overflow-hidden border-l border-white/70 bg-[linear-gradient(180deg,#fffdf8_0%,#f8fafc_38%,#f4f4f5_100%)] shadow-[0_24px_80px_rgba(15,23,42,0.24)] ring-1 ring-zinc-950/5 transition duration-300 data-closed:translate-x-full data-enter:ease-out data-leave:ease-in"
> >
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<header className="relative overflow-hidden border-b border-white/70 bg-[linear-gradient(135deg,rgba(255,255,255,0.95),rgba(255,247,237,0.9)_45%,rgba(244,244,245,0.95))] px-6 pb-5 pt-6 shadow-[0_8px_24px_rgba(15,23,42,0.06)] backdrop-blur"> <Headless.DialogTitle className="sr-only">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-rose-200 to-transparent" />
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="text-[11px] font-semibold uppercase tracking-[0.32em] text-rose-500">
Record Detail
</div>
<Headless.DialogTitle className="mt-2 truncate text-[28px] font-semibold tracking-tight text-zinc-950">
{editableRecord?.title || editableRecord?.name || '记录详情'} {editableRecord?.title || editableRecord?.name || '记录详情'}
</Headless.DialogTitle> </Headless.DialogTitle>
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-zinc-500"> <header className="relative border-b border-white/70 bg-white/84 px-6 py-4 shadow-[0_8px_24px_rgba(15,23,42,0.05)] backdrop-blur">
<span className="inline-flex rounded-full bg-white/90 px-3 py-1 font-medium text-zinc-700 ring-1 ring-white shadow-sm"> <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-rose-200 to-transparent" />
{readonly ? '只读模式' : '可编辑'} <div className="flex items-center justify-between gap-4">
</span> <div className="min-w-0 flex-1">
{editableRecord?.status ? ( <div className="flex gap-2 overflow-x-auto">
<span className="inline-flex rounded-full bg-rose-50 px-3 py-1 font-medium text-rose-700 ring-1 ring-rose-100"> {detailViews.map((view) => {
{editableRecord.status} const selected = view.name === activeView?.name;
</span> return (
) : null} <button
</div> key={view.name}
<div className="mt-4 grid gap-3 md:grid-cols-3"> type="button"
{summaryItems.map((item) => ( onClick={() => setActiveTabKey(view.name)}
<div className={clsx(
key={item.label} 'shrink-0 rounded-full px-4 py-2 text-sm font-medium transition',
className="rounded-2xl border border-white/80 bg-white/72 px-4 py-3 shadow-sm ring-1 ring-zinc-950/5" selected
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
: 'bg-white/92 text-zinc-600 ring-1 ring-zinc-200/80 hover:bg-rose-50 hover:text-rose-700',
)}
> >
<div className="text-xs font-medium uppercase tracking-[0.16em] text-zinc-400"> {view.title || view.name}
{item.label} </button>
</div> );
<div className="mt-1 truncate text-sm font-semibold text-zinc-800"> })}
{item.value}
</div>
</div>
))}
</div> </div>
</div> </div>
<div className="flex shrink-0 gap-2 rounded-full border border-white/80 bg-white/88 p-1.5 shadow-sm ring-1 ring-zinc-950/5 backdrop-blur"> <div className="flex shrink-0 gap-2 rounded-full border border-white/80 bg-white/88 p-1.5 shadow-sm ring-1 ring-zinc-950/5 backdrop-blur">
...@@ -231,29 +190,6 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -231,29 +190,6 @@ const RecordDetailDrawer = (props, ref) => {
</button> </button>
</div> </div>
</div> </div>
<div className="mt-5">
<div className="flex gap-2 overflow-x-auto">
{detailViews.map((view) => {
const selected = view.name === activeView?.name;
return (
<button
key={view.name}
type="button"
onClick={() => setActiveTabKey(view.name)}
className={clsx(
'shrink-0 rounded-full px-4 py-2 text-sm font-medium transition',
selected
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
: 'bg-white/92 text-zinc-600 ring-1 ring-zinc-200/80 hover:bg-rose-50 hover:text-rose-700',
)}
>
{view.title || view.name}
</button>
);
})}
</div>
</div>
</header> </header>
<main className="min-h-0 flex-1 overflow-y-auto bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(248,250,252,0.96)_32%,rgba(244,244,245,0.98)_100%)] px-6 py-5"> <main className="min-h-0 flex-1 overflow-y-auto bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(248,250,252,0.96)_32%,rgba(244,244,245,0.98)_100%)] px-6 py-5">
......
...@@ -14,6 +14,7 @@ import RecordDetailDrawer from '@/pages/RecordDetailDrawer'; ...@@ -14,6 +14,7 @@ import RecordDetailDrawer from '@/pages/RecordDetailDrawer';
import { import {
batchRemoveRecords, batchRemoveRecords,
batchUpdateRecords, batchUpdateRecords,
cloneRecord,
createNotice, createNotice,
createRecord, createRecord,
executeAction, executeAction,
...@@ -491,13 +492,16 @@ export default function ProblemRectificationListPage() { ...@@ -491,13 +492,16 @@ export default function ProblemRectificationListPage() {
const [selectedIds, setSelectedIds] = useState([]); const [selectedIds, setSelectedIds] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false); const [showPushModal, setShowPushModal] = useState(false);
const [cloneTargetRecord, setCloneTargetRecord] = useState(null);
const [formValues, setFormValues] = useState({}); const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]); const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]); const [auditorUsers, setAuditorUsers] = useState([]);
const [successMessage, setSuccessMessage] = useState('');
const reloadList = useCallback(async () => { const reloadList = useCallback(async () => {
setLoading(true); setLoading(true);
setError(''); setError('');
setSuccessMessage('');
try { try {
const resources = await loadProblemRectificationListResources(); const resources = await loadProblemRectificationListResources();
...@@ -590,6 +594,7 @@ export default function ProblemRectificationListPage() { ...@@ -590,6 +594,7 @@ export default function ProblemRectificationListPage() {
const pendingAuditCount = filteredRecords.filter( const pendingAuditCount = filteredRecords.filter(
(item) => item.auditStatus && item.auditStatus !== '已审批', (item) => item.auditStatus && item.auditStatus !== '已审批',
).length; ).length;
const quickSelectEnabled = activeGridView?.settings?.quickSelect !== false;
const handleCreate = async () => { const handleCreate = async () => {
if ( if (
...@@ -657,6 +662,34 @@ export default function ProblemRectificationListPage() { ...@@ -657,6 +662,34 @@ export default function ProblemRectificationListPage() {
} }
}; };
const handleClone = async () => {
if (!cloneTargetRecord) {
return;
}
const recordName = cloneTargetRecord.name || cloneTargetRecord.id;
if (!recordName) {
setError('当前记录缺少可复制的 name 标识。');
return;
}
setActionLoading(true);
setError('');
setSuccessMessage('');
try {
await cloneRecord(recordName);
const title = cloneTargetRecord.title || cloneTargetRecord.projectName || cloneTargetRecord.name || '当前记录';
setCloneTargetRecord(null);
await reloadList();
setSuccessMessage(`已复制“${title}”,新记录已加入当前列表。`);
} catch (requestError) {
setError(requestError?.message || '复制问题整改记录失败,请稍后重试。');
} finally {
setActionLoading(false);
}
};
const handleSearchUsers = useCallback(async (keyword) => { const handleSearchUsers = useCallback(async (keyword) => {
const filter = keyword.trim() const filter = keyword.trim()
? { ? {
...@@ -916,9 +949,11 @@ export default function ProblemRectificationListPage() { ...@@ -916,9 +949,11 @@ export default function ProblemRectificationListPage() {
title="问题清单" title="问题清单"
description={`当前视图显示 ${filteredRecords.length} 条记录,已选中 ${selectedIds.length} 条,其中重点后果问题 ${consequenceCount} 条、逾期未完成 ${overdueCount} 条。`} description={`当前视图显示 ${filteredRecords.length} 条记录,已选中 ${selectedIds.length} 条,其中重点后果问题 ${consequenceCount} 条、逾期未完成 ${overdueCount} 条。`}
columns={configuredView.columns || []} columns={configuredView.columns || []}
columnGroups={activeGridView?.settings?.groups || []}
records={filteredRecords} records={filteredRecords}
selectedIds={selectedIds} selectedIds={selectedIds}
onSelectionChange={setSelectedIds} onSelectionChange={setSelectedIds}
selectable={quickSelectEnabled}
toolbarActions={[ toolbarActions={[
{ {
key: 'create', key: 'create',
...@@ -964,6 +999,13 @@ export default function ProblemRectificationListPage() { ...@@ -964,6 +999,13 @@ export default function ProblemRectificationListPage() {
variant: 'plain', variant: 'plain',
onClick: ({ openDetail, record }) => openDetail(record), onClick: ({ openDetail, record }) => openDetail(record),
}, },
{
key: 'clone',
label: '复制',
variant: 'outline',
disabled: () => actionLoading,
onClick: ({ record }) => setCloneTargetRecord(record),
},
{ {
key: 'delete', key: 'delete',
label: '删除', label: '删除',
...@@ -988,6 +1030,7 @@ export default function ProblemRectificationListPage() { ...@@ -988,6 +1030,7 @@ export default function ProblemRectificationListPage() {
states={{ states={{
loading, loading,
error, error,
success: successMessage,
loadingTitle: '正在加载问题整改列表', loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。', loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
emptyTitle: '当前视图暂无数据', emptyTitle: '当前视图暂无数据',
...@@ -1116,6 +1159,31 @@ export default function ProblemRectificationListPage() { ...@@ -1116,6 +1159,31 @@ export default function ProblemRectificationListPage() {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
) : null} ) : null}
{cloneTargetRecord ? (
<Dialog open={Boolean(cloneTargetRecord)} onClose={() => setCloneTargetRecord(null)} size="lg">
<DialogTitle>复制问题整改记录</DialogTitle>
<DialogDescription>
将基于当前记录复制一条新记录,复制完成后会刷新列表。
</DialogDescription>
<DialogBody>
<div className="rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4">
<div className="text-sm font-medium text-slate-900">待复制记录</div>
<div className="mt-2 text-sm leading-6 text-slate-600">
{cloneTargetRecord.title || cloneTargetRecord.projectName || cloneTargetRecord.name || '当前记录'}
</div>
</div>
</DialogBody>
<DialogActions>
<Button outline disabled={actionLoading} onClick={() => setCloneTargetRecord(null)}>
取消
</Button>
<Button color="blue" disabled={actionLoading} onClick={handleClone}>
{actionLoading ? '复制中...' : '确认复制'}
</Button>
</DialogActions>
</Dialog>
) : null}
</> </>
); );
} }
...@@ -56,7 +56,9 @@ ...@@ -56,7 +56,9 @@
问题整改主列表与详情抽屉统一以 `getSubjectView(subjectName)` 返回的视图配置驱动,不再在页面内写死部门标签和字段顺序: 问题整改主列表与详情抽屉统一以 `getSubjectView(subjectName)` 返回的视图配置驱动,不再在页面内写死部门标签和字段顺序:
- `type = "grid"` 的视图用于主列表顶部视图切换,替代原先写死的 tab - `type = "grid"` 的视图用于主列表顶部视图切换,替代原先写死的 tab
- `settings.columns` 决定列表列顺序、宽度和排序优先级 - `settings.columns` 决定列表列顺序、宽度、固定列位置和排序优先级,列表页不得在视图列后追加自定义业务字段列
- `settings.groups` 决定列表列分组,例如把多个问题分类字段归入同一分组;前端需展示分组表头,同一分组下的列即使在 `settings.columns` 中不连续,也应在表格内聚拢展示,并支持每个分组独立折叠 / 展开,折叠后隐藏该分组子列
- `settings.quickSelect` 决定列表是否展示勾选列,用于批量推送、删除等操作
- `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围 - `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围
- `settings.viewAuth` 决定当前用户是否能看到该视图 - `settings.viewAuth` 决定当前用户是否能看到该视图
- `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段 - `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段
...@@ -69,6 +71,8 @@ ...@@ -69,6 +71,8 @@
- `problem_rectification` 与 `issue_rectification_archive` 列表页统一复用 `RecordWorkbench` 作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器 - `problem_rectification` 与 `issue_rectification_archive` 列表页统一复用 `RecordWorkbench` 作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
- 页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作 - 页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
- 详情侧栏统一复用 `RecordDetailDrawer`,通过 `recordName`、`subjectName`、`preferredViewName` 驱动,不再在各页面重复实现抽屉骨架 - 详情侧栏统一复用 `RecordDetailDrawer`,通过 `recordName`、`subjectName`、`preferredViewName` 驱动,不再在各页面重复实现抽屉骨架
- 详情侧栏顶部不展示记录标题、状态和摘要卡片,只保留详情视图切换与全屏 / 关闭操作,字段主体作为主要阅读区域
- 列表行操作列使用图标按钮横向排列,查看、复制、删除等常用操作通过 tooltip / aria-label 标明含义,避免文字按钮纵向堆叠
- 新增同类列表页时,优先复用工作台与详情抽屉组合,避免再次复制问题整改页 JSX - 新增同类列表页时,优先复用工作台与详情抽屉组合,避免再次复制问题整改页 JSX
### 3.1 目标页面拆分 ### 3.1 目标页面拆分
...@@ -134,10 +138,11 @@ ...@@ -134,10 +138,11 @@
旧应用 `problem_rectification` 列表配置中实际暴露了 5 个操作: 旧应用 `problem_rectification` 列表配置中实际暴露了 5 个操作:
1. 查看详情 1. 查看详情
2. 删除数据 2. 复制数据
3. 推送问题整改表 3. 删除数据
4. 导出问题整改表 4. 推送问题整改表
5. 一键归档 5. 导出问题整改表
6. 一键归档
这些操作分别对应如下业务含义: 这些操作分别对应如下业务含义:
...@@ -146,12 +151,19 @@ ...@@ -146,12 +151,19 @@
- 查看问题记录完整字段 - 查看问题记录完整字段
- 支持后续在详情中编辑整改情况、证明材料、审批状态 - 支持后续在详情中编辑整改情况、证明材料、审批状态
### 5.2 删除数据 ### 5.2 复制数据
- 在问题整改列表的单行操作中触发
- 弹窗确认后调用 `cloneRecord(record.name)`,由 `/record/clone` 基于当前记录生成新记录
- 复制完成后刷新当前列表,并展示成功提示
- 复制操作不改变原记录的整改、复核或归档状态
### 5.3 删除数据
- 支持删除单条或批量删除选中项 - 支持删除单条或批量删除选中项
- 本质调用 `/record/batch/remove` - 本质调用 `/record/batch/remove`
### 5.3 推送问题整改表 ### 5.4 推送问题整改表
- 从已选问题记录中选择填报人和审核人 - 从已选问题记录中选择填报人和审核人
- 创建一条 `message_reference_id` 流程主单 - 创建一条 `message_reference_id` 流程主单
...@@ -160,19 +172,19 @@ ...@@ -160,19 +172,19 @@
- 给填报人发送内部通知 - 给填报人发送内部通知
- 创建 OA 待办,链接指向 `/problem_rectification_form?id=<message_record_name>` - 创建 OA 待办,链接指向 `/problem_rectification_form?id=<message_record_name>`
### 5.4 导出问题整改表 ### 5.5 导出问题整改表
- 按选中问题导出 Excel - 按选中问题导出 Excel
- 导出字段来自 `problem_rectification.metadata` - 导出字段来自 `problem_rectification.metadata`
- 本质是把主表字段映射成报表列,不参与状态流转 - 本质是把主表字段映射成报表列,不参与状态流转
### 5.5 一键归档 ### 5.6 一键归档
- 将选中问题记录导入 `problem_rectification_archive_records` - 将选中问题记录导入 `problem_rectification_archive_records`
- 再创建一条 `issue_rectification_archive` 记录,保存归档明细引用列表 - 再创建一条 `issue_rectification_archive` 记录,保存归档明细引用列表
- 归档的本质是生成快照,不应直接依赖原始问题记录后续继续存在 - 归档的本质是生成快照,不应直接依赖原始问题记录后续继续存在
### 5.6 旧多维表格能力盘点 ### 5.7 旧多维表格能力盘点
实际页面检查后,旧系统“问题整改”和“往期材料归档”页都运行在同一套多维表格容器上,页面共有能力包括: 实际页面检查后,旧系统“问题整改”和“往期材料归档”页都运行在同一套多维表格容器上,页面共有能力包括:
...@@ -187,6 +199,7 @@ ...@@ -187,6 +199,7 @@
- 搜索 / 筛选 - 搜索 / 筛选
- 批量勾选 - 批量勾选
- 查看详情 - 查看详情
- 复制数据
- 推送整改表 - 推送整改表
- 导出 - 导出
- 一键归档 - 一键归档
...@@ -444,7 +457,7 @@ ...@@ -444,7 +457,7 @@
当前图表与字段映射: 当前图表与字段映射:
- 问题类型 / 业务领域分布:优先使用 `problem_category_level_1` 至 `problem_category_level_4` 生成矩形树图;若分类未填写,则回退到 `related_issue_domain` - 问题类型 / 业务领域分布:使用 `problem_category_level_1` 至 `problem_category_level_4` 分别生成四个层级 tab,切换后以横向柱状图展示当前层级各分类数量;一级分类缺失时回退到 `related_issue_domain`,其余层级缺失时归入“未分类”
- 问题根本原因分析:使用 `problem_root_cause` 聚合 - 问题根本原因分析:使用 `problem_root_cause` 聚合
- 问题来源对比:以 `problem_from` 为横轴,并按 `inspect_the_implementing_unit` 做堆叠比较 - 问题来源对比:以 `problem_from` 为横轴,并按 `inspect_the_implementing_unit` 做堆叠比较
- 重点问题集中展示: - 重点问题集中展示:
...@@ -479,17 +492,16 @@ ...@@ -479,17 +492,16 @@
当前图表与字段映射: 当前图表与字段映射:
- 问题整改时效:按责任部门计算 `rectification_timeliness_level` 平均分,低=1、中=2、高=3 - 驾驶舱只展示最终“综合整改质效结果”的高 / 中 / 低分布,不再展示整改时间评估、整改证明材料质量、制度修订、系统建设、往期发现、重复出现等明细维度
- 问题整改质量:按责任部门计算 `rectification_material_quality_level` 平均分,低=1、中=2、高=3 - 明细维度仍在维护页录入和校验,包括:
- 问题整改助力总结: - `rectification_timeliness_level`:整改时间评估,低 / 中 / 高
- 制度修订数量:`involves_policy_revision = 是` - `rectification_material_quality_level`:整改证明材料质量,低 / 中 / 高
- 系统建设数量:`involves_system_construction = 是` - `involves_policy_revision`、`policy_revision_name`:是否涉及制度修订及制度名称
- 风险-质量热力图: - `involves_system_construction`、`system_construction_name`:是否涉及系统建设及系统名称
- X 轴:`problem_severity_level` - `is_historical_problem`:是否为往期发现问题
- Y 轴:`rectification_overall_rating` - `is_repeated_problem`:问题是否重复出现
- 部门综合整改评定等级排行: - `rectification_overall_rating`:综合整改评定等级,低 / 中 / 高
- 以责任部门为维度统计 `rectification_overall_rating` 的低/中/高占比 - 当前驾驶舱优先使用已维护的 `rectification_overall_rating`;若该字段为空,则按初步规则从明细维度推导结果:两个及以上高为高;出现低则低;一个高则中。具体参与维度和最终规则后续需业务确认
- 按高评级占比降序展示
--- ---
......
...@@ -35,11 +35,6 @@ const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']); ...@@ -35,11 +35,6 @@ const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']);
const POSITIVE_FLAGS = new Set(['是', '有', '涉及', 'TRUE', '1', 'Y', 'YES']); const POSITIVE_FLAGS = new Set(['是', '有', '涉及', 'TRUE', '1', 'Y', 'YES']);
const SEVERITY_LEVELS = ['低', '中', '高']; const SEVERITY_LEVELS = ['低', '中', '高'];
const RATING_LEVELS = ['低', '中', '高']; const RATING_LEVELS = ['低', '中', '高'];
const SCORE_LEVEL_MAP = {
低: 1,
中: 2,
高: 3,
};
function normalizeText(value) { function normalizeText(value) {
return String(value ?? '').trim(); return String(value ?? '').trim();
...@@ -113,6 +108,42 @@ function isFixedProblem(record) { ...@@ -113,6 +108,42 @@ function isFixedProblem(record) {
return FIXED_PROGRESS_VALUES.has(normalizeText(record?.metadata?.rectification_progress)); return FIXED_PROGRESS_VALUES.has(normalizeText(record?.metadata?.rectification_progress));
} }
function normalizeRatingLevel(value) {
const text = normalizeText(value);
return RATING_LEVELS.includes(text) ? text : '';
}
function calculateQualityRating(record) {
const explicitRating = normalizeRatingLevel(record.overallRating);
if (explicitRating) {
return explicitRating;
}
const dimensions = [
normalizeRatingLevel(record.timelinessLevel),
normalizeRatingLevel(record.materialQualityLevel),
record.involvesPolicyRevision ? '高' : '',
record.involvesSystemConstruction ? '高' : '',
record.isHistoricalProblem ? '低' : '',
record.isRepeatedProblem ? '低' : '',
].filter(Boolean);
if (dimensions.length === 0) {
return '';
}
if (dimensions.includes('低')) {
return '低';
}
const highCount = dimensions.filter((item) => item === '高').length;
if (highCount >= 2) {
return '高';
}
return '中';
}
function extractPrimaryDepartment(metadata) { function extractPrimaryDepartment(metadata) {
const primary = normalizeText(metadata.primary_responsible_department); const primary = normalizeText(metadata.primary_responsible_department);
if (primary) { if (primary) {
...@@ -151,39 +182,16 @@ function buildDistributionMap(records, getKey) { ...@@ -151,39 +182,16 @@ function buildDistributionMap(records, getKey) {
); );
} }
function upsertTreeNode(nodes, name) { function buildCategoryDistributions(records) {
let existing = nodes.find((node) => node.name === name); return {
if (!existing) { level1: buildDistributionMap(
existing = { name, value: 0, children: [] }; records,
nodes.push(existing); (item) => item.categoryLevel1 || item.relatedIssueDomain || '未分类',
} ),
existing.value += 1; level2: buildDistributionMap(records, (item) => item.categoryLevel2 || '未分类'),
return existing; level3: buildDistributionMap(records, (item) => item.categoryLevel3 || '未分类'),
} level4: buildDistributionMap(records, (item) => item.categoryLevel4 || '未分类'),
};
function buildCategoryTreemap(records) {
const root = [];
records.forEach((record) => {
const path = [
record.categoryLevel1,
record.categoryLevel2,
record.categoryLevel3,
record.categoryLevel4,
].filter(Boolean);
if (path.length === 0) {
path.push(record.relatedIssueDomain || '未分类');
}
let currentNodes = root;
path.forEach((name) => {
const node = upsertTreeNode(currentNodes, name);
currentNodes = node.children;
});
});
return root;
} }
function buildSourceComparison(records) { function buildSourceComparison(records) {
...@@ -306,15 +314,6 @@ function collectDepartmentStats(records) { ...@@ -306,15 +314,6 @@ function collectDepartmentStats(records) {
totalCount: 0, totalCount: 0,
fixedCount: 0, fixedCount: 0,
overdueCount: 0, overdueCount: 0,
timelinessScoreTotal: 0,
timelinessScoreCount: 0,
qualityScoreTotal: 0,
qualityScoreCount: 0,
ratingCounts: {
低: 0,
中: 0,
高: 0,
},
}); });
} }
...@@ -328,36 +327,12 @@ function collectDepartmentStats(records) { ...@@ -328,36 +327,12 @@ function collectDepartmentStats(records) {
if (record.isOverdue) { if (record.isOverdue) {
stats.overdueCount += 1; stats.overdueCount += 1;
} }
const timelinessScore = SCORE_LEVEL_MAP[record.timelinessLevel];
if (timelinessScore) {
stats.timelinessScoreTotal += timelinessScore;
stats.timelinessScoreCount += 1;
}
const qualityScore = SCORE_LEVEL_MAP[record.materialQualityLevel];
if (qualityScore) {
stats.qualityScoreTotal += qualityScore;
stats.qualityScoreCount += 1;
}
if (record.overallRating && stats.ratingCounts[record.overallRating] !== undefined) {
stats.ratingCounts[record.overallRating] += 1;
}
}); });
}); });
return Array.from(departmentMap.values()).map((item) => ({ return Array.from(departmentMap.values()).map((item) => ({
...item, ...item,
completionRate: item.totalCount > 0 ? (item.fixedCount / item.totalCount) * 100 : 0, completionRate: item.totalCount > 0 ? (item.fixedCount / item.totalCount) * 100 : 0,
timelinessAverage:
item.timelinessScoreCount > 0
? item.timelinessScoreTotal / item.timelinessScoreCount
: 0,
qualityAverage:
item.qualityScoreCount > 0
? item.qualityScoreTotal / item.qualityScoreCount
: 0,
})); }));
} }
...@@ -412,58 +387,25 @@ function buildTimelinessScatter(records) { ...@@ -412,58 +387,25 @@ function buildTimelinessScatter(records) {
.slice(0, 120); .slice(0, 120);
} }
function buildScoreRanking(departmentStats, key) { function buildQualityRatingDistribution(records) {
return [...departmentStats] const counts = {
.filter((item) => item[key] > 0) 高: 0,
.sort((left, right) => right[key] - left[key]) 中: 0,
.map((item) => ({ 低: 0,
name: item.name,
value: Number(item[key].toFixed(2)),
}));
}
function buildSeverityRatingHeatmap(records) {
const matrix = [];
RATING_LEVELS.forEach((rating, yIndex) => {
SEVERITY_LEVELS.forEach((severity, xIndex) => {
const count = records.filter(
(record) =>
record.severityLevel === severity && record.overallRating === rating,
).length;
matrix.push([xIndex, yIndex, count]);
});
});
return {
xLabels: SEVERITY_LEVELS,
yLabels: RATING_LEVELS,
data: matrix,
}; };
}
function buildDepartmentRatingDistribution(departmentStats) { records.forEach((record) => {
return [...departmentStats] if (record.qualityRating && counts[record.qualityRating] !== undefined) {
.filter((item) => item.totalCount > 0) counts[record.qualityRating] += 1;
.map((item) => { }
const high = item.ratingCounts.高 || 0; });
const medium = item.ratingCounts.中 || 0;
const low = item.ratingCounts.低 || 0;
const ratedTotal = high + medium + low;
return { return ['高', '中', '低']
name: item.name, .map((level) => ({
low, name: level,
medium, value: counts[level],
high, }))
lowPercent: ratedTotal > 0 ? (low / ratedTotal) * 100 : 0, .filter((item) => item.value > 0);
mediumPercent: ratedTotal > 0 ? (medium / ratedTotal) * 100 : 0,
highPercent: ratedTotal > 0 ? (high / ratedTotal) * 100 : 0,
highShare: ratedTotal > 0 ? high / ratedTotal : 0,
};
})
.sort((left, right) => right.highShare - left.highShare);
} }
function summarizeRecords(records) { function summarizeRecords(records) {
...@@ -481,19 +423,14 @@ function summarizeRecords(records) { ...@@ -481,19 +423,14 @@ function summarizeRecords(records) {
overdueCount, overdueCount,
overdueRate, overdueRate,
progressDistribution: buildDistributionMap(records, (item) => item.progress || '未填写'), progressDistribution: buildDistributionMap(records, (item) => item.progress || '未填写'),
categoryTreemap: buildCategoryTreemap(records), categoryDistributions: buildCategoryDistributions(records),
rootCauseDistribution: buildDistributionMap(records, (item) => item.rootCause || '未分类原因'), rootCauseDistribution: buildDistributionMap(records, (item) => item.rootCause || '未分类原因'),
sourceComparison: buildSourceComparison(records), sourceComparison: buildSourceComparison(records),
focusIssueGroups: buildFocusIssueGroups(records), focusIssueGroups: buildFocusIssueGroups(records),
overdueRanking: buildOverdueRanking(departmentStats), overdueRanking: buildOverdueRanking(departmentStats),
departmentMatrix: buildDepartmentMatrix(departmentStats), departmentMatrix: buildDepartmentMatrix(departmentStats),
timelinessScatter: buildTimelinessScatter(records), timelinessScatter: buildTimelinessScatter(records),
timelinessScoreRanking: buildScoreRanking(departmentStats, 'timelinessAverage'), qualityRatingDistribution: buildQualityRatingDistribution(records),
qualityScoreRanking: buildScoreRanking(departmentStats, 'qualityAverage'),
policyRevisionCount: records.filter((item) => item.involvesPolicyRevision).length,
systemConstructionCount: records.filter((item) => item.involvesSystemConstruction).length,
severityRatingHeatmap: buildSeverityRatingHeatmap(records),
departmentRatingDistribution: buildDepartmentRatingDistribution(departmentStats),
}; };
} }
...@@ -514,7 +451,7 @@ function normalizeProblemRecord(record) { ...@@ -514,7 +451,7 @@ function normalizeProblemRecord(record) {
const delayedByPending = dueDate && dueDate.isBefore(now, 'day') && !isFixed; const delayedByPending = dueDate && dueDate.isBefore(now, 'day') && !isFixed;
const isDelayed = Boolean(delayedByProgress || delayedByActual || delayedByPending); const isDelayed = Boolean(delayedByProgress || delayedByActual || delayedByPending);
return { const normalizedRecord = {
id: record?._id || record?.name || '', id: record?._id || record?.name || '',
title: normalizeText(record?.title) || normalizeText(metadata.project_name) || '-', title: normalizeText(record?.title) || normalizeText(metadata.project_name) || '-',
projectName: normalizeText(metadata.project_name), projectName: normalizeText(metadata.project_name),
...@@ -562,6 +499,11 @@ function normalizeProblemRecord(record) { ...@@ -562,6 +499,11 @@ function normalizeProblemRecord(record) {
isDelayed, isDelayed,
isOverdue: delayedByPending, isOverdue: delayedByPending,
}; };
return {
...normalizedRecord,
qualityRating: calculateQualityRating(normalizedRecord),
};
} }
function buildDepartmentBuckets(records) { function buildDepartmentBuckets(records) {
......
...@@ -22,13 +22,6 @@ import { ...@@ -22,13 +22,6 @@ import {
export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification'; export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']); const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']);
const LEVEL_ORDER = ['高', '中', '低']; const LEVEL_ORDER = ['高', '中', '低'];
const WORKBENCH_ENHANCED_COLUMN_FIELDS = [
'problem_severity_level',
'rectification_overall_rating',
'primary_responsible_department',
'problem_root_cause',
'actual_rectification_completed_at',
];
const WORKBENCH_PRIORITY_CREATE_FIELD_NAMES = [ const WORKBENCH_PRIORITY_CREATE_FIELD_NAMES = [
'problem_severity_level', 'problem_severity_level',
'problem_category_level_1', 'problem_category_level_1',
...@@ -263,28 +256,9 @@ export function buildGridViews(views = []) { ...@@ -263,28 +256,9 @@ export function buildGridViews(views = []) {
} }
export function buildViewColumnsForList(view, subject) { export function buildViewColumnsForList(view, subject) {
const baseColumns = buildViewColumns(view, subject).filter( return buildViewColumns(view, subject).filter(
(column) => column.field !== 'title' || !column.hide, (column) => column.field !== 'title' || !column.hide,
); );
const existingFields = new Set(baseColumns.map((column) => getFieldNameFromPath(column.field)));
const extraColumns = WORKBENCH_ENHANCED_COLUMN_FIELDS
.filter((fieldName) => !existingFields.has(fieldName))
.map((fieldName) => {
const fieldDef = findSubjectField(subject, fieldName);
if (!fieldDef) {
return null;
}
return {
field: `metadata.${fieldDef.name}`,
label: fieldDef.label || fieldDef.name,
width: 168,
fieldDef,
};
})
.filter(Boolean);
return [...baseColumns, ...extraColumns];
} }
export function getRecordCellText(record, column) { export function getRecordCellText(record, column) {
......
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