diff --git a/apps/server/src/models/resource/resource.router.ts b/apps/server/src/models/resource/resource.router.ts index 12afc82..ff3f9a4 100755 --- a/apps/server/src/models/resource/resource.router.ts +++ b/apps/server/src/models/resource/resource.router.ts @@ -74,5 +74,25 @@ export class ResourceRouter { const { staff } = ctx; 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); + }), }); } diff --git a/apps/server/src/models/resource/resource.service.ts b/apps/server/src/models/resource/resource.service.ts index 332bfe3..87e9e42 100755 --- a/apps/server/src/models/resource/resource.service.ts +++ b/apps/server/src/models/resource/resource.service.ts @@ -35,4 +35,22 @@ export class ResourceService extends BaseService { }, }); } + + // 添加关联设备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, + }, + }); + } } diff --git a/apps/web/public/LOGO.svg b/apps/web/public/LOGO.svg old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/devicepage/dashboard/page.tsx b/apps/web/src/app/main/devicepage/dashboard/page.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/devicepage/devicemodal/page.tsx b/apps/web/src/app/main/devicepage/devicemodal/page.tsx index 60a4b50..67e2e16 100755 --- a/apps/web/src/app/main/devicepage/devicemodal/page.tsx +++ b/apps/web/src/app/main/devicepage/devicemodal/page.tsx @@ -1,13 +1,17 @@ // import { api, useStaff, useDevice } from "@nice/client"; import { useMainContext } from "../../layout/MainProvider"; 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 { useEffect } from "react"; +import { useEffect, useState } from "react"; import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select"; import DepartmentSelect from "@web/src/components/models/department/department-select"; import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-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() { const { form, @@ -18,45 +22,124 @@ export default function DeviceModal() { setEditingRecord, } = useMainContext(); const { create, update } = useDevice(); + + // 添加状态跟踪当前选择的网系类别和故障类型 + const [selectedSystemType, setSelectedSystemType] = useState(); + const [selectedDeviceType, setSelectedDeviceType] = useState(); + + // 在组件顶层声明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 () => { try { const values = form.getFieldsValue(); + const { attachments = [], ...deviceData } = values; + let deviceId; + if (editingRecord?.id) { - // 编辑现有记录 await update.mutateAsync({ where: { id: editingRecord.id }, - data: values + data: deviceData, }); - toast.success("更新故障成功"); + deviceId = editingRecord.id; } else { - // 创建新记录 - await create.mutateAsync(values); - toast.success("创建故障成功"); + const result = await create.mutateAsync(deviceData); + deviceId = result.id; } - - // 关闭模态框并重置 + + // 如果有附件,将它们关联到此设备 + 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); setEditingRecord(null); form.resetFields(); } catch (error) { - console.error("保存故障信息失败:", error); + console.error("操作失败:", error); toast.error("操作失败"); } }; - // 当模态框关闭时清空编辑状态 - const handleCancel = () => { - setVisible(false); - setEditingRecord(null); - form.resetFields(); - }; + // 当模态框关闭时清空编辑状态 + const handleCancel = () => { + setVisible(false); + setEditingRecord(null); + 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 ? "编辑故障信息" : "新增故障"; return ( - <>
- + - + - + @@ -87,7 +178,7 @@ export default function DeviceModal() { > - + @@ -95,7 +186,6 @@ export default function DeviceModal() { - + + + + + + ); +}; + +export default TableHeader; diff --git a/apps/web/src/app/main/devicepage/devicetable/page.tsx b/apps/web/src/app/main/devicepage/devicetable/page.tsx index e41f46f..8dd0d41 100755 --- a/apps/web/src/app/main/devicepage/devicetable/page.tsx +++ b/apps/web/src/app/main/devicepage/devicetable/page.tsx @@ -18,6 +18,7 @@ import { } from "@ant-design/icons"; import dayjs from "dayjs"; import { utils, writeFile, read } from "xlsx"; +import ResourceFileList from "./components/ResourceFileList"; // 提取处理嵌套字段的函数 const getNestedValue = (record: any, dataIndex: string | string[]) => { @@ -49,11 +50,16 @@ const DeviceTable = forwardRef( const { create } = useDevice(); // 描述信息模态框状态:控制可见性和内容 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) => { setCurrentDesc({ + id: record.id, title: record.showname || "未知故障", desc: record.notes || "无故障详情", }); @@ -656,6 +662,14 @@ const DeviceTable = forwardRef(
{currentDesc.desc}
+ + {/* 添加附件展示区域 */} + {currentDesc?.id && ( +
+
相关附件
+ +
+ )} diff --git a/apps/web/src/app/main/devicepage/devicetable/services/ImportExportService.ts b/apps/web/src/app/main/devicepage/devicetable/services/ImportExportService.ts new file mode 100755 index 0000000..e32a835 --- /dev/null +++ b/apps/web/src/app/main/devicepage/devicetable/services/ImportExportService.ts @@ -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 + ) { + 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 + ) { + 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; \ No newline at end of file diff --git a/apps/web/src/app/main/devicepage/devicetable/utils/helpers.ts b/apps/web/src/app/main/devicepage/devicetable/utils/helpers.ts new file mode 100755 index 0000000..dbd2aa4 --- /dev/null +++ b/apps/web/src/app/main/devicepage/devicetable/utils/helpers.ts @@ -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 = { + 已修复: "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; +}; \ No newline at end of file diff --git a/apps/web/src/app/main/devicepage/page.tsx b/apps/web/src/app/main/devicepage/page.tsx index 6886364..9cef5e2 100755 --- a/apps/web/src/app/main/devicepage/page.tsx +++ b/apps/web/src/app/main/devicepage/page.tsx @@ -152,7 +152,6 @@ export default function DeviceMessage() { 新建 -
- - {/*
- - - -
*/} -
diff --git a/apps/web/src/app/main/devicepage/select/Fix-select.tsx b/apps/web/src/app/main/devicepage/select/Fix-select.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/devicepage/select/System-select.tsx b/apps/web/src/app/main/devicepage/select/System-select.tsx index 433c6f8..dffee94 100755 --- a/apps/web/src/app/main/devicepage/select/System-select.tsx +++ b/apps/web/src/app/main/devicepage/select/System-select.tsx @@ -1,7 +1,7 @@ // apps/web/src/components/models/term/system-type-select.tsx import { Select } from "antd"; import { api } from "@nice/client"; -import React from "react"; +import React, { useEffect, useState } from "react"; interface SystemTypeSelectProps { value?: string; @@ -10,6 +10,7 @@ interface SystemTypeSelectProps { disabled?: boolean; className?: string; style?: React.CSSProperties; + deviceTypeId?: string; // 添加故障类型ID参数用于筛选 } export default function SystemTypeSelect({ @@ -19,30 +20,116 @@ export default function SystemTypeSelect({ disabled = false, className, style, + deviceTypeId, // 接收故障类型ID }: SystemTypeSelectProps) { - const { data: terms, isLoading } = api.term.findMany.useQuery({ - where: { - taxonomy: { slug: "system_type" }, - deletedAt: null, - parentId: null, // 只查询顶级网系类别 - }, - orderBy: { order: "asc" }, - }); + const [options, setOptions] = useState<{ label: string; value: string }[]>( + [] + ); + const [loading, setLoading] = useState(false); - const options = - terms?.map((term) => ({ - label: term.name, - value: term.id, - })) || []; + // 获取所有网系类别 + const { data: allSystemTypes, isLoading: isSystemLoading } = + api.term.findMany.useQuery({ + where: { + taxonomy: { slug: "system_type" }, + deletedAt: null, + parentId: null, // 只查询顶级网系类别 + }, + orderBy: { order: "asc" }, + }); + + // 获取所有故障类型数据(包括父级信息) + 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, + 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 (