youhua1
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog"
|
||||
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 { getUserData } from "../../lib/utils/storage"
|
||||
|
||||
@@ -547,11 +547,6 @@ export default function CompanyPermissionsPage() {
|
||||
<SelectItem value="locked">锁定</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button variant="outline">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} 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"
|
||||
|
||||
// 商户数据类型(根据新接口返回格式定义)
|
||||
@@ -188,9 +188,9 @@ export default function EquipmentPage() {
|
||||
}
|
||||
const lowerSearch = searchTerm.toLowerCase()
|
||||
return merchants.filter(merchant =>
|
||||
merchant.merchantName.toLowerCase().includes(lowerSearch) ||
|
||||
merchant.contactPerson.toLowerCase().includes(lowerSearch) ||
|
||||
merchant.contactPhone.includes(lowerSearch) ||
|
||||
(merchant.merchantName && merchant.merchantName.toLowerCase().includes(lowerSearch)) ||
|
||||
(merchant.contactPerson && merchant.contactPerson.toLowerCase().includes(lowerSearch)) ||
|
||||
(merchant.contactPhone && merchant.contactPhone.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>
|
||||
<p className="text-sm sm:text-base text-gray-600">管理所有消防设备的档案和状态</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto">
|
||||
<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 })
|
||||
</div>
|
||||
|
||||
// 自动更新下一次检测时间(安装日期+半年)
|
||||
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>
|
||||
|
||||
{/* 编辑设备对话框 */}
|
||||
<Dialog open={isEditEquipmentOpen} onOpenChange={(open) => {
|
||||
{/* 编辑设备对话框 */}
|
||||
<Dialog open={isEditEquipmentOpen} onOpenChange={(open) => {
|
||||
setIsEditEquipmentOpen(open)
|
||||
if (!open) {
|
||||
setMerchantSearchForEdit("")
|
||||
@@ -1148,8 +906,6 @@ export default function EquipmentPage() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<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>
|
||||
<CardDescription className="text-xs sm:text-sm">共 {pagination.total} 台设备</CardDescription>
|
||||
</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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -271,6 +271,15 @@ export default function MallsPage() {
|
||||
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) => {
|
||||
switch (status) {
|
||||
@@ -1105,7 +1114,8 @@ export default function MallsPage() {
|
||||
<DialogDescription className="text-xs sm:text-sm">查看该商户的所有设备信息和状态</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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>
|
||||
<CardContent className="p-3 sm:p-4">
|
||||
<div className="text-center">
|
||||
@@ -1130,7 +1140,7 @@ export default function MallsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden md:block rounded-md border overflow-hidden">
|
||||
@@ -1163,7 +1173,7 @@ export default function MallsPage() {
|
||||
{equipment.equipmentName}
|
||||
</TableCell>
|
||||
<TableCell className="truncate max-w-[120px]" title={equipment.equipmentType}>
|
||||
{equipment.equipmentType}
|
||||
{getEquipmentTypeName(equipment.equipmentType)}
|
||||
</TableCell>
|
||||
<TableCell className="truncate max-w-[120px]" title={equipment.installationDate}>
|
||||
{equipment.installationDate}
|
||||
@@ -1209,7 +1219,7 @@ export default function MallsPage() {
|
||||
<div className="border-t pt-3 space-y-2.5">
|
||||
<div className="space-y-1">
|
||||
<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 className="space-y-1">
|
||||
<div className="text-xs font-medium text-gray-700">安装日期</div>
|
||||
|
||||
@@ -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 { Button } from "../ui/button"
|
||||
import { Input } from "../ui/input"
|
||||
@@ -14,75 +14,94 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog"
|
||||
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() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [users, setUsers] = useState<DisplayUser[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: "CU001",
|
||||
name: "张小明",
|
||||
email: "zhangxm@email.com",
|
||||
phone: "138****1234",
|
||||
address: "上海市黄浦区南京东路123号",
|
||||
registrationDate: "2024-01-15",
|
||||
lastActive: "2024-01-16 14:30",
|
||||
status: "active",
|
||||
orderCount: 5,
|
||||
totalSpent: 2580,
|
||||
},
|
||||
{
|
||||
id: "CU002",
|
||||
name: "李小红",
|
||||
email: "lixh@email.com",
|
||||
phone: "139****5678",
|
||||
address: "北京市朝阳区建国门外大街1号",
|
||||
registrationDate: "2024-01-10",
|
||||
lastActive: "2024-01-16 09:15",
|
||||
status: "active",
|
||||
orderCount: 3,
|
||||
totalSpent: 1200,
|
||||
},
|
||||
{
|
||||
id: "CU003",
|
||||
name: "王大华",
|
||||
email: "wangdh@email.com",
|
||||
phone: "137****9012",
|
||||
address: "广州市天河区天河路208号",
|
||||
registrationDate: "2023-12-20",
|
||||
lastActive: "2024-01-15 16:45",
|
||||
status: "active",
|
||||
orderCount: 8,
|
||||
totalSpent: 4200,
|
||||
},
|
||||
{
|
||||
id: "CU004",
|
||||
name: "赵小丽",
|
||||
email: "zhaoxl@email.com",
|
||||
phone: "136****3456",
|
||||
address: "深圳市南山区科技园南区",
|
||||
registrationDate: "2023-11-15",
|
||||
lastActive: "2024-01-12 11:20",
|
||||
status: "inactive",
|
||||
orderCount: 2,
|
||||
totalSpent: 800,
|
||||
},
|
||||
{
|
||||
id: "CU005",
|
||||
name: "陈小军",
|
||||
email: "chenxj@email.com",
|
||||
phone: "135****7890",
|
||||
address: "杭州市西湖区文三路259号",
|
||||
registrationDate: "2024-01-05",
|
||||
lastActive: "2024-01-16 13:10",
|
||||
status: "active",
|
||||
orderCount: 1,
|
||||
totalSpent: 350,
|
||||
},
|
||||
]
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const result = await apiGet<any>("/back/user/list?loginRole=normal")
|
||||
|
||||
console.log("用户列表API返回结果:", result)
|
||||
|
||||
if (result && result.code === 200) {
|
||||
// 处理不同的响应格式
|
||||
const userList = result.rows || result.data || []
|
||||
|
||||
// 调试:打印第一条用户数据,查看实际字段名
|
||||
if (userList.length > 0) {
|
||||
console.log("API 返回的用户数据示例:", userList[0])
|
||||
}
|
||||
|
||||
// 将 API 返回的用户数据映射到显示格式
|
||||
const mappedUsers: DisplayUser[] = userList.map((user: any) => {
|
||||
// 尝试多种可能的字段名(处理大小写和命名差异)
|
||||
const nickName = user.nickName || user.nickname || user.nick_name || ""
|
||||
const userName = user.userName || user.username || user.user_name || ""
|
||||
|
||||
return {
|
||||
id: user.userId || `CU${user.userId?.slice(-3) || '000'}`,
|
||||
name: nickName || userName || "未知用户",
|
||||
nickName: nickName,
|
||||
userName: userName,
|
||||
phone: user.phone || "",
|
||||
address: user.addr || "未填写",
|
||||
registrationDate: user.registerTime ? user.registerTime.split(" ")[0] : "",
|
||||
lastActive: user.updateTime || "",
|
||||
status: user.status === "0" ? "active" : user.status === "1" ? "inactive" : "blocked",
|
||||
orderCount: 0, // API 可能不包含此字段,需要后续补充
|
||||
totalSpent: 0, // API 可能不包含此字段,需要后续补充
|
||||
}
|
||||
})
|
||||
|
||||
setUsers(mappedUsers)
|
||||
} else {
|
||||
console.error("获取用户列表失败:", result?.msg || "未知错误")
|
||||
setUsers([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("请求用户列表失败:", error)
|
||||
setUsers([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [])
|
||||
|
||||
// 格式化手机号显示(脱敏)
|
||||
const formatPhone = (phone: string) => {
|
||||
if (!phone) return ""
|
||||
if (phone.length === 11) {
|
||||
return `${phone.slice(0, 3)}****${phone.slice(7)}`
|
||||
}
|
||||
return phone
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -100,7 +119,8 @@ export default function UsersPage() {
|
||||
const filteredUsers = users.filter((user) => {
|
||||
const matchesSearch =
|
||||
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.address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesStatus = statusFilter === "all" || user.status === statusFilter
|
||||
@@ -144,7 +164,7 @@ export default function UsersPage() {
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜索用户姓名、邮箱、手机号或地址..."
|
||||
placeholder="搜索用户姓名、手机号或地址..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
@@ -170,23 +190,30 @@ export default function UsersPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden md:block rounded-md border overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户信息</TableHead>
|
||||
<TableHead>联系方式</TableHead>
|
||||
<TableHead>地址</TableHead>
|
||||
<TableHead>注册时间</TableHead>
|
||||
<TableHead>最后活跃</TableHead>
|
||||
<TableHead>订单/消费</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">暂无用户数据</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden md:block rounded-md border overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户信息</TableHead>
|
||||
<TableHead>联系方式</TableHead>
|
||||
<TableHead>地址</TableHead>
|
||||
<TableHead>注册时间</TableHead>
|
||||
<TableHead>最后活跃</TableHead>
|
||||
<TableHead>订单/消费</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{user.name}</div>
|
||||
<div className="text-sm text-gray-500">{user.id}</div>
|
||||
<div className="font-medium">{user.nickName || "-"}</div>
|
||||
<div className="text-sm text-gray-500">{user.userName || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center text-sm">
|
||||
<Mail className="h-3 w-3 mr-1 text-gray-400" />
|
||||
{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 className="flex items-center text-sm">
|
||||
<Phone className="h-3 w-3 mr-1 text-gray-400" />
|
||||
{formatPhone(user.phone)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -241,24 +262,24 @@ export default function UsersPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden space-y-4">
|
||||
{filteredUsers.map((user) => (
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden space-y-4">
|
||||
{filteredUsers.map((user) => (
|
||||
<Card key={user.id}>
|
||||
<CardContent className="p-4 space-y-3">
|
||||
<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">
|
||||
<User className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{user.name}</div>
|
||||
<div className="text-sm text-gray-500">{user.id}</div>
|
||||
<div className="font-medium">{user.nickName || "-"}</div>
|
||||
<div className="text-sm text-gray-500">{user.userName || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(user.status)}
|
||||
@@ -267,13 +288,9 @@ export default function UsersPage() {
|
||||
<div className="border-t pt-3 space-y-2">
|
||||
<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">
|
||||
<Phone className="h-3 w-3 mr-1 text-gray-400" />
|
||||
{user.phone}
|
||||
{formatPhone(user.phone)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -315,10 +332,12 @@ export default function UsersPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@ interface ArchivedWorkOrder {
|
||||
brand: string;
|
||||
reformType: string;
|
||||
remark: string;
|
||||
overallResult: number | null;
|
||||
}
|
||||
|
||||
export default function WorkOrderArchivePage() {
|
||||
@@ -83,7 +84,19 @@ export default function WorkOrderArchivePage() {
|
||||
const fetchArchivedOrders = async (pageNum = 1, pageSize = 10) => {
|
||||
setLoading(true);
|
||||
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) {
|
||||
setArchivedOrders(response.data || []);
|
||||
setPagination({
|
||||
@@ -101,9 +114,19 @@ export default function WorkOrderArchivePage() {
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
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) => {
|
||||
setSelectedOrder(order);
|
||||
setIsDetailDialogOpen(true);
|
||||
@@ -244,7 +258,7 @@ export default function WorkOrderArchivePage() {
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜索工单编号、商户名称或设备..."
|
||||
placeholder="搜索工单编号..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 w-full"
|
||||
@@ -256,8 +270,9 @@ export default function WorkOrderArchivePage() {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="completed">已完成</SelectItem>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="1">合格</SelectItem>
|
||||
<SelectItem value="0">不合格</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -284,14 +299,14 @@ export default function WorkOrderArchivePage() {
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredOrders.length === 0 ? (
|
||||
) : archivedOrders.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8">
|
||||
<div className="text-gray-500">暂无数据</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
archivedOrders.map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-medium">{order.workOrderNumber}</TableCell>
|
||||
<TableCell>{order.workOrderType}</TableCell>
|
||||
@@ -325,10 +340,10 @@ export default function WorkOrderArchivePage() {
|
||||
<div className="md:hidden space-y-4">
|
||||
{loading ? (
|
||||
<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>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
archivedOrders.map((order) => (
|
||||
<Card key={order.id}>
|
||||
<CardContent className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
|
||||
@@ -155,7 +155,6 @@ interface WorkOrder {
|
||||
export default function WorkOrdersPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [workerFilter, setWorkerFilter] = useState("all")
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
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 matchesSearch =
|
||||
item.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.equipmentName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.merchantName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.workerName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
(item.workOrderNumber && item.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
(item.equipmentName && item.equipmentName.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
(item.merchantName && item.merchantName.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
(item.workerName && item.workerName.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
|
||||
// 使用工单的实际状态进行过滤
|
||||
const itemStatus = item.status || "1" // 默认为待接单状态
|
||||
const matchesStatus = statusFilter === "all" || itemStatus === statusFilter
|
||||
const matchesWorker = workerFilter === "all" || item.workerName === workerFilter
|
||||
return matchesSearch && matchesStatus && matchesWorker
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -430,20 +419,6 @@ export default function WorkOrdersPage() {
|
||||
</SelectContent>
|
||||
</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">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
@@ -451,7 +426,7 @@ export default function WorkOrdersPage() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -719,9 +694,9 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
const lowerSearch = searchTerm.toLowerCase()
|
||||
return merchants.filter(merchant =>
|
||||
merchant.merchantName.toLowerCase().includes(lowerSearch) ||
|
||||
merchant.contactPerson.toLowerCase().includes(lowerSearch) ||
|
||||
merchant.contactPhone.includes(lowerSearch) ||
|
||||
(merchant.merchantName && merchant.merchantName.toLowerCase().includes(lowerSearch)) ||
|
||||
(merchant.contactPerson && merchant.contactPerson.toLowerCase().includes(lowerSearch)) ||
|
||||
(merchant.contactPhone && merchant.contactPhone.includes(lowerSearch)) ||
|
||||
(merchant.mallLocation && merchant.mallLocation.toLowerCase().includes(lowerSearch))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ export default function WorkersPage() {
|
||||
const [workers, setWorkers] = useState<any[]>([])
|
||||
const [provinces, setProvinces] = useState<Province[]>([])
|
||||
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 [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
@@ -70,9 +72,6 @@ export default function WorkersPage() {
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [total, setTotal] = useState(0)
|
||||
// 工单选择相关状态
|
||||
const [workOrders, setWorkOrders] = useState<any[]>([])
|
||||
const [selectedWorkOrder, setSelectedWorkOrder] = useState("all")
|
||||
// 每个工人的工单分页状态
|
||||
const [workerOrdersPagination, setWorkerOrdersPagination] = useState<{
|
||||
[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) => {
|
||||
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 {
|
||||
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)
|
||||
|
||||
@@ -238,32 +232,32 @@ export default function WorkersPage() {
|
||||
// 组件挂载时获取数据
|
||||
useEffect(() => {
|
||||
fetchWorkersList()
|
||||
fetchWorkOrdersList()
|
||||
}, [])
|
||||
|
||||
// 分页变化时重新获取数据
|
||||
// 搜索条件变化时调用API(使用防抖)
|
||||
useEffect(() => {
|
||||
fetchWorkersList(currentPage, pageSize)
|
||||
}, [currentPage, pageSize])
|
||||
const timer = setTimeout(() => {
|
||||
// 重置到第一页
|
||||
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) => {
|
||||
setCurrentPage(page)
|
||||
fetchWorkersList(page, pageSize, searchName, searchWorkerId, searchPhone)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
setPageSize(size)
|
||||
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 () => {
|
||||
if (!newWorker.name || !newWorker.phone || !newWorker.dealerId) {
|
||||
alert('请填写必填信息')
|
||||
@@ -294,7 +288,7 @@ export default function WorkersPage() {
|
||||
console.log('添加工人成功:', result)
|
||||
alert('添加工人成功!')
|
||||
|
||||
fetchWorkersList(currentPage, pageSize)
|
||||
fetchWorkersList(currentPage, pageSize, searchName, searchWorkerId, searchPhone)
|
||||
setNewWorker({ name: "", phone: "", dealerId: "", province: "", skillLevel: "", specialties: "", jobNum: "" })
|
||||
setIsAddDialogOpen(false)
|
||||
} else {
|
||||
@@ -368,7 +362,7 @@ export default function WorkersPage() {
|
||||
console.log('编辑工人成功:', result)
|
||||
alert('编辑工人成功!')
|
||||
|
||||
fetchWorkersList(currentPage, pageSize)
|
||||
fetchWorkersList(currentPage, pageSize, searchName, searchWorkerId, searchPhone)
|
||||
setEditWorker({ id: "", name: "", phone: "", dealerId: "", province: "", skillLevel: "", specialties: "", jobNum: "", job: "1" })
|
||||
setIsEditDialogOpen(false)
|
||||
} else {
|
||||
@@ -418,7 +412,7 @@ export default function WorkersPage() {
|
||||
console.log('删除工人成功:', result)
|
||||
alert('删除工人成功!')
|
||||
|
||||
fetchWorkersList(currentPage, pageSize)
|
||||
fetchWorkersList(currentPage, pageSize, searchName, searchWorkerId, searchPhone)
|
||||
setDeletingWorker(null)
|
||||
setIsDeleteDialogOpen(false)
|
||||
} else {
|
||||
@@ -811,25 +805,30 @@ export default function WorkersPage() {
|
||||
<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={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8 w-full sm:w-80"
|
||||
placeholder="搜索工人姓名..."
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(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={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>
|
||||
<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>
|
||||
</CardHeader>
|
||||
@@ -837,10 +836,10 @@ export default function WorkersPage() {
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<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>
|
||||
) : (
|
||||
filteredWorkers.map((worker) => (
|
||||
workers.map((worker) => (
|
||||
<div key={worker.id} className="border rounded-lg">
|
||||
<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"
|
||||
|
||||
Reference in New Issue
Block a user