更新时间

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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
@@ -6,134 +6,79 @@ import { Badge } from "../ui/badge";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; 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 { interface ArchivedWorkOrder {
id: string; id: string;
title: string; workOrderNumber: string;
merchant: string; workOrderType: string;
equipment: string; merchantName: string;
worker: string; equipmentName: string;
dealer: string; workerName: string;
completedDate: string; priority: string;
archiveDate: string; completedDate: string | null;
status: "completed"; createdDate: string;
priority: "high" | "medium" | "low"; status: string;
signatures: { createdAt: string;
worker: { updatedAt: string;
name: string; merchantAddress: string;
signature: string; responsiblePerson: string;
timestamp: string; responsibleVirtualPhone: string;
photos: string[]; workerVirtualPhone: string;
}; frontImg: string;
merchant: { openImg: string;
name: string; ropeReImg: string;
signature: string; hostReImg: string;
timestamp: string; otherImg: string;
}; repairImg: string;
dealerAdmin: { repairVideo: string;
name: string; needRepair: boolean;
signature: string; equNormal: boolean;
timestamp: string;
};
};
workDetails: {
description: string;
beforePhotos: string[];
afterPhotos: string[];
materials: string[];
notes: string;
};
} }
export default function WorkOrderArchivePage() { export default function WorkOrderArchivePage() {
const [archivedOrders, setArchivedOrders] = useState<ArchivedWorkOrder[]>([ 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 [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all");
const [selectedOrder, setSelectedOrder] = useState<ArchivedWorkOrder | null>(null); const [selectedOrder, setSelectedOrder] = useState<ArchivedWorkOrder | null>(null);
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false); 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 filteredOrders = archivedOrders.filter((order) => {
const matchesSearch = const matchesSearch =
order.title.toLowerCase().includes(searchTerm.toLowerCase()) || order.workOrderType.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.merchant.toLowerCase().includes(searchTerm.toLowerCase()) || order.merchantName.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase()); order.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === "all" || order.status === statusFilter; const matchesStatus = statusFilter === "all" || order.status === statusFilter;
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}); });
@@ -145,11 +90,11 @@ export default function WorkOrderArchivePage() {
const getPriorityColor = (priority: string) => { const getPriorityColor = (priority: string) => {
switch (priority) { switch (priority) {
case "high": case "1":
return "bg-red-100 text-red-800"; return "bg-red-100 text-red-800";
case "medium": case "2":
return "bg-yellow-100 text-yellow-800"; return "bg-yellow-100 text-yellow-800";
case "low": case "3":
return "bg-green-100 text-green-800"; return "bg-green-100 text-green-800";
default: default:
return "bg-gray-100 text-gray-800"; return "bg-gray-100 text-gray-800";
@@ -158,11 +103,11 @@ export default function WorkOrderArchivePage() {
const getPriorityText = (priority: string) => { const getPriorityText = (priority: string) => {
switch (priority) { switch (priority) {
case "high": case "1":
return "高"; return "高";
case "medium": case "2":
return "中"; return "中";
case "low": case "3":
return "低"; return "低";
default: default:
return "未知"; return "未知";
@@ -190,7 +135,7 @@ export default function WorkOrderArchivePage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"></p> <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> </div>
<FileText className="h-8 w-8 text-blue-500" /> <FileText className="h-8 w-8 text-blue-500" />
</div> </div>
@@ -201,8 +146,8 @@ export default function WorkOrderArchivePage() {
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"></p> <p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">156</p> <p className="text-3xl font-bold">{archivedOrders.length}</p>
</div> </div>
<CheckCircle className="h-8 w-8 text-green-500" /> <CheckCircle className="h-8 w-8 text-green-500" />
</div> </div>
@@ -213,8 +158,8 @@ export default function WorkOrderArchivePage() {
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"></p> <p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">98.5%</p> <p className="text-3xl font-bold">{pagination.pageNum}/{Math.ceil(pagination.total / pagination.pageSize)}</p>
</div> </div>
<User className="h-8 w-8 text-purple-500" /> <User className="h-8 w-8 text-purple-500" />
</div> </div>
@@ -225,8 +170,8 @@ export default function WorkOrderArchivePage() {
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-600"></p> <p className="text-sm font-medium text-gray-600"></p>
<p className="text-3xl font-bold">2.4GB</p> <p className="text-3xl font-bold">{pagination.pageSize}</p>
</div> </div>
<Building className="h-8 w-8 text-orange-500" /> <Building className="h-8 w-8 text-orange-500" />
</div> </div>
@@ -279,37 +224,78 @@ export default function WorkOrderArchivePage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredOrders.map((order) => ( {loading ? (
<TableRow key={order.id}> <TableRow>
<TableCell className="font-medium">{order.id}</TableCell> <TableCell colSpan={9} className="text-center py-8">
<TableCell>{order.title}</TableCell> <div className="text-gray-500">...</div>
<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>
</TableCell> </TableCell>
</TableRow> </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> </TableBody>
</Table> </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> </CardContent>
</Card> </Card>
{/* Detail Dialog */} {/* Detail Dialog */}
<Dialog open={isDetailDialogOpen} onOpenChange={setIsDetailDialogOpen}> <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> <DialogHeader>
<DialogTitle> - {selectedOrder?.id}</DialogTitle> <DialogTitle> - {selectedOrder?.workOrderNumber}</DialogTitle>
<DialogDescription></DialogDescription> <DialogDescription></DialogDescription>
</DialogHeader> </DialogHeader>
{selectedOrder && ( {selectedOrder && (
@@ -322,49 +308,40 @@ export default function WorkOrderArchivePage() {
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.title}</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>
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.merchant}</p> <p className="font-medium">{selectedOrder.merchantName}</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.equipment}</p> <p className="font-medium">{selectedOrder.equipmentName}</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.dealer}</p> <p className="font-medium">{selectedOrder.workerName}</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>
</div> </div>
<div> <div>
<p className="text-sm text-gray-600">使</p> <p className="text-sm text-gray-600"></p>
<div className="flex flex-wrap gap-2 mt-1"> <Badge className={getPriorityColor(selectedOrder.priority)}>{getPriorityText(selectedOrder.priority)}</Badge>
{selectedOrder.workDetails.materials.map((material, index) => (
<Badge key={index} variant="outline">
{material}
</Badge>
))}
</div>
</div> </div>
<div> <div>
<p className="text-sm text-gray-600"></p> <p className="text-sm text-gray-600"></p>
<p className="font-medium">{selectedOrder.workDetails.notes}</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>
</div> </div>
</CardContent> </CardContent>
@@ -376,90 +353,127 @@ export default function WorkOrderArchivePage() {
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-6"> <div className="space-y-4">
<div> {selectedOrder.frontImg && (
<p className="text-sm text-gray-600 mb-2"></p> <div>
<div className="grid grid-cols-2 gap-2"> <p className="text-sm text-gray-600 mb-2"></p>
{selectedOrder.workDetails.beforePhotos.map((photo, index) => ( <div className="grid grid-cols-4 gap-2">
<img {selectedOrder.frontImg.split(',').filter(url => url.trim()).map((photo, index) => (
key={index} <img
src={photo || "/placeholder.svg"} key={index}
alt={`作业前照片 ${index + 1}`} src={photo || "/placeholder.svg"}
className="w-full h-32 object-cover rounded border" alt={`正面照 ${index + 1}`}
/> className="w-full h-32 object-cover rounded border"
))} />
))}
</div>
</div> </div>
</div> )}
<div> {selectedOrder.openImg && (
<p className="text-sm text-gray-600 mb-2"></p> <div>
<div className="grid grid-cols-2 gap-2"> <p className="text-sm text-gray-600 mb-2"></p>
{selectedOrder.workDetails.afterPhotos.map((photo, index) => ( <div className="grid grid-cols-4 gap-2">
<img {selectedOrder.openImg.split(',').filter(url => url.trim()).map((photo, index) => (
key={index} <img
src={photo || "/placeholder.svg"} key={index}
alt={`作业后照片 ${index + 1}`} src={photo || "/placeholder.svg"}
className="w-full h-32 object-cover rounded border" alt={`开箱照 ${index + 1}`}
/> className="w-full h-32 object-cover rounded border"
))} />
))}
</div>
</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> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Signatures */} {/* Status Info */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-2 gap-4">
{/* Worker Signature */} <div>
<div className="space-y-3"> <p className="text-sm text-gray-600"></p>
<h4 className="font-medium text-green-700"></h4> <Badge variant={selectedOrder.needRepair ? "destructive" : "outline"}>
<div className="border rounded p-3"> {selectedOrder.needRepair ? "需要维修" : "无需维修"}
<p className="text-sm text-gray-600">{selectedOrder.signatures.worker.name}</p> </Badge>
<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> </div>
<div>
{/* Merchant Signature */} <p className="text-sm text-gray-600"></p>
<div className="space-y-3"> <Badge variant={selectedOrder.equNormal ? "default" : "destructive"}>
<h4 className="font-medium text-blue-700"></h4> {selectedOrder.equNormal ? "正常" : "异常"}
<div className="border rounded p-3"> </Badge>
<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> </div>
<div>
{/* Dealer Admin Signature */} <p className="text-sm text-gray-600"></p>
<div className="space-y-3"> <p className="font-medium">{selectedOrder.createdAt}</p>
<h4 className="font-medium text-purple-700"></h4> </div>
<div className="border rounded p-3"> <div>
<p className="text-sm text-gray-600">{selectedOrder.signatures.dealerAdmin.name}</p> <p className="text-sm text-gray-600"></p>
<p className="text-sm text-gray-600">{selectedOrder.signatures.dealerAdmin.timestamp}</p> <p className="font-medium">{selectedOrder.updatedAt}</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> </div>
</div> </div>
</CardContent> </CardContent>