483 lines
14 KiB
TypeScript
483 lines
14 KiB
TypeScript
|
// apps/web/src/components/models/term/term-manager.tsx
|
|||
|
import { Button, Input, Modal, Space, Table, TreeSelect, Select } from "antd";
|
|||
|
import { api } from "@nice/client";
|
|||
|
import { useState, useEffect } from "react";
|
|||
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
|||
|
import { ObjectType } from "@nice/common";
|
|||
|
|
|||
|
interface TermManagerProps {
|
|||
|
title: string;
|
|||
|
}
|
|||
|
|
|||
|
export default function DeviceManager({ title }: TermManagerProps) {
|
|||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|||
|
const [editingTerm, setEditingTerm] = useState<any>(null);
|
|||
|
const [termName, setTermName] = useState("");
|
|||
|
const [parentId, setParentId] = useState<string | null>(null);
|
|||
|
const [taxonomyId, setTaxonomyId] = useState<string | null>(null);
|
|||
|
const [searchValue, setSearchValue] = useState("");
|
|||
|
const [treeData, setTreeData] = useState<any[]>([]);
|
|||
|
const [taxonomySelectDisabled, setTaxonomySelectDisabled] = useState(false);
|
|||
|
|
|||
|
// 获取所有taxonomy
|
|||
|
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
|||
|
type: ObjectType.DEVICE,
|
|||
|
});
|
|||
|
|
|||
|
// 获取所有故障类型taxonomy
|
|||
|
// 故障网系类别taxonomy
|
|||
|
const { data: systemTypeTaxonomy } = api.taxonomy.findBySlug.useQuery({
|
|||
|
slug: "system_type",
|
|||
|
});
|
|||
|
|
|||
|
// 故障类型taxonomy
|
|||
|
const { data: deviceTypeTaxonomy } = api.taxonomy.findBySlug.useQuery({
|
|||
|
slug: "device_type",
|
|||
|
});
|
|||
|
|
|||
|
// 获取所有网系类别条目
|
|||
|
const { data: systemTypeTerms, refetch: refetchSystemType } =
|
|||
|
api.term.findMany.useQuery({
|
|||
|
where: {
|
|||
|
taxonomy: { slug: "system_type" },
|
|||
|
deletedAt: null,
|
|||
|
},
|
|||
|
include: {
|
|||
|
children: true,
|
|||
|
},
|
|||
|
orderBy: { order: "asc" },
|
|||
|
});
|
|||
|
|
|||
|
// 获取所有故障类型条目
|
|||
|
const { data: deviceTypeTerms, refetch: refetchDeviceType } =
|
|||
|
api.term.findMany.useQuery({
|
|||
|
where: {
|
|||
|
taxonomy: { slug: "device_type" },
|
|||
|
deletedAt: null,
|
|||
|
},
|
|||
|
include: {
|
|||
|
children: true,
|
|||
|
},
|
|||
|
orderBy: { order: "asc" },
|
|||
|
});
|
|||
|
|
|||
|
// 构建包含两种分类的树形数据
|
|||
|
useEffect(() => {
|
|||
|
if (systemTypeTerms && deviceTypeTerms) {
|
|||
|
// 先获取顶级网系类别
|
|||
|
const rootTerms = systemTypeTerms.filter((term) => !term.parentId);
|
|||
|
|
|||
|
// 构建树形数据
|
|||
|
const buildTreeData = (items: any[]): any[] => {
|
|||
|
return items.map((item) => {
|
|||
|
// 找到与此网系类别关联的故障类型
|
|||
|
const deviceChildren = deviceTypeTerms.filter(
|
|||
|
(t) => t.parentId === item.id
|
|||
|
);
|
|||
|
|
|||
|
// 为每个故障类型找到其子故障
|
|||
|
const processedDeviceChildren = deviceChildren.map((deviceType) => {
|
|||
|
const deviceItems = deviceTypeTerms.filter(
|
|||
|
(t) => t.parentId === deviceType.id
|
|||
|
);
|
|||
|
|
|||
|
return {
|
|||
|
...deviceType,
|
|||
|
key: deviceType.id,
|
|||
|
children: deviceItems.map((device) => ({
|
|||
|
...device,
|
|||
|
key: device.id,
|
|||
|
})),
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
return {
|
|||
|
...item,
|
|||
|
key: item.id,
|
|||
|
children: processedDeviceChildren,
|
|||
|
};
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
setTreeData(buildTreeData(rootTerms));
|
|||
|
}
|
|||
|
}, [systemTypeTerms, deviceTypeTerms]);
|
|||
|
|
|||
|
// 搜索过滤逻辑
|
|||
|
useEffect(() => {
|
|||
|
if (systemTypeTerms && deviceTypeTerms) {
|
|||
|
if (!searchValue) {
|
|||
|
// 重新构建完整的树形结构
|
|||
|
const rootTerms = systemTypeTerms.filter((term) => !term.parentId);
|
|||
|
const buildTreeData = (items: any[]): any[] => {
|
|||
|
return items.map((item) => {
|
|||
|
const deviceChildren = deviceTypeTerms.filter(
|
|||
|
(t) => t.parentId === item.id
|
|||
|
);
|
|||
|
|
|||
|
const processedDeviceChildren = deviceChildren.map((deviceType) => {
|
|||
|
const deviceItems = deviceTypeTerms.filter(
|
|||
|
(t) => t.parentId === deviceType.id
|
|||
|
);
|
|||
|
|
|||
|
return {
|
|||
|
...deviceType,
|
|||
|
key: deviceType.id,
|
|||
|
children: deviceItems.map((device) => ({
|
|||
|
...device,
|
|||
|
key: device.id,
|
|||
|
})),
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
return {
|
|||
|
...item,
|
|||
|
key: item.id,
|
|||
|
children: processedDeviceChildren,
|
|||
|
};
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
setTreeData(buildTreeData(rootTerms));
|
|||
|
} else {
|
|||
|
// 搜索匹配所有项
|
|||
|
const allTerms = [...systemTypeTerms, ...deviceTypeTerms];
|
|||
|
const filtered = allTerms.filter((term) =>
|
|||
|
term.name.toLowerCase().includes(searchValue.toLowerCase())
|
|||
|
);
|
|||
|
setTreeData(filtered.map((item) => ({ ...item, key: item.id })));
|
|||
|
}
|
|||
|
}
|
|||
|
}, [systemTypeTerms, deviceTypeTerms, searchValue]);
|
|||
|
|
|||
|
const handleSearch = (value: string) => {
|
|||
|
setSearchValue(value);
|
|||
|
};
|
|||
|
|
|||
|
// API调用
|
|||
|
const { mutate: createTerm } = api.term.create.useMutation({
|
|||
|
onSuccess: () => {
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
setIsModalVisible(false);
|
|||
|
setTermName("");
|
|||
|
setParentId(null);
|
|||
|
setTaxonomyId(null);
|
|||
|
},
|
|||
|
});
|
|||
|
|
|||
|
const { mutate: updateTerm } = api.term.update.useMutation({
|
|||
|
onSuccess: () => {
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
setIsModalVisible(false);
|
|||
|
setEditingTerm(null);
|
|||
|
setTermName("");
|
|||
|
setParentId(null);
|
|||
|
setTaxonomyId(null);
|
|||
|
},
|
|||
|
});
|
|||
|
|
|||
|
const { mutate: softDeleteByIds } = api.term.softDeleteByIds.useMutation({
|
|||
|
onSuccess: () => {
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
},
|
|||
|
});
|
|||
|
|
|||
|
// 操作处理函数
|
|||
|
// 修改handleAdd函数
|
|||
|
const handleAdd = (parentRecord?: any) => {
|
|||
|
setEditingTerm(null);
|
|||
|
setTermName("");
|
|||
|
setParentId(parentRecord?.id || null);
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
// 根据父记录类型自动选择taxonomy
|
|||
|
if (parentRecord) {
|
|||
|
// 如果父类是网系类别,则自动设置为故障类型
|
|||
|
if (parentRecord.taxonomyId === systemTypeTaxonomy?.id) {
|
|||
|
setTaxonomyId(deviceTypeTaxonomy?.id);
|
|||
|
// 可以设置状态来禁用Select组件
|
|||
|
setTaxonomySelectDisabled(true);
|
|||
|
} else {
|
|||
|
setTaxonomyId(parentRecord.taxonomyId);
|
|||
|
setTaxonomySelectDisabled(false);
|
|||
|
}
|
|||
|
} else {
|
|||
|
// 如果是顶级项,默认设为网系类别
|
|||
|
setTaxonomyId(systemTypeTaxonomy?.id || null);
|
|||
|
setTaxonomySelectDisabled(false);
|
|||
|
}
|
|||
|
|
|||
|
setIsModalVisible(true);
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
const handleEdit = (term: any) => {
|
|||
|
setEditingTerm(term);
|
|||
|
setTermName(term.name);
|
|||
|
setParentId(term.parentId);
|
|||
|
setTaxonomyId(term.taxonomyId);
|
|||
|
setIsModalVisible(true);
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
};
|
|||
|
|
|||
|
const handleDelete = (term: any) => {
|
|||
|
Modal.confirm({
|
|||
|
title: "确认删除",
|
|||
|
content: `确定要删除"${term.name}"吗?这将同时删除其下所有子项!`,
|
|||
|
onOk: () => softDeleteByIds({ ids: [term.id] }),
|
|||
|
});
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
};
|
|||
|
|
|||
|
const handleSave = () => {
|
|||
|
if (!termName.trim() || !taxonomyId) return;
|
|||
|
|
|||
|
if (editingTerm) {
|
|||
|
updateTerm({
|
|||
|
where: { id: editingTerm.id },
|
|||
|
data: {
|
|||
|
name: termName,
|
|||
|
parentId: parentId,
|
|||
|
hasChildren: editingTerm.hasChildren,
|
|||
|
},
|
|||
|
});
|
|||
|
} else {
|
|||
|
createTerm({
|
|||
|
data: {
|
|||
|
name: termName,
|
|||
|
taxonomyId: taxonomyId,
|
|||
|
parentId: parentId,
|
|||
|
},
|
|||
|
});
|
|||
|
}
|
|||
|
refetchSystemType();
|
|||
|
refetchDeviceType();
|
|||
|
};
|
|||
|
|
|||
|
// 构建父级选择器的选项
|
|||
|
const getParentOptions = () => {
|
|||
|
if (!systemTypeTerms || !deviceTypeTerms) return [];
|
|||
|
|
|||
|
const allTerms = [...systemTypeTerms, ...deviceTypeTerms];
|
|||
|
|
|||
|
// 根据编辑对象和当前选择的taxonomy过滤有效的父级选项
|
|||
|
let validParents = allTerms;
|
|||
|
|
|||
|
// 如果是编辑现有项
|
|||
|
if (editingTerm) {
|
|||
|
// 递归查找所有子孙节点ID,避免循环引用
|
|||
|
const findAllDescendantIds = (itemId: string): string[] => {
|
|||
|
const directChildren = allTerms.filter((t) => t.parentId === itemId);
|
|||
|
const descendantIds = directChildren.map((c) => c.id);
|
|||
|
|
|||
|
directChildren.forEach((child) => {
|
|||
|
const childDescendants = findAllDescendantIds(child.id);
|
|||
|
descendantIds.push(...childDescendants);
|
|||
|
});
|
|||
|
|
|||
|
return descendantIds;
|
|||
|
};
|
|||
|
|
|||
|
const invalidIds = [
|
|||
|
editingTerm.id,
|
|||
|
...findAllDescendantIds(editingTerm.id),
|
|||
|
];
|
|||
|
validParents = allTerms.filter((t) => !invalidIds.includes(t.id));
|
|||
|
}
|
|||
|
|
|||
|
// 如果是添加故障类型,只能选择网系类别作为父级
|
|||
|
if (!editingTerm && taxonomyId === deviceTypeTaxonomy?.id) {
|
|||
|
validParents = systemTypeTerms;
|
|||
|
}
|
|||
|
|
|||
|
// 如果是添加具体故障,只能选择故障类型作为父级
|
|||
|
if (
|
|||
|
!editingTerm &&
|
|||
|
taxonomyId &&
|
|||
|
taxonomyId !== systemTypeTaxonomy?.id &&
|
|||
|
taxonomyId !== deviceTypeTaxonomy?.id
|
|||
|
) {
|
|||
|
validParents = deviceTypeTerms;
|
|||
|
}
|
|||
|
|
|||
|
// 转换为TreeSelect需要的格式
|
|||
|
const buildTreeOptions = (items: any[], depth = 0): any[] => {
|
|||
|
return items
|
|||
|
.filter((item) => (depth === 0 ? !item.parentId : true))
|
|||
|
.map((item) => {
|
|||
|
const children = allTerms.filter((t) => t.parentId === item.id);
|
|||
|
return {
|
|||
|
title: "—".repeat(depth) + (depth > 0 ? " " : "") + item.name,
|
|||
|
value: item.id,
|
|||
|
children:
|
|||
|
children.length > 0
|
|||
|
? buildTreeOptions(children, depth + 1)
|
|||
|
: undefined,
|
|||
|
};
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
return buildTreeOptions(validParents);
|
|||
|
};
|
|||
|
|
|||
|
return (
|
|||
|
<div>
|
|||
|
<div
|
|||
|
style={{
|
|||
|
display: "flex",
|
|||
|
justifyContent: "flex-end",
|
|||
|
marginBottom: 16,
|
|||
|
alignItems: "center",
|
|||
|
gap: "10px",
|
|||
|
}}
|
|||
|
>
|
|||
|
<Input.Search
|
|||
|
placeholder={`根据${title}搜索`}
|
|||
|
onSearch={handleSearch}
|
|||
|
onChange={(e) => handleSearch(e.target.value)}
|
|||
|
value={searchValue}
|
|||
|
style={{ width: 400 }}
|
|||
|
allowClear
|
|||
|
/>
|
|||
|
<Button
|
|||
|
type="primary"
|
|||
|
icon={<PlusOutlined />}
|
|||
|
onClick={() => handleAdd()}
|
|||
|
>
|
|||
|
添加顶级{title}
|
|||
|
</Button>
|
|||
|
</div>
|
|||
|
|
|||
|
<Table
|
|||
|
dataSource={treeData}
|
|||
|
expandable={{
|
|||
|
defaultExpandAllRows: true,
|
|||
|
}}
|
|||
|
columns={[
|
|||
|
{
|
|||
|
title: "名称",
|
|||
|
dataIndex: "name",
|
|||
|
key: "name",
|
|||
|
},
|
|||
|
{
|
|||
|
title: "分类类型",
|
|||
|
key: "taxonomyType",
|
|||
|
render: (_, record) => {
|
|||
|
if (record.taxonomyId === systemTypeTaxonomy?.id) {
|
|||
|
return "网系类别";
|
|||
|
} else if (record.taxonomyId === deviceTypeTaxonomy?.id) {
|
|||
|
return "故障类型";
|
|||
|
} else {
|
|||
|
return "具体故障";
|
|||
|
}
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
title: "操作",
|
|||
|
key: "action",
|
|||
|
width: 200,
|
|||
|
render: (_, record: any) => (
|
|||
|
<Space>
|
|||
|
<Button
|
|||
|
type="text"
|
|||
|
icon={<PlusOutlined />}
|
|||
|
onClick={() => handleAdd(record)}
|
|||
|
style={{ color: "green" }}
|
|||
|
>
|
|||
|
添加子项
|
|||
|
</Button>
|
|||
|
<Button
|
|||
|
type="text"
|
|||
|
icon={<EditOutlined />}
|
|||
|
onClick={() => handleEdit(record)}
|
|||
|
style={{ color: "#1890ff" }}
|
|||
|
/>
|
|||
|
<Button
|
|||
|
type="text"
|
|||
|
danger
|
|||
|
icon={<DeleteOutlined />}
|
|||
|
onClick={() => handleDelete(record)}
|
|||
|
/>
|
|||
|
</Space>
|
|||
|
),
|
|||
|
},
|
|||
|
]}
|
|||
|
rowKey="id"
|
|||
|
pagination={false}
|
|||
|
rowClassName={(record, index) =>
|
|||
|
index % 2 === 0 ? "bg-white" : "bg-gray-100"
|
|||
|
}
|
|||
|
onHeaderRow={() => {
|
|||
|
return {
|
|||
|
style: {
|
|||
|
backgroundColor: "#d6e4ff",
|
|||
|
},
|
|||
|
};
|
|||
|
}}
|
|||
|
bordered
|
|||
|
size="middle"
|
|||
|
locale={{ emptyText: "暂无数据" }}
|
|||
|
/>
|
|||
|
|
|||
|
<Modal
|
|||
|
title={editingTerm ? `编辑${title}` : `添加${title}`}
|
|||
|
open={isModalVisible}
|
|||
|
onOk={handleSave}
|
|||
|
onCancel={() => setIsModalVisible(false)}
|
|||
|
>
|
|||
|
<div style={{ marginBottom: 16 }}>
|
|||
|
<label style={{ display: "block", marginBottom: 8 }}>名称:</label>
|
|||
|
<Input
|
|||
|
placeholder={`请输入${title}名称`}
|
|||
|
value={termName}
|
|||
|
onChange={(e) => setTermName(e.target.value)}
|
|||
|
/>
|
|||
|
</div>
|
|||
|
|
|||
|
{!editingTerm && (
|
|||
|
<div style={{ marginBottom: 16 }}>
|
|||
|
<label style={{ display: "block", marginBottom: 8 }}>
|
|||
|
分类类型:
|
|||
|
</label>
|
|||
|
<Select
|
|||
|
style={{ width: "100%" }}
|
|||
|
placeholder="请选择分类类型"
|
|||
|
value={taxonomyId}
|
|||
|
onChange={setTaxonomyId}
|
|||
|
disabled={taxonomySelectDisabled}
|
|||
|
options={taxonomies?.map((tax) => ({
|
|||
|
label: tax.name,
|
|||
|
value: tax.id,
|
|||
|
}))}
|
|||
|
/>
|
|||
|
</div>
|
|||
|
)}
|
|||
|
|
|||
|
<div>
|
|||
|
<label style={{ display: "block", marginBottom: 8 }}>
|
|||
|
上级分类:
|
|||
|
</label>
|
|||
|
<TreeSelect
|
|||
|
style={{ width: "100%" }}
|
|||
|
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
|
|||
|
placeholder="请选择上级分类"
|
|||
|
allowClear
|
|||
|
treeDefaultExpandAll
|
|||
|
value={parentId}
|
|||
|
onChange={setParentId}
|
|||
|
treeData={getParentOptions()}
|
|||
|
/>
|
|||
|
</div>
|
|||
|
</Modal>
|
|||
|
</div>
|
|||
|
);
|
|||
|
}
|