更新时间

This commit is contained in:
menxipeng
2025-10-19 16:50:27 +08:00
parent 282514e9c4
commit 44ed397f6e

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
@@ -6,134 +6,79 @@ import { Badge } from "../ui/badge";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { Search, Eye, Download, FileText, CheckCircle, User, Building } from "lucide-react";
import { Search, Eye, Download, FileText, CheckCircle, User, Building, ChevronLeft, ChevronRight } from "lucide-react";
import { apiGet } from "../../lib/services/api";
interface ArchivedWorkOrder {
id: string;
title: string;
merchant: string;
equipment: string;
worker: string;
dealer: string;
completedDate: string;
archiveDate: string;
status: "completed";
priority: "high" | "medium" | "low";
signatures: {
worker: {
name: string;
signature: string;
timestamp: string;
photos: string[];
};
merchant: {
name: string;
signature: string;
timestamp: string;
};
dealerAdmin: {
name: string;
signature: string;
timestamp: string;
};
};
workDetails: {
description: string;
beforePhotos: string[];
afterPhotos: string[];
materials: string[];
notes: string;
};
workOrderNumber: string;
workOrderType: string;
merchantName: string;
equipmentName: string;
workerName: string;
priority: string;
completedDate: string | null;
createdDate: string;
status: string;
createdAt: string;
updatedAt: string;
merchantAddress: string;
responsiblePerson: string;
responsibleVirtualPhone: string;
workerVirtualPhone: string;
frontImg: string;
openImg: string;
ropeReImg: string;
hostReImg: string;
otherImg: string;
repairImg: string;
repairVideo: string;
needRepair: boolean;
equNormal: boolean;
}
export default function WorkOrderArchivePage() {
const [archivedOrders, setArchivedOrders] = useState<ArchivedWorkOrder[]>([
{
id: "WO-2024-001",
title: "干粉灭火器年检",
merchant: "星巴克咖啡店",
equipment: "干粉灭火器-001",
worker: "张师傅",
dealer: "华东经销商",
completedDate: "2024-01-15",
archiveDate: "2024-01-16",
status: "completed",
priority: "medium",
signatures: {
worker: {
name: "张师傅",
signature: "/placeholder.svg?height=100&width=200",
timestamp: "2024-01-15 14:30:00",
photos: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
},
merchant: {
name: "李经理",
signature: "/placeholder.svg?height=100&width=200",
timestamp: "2024-01-15 15:00:00",
},
dealerAdmin: {
name: "王主管",
signature: "/placeholder.svg?height=100&width=200",
timestamp: "2024-01-15 16:00:00",
},
},
workDetails: {
description: "对干粉灭火器进行年度检测,检查压力表、安全销、喷嘴等部件",
beforePhotos: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
afterPhotos: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
materials: ["压力表", "安全销", "检测标签"],
notes: "设备状态良好,已更换压力表,贴上检测合格标签",
},
},
{
id: "WO-2024-002",
title: "烟感器故障维修",
merchant: "麦当劳餐厅",
equipment: "烟感器-025",
worker: "李师傅",
dealer: "华南经销商",
completedDate: "2024-01-12",
archiveDate: "2024-01-13",
status: "completed",
priority: "high",
signatures: {
worker: {
name: "李师傅",
signature: "/placeholder.svg?height=100&width=200",
timestamp: "2024-01-12 11:45:00",
photos: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
},
merchant: {
name: "陈店长",
signature: "/placeholder.svg?height=100&width=200",
timestamp: "2024-01-12 12:15:00",
},
dealerAdmin: {
name: "赵经理",
signature: "/placeholder.svg?height=100&width=200",
timestamp: "2024-01-12 13:00:00",
},
},
workDetails: {
description: "烟感器报警异常,需要更换传感器模块",
beforePhotos: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
afterPhotos: ["/placeholder.svg?height=300&width=400", "/placeholder.svg?height=300&width=400"],
materials: ["传感器模块", "电池", "固定螺丝"],
notes: "已更换传感器模块,测试正常,设备恢复正常工作",
},
},
]);
const [archivedOrders, setArchivedOrders] = useState<ArchivedWorkOrder[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [selectedOrder, setSelectedOrder] = useState<ArchivedWorkOrder | null>(null);
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
pageNum: 1,
pageSize: 10,
total: 0
});
// 获取归档工单列表
const fetchArchivedOrders = async (pageNum = 1, pageSize = 10) => {
setLoading(true);
try {
const response = await apiGet(`/client/work/list?queryStaus=6&pageNum=${pageNum}&pageSize=${pageSize}`);
if (response.code === 200) {
setArchivedOrders(response.data || []);
setPagination({
pageNum,
pageSize,
total: parseInt(response.total) || 0
});
}
} catch (error) {
console.error('获取归档工单列表失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchArchivedOrders();
}, []);
const filteredOrders = archivedOrders.filter((order) => {
const matchesSearch =
order.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.merchant.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase());
order.workOrderType.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.merchantName.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === "all" || order.status === statusFilter;
return matchesSearch && matchesStatus;
});
@@ -145,11 +90,11 @@ export default function WorkOrderArchivePage() {
const getPriorityColor = (priority: string) => {
switch (priority) {
case "high":
case "1":
return "bg-red-100 text-red-800";
case "medium":
case "2":
return "bg-yellow-100 text-yellow-800";
case "low":
case "3":
return "bg-green-100 text-green-800";
default:
return "bg-gray-100 text-gray-800";
@@ -158,11 +103,11 @@ export default function WorkOrderArchivePage() {
const getPriorityText = (priority: string) => {
switch (priority) {
case "high":
case "1":
return "高";
case "medium":
case "2":
return "中";
case "low":
case "3":
return "低";
default:
return "未知";
@@ -190,7 +135,7 @@ export default function WorkOrderArchivePage() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">1,248</p>
<p className="text-3xl font-bold">{pagination.total}</p>
</div>
<FileText className="h-8 w-8 text-blue-500" />
</div>
@@ -201,8 +146,8 @@ export default function WorkOrderArchivePage() {
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">156</p>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">{archivedOrders.length}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-500" />
</div>
@@ -213,8 +158,8 @@ export default function WorkOrderArchivePage() {
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">98.5%</p>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">{pagination.pageNum}/{Math.ceil(pagination.total / pagination.pageSize)}</p>
</div>
<User className="h-8 w-8 text-purple-500" />
</div>
@@ -225,8 +170,8 @@ export default function WorkOrderArchivePage() {
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">2.4GB</p>
<p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">{pagination.pageSize}</p>
</div>
<Building className="h-8 w-8 text-orange-500" />
</div>
@@ -279,37 +224,78 @@ export default function WorkOrderArchivePage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium">{order.id}</TableCell>
<TableCell>{order.title}</TableCell>
<TableCell>{order.merchant}</TableCell>
<TableCell>{order.equipment}</TableCell>
<TableCell>{order.worker}</TableCell>
<TableCell>
<Badge className={getPriorityColor(order.priority)}>{getPriorityText(order.priority)}</Badge>
</TableCell>
<TableCell>{order.completedDate}</TableCell>
<TableCell>{order.archiveDate}</TableCell>
<TableCell>
<Button variant="outline" size="sm" onClick={() => openDetailDialog(order)}>
<Eye className="h-4 w-4 mr-1" />
</Button>
{loading ? (
<TableRow>
<TableCell colSpan={9} className="text-center py-8">
<div className="text-gray-500">...</div>
</TableCell>
</TableRow>
))}
) : filteredOrders.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-center py-8">
<div className="text-gray-500"></div>
</TableCell>
</TableRow>
) : (
filteredOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium">{order.workOrderNumber}</TableCell>
<TableCell>{order.workOrderType}</TableCell>
<TableCell>{order.merchantName}</TableCell>
<TableCell>{order.equipmentName}</TableCell>
<TableCell>{order.workerName}</TableCell>
<TableCell>
<Badge className={getPriorityColor(order.priority)}>{getPriorityText(order.priority)}</Badge>
</TableCell>
<TableCell>{order.updatedAt}</TableCell>
<TableCell>{order.updatedAt}</TableCell>
<TableCell>
<Button variant="outline" size="sm" onClick={() => openDetailDialog(order)}>
<Eye className="h-4 w-4 mr-1" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
{/* Pagination */}
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-gray-600">
{pagination.total} {pagination.pageNum} {Math.ceil(pagination.total / pagination.pageSize)}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => fetchArchivedOrders(pagination.pageNum - 1, pagination.pageSize)}
disabled={pagination.pageNum === 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => fetchArchivedOrders(pagination.pageNum + 1, pagination.pageSize)}
disabled={pagination.pageNum >= Math.ceil(pagination.total / pagination.pageSize) || loading}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Detail Dialog */}
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogContent className="max-w-7xl sm:max-w-7xl max-h-[90vh] overflow-y-auto w-[95vw]">
<DialogHeader>
<DialogTitle> - {selectedOrder?.id}</DialogTitle>
<DialogDescription></DialogDescription>
<DialogTitle> - {selectedOrder?.workOrderNumber}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedOrder && (
@@ -322,49 +308,40 @@ export default function WorkOrderArchivePage() {
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.title}</p>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.workOrderNumber}</p>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.workOrderType}</p>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.merchant}</p>
<p className="font-medium">{selectedOrder.merchantName}</p>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.equipment}</p>
<p className="font-medium">{selectedOrder.equipmentName}</p>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.dealer}</p>
</div>
</div>
</CardContent>
</Card>
{/* Work Details */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.workDetails.description}</p>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.workerName}</p>
</div>
<div>
<p className="text-sm text-gray-600">使</p>
<div className="flex flex-wrap gap-2 mt-1">
{selectedOrder.workDetails.materials.map((material, index) => (
<Badge key={index} variant="outline">
{material}
</Badge>
))}
</div>
<p className="text-sm text-gray-600"></p>
<Badge className={getPriorityColor(selectedOrder.priority)}>{getPriorityText(selectedOrder.priority)}</Badge>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.workDetails.notes}</p>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.responsiblePerson}</p>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.responsibleVirtualPhone}</p>
</div>
<div className="col-span-2">
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.merchantAddress}</p>
</div>
</div>
</CardContent>
@@ -376,90 +353,127 @@ export default function WorkOrderArchivePage() {
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-2 gap-2">
{selectedOrder.workDetails.beforePhotos.map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`作业前照片 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
<div className="space-y-4">
{selectedOrder.frontImg && (
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-4 gap-2">
{selectedOrder.frontImg.split(',').filter(url => url.trim()).map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`正面照 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
</div>
</div>
</div>
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-2 gap-2">
{selectedOrder.workDetails.afterPhotos.map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`作业后照片 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
)}
{selectedOrder.openImg && (
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-4 gap-2">
{selectedOrder.openImg.split(',').filter(url => url.trim()).map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`开箱照 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
</div>
</div>
</div>
)}
{selectedOrder.ropeReImg && (
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-4 gap-2">
{selectedOrder.ropeReImg.split(',').filter(url => url.trim()).map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`绳索恢复 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
</div>
</div>
)}
{selectedOrder.hostReImg && (
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-4 gap-2">
{selectedOrder.hostReImg.split(',').filter(url => url.trim()).map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`主机恢复 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
</div>
</div>
)}
{selectedOrder.otherImg && (
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-4 gap-2">
{selectedOrder.otherImg.split(',').filter(url => url.trim()).map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`其他照片 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
</div>
</div>
)}
{selectedOrder.repairImg && (
<div>
<p className="text-sm text-gray-600 mb-2"></p>
<div className="grid grid-cols-4 gap-2">
{selectedOrder.repairImg.split(',').filter(url => url.trim()).map((photo, index) => (
<img
key={index}
src={photo || "/placeholder.svg"}
alt={`维修照片 ${index + 1}`}
className="w-full h-32 object-cover rounded border"
/>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Signatures */}
{/* Status Info */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Worker Signature */}
<div className="space-y-3">
<h4 className="font-medium text-green-700"></h4>
<div className="border rounded p-3">
<p className="text-sm text-gray-600">{selectedOrder.signatures.worker.name}</p>
<p className="text-sm text-gray-600">{selectedOrder.signatures.worker.timestamp}</p>
<div className="mt-2">
<img
src={selectedOrder.signatures.worker.signature || "/placeholder.svg"}
alt="工人签字"
className="w-full h-16 object-contain border rounded"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600"></p>
<Badge variant={selectedOrder.needRepair ? "destructive" : "outline"}>
{selectedOrder.needRepair ? "需要维修" : "无需维修"}
</Badge>
</div>
{/* Merchant Signature */}
<div className="space-y-3">
<h4 className="font-medium text-blue-700"></h4>
<div className="border rounded p-3">
<p className="text-sm text-gray-600">{selectedOrder.signatures.merchant.name}</p>
<p className="text-sm text-gray-600">{selectedOrder.signatures.merchant.timestamp}</p>
<div className="mt-2">
<img
src={selectedOrder.signatures.merchant.signature || "/placeholder.svg"}
alt="商户签字"
className="w-full h-16 object-contain border rounded"
/>
</div>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<Badge variant={selectedOrder.equNormal ? "default" : "destructive"}>
{selectedOrder.equNormal ? "正常" : "异常"}
</Badge>
</div>
{/* Dealer Admin Signature */}
<div className="space-y-3">
<h4 className="font-medium text-purple-700"></h4>
<div className="border rounded p-3">
<p className="text-sm text-gray-600">{selectedOrder.signatures.dealerAdmin.name}</p>
<p className="text-sm text-gray-600">{selectedOrder.signatures.dealerAdmin.timestamp}</p>
<div className="mt-2">
<img
src={selectedOrder.signatures.dealerAdmin.signature || "/placeholder.svg"}
alt="经销商管理员签字"
className="w-full h-16 object-contain border rounded"
/>
</div>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.createdAt}</p>
</div>
<div>
<p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.updatedAt}</p>
</div>
</div>
</CardContent>