| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052 |
- /*
- * @Author: code4eat awesomedema@gmail.com
- * @Date: 2025-09-30 00:00:00
- * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-10-17 11:57:04
- * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/serviceEvaluate/index.tsx
- * @Description: 服务评价管理 - 左树右表 + 可展开选项子表
- */
- import React, { useEffect, useMemo, useState } from 'react';
- import './style.less';
- import { Button, Input, InputNumber, message, Modal, Popconfirm, Tooltip, Transfer, Tree } from 'antd';
- import { ProColumns } from '@ant-design/pro-table';
- import { ModalForm, ProFormDigit, ProFormText } from '@ant-design/pro-form';
- import KCTable from '@/components/kcTable';
- import DirectoryTree from 'antd/es/tree/DirectoryTree';
- import { DataNode } from 'antd/es/tree';
- import type { TransferDirection } from 'antd/es/transfer';
- import { createFromIconfontCN } from '@ant-design/icons';
- import expandedIcon from '../../../../../public/images/treenode_open.png';
- import closeIcon from '../../../../../public/images/treenode_collapse.png';
- import { KCInput } from '@/components/KCInput';
- import { getDictByDictTypeAndSysid } from '@/service/dictionary';
- import {
- getCategoryTree,
- addCategory,
- getProjectList,
- getOptionList,
- addProject,
- delProject,
- addOption,
- delOption,
- updateProjectWeight,
- updateOptionScore,
- CategoryNode,
- ProjectItem,
- OptionItem,
- SystemTreeNode,
- getSystemListForModal,
- addSystems,
- getSelectedSystemList,
- addServiceTypes,
- getServiceEvaluationItem,
- getEvaluationSelect,
- addEvaluationItem,
- addEvaluationSelect,
- editEvaluationSelect,
- deleteEvaluationItem,
- deleteEvaluationSelect,
- } from './service';
- const IconFont = createFromIconfontCN({
- scriptUrl: '/zhongtaiC.js',
- });
- // 子表格:选项列表
- const OptionsList: React.FC<{
- project: ProjectItem;
- options: OptionItem[];
- onAdd: (data: { optionCode: string; optionName: string; score: number }) => Promise<void>;
- onDelete: (optionId: string) => Promise<void>;
- onScoreChange: (optionId: string, score: number) => Promise<void>;
- }> = ({ project, options, onAdd, onDelete, onScoreChange }) => {
- return (
- <div className="SE-OptionsWrap">
- <div className="SE-OptionsHeader">
- <span className="SE-SubTitle">{project.projectName} - 选项</span>
- <ModalForm
- title="新增选项"
- width={520}
- trigger={<Button type="link">新增选项</Button>}
- modalProps={{ destroyOnClose: true }}
- onFinish={async (values) => {
- await onAdd(values as any);
- return true;
- }}
- >
- <ProFormText name="optionCode" label="选项代码" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
- <ProFormText name="optionName" label="选项名称" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
- <ProFormDigit name="score" label="选项得分" placeholder="请输入" min={0} max={9999} rules={[{ required: true, message: '必填' }]} />
- </ModalForm>
- </div>
- <div className="SE-OptionsTable">
- <div className="SE-OptionsRow SE-OptionsHead">
- <div className="col idx">序号</div>
- <div className="col code">选项代码</div>
- <div className="col name">选项名称</div>
- <div className="col score">选项得分</div>
- <div className="col act">操作</div>
- </div>
- {options && options.length > 0 ? (
- options.map((opt, index) => (
- <div className="SE-OptionsRow" key={opt.optionId}>
- <div className="col idx">{index + 1}</div>
- <div className="col code">{opt.optionCode}</div>
- <div className="col name">{opt.optionName}</div>
- <div className="col score">
- <InputNumber
- min={0}
- max={9999}
- value={opt.score}
- onChange={(val) => {
- const v = typeof val === 'number' ? val : Number(val);
- if (!Number.isNaN(v)) onScoreChange(opt.optionId, v);
- }}
- />
- </div>
- <div className="col act">
- <Popconfirm title="是否确认删除?" onConfirm={() => onDelete(opt.optionId)}>
- <a>删除</a>
- </Popconfirm>
- </div>
- </div>
- ))
- ) : (
- <div className="SE-Empty">暂无选项</div>
- )}
- </div>
- </div>
- );
- };
- const ServiceEvaluatePage: React.FC = () => {
- // 左侧分类树
- const [categoryTree, set_categoryTree] = useState<CategoryNode[]>([]);
- const [currentCategory, set_currentCategory] = useState<CategoryNode | undefined>();
- const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
- const [autoExpandParent, setAutoExpandParent] = useState(true);
- const [searchValue, setSearchValue] = useState('');
- // 主表相关
- const [reload, set_reload] = useState(false);
- const [tableParams, set_tableParams] = useState<{ current: number; pageSize: number; categoryId?: string }>({ current: 1, pageSize: 10 });
- const [tableKey, set_tableKey] = useState(0); // 用于强制刷新表格
- const [expandedRowKeys, set_expandedRowKeys] = useState<React.Key[]>([]); // 追踪展开的行
- // 展开区缓存选项数据,避免频繁请求
- const [projectOptionsMap, set_projectOptionsMap] = useState<Record<string, OptionItem[]>>({});
- // 项目权重编辑态(保持原交互:编辑/勾选)
- const [editingProjectId, set_editingProjectId] = useState<string | null>(null);
- const [editingProjectWeight, set_editingProjectWeight] = useState<number>(0);
- // 添加系统弹窗
- const [sysModalOpen, set_sysModalOpen] = useState(false);
- const [systemTree, set_systemTree] = useState<SystemTreeNode[]>([]);
- const [sysTargetKeys, set_sysTargetKeys] = useState<string[]>([]);
- const [sysSelectedKeys, set_sysSelectedKeys] = useState<string[]>([]);
- const [sysExpandedKeys, set_sysExpandedKeys] = useState<string[]>([]);
- // 服务项弹窗
- const [serviceModalOpen, set_serviceModalOpen] = useState(false);
- const [serviceDict, set_serviceDict] = useState<{ key: string; title: string }[]>([]);
- const [serviceSelected, set_serviceSelected] = useState<string[]>([]);
- const [serviceBindSystemId, set_serviceBindSystemId] = useState<string>('');
- const [serviceSaving, set_serviceSaving] = useState(false);
- // 服务节点展开行:缓存“评价项 -> 选项”数据(与 /centerSys/evaluation/getEvaluationSelect 返回结构一致)
- const [serviceOptionsMap, set_serviceOptionsMap] = useState<
- Record<string, { id: number | string; hospId?: string; itemCode: string; code: string; name: string; value: number; sort: number }[]>
- >({});
- // 评价项目弹窗(右侧“添加”)
- const [evalItemModalOpen, set_evalItemModalOpen] = useState(false);
- const [evalItemDict, set_evalItemDict] = useState<{ key: string; title: string }[]>([]);
- const [evalItemTargetKeys, set_evalItemTargetKeys] = useState<string[]>([]);
- const [evalItemSelectedKeys, set_evalItemSelectedKeys] = useState<string[]>([]);
- // 评价项权重编辑态
- const [editingEvalCode, set_editingEvalCode] = useState<string | null>(null);
- const [editingEvalValue, set_editingEvalValue] = useState<number>(0);
- // 选项弹窗(评价项 -> 选项)
- const [selectModalOpen, set_selectModalOpen] = useState(false);
- const [selectDict, set_selectDict] = useState<{ key: string; title: string }[]>([]);
- const [selectTargetKeys, set_selectTargetKeys] = useState<string[]>([]);
- const [selectSelectedKeys, set_selectSelectedKeys] = useState<string[]>([]);
- const [bindItemCode, set_bindItemCode] = useState<string>('');
- // 服务节点主表数据缓存(用于拖拽排序后保持展示顺序)
- const [evalItemRows, set_evalItemRows] = useState<any[]>([]);
- const [draggingMainKey, set_draggingMainKey] = useState<string | null>(null);
- const [draggingOverMainKey, set_draggingOverMainKey] = useState<string | null>(null);
- const [useLocalEvalOrder, set_useLocalEvalOrder] = useState<boolean>(false);
- // 嵌套选项拖拽的中间态
- const [draggingOption, set_draggingOption] = useState<{ parent: string; key: string } | null>(null);
- const [draggingOverOption, set_draggingOverOption] = useState<{ parent: string; key: string } | null>(null);
- // 工具:移动数组元素
- const moveItem = <T,>(list: T[], from: number, to: number): T[] => {
- const next = [...list];
- const [m] = next.splice(from, 1);
- next.splice(to, 0, m);
- return next;
- };
- // 获取左侧“已选系统”并渲染为单层树
- const fetchTree = async () => {
- const list = await getSelectedSystemList();
- const tree: any[] = (list || []).map((item: any) => ({
- id: String(item.systemId),
- name: String(item.systemName),
- nodeType: 'system',
- selectable: false,
- // 将 serviceList 映射为子节点
- children: (item.serviceList || []).map((s: any) => ({
- id: String(s.id),
- name: String(s.name),
- code: String(s.code || ''),
- nodeType: 'service',
- selectable: true,
- })),
- }));
- set_categoryTree(tree as any);
- // 默认选中第一个服务;若无服务则只展开系统,不触发右侧
- if (tree.length > 0) {
- const sys = tree[0];
- if (sys.children && sys.children.length > 0) {
- const firstChild = sys.children[0];
- set_currentCategory(firstChild as any);
- set_tableParams((p) => ({ ...p, current: 1, categoryId: firstChild.id }));
- set_reload(true);
- set_editingEvalCode(null);
- // 服务节点:首次加载右侧评价项
- if ((firstChild as any).nodeType === 'service') {
- await loadServiceEvalItems(String((firstChild as any).code || (firstChild as any).name));
- }
- }
- if (sys.children && sys.children.length > 0) {
- setExpandedKeys([sys.id]);
- setAutoExpandParent(true);
- }
- }
- };
- // 获取系统树(弹窗数据源)
- const fetchSystemTree = async () => {
- const { systemList, selectSystemList } = await getSystemListForModal();
- set_systemTree(systemList || []);
- // 默认已选
- set_sysTargetKeys((selectSystemList || []).map((v)=> String(v)));
- // 默认全部展开
- const allKeys: string[] = [];
- const walk = (list: SystemTreeNode[]) => { list.forEach(n=>{ allKeys.push(String(n.code)); if (n.children && n.children.length) walk(n.children); }); };
- walk(systemList || []);
- set_sysExpandedKeys(allKeys);
- };
- // 获取服务项字典
- const fetchServiceDict = async () => {
- // 使用中心系统ID:1547394914533380096 获取指定字典类型
- const resp: any = await getDictByDictTypeAndSysid('1547394914533380096', 'EVALUATION_SERVICE');
- const list = (resp?.dataVoList || []).map((x: any) => ({ key: String(x.code || x.value), title: String(x.name) }));
- set_serviceDict(list);
- };
- // 获取评价项目字典
- const fetchEvaluationItemsDict = async () => {
- const resp: any = await getDictByDictTypeAndSysid('1547394914533380096', 'EVALUATION_ITEM');
- const list = (resp?.dataVoList || []).map((x: any) => ({ key: String(x.code || x.value), title: String(x.name) }));
- set_evalItemDict(list);
- };
- // 获取“选项”字典
- const fetchEvaluationSelectDict = async () => {
- const resp: any = await getDictByDictTypeAndSysid('1547394914533380096', 'EVALUATION_SELECT');
- const list = (resp?.dataVoList || []).map((x: any) => ({ key: String(x.code || x.value), title: String(x.name) }));
- set_selectDict(list);
- };
- // 拉取服务类型下的评价项(用于 dataSource 渲染 & 拖拽)
- const loadServiceEvalItems = async (serviceCode: string) => {
- const list = (await getServiceEvaluationItem(serviceCode)) || [];
- const normalizePercent = (v: any): number => {
- const n = Number(v);
- return Number.isFinite(n) && n >= 0 ? n : 0;
- };
- const mapped = (list || []).map((it: any, idx: number) => ({
- id: it.id, // 保留 id 字段,用于删除等操作
- itemCode: it.itemCode,
- itemName: it.itemName,
- percent: normalizePercent(it.percent),
- sort: it.sort ?? idx + 1,
- }));
- set_evalItemRows(mapped);
- set_useLocalEvalOrder(true);
- };
- // 获取表格数据:服务节点 → 评价项;系统/分类节点 → 项目列表
- const getTableData = async (params: any) => {
- set_reload(false);
- if (!currentCategory) return { data: [], success: true } as any;
- const node: any = currentCategory as any;
- if (node.nodeType === 'service') {
- // 使用 dataSource 渲染时,这里返回本地 rows,避免与 KCTable 拖拽冲突
- if (evalItemRows && evalItemRows.length > 0) {
- return { data: evalItemRows, success: true } as any;
- }
- await loadServiceEvalItems(String(node.code || node.name));
- return { data: evalItemRows, success: true } as any;
- }
- const resp = await getProjectList({ categoryId: currentCategory.id, ...params });
- return { data: resp.list || [], success: true } as any;
- };
- // 主表列定义 - 项目
- const projectColumns: ProColumns<ProjectItem>[] = useMemo(
- () => [
- { title: '', dataIndex: 'drag', width: 32, render: () => (<span className="SE-DragHandle" style={{ cursor:'move', display:'flex', alignItems:'center', justifyContent:'center', height:'100%' }}><IconFont type="iconliebiao" style={{ fontSize: 16, color: '#99A6BF' }} /></span>) },
- {
- title: '序号',
- dataIndex: 'index',
- width: 60,
- render: (_, __, index) => index + 1,
- },
- {
- title: '评价项目代码',
- dataIndex: 'projectCode',
- width: 100,
- },
- {
- title: '评价项目名称',
- dataIndex: 'projectName',
- ellipsis: true,
- },
- {
- title: '权重',
- dataIndex: 'weight',
- width: 120,
- render: (_, record) => (
- <InputNumber
- min={0}
- max={100}
- formatter={(value) => `${value}%`}
- parser={(value) => Number(String(value).replace('%', ''))}
- value={record.weight}
- onChange={async (val) => {
- const weight = typeof val === 'number' ? val : Number(val);
- if (Number.isNaN(weight)) return;
- const ok = await updateProjectWeight(record.projectId, weight);
- if (ok) set_reload(true);
- }}
- />
- ),
- },
- {
- title: '操作',
- key: 'option',
- width: 160,
- valueType: 'option',
- render: (_, record) => [
- <ModalForm
- key="add"
- title={`新增“${record.projectName}”的选项`}
- trigger={<a>选项</a>}
- width={520}
- onFinish={async (values) => {
- const ok = await addOption({ projectId: record.projectId, ...(values as any) });
- if (ok) {
- const opts = await getOptionList({ projectId: record.projectId });
- set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
- }
- return !!ok;
- }}
- >
- <ProFormText name="optionCode" label="选项代码" rules={[{ required: true, message: '必填' }]} />
- <ProFormText name="optionName" label="选项名称" rules={[{ required: true, message: '必填' }]} />
- <ProFormDigit name="score" label="选项得分" min={0} max={9999} rules={[{ required: true, message: '必填' }]} />
- </ModalForm>,
- <Popconfirm key="del" title="是否确认删除?" onConfirm={async () => { const ok = await delProject(record.projectId); if (ok) set_reload(true); }}>
- <a>删除</a>
- </Popconfirm>,
- ],
- },
- ],
- [projectOptionsMap]
- );
- // 评价项表格列
- const evalItemColumns: ProColumns<any>[] = useMemo(
- () => [
- { title: '', dataIndex: 'drag', width: 32, render: () => (<span className="SE-DragHandle" style={{ cursor:'move', display:'flex', alignItems:'center', justifyContent:'center', height:'100%' }}><IconFont type="iconliebiao" style={{ fontSize: 16, color: '#99A6BF' }} /></span>) },
- { title: '序号', dataIndex: 'index', width: 60, render: (_, __, index) => index + 1 },
- { title: '评价项目代码', dataIndex: 'itemCode', width: 120 },
- { title: '评价项目名称', dataIndex: 'itemName', ellipsis: true },
- {
- title: '权重',
- dataIndex: 'percent',
- width: 120,
- render: (v: any) => `${(Number.isFinite(Number(v)) && Number(v) >= 0 ? Number(v) : 0)}%`,
- },
- // 移除不需要的排序列
- {
- title: '操作',
- valueType: 'option',
- width: 120,
- render: (_, record) => [
- <a key="opt" onClick={async () => {
- set_bindItemCode(record.itemCode);
- // 获取该评价项已有的选项
- const existingOptions = await getEvaluationSelect(record.itemCode);
- const existingKeys = (existingOptions || []).map((item: any) => String(item.code));
- set_selectTargetKeys(existingKeys);
- set_selectSelectedKeys([]);
- await fetchEvaluationSelectDict();
- set_selectModalOpen(true);
- }}>选项</a>,
- <Popconfirm key="del" title="是否确认删除?" onConfirm={async () => {
- const res = await deleteEvaluationItem(record.id);
- if (res !== false) {
- // 删除成功后,如果该项是展开状态,从展开列表中移除
- set_expandedRowKeys((prev) => prev.filter(k => k !== record.itemCode));
- // 重新加载评价项列表
- const current: any = currentCategory as any;
- if (current && current.nodeType === 'service') {
- await loadServiceEvalItems(String(current.code || current.name));
- }
- }
- }}>
- <a>删除</a>
- </Popconfirm>,
- ],
- },
- ],
- [editingEvalCode, editingEvalValue]
- );
- // 展开行渲染(分类/系统场景使用)
- const expandedRowRender = (record: ProjectItem) => {
- const options = projectOptionsMap[record.projectId] || [];
- return (
- <OptionsList
- project={record}
- options={options}
- onAdd={async (data) => {
- const ok = await addOption({ projectId: record.projectId, ...data });
- if (ok) {
- const opts = await getOptionList({ projectId: record.projectId });
- set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
- }
- }}
- onDelete={async (optionId) => {
- const ok = await delOption(optionId);
- if (ok) {
- const opts = await getOptionList({ projectId: record.projectId });
- set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
- }
- }}
- onScoreChange={async (optionId, score) => {
- const ok = await updateOptionScore(optionId, score);
- if (ok) {
- const opts = await getOptionList({ projectId: record.projectId });
- set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
- }
- }}
- />
- );
- };
- // 服务节点:展开渲染(与外层保持一致的 KCTable 风格)
- const expandedRowRenderService = (record: any) => {
- const parentCode: string = record.itemCode;
- const rows = serviceOptionsMap[parentCode] || [];
- const columns: ProColumns<any>[] = [
- {
- title: '',
- dataIndex: 'drag',
- width: 32,
- render: (_, row: any) => (
- <span
- className="SE-DragHandle"
- style={{ cursor: 'move', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}
- draggable
- onDragStart={(e) => {
- set_draggingOption({ parent: parentCode, key: String(row.id ?? row.code) });
- e.dataTransfer.effectAllowed = 'move';
- try { e.dataTransfer.setData('text/plain', String(row.id ?? row.code)); } catch {}
- }}
- onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
- onDrop={(e) => {
- e.preventDefault();
- if (!draggingOption || draggingOption.parent !== parentCode) return;
- const list = serviceOptionsMap[parentCode] || [];
- const from = list.findIndex((r: any) => String(r.id ?? r.code) === draggingOption.key);
- const to = list.findIndex((r: any) => String(r.id ?? r.code) === String(row.id ?? row.code));
- if (from === -1 || to === -1 || from === to) return;
- const next = moveItem(list, from, to);
- set_serviceOptionsMap((old) => ({ ...old, [parentCode]: next }));
- set_draggingOption(null);
- set_reload((v)=>!v);
- // TODO: 保存选项排序
- }}
- title="拖动排序"
- >
- <IconFont type="iconliebiao" style={{ fontSize: 16, color: '#99A6BF' }} />
- </span>
- ),
- },
- { title: '序号', dataIndex: 'index', width: 60, render: (_, __, index) => index + 1 },
- { title: '选项代码', dataIndex: 'code', width: 120 },
- { title: '选项名称', dataIndex: 'name', ellipsis: true },
- {
- title: '选项得分',
- dataIndex: 'value',
- width: 200,
- render: (v: any, row: any) => {
- const isEditing = row.__editing === true;
- if (!isEditing) {
- return (
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
- <div style={{ padding: '2px 10px', border: '1px solid #dae2f2', borderRadius: 6, minWidth: 72, textAlign: 'center', background: '#f7f9fc' }}>{v ?? 0}</div>
- <span
- onClick={() => {
- set_serviceOptionsMap((old) => {
- const list = [...(old[parentCode] || [])];
- const idx = list.findIndex((i: any) => String(i.id ?? i.code) === String(row.id ?? row.code));
- if (idx > -1) list[idx] = { ...list[idx], __editing: true } as any;
- return { ...old, [parentCode]: list };
- });
- }}
- style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: 28, width: 28, border: '1px solid #dae2f2', borderRadius: 6, cursor: 'pointer' }}
- >
- <IconFont type="iconbianji" style={{ fontSize: 16, color: '#17181A' }} />
- </span>
- </div>
- );
- }
- return (
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
- <InputNumber
- min={0}
- max={9999}
- value={row.value}
- onChange={(val) => {
- const next = Number(val) || 0;
- set_serviceOptionsMap((old) => {
- const list = [...(old[parentCode] || [])];
- const idx = list.findIndex((i: any) => String(i.id ?? i.code) === String(row.id ?? row.code));
- if (idx > -1) list[idx] = { ...list[idx], value: next, __editing: true } as any;
- return { ...old, [parentCode]: list };
- });
- }}
- style={{ width: 100 }}
- />
- <span
- onClick={async () => {
- // 调用接口保存选项分值
- const res = await editEvaluationSelect(row.id, row.value);
- // 响应拦截器:成功返回 data 或 true,失败返回 false
- // 且 POST 成功时拦截器已自动显示"操作成功!"提示
- if (res !== false) {
- // 成功,退出编辑状态
- set_serviceOptionsMap((old) => {
- const list = [...(old[parentCode] || [])];
- const idx = list.findIndex((i: any) => String(i.id ?? i.code) === String(row.id ?? row.code));
- if (idx > -1) list[idx] = { ...list[idx], __editing: false } as any;
- return { ...old, [parentCode]: list };
- });
- }
- // 失败时拦截器已显示错误提示,这里只需保持编辑状态
- }}
- style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: 28, width: 28, border: '1px solid #dae2f2', borderRadius: 6, cursor: 'pointer' }}
- >
- <IconFont type="iconqueren" style={{ fontSize: 16, color: '#17181A' }} />
- </span>
- </div>
- );
- },
- },
- {
- title: '操作',
- valueType: 'option',
- width: 120,
- render: (_, row: any) => [
- <Popconfirm
- key="del"
- title="是否确认删除?"
- onConfirm={async () => {
- const res = await deleteEvaluationSelect(row.id);
- if (res !== false) {
- // 删除成功,重新加载该评价项的选项列表
- const list = await getEvaluationSelect(parentCode);
- set_serviceOptionsMap((old) => ({
- ...old,
- [parentCode]: (list || []) as any,
- }));
- }
- }}
- >
- <a>删除</a>
- </Popconfirm>
- ],
- },
- ];
- return (
- <div className="SE-ExpandWrap">
- <KCTable
- newVer
- columns={columns as ProColumns[]}
- dataSource={rows}
- rowKey={(r: any) => String(r.id ?? r.code)}
- dragSortKey="id"
- dragType="KC_TABLE_NESTED_ROW"
- pagination={false}
- toolBarRender={false as any}
- search={false as any}
- options={false as any}
- bordered={false as any}
- size={'small' as any}
- dragSort
- onDragSortEnd={(next)=>{
- set_serviceOptionsMap((old)=> ({ ...old, [parentCode]: next }));
- }}
- />
- </div>
- );
- };
- // 表格展开回调
- const onExpand = async (expanded: boolean, record: any) => {
- const node: any = currentCategory as any;
- const rowKey = node?.nodeType === 'service' ? record.itemCode : record.projectId;
-
- // 更新展开行状态
- if (expanded) {
- set_expandedRowKeys((prev) => [...prev, rowKey]);
- } else {
- set_expandedRowKeys((prev) => prev.filter(k => k !== rowKey));
- return;
- }
-
- // 加载数据
- if (node && node.nodeType === 'service') {
- const list = await getEvaluationSelect(record.itemCode);
- set_serviceOptionsMap((old: Record<string, { id: number | string; hospId?: string; itemCode: string; code: string; name: string; value: number; sort: number }[]>) => ({
- ...old,
- [record.itemCode]: (list || []) as { id: number | string; hospId?: string; itemCode: string; code: string; name: string; value: number; sort: number }[],
- }));
- } else {
- const opts = await getOptionList({ projectId: record.projectId });
- set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
- }
- };
-
- // 收起所有展开行
- const collapseAllRows = () => {
- set_expandedRowKeys([]);
- };
- useEffect(() => { fetchTree(); }, []);
- useEffect(() => { if (sysModalOpen) fetchSystemTree(); }, [sysModalOpen]);
- useEffect(() => {
- if (currentCategory) {
- set_tableParams((p) => ({ ...p, current: 1, categoryId: currentCategory.id }));
- set_reload(true);
- // 切换分类时清空展开行
- set_expandedRowKeys([]);
- }
- }, [currentCategory]);
- return (
- <>
- <div className="SystemNavMana">
- <div className="leftTree">
- <div className="search" style={{display:'flex',gap:8,alignItems:'center'}}>
- <Input className="searchInput" placeholder="请输入" size="small" allowClear onChange={(e)=>setSearchValue(e.target.value)} />
- <Tooltip title="添加系统">
- <span className="add" onClick={()=> set_sysModalOpen(true)}>
- <IconFont style={{ color: '#17181A', fontSize: 12 }} type="icon-xinzeng" />
- </span>
- </Tooltip>
- </div>
- <div className="treeeWrap">
- {categoryTree && categoryTree.length>0 && (
- <DirectoryTree
- fieldNames={{ title:'name', key:'id' }}
- rootStyle={{ overflowY:'scroll', overflowX:'hidden' }}
- onSelect={async (keys, info)=> {
- const node = info.node as any;
- // 仅允许选择服务节点
- if (node.nodeType !== 'service') return;
- set_currentCategory(node);
- set_tableParams((p)=>({ ...p, current:1, categoryId: node.id }));
- set_reload(true);
- // 服务节点:拉取右侧评价项(dataSource 渲染)
- await loadServiceEvalItems(String(node.code || node.name));
- }}
- onExpand={(keys)=>{ setExpandedKeys(keys as React.Key[]); setAutoExpandParent(false);} }
- expandedKeys={expandedKeys}
- autoExpandParent={autoExpandParent}
- selectedKeys={currentCategory && (currentCategory as any).nodeType==='service' ? [currentCategory.id] : []}
- blockNode
- className="KC-DirectoryTree"
- icon={() => null}
- titleRender={(nodeData: any) => {
- const strTitle = nodeData.name as string;
- const index = strTitle.indexOf(searchValue);
- const beforeStr = strTitle.substring(0, index);
- const afterStr = strTitle.slice(index + searchValue.length);
- const title = index > -1 ? (
- <span>
- {beforeStr}
- <span className="site-tree-search-value" style={{ color: 'red', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{searchValue}</span>
- {afterStr}
- </span>
- ) : (
- <span className="strTitle">{strTitle}</span>
- );
- const isDirectory = Array.isArray(nodeData.children) && nodeData.children.length > 0;
- return (
- <div style={{display:'flex',alignItems:'center',height:32}}>
- <span className="node-title-area" style={{flex:1, minWidth:0}}>{title}</span>
- {nodeData.nodeType === 'system' && (
- <span
- className="inline-add"
- style={{display:'inline-flex', justifyContent:'center', alignItems:'center'}}
- onClick={async (e)=>{
- e.stopPropagation();
- set_serviceBindSystemId(String(nodeData.id));
- // 打开前重置已选,避免上次选择残留
- set_serviceSelected([]);
- await fetchServiceDict();
- set_serviceModalOpen(true);
- }}
- >
- <IconFont style={{ color: '#99A6BF', fontSize: 12 }} type="icon-xinzeng" />
- </span>
- )}
- </div>
- );
- }}
- treeData={categoryTree as unknown as DataNode[]}
- switcherIcon={(props:any)=> props.expanded ? (<img style={{ width:20,height:20, position:'relative', top:5 }} src={closeIcon} />) : (<img style={{ width:20,height:20, position:'relative', top:5 }} src={expandedIcon} />)}
- />
- )}
- </div>
- </div>
- <div className="rightContent">
- <div className="tableToolbar">
- <div className="filter">
- <span style={{ fontSize: 16, fontWeight: 500, color: '#17181A' }}>
- {currentCategory?.name || '-'}
- </span>
- </div>
- <div className={'btnGroup'}>
- {expandedRowKeys.length > 0 && (
- <span className="collapse" onClick={collapseAllRows}>
- 收起所有 ({expandedRowKeys.length})
- </span>
- )}
- <span className="add" onClick={async ()=>{
- // 获取当前已有的评价项,用于回显
- const current: any = currentCategory as any;
- if (current && current.nodeType === 'service') {
- const existingItems = await getServiceEvaluationItem(String(current.code || current.name));
- const existingKeys = (existingItems || []).map((item: any) => String(item.itemCode));
- set_evalItemTargetKeys(existingKeys);
- } else {
- set_evalItemTargetKeys([]);
- }
- set_evalItemSelectedKeys([]);
- await fetchEvaluationItemsDict();
- set_evalItemModalOpen(true);
- }}>
- 添加
- </span>
- </div>
- <ModalForm
- title="新增评价项目"
- width={520}
- trigger={<span style={{ display:'none' }} />}
- onFinish={async (values) => {
- if (!currentCategory) return false;
- const ok = await addProject({ categoryId: currentCategory.id, ...(values as any) });
- if (ok) set_reload(true);
- return !!ok;
- }}
- >
- <ProFormText name="projectCode" label="项目代码" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
- <ProFormText name="projectName" label="项目名称" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
- <ProFormDigit name="weight" label="权重%" min={0} max={100} placeholder="0-100" rules={[{ required: true, message: '必填' }]} />
- </ModalForm>
- </div>
- {currentCategory && (
- <KCTable
- key={`table-${currentCategory.id}`}
- newVer
- reload={(currentCategory as any)?.nodeType === 'service' ? false : reload}
- params={(currentCategory as any)?.nodeType === 'service' ? undefined : tableParams}
- rowKey={(((currentCategory as any)?.nodeType === 'service')
- ? ((r: any) => String(r?.itemCode ?? ''))
- : ((r: any) => String(r?.projectId ?? ''))) as any}
- dragSortKey={(currentCategory as any)?.nodeType === 'service' ? 'itemCode' : 'projectId'}
- dragType="KC_TABLE_MAIN_ROW"
- disabledDragKeys={expandedRowKeys}
- scroll={{ y: `calc(100vh - 240px)` }}
- columns={((currentCategory as any)?.nodeType === 'service' ? evalItemColumns : projectColumns) as ProColumns[]}
- request={(((currentCategory as any)?.nodeType === 'service') ? undefined : ((p: any, s: any, f: any) => getTableData(p))) as any}
- expandable={{
- expandedRowRender: (currentCategory as any)?.nodeType === 'service' ? expandedRowRenderService : expandedRowRender,
- onExpand,
- expandedRowKeys,
- onExpandedRowsChange: (keys: readonly React.Key[]) => set_expandedRowKeys([...keys]),
- expandIconColumnIndex: 1 // 展开图标显示在第2列,第1列留给拖拽手柄
- }}
- pagination={false}
- dragSort={(currentCategory as any)?.nodeType === 'service'}
- dataSource={((currentCategory as any)?.nodeType === 'service') ? evalItemRows : undefined}
- onDragSortEnd={(rows: any[])=>{
- set_evalItemRows(rows);
- set_useLocalEvalOrder(true);
- }}
- />
- )}
- </div>
- </div>
- <Modal
- title="添加系统"
- open={sysModalOpen}
- onCancel={()=> set_sysModalOpen(false)}
- onOk={async ()=>{
- const flat = (nodes: SystemTreeNode[]): SystemTreeNode[] => nodes.reduce<SystemTreeNode[]>((acc, n)=>{
- acc.push(n);
- if (n.children && n.children.length) acc.push(...flat(n.children));
- return acc;
- }, []);
- const all = flat(systemTree);
- const payload = all
- .filter(n => sysTargetKeys.includes(n.code) && n.type === 3)
- .map(n => ({ systemId: n.code, systemName: n.name }));
- if (payload.length === 0) {
- message.warning('请选择系统');
- return;
- }
- const ok = await addSystems(payload as any);
- if (ok) {
- set_sysModalOpen(false);
- // 刷新左侧“已选系统”列表
- set_reload(true);
- fetchTree();
- }
- }}
- width={720}
- destroyOnClose
- >
- <Transfer
- className="tree-transfer"
- listStyle={{ height: 540 }}
- dataSource={(function buildFlat(){
- const res: any[] = [];
- const walk = (list: SystemTreeNode[], parentChecked: boolean)=>{
- list.forEach(n=>{
- res.push({ key: n.code, code: n.code, name: n.name, type: n.type, node: n });
- if (n.children && n.children.length) walk(n.children, false);
- })
- };
- walk(systemTree, false);
- return res;
- })()}
- rowKey={(item)=> item.code}
- titles={[`待选项`, `已选项`]}
- targetKeys={sysTargetKeys}
- selectedKeys={sysSelectedKeys}
- showSearch
- locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
- render={(item)=> item.name}
- onChange={(nextTargetKeys)=> set_sysTargetKeys(nextTargetKeys as string[])}
- onSelectChange={(sourceSelectedKeys, targetSelectedKeys)=> set_sysSelectedKeys([...(sourceSelectedKeys as string[]), ...(targetSelectedKeys as string[])])}
- >
- {({ direction, onItemSelect, onItemSelectAll, selectedKeys })=>{
- if (direction === 'left') {
- const treeData = (function toTree(list: SystemTreeNode[]): any[] {
- return list.map((n: SystemTreeNode): any => ({
- key: n.code,
- title: n.name,
- // 不再用 children 判断是否可勾选;统一允许勾选,由 onCheck 决定加入哪些 key
- disabled: false,
- children: n.children ? (toTree(n.children) as any[]) : undefined,
- }));
- })(systemTree);
- const checkedKeys = [...selectedKeys, ...sysTargetKeys];
- return (
- <Tree
- checkable
- defaultExpandAll
- checkedKeys={checkedKeys}
- treeData={treeData as any}
- height={480}
- expandedKeys={sysExpandedKeys}
- onExpand={(keys)=> set_sysExpandedKeys(keys as string[])}
- onCheck={(keys, { node })=>{
- // 仅加入“系统叶子”:type==3,或 type==5 但无 children(后端把系统标成5时兜底)
- const isSystemLeafByCode = (code: string) => {
- const flat = (function flat(){
- const res: any[] = [];
- const walk = (list: SystemTreeNode[])=>{ list.forEach(nn=>{ res.push({ code: nn.code, type: nn.type, children: nn.children }); if(nn.children) walk(nn.children); }); };
- walk(systemTree); return res;
- })();
- const found = flat.find(i=> i.code===code);
- if (!found) return false;
- const noChildren = !found.children || found.children.length===0;
- return found.type===3 || (found.type===5 && noChildren);
- };
- const getChildCodes = (n:any): string[] => {
- if (!n.children || n.children.length===0) return [n.key as string];
- return n.children.reduce((acc:any,c:any)=> acc.concat(getChildCodes(c)), []);
- };
- const isSelect = !(node as any).checked;
- const candidate = (node.children && node.children.length>0) ? getChildCodes(node) : [node.key as string];
- const systemLeafKeys = candidate.filter((code)=> isSystemLeafByCode(code));
- onItemSelectAll(systemLeafKeys as string[], isSelect);
- }}
- />
- );
- }
- return null;
- }}
- </Transfer>
- </Modal>
- <Modal
- title="添加服务"
- open={serviceModalOpen}
- onCancel={()=> { set_serviceModalOpen(false); set_serviceSelected([]); set_serviceBindSystemId(''); }}
- confirmLoading={serviceSaving}
- onOk={async ()=> {
- if (!serviceBindSystemId) { set_serviceModalOpen(false); return; }
- try {
- set_serviceSaving(true);
- const ok: any = await addServiceTypes(serviceBindSystemId, serviceSelected);
- if (ok) {
- set_serviceModalOpen(false);
- set_serviceSelected([]);
- set_serviceBindSystemId('');
- // 刷新左侧列表
- set_reload(true);
- fetchTree();
- }
- } finally {
- set_serviceSaving(false);
- }
- }}
- width={560}
- destroyOnClose
- >
- <Transfer
- className="ServiceTransfer"
- style={{ width: '100%' }}
- titles={[`待选项`,`已选项`]}
- dataSource={serviceDict}
- rowKey={(item)=> item.key}
- render={(item)=> item.title}
- targetKeys={serviceSelected}
- onChange={(next)=> set_serviceSelected(next as string[])}
- listStyle={{ height: 460, width: 'calc(50% - 32px)' }}
- operationStyle={{ margin: '0 12px', display: 'flex', alignItems: 'center' }}
- locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
- showSearch
- />
- </Modal>
- <Modal
- title="评价项目"
- open={evalItemModalOpen}
- onCancel={()=> { set_evalItemModalOpen(false); set_evalItemTargetKeys([]); set_evalItemSelectedKeys([]); }}
- onOk={async ()=> {
- // 依据当前选中的服务类型保存评价项目
- const current: any = currentCategory as any;
- if (!current || current.nodeType !== 'service') { set_evalItemModalOpen(false); return; }
- // 从字典中匹配详细信息
- const itemCodes = evalItemTargetKeys.map((k, idx)=>{
- const found = evalItemDict.find(i=> i.key === k);
- return { itemCode: k, itemName: found?.title || k, itemValue: 1, sort: idx + 1 };
- });
- const ok: any = await addEvaluationItem(String(current.code || current.name), itemCodes);
- if (ok) {
- set_evalItemModalOpen(false);
- set_evalItemTargetKeys([]);
- set_evalItemSelectedKeys([]);
- // 刷新右侧表格:重新加载评价项列表
- await loadServiceEvalItems(String(current.code || current.name));
- }
- }}
- width={560}
- destroyOnClose
- >
- <Transfer
- titles={[`待选项`,`已选项`]}
- dataSource={evalItemDict}
- rowKey={(item)=> item.key}
- render={(item)=> item.title}
- targetKeys={evalItemTargetKeys}
- selectedKeys={evalItemSelectedKeys}
- onChange={(next)=> set_evalItemTargetKeys(next as string[])}
- onSelectChange={(l,r)=> set_evalItemSelectedKeys([...(l as string[]), ...(r as string[])])}
- listStyle={{ height: 460, width: 'calc(50% - 32px)' }}
- operationStyle={{ margin: '0 12px', display: 'flex', alignItems: 'center' }}
- showSearch
- locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
- />
- </Modal>
- <Modal
- title="添加选项"
- open={selectModalOpen}
- onCancel={()=> { set_selectModalOpen(false); set_selectTargetKeys([]); set_selectSelectedKeys([]); set_bindItemCode(''); }}
- onOk={async ()=> {
- const selectCodes = selectTargetKeys.map((k, idx)=>{
- const found = selectDict.find(i=> i.key === k);
- return { itemCode: k, itemName: found?.title || k, itemValue: 1, sort: idx + 1 };
- });
- const ok: any = await addEvaluationSelect(bindItemCode, selectCodes);
- if (ok !== false) {
- set_selectModalOpen(false);
- set_selectTargetKeys([]);
- set_selectSelectedKeys([]);
- // 重新加载该评价项的选项数据
- const list = await getEvaluationSelect(bindItemCode);
- set_serviceOptionsMap((old) => ({
- ...old,
- [bindItemCode]: (list || []) as any,
- }));
- set_bindItemCode('');
- set_reload(true);
- }
- }}
- width={560}
- destroyOnClose
- >
- <Transfer
- titles={[`待选项`,`已选项`]}
- dataSource={selectDict}
- rowKey={(item)=> item.key}
- render={(item)=> item.title}
- targetKeys={selectTargetKeys}
- selectedKeys={selectSelectedKeys}
- onChange={(next)=> set_selectTargetKeys(next as string[])}
- onSelectChange={(l,r)=> set_selectSelectedKeys([...(l as string[]), ...(r as string[])])}
- listStyle={{ height: 360, width: 'calc(50% - 32px)' }}
- operationStyle={{ margin: '0 12px', display: 'flex', alignItems: 'center' }}
- showSearch
- locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
- />
- </Modal>
- </>
- );
- };
- export default ServiceEvaluatePage;
|