新增历史搜索、下载附件、分页调整页面功能
This commit is contained in:
parent
fbc2c567e2
commit
33185aa340
|
@ -231,7 +231,11 @@ const DashboardPage = () => {
|
|||
type: 'value',
|
||||
name: '故障数量'
|
||||
},
|
||||
series: data.series
|
||||
series: data.series,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
Divider,
|
||||
Tabs,
|
||||
Image,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
|
@ -28,6 +29,7 @@ interface ResourceFileListProps {
|
|||
deviceId: string;
|
||||
}
|
||||
|
||||
//表示资源项的结构
|
||||
interface ResourceItem {
|
||||
id?: string;
|
||||
title?: string;
|
||||
|
@ -52,16 +54,61 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
enabled: !!deviceId, // 只有当deviceId存在时才执行查询
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 每3秒刷新一次数据
|
||||
// 每5秒刷新一次数据,确保显示最新的附件信息。
|
||||
const intervalId = setInterval(() => {
|
||||
if (deviceId) {
|
||||
refetch();
|
||||
}
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [deviceId, refetch]);
|
||||
|
||||
// 下载文件的函数,支持中文文件名
|
||||
const downloadFile = async (item: any) => {
|
||||
console.log("下载文件:", item);
|
||||
try {
|
||||
const url = item.originalUrl || getFileUrl(item.fileId);
|
||||
if (!url) {
|
||||
message.error("文件链接无效");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
const fileExtension = item.url?.split(".").pop() || "";
|
||||
const fileName = item.title
|
||||
? `${item.title}${fileExtension ? "." + fileExtension : ""}`
|
||||
: `文件-${item.id}${fileExtension ? "." + fileExtension : ""}`;
|
||||
|
||||
// 使用fetch下载文件
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("下载失败");
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
// 创建下载链接
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = fileName; // 设置文件名
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理下载链接
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
message.success("下载成功");
|
||||
} catch (error) {
|
||||
console.error("下载失败:", error);
|
||||
message.error("下载失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 使用useMemo处理资源数据
|
||||
const { resources, imageResources, fileResources } = useMemo(() => {
|
||||
if (!data || !Array.isArray(data))
|
||||
|
@ -73,7 +120,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
const processedResources = data.map((resource: any) => {
|
||||
const fileUrl = resource.url || resource.fileId;
|
||||
if (!fileUrl) return resource;
|
||||
|
||||
// 构建文件URL
|
||||
const originalUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
|
||||
const isImageFile = isImage(fileUrl);
|
||||
// console.log("环境变量:", {
|
||||
|
@ -89,6 +136,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
});
|
||||
|
||||
return {
|
||||
// 将资源数据分为图片和文件两类,便于后续的展示和处理。
|
||||
resources: processedResources,
|
||||
imageResources: processedResources.filter((item) => item.isImage),
|
||||
fileResources: processedResources.filter((item) => !item.isImage),
|
||||
|
@ -103,11 +151,13 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
return `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileId}`;
|
||||
};
|
||||
|
||||
// 获取压缩后的图片URL
|
||||
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">
|
||||
|
@ -116,6 +166,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// 如果没有任何附件,显示空状态
|
||||
if (resources.length === 0) {
|
||||
return (
|
||||
<Empty description="暂无附件" className="bg-gray-50 p-4 rounded-md" />
|
||||
|
@ -163,13 +214,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
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");
|
||||
}}
|
||||
onClick={() => downloadFile(item)}
|
||||
>
|
||||
下载
|
||||
</Button>,
|
||||
|
@ -261,15 +306,13 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
<div className="rounded-xl p-1 border border-gray-100 bg-white">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fileResources.map((item) => (
|
||||
<a
|
||||
<div
|
||||
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}
|
||||
className="flex-shrink-0 relative active:scale-95 transition-transform select-none cursor-pointer"
|
||||
onClick={() => downloadFile(item)}
|
||||
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="w-[120px] h-[90px] p-2 flex flex-col items-center justify-between rounded-xl hover:bg-blue-50">
|
||||
<div className="text-blue-600 text-xl">
|
||||
{getFileIcon(item.url || "")}
|
||||
</div>
|
||||
|
@ -290,7 +333,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -50,6 +50,8 @@ const DeviceTable = forwardRef(
|
|||
const { create } = useDevice();
|
||||
// 描述信息模态框状态:控制可见性和内容
|
||||
const [descModalVisible, setDescModalVisible] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [currentDesc, setCurrentDesc] = useState<{
|
||||
id: string;
|
||||
title: string;
|
||||
|
@ -146,6 +148,16 @@ const DeviceTable = forwardRef(
|
|||
},
|
||||
});
|
||||
};
|
||||
// 在分页配置中添加事件处理
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (current: number, size: number) => {
|
||||
setCurrentPage(1); // 重置到第一页
|
||||
setPageSize(size);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{
|
||||
|
@ -413,7 +425,6 @@ const DeviceTable = forwardRef(
|
|||
);
|
||||
return matchedDevice?.deptId || null;
|
||||
};
|
||||
|
||||
// 获取状态键
|
||||
const getStatusKeyByValue = (value: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
|
@ -423,7 +434,6 @@ const DeviceTable = forwardRef(
|
|||
};
|
||||
return statusMap[value] || "normal";
|
||||
};
|
||||
|
||||
// 改进批量导入记录函数
|
||||
// 改进批量导入记录函数,使用分批处理
|
||||
const batchImportRecords = async (records: any[]) => {
|
||||
|
@ -447,7 +457,6 @@ const DeviceTable = forwardRef(
|
|||
// 分批处理数据
|
||||
for (let i = 0; i < validRecords.length; i += batchSize) {
|
||||
const batch = validRecords.slice(i, i + batchSize);
|
||||
|
||||
// 串行处理每一批数据
|
||||
for (const record of batch) {
|
||||
try {
|
||||
|
@ -476,7 +485,6 @@ const DeviceTable = forwardRef(
|
|||
`成功导入 ${successCount}/${validRecords.length} 条数据,部分记录导入失败`
|
||||
);
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
refetch();
|
||||
} catch (error) {
|
||||
|
@ -484,7 +492,6 @@ const DeviceTable = forwardRef(
|
|||
toast.error("导入过程中发生错误");
|
||||
}
|
||||
};
|
||||
|
||||
// 添加导出模板功能
|
||||
const handleExportTemplate = () => {
|
||||
try {
|
||||
|
@ -514,14 +521,12 @@ const DeviceTable = forwardRef(
|
|||
toast.error("下载模板失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 触发文件选择
|
||||
const handleImportClick = () => {
|
||||
if (uploadRef.current) {
|
||||
uploadRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: onSelectChange,
|
||||
|
@ -622,12 +627,15 @@ const DeviceTable = forwardRef(
|
|||
pagination={{
|
||||
position: ["bottomCenter"],
|
||||
className: "flex justify-center mt-4",
|
||||
pageSize: 10,
|
||||
pageSize: pageSize,
|
||||
current: currentPage,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ["10", "20", "30"],
|
||||
pageSizeOptions: ["10", "15", "20"],
|
||||
responsive: true,
|
||||
showTotal: (total, range) => `共${total} 条数据`,
|
||||
showQuickJumper: true,
|
||||
onChange: handlePageChange,
|
||||
onShowSizeChange: handlePageSizeChange,
|
||||
}}
|
||||
components={{
|
||||
header: {
|
||||
|
@ -662,7 +670,6 @@ const DeviceTable = forwardRef(
|
|||
<div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap">
|
||||
{currentDesc.desc}
|
||||
</div>
|
||||
|
||||
{/* 添加附件展示区域 */}
|
||||
{currentDesc?.id && (
|
||||
<div className="mt-4">
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { Button, DatePicker, Form, Input, Modal, Select } from "antd";
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
AutoComplete,
|
||||
Tag,
|
||||
Dropdown,
|
||||
Space,
|
||||
} from "antd";
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import _ from "lodash";
|
||||
import { useMainContext } from "../layout/MainProvider";
|
||||
|
@ -14,13 +25,18 @@ import {
|
|||
ExportOutlined,
|
||||
UpOutlined,
|
||||
DownOutlined,
|
||||
HistoryOutlined,
|
||||
CloseOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||
|
||||
import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-select";
|
||||
import DeviceTypeSelect from "@web/src/app/main/devicepage/select/Device-select";
|
||||
import dayjs from "dayjs";
|
||||
import FixTypeSelect from "./select/Fix-select";
|
||||
import { api } from "@nice/client";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
// 添加筛选条件类型
|
||||
type SearchCondition = {
|
||||
deletedAt: null;
|
||||
|
@ -34,6 +50,15 @@ type SearchCondition = {
|
|||
};
|
||||
};
|
||||
|
||||
// 搜索历史类型
|
||||
type SearchHistory = {
|
||||
id: string;
|
||||
keyword: string;
|
||||
conditions: SearchCondition;
|
||||
timestamp: number;
|
||||
label: string; // 用于显示的标签
|
||||
};
|
||||
|
||||
export default function DeviceMessage() {
|
||||
const {
|
||||
form,
|
||||
|
@ -43,15 +68,20 @@ export default function DeviceMessage() {
|
|||
setSearchValue,
|
||||
editingRecord,
|
||||
} = useMainContext();
|
||||
|
||||
// 控制展开/收起状态
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// 添加所有筛选条件的状态
|
||||
const [selectedSystem, setSelectedSystem] = useState<string | null>(null);
|
||||
const [selectedDeviceType, setSelectedDeviceType] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [selectedDept, setSelectedDept] = useState<string | null>(null);
|
||||
const [time, setTime] = useState<string>("");
|
||||
// 修改为日期范围
|
||||
const [dateRange, setDateRange] = useState<
|
||||
[dayjs.Dayjs | null, dayjs.Dayjs | null] | null
|
||||
>(null);
|
||||
const [ipAddress, setIpAddress] = useState<string>("");
|
||||
const [macAddress, setMacAddress] = useState<string>("");
|
||||
const [serialNumber, setSerialNumber] = useState<string>("");
|
||||
|
@ -63,19 +93,156 @@ export default function DeviceMessage() {
|
|||
const [selectedSystemTypeId, setSelectedSystemTypeId] = useState<string>("");
|
||||
const [selectedFixType, setSelectedFixType] = useState<string | null>(null);
|
||||
|
||||
// 搜索历史相关状态
|
||||
const [searchHistory, setSearchHistory] = useState<SearchHistory[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState<string>("");
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
// 创建ref以访问DeviceTable内部方法
|
||||
const tableRef = useRef(null);
|
||||
// 存储选中行的状态
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const [selectedData, setSelectedData] = useState<any[]>([]);
|
||||
|
||||
// 添加数据查询以获取名称信息
|
||||
const { data: systemTypeTerms } = api.term.findMany.useQuery({
|
||||
where: { taxonomy: { slug: "system_type" }, deletedAt: null },
|
||||
orderBy: { order: "asc" },
|
||||
});
|
||||
|
||||
const { data: deviceTypeTerms } = api.term.findMany.useQuery({
|
||||
where: { taxonomy: { slug: "device_type" }, deletedAt: null },
|
||||
orderBy: { order: "asc" },
|
||||
});
|
||||
|
||||
const { data: departments } = api.department.findMany.useQuery({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
|
||||
// 根据ID获取名称的辅助函数
|
||||
const getSystemTypeName = (id: string) => {
|
||||
const term = systemTypeTerms?.find((t) => t.id === id);
|
||||
return term?.name || id;
|
||||
};
|
||||
|
||||
const getDeviceTypeName = (id: string) => {
|
||||
const term = deviceTypeTerms?.find((t) => t.id === id);
|
||||
return term?.name || id;
|
||||
};
|
||||
|
||||
const getDepartmentName = (id: string) => {
|
||||
const dept = departments?.find((d) => d.id === id);
|
||||
return dept?.name || id;
|
||||
};
|
||||
|
||||
// 初始化时加载搜索历史
|
||||
useEffect(() => {
|
||||
const savedHistory = localStorage.getItem("device-search-history");
|
||||
if (savedHistory) {
|
||||
try {
|
||||
setSearchHistory(JSON.parse(savedHistory));
|
||||
} catch (error) {
|
||||
console.error("加载搜索历史失败:", error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 保存搜索历史到localStorage
|
||||
const saveSearchHistory = (history: SearchHistory[]) => {
|
||||
localStorage.setItem("device-search-history", JSON.stringify(history));
|
||||
setSearchHistory(history);
|
||||
};
|
||||
|
||||
// 生成搜索标签 - 修改为显示名称而不是ID,并支持日期范围
|
||||
const generateSearchLabel = (
|
||||
conditions: SearchCondition,
|
||||
keyword: string
|
||||
) => {
|
||||
const parts = [];
|
||||
if (keyword) parts.push(`关键词: ${keyword}`);
|
||||
if (conditions.systemType)
|
||||
parts.push(`网系: ${getSystemTypeName(conditions.systemType)}`);
|
||||
if (conditions.deviceType)
|
||||
parts.push(`故障类型: ${getDeviceTypeName(conditions.deviceType)}`);
|
||||
if (conditions.deptId)
|
||||
parts.push(`单位: ${getDepartmentName(conditions.deptId)}`);
|
||||
if (conditions.createdAt) {
|
||||
const startDate = dayjs(conditions.createdAt.gte).format("YYYY-MM-DD");
|
||||
const endDate = dayjs(conditions.createdAt.lte).format("YYYY-MM-DD");
|
||||
if (startDate === endDate) {
|
||||
parts.push(`日期: ${startDate}`);
|
||||
} else {
|
||||
parts.push(`日期: ${startDate} ~ ${endDate}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(" | ") : "全部故障";
|
||||
};
|
||||
|
||||
// 添加搜索历史
|
||||
const addSearchHistory = (
|
||||
conditions: SearchCondition,
|
||||
keyword: string = ""
|
||||
) => {
|
||||
const newHistory: SearchHistory = {
|
||||
id: Date.now().toString(),
|
||||
keyword,
|
||||
conditions,
|
||||
timestamp: Date.now(),
|
||||
label: generateSearchLabel(conditions, keyword),
|
||||
};
|
||||
|
||||
const updatedHistory = [
|
||||
newHistory,
|
||||
...searchHistory.filter((h) => h.label !== newHistory.label),
|
||||
].slice(0, 10); // 只保留最近10条
|
||||
|
||||
saveSearchHistory(updatedHistory);
|
||||
};
|
||||
|
||||
// 应用历史搜索 - 修改为支持日期范围
|
||||
const applyHistorySearch = (history: SearchHistory) => {
|
||||
const { conditions, keyword } = history;
|
||||
|
||||
// 恢复搜索条件
|
||||
setSelectedSystem(conditions.systemType || null);
|
||||
setSelectedDeviceType(conditions.deviceType || null);
|
||||
setSelectedDept(conditions.deptId || null);
|
||||
setSelectedFixType(conditions.responsiblePerson?.contains || null);
|
||||
setSearchKeyword(keyword);
|
||||
|
||||
if (conditions.createdAt) {
|
||||
const startDate = dayjs(conditions.createdAt.gte);
|
||||
const endDate = dayjs(conditions.createdAt.lte);
|
||||
setDateRange([startDate, endDate]);
|
||||
} else {
|
||||
setDateRange(null);
|
||||
}
|
||||
|
||||
// 应用搜索条件
|
||||
setSearchValue(conditions as any);
|
||||
setShowHistory(false);
|
||||
};
|
||||
|
||||
// 删除搜索历史
|
||||
const removeSearchHistory = (historyId: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const updatedHistory = searchHistory.filter((h) => h.id !== historyId);
|
||||
saveSearchHistory(updatedHistory);
|
||||
};
|
||||
|
||||
// 清空所有搜索历史
|
||||
const clearAllHistory = () => {
|
||||
saveSearchHistory([]);
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
form.setFieldsValue(formValue);
|
||||
console.log(editingRecord);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 查询按钮点击处理
|
||||
// 查询按钮点击处理 - 修改为支持日期范围
|
||||
const handleSearch = () => {
|
||||
// 构建查询条件
|
||||
const whereCondition: SearchCondition = {
|
||||
|
@ -84,29 +251,37 @@ export default function DeviceMessage() {
|
|||
...(selectedDeviceType && { deviceType: selectedDeviceType }),
|
||||
...(selectedFixType && { deviceStatus: selectedFixType }),
|
||||
...(selectedDept && { deptId: selectedDept }),
|
||||
...(time && {
|
||||
...(dateRange &&
|
||||
dateRange[0] &&
|
||||
dateRange[1] && {
|
||||
createdAt: {
|
||||
gte: dayjs(time).startOf("day").toISOString(),
|
||||
lte: dayjs(time).endOf("day").toISOString(),
|
||||
gte: dateRange[0].startOf("day").toISOString(),
|
||||
lte: dateRange[1].endOf("day").toISOString(),
|
||||
},
|
||||
}),
|
||||
|
||||
// ...(status && { status: { contains: status } }),
|
||||
...(searchKeyword && {
|
||||
OR: [
|
||||
{ description: { contains: searchKeyword } },
|
||||
{ location: { contains: searchKeyword } },
|
||||
{ responsiblePerson: { contains: searchKeyword } },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
// 添加到搜索历史
|
||||
addSearchHistory(whereCondition, searchKeyword);
|
||||
|
||||
// 更新查询条件到全局上下文
|
||||
setSearchValue(whereCondition as any);
|
||||
|
||||
// 也可以直接使用refetch方法进行刷新,确保查询条件生效
|
||||
// 如果DeviceTable组件暴露了refetch方法,可以通过ref调用
|
||||
};
|
||||
|
||||
// 重置按钮处理
|
||||
// 重置按钮处理 - 修改为重置日期范围
|
||||
const handleReset = () => {
|
||||
setSelectedSystem(null);
|
||||
setSelectedDeviceType(null);
|
||||
setSelectedDept(null);
|
||||
setTime("");
|
||||
setDateRange(null); // 重置日期范围
|
||||
setSearchKeyword("");
|
||||
setIpAddress("");
|
||||
setMacAddress("");
|
||||
setSerialNumber("");
|
||||
|
@ -114,6 +289,7 @@ export default function DeviceMessage() {
|
|||
setManufacturer("");
|
||||
setModel("");
|
||||
setLocation("");
|
||||
saveSearchHistory([]);
|
||||
|
||||
// 重置为只查询未删除的记录
|
||||
setSearchValue({ deletedAt: null } as any);
|
||||
|
@ -144,14 +320,85 @@ export default function DeviceMessage() {
|
|||
form.setFieldValue("deviceType", undefined); // 清空已选故障类型
|
||||
};
|
||||
|
||||
// 搜索历史下拉菜单
|
||||
const historyMenuItems = [
|
||||
{
|
||||
key: "header",
|
||||
label: (
|
||||
<div className="flex justify-between items-center p-2 border-b">
|
||||
<span className="font-medium">搜索历史</span>
|
||||
{searchHistory.length > 0 && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={clearAllHistory}
|
||||
className="text-red-500"
|
||||
>
|
||||
清空全部
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
...searchHistory.map((history) => ({
|
||||
key: history.id,
|
||||
label: (
|
||||
<div
|
||||
className="flex justify-between items-center p-2 hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => applyHistorySearch(history)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium truncate">{history.label}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{dayjs(history.timestamp).format("MM-DD HH:mm")}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={(e) => removeSearchHistory(history.id, e)}
|
||||
className="ml-2 opacity-60 hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
...(searchHistory.length === 0
|
||||
? [
|
||||
{
|
||||
key: "empty",
|
||||
label: (
|
||||
<div className="text-center text-gray-500 p-4">暂无搜索历史</div>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-2 min-h-screen bg-white">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-xl font-normal">故障收录检索</h1>
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Dropdown
|
||||
menu={{ items: historyMenuItems }}
|
||||
trigger={["click"]}
|
||||
open={showHistory}
|
||||
onOpenChange={setShowHistory}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<HistoryOutlined />} title="搜索历史">
|
||||
历史
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleNew}>
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<SystemTypeSelect
|
||||
|
@ -186,12 +433,12 @@ export default function DeviceMessage() {
|
|||
onChange={handleDeptChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<DatePicker
|
||||
placeholder="选择日期"
|
||||
<div className="flex-1 min-w-[250px]">
|
||||
<RangePicker
|
||||
placeholder={["开始日期", "结束日期"]}
|
||||
className="w-full"
|
||||
value={time ? dayjs(time) : null}
|
||||
onChange={(date, dateString) => setTime(dateString as string)}
|
||||
value={dateRange}
|
||||
onChange={(dates) => setDateRange(dates)}
|
||||
format="YYYY-MM-DD"
|
||||
allowClear
|
||||
/>
|
||||
|
@ -203,6 +450,36 @@ export default function DeviceMessage() {
|
|||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 显示当前搜索历史标签 */}
|
||||
{searchHistory.length > 0 && (
|
||||
<div className="mb-4 flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="text-sm font-medium text-gray-700 whitespace-nowrap">
|
||||
最近搜索:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 flex-1 min-w-0">
|
||||
{searchHistory.slice(0, 5).map((history) => (
|
||||
<Tag
|
||||
key={history.id}
|
||||
className="cursor-pointer hover:bg-blue-50 transition-colors duration-200 border-blue-200 text-blue-700"
|
||||
onClick={() => applyHistorySearch(history)}
|
||||
closable
|
||||
onClose={(e) => {
|
||||
e.preventDefault();
|
||||
removeSearchHistory(history.id, e as any);
|
||||
}}
|
||||
>
|
||||
<span className="text-xs">
|
||||
{history.label.length > 25
|
||||
? `${history.label.substring(0, 25)}...`
|
||||
: history.label}
|
||||
</span>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<DeviceTable ref={tableRef} onSelectedChange={handleSelectedChange} />
|
||||
<DeviceModal />
|
||||
|
|
|
@ -1,10 +1,31 @@
|
|||
// apps/web/src/components/models/term/term-manager.tsx
|
||||
import { Button, Input, Modal, Space, Table, TreeSelect, Select } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
TreeSelect,
|
||||
Select,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Divider,
|
||||
} from "antd";
|
||||
import { api } from "@nice/client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ObjectType } from "@nice/common";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
interface TermManagerProps {
|
||||
title: string;
|
||||
}
|
||||
|
@ -18,6 +39,10 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
const [searchValue, setSearchValue] = useState("");
|
||||
const [treeData, setTreeData] = useState<any[]>([]);
|
||||
const [taxonomySelectDisabled, setTaxonomySelectDisabled] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// ... 保持原有的 API 调用和逻辑 ...
|
||||
|
||||
// 获取所有taxonomy
|
||||
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
||||
|
@ -61,6 +86,15 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
orderBy: { order: "asc" },
|
||||
});
|
||||
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (current: number, size: number) => {
|
||||
setCurrentPage(1); // 重置到第一页
|
||||
setPageSize(size);
|
||||
};
|
||||
// 构建包含两种分类的树形数据
|
||||
useEffect(() => {
|
||||
if (systemTypeTerms && deviceTypeTerms) {
|
||||
|
@ -186,7 +220,6 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
});
|
||||
|
||||
// 操作处理函数
|
||||
// 修改handleAdd函数
|
||||
const handleAdd = (parentRecord?: any) => {
|
||||
setEditingTerm(null);
|
||||
setTermName("");
|
||||
|
@ -213,10 +246,6 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleEdit = (term: any) => {
|
||||
setEditingTerm(term);
|
||||
setTermName(term.name);
|
||||
|
@ -329,123 +358,240 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: 16,
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<div className="p-6 min-h-screen bg-gray-50">
|
||||
{/* 主要内容卡片 */}
|
||||
<Card className="shadow-sm" bodyStyle={{ padding: 0 }}>
|
||||
{/* 统计信息区域 */}
|
||||
<div className="p-4 bg-gray-50 border-b border-gray-100">
|
||||
<Row gutter={16} justify="center">
|
||||
<Col span={8}>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{systemTypeTerms?.length || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">网系类别</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{deviceTypeTerms?.filter((term) =>
|
||||
systemTypeTerms?.some(
|
||||
(sysType) => sysType.id === term.parentId
|
||||
)
|
||||
).length || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">故障类型</div>
|
||||
</div>
|
||||
</Col>
|
||||
{/* <Col span={8}> */}
|
||||
{/* <div className="text-center"> */}
|
||||
{/* <div className="text-2xl font-bold text-orange-600">
|
||||
{deviceTypeTerms?.filter(term =>
|
||||
!systemTypeTerms?.some(sysType => sysType.id === term.parentId) &&
|
||||
deviceTypeTerms?.some(devType => devType.id === term.parentId)
|
||||
).length || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">具体故障</div> */}
|
||||
{/* </div>
|
||||
</Col> */}
|
||||
</Row>
|
||||
</div>
|
||||
{/* 工具栏区域 */}
|
||||
<div className="p-4 border-b border-gray-100 bg-white">
|
||||
<Row gutter={[16, 16]} align="middle" justify="space-between">
|
||||
<Col xs={24} sm={16} md={12} lg={10}>
|
||||
<Input.Search
|
||||
placeholder={`根据${title}搜索`}
|
||||
placeholder={`搜索${title}名称...`}
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchValue}
|
||||
style={{ width: 400 }}
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
className="w-full"
|
||||
size="middle"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8} md={12} lg={14} className="flex justify-end">
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAdd()}
|
||||
size="middle"
|
||||
className="shadow-sm"
|
||||
>
|
||||
添加顶级{title}
|
||||
添加{title}
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* 表格区域 */}
|
||||
<div className="p-4">
|
||||
<Table
|
||||
dataSource={treeData}
|
||||
expandable={{
|
||||
defaultExpandAllRows: true,
|
||||
defaultExpandAllRows: false, // 改为不默认展开所有行
|
||||
expandRowByClick: true,
|
||||
indentSize: 20, // 设置缩进大小
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "50%", // 增加名称列的宽度
|
||||
render: (text, record, index, ) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 根据层级添加不同的图标 */}
|
||||
{record.taxonomyId === systemTypeTaxonomy?.id ? (
|
||||
<span className="text-blue-500">📁</span>
|
||||
) : record.taxonomyId === deviceTypeTaxonomy?.id ? (
|
||||
<span className="text-green-500">📂</span>
|
||||
) : (
|
||||
<span className="text-orange-500">📄</span>
|
||||
)}
|
||||
<span className="font-medium">{text}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "分类类型",
|
||||
key: "taxonomyType",
|
||||
width: "25%",
|
||||
render: (_, record) => {
|
||||
if (record.taxonomyId === systemTypeTaxonomy?.id) {
|
||||
return "网系类别";
|
||||
return (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
网系类别
|
||||
</span>
|
||||
);
|
||||
} else if (record.taxonomyId === deviceTypeTaxonomy?.id) {
|
||||
return "故障类型";
|
||||
return (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
故障类型
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return "具体故障";
|
||||
return (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
||||
具体故障
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 200,
|
||||
width: "25%",
|
||||
render: (_, record: any) => (
|
||||
<Space>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAdd(record)}
|
||||
style={{ color: "green" }}
|
||||
>
|
||||
添加子项
|
||||
</Button>
|
||||
className="text-green-600 hover:text-green-700 hover:bg-green-50"
|
||||
size="small"
|
||||
title="添加子项"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
style={{ color: "#1890ff" }}
|
||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||
size="small"
|
||||
title="编辑"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
size="small"
|
||||
title="删除"
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
rowClassName={(record, index) =>
|
||||
index % 2 === 0 ? "bg-white" : "bg-gray-100"
|
||||
}
|
||||
onHeaderRow={() => {
|
||||
return {
|
||||
style: {
|
||||
backgroundColor: "#d6e4ff",
|
||||
},
|
||||
};
|
||||
pagination={{
|
||||
pageSize: pageSize, // 每页显示数量
|
||||
current: currentPage,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
pageSizeOptions: [ "10", "15", "20"],
|
||||
onChange: handlePageChange,
|
||||
onShowSizeChange: handlePageSizeChange,
|
||||
}}
|
||||
bordered
|
||||
rowClassName={(record, index) =>
|
||||
`hover:bg-blue-50 transition-colors duration-200 ${
|
||||
record.taxonomyId === systemTypeTaxonomy?.id
|
||||
? "bg-blue-25"
|
||||
: record.taxonomyId === deviceTypeTaxonomy?.id
|
||||
? "bg-green-25"
|
||||
: "bg-orange-25"
|
||||
}`
|
||||
}
|
||||
onHeaderRow={() => ({
|
||||
style: {
|
||||
backgroundColor: "#f8fafc",
|
||||
fontWeight: "600",
|
||||
borderBottom: "2px solid #e2e8f0",
|
||||
},
|
||||
})}
|
||||
bordered={true} // 改为有边框
|
||||
size="middle"
|
||||
locale={{ emptyText: "暂无数据" }}
|
||||
className="rounded-lg overflow-hidden shadow-sm"
|
||||
showHeader={true}
|
||||
scroll={{ x: 800 }} // 添加横向滚动
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 编辑/添加模态框 */}
|
||||
<Modal
|
||||
title={editingTerm ? `编辑${title}` : `添加${title}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
{editingTerm ? <EditOutlined /> : <PlusOutlined />}
|
||||
{editingTerm ? `编辑${title}` : `添加${title}`}
|
||||
</div>
|
||||
}
|
||||
open={isModalVisible}
|
||||
onOk={handleSave}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: "block", marginBottom: 8 }}>名称:</label>
|
||||
<Divider className="mt-4 mb-6" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
placeholder={`请输入${title}名称`}
|
||||
value={termName}
|
||||
onChange={(e) => setTermName(e.target.value)}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!editingTerm && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: "block", marginBottom: 8 }}>
|
||||
分类类型:
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
分类类型 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
|
@ -453,6 +599,7 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
value={taxonomyId}
|
||||
onChange={setTaxonomyId}
|
||||
disabled={taxonomySelectDisabled}
|
||||
size="large"
|
||||
options={taxonomies?.map((tax) => ({
|
||||
label: tax.name,
|
||||
value: tax.id,
|
||||
|
@ -462,20 +609,22 @@ export default function DeviceManager({ title }: TermManagerProps) {
|
|||
)}
|
||||
|
||||
<div>
|
||||
<label style={{ display: "block", marginBottom: 8 }}>
|
||||
上级分类:
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
上级分类
|
||||
</label>
|
||||
<TreeSelect
|
||||
style={{ width: "100%" }}
|
||||
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
|
||||
placeholder="请选择上级分类"
|
||||
placeholder="请选择上级分类(可选)"
|
||||
allowClear
|
||||
treeDefaultExpandAll
|
||||
value={parentId}
|
||||
onChange={setParentId}
|
||||
treeData={getParentOptions()}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue