新增上传和下载附件功能,选择器根据设备id联动
This commit is contained in:
parent
38d93ddeab
commit
fbc2c567e2
|
@ -74,5 +74,25 @@ export class ResourceRouter {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.resourceService.findManyWithCursor(input);
|
return await this.resourceService.findManyWithCursor(input);
|
||||||
}),
|
}),
|
||||||
|
findByDeviceId: this.trpc.procedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
deviceId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return this.resourceService.findByDeviceId(input.deviceId);
|
||||||
|
}),
|
||||||
|
linkToDevice: this.trpc.procedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
fileId: z.string(),
|
||||||
|
deviceId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const { fileId, deviceId } = input;
|
||||||
|
return this.resourceService.linkToDevice(fileId, deviceId);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,4 +35,22 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加关联设备ID的方法
|
||||||
|
async linkToDevice(fileId: string, deviceId: string) {
|
||||||
|
return this.update({
|
||||||
|
where: { fileId },
|
||||||
|
data: { deviceId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加根据设备ID查询资源的方法
|
||||||
|
async findByDeviceId(deviceId: string) {
|
||||||
|
return this.findMany({
|
||||||
|
where: {
|
||||||
|
deviceId,
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
@ -1,13 +1,17 @@
|
||||||
// import { api, useStaff, useDevice } from "@nice/client";
|
// import { api, useStaff, useDevice } from "@nice/client";
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Button, Form, Input, Modal, Select, Row, Col } from "antd";
|
import { Button, Form, Input, Modal, Select, Row, Col, Tabs, Card } from "antd";
|
||||||
import { useDevice } from "@nice/client";
|
import { useDevice } from "@nice/client";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select";
|
||||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||||
import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-select";
|
import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-select";
|
||||||
import DeviceTypeSelect from "../select/Device-select";
|
import DeviceTypeSelect from "../select/Device-select";
|
||||||
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
|
import { api } from "@nice/client";
|
||||||
|
import type { TabsProps } from "antd";
|
||||||
|
|
||||||
export default function DeviceModal() {
|
export default function DeviceModal() {
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
|
@ -18,28 +22,94 @@ export default function DeviceModal() {
|
||||||
setEditingRecord,
|
setEditingRecord,
|
||||||
} = useMainContext();
|
} = useMainContext();
|
||||||
const { create, update } = useDevice();
|
const { create, update } = useDevice();
|
||||||
|
|
||||||
|
// 添加状态跟踪当前选择的网系类别和故障类型
|
||||||
|
const [selectedSystemType, setSelectedSystemType] = useState<string>();
|
||||||
|
const [selectedDeviceType, setSelectedDeviceType] = useState<string>();
|
||||||
|
|
||||||
|
// 在组件顶层声明mutation
|
||||||
|
const linkToDeviceMutation = api.resource.linkToDevice.useMutation();
|
||||||
|
|
||||||
|
// 初始化时从表单获取初始值
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
const systemType = form.getFieldValue("systemType");
|
||||||
|
const deviceType = form.getFieldValue("deviceType");
|
||||||
|
setSelectedSystemType(systemType);
|
||||||
|
setSelectedDeviceType(deviceType);
|
||||||
|
}
|
||||||
|
}, [visible, form]);
|
||||||
|
|
||||||
|
// 监听表单中系统类型的变化
|
||||||
|
useEffect(() => {
|
||||||
|
const systemType = form.getFieldValue("systemType");
|
||||||
|
|
||||||
|
if (systemType !== selectedSystemType) {
|
||||||
|
setSelectedSystemType(systemType);
|
||||||
|
}
|
||||||
|
}, [form.getFieldValue("systemType")]);
|
||||||
|
|
||||||
|
// 监听表单中故障类型的变化
|
||||||
|
useEffect(() => {
|
||||||
|
const deviceType = form.getFieldValue("deviceType");
|
||||||
|
|
||||||
|
if (deviceType !== selectedDeviceType) {
|
||||||
|
setSelectedDeviceType(deviceType);
|
||||||
|
}
|
||||||
|
}, [form.getFieldValue("deviceType")]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
try {
|
try {
|
||||||
const values = form.getFieldsValue();
|
const values = form.getFieldsValue();
|
||||||
|
const { attachments = [], ...deviceData } = values;
|
||||||
|
let deviceId;
|
||||||
|
|
||||||
if (editingRecord?.id) {
|
if (editingRecord?.id) {
|
||||||
// 编辑现有记录
|
|
||||||
await update.mutateAsync({
|
await update.mutateAsync({
|
||||||
where: { id: editingRecord.id },
|
where: { id: editingRecord.id },
|
||||||
data: values
|
data: deviceData,
|
||||||
});
|
});
|
||||||
toast.success("更新故障成功");
|
deviceId = editingRecord.id;
|
||||||
} else {
|
} else {
|
||||||
// 创建新记录
|
const result = await create.mutateAsync(deviceData);
|
||||||
await create.mutateAsync(values);
|
deviceId = result.id;
|
||||||
toast.success("创建故障成功");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭模态框并重置
|
// 如果有附件,将它们关联到此设备
|
||||||
|
if (attachments.length > 0 && deviceId) {
|
||||||
|
try {
|
||||||
|
console.log("开始关联附件到设备:", deviceId, attachments);
|
||||||
|
|
||||||
|
// 使用组件顶层声明的mutation
|
||||||
|
for (const fileId of attachments) {
|
||||||
|
if (!fileId) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`正在关联文件 ${fileId} 到设备 ${deviceId}`);
|
||||||
|
|
||||||
|
// 使用已在顶层声明的mutation
|
||||||
|
await linkToDeviceMutation.mutateAsync({
|
||||||
|
fileId,
|
||||||
|
deviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`文件 ${fileId} 关联成功`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`文件 ${fileId} 关联失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("关联附件失败:", err);
|
||||||
|
toast.error("附件关联失败,但设备信息已保存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(editingRecord?.id ? "更新故障成功" : "创建故障成功");
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setEditingRecord(null);
|
setEditingRecord(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("保存故障信息失败:", error);
|
console.error("操作失败:", error);
|
||||||
toast.error("操作失败");
|
toast.error("操作失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -51,12 +121,25 @@ export default function DeviceModal() {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理网系类别变化
|
||||||
|
const handleSystemTypeChange = (value: string) => {
|
||||||
|
setSelectedSystemType(value);
|
||||||
|
// 如果网系类别变化,且故障类型不属于此网系类别,则清空故障类型
|
||||||
|
if (selectedDeviceType) {
|
||||||
|
form.setFieldValue("deviceType", undefined);
|
||||||
|
setSelectedDeviceType(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理故障类型变化
|
||||||
|
const handleDeviceTypeChange = (value: string) => {
|
||||||
|
setSelectedDeviceType(value);
|
||||||
|
};
|
||||||
|
|
||||||
// 模态框标题根据是否编辑而变化
|
// 模态框标题根据是否编辑而变化
|
||||||
const modalTitle = editingRecord?.id ? "编辑故障信息" : "新增故障";
|
const modalTitle = editingRecord?.id ? "编辑故障信息" : "新增故障";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
title={modalTitle}
|
title={modalTitle}
|
||||||
|
@ -71,12 +154,20 @@ export default function DeviceModal() {
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="systemType" label="网系类别">
|
<Form.Item name="systemType" label="网系类别">
|
||||||
<SystemTypeSelect className="rounded-lg" />
|
<SystemTypeSelect
|
||||||
|
className="rounded-lg"
|
||||||
|
onChange={handleSystemTypeChange}
|
||||||
|
deviceTypeId={selectedDeviceType} // 传递当前选中的故障类型ID
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="deviceType" label="故障类型">
|
<Form.Item name="deviceType" label="故障类型">
|
||||||
<DeviceTypeSelect className="rounded-lg" />
|
<DeviceTypeSelect
|
||||||
|
className="rounded-lg"
|
||||||
|
systemTypeId={selectedSystemType}
|
||||||
|
onChange={handleDeviceTypeChange} // 添加故障类型变化的处理函数
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
@ -95,7 +186,6 @@ export default function DeviceModal() {
|
||||||
<DepartmentSelect />
|
<DepartmentSelect />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="deviceStatus" label="故障状态">
|
<Form.Item name="deviceStatus" label="故障状态">
|
||||||
<Select className="rounded-lg" placeholder="请选择故障状态">
|
<Select className="rounded-lg" placeholder="请选择故障状态">
|
||||||
|
@ -106,9 +196,39 @@ export default function DeviceModal() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item name="notes" label="描述">
|
{/* 使用Card和Tabs组件合并描述和附件上传 */}
|
||||||
<Input.TextArea rows={4} className="rounded-lg" />
|
<Card className="mt-2 rounded-lg">
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="description"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "description",
|
||||||
|
label: "描述",
|
||||||
|
children: (
|
||||||
|
<Form.Item name="notes" noStyle>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
className="rounded-lg"
|
||||||
|
placeholder="请输入故障描述信息"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "attachments",
|
||||||
|
label: "附件",
|
||||||
|
children: (
|
||||||
|
<Form.Item name="attachments" noStyle>
|
||||||
|
<TusUploader
|
||||||
|
multiple={true}
|
||||||
|
description="点击或拖拽文件到此区域上传故障相关附件"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Modal, Button } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
interface DescriptionModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DescriptionModal: React.FC<DescriptionModalProps> = ({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={title}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="text-lg font-medium mb-2">故障详情</div>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DescriptionModal;
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Button, Modal } from "antd";
|
||||||
|
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import React from "react";
|
||||||
|
interface DeviceActionsProps {
|
||||||
|
record: any;
|
||||||
|
onEdit: (record: any) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeviceActions: React.FC<DeviceActionsProps> = ({
|
||||||
|
record,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}) => {
|
||||||
|
const handleDelete = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认删除",
|
||||||
|
icon: <ExclamationCircleOutlined />,
|
||||||
|
content: `确定要删除故障 "${record.showname || "未命名故障"}" 吗?`,
|
||||||
|
okText: "删除",
|
||||||
|
okType: "danger",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk() {
|
||||||
|
onDelete(record.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-2 justify-center">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => onEdit(record)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button danger onClick={handleDelete}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceActions;
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { ColumnsType } from "antd/es/table";
|
||||||
|
import { formatDateTime } from "../utils/helpers";
|
||||||
|
import DeviceStatus from "./DeviceStatus";
|
||||||
|
import DeviceActions from "./DeviceActions";
|
||||||
|
|
||||||
|
interface CreateColumnsProps {
|
||||||
|
systemTypeTerms: any[];
|
||||||
|
deviceTypeTerms: any[];
|
||||||
|
onShowDesc: (record: any) => void;
|
||||||
|
onEdit: (record: any) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createColumns = ({
|
||||||
|
systemTypeTerms,
|
||||||
|
deviceTypeTerms,
|
||||||
|
onShowDesc,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: CreateColumnsProps): ColumnsType<any> => {
|
||||||
|
// 根据术语ID和类型获取术语名称
|
||||||
|
const getTermNameById = (termId: string, termType: string) => {
|
||||||
|
if (!termId) return "未知";
|
||||||
|
const terms = termType === "system_type" ? systemTypeTerms : deviceTypeTerms;
|
||||||
|
const term = terms?.find((t) => t.id === termId);
|
||||||
|
return term?.name || "未知";
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "网系类别",
|
||||||
|
dataIndex: "systemType",
|
||||||
|
key: "systemType",
|
||||||
|
align: "center",
|
||||||
|
render: (text, record) => {
|
||||||
|
return getTermNameById(record.systemType, "system_type");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "故障类型",
|
||||||
|
dataIndex: "deviceType",
|
||||||
|
key: "deviceType",
|
||||||
|
align: "center",
|
||||||
|
render: (text, record) => {
|
||||||
|
return getTermNameById(record.deviceType, "device_type");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "单位",
|
||||||
|
dataIndex: "deptId",
|
||||||
|
key: "deptId",
|
||||||
|
align: "center",
|
||||||
|
render: (text, record) => {
|
||||||
|
// 如果有部门关联,显示部门名称
|
||||||
|
if (record.deptId && (record as any)?.department?.name) {
|
||||||
|
return (record as any)?.department?.name;
|
||||||
|
}
|
||||||
|
// 否则显示responsiblePerson(如果存在)
|
||||||
|
if (record.responsiblePerson) {
|
||||||
|
return record.responsiblePerson;
|
||||||
|
}
|
||||||
|
// 最后才显示未知
|
||||||
|
return "未知";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "故障名称",
|
||||||
|
dataIndex: "showname",
|
||||||
|
key: "showname",
|
||||||
|
align: "center",
|
||||||
|
render: (text, record) => (
|
||||||
|
<div
|
||||||
|
onClick={() => onShowDesc(record)}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "8px 0",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text || "未命名故障"}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "故障状态",
|
||||||
|
dataIndex: "deviceStatus",
|
||||||
|
key: "deviceStatus",
|
||||||
|
align: "center",
|
||||||
|
render: (status) => <DeviceStatus status={status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "时间",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
key: "createdAt",
|
||||||
|
align: "center",
|
||||||
|
render: (text, record) => formatDateTime(record.createdAt),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
align: "center",
|
||||||
|
render: (_, record) => (
|
||||||
|
<DeviceActions
|
||||||
|
record={record}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createColumns;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Tag } from "antd";
|
||||||
|
import { getStatusConfig } from "../utils/helpers";
|
||||||
|
import React from "react";
|
||||||
|
interface DeviceStatusProps {
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeviceStatus: React.FC<DeviceStatusProps> = ({ status }) => {
|
||||||
|
const config = getStatusConfig(status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
color={config.color}
|
||||||
|
style={{ minWidth: "60px", textAlign: "center" }}
|
||||||
|
>
|
||||||
|
{config.text}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceStatus;
|
|
@ -0,0 +1,305 @@
|
||||||
|
// 新建文件: apps/web/src/components/resource/resource-file-list.tsx
|
||||||
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Empty,
|
||||||
|
List,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
Tabs,
|
||||||
|
Image,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
DownloadOutlined,
|
||||||
|
FileOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
FileImageOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { api } from "@nice/client";
|
||||||
|
import { env } from "@web/src/env";
|
||||||
|
import { getCompressedImageUrl, formatFileSize } from "@nice/utils";
|
||||||
|
import { getFileIcon } from "@web/src/components/common/uploader/utils";
|
||||||
|
|
||||||
|
interface ResourceFileListProps {
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceItem {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
fileId?: string;
|
||||||
|
createdAt?: Date | string;
|
||||||
|
url?: string;
|
||||||
|
type?: string;
|
||||||
|
meta?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
||||||
|
// 直接在组件顶层使用useQuery
|
||||||
|
const { data, isLoading, refetch } = api.resource.findMany.useQuery(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
deviceId: deviceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
staleTime: 3000,
|
||||||
|
enabled: !!deviceId, // 只有当deviceId存在时才执行查询
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
// 每3秒刷新一次数据
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (deviceId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [deviceId, refetch]);
|
||||||
|
// 使用useMemo处理资源数据
|
||||||
|
const { resources, imageResources, fileResources } = useMemo(() => {
|
||||||
|
if (!data || !Array.isArray(data))
|
||||||
|
return { resources: [], imageResources: [], fileResources: [] };
|
||||||
|
|
||||||
|
const isImage = (url: string) => /\.(png|jpg|jpeg|gif|webp)$/i.test(url);
|
||||||
|
|
||||||
|
// 使用显式类型断言,避免递归类型推断
|
||||||
|
const processedResources = data.map((resource: any) => {
|
||||||
|
const fileUrl = resource.url || resource.fileId;
|
||||||
|
if (!fileUrl) return resource;
|
||||||
|
|
||||||
|
const originalUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
|
||||||
|
const isImageFile = isImage(fileUrl);
|
||||||
|
// console.log("环境变量:", {
|
||||||
|
// SERVER_IP: env.SERVER_IP,
|
||||||
|
// FILE_PORT: env.FILE_PORT,
|
||||||
|
// fileUrl: fileUrl,
|
||||||
|
// });
|
||||||
|
return {
|
||||||
|
...resource,
|
||||||
|
originalUrl,
|
||||||
|
isImage: isImageFile,
|
||||||
|
} as ResourceItem & { originalUrl: string; isImage: boolean };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources: processedResources,
|
||||||
|
imageResources: processedResources.filter((item) => item.isImage),
|
||||||
|
fileResources: processedResources.filter((item) => !item.isImage),
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// 获取文件URL(保留你的tabs结构,但改进URL生成逻辑)
|
||||||
|
const getFileUrl = (fileId: string | undefined) => {
|
||||||
|
if (!fileId) return "";
|
||||||
|
|
||||||
|
// 使用与ResourceShower相同的URL格式
|
||||||
|
return `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCompressedUrl = (fileId: string | undefined) => {
|
||||||
|
if (!fileId) return "";
|
||||||
|
return getCompressedImageUrl(getFileUrl(fileId));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-4 bg-gray-50 rounded-md">
|
||||||
|
加载附件中...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resources.length === 0) {
|
||||||
|
return (
|
||||||
|
<Empty description="暂无附件" className="bg-gray-50 p-4 rounded-md" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 rounded-md p-3">
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="all"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "all",
|
||||||
|
label: "全部附件",
|
||||||
|
children: (
|
||||||
|
<List
|
||||||
|
grid={{ gutter: 16, column: 2 }}
|
||||||
|
dataSource={resources}
|
||||||
|
renderItem={(item: any) => (
|
||||||
|
<List.Item>
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
hoverable
|
||||||
|
className="flex flex-col h-full"
|
||||||
|
actions={[
|
||||||
|
...(item.isImage
|
||||||
|
? [
|
||||||
|
<Button
|
||||||
|
key="preview"
|
||||||
|
type="link"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(
|
||||||
|
item.originalUrl || getFileUrl(item.fileId),
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
预览
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
<Button
|
||||||
|
key="download"
|
||||||
|
type="link"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
const url =
|
||||||
|
"originalUrl" in item
|
||||||
|
? (item as any).originalUrl
|
||||||
|
: getFileUrl(item.fileId);
|
||||||
|
if (url) window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
{item.isImage ? (
|
||||||
|
<FileImageOutlined
|
||||||
|
style={{ fontSize: 24, color: "#1890ff" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getFileIcon(item.url || "")
|
||||||
|
)}
|
||||||
|
<Typography.Text strong className="ml-2 truncate">
|
||||||
|
{item.title || `文件-${item.id}`}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Divider className="my-2" />
|
||||||
|
<Typography.Text type="secondary" className="text-xs">
|
||||||
|
文件大小:{" "}
|
||||||
|
{item.meta?.size
|
||||||
|
? formatFileSize(item.meta.size)
|
||||||
|
: "未知"}
|
||||||
|
</Typography.Text>
|
||||||
|
<br />
|
||||||
|
<Typography.Text type="secondary" className="text-xs">
|
||||||
|
上传时间:{" "}
|
||||||
|
{item.createdAt
|
||||||
|
? new Date(item.createdAt).toLocaleString()
|
||||||
|
: "未知"}
|
||||||
|
</Typography.Text>
|
||||||
|
</Card>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "images",
|
||||||
|
label: "图片附件",
|
||||||
|
children:
|
||||||
|
imageResources.length > 0 ? (
|
||||||
|
<div className="p-2">
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{imageResources.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.fileId || item.id}
|
||||||
|
className="aspect-square rounded-lg overflow-hidden shadow-sm border border-gray-100 relative group flex items-center justify-center bg-gray-100"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
item.originalUrl || getCompressedUrl(item.fileId)
|
||||||
|
}
|
||||||
|
alt={item.title || "图片"}
|
||||||
|
preview={{
|
||||||
|
src: item.originalUrl || getFileUrl(item.fileId),
|
||||||
|
mask: (
|
||||||
|
<div className="flex items-center justify-center text-white">
|
||||||
|
预览图片
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "contain",
|
||||||
|
objectPosition: "center",
|
||||||
|
}}
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white p-1 text-xs opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{item.title || `图片-${item.id}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty description="暂无图片附件" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "files",
|
||||||
|
label: "文档附件",
|
||||||
|
children:
|
||||||
|
fileResources.length > 0 ? (
|
||||||
|
<div className="rounded-xl p-1 border border-gray-100 bg-white">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{fileResources.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.fileId || item.id}
|
||||||
|
className="flex-shrink-0 relative active:scale-95 transition-transform select-none"
|
||||||
|
href={item.originalUrl || getFileUrl(item.fileId)}
|
||||||
|
target="_blank"
|
||||||
|
download={true}
|
||||||
|
title={`点击下载"${item.title || "未命名文件"}"`}
|
||||||
|
>
|
||||||
|
<div className="w-[120px] h-[90px] p-2 flex flex-col items-center justify-between rounded-xl hover:bg-blue-50 cursor-pointer">
|
||||||
|
<div className="text-blue-600 text-xl">
|
||||||
|
{getFileIcon(item.url || "")}
|
||||||
|
</div>
|
||||||
|
<div className="w-full text-center space-y-0.5">
|
||||||
|
<p className="text-xs font-medium text-gray-800 truncate">
|
||||||
|
{item.title?.slice(0, 12) || "未命名"}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<span className="bg-gray-100 px-1 rounded-sm">
|
||||||
|
{item.url?.split(".").pop()?.toUpperCase() ||
|
||||||
|
"文件"}
|
||||||
|
</span>
|
||||||
|
<span className="bg-gray-100 px-1 rounded-sm">
|
||||||
|
{item.meta?.size
|
||||||
|
? formatFileSize(item.meta.size)
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty description="暂无文档附件" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { ImportOutlined, ExportOutlined } from "@ant-design/icons";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface TableHeaderProps {
|
||||||
|
selectedCount: number;
|
||||||
|
onExportTemplate: () => void;
|
||||||
|
onImport: (file: File) => void;
|
||||||
|
onExport: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableHeader: React.FC<TableHeaderProps> = ({
|
||||||
|
selectedCount,
|
||||||
|
onExportTemplate,
|
||||||
|
onImport,
|
||||||
|
onExport,
|
||||||
|
}) => {
|
||||||
|
const uploadRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleImportClick = () => {
|
||||||
|
if (uploadRef.current) {
|
||||||
|
uploadRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
onImport(files[0]);
|
||||||
|
// 清空文件输入,允许再次选择同一文件
|
||||||
|
if (uploadRef.current) uploadRef.current.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-between mb-2">
|
||||||
|
<span></span>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={uploadRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
accept=".xlsx,.xls,.csv"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<ImportOutlined />}
|
||||||
|
type="primary"
|
||||||
|
onClick={onExportTemplate}
|
||||||
|
>
|
||||||
|
下载模板文件
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ImportOutlined />}
|
||||||
|
type="primary"
|
||||||
|
onClick={handleImportClick}
|
||||||
|
>
|
||||||
|
导入
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={onExport}
|
||||||
|
disabled={selectedCount === 0}
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
>
|
||||||
|
{selectedCount > 0 ? `导出 (${selectedCount})项数据` : "导出选中数据"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableHeader;
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { utils, writeFile, read } from "xlsx";
|
import { utils, writeFile, read } from "xlsx";
|
||||||
|
import ResourceFileList from "./components/ResourceFileList";
|
||||||
|
|
||||||
// 提取处理嵌套字段的函数
|
// 提取处理嵌套字段的函数
|
||||||
const getNestedValue = (record: any, dataIndex: string | string[]) => {
|
const getNestedValue = (record: any, dataIndex: string | string[]) => {
|
||||||
|
@ -49,11 +50,16 @@ const DeviceTable = forwardRef(
|
||||||
const { create } = useDevice();
|
const { create } = useDevice();
|
||||||
// 描述信息模态框状态:控制可见性和内容
|
// 描述信息模态框状态:控制可见性和内容
|
||||||
const [descModalVisible, setDescModalVisible] = useState(false);
|
const [descModalVisible, setDescModalVisible] = useState(false);
|
||||||
const [currentDesc, setCurrentDesc] = useState({ title: "", desc: "" });
|
const [currentDesc, setCurrentDesc] = useState<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
}>({ id: "", title: "", desc: "" });
|
||||||
|
|
||||||
// 处理点击故障名称显示描述信息
|
// 处理点击故障名称显示描述信息
|
||||||
const handleShowDesc = (record) => {
|
const handleShowDesc = (record) => {
|
||||||
setCurrentDesc({
|
setCurrentDesc({
|
||||||
|
id: record.id,
|
||||||
title: record.showname || "未知故障",
|
title: record.showname || "未知故障",
|
||||||
desc: record.notes || "无故障详情",
|
desc: record.notes || "无故障详情",
|
||||||
});
|
});
|
||||||
|
@ -656,6 +662,14 @@ const DeviceTable = forwardRef(
|
||||||
<div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap">
|
<div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap">
|
||||||
{currentDesc.desc}
|
{currentDesc.desc}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 添加附件展示区域 */}
|
||||||
|
{currentDesc?.id && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="text-lg font-medium mb-2">相关附件</div>
|
||||||
|
<ResourceFileList deviceId={currentDesc.id} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { utils, writeFile, read } from "xlsx";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { findTermIdByName, findDeptIdByName, getStatusKeyByValue } from "../utils/helpers";
|
||||||
|
|
||||||
|
export class ImportExportService {
|
||||||
|
// 导出选中数据
|
||||||
|
static exportSelectedData(
|
||||||
|
selectedData: any[],
|
||||||
|
getTermNameById: (id: string, type: string) => string
|
||||||
|
) {
|
||||||
|
if (!selectedData || selectedData.length === 0) {
|
||||||
|
toast.error("没有选中任何数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 格式化导出数据
|
||||||
|
const exportData = selectedData.map((item) => ({
|
||||||
|
网系类别: getTermNameById(item.systemType, "system_type"),
|
||||||
|
故障类型: getTermNameById(item.deviceType, "device_type"),
|
||||||
|
单位: (item as any)?.department?.name || "未知",
|
||||||
|
故障名称: item?.showname || "未命名故障",
|
||||||
|
故障状态: (() => {
|
||||||
|
const statusMap = {
|
||||||
|
normal: "已修复",
|
||||||
|
maintenance: "维修中",
|
||||||
|
broken: "未修复",
|
||||||
|
idle: "闲置",
|
||||||
|
};
|
||||||
|
return statusMap[item.deviceStatus] || "未知";
|
||||||
|
})(),
|
||||||
|
时间: item.createdAt
|
||||||
|
? dayjs(item.createdAt).format("YYYY-MM-DD")
|
||||||
|
: "未知",
|
||||||
|
描述: item.notes || "无描述",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 创建工作簿
|
||||||
|
const wb = utils.book_new();
|
||||||
|
const ws = utils.json_to_sheet(exportData);
|
||||||
|
utils.book_append_sheet(wb, ws, "故障数据");
|
||||||
|
|
||||||
|
// 导出Excel文件
|
||||||
|
writeFile(wb, `故障数据_${dayjs().format("YYYYMMDD_HHmmss")}.xlsx`);
|
||||||
|
|
||||||
|
toast.success(`成功导出 ${selectedData.length} 条数据`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出数据错误:", error);
|
||||||
|
toast.error("导出失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出模板
|
||||||
|
static exportTemplate(systemTypeTerms: any[], deviceTypeTerms: any[]) {
|
||||||
|
try {
|
||||||
|
// 创建一个示例记录
|
||||||
|
const templateData = [
|
||||||
|
{
|
||||||
|
网系类别: systemTypeTerms?.[0]?.name || "网系类别1",
|
||||||
|
故障类型: deviceTypeTerms?.[0]?.name || "故障类型1",
|
||||||
|
单位: "单位名称",
|
||||||
|
故障名称: "示例故障名称",
|
||||||
|
故障状态: "未修复", // 可选值:已修复, 维修中, 未修复
|
||||||
|
描述: "这是一个示例描述",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 创建工作簿
|
||||||
|
const wb = utils.book_new();
|
||||||
|
const ws = utils.json_to_sheet(templateData);
|
||||||
|
utils.book_append_sheet(wb, ws, "导入模板");
|
||||||
|
|
||||||
|
// 导出Excel文件
|
||||||
|
writeFile(wb, `故障导入模板.xlsx`);
|
||||||
|
|
||||||
|
toast.success("已下载导入模板");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导出模板失败:", error);
|
||||||
|
toast.error("下载模板失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入数据
|
||||||
|
static async importData(
|
||||||
|
file: File,
|
||||||
|
systemTypeTerms: any[],
|
||||||
|
deviceTypeTerms: any[],
|
||||||
|
departments: any[],
|
||||||
|
devices: any[],
|
||||||
|
createRecord: (data: any) => Promise<any>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 读取Excel/CSV文件
|
||||||
|
const data = await file.arrayBuffer();
|
||||||
|
const workbook = read(data);
|
||||||
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
const jsonData = utils.sheet_to_json(worksheet);
|
||||||
|
|
||||||
|
// 转换为符合系统格式的数据
|
||||||
|
const records = jsonData.map((row: any) => {
|
||||||
|
const unitName = row["单位"] ? String(row["单位"]) : "";
|
||||||
|
return {
|
||||||
|
systemType: findTermIdByName(row["网系类别"], systemTypeTerms),
|
||||||
|
deviceType: findTermIdByName(row["故障类型"], deviceTypeTerms),
|
||||||
|
showname: row["故障名称"] ? String(row["故障名称"]) : "",
|
||||||
|
// 尝试关联部门ID
|
||||||
|
deptId: unitName ? findDeptIdByName(unitName, departments, devices) : null,
|
||||||
|
// 同时保存原始单位名称
|
||||||
|
responsiblePerson: unitName,
|
||||||
|
deviceStatus: getStatusKeyByValue(row["故障状态"]),
|
||||||
|
notes: row["描述"] ? String(row["描述"]) : "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确认是否有有效数据
|
||||||
|
if (records.length === 0) {
|
||||||
|
toast.error("未找到有效数据");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量创建记录
|
||||||
|
await this.batchImportRecords(records, createRecord);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导入失败:", error);
|
||||||
|
toast.error("导入失败,请检查文件格式");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量导入记录
|
||||||
|
static async batchImportRecords(
|
||||||
|
records: any[],
|
||||||
|
createRecord: (data: any) => Promise<any>
|
||||||
|
) {
|
||||||
|
if (!records || records.length === 0) return;
|
||||||
|
try {
|
||||||
|
// 过滤出有效记录
|
||||||
|
const validRecords = records.filter(
|
||||||
|
(record) => record.systemType || record.deviceType || record.showname
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validRecords.length === 0) {
|
||||||
|
toast.error("没有找到有效的记录数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置批处理大小
|
||||||
|
const batchSize = 5;
|
||||||
|
let successCount = 0;
|
||||||
|
let totalProcessed = 0;
|
||||||
|
// 显示进度提示
|
||||||
|
const loadingToast = toast.loading(`正在导入数据...`);
|
||||||
|
// 分批处理数据
|
||||||
|
for (let i = 0; i < validRecords.length; i += batchSize) {
|
||||||
|
const batch = validRecords.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
// 串行处理每一批数据
|
||||||
|
for (const record of batch) {
|
||||||
|
try {
|
||||||
|
await createRecord(record);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`导入记录失败: ${record.showname || "未命名"}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalProcessed += batch.length;
|
||||||
|
// 更新导入进度
|
||||||
|
toast.loading(
|
||||||
|
`已处理 ${totalProcessed}/${validRecords.length} 条数据...`,
|
||||||
|
{ id: loadingToast }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
|
// 显示结果
|
||||||
|
if (successCount === validRecords.length) {
|
||||||
|
toast.success(`成功导入 ${successCount} 条数据`);
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
`成功导入 ${successCount}/${validRecords.length} 条数据,部分记录导入失败`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("批量导入失败:", error);
|
||||||
|
toast.error("导入过程中发生错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportExportService;
|
|
@ -0,0 +1,79 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
// 提取处理嵌套字段的函数
|
||||||
|
export const getNestedValue = (record: any, dataIndex: string | string[]) => {
|
||||||
|
if (Array.isArray(dataIndex)) {
|
||||||
|
return dataIndex.reduce((obj, key) => obj?.[key], record);
|
||||||
|
}
|
||||||
|
return record[dataIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据术语ID和类型获取术语名称
|
||||||
|
export const getTermNameById = (termId: string, terms: any[], fallback = "未知") => {
|
||||||
|
if (!termId) return fallback;
|
||||||
|
const term = terms?.find((t) => t.id === termId);
|
||||||
|
return term?.name || fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态键值对应
|
||||||
|
export const getStatusKeyByValue = (value: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
已修复: "normal",
|
||||||
|
维修中: "maintenance",
|
||||||
|
"未修复": "broken",
|
||||||
|
};
|
||||||
|
return statusMap[value] || "normal";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据状态获取显示配置
|
||||||
|
export const getStatusConfig = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
normal: {
|
||||||
|
text: "已修复",
|
||||||
|
color: "success",
|
||||||
|
},
|
||||||
|
maintenance: {
|
||||||
|
text: "维修中",
|
||||||
|
color: "processing",
|
||||||
|
},
|
||||||
|
broken: {
|
||||||
|
text: "未修复",
|
||||||
|
color: "error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusConfig[status] || {
|
||||||
|
text: "未知",
|
||||||
|
color: "default",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
export const formatDateTime = (dateTime: string | Date | null) => {
|
||||||
|
return dateTime ? dayjs(dateTime).format("YYYY-MM-DD HH:mm:ss") : "未知";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查找术语ID
|
||||||
|
export const findTermIdByName = (
|
||||||
|
name: string,
|
||||||
|
terms: any[]
|
||||||
|
) => {
|
||||||
|
if (!name) return null;
|
||||||
|
const term = terms?.find((t) => t.name === name);
|
||||||
|
return term?.id || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查找部门ID
|
||||||
|
export const findDeptIdByName = (name: string, departments: any[], devices: any[]) => {
|
||||||
|
if (!name || name === "未知") return null;
|
||||||
|
// 直接从部门数据中查找
|
||||||
|
const department = departments?.find((dept) => dept.name === name);
|
||||||
|
if (department) {
|
||||||
|
return department.id;
|
||||||
|
}
|
||||||
|
// 备用:尝试从设备中查找
|
||||||
|
const matchedDevice = devices?.find(
|
||||||
|
(device) => (device as any)?.department?.name === name
|
||||||
|
);
|
||||||
|
return matchedDevice?.deptId || null;
|
||||||
|
};
|
|
@ -152,7 +152,6 @@ export default function DeviceMessage() {
|
||||||
新建
|
新建
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 mb-4">
|
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||||||
<div className="flex-1 min-w-[200px]">
|
<div className="flex-1 min-w-[200px]">
|
||||||
<SystemTypeSelect
|
<SystemTypeSelect
|
||||||
|
@ -204,13 +203,6 @@ export default function DeviceMessage() {
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className="flex justify-end gap-2 mb-4 border-b pb-2">
|
|
||||||
<Button icon={<ExportOutlined />}>下载模板文件</Button>
|
|
||||||
<Button icon={<ImportOutlined />}>导入</Button>
|
|
||||||
<Button icon={<ExportOutlined />}>导出选中数据</Button>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<DeviceTable ref={tableRef} onSelectedChange={handleSelectedChange} />
|
<DeviceTable ref={tableRef} onSelectedChange={handleSelectedChange} />
|
||||||
<DeviceModal />
|
<DeviceModal />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// apps/web/src/components/models/term/system-type-select.tsx
|
// apps/web/src/components/models/term/system-type-select.tsx
|
||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
import { api } from "@nice/client";
|
import { api } from "@nice/client";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface SystemTypeSelectProps {
|
interface SystemTypeSelectProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
@ -10,6 +10,7 @@ interface SystemTypeSelectProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
deviceTypeId?: string; // 添加故障类型ID参数用于筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SystemTypeSelect({
|
export default function SystemTypeSelect({
|
||||||
|
@ -19,8 +20,16 @@ export default function SystemTypeSelect({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
deviceTypeId, // 接收故障类型ID
|
||||||
}: SystemTypeSelectProps) {
|
}: SystemTypeSelectProps) {
|
||||||
const { data: terms, isLoading } = api.term.findMany.useQuery({
|
const [options, setOptions] = useState<{ label: string; value: string }[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 获取所有网系类别
|
||||||
|
const { data: allSystemTypes, isLoading: isSystemLoading } =
|
||||||
|
api.term.findMany.useQuery({
|
||||||
where: {
|
where: {
|
||||||
taxonomy: { slug: "system_type" },
|
taxonomy: { slug: "system_type" },
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
@ -29,20 +38,98 @@ export default function SystemTypeSelect({
|
||||||
orderBy: { order: "asc" },
|
orderBy: { order: "asc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const options =
|
// 获取所有故障类型数据(包括父级信息)
|
||||||
terms?.map((term) => ({
|
const { data: allDeviceTypes, isLoading: isDeviceTypesLoading } =
|
||||||
|
api.term.findMany.useQuery({
|
||||||
|
where: {
|
||||||
|
taxonomy: { slug: "device_type" },
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理网系类别列表
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(isSystemLoading || isDeviceTypesLoading);
|
||||||
|
|
||||||
|
if (allSystemTypes) {
|
||||||
|
// 如果指定了故障类型
|
||||||
|
if (deviceTypeId && allDeviceTypes) {
|
||||||
|
// 查找故障类型对应的信息
|
||||||
|
const deviceType = allDeviceTypes.find((dt) => dt.id === deviceTypeId);
|
||||||
|
|
||||||
|
if (deviceType && deviceType.parentId) {
|
||||||
|
// 找到对应的网系类别
|
||||||
|
const parentSystemType = allSystemTypes.find(
|
||||||
|
(st) => st.id === deviceType.parentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parentSystemType) {
|
||||||
|
// 只显示该故障类型对应的网系类别
|
||||||
|
setOptions([
|
||||||
|
{
|
||||||
|
label: parentSystemType.name,
|
||||||
|
value: parentSystemType.id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 如果当前值与父级不匹配,则自动更新表单值
|
||||||
|
if (value !== parentSystemType.id) {
|
||||||
|
onChange?.(parentSystemType.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果找不到对应的网系类别,则显示所有
|
||||||
|
setOptions(
|
||||||
|
allSystemTypes.map((term) => ({
|
||||||
label: term.name,
|
label: term.name,
|
||||||
value: term.id,
|
value: term.id,
|
||||||
})) || [];
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果故障类型没有父级,显示所有网系类别
|
||||||
|
setOptions(
|
||||||
|
allSystemTypes.map((term) => ({
|
||||||
|
label: term.name,
|
||||||
|
value: term.id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有指定故障类型,显示所有网系类别
|
||||||
|
setOptions(
|
||||||
|
allSystemTypes.map((term) => ({
|
||||||
|
label: term.name,
|
||||||
|
value: term.id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
allSystemTypes,
|
||||||
|
deviceTypeId,
|
||||||
|
allDeviceTypes,
|
||||||
|
isSystemLoading,
|
||||||
|
isDeviceTypesLoading,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
loading={isLoading}
|
loading={loading}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled || (deviceTypeId && options.length === 1)} // 如果只有一个选项且有故障类型ID,则禁用选择
|
||||||
className={className}
|
className={className}
|
||||||
style={style}
|
style={style}
|
||||||
showSearch
|
showSearch
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { HTMLMotionProps, motion } from 'framer-motion';
|
||||||
import { forwardRef, ReactNode } from 'react';
|
import { forwardRef, ReactNode } from 'react';
|
||||||
import { cn } from '@web/src/utils/classname';
|
import { cn } from '@web/src/utils/classname';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export interface ButtonProps extends Omit<HTMLMotionProps<"button">, keyof React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
export interface ButtonProps extends Omit<HTMLMotionProps<"button">, keyof React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning' | 'info' | 'light' | 'dark' |
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning' | 'info' | 'light' | 'dark' |
|
||||||
|
|
|
@ -4,6 +4,7 @@ export const env: {
|
||||||
VERSION: string;
|
VERSION: string;
|
||||||
FILE_PORT: string;
|
FILE_PORT: string;
|
||||||
SERVER_PORT: string;
|
SERVER_PORT: string;
|
||||||
|
UPLOAD_PORT: string;
|
||||||
} = {
|
} = {
|
||||||
APP_NAME: import.meta.env.PROD
|
APP_NAME: import.meta.env.PROD
|
||||||
? (window as any).env.VITE_APP_APP_NAME
|
? (window as any).env.VITE_APP_APP_NAME
|
||||||
|
@ -20,4 +21,7 @@ export const env: {
|
||||||
VERSION: import.meta.env.PROD
|
VERSION: import.meta.env.PROD
|
||||||
? (window as any).env.VITE_APP_VERSION
|
? (window as any).env.VITE_APP_VERSION
|
||||||
: import.meta.env.VITE_APP_VERSION,
|
: import.meta.env.VITE_APP_VERSION,
|
||||||
|
UPLOAD_PORT: import.meta.env.PROD
|
||||||
|
? (window as any).env.VITE_APP_UPLOAD_PORT
|
||||||
|
: import.meta.env.VITE_APP_UPLOAD_PORT,
|
||||||
};
|
};
|
||||||
|
|
|
@ -178,8 +178,6 @@ model AppConfig {
|
||||||
@@map("app_config")
|
@@map("app_config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model Resource {
|
model Resource {
|
||||||
id String @id @default(cuid()) @map("id")
|
id String @id @default(cuid()) @map("id")
|
||||||
title String? @map("title")
|
title String? @map("title")
|
||||||
|
@ -199,6 +197,8 @@ model Resource {
|
||||||
isPublic Boolean? @default(true) @map("is_public")
|
isPublic Boolean? @default(true) @map("is_public")
|
||||||
owner Staff? @relation(fields: [ownerId], references: [id])
|
owner Staff? @relation(fields: [ownerId], references: [id])
|
||||||
ownerId String? @map("owner_id")
|
ownerId String? @map("owner_id")
|
||||||
|
deviceId String? @map("device_id")
|
||||||
|
device Device? @relation("DeviceResources", fields: [deviceId], references: [id])
|
||||||
|
|
||||||
// 索引
|
// 索引
|
||||||
@@index([type])
|
@@index([type])
|
||||||
|
@ -226,6 +226,7 @@ model Device {
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
|
resources Resource[] @relation("DeviceResources")
|
||||||
|
|
||||||
@@index([deptId])
|
@@index([deptId])
|
||||||
@@index([systemType])
|
@@index([systemType])
|
||||||
|
@ -233,4 +234,3 @@ model Device {
|
||||||
@@index([responsiblePerson])
|
@@index([responsiblePerson])
|
||||||
@@map("device")
|
@@map("device")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue