新增上传和下载附件功能,选择器根据设备id联动
This commit is contained in:
parent
38d93ddeab
commit
fbc2c567e2
|
@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 { 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,28 +22,94 @@ export default function DeviceModal() {
|
|||
setEditingRecord,
|
||||
} = useMainContext();
|
||||
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 () => {
|
||||
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("操作失败");
|
||||
}
|
||||
};
|
||||
|
@ -51,12 +121,25 @@ export default function DeviceModal() {
|
|||
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 (
|
||||
|
||||
<>
|
||||
<Modal
|
||||
title={modalTitle}
|
||||
|
@ -71,12 +154,20 @@ export default function DeviceModal() {
|
|||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="systemType" label="网系类别">
|
||||
<SystemTypeSelect className="rounded-lg" />
|
||||
<SystemTypeSelect
|
||||
className="rounded-lg"
|
||||
onChange={handleSystemTypeChange}
|
||||
deviceTypeId={selectedDeviceType} // 传递当前选中的故障类型ID
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="deviceType" label="故障类型">
|
||||
<DeviceTypeSelect className="rounded-lg" />
|
||||
<DeviceTypeSelect
|
||||
className="rounded-lg"
|
||||
systemTypeId={selectedSystemType}
|
||||
onChange={handleDeviceTypeChange} // 添加故障类型变化的处理函数
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
|
@ -95,7 +186,6 @@ export default function DeviceModal() {
|
|||
<DepartmentSelect />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Form.Item name="deviceStatus" label="故障状态">
|
||||
<Select className="rounded-lg" placeholder="请选择故障状态">
|
||||
|
@ -106,9 +196,39 @@ export default function DeviceModal() {
|
|||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="notes" label="描述">
|
||||
<Input.TextArea rows={4} className="rounded-lg" />
|
||||
{/* 使用Card和Tabs组件合并描述和附件上传 */}
|
||||
<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>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "attachments",
|
||||
label: "附件",
|
||||
children: (
|
||||
<Form.Item name="attachments" noStyle>
|
||||
<TusUploader
|
||||
multiple={true}
|
||||
description="点击或拖拽文件到此区域上传故障相关附件"
|
||||
/>
|
||||
</Form.Item>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Form>
|
||||
</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";
|
||||
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(
|
|||
<div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap">
|
||||
{currentDesc.desc}
|
||||
</div>
|
||||
|
||||
{/* 添加附件展示区域 */}
|
||||
{currentDesc?.id && (
|
||||
<div className="mt-4">
|
||||
<div className="text-lg font-medium mb-2">相关附件</div>
|
||||
<ResourceFileList deviceId={currentDesc.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<SystemTypeSelect
|
||||
|
@ -204,13 +203,6 @@ export default function DeviceMessage() {
|
|||
重置
|
||||
</Button>
|
||||
</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>
|
||||
<DeviceTable ref={tableRef} onSelectedChange={handleSelectedChange} />
|
||||
<DeviceModal />
|
||||
|
|
|
@ -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,8 +20,16 @@ export default function SystemTypeSelect({
|
|||
disabled = false,
|
||||
className,
|
||||
style,
|
||||
deviceTypeId, // 接收故障类型ID
|
||||
}: 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: {
|
||||
taxonomy: { slug: "system_type" },
|
||||
deletedAt: null,
|
||||
|
@ -29,20 +38,98 @@ export default function SystemTypeSelect({
|
|||
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,
|
||||
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 (
|
||||
<Select
|
||||
loading={isLoading}
|
||||
loading={loading}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
disabled={disabled || (deviceTypeId && options.length === 1)} // 如果只有一个选项且有故障类型ID,则禁用选择
|
||||
className={className}
|
||||
style={style}
|
||||
showSearch
|
||||
|
|
|
@ -2,6 +2,7 @@ import { HTMLMotionProps, motion } from 'framer-motion';
|
|||
import { forwardRef, ReactNode } from 'react';
|
||||
import { cn } from '@web/src/utils/classname';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
|
||||
export interface ButtonProps extends Omit<HTMLMotionProps<"button">, keyof React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning' | 'info' | 'light' | 'dark' |
|
||||
|
|
|
@ -4,6 +4,7 @@ export const env: {
|
|||
VERSION: string;
|
||||
FILE_PORT: string;
|
||||
SERVER_PORT: string;
|
||||
UPLOAD_PORT: string;
|
||||
} = {
|
||||
APP_NAME: import.meta.env.PROD
|
||||
? (window as any).env.VITE_APP_APP_NAME
|
||||
|
@ -20,4 +21,7 @@ export const env: {
|
|||
VERSION: import.meta.env.PROD
|
||||
? (window as any).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")
|
||||
}
|
||||
|
||||
|
||||
|
||||
model Resource {
|
||||
id String @id @default(cuid()) @map("id")
|
||||
title String? @map("title")
|
||||
|
@ -199,6 +197,8 @@ model Resource {
|
|||
isPublic Boolean? @default(true) @map("is_public")
|
||||
owner Staff? @relation(fields: [ownerId], references: [id])
|
||||
ownerId String? @map("owner_id")
|
||||
deviceId String? @map("device_id")
|
||||
device Device? @relation("DeviceResources", fields: [deviceId], references: [id])
|
||||
|
||||
// 索引
|
||||
@@index([type])
|
||||
|
@ -226,6 +226,7 @@ model Device {
|
|||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
resources Resource[] @relation("DeviceResources")
|
||||
|
||||
@@index([deptId])
|
||||
@@index([systemType])
|
||||
|
@ -233,4 +234,3 @@ model Device {
|
|||
@@index([responsiblePerson])
|
||||
@@map("device")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue