新增历史搜索、下载附件、分页调整页面功能

This commit is contained in:
Li1304553726 2025-05-29 11:14:03 +08:00
parent fbc2c567e2
commit 33185aa340
5 changed files with 669 additions and 189 deletions

View File

@ -231,7 +231,11 @@ const DashboardPage = () => {
type: 'value', type: 'value',
name: '故障数量' name: '故障数量'
}, },
series: data.series series: data.series,
label: {
show: true,
position: 'top',
}
}; };
}; };

View File

@ -10,6 +10,7 @@ import {
Divider, Divider,
Tabs, Tabs,
Image, Image,
message,
} from "antd"; } from "antd";
import { import {
DownloadOutlined, DownloadOutlined,
@ -28,6 +29,7 @@ interface ResourceFileListProps {
deviceId: string; deviceId: string;
} }
//表示资源项的结构
interface ResourceItem { interface ResourceItem {
id?: string; id?: string;
title?: string; title?: string;
@ -52,16 +54,61 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
enabled: !!deviceId, // 只有当deviceId存在时才执行查询 enabled: !!deviceId, // 只有当deviceId存在时才执行查询
} }
); );
useEffect(() => { useEffect(() => {
// 每3秒刷新一次数据 // 每5秒刷新一次数据确保显示最新的附件信息。
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
if (deviceId) { if (deviceId) {
refetch(); refetch();
} }
}, 3000); }, 5000);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [deviceId, refetch]); }, [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处理资源数据 // 使用useMemo处理资源数据
const { resources, imageResources, fileResources } = useMemo(() => { const { resources, imageResources, fileResources } = useMemo(() => {
if (!data || !Array.isArray(data)) if (!data || !Array.isArray(data))
@ -73,7 +120,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
const processedResources = data.map((resource: any) => { const processedResources = data.map((resource: any) => {
const fileUrl = resource.url || resource.fileId; const fileUrl = resource.url || resource.fileId;
if (!fileUrl) return resource; if (!fileUrl) return resource;
// 构建文件URL
const originalUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`; const originalUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileUrl}`;
const isImageFile = isImage(fileUrl); const isImageFile = isImage(fileUrl);
// console.log("环境变量:", { // console.log("环境变量:", {
@ -89,6 +136,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
}); });
return { return {
// 将资源数据分为图片和文件两类,便于后续的展示和处理。
resources: processedResources, resources: processedResources,
imageResources: processedResources.filter((item) => item.isImage), imageResources: processedResources.filter((item) => item.isImage),
fileResources: 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}`; return `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${fileId}`;
}; };
// 获取压缩后的图片URL
const getCompressedUrl = (fileId: string | undefined) => { const getCompressedUrl = (fileId: string | undefined) => {
if (!fileId) return ""; if (!fileId) return "";
return getCompressedImageUrl(getFileUrl(fileId)); return getCompressedImageUrl(getFileUrl(fileId));
}; };
// 加载状态的显示
if (isLoading) { if (isLoading) {
return ( return (
<div className="text-center py-4 bg-gray-50 rounded-md"> <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) { if (resources.length === 0) {
return ( return (
<Empty description="暂无附件" className="bg-gray-50 p-4 rounded-md" /> <Empty description="暂无附件" className="bg-gray-50 p-4 rounded-md" />
@ -163,13 +214,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
key="download" key="download"
type="link" type="link"
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
onClick={() => { onClick={() => downloadFile(item)}
const url =
"originalUrl" in item
? (item as any).originalUrl
: getFileUrl(item.fileId);
if (url) window.open(url, "_blank");
}}
> >
</Button>, </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="rounded-xl p-1 border border-gray-100 bg-white">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{fileResources.map((item) => ( {fileResources.map((item) => (
<a <div
key={item.fileId || item.id} key={item.fileId || item.id}
className="flex-shrink-0 relative active:scale-95 transition-transform select-none" className="flex-shrink-0 relative active:scale-95 transition-transform select-none cursor-pointer"
href={item.originalUrl || getFileUrl(item.fileId)} onClick={() => downloadFile(item)}
target="_blank"
download={true}
title={`点击下载"${item.title || "未命名文件"}"`} 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"> <div className="text-blue-600 text-xl">
{getFileIcon(item.url || "")} {getFileIcon(item.url || "")}
</div> </div>
@ -290,7 +333,7 @@ export default function ResourceFileList({ deviceId }: ResourceFileListProps) {
</div> </div>
</div> </div>
</div> </div>
</a> </div>
))} ))}
</div> </div>
</div> </div>

View File

@ -50,6 +50,8 @@ const DeviceTable = forwardRef(
const { create } = useDevice(); const { create } = useDevice();
// 描述信息模态框状态:控制可见性和内容 // 描述信息模态框状态:控制可见性和内容
const [descModalVisible, setDescModalVisible] = useState(false); const [descModalVisible, setDescModalVisible] = useState(false);
const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [currentDesc, setCurrentDesc] = useState<{ const [currentDesc, setCurrentDesc] = useState<{
id: string; id: string;
title: 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> = [ const columns: ColumnsType<any> = [
{ {
@ -413,7 +425,6 @@ const DeviceTable = forwardRef(
); );
return matchedDevice?.deptId || null; return matchedDevice?.deptId || null;
}; };
// 获取状态键 // 获取状态键
const getStatusKeyByValue = (value: string) => { const getStatusKeyByValue = (value: string) => {
const statusMap: Record<string, string> = { const statusMap: Record<string, string> = {
@ -423,7 +434,6 @@ const DeviceTable = forwardRef(
}; };
return statusMap[value] || "normal"; return statusMap[value] || "normal";
}; };
// 改进批量导入记录函数 // 改进批量导入记录函数
// 改进批量导入记录函数,使用分批处理 // 改进批量导入记录函数,使用分批处理
const batchImportRecords = async (records: any[]) => { const batchImportRecords = async (records: any[]) => {
@ -447,7 +457,6 @@ const DeviceTable = forwardRef(
// 分批处理数据 // 分批处理数据
for (let i = 0; i < validRecords.length; i += batchSize) { for (let i = 0; i < validRecords.length; i += batchSize) {
const batch = validRecords.slice(i, i + batchSize); const batch = validRecords.slice(i, i + batchSize);
// 串行处理每一批数据 // 串行处理每一批数据
for (const record of batch) { for (const record of batch) {
try { try {
@ -476,7 +485,6 @@ const DeviceTable = forwardRef(
`成功导入 ${successCount}/${validRecords.length} 条数据,部分记录导入失败` `成功导入 ${successCount}/${validRecords.length} 条数据,部分记录导入失败`
); );
} }
// 刷新数据 // 刷新数据
refetch(); refetch();
} catch (error) { } catch (error) {
@ -484,7 +492,6 @@ const DeviceTable = forwardRef(
toast.error("导入过程中发生错误"); toast.error("导入过程中发生错误");
} }
}; };
// 添加导出模板功能 // 添加导出模板功能
const handleExportTemplate = () => { const handleExportTemplate = () => {
try { try {
@ -514,14 +521,12 @@ const DeviceTable = forwardRef(
toast.error("下载模板失败"); toast.error("下载模板失败");
} }
}; };
// 触发文件选择 // 触发文件选择
const handleImportClick = () => { const handleImportClick = () => {
if (uploadRef.current) { if (uploadRef.current) {
uploadRef.current.click(); uploadRef.current.click();
} }
}; };
const rowSelection = { const rowSelection = {
selectedRowKeys, selectedRowKeys,
onChange: onSelectChange, onChange: onSelectChange,
@ -622,12 +627,15 @@ const DeviceTable = forwardRef(
pagination={{ pagination={{
position: ["bottomCenter"], position: ["bottomCenter"],
className: "flex justify-center mt-4", className: "flex justify-center mt-4",
pageSize: 10, pageSize: pageSize,
current: currentPage,
showSizeChanger: true, showSizeChanger: true,
pageSizeOptions: ["10", "20", "30"], pageSizeOptions: ["10", "15", "20"],
responsive: true, responsive: true,
showTotal: (total, range) => `${total} 条数据`, showTotal: (total, range) => `${total} 条数据`,
showQuickJumper: true, showQuickJumper: true,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
}} }}
components={{ components={{
header: { header: {
@ -662,7 +670,6 @@ const DeviceTable = forwardRef(
<div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap"> <div className="bg-gray-50 p-4 rounded-md whitespace-pre-wrap">
{currentDesc.desc} {currentDesc.desc}
</div> </div>
{/* 添加附件展示区域 */} {/* 添加附件展示区域 */}
{currentDesc?.id && ( {currentDesc?.id && (
<div className="mt-4"> <div className="mt-4">

View File

@ -1,5 +1,16 @@
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; 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 { useCallback, useEffect, useState, useRef } from "react";
import _ from "lodash"; import _ from "lodash";
import { useMainContext } from "../layout/MainProvider"; import { useMainContext } from "../layout/MainProvider";
@ -14,13 +25,18 @@ import {
ExportOutlined, ExportOutlined,
UpOutlined, UpOutlined,
DownOutlined, DownOutlined,
HistoryOutlined,
CloseOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import DepartmentSelect from "@web/src/components/models/department/department-select"; import DepartmentSelect from "@web/src/components/models/department/department-select";
import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-select"; import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-select";
import DeviceTypeSelect from "@web/src/app/main/devicepage/select/Device-select"; import DeviceTypeSelect from "@web/src/app/main/devicepage/select/Device-select";
import dayjs from "dayjs"; import dayjs from "dayjs";
import FixTypeSelect from "./select/Fix-select"; import FixTypeSelect from "./select/Fix-select";
import { api } from "@nice/client";
const { RangePicker } = DatePicker;
// 添加筛选条件类型 // 添加筛选条件类型
type SearchCondition = { type SearchCondition = {
deletedAt: null; deletedAt: null;
@ -34,6 +50,15 @@ type SearchCondition = {
}; };
}; };
// 搜索历史类型
type SearchHistory = {
id: string;
keyword: string;
conditions: SearchCondition;
timestamp: number;
label: string; // 用于显示的标签
};
export default function DeviceMessage() { export default function DeviceMessage() {
const { const {
form, form,
@ -43,15 +68,20 @@ export default function DeviceMessage() {
setSearchValue, setSearchValue,
editingRecord, editingRecord,
} = useMainContext(); } = useMainContext();
// 控制展开/收起状态 // 控制展开/收起状态
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
// 添加所有筛选条件的状态 // 添加所有筛选条件的状态
const [selectedSystem, setSelectedSystem] = useState<string | null>(null); const [selectedSystem, setSelectedSystem] = useState<string | null>(null);
const [selectedDeviceType, setSelectedDeviceType] = useState<string | null>( const [selectedDeviceType, setSelectedDeviceType] = useState<string | null>(
null null
); );
const [selectedDept, setSelectedDept] = 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 [ipAddress, setIpAddress] = useState<string>("");
const [macAddress, setMacAddress] = useState<string>(""); const [macAddress, setMacAddress] = useState<string>("");
const [serialNumber, setSerialNumber] = useState<string>(""); const [serialNumber, setSerialNumber] = useState<string>("");
@ -63,19 +93,156 @@ export default function DeviceMessage() {
const [selectedSystemTypeId, setSelectedSystemTypeId] = useState<string>(""); const [selectedSystemTypeId, setSelectedSystemTypeId] = useState<string>("");
const [selectedFixType, setSelectedFixType] = useState<string | null>(null); const [selectedFixType, setSelectedFixType] = useState<string | null>(null);
// 搜索历史相关状态
const [searchHistory, setSearchHistory] = useState<SearchHistory[]>([]);
const [searchKeyword, setSearchKeyword] = useState<string>("");
const [showHistory, setShowHistory] = useState(false);
// 创建ref以访问DeviceTable内部方法 // 创建ref以访问DeviceTable内部方法
const tableRef = useRef(null); const tableRef = useRef(null);
// 存储选中行的状态 // 存储选中行的状态
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]); const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [selectedData, setSelectedData] = useState<any[]>([]); 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 = () => { const handleNew = () => {
form.setFieldsValue(formValue); form.setFieldsValue(formValue);
console.log(editingRecord); console.log(editingRecord);
setVisible(true); setVisible(true);
}; };
// 查询按钮点击处理 // 查询按钮点击处理 - 修改为支持日期范围
const handleSearch = () => { const handleSearch = () => {
// 构建查询条件 // 构建查询条件
const whereCondition: SearchCondition = { const whereCondition: SearchCondition = {
@ -84,29 +251,37 @@ export default function DeviceMessage() {
...(selectedDeviceType && { deviceType: selectedDeviceType }), ...(selectedDeviceType && { deviceType: selectedDeviceType }),
...(selectedFixType && { deviceStatus: selectedFixType }), ...(selectedFixType && { deviceStatus: selectedFixType }),
...(selectedDept && { deptId: selectedDept }), ...(selectedDept && { deptId: selectedDept }),
...(time && { ...(dateRange &&
createdAt: { dateRange[0] &&
gte: dayjs(time).startOf("day").toISOString(), dateRange[1] && {
lte: dayjs(time).endOf("day").toISOString(), createdAt: {
}, gte: dateRange[0].startOf("day").toISOString(),
lte: dateRange[1].endOf("day").toISOString(),
},
}),
...(searchKeyword && {
OR: [
{ description: { contains: searchKeyword } },
{ location: { contains: searchKeyword } },
{ responsiblePerson: { contains: searchKeyword } },
],
}), }),
// ...(status && { status: { contains: status } }),
}; };
// 添加到搜索历史
addSearchHistory(whereCondition, searchKeyword);
// 更新查询条件到全局上下文 // 更新查询条件到全局上下文
setSearchValue(whereCondition as any); setSearchValue(whereCondition as any);
// 也可以直接使用refetch方法进行刷新确保查询条件生效
// 如果DeviceTable组件暴露了refetch方法可以通过ref调用
}; };
// 重置按钮处理 // 重置按钮处理 - 修改为重置日期范围
const handleReset = () => { const handleReset = () => {
setSelectedSystem(null); setSelectedSystem(null);
setSelectedDeviceType(null); setSelectedDeviceType(null);
setSelectedDept(null); setSelectedDept(null);
setTime(""); setDateRange(null); // 重置日期范围
setSearchKeyword("");
setIpAddress(""); setIpAddress("");
setMacAddress(""); setMacAddress("");
setSerialNumber(""); setSerialNumber("");
@ -114,6 +289,7 @@ export default function DeviceMessage() {
setManufacturer(""); setManufacturer("");
setModel(""); setModel("");
setLocation(""); setLocation("");
saveSearchHistory([]);
// 重置为只查询未删除的记录 // 重置为只查询未删除的记录
setSearchValue({ deletedAt: null } as any); setSearchValue({ deletedAt: null } as any);
@ -144,14 +320,85 @@ export default function DeviceMessage() {
form.setFieldValue("deviceType", undefined); // 清空已选故障类型 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 ( return (
<div className="p-2 min-h-screen bg-white"> <div className="p-2 min-h-screen bg-white">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h1 className="text-xl font-normal"></h1> <h1 className="text-xl font-normal"></h1>
<Button type="primary" icon={<PlusOutlined />} onClick={handleNew}> <div className="flex items-center gap-2 justify-end">
<Dropdown
</Button> 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>
<div className="flex flex-wrap items-center gap-2 mb-4"> <div className="flex flex-wrap items-center gap-2 mb-4">
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[200px]">
<SystemTypeSelect <SystemTypeSelect
@ -186,12 +433,12 @@ export default function DeviceMessage() {
onChange={handleDeptChange} onChange={handleDeptChange}
/> />
</div> </div>
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[250px]">
<DatePicker <RangePicker
placeholder="选择日期" placeholder={["开始日期", "结束日期"]}
className="w-full" className="w-full"
value={time ? dayjs(time) : null} value={dateRange}
onChange={(date, dateString) => setTime(dateString as string)} onChange={(dates) => setDateRange(dates)}
format="YYYY-MM-DD" format="YYYY-MM-DD"
allowClear allowClear
/> />
@ -203,6 +450,36 @@ export default function DeviceMessage() {
</Button> </Button>
</div> </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> <div>
<DeviceTable ref={tableRef} onSelectedChange={handleSelectedChange} /> <DeviceTable ref={tableRef} onSelectedChange={handleSelectedChange} />
<DeviceModal /> <DeviceModal />

View File

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