This commit is contained in:
menxipeng
2025-10-19 13:07:43 +08:00
parent 83f608e71d
commit ac28965614
3 changed files with 275 additions and 246 deletions

View File

@@ -527,7 +527,7 @@ function CreateUserForm({ roles, onClose }: { roles: Role[]; onClose: () => void
})) }))
} }
const shouldShowProvinces = formData.role && formData.role !== "admin" const shouldShowProvinces = formData.role && formData.role !== "admin" && formData.role !== "merchant"
return ( return (
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">

View File

@@ -94,6 +94,7 @@ export default function MallsPage() {
const [provinces, setProvinces] = useState<Province[]>([]) const [provinces, setProvinces] = useState<Province[]>([])
const [mallUsers, setMallUsers] = useState<MallUser[]>([]) const [mallUsers, setMallUsers] = useState<MallUser[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [userRole, setUserRole] = useState<string>("")
const [newMall, setNewMall] = useState({ const [newMall, setNewMall] = useState({
name: "", name: "",
address: "", address: "",
@@ -102,6 +103,18 @@ export default function MallsPage() {
description: "", description: "",
}) })
// 获取用户角色
const fetchUserRole = async () => {
try {
const response = await apiGet('/back/getUserRole')
if (response.code === 200) {
setUserRole(response.data || "")
}
} catch (error) {
console.error('获取用户角色失败:', error)
}
}
// 获取商场列表 // 获取商场列表
const fetchMalls = async () => { const fetchMalls = async () => {
try { try {
@@ -160,6 +173,7 @@ export default function MallsPage() {
// 页面加载时获取数据 // 页面加载时获取数据
useEffect(() => { useEffect(() => {
fetchUserRole()
fetchProvinces() fetchProvinces()
fetchMalls() fetchMalls()
}, []) }, [])
@@ -250,108 +264,110 @@ export default function MallsPage() {
<h1 className="text-2xl font-bold text-gray-900"></h1> <h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p> <p className="text-gray-600"></p>
</div> </div>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> {userRole !== "mall" && (
<DialogTrigger asChild> <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<Button> <DialogTrigger asChild>
<Plus className="h-4 w-4 mr-2" /> <Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={newMall.name}
onChange={(e) => setNewMall({ ...newMall, name: e.target.value })}
placeholder="请输入商场名称"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="address"></Label>
<Input
id="address"
value={newMall.address}
onChange={(e) => setNewMall({ ...newMall, address: e.target.value })}
placeholder="请输入详细地址"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="province"></Label>
<Select
value={newMall.provinceCode}
onValueChange={(value) => setNewMall({ ...newMall, provinceCode: value })}
disabled={loading}
>
<SelectTrigger>
<SelectValue placeholder="请选择省份" />
</SelectTrigger>
<SelectContent>
{provinces.map((province) => (
<SelectItem key={province.code} value={province.code}>
{province.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="mallUser"></Label>
<Select
value={newMall.mallUserId}
onValueChange={(value) => setNewMall({ ...newMall, mallUserId: value })}
disabled={loading || !newMall.provinceCode}
>
<SelectTrigger>
<SelectValue placeholder={
!newMall.provinceCode
? "请先选择省份"
: mallUsers.length === 0
? "该省份暂无可选商场"
: "请选择商场"
} />
</SelectTrigger>
<SelectContent>
{mallUsers.map((mallUser) => (
<SelectItem key={mallUser.userId} value={mallUser.userId}>
<div className="flex flex-col items-start">
<span>{mallUser.userName}</span>
<span className="text-xs text-gray-500">{mallUser.phonenumber}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={newMall.description}
onChange={(e) => setNewMall({ ...newMall, description: e.target.value })}
placeholder="请输入备注信息(可选)"
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
</Button> </Button>
<Button </DialogTrigger>
onClick={handleAddMall} <DialogContent className="sm:max-w-[500px]">
disabled={!newMall.name || !newMall.address || !newMall.provinceCode || !newMall.mallUserId || loading} <DialogHeader>
> <DialogTitle></DialogTitle>
{loading ? "加载中..." : "确认添加"} <DialogDescription></DialogDescription>
</Button> </DialogHeader>
</DialogFooter> <div className="grid gap-4 py-4">
</DialogContent> <div className="grid gap-2">
</Dialog> <Label htmlFor="name"></Label>
<Input
id="name"
value={newMall.name}
onChange={(e) => setNewMall({ ...newMall, name: e.target.value })}
placeholder="请输入商场名称"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="address"></Label>
<Input
id="address"
value={newMall.address}
onChange={(e) => setNewMall({ ...newMall, address: e.target.value })}
placeholder="请输入详细地址"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="province"></Label>
<Select
value={newMall.provinceCode}
onValueChange={(value) => setNewMall({ ...newMall, provinceCode: value })}
disabled={loading}
>
<SelectTrigger>
<SelectValue placeholder="请选择省份" />
</SelectTrigger>
<SelectContent>
{provinces.map((province) => (
<SelectItem key={province.code} value={province.code}>
{province.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="mallUser"></Label>
<Select
value={newMall.mallUserId}
onValueChange={(value) => setNewMall({ ...newMall, mallUserId: value })}
disabled={loading || !newMall.provinceCode}
>
<SelectTrigger>
<SelectValue placeholder={
!newMall.provinceCode
? "请先选择省份"
: mallUsers.length === 0
? "该省份暂无可选商场"
: "请选择商场"
} />
</SelectTrigger>
<SelectContent>
{mallUsers.map((mallUser) => (
<SelectItem key={mallUser.userId} value={mallUser.userId}>
<div className="flex flex-col items-start">
<span>{mallUser.userName}</span>
<span className="text-xs text-gray-500">{mallUser.phonenumber}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={newMall.description}
onChange={(e) => setNewMall({ ...newMall, description: e.target.value })}
placeholder="请输入备注信息(可选)"
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
</Button>
<Button
onClick={handleAddMall}
disabled={!newMall.name || !newMall.address || !newMall.provinceCode || !newMall.mallUserId || loading}
>
{loading ? "加载中..." : "确认添加"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div> </div>
{/* 统计卡片 */} {/* 统计卡片 */}
@@ -451,26 +467,28 @@ export default function MallsPage() {
</div> </div>
</div> </div>
{getStatusBadge(mall.status)} {getStatusBadge(mall.status)}
<div className="flex space-x-1"> {userRole !== "mall" && (
<div <div className="flex space-x-1">
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 px-3 cursor-pointer" <div
onClick={(e) => { className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 px-3 cursor-pointer"
e.stopPropagation() onClick={(e) => {
// 处理编辑逻辑 e.stopPropagation()
}} // 处理编辑逻辑
> }}
<Edit className="h-4 w-4" /> >
<Edit className="h-4 w-4" />
</div>
<div
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 px-3 cursor-pointer"
onClick={(e) => {
e.stopPropagation()
// 处理删除逻辑
}}
>
<Trash2 className="h-4 w-4" />
</div>
</div> </div>
<div )}
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 px-3 cursor-pointer"
onClick={(e) => {
e.stopPropagation()
// 处理删除逻辑
}}
>
<Trash2 className="h-4 w-4" />
</div>
</div>
</div> </div>
</div> </div>
{expandedMalls.includes(mall.id) && ( {expandedMalls.includes(mall.id) && (

View File

@@ -63,6 +63,7 @@ export default function MerchantsPage() {
const [isAddMerchantOpen, setIsAddMerchantOpen] = useState(false) const [isAddMerchantOpen, setIsAddMerchantOpen] = useState(false)
const [isAddEquipmentOpen, setIsAddEquipmentOpen] = useState(false) const [isAddEquipmentOpen, setIsAddEquipmentOpen] = useState(false)
const [selectedMerchant, setSelectedMerchant] = useState<any>(null) const [selectedMerchant, setSelectedMerchant] = useState<any>(null)
const [isEquipmentDialogOpen, setIsEquipmentDialogOpen] = useState(false)
const [malls, setMalls] = useState<any[]>([]) const [malls, setMalls] = useState<any[]>([])
const [loadingMalls, setLoadingMalls] = useState(false) const [loadingMalls, setLoadingMalls] = useState(false)
const [totalMerchants, setTotalMerchants] = useState<TotalMerchant[]>([]) const [totalMerchants, setTotalMerchants] = useState<TotalMerchant[]>([])
@@ -74,6 +75,7 @@ export default function MerchantsPage() {
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [merchantEquipments, setMerchantEquipments] = useState<{[key: string]: any[]}>({}) const [merchantEquipments, setMerchantEquipments] = useState<{[key: string]: any[]}>({})
const [loadingMerchantEquipments, setLoadingMerchantEquipments] = useState<{[key: string]: boolean}>({}) const [loadingMerchantEquipments, setLoadingMerchantEquipments] = useState<{[key: string]: boolean}>({})
const [userRole, setUserRole] = useState<string>("")
const [newMerchant, setNewMerchant] = useState({ const [newMerchant, setNewMerchant] = useState({
name: "", name: "",
@@ -90,6 +92,18 @@ export default function MerchantsPage() {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
// 获取用户角色
const fetchUserRole = async () => {
try {
const response = await apiGet('/back/getUserRole')
if (response.code === 200) {
setUserRole(response.data || "")
}
} catch (error) {
console.error('获取用户角色失败:', error)
}
}
// 处理省份变化 // 处理省份变化
const handleProvinceChange = (province: string) => { const handleProvinceChange = (province: string) => {
setNewMerchant({ setNewMerchant({
@@ -166,20 +180,20 @@ export default function MerchantsPage() {
} }
// 获取商户设备列表 // 获取商户设备列表
const fetchMerchantEquipments = async (merchantsId: string) => { const fetchMerchantEquipments = async (merchantId: string) => {
setLoadingMerchantEquipments(prev => ({ ...prev, [merchantsId]: true })) setLoadingMerchantEquipments(prev => ({ ...prev, [merchantId]: true }))
try { try {
const response = await apiGet(`/back/equipment/list?pageNum=1&pageSize=10&merchantsId=${merchantsId}`) const response = await apiGet(`/back/equipment/list?pageNum=1&pageSize=10&merchantId=${merchantId}`)
if (response.code === 200) { if (response.code === 200) {
setMerchantEquipments(prev => ({ ...prev, [merchantsId]: response.rows || [] })) setMerchantEquipments(prev => ({ ...prev, [merchantId]: response.rows || [] }))
return response.rows || [] return response.rows || []
} }
return [] return []
} catch (error) { } catch (error) {
console.error(`获取商户${merchantsId}设备列表失败:`, error) console.error(`获取商户${merchantId}设备列表失败:`, error)
return [] return []
} finally { } finally {
setLoadingMerchantEquipments(prev => ({ ...prev, [merchantsId]: false })) setLoadingMerchantEquipments(prev => ({ ...prev, [merchantId]: false }))
} }
} }
@@ -218,6 +232,7 @@ export default function MerchantsPage() {
// 组件加载时获取数据 // 组件加载时获取数据
useEffect(() => { useEffect(() => {
fetchUserRole()
fetchMerchants() fetchMerchants()
}, []) }, [])
@@ -347,6 +362,14 @@ export default function MerchantsPage() {
} }
} }
// 打开设备对话框
const handleViewEquipment = (merchant: any) => {
setSelectedMerchant(merchant)
setIsEquipmentDialogOpen(true)
const merchantId = merchant.merchantsId || merchant.id
fetchMerchantEquipments(merchantId)
}
const filteredMerchants = merchants.filter((merchant) => { const filteredMerchants = merchants.filter((merchant) => {
const matchesSearch = const matchesSearch =
merchant.merchantName?.toLowerCase().includes(searchTerm.toLowerCase()) || merchant.merchantName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -365,118 +388,6 @@ export default function MerchantsPage() {
return matchesSearch && matchesStatus return matchesSearch && matchesStatus
}) })
const EquipmentDialog = ({ merchant }: { merchant: any }) => {
const merchantsId = merchant.merchantsId || merchant.id
const equipments = merchantEquipments[merchantsId] || []
const isLoading = loadingMerchantEquipments[merchantsId] || false
const handleOpenChange = (open: boolean) => {
if (open && !merchantEquipments[merchantsId]) {
fetchMerchantEquipments(merchantsId)
}
}
return (
<Dialog onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
({merchant.equipmentCount})
</Button>
</DialogTrigger>
<DialogContent className="!w-[1400px] !max-w-[1400px] max-h-[800px] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>{merchant.merchantName} - </DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{merchant.normalCount}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-yellow-600">{merchant.expiringCount}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-red-600">{merchant.expiredCount}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
</div>
<div className="rounded-md border overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-4">
...
</TableCell>
</TableRow>
) : equipments.length > 0 ? (
equipments.map((equipment: any) => (
<TableRow key={equipment.id}>
<TableCell className="font-medium truncate max-w-[120px]" title={equipment.equipmentId}>
{equipment.equipmentId}
</TableCell>
<TableCell className="truncate max-w-[150px]" title={equipment.equipmentName}>
{equipment.equipmentName}
</TableCell>
<TableCell className="truncate max-w-[120px]" title={equipment.equipmentType}>
{equipment.equipmentType}
</TableCell>
<TableCell className="truncate max-w-[120px]" title={equipment.installationDate}>
{equipment.installationDate}
</TableCell>
<TableCell className="truncate max-w-[150px]" title={equipment.installationLocation}>
{equipment.installationLocation}
</TableCell>
<TableCell>
{getStatusBadge(equipment.status)}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="text-center py-4">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -484,10 +395,12 @@ export default function MerchantsPage() {
<h1 className="text-3xl font-bold text-gray-900"></h1> <h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p> <p className="text-gray-600"></p>
</div> </div>
<Button onClick={() => setIsAddMerchantOpen(true)}> {userRole !== "merchant" && (
<Plus className="h-4 w-4 mr-2" /> <Button onClick={() => setIsAddMerchantOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</Button>
)}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
@@ -664,10 +577,15 @@ export default function MerchantsPage() {
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex space-x-2"> <div className="flex space-x-2">
<EquipmentDialog merchant={merchant} /> <Button variant="outline" size="sm" onClick={() => handleViewEquipment(merchant)}>
<Button variant="outline" size="sm"> <Eye className="h-4 w-4 mr-1" />
({merchant.equipmentCount})
</Button> </Button>
{userRole !== "merchant" && (
<Button variant="outline" size="sm">
</Button>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -943,6 +861,99 @@ export default function MerchantsPage() {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* 设备详情对话框 */}
<Dialog open={isEquipmentDialogOpen} onOpenChange={setIsEquipmentDialogOpen}>
<DialogContent className="!w-[1400px] !max-w-[1400px] max-h-[800px] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>{selectedMerchant?.merchantName} - </DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{selectedMerchant?.normalCount || 0}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-yellow-600">{selectedMerchant?.expiringCount || 0}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-center">
<p className="text-2xl font-bold text-red-600">{selectedMerchant?.expiredCount || 0}</p>
<p className="text-sm text-gray-600"></p>
</div>
</CardContent>
</Card>
</div>
<div className="rounded-md border overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loadingMerchantEquipments[selectedMerchant?.merchantsId || selectedMerchant?.id] ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-4">
...
</TableCell>
</TableRow>
) : (merchantEquipments[selectedMerchant?.merchantsId || selectedMerchant?.id] || []).length > 0 ? (
(merchantEquipments[selectedMerchant?.merchantsId || selectedMerchant?.id] || []).map((equipment: any) => (
<TableRow key={equipment.id}>
<TableCell className="font-medium truncate max-w-[120px]" title={equipment.equipmentId}>
{equipment.equipmentId}
</TableCell>
<TableCell className="truncate max-w-[150px]" title={equipment.equipmentName}>
{equipment.equipmentName}
</TableCell>
<TableCell className="truncate max-w-[120px]" title={equipment.equipmentType}>
{equipment.equipmentType}
</TableCell>
<TableCell className="truncate max-w-[120px]" title={equipment.installationDate}>
{equipment.installationDate}
</TableCell>
<TableCell className="truncate max-w-[150px]" title={equipment.installationLocation}>
{equipment.installationLocation}
</TableCell>
<TableCell>
{getStatusBadge(equipment.status)}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="text-center py-4">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</div> </div>
) )
} }