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 (
<form onSubmit={handleSubmit} className="space-y-4">

View File

@@ -94,6 +94,7 @@ export default function MallsPage() {
const [provinces, setProvinces] = useState<Province[]>([])
const [mallUsers, setMallUsers] = useState<MallUser[]>([])
const [loading, setLoading] = useState(false)
const [userRole, setUserRole] = useState<string>("")
const [newMall, setNewMall] = useState({
name: "",
address: "",
@@ -102,6 +103,18 @@ export default function MallsPage() {
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 () => {
try {
@@ -160,6 +173,7 @@ export default function MallsPage() {
// 页面加载时获取数据
useEffect(() => {
fetchUserRole()
fetchProvinces()
fetchMalls()
}, [])
@@ -250,108 +264,110 @@ export default function MallsPage() {
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<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)}>
{userRole !== "mall" && (
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
<Button
onClick={handleAddMall}
disabled={!newMall.name || !newMall.address || !newMall.provinceCode || !newMall.mallUserId || loading}
>
{loading ? "加载中..." : "确认添加"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</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
onClick={handleAddMall}
disabled={!newMall.name || !newMall.address || !newMall.provinceCode || !newMall.mallUserId || loading}
>
{loading ? "加载中..." : "确认添加"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
{/* 统计卡片 */}
@@ -451,26 +467,28 @@ export default function MallsPage() {
</div>
</div>
{getStatusBadge(mall.status)}
<div className="flex space-x-1">
<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()
// 处理编辑逻辑
}}
>
<Edit className="h-4 w-4" />
{userRole !== "mall" && (
<div className="flex space-x-1">
<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()
// 处理编辑逻辑
}}
>
<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
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>
{expandedMalls.includes(mall.id) && (

View File

@@ -63,6 +63,7 @@ export default function MerchantsPage() {
const [isAddMerchantOpen, setIsAddMerchantOpen] = useState(false)
const [isAddEquipmentOpen, setIsAddEquipmentOpen] = useState(false)
const [selectedMerchant, setSelectedMerchant] = useState<any>(null)
const [isEquipmentDialogOpen, setIsEquipmentDialogOpen] = useState(false)
const [malls, setMalls] = useState<any[]>([])
const [loadingMalls, setLoadingMalls] = useState(false)
const [totalMerchants, setTotalMerchants] = useState<TotalMerchant[]>([])
@@ -74,6 +75,7 @@ export default function MerchantsPage() {
const [total, setTotal] = useState(0)
const [merchantEquipments, setMerchantEquipments] = useState<{[key: string]: any[]}>({})
const [loadingMerchantEquipments, setLoadingMerchantEquipments] = useState<{[key: string]: boolean}>({})
const [userRole, setUserRole] = useState<string>("")
const [newMerchant, setNewMerchant] = useState({
name: "",
@@ -90,6 +92,18 @@ export default function MerchantsPage() {
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) => {
setNewMerchant({
@@ -166,20 +180,20 @@ export default function MerchantsPage() {
}
// 获取商户设备列表
const fetchMerchantEquipments = async (merchantsId: string) => {
setLoadingMerchantEquipments(prev => ({ ...prev, [merchantsId]: true }))
const fetchMerchantEquipments = async (merchantId: string) => {
setLoadingMerchantEquipments(prev => ({ ...prev, [merchantId]: true }))
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) {
setMerchantEquipments(prev => ({ ...prev, [merchantsId]: response.rows || [] }))
setMerchantEquipments(prev => ({ ...prev, [merchantId]: response.rows || [] }))
return response.rows || []
}
return []
} catch (error) {
console.error(`获取商户${merchantsId}设备列表失败:`, error)
console.error(`获取商户${merchantId}设备列表失败:`, error)
return []
} finally {
setLoadingMerchantEquipments(prev => ({ ...prev, [merchantsId]: false }))
setLoadingMerchantEquipments(prev => ({ ...prev, [merchantId]: false }))
}
}
@@ -218,6 +232,7 @@ export default function MerchantsPage() {
// 组件加载时获取数据
useEffect(() => {
fetchUserRole()
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 matchesSearch =
merchant.merchantName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -365,118 +388,6 @@ export default function MerchantsPage() {
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 (
<div className="space-y-6">
<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>
<p className="text-gray-600"></p>
</div>
<Button onClick={() => setIsAddMerchantOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
{userRole !== "merchant" && (
<Button onClick={() => setIsAddMerchantOpen(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
@@ -664,10 +577,15 @@ export default function MerchantsPage() {
</TableCell>
<TableCell>
<div className="flex space-x-2">
<EquipmentDialog merchant={merchant} />
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" onClick={() => handleViewEquipment(merchant)}>
<Eye className="h-4 w-4 mr-1" />
({merchant.equipmentCount})
</Button>
{userRole !== "merchant" && (
<Button variant="outline" size="sm">
</Button>
)}
</div>
</TableCell>
</TableRow>
@@ -943,6 +861,99 @@ export default function MerchantsPage() {
</div>
</DialogContent>
</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>
)
}