book_manage/apps/web/src/app/main/devicepage/select/Device-manager.tsx

632 lines
20 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// apps/web/src/components/models/term/term-manager.tsx
import {
Button,
Input,
Modal,
Space,
Table,
TreeSelect,
Select,
Card,
Row,
Col,
Typography,
Divider,
} from "antd";
import { api } from "@nice/client";
import { useState, useEffect } from "react";
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
FileTextOutlined,
} from "@ant-design/icons";
import { ObjectType } from "@nice/common";
const { Title } = Typography;
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);
const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
// ... 保持原有的 API 调用和逻辑 ...
// 获取所有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" },
});
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
};
const handlePageSizeChange = (current: number, size: number) => {
setCurrentPage(1); // 重置到第一页
setPageSize(size);
};
// 构建包含两种分类的树形数据
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();
},
});
// 操作处理函数
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 className="p-6 min-h-screen bg-gray-50">
{/* 主要内容卡片 */}
<Card className="shadow-sm" bodyStyle={{ padding: 0 }}>
{/* 统计信息区域 */}
<div className="p-4 bg-gray-50 border-b border-gray-100">
<Row gutter={16} justify="center">
<Col span={8}>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">
{systemTypeTerms?.length || 0}
</div>
<div className="text-sm text-gray-600"></div>
</div>
</Col>
<Col span={8}>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{deviceTypeTerms?.filter((term) =>
systemTypeTerms?.some(
(sysType) => sysType.id === term.parentId
)
).length || 0}
</div>
<div className="text-sm text-gray-600"></div>
</div>
</Col>
{/* <Col span={8}> */}
{/* <div className="text-center"> */}
{/* <div className="text-2xl font-bold text-orange-600">
{deviceTypeTerms?.filter(term =>
!systemTypeTerms?.some(sysType => sysType.id === term.parentId) &&
deviceTypeTerms?.some(devType => devType.id === term.parentId)
).length || 0}
</div>
<div className="text-sm text-gray-600">具体故障</div> */}
{/* </div>
</Col> */}
</Row>
</div>
{/* 工具栏区域 */}
<div className="p-4 border-b border-gray-100 bg-white">
<Row gutter={[16, 16]} align="middle" justify="space-between">
<Col xs={24} sm={16} md={12} lg={10}>
<Input.Search
placeholder={`搜索${title}名称...`}
onSearch={handleSearch}
onChange={(e) => handleSearch(e.target.value)}
value={searchValue}
allowClear
prefix={<SearchOutlined className="text-gray-400" />}
className="w-full"
size="middle"
/>
</Col>
<Col xs={24} sm={8} md={12} lg={14} className="flex justify-end">
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleAdd()}
size="middle"
className="shadow-sm"
>
{title}
</Button>
</Space>
</Col>
</Row>
</div>
{/* 表格区域 */}
<div className="p-4">
<Table
dataSource={treeData}
expandable={{
defaultExpandAllRows: false, // 改为不默认展开所有行
expandRowByClick: true,
indentSize: 20, // 设置缩进大小
}}
columns={[
{
title: "名称",
dataIndex: "name",
key: "name",
width: "50%", // 增加名称列的宽度
render: (text, record, index, ) => (
<div className="flex items-center gap-2">
{/* 根据层级添加不同的图标 */}
{record.taxonomyId === systemTypeTaxonomy?.id ? (
<span className="text-blue-500">📁</span>
) : record.taxonomyId === deviceTypeTaxonomy?.id ? (
<span className="text-green-500">📂</span>
) : (
<span className="text-orange-500">📄</span>
)}
<span className="font-medium">{text}</span>
</div>
),
},
{
title: "分类类型",
key: "taxonomyType",
width: "25%",
render: (_, record) => {
if (record.taxonomyId === systemTypeTaxonomy?.id) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
</span>
);
} else if (record.taxonomyId === deviceTypeTaxonomy?.id) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
</span>
);
} else {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
</span>
);
}
},
},
{
title: "操作",
key: "action",
width: "25%",
render: (_, record: any) => (
<div className="flex items-center gap-1">
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => handleAdd(record)}
className="text-green-600 hover:text-green-700 hover:bg-green-50"
size="small"
title="添加子项"
/>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
size="small"
title="编辑"
/>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
size="small"
title="删除"
className="hover:bg-red-50"
/>
</div>
),
},
]}
rowKey="id"
pagination={{
pageSize: pageSize, // 每页显示数量
current: currentPage,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: [ "10", "15", "20"],
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
}}
rowClassName={(record, index) =>
`hover:bg-blue-50 transition-colors duration-200 ${
record.taxonomyId === systemTypeTaxonomy?.id
? "bg-blue-25"
: record.taxonomyId === deviceTypeTaxonomy?.id
? "bg-green-25"
: "bg-orange-25"
}`
}
onHeaderRow={() => ({
style: {
backgroundColor: "#f8fafc",
fontWeight: "600",
borderBottom: "2px solid #e2e8f0",
},
})}
bordered={true} // 改为有边框
size="middle"
locale={{ emptyText: "暂无数据" }}
className="rounded-lg overflow-hidden shadow-sm"
showHeader={true}
scroll={{ x: 800 }} // 添加横向滚动
/>
</div>
</Card>
{/* 编辑/添加模态框 */}
<Modal
title={
<div className="flex items-center gap-2">
{editingTerm ? <EditOutlined /> : <PlusOutlined />}
{editingTerm ? `编辑${title}` : `添加${title}`}
</div>
}
open={isModalVisible}
onOk={handleSave}
onCancel={() => setIsModalVisible(false)}
okText="保存"
cancelText="取消"
width={600}
destroyOnClose
>
<Divider className="mt-4 mb-6" />
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<Input
placeholder={`请输入${title}名称`}
value={termName}
onChange={(e) => setTermName(e.target.value)}
size="large"
/>
</div>
{!editingTerm && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<Select
style={{ width: "100%" }}
placeholder="请选择分类类型"
value={taxonomyId}
onChange={setTaxonomyId}
disabled={taxonomySelectDisabled}
size="large"
options={taxonomies?.map((tax) => ({
label: tax.name,
value: tax.id,
}))}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<TreeSelect
style={{ width: "100%" }}
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
placeholder="请选择上级分类(可选)"
allowClear
treeDefaultExpandAll
value={parentId}
onChange={setParentId}
treeData={getParentOptions()}
size="large"
/>
</div>
</div>
</Modal>
</div>
);
}