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,
} 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 */}

View File

@@ -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>

View File

@@ -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>

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 { 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>

View File

@@ -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">

View File

@@ -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))
)
}

View File

@@ -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"