|
|
@@ -0,0 +1,1052 @@
|
|
|
+/*
|
|
|
+ * @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;
|
|
|
+
|
|
|
+
|