pms-front-react/src/pages/workAppraisal/AppraisalModuleDetailPage.tsx

368 lines
12 KiB
TypeScript
Raw Normal View History

2026-03-17 07:18:07 +00:00
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Alert, Collapse, Empty, Form, Input, Select, Spin, message } from 'antd';
import type { CollapseProps } from 'antd';
import { getTaskModelSet } from '@/api/appraisal';
import { useSearchParams } from 'react-router-dom';
import PageBackButton from '@/components/PageBackButton';
import './appraisal-module-detail.css';
interface ScoreConfigItem {
_itemKey: string;
id?: string | number;
reviewType?: string | number;
reviewCategory?: string;
reviewItem?: string;
remarks?: string;
weight?: number;
[key: string]: unknown;
}
interface ScoreCategory {
key: string;
title: string;
type: string;
rightArr: ScoreConfigItem[];
weight: number;
}
interface ScoreGroup {
type: string;
title: string;
list: ScoreCategory[];
weight: number;
}
const REVIEW_GROUP_META = [
{ type: '0', title: '组长评估绩效指标' },
{ type: '1', title: '个人自评绩效指标' },
{ type: '2', title: '系统核算绩效指标' },
];
const TEMPLATE_TYPE_OPTIONS = [
{ label: '年度考核', value: '0' },
{ label: '季度考核', value: '1' },
{ label: '月度考核', value: '2' },
];
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const toNumber = (value: unknown, fallback = 0) => {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
};
const clampWeight = (value: unknown) => Math.min(20, Math.max(0, Math.round(toNumber(value, 0))));
const normalizeResponseData = (response: unknown) =>
isObject(response) && response.data !== undefined ? response.data : response;
const normalizeScoreList = (response: unknown): ScoreConfigItem[] => {
const source = normalizeResponseData(response);
if (!Array.isArray(source)) {
return [];
}
return source
.filter((item) => isObject(item))
.map((item, index) => ({
...item,
_itemKey: String(item.id ?? `cfg_${index}`),
weight: clampWeight(item.weight),
})) as ScoreConfigItem[];
};
const buildScoreGroups = (items: ScoreConfigItem[]): ScoreGroup[] => {
const typeMap = new Map<string, ScoreConfigItem[]>();
items.forEach((item) => {
const type = String(item.reviewType ?? '');
const current = typeMap.get(type) ?? [];
current.push(item);
typeMap.set(type, current);
});
return REVIEW_GROUP_META.map(({ type, title }) => {
const categoryMap = new Map<string, ScoreConfigItem[]>();
(typeMap.get(type) ?? []).forEach((item) => {
const categoryTitle = String(item.reviewCategory ?? '未分类');
const current = categoryMap.get(categoryTitle) ?? [];
current.push(item);
categoryMap.set(categoryTitle, current);
});
const list: ScoreCategory[] = Array.from(categoryMap.entries()).map(([categoryTitle, rightArr]) => ({
key: `${type}_${categoryTitle}`,
title: categoryTitle,
type,
rightArr,
weight: rightArr.reduce((sum, cfg) => sum + clampWeight(cfg.weight), 0),
}));
return {
type,
title,
list,
weight: list.reduce((sum, category) => sum + category.weight, 0),
};
}).filter((group) => group.list.length > 0);
};
interface WeightBarProps {
value: number;
}
const WeightBar = ({ value }: WeightBarProps) => {
const normalized = clampWeight(value);
const leftPercent = normalized * 5;
const bubbleClass =
normalized === 0
? 'module-score-text is-start'
: normalized === 20
? 'module-score-text is-end'
: 'module-score-text';
return (
<div className="module-score-wrap">
<div className="module-score-scale">
<span>0</span>
{normalized !== 20 && <span>20</span>}
</div>
<div className="module-score-track">
<div className="module-score-fill" style={{ width: `${leftPercent}%` }} />
{normalized > 0 && (
<div className={bubbleClass} style={{ left: `${leftPercent}%` }}>
{normalized}
</div>
)}
</div>
</div>
);
};
const AppraisalModuleDetailPage = () => {
const [searchParams] = useSearchParams();
const moduleId = searchParams.get('id') ?? '';
const [moduleName, setModuleName] = useState('');
const [moduleType, setModuleType] = useState<string>();
const [loading, setLoading] = useState(false);
const [scoreList, setScoreList] = useState<ScoreGroup[]>([]);
const [activeGroupTitle, setActiveGroupTitle] = useState('');
const [selectedCategoryKey, setSelectedCategoryKey] = useState('');
const [viewMode, setViewMode] = useState<'group' | 'category'>('group');
useEffect(() => {
setModuleName(searchParams.get('moduleName') ?? '');
const currentType = String(searchParams.get('moduleType') ?? '').trim();
setModuleType(currentType || undefined);
}, [searchParams]);
const loadDetail = useCallback(async () => {
if (!moduleId) {
setScoreList([]);
setActiveGroupTitle('');
setSelectedCategoryKey('');
setViewMode('group');
return;
}
setLoading(true);
try {
const response = await getTaskModelSet(moduleId);
const groups = buildScoreGroups(normalizeScoreList(response));
setScoreList(groups);
if (groups.length > 0) {
setActiveGroupTitle(groups[0].title);
setSelectedCategoryKey(groups[0].list[0]?.key ?? '');
setViewMode('group');
} else {
setActiveGroupTitle('');
setSelectedCategoryKey('');
setViewMode('group');
}
} catch (error) {
console.error('Failed to fetch appraisal module detail:', error);
message.error('获取考核看板详情失败');
setScoreList([]);
setActiveGroupTitle('');
setSelectedCategoryKey('');
setViewMode('group');
} finally {
setLoading(false);
}
}, [moduleId]);
useEffect(() => {
void loadDetail();
}, [loadDetail]);
const totalWeight = useMemo(
() => scoreList.reduce((sum, group) => sum + toNumber(group.weight, 0), 0),
[scoreList],
);
const activeGroup = useMemo(() => {
if (scoreList.length === 0) {
return undefined;
}
return scoreList.find((group) => group.title === activeGroupTitle) ?? scoreList[0];
}, [activeGroupTitle, scoreList]);
const selectedCategory = useMemo(() => {
if (!activeGroup) {
return undefined;
}
return activeGroup.list.find((category) => category.key === selectedCategoryKey) ?? activeGroup.list[0];
}, [activeGroup, selectedCategoryKey]);
const rightCategoryList = useMemo(() => {
if (!activeGroup) {
return [] as ScoreCategory[];
}
if (viewMode === 'group') {
return activeGroup.list;
}
return selectedCategory ? [selectedCategory] : [];
}, [activeGroup, selectedCategory, viewMode]);
const handleCollapseChange: CollapseProps['onChange'] = (key) => {
const keyText = Array.isArray(key) ? String(key[0] ?? '') : String(key ?? '');
const nextTitle = keyText || scoreList[0]?.title || '';
setActiveGroupTitle(nextTitle);
setViewMode('group');
const group = scoreList.find((item) => item.title === nextTitle);
if (!group || group.list.length === 0) {
setSelectedCategoryKey('');
return;
}
setSelectedCategoryKey((previous) =>
group.list.some((category) => category.key === previous) ? previous : group.list[0].key,
);
};
const collapseItems: CollapseProps['items'] = scoreList.map((group) => ({
key: group.title,
label: (
<div className="module-content-title">
<span className="module-set-title">{group.title}</span>
<span className="module-status-text">{group.weight}%</span>
</div>
),
children: (
<div>
{group.list.map((category) => {
const selected = activeGroup?.title === group.title && selectedCategoryKey === category.key;
return (
<button
key={category.key}
type="button"
className={`module-left-sub ${selected ? 'is-selected' : ''}`}
onClick={() => {
setActiveGroupTitle(group.title);
setSelectedCategoryKey(category.key);
setViewMode('category');
}}
>
<span className="module-left-sub-title">{category.title}</span>
<span className="module-status-text">{category.weight}%</span>
</button>
);
})}
</div>
),
}));
return (
<div className="app-container appraisal-module-detail-page">
2026-03-17 07:18:56 +00:00
<div className="appraisal-module-back">
2026-03-17 07:18:07 +00:00
<PageBackButton fallbackPath="/workAppraisal/taskModule" />
</div>
2026-03-17 07:18:56 +00:00
<div className="appraisal-module-hero">
<div>
<div className="appraisal-module-kicker">PERFORMANCE MODULE</div>
<div className="appraisal-module-title"></div>
<div className="appraisal-module-subtitle"></div>
</div>
<div className="appraisal-module-summary">
<span></span>
<strong>{TEMPLATE_TYPE_OPTIONS.find((item) => item.value === moduleType)?.label ?? '-'}</strong>
</div>
</div>
2026-03-17 07:18:07 +00:00
<div className="module-detail-header">
<Form layout="inline">
<Form.Item label="看板名称">
<Input value={moduleName} readOnly style={{ width: 300 }} />
</Form.Item>
<Form.Item label="看板类型">
<Select
value={moduleType}
disabled
style={{ width: 200 }}
placeholder="看板类型"
options={TEMPLATE_TYPE_OPTIONS}
/>
</Form.Item>
</Form>
</div>
{!moduleId && <Alert type="warning" showIcon message="缺少看板ID参数无法加载看板详情" style={{ marginBottom: 12 }} />}
<Spin spinning={loading}>
<div className="module-detail-layout">
<div className="module-detail-left">
<div className="module-set-text"></div>
<Collapse
accordion
activeKey={activeGroup?.title}
onChange={handleCollapseChange}
items={collapseItems}
className="module-collapse"
/>
<div className="module-total-box">
<span className="module-set-title"></span>
<span className="module-status-text">{totalWeight}%</span>
</div>
</div>
<div className="module-detail-right">
{rightCategoryList.length === 0 ? (
<Empty description="暂无指标配置" />
) : (
rightCategoryList.map((category) => (
<div key={category.key} className="module-category-box">
<div className="module-category-title">{category.title}</div>
<div className="module-set-header">
<div className="module-header-item is-name"></div>
<div className="module-header-item is-remark"></div>
<div className="module-header-item is-weight"></div>
</div>
{category.rightArr.map((item) => (
<div key={item._itemKey} className="module-content-row">
<div className="module-row-item is-name">{String(item.reviewItem ?? '-')}</div>
<div className="module-row-item is-remark">{String(item.remarks ?? '-')}</div>
<div className="module-row-item is-weight">
<WeightBar value={toNumber(item.weight, 0)} />
</div>
</div>
))}
</div>
))
)}
</div>
</div>
</Spin>
</div>
);
};
export default AppraisalModuleDetailPage;