This commit is contained in:
menxipeng
2025-11-30 19:13:04 +08:00
parent 2439766af5
commit 318604a79f
7 changed files with 490 additions and 495 deletions

View File

@@ -14,7 +14,7 @@ import {
DialogTrigger, DialogTrigger,
} from "../ui/dialog" } from "../ui/dialog"
import { Label } from "../ui/label" import { Label } from "../ui/label"
import { Plus, Search, Download, User, Shield, Building, Store, Wrench, Eye, Edit, Trash2 } from "lucide-react" import { Plus, Search, User, Shield, Building, Store, Wrench, Eye, Edit, Trash2 } from "lucide-react"
import { apiGet, apiPost, apiPut, apiDelete } from "../../lib/services/api" import { apiGet, apiPost, apiPut, apiDelete } from "../../lib/services/api"
import { getUserData } from "../../lib/utils/storage" import { getUserData } from "../../lib/utils/storage"
@@ -547,11 +547,6 @@ export default function CompanyPermissionsPage() {
<SelectItem value="locked"></SelectItem> <SelectItem value="locked"></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
</Button>
</div> </div>
{/* Tabs */} {/* Tabs */}

View File

@@ -15,7 +15,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "../ui/dialog" } from "../ui/dialog"
import { Plus, Filter, Download, MapPin, Calendar, Shield, ChevronLeft, ChevronRight, Minus, Plus as PlusIcon, Search } from "lucide-react" import { Plus, Download, MapPin, Calendar, Shield, ChevronLeft, ChevronRight, Minus, Plus as PlusIcon, Search } from "lucide-react"
import { apiGet, apiPost, apiPut } from "../../lib/services/api" import { apiGet, apiPost, apiPut } from "../../lib/services/api"
// 商户数据类型(根据新接口返回格式定义) // 商户数据类型(根据新接口返回格式定义)
@@ -188,9 +188,9 @@ export default function EquipmentPage() {
} }
const lowerSearch = searchTerm.toLowerCase() const lowerSearch = searchTerm.toLowerCase()
return merchants.filter(merchant => return merchants.filter(merchant =>
merchant.merchantName.toLowerCase().includes(lowerSearch) || (merchant.merchantName && merchant.merchantName.toLowerCase().includes(lowerSearch)) ||
merchant.contactPerson.toLowerCase().includes(lowerSearch) || (merchant.contactPerson && merchant.contactPerson.toLowerCase().includes(lowerSearch)) ||
merchant.contactPhone.includes(lowerSearch) || (merchant.contactPhone && merchant.contactPhone.includes(lowerSearch)) ||
(merchant.mallLocation && merchant.mallLocation.toLowerCase().includes(lowerSearch)) (merchant.mallLocation && merchant.mallLocation.toLowerCase().includes(lowerSearch))
) )
} }
@@ -544,252 +544,10 @@ export default function EquipmentPage() {
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900"></h1> <h1 className="text-2xl sm:text-3xl font-bold text-gray-900"></h1>
<p className="text-sm sm:text-base text-gray-600"></p> <p className="text-sm sm:text-base text-gray-600"></p>
</div> </div>
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto"> </div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-40">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="筛选状态" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
<SelectItem value="all"></SelectItem>
<SelectItem value="1"></SelectItem>
<SelectItem value="2"></SelectItem>
<SelectItem value="3"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" className="w-full sm:w-auto">
<Download className="h-4 w-4 mr-2" />
</Button>
<Dialog open={isAddEquipmentOpen} onOpenChange={(open) => {
setIsAddEquipmentOpen(open)
if (!open) {
setMerchantSearchForAdd("")
}
}}>
<DialogTrigger asChild>
<Button className="bg-black text-white hover:bg-gray-800 w-full sm:w-auto">
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[95vh] overflow-y-auto w-[95vw] sm:w-full p-3 sm:p-6">
<DialogHeader className="pb-2 sm:pb-4">
<DialogTitle className="text-base sm:text-lg"></DialogTitle>
<DialogDescription className="text-xs sm:text-sm"></DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 sm:gap-x-6 gap-y-3 sm:gap-y-5 py-2 sm:py-4">
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="equipmentName" className="text-sm sm:text-base"></Label>
<Input
id="equipmentName"
value={newEquipment.name}
onChange={(e) => setNewEquipment({ ...newEquipment, name: e.target.value })}
placeholder="请输入设备名称"
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="equipmentType" className="text-sm sm:text-base"></Label>
<Select
value={newEquipment.type}
onValueChange={(value) => setNewEquipment({ ...newEquipment, type: value })}
disabled={loadingEquipmentTypes}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={loadingEquipmentTypes ? "加载中..." : "选择设备类型"} />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{Object.entries(equipmentTypes).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="model" className="text-sm sm:text-base"></Label>
<Input
id="model"
value={newEquipment.model}
onChange={(e) => setNewEquipment({ ...newEquipment, model: e.target.value })}
placeholder="请输入设备型号"
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="merchant" className="text-sm sm:text-base"></Label>
<Select
value={newEquipment.merchantId}
onValueChange={(value) => {
setNewEquipment({ ...newEquipment, merchantId: value })
setMerchantSearchForAdd("")
}}
disabled={loadingMerchants}
onOpenChange={(open) => {
if (open && merchants.length === 0 && !loadingMerchants) {
fetchMerchantsForSelect()
}
if (!open) {
setMerchantSearchForAdd("")
}
}}
>
<SelectTrigger>
<SelectValue placeholder={
merchants.length === 0 && !loadingMerchants
? "点击加载商户数据"
: loadingMerchants
? "加载中..."
: "选择商户"
} />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-hidden">
<div className="p-2 border-b">
<div className="relative">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜索商户名称、联系人、电话..."
value={merchantSearchForAdd}
onChange={(e) => setMerchantSearchForAdd(e.target.value)}
className="pl-8 h-8 text-sm"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</div>
</div>
<div className="overflow-y-auto max-h-[240px]">
{merchants.length === 0 && !loadingMerchants ? (
<SelectItem value="no-data" disabled>
</SelectItem>
) : filterMerchants(merchants, merchantSearchForAdd).length === 0 ? (
<div className="px-2 py-6 text-center text-sm text-gray-500">
</div>
) : (
filterMerchants(merchants, merchantSearchForAdd).map((merchant) => (
<SelectItem key={merchant.id} value={merchant.merchantsId || merchant.id}>
{merchant.merchantName} - {merchant.contactPerson}
</SelectItem>
))
)}
</div>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="location" className="text-sm sm:text-base"></Label>
<Input
id="location"
value={newEquipment.location}
onChange={(e) => setNewEquipment({ ...newEquipment, location: e.target.value })}
placeholder="请输入安装位置"
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="installDate" className="text-sm sm:text-base"></Label>
<Input
id="installDate"
type="date"
value={newEquipment.installDate}
onChange={(e) => {
const installDate = e.target.value
setNewEquipment({ ...newEquipment, installDate })
// 自动更新下一次检测时间(安装日期+半年) {/* 编辑设备对话框 */}
if (installDate) { <Dialog open={isEditEquipmentOpen} onOpenChange={(open) => {
const nextDate = new Date(installDate)
nextDate.setMonth(nextDate.getMonth() + 6)
const nextDateStr = nextDate.toISOString().split('T')[0]
setNewEquipment(prev => ({ ...prev, nextInspectionDate: nextDateStr }))
}
}}
/>
</div>
<div className="col-span-1 sm:col-span-2 space-y-1.5 sm:space-y-2">
<Label htmlFor="nextInspectionDate" className="text-sm sm:text-base"></Label>
<div className="flex gap-2 w-full sm:max-w-md">
<Input
id="nextInspectionDate"
type="date"
value={newEquipment.nextInspectionDate}
min={newEquipment.installDate || undefined}
onChange={(e) => setNewEquipment({ ...newEquipment, nextInspectionDate: e.target.value })}
className="flex-1 min-w-0"
/>
<div className="flex gap-1 sm:gap-2 flex-shrink-0">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustNextInspectionDate(-1)}
title="上一年"
className="px-2 sm:px-3 h-9 sm:h-10"
>
<Minus className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustNextInspectionDate(1)}
title="下一年"
className="px-2 sm:px-3 h-9 sm:h-10"
>
<PlusIcon className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
</div>
</div>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="status" className="text-sm sm:text-base"></Label>
<Select
value={newEquipment.status}
onValueChange={(value) => setNewEquipment({ ...newEquipment, status: value })}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择设备状态" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
<SelectItem value="normal"></SelectItem>
<SelectItem value="expiring"></SelectItem>
<SelectItem value="expired"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-2 space-y-1.5 sm:space-y-2">
<Label htmlFor="notes" className="text-sm sm:text-base"></Label>
<Textarea
id="notes"
value={newEquipment.notes}
onChange={(e) => setNewEquipment({ ...newEquipment, notes: e.target.value })}
placeholder="请输入备注信息"
rows={3}
/>
</div>
</div>
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 mt-6">
<Button
variant="outline"
onClick={() => setIsAddEquipmentOpen(false)}
disabled={isSubmitting}
className="w-full sm:w-auto"
>
</Button>
<Button
onClick={handleAddEquipment}
disabled={!newEquipment.name || !newEquipment.type || !newEquipment.merchantId || isSubmitting}
className="w-full sm:w-auto"
>
{isSubmitting ? "添加中..." : "添加设备"}
</Button>
</div>
</DialogContent>
</Dialog>
{/* 编辑设备对话框 */}
<Dialog open={isEditEquipmentOpen} onOpenChange={(open) => {
setIsEditEquipmentOpen(open) setIsEditEquipmentOpen(open)
if (!open) { if (!open) {
setMerchantSearchForEdit("") setMerchantSearchForEdit("")
@@ -1148,8 +906,6 @@ export default function EquipmentPage() {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div>
</div>
{/* 统计卡片 */} {/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -1209,6 +965,232 @@ export default function EquipmentPage() {
<CardTitle className="text-lg sm:text-xl"></CardTitle> <CardTitle className="text-lg sm:text-xl"></CardTitle>
<CardDescription className="text-xs sm:text-sm"> {pagination.total} </CardDescription> <CardDescription className="text-xs sm:text-sm"> {pagination.total} </CardDescription>
</div> </div>
<Dialog open={isAddEquipmentOpen} onOpenChange={(open) => {
setIsAddEquipmentOpen(open)
if (!open) {
setMerchantSearchForAdd("")
}
}}>
<DialogTrigger asChild>
<Button className="bg-black text-white hover:bg-gray-800 w-full sm:w-auto">
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[95vh] overflow-y-auto w-[95vw] sm:w-full p-3 sm:p-6">
<DialogHeader className="pb-2 sm:pb-4">
<DialogTitle className="text-base sm:text-lg"></DialogTitle>
<DialogDescription className="text-xs sm:text-sm"></DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 sm:gap-x-6 gap-y-3 sm:gap-y-5 py-2 sm:py-4">
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="equipmentName" className="text-sm sm:text-base"></Label>
<Input
id="equipmentName"
value={newEquipment.name}
onChange={(e) => setNewEquipment({ ...newEquipment, name: e.target.value })}
placeholder="请输入设备名称"
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="equipmentType" className="text-sm sm:text-base"></Label>
<Select
value={newEquipment.type}
onValueChange={(value) => setNewEquipment({ ...newEquipment, type: value })}
disabled={loadingEquipmentTypes}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={loadingEquipmentTypes ? "加载中..." : "选择设备类型"} />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{Object.entries(equipmentTypes).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="model" className="text-sm sm:text-base"></Label>
<Input
id="model"
value={newEquipment.model}
onChange={(e) => setNewEquipment({ ...newEquipment, model: e.target.value })}
placeholder="请输入设备型号"
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="merchant" className="text-sm sm:text-base"></Label>
<Select
value={newEquipment.merchantId}
onValueChange={(value) => {
setNewEquipment({ ...newEquipment, merchantId: value })
setMerchantSearchForAdd("")
}}
disabled={loadingMerchants}
onOpenChange={(open) => {
if (open && merchants.length === 0 && !loadingMerchants) {
fetchMerchantsForSelect()
}
if (!open) {
setMerchantSearchForAdd("")
}
}}
>
<SelectTrigger>
<SelectValue placeholder={
merchants.length === 0 && !loadingMerchants
? "点击加载商户数据"
: loadingMerchants
? "加载中..."
: "选择商户"
} />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-hidden">
<div className="p-2 border-b">
<div className="relative">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜索商户名称、联系人、电话..."
value={merchantSearchForAdd}
onChange={(e) => setMerchantSearchForAdd(e.target.value)}
className="pl-8 h-8 text-sm"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</div>
</div>
<div className="overflow-y-auto max-h-[240px]">
{merchants.length === 0 && !loadingMerchants ? (
<SelectItem value="no-data" disabled>
</SelectItem>
) : filterMerchants(merchants, merchantSearchForAdd).length === 0 ? (
<div className="px-2 py-6 text-center text-sm text-gray-500">
</div>
) : (
filterMerchants(merchants, merchantSearchForAdd).map((merchant) => (
<SelectItem key={merchant.id} value={merchant.merchantsId || merchant.id}>
{merchant.merchantName} - {merchant.contactPerson}
</SelectItem>
))
)}
</div>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="location" className="text-sm sm:text-base"></Label>
<Input
id="location"
value={newEquipment.location}
onChange={(e) => setNewEquipment({ ...newEquipment, location: e.target.value })}
placeholder="请输入安装位置"
/>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="installDate" className="text-sm sm:text-base"></Label>
<Input
id="installDate"
type="date"
value={newEquipment.installDate}
onChange={(e) => {
const installDate = e.target.value
setNewEquipment({ ...newEquipment, installDate })
// 自动更新下一次检测时间(安装日期+半年)
if (installDate) {
const nextDate = new Date(installDate)
nextDate.setMonth(nextDate.getMonth() + 6)
const nextDateStr = nextDate.toISOString().split('T')[0]
setNewEquipment(prev => ({ ...prev, nextInspectionDate: nextDateStr }))
}
}}
/>
</div>
<div className="col-span-1 sm:col-span-2 space-y-1.5 sm:space-y-2">
<Label htmlFor="nextInspectionDate" className="text-sm sm:text-base"></Label>
<div className="flex gap-2 w-full sm:max-w-md">
<Input
id="nextInspectionDate"
type="date"
value={newEquipment.nextInspectionDate}
min={newEquipment.installDate || undefined}
onChange={(e) => setNewEquipment({ ...newEquipment, nextInspectionDate: e.target.value })}
className="flex-1 min-w-0"
/>
<div className="flex gap-1 sm:gap-2 flex-shrink-0">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustNextInspectionDate(-1)}
title="上一年"
className="px-2 sm:px-3 h-9 sm:h-10"
>
<Minus className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustNextInspectionDate(1)}
title="下一年"
className="px-2 sm:px-3 h-9 sm:h-10"
>
<PlusIcon className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</Button>
</div>
</div>
</div>
<div className="space-y-1.5 sm:space-y-2">
<Label htmlFor="status" className="text-sm sm:text-base"></Label>
<Select
value={newEquipment.status}
onValueChange={(value) => setNewEquipment({ ...newEquipment, status: value })}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择设备状态" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
<SelectItem value="normal"></SelectItem>
<SelectItem value="expiring"></SelectItem>
<SelectItem value="expired"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-2 space-y-1.5 sm:space-y-2">
<Label htmlFor="notes" className="text-sm sm:text-base"></Label>
<Textarea
id="notes"
value={newEquipment.notes}
onChange={(e) => setNewEquipment({ ...newEquipment, notes: e.target.value })}
placeholder="请输入备注信息"
rows={3}
/>
</div>
</div>
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 mt-6">
<Button
variant="outline"
onClick={() => setIsAddEquipmentOpen(false)}
disabled={isSubmitting}
className="w-full sm:w-auto"
>
</Button>
<Button
onClick={handleAddEquipment}
disabled={!newEquipment.name || !newEquipment.type || !newEquipment.merchantId || isSubmitting}
className="w-full sm:w-auto"
>
{isSubmitting ? "添加中..." : "添加设备"}
</Button>
</div>
</DialogContent>
</Dialog>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@@ -271,6 +271,15 @@ export default function MallsPage() {
fetchMerchantEquipments(merchantId) fetchMerchantEquipments(merchantId)
} }
// 设备类型映射
const getEquipmentTypeName = (type: string) => {
const typeMap: { [key: string]: string } = {
"kitchen_automatic_fire_extinguisher": "厨房自动灭火",
"fire_extinguisher": "动火离人",
}
return typeMap[type] || type
}
// 获取设备状态徽章 // 获取设备状态徽章
const getEquipmentStatusBadge = (status: string) => { const getEquipmentStatusBadge = (status: string) => {
switch (status) { switch (status) {
@@ -1105,7 +1114,8 @@ export default function MallsPage() {
<DialogDescription className="text-xs sm:text-sm"></DialogDescription> <DialogDescription className="text-xs sm:text-sm"></DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex-1 overflow-y-auto space-y-3 sm:space-y-4"> <div className="flex-1 overflow-y-auto space-y-3 sm:space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4"> {/* 统计卡片 - 已注释 */}
{/* <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
<Card> <Card>
<CardContent className="p-3 sm:p-4"> <CardContent className="p-3 sm:p-4">
<div className="text-center"> <div className="text-center">
@@ -1130,7 +1140,7 @@ export default function MallsPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div> */}
{/* Desktop Table View */} {/* Desktop Table View */}
<div className="hidden md:block rounded-md border overflow-hidden"> <div className="hidden md:block rounded-md border overflow-hidden">
@@ -1163,7 +1173,7 @@ export default function MallsPage() {
{equipment.equipmentName} {equipment.equipmentName}
</TableCell> </TableCell>
<TableCell className="truncate max-w-[120px]" title={equipment.equipmentType}> <TableCell className="truncate max-w-[120px]" title={equipment.equipmentType}>
{equipment.equipmentType} {getEquipmentTypeName(equipment.equipmentType)}
</TableCell> </TableCell>
<TableCell className="truncate max-w-[120px]" title={equipment.installationDate}> <TableCell className="truncate max-w-[120px]" title={equipment.installationDate}>
{equipment.installationDate} {equipment.installationDate}
@@ -1209,7 +1219,7 @@ export default function MallsPage() {
<div className="border-t pt-3 space-y-2.5"> <div className="border-t pt-3 space-y-2.5">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-xs font-medium text-gray-700"></div> <div className="text-xs font-medium text-gray-700"></div>
<div className="text-sm text-gray-900 break-words">{equipment.equipmentType || '-'}</div> <div className="text-sm text-gray-900 break-words">{getEquipmentTypeName(equipment.equipmentType) || '-'}</div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="text-xs font-medium text-gray-700"></div> <div className="text-xs font-medium text-gray-700"></div>

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react" import React, { 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"
@@ -14,75 +14,94 @@ import {
DialogTrigger, DialogTrigger,
} from "../ui/dialog" } from "../ui/dialog"
import { Label } from "../ui/label" import { Label } from "../ui/label"
import { Plus, Search, Download, User, Eye, Edit, MapPin, Phone, Mail } from "lucide-react" import { Plus, Search, Download, User, Eye, Edit, MapPin, Phone } from "lucide-react"
import { apiGet } from "../../lib/services/api"
interface DisplayUser {
id: string
name: string
nickName: string
userName: string
phone: string
address: string
registrationDate: string
lastActive: string
status: string
orderCount: number
totalSpent: number
}
export default function UsersPage() { export default function UsersPage() {
const [searchTerm, setSearchTerm] = useState("") const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState("all") const [statusFilter, setStatusFilter] = useState("all")
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [users, setUsers] = useState<DisplayUser[]>([])
const [isLoading, setIsLoading] = useState(true)
const users = [ // 获取用户列表
{ const fetchUsers = async () => {
id: "CU001", try {
name: "张小明", setIsLoading(true)
email: "zhangxm@email.com", const result = await apiGet<any>("/back/user/list?loginRole=normal")
phone: "138****1234",
address: "上海市黄浦区南京东路123号", console.log("用户列表API返回结果:", result)
registrationDate: "2024-01-15",
lastActive: "2024-01-16 14:30", if (result && result.code === 200) {
status: "active", // 处理不同的响应格式
orderCount: 5, const userList = result.rows || result.data || []
totalSpent: 2580,
}, // 调试:打印第一条用户数据,查看实际字段名
{ if (userList.length > 0) {
id: "CU002", console.log("API 返回的用户数据示例:", userList[0])
name: "李小红", }
email: "lixh@email.com",
phone: "139****5678", // 将 API 返回的用户数据映射到显示格式
address: "北京市朝阳区建国门外大街1号", const mappedUsers: DisplayUser[] = userList.map((user: any) => {
registrationDate: "2024-01-10", // 尝试多种可能的字段名(处理大小写和命名差异)
lastActive: "2024-01-16 09:15", const nickName = user.nickName || user.nickname || user.nick_name || ""
status: "active", const userName = user.userName || user.username || user.user_name || ""
orderCount: 3,
totalSpent: 1200, return {
}, id: user.userId || `CU${user.userId?.slice(-3) || '000'}`,
{ name: nickName || userName || "未知用户",
id: "CU003", nickName: nickName,
name: "王大华", userName: userName,
email: "wangdh@email.com", phone: user.phone || "",
phone: "137****9012", address: user.addr || "未填写",
address: "广州市天河区天河路208号", registrationDate: user.registerTime ? user.registerTime.split(" ")[0] : "",
registrationDate: "2023-12-20", lastActive: user.updateTime || "",
lastActive: "2024-01-15 16:45", status: user.status === "0" ? "active" : user.status === "1" ? "inactive" : "blocked",
status: "active", orderCount: 0, // API 可能不包含此字段,需要后续补充
orderCount: 8, totalSpent: 0, // API 可能不包含此字段,需要后续补充
totalSpent: 4200, }
}, })
{
id: "CU004", setUsers(mappedUsers)
name: "赵小丽", } else {
email: "zhaoxl@email.com", console.error("获取用户列表失败:", result?.msg || "未知错误")
phone: "136****3456", setUsers([])
address: "深圳市南山区科技园南区", }
registrationDate: "2023-11-15", } catch (error) {
lastActive: "2024-01-12 11:20", console.error("请求用户列表失败:", error)
status: "inactive", setUsers([])
orderCount: 2, } finally {
totalSpent: 800, setIsLoading(false)
}, }
{ }
id: "CU005",
name: "陈小军", // 组件挂载时获取数据
email: "chenxj@email.com", useEffect(() => {
phone: "135****7890", fetchUsers()
address: "杭州市西湖区文三路259号", }, [])
registrationDate: "2024-01-05",
lastActive: "2024-01-16 13:10", // 格式化手机号显示(脱敏)
status: "active", const formatPhone = (phone: string) => {
orderCount: 1, if (!phone) return ""
totalSpent: 350, if (phone.length === 11) {
}, return `${phone.slice(0, 3)}****${phone.slice(7)}`
] }
return phone
}
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
switch (status) { switch (status) {
@@ -100,7 +119,8 @@ export default function UsersPage() {
const filteredUsers = users.filter((user) => { const filteredUsers = users.filter((user) => {
const matchesSearch = const matchesSearch =
user.name.toLowerCase().includes(searchTerm.toLowerCase()) || user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) || user.nickName.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.userName.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.phone.toLowerCase().includes(searchTerm.toLowerCase()) || user.phone.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.address.toLowerCase().includes(searchTerm.toLowerCase()) user.address.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === "all" || user.status === statusFilter const matchesStatus = statusFilter === "all" || user.status === statusFilter
@@ -144,7 +164,7 @@ export default function UsersPage() {
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input <Input
placeholder="搜索用户姓名、邮箱、手机号或地址..." placeholder="搜索用户姓名、手机号或地址..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10" className="pl-10"
@@ -170,23 +190,30 @@ export default function UsersPage() {
</Button> </Button>
</div> </div>
{/* Desktop Table View */} {/* Loading State */}
<div className="hidden md:block rounded-md border overflow-x-auto"> {isLoading ? (
<Table> <div className="text-center py-8 text-gray-500">...</div>
<TableHeader> ) : filteredUsers.length === 0 ? (
<TableRow> <div className="text-center py-8 text-gray-500"></div>
<TableHead></TableHead> ) : (
<TableHead></TableHead> <>
<TableHead></TableHead> {/* Desktop Table View */}
<TableHead></TableHead> <div className="hidden md:block rounded-md border overflow-x-auto">
<TableHead></TableHead> <Table>
<TableHead>/</TableHead> <TableHeader>
<TableHead></TableHead> <TableRow>
<TableHead></TableHead> <TableHead></TableHead>
</TableRow> <TableHead></TableHead>
</TableHeader> <TableHead></TableHead>
<TableBody> <TableHead></TableHead>
{filteredUsers.map((user) => ( <TableHead></TableHead>
<TableHead>/</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.id}> <TableRow key={user.id}>
<TableCell> <TableCell>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@@ -194,21 +221,15 @@ export default function UsersPage() {
<User className="h-4 w-4 text-blue-600" /> <User className="h-4 w-4 text-blue-600" />
</div> </div>
<div> <div>
<div className="font-medium">{user.name}</div> <div className="font-medium">{user.nickName || "-"}</div>
<div className="text-sm text-gray-500">{user.id}</div> <div className="text-sm text-gray-500">{user.userName || "-"}</div>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="space-y-1"> <div className="flex items-center text-sm">
<div className="flex items-center text-sm"> <Phone className="h-3 w-3 mr-1 text-gray-400" />
<Mail className="h-3 w-3 mr-1 text-gray-400" /> {formatPhone(user.phone)}
{user.email}
</div>
<div className="flex items-center text-sm text-gray-500">
<Phone className="h-3 w-3 mr-1 text-gray-400" />
{user.phone}
</div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -241,24 +262,24 @@ export default function UsersPage() {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
{/* Mobile Card View */} {/* Mobile Card View */}
<div className="md:hidden space-y-4"> <div className="md:hidden space-y-4">
{filteredUsers.map((user) => ( {filteredUsers.map((user) => (
<Card key={user.id}> <Card key={user.id}>
<CardContent className="p-4 space-y-3"> <CardContent className="p-4 space-y-3">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-3 flex-1"> <div className="flex items-center space-x-3 flex-1">
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center"> <div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
<User className="h-4 w-4 text-blue-600" /> <User className="h-4 w-4 text-blue-600" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="font-medium">{user.name}</div> <div className="font-medium">{user.nickName || "-"}</div>
<div className="text-sm text-gray-500">{user.id}</div> <div className="text-sm text-gray-500">{user.userName || "-"}</div>
</div> </div>
</div> </div>
{getStatusBadge(user.status)} {getStatusBadge(user.status)}
@@ -267,13 +288,9 @@ export default function UsersPage() {
<div className="border-t pt-3 space-y-2"> <div className="border-t pt-3 space-y-2">
<div> <div>
<div className="text-xs text-gray-500 mb-1"></div> <div className="text-xs text-gray-500 mb-1"></div>
<div className="flex items-center text-sm mb-1">
<Mail className="h-3 w-3 mr-1 text-gray-400" />
{user.email}
</div>
<div className="flex items-center text-sm text-gray-500"> <div className="flex items-center text-sm text-gray-500">
<Phone className="h-3 w-3 mr-1 text-gray-400" /> <Phone className="h-3 w-3 mr-1 text-gray-400" />
{user.phone} {formatPhone(user.phone)}
</div> </div>
</div> </div>
@@ -315,10 +332,12 @@ export default function UsersPage() {
</Button> </Button>
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
</>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -43,6 +43,7 @@ interface ArchivedWorkOrder {
brand: string; brand: string;
reformType: string; reformType: string;
remark: string; remark: string;
overallResult: number | null;
} }
export default function WorkOrderArchivePage() { export default function WorkOrderArchivePage() {
@@ -83,7 +84,19 @@ export default function WorkOrderArchivePage() {
const fetchArchivedOrders = async (pageNum = 1, pageSize = 10) => { const fetchArchivedOrders = async (pageNum = 1, pageSize = 10) => {
setLoading(true); setLoading(true);
try { try {
const response = await apiGet(`/back/workOrderArchive/list?pageNum=${pageNum}&pageSize=${pageSize}`); let url = `/back/workOrderArchive/list?pageNum=${pageNum}&pageSize=${pageSize}`;
// 添加工单编号搜索参数
if (searchTerm.trim()) {
url += `&workOrderNumber=${encodeURIComponent(searchTerm.trim())}`;
}
// 添加合格/不合格筛选参数
if (statusFilter !== "all") {
url += `&overallResult=${statusFilter}`;
}
const response = await apiGet(url);
if (response.code === 200) { if (response.code === 200) {
setArchivedOrders(response.data || []); setArchivedOrders(response.data || []);
setPagination({ setPagination({
@@ -101,9 +114,19 @@ export default function WorkOrderArchivePage() {
useEffect(() => { useEffect(() => {
fetchArchiveCount(); fetchArchiveCount();
fetchArchivedOrders(); fetchArchivedOrders(1, 10);
}, []); }, []);
// 监听搜索词和筛选条件变化,重新请求数据
useEffect(() => {
const timer = setTimeout(() => {
fetchArchivedOrders(1, pagination.pageSize);
}, 300); // 防抖300ms
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchTerm, statusFilter]);
// 下载报告 // 下载报告
const handleDownloadReport = (workOrderNumber: string) => { const handleDownloadReport = (workOrderNumber: string) => {
try { try {
@@ -139,15 +162,6 @@ export default function WorkOrderArchivePage() {
} }
}; };
const filteredOrders = archivedOrders.filter((order) => {
const matchesSearch =
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;
});
const openDetailDialog = (order: ArchivedWorkOrder) => { const openDetailDialog = (order: ArchivedWorkOrder) => {
setSelectedOrder(order); setSelectedOrder(order);
setIsDetailDialogOpen(true); setIsDetailDialogOpen(true);
@@ -244,7 +258,7 @@ export default function WorkOrderArchivePage() {
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input <Input
placeholder="搜索工单编号、商户名称或设备..." placeholder="搜索工单编号..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 w-full" className="pl-10 w-full"
@@ -256,8 +270,9 @@ export default function WorkOrderArchivePage() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all"></SelectItem> <SelectItem value="all"></SelectItem>
<SelectItem value="completed"></SelectItem> <SelectItem value="1"></SelectItem>
<SelectItem value="0"></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -284,14 +299,14 @@ export default function WorkOrderArchivePage() {
<div className="text-gray-500">...</div> <div className="text-gray-500">...</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) : filteredOrders.length === 0 ? ( ) : archivedOrders.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center py-8"> <TableCell colSpan={8} className="text-center py-8">
<div className="text-gray-500"></div> <div className="text-gray-500"></div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
filteredOrders.map((order) => ( archivedOrders.map((order) => (
<TableRow key={order.id}> <TableRow key={order.id}>
<TableCell className="font-medium">{order.workOrderNumber}</TableCell> <TableCell className="font-medium">{order.workOrderNumber}</TableCell>
<TableCell>{order.workOrderType}</TableCell> <TableCell>{order.workOrderType}</TableCell>
@@ -325,10 +340,10 @@ export default function WorkOrderArchivePage() {
<div className="md:hidden space-y-4"> <div className="md:hidden space-y-4">
{loading ? ( {loading ? (
<div className="text-center py-8 text-gray-500">...</div> <div className="text-center py-8 text-gray-500">...</div>
) : filteredOrders.length === 0 ? ( ) : archivedOrders.length === 0 ? (
<div className="text-center py-8 text-gray-500"></div> <div className="text-center py-8 text-gray-500"></div>
) : ( ) : (
filteredOrders.map((order) => ( archivedOrders.map((order) => (
<Card key={order.id}> <Card key={order.id}>
<CardContent className="p-4 space-y-3"> <CardContent className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">

View File

@@ -155,7 +155,6 @@ interface WorkOrder {
export default function WorkOrdersPage() { export default function WorkOrdersPage() {
const [searchTerm, setSearchTerm] = useState("") const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState("all") const [statusFilter, setStatusFilter] = useState("all")
const [workerFilter, setWorkerFilter] = useState("all")
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [editingWorkOrder, setEditingWorkOrder] = useState<WorkOrder | null>(null) const [editingWorkOrder, setEditingWorkOrder] = useState<WorkOrder | null>(null)
@@ -254,27 +253,17 @@ export default function WorkOrdersPage() {
} }
} }
// 从工单数据中提取唯一的工人列表
const uniqueWorkers = Array.from(
new Set(
workOrders
.filter(order => order.workerName && order.workerName.trim() !== "")
.map(order => order.workerName)
)
).sort()
const filteredWorkOrders = workOrders.filter((item) => { const filteredWorkOrders = workOrders.filter((item) => {
const matchesSearch = const matchesSearch =
item.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase()) || (item.workOrderNumber && item.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase())) ||
item.equipmentName.toLowerCase().includes(searchTerm.toLowerCase()) || (item.equipmentName && item.equipmentName.toLowerCase().includes(searchTerm.toLowerCase())) ||
item.merchantName.toLowerCase().includes(searchTerm.toLowerCase()) || (item.merchantName && item.merchantName.toLowerCase().includes(searchTerm.toLowerCase())) ||
item.workerName.toLowerCase().includes(searchTerm.toLowerCase()) (item.workerName && item.workerName.toLowerCase().includes(searchTerm.toLowerCase()))
// 使用工单的实际状态进行过滤 // 使用工单的实际状态进行过滤
const itemStatus = item.status || "1" // 默认为待接单状态 const itemStatus = item.status || "1" // 默认为待接单状态
const matchesStatus = statusFilter === "all" || itemStatus === statusFilter const matchesStatus = statusFilter === "all" || itemStatus === statusFilter
const matchesWorker = workerFilter === "all" || item.workerName === workerFilter return matchesSearch && matchesStatus
return matchesSearch && matchesStatus && matchesWorker
}) })
return ( return (
@@ -430,20 +419,6 @@ export default function WorkOrdersPage() {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={workerFilter} onValueChange={setWorkerFilter}>
<SelectTrigger className="w-full sm:w-32">
<SelectValue placeholder="工人" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
<SelectItem value="all"></SelectItem>
{uniqueWorkers.map((worker) => (
<SelectItem key={worker} value={worker}>
{worker}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline"> <Button variant="outline">
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
@@ -451,7 +426,7 @@ export default function WorkOrdersPage() {
</div> </div>
{/* Desktop Table View */} {/* Desktop Table View */}
<div className="hidden md:block rounded-md border overflow-x-auto"> <div className="hidden md:block rounded-md border overflow-x-hidden hover:overflow-x-auto transition-all">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -719,9 +694,9 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
} }
const lowerSearch = searchTerm.toLowerCase() const lowerSearch = searchTerm.toLowerCase()
return merchants.filter(merchant => return merchants.filter(merchant =>
merchant.merchantName.toLowerCase().includes(lowerSearch) || (merchant.merchantName && merchant.merchantName.toLowerCase().includes(lowerSearch)) ||
merchant.contactPerson.toLowerCase().includes(lowerSearch) || (merchant.contactPerson && merchant.contactPerson.toLowerCase().includes(lowerSearch)) ||
merchant.contactPhone.includes(lowerSearch) || (merchant.contactPhone && merchant.contactPhone.includes(lowerSearch)) ||
(merchant.mallLocation && merchant.mallLocation.toLowerCase().includes(lowerSearch)) (merchant.mallLocation && merchant.mallLocation.toLowerCase().includes(lowerSearch))
) )
} }

View File

@@ -38,7 +38,9 @@ export default function WorkersPage() {
const [workers, setWorkers] = useState<any[]>([]) const [workers, setWorkers] = useState<any[]>([])
const [provinces, setProvinces] = useState<Province[]>([]) const [provinces, setProvinces] = useState<Province[]>([])
const [dealers, setDealers] = useState<Dealer[]>([]) const [dealers, setDealers] = useState<Dealer[]>([])
const [searchTerm, setSearchTerm] = useState("") const [searchName, setSearchName] = useState("")
const [searchWorkerId, setSearchWorkerId] = useState("")
const [searchPhone, setSearchPhone] = useState("")
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
@@ -70,9 +72,6 @@ export default function WorkersPage() {
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
// 工单选择相关状态
const [workOrders, setWorkOrders] = useState<any[]>([])
const [selectedWorkOrder, setSelectedWorkOrder] = useState("all")
// 每个工人的工单分页状态 // 每个工人的工单分页状态
const [workerOrdersPagination, setWorkerOrdersPagination] = useState<{ const [workerOrdersPagination, setWorkerOrdersPagination] = useState<{
[workerId: string]: { [workerId: string]: {
@@ -82,35 +81,6 @@ export default function WorkersPage() {
} }
}>({}) }>({})
// 获取工人工单列表
const fetchWorkOrdersList = async (workersId?: string) => {
try {
const url = workersId
? `/back/orders/list?pageNum=1&pageSize=10&workersId=${workersId}`
: `/back/orders/list?pageNum=1&pageSize=10&workersId`
const result = await apiGet<any>(url)
console.log('工单列表API返回结果:', result)
if (result && result.code === 200 && result.rows) {
const processedOrders = result.rows.map((order: any) => ({
value: order.id,
label: `${order.workOrderNumber} - ${order.workOrderType} - ${order.merchantName}`,
data: order
}))
setWorkOrders(processedOrders)
} else {
console.error('获取工单列表失败:', result?.msg || '未知错误')
setWorkOrders([])
}
} catch (error) {
console.error('请求工单列表失败:', error)
setWorkOrders([])
}
}
// 获取工人完成工单数量 // 获取工人完成工单数量
const fetchCompletedOrdersCount = async (workersId: string) => { const fetchCompletedOrdersCount = async (workersId: string) => {
try { try {
@@ -129,10 +99,34 @@ export default function WorkersPage() {
} }
// 获取工人列表 // 获取工人列表
const fetchWorkersList = async (pageNum = currentPage, pageSizeParam = pageSize) => { const fetchWorkersList = async (
pageNum = currentPage,
pageSizeParam = pageSize,
name = searchName,
workerId = searchWorkerId,
phone = searchPhone
) => {
try { try {
setIsLoading(true) setIsLoading(true)
const result = await apiGet<any>(`/back/workers/list?pageNum=${pageNum}&pageSize=${pageSizeParam}`)
// 构建查询参数
const params = new URLSearchParams({
pageNum: pageNum.toString(),
pageSize: pageSizeParam.toString(),
})
// 添加搜索参数(如果有值)
if (name.trim()) {
params.append('name', name.trim())
}
if (workerId.trim()) {
params.append('jobNum', workerId.trim())
}
if (phone.trim()) {
params.append('phone', phone.trim())
}
const result = await apiGet<any>(`/back/workers/list?${params.toString()}`)
console.log('工人列表API返回结果:', result) console.log('工人列表API返回结果:', result)
@@ -238,32 +232,32 @@ export default function WorkersPage() {
// 组件挂载时获取数据 // 组件挂载时获取数据
useEffect(() => { useEffect(() => {
fetchWorkersList() fetchWorkersList()
fetchWorkOrdersList()
}, []) }, [])
// 分页变化时重新获取数据 // 搜索条件变化时调用API使用防抖
useEffect(() => { useEffect(() => {
fetchWorkersList(currentPage, pageSize) const timer = setTimeout(() => {
}, [currentPage, pageSize]) // 重置到第一页
setCurrentPage(1)
fetchWorkersList(1, pageSize, searchName, searchWorkerId, searchPhone)
}, 500) // 500ms 防抖
return () => clearTimeout(timer)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchName, searchWorkerId, searchPhone])
// 分页处理函数 // 分页处理函数
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
setCurrentPage(page) setCurrentPage(page)
fetchWorkersList(page, pageSize, searchName, searchWorkerId, searchPhone)
} }
const handlePageSizeChange = (size: number) => { const handlePageSizeChange = (size: number) => {
setPageSize(size) setPageSize(size)
setCurrentPage(1) setCurrentPage(1)
fetchWorkersList(1, size, searchName, searchWorkerId, searchPhone)
} }
const filteredWorkers = workers.filter((worker) => {
const matchesSearch =
worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
worker.workerId.toLowerCase().includes(searchTerm.toLowerCase()) ||
worker.dealerName.toLowerCase().includes(searchTerm.toLowerCase())
return matchesSearch
})
const handleAddWorker = async () => { const handleAddWorker = async () => {
if (!newWorker.name || !newWorker.phone || !newWorker.dealerId) { if (!newWorker.name || !newWorker.phone || !newWorker.dealerId) {
alert('请填写必填信息') alert('请填写必填信息')
@@ -294,7 +288,7 @@ export default function WorkersPage() {
console.log('添加工人成功:', result) console.log('添加工人成功:', result)
alert('添加工人成功!') alert('添加工人成功!')
fetchWorkersList(currentPage, pageSize) fetchWorkersList(currentPage, pageSize, searchName, searchWorkerId, searchPhone)
setNewWorker({ name: "", phone: "", dealerId: "", province: "", skillLevel: "", specialties: "", jobNum: "" }) setNewWorker({ name: "", phone: "", dealerId: "", province: "", skillLevel: "", specialties: "", jobNum: "" })
setIsAddDialogOpen(false) setIsAddDialogOpen(false)
} else { } else {
@@ -368,7 +362,7 @@ export default function WorkersPage() {
console.log('编辑工人成功:', result) console.log('编辑工人成功:', result)
alert('编辑工人成功!') alert('编辑工人成功!')
fetchWorkersList(currentPage, pageSize) fetchWorkersList(currentPage, pageSize, searchName, searchWorkerId, searchPhone)
setEditWorker({ id: "", name: "", phone: "", dealerId: "", province: "", skillLevel: "", specialties: "", jobNum: "", job: "1" }) setEditWorker({ id: "", name: "", phone: "", dealerId: "", province: "", skillLevel: "", specialties: "", jobNum: "", job: "1" })
setIsEditDialogOpen(false) setIsEditDialogOpen(false)
} else { } else {
@@ -418,7 +412,7 @@ export default function WorkersPage() {
console.log('删除工人成功:', result) console.log('删除工人成功:', result)
alert('删除工人成功!') alert('删除工人成功!')
fetchWorkersList(currentPage, pageSize) fetchWorkersList(currentPage, pageSize, searchName, searchWorkerId, searchPhone)
setDeletingWorker(null) setDeletingWorker(null)
setIsDeleteDialogOpen(false) setIsDeleteDialogOpen(false)
} else { } else {
@@ -811,25 +805,30 @@ export default function WorkersPage() {
<div className="relative flex-1 sm:flex-initial"> <div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="搜索工人姓名、工号或区域负责人..." placeholder="搜索工人姓名..."
value={searchTerm} value={searchName}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchName(e.target.value)}
className="pl-8 w-full sm:w-80" className="pl-8 w-full sm:w-48"
/>
</div>
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索工号..."
value={searchWorkerId}
onChange={(e) => setSearchWorkerId(e.target.value)}
className="pl-8 w-full sm:w-48"
/>
</div>
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索联系电话..."
value={searchPhone}
onChange={(e) => setSearchPhone(e.target.value)}
className="pl-8 w-full sm:w-48"
/> />
</div> </div>
<Select value={selectedWorkOrder} onValueChange={setSelectedWorkOrder}>
<SelectTrigger className="w-full sm:w-64">
<SelectValue placeholder="筛选工人" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
<SelectItem value="all"></SelectItem>
{workOrders.map((order) => (
<SelectItem key={order.value} value={order.value}>
{order.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@@ -837,10 +836,10 @@ export default function WorkersPage() {
<div className="space-y-4"> <div className="space-y-4">
{isLoading ? ( {isLoading ? (
<div className="text-center py-8 text-gray-500">...</div> <div className="text-center py-8 text-gray-500">...</div>
) : filteredWorkers.length === 0 ? ( ) : workers.length === 0 ? (
<div className="text-center py-8 text-gray-500"></div> <div className="text-center py-8 text-gray-500"></div>
) : ( ) : (
filteredWorkers.map((worker) => ( workers.map((worker) => (
<div key={worker.id} className="border rounded-lg"> <div key={worker.id} className="border rounded-lg">
<div <div
className="w-full p-3 sm:p-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4 hover:bg-gray-50 transition-colors cursor-pointer" className="w-full p-3 sm:p-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4 hover:bg-gray-50 transition-colors cursor-pointer"