页面
This commit is contained in:
@@ -14,8 +14,9 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog"
|
||||
import { Label } from "../ui/label"
|
||||
import { Plus, Search, Download, User, Shield, Building, Store, Wrench, Eye, Edit } from "lucide-react"
|
||||
import { apiGet, apiPost } from "../../lib/services/api"
|
||||
import { Plus, Search, Download, 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"
|
||||
|
||||
// 定义角色数据类型
|
||||
interface Role {
|
||||
@@ -67,6 +68,12 @@ export default function CompanyPermissionsPage() {
|
||||
const [total, setTotal] = useState(0)
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<string>("")
|
||||
const [activeTab, setActiveTab] = useState("all")
|
||||
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
const [selectedUser, setSelectedUser] = useState<UserData | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [currentUserRole, setCurrentUserRole] = useState<string>("")
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
@@ -134,6 +141,20 @@ export default function CompanyPermissionsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前用户是否是总部管理员
|
||||
useEffect(() => {
|
||||
const userData = getUserData()
|
||||
if (userData) {
|
||||
// 从用户数据中获取角色信息
|
||||
const roleName = userData.roleName || userData.roles?.[0]?.roleName || ""
|
||||
const roleKey = roles.find(r => r.roleName === roleName)?.roleKey || ""
|
||||
setCurrentUserRole(roleKey)
|
||||
}
|
||||
}, [roles])
|
||||
|
||||
// 判断是否是总部管理员
|
||||
const isAdmin = currentUserRole === "admin"
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
fetchRoles()
|
||||
@@ -204,6 +225,56 @@ export default function CompanyPermissionsPage() {
|
||||
return acc
|
||||
}, {} as Record<string, UserData[]>)
|
||||
|
||||
// 查看用户详情
|
||||
const handleViewUser = (user: UserData) => {
|
||||
setSelectedUser(user)
|
||||
setIsViewDialogOpen(true)
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEditUser = (user: UserData) => {
|
||||
setSelectedUser(user)
|
||||
setIsEditDialogOpen(true)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = (user: UserData) => {
|
||||
setSelectedUser(user)
|
||||
setIsDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
// 确认删除用户
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!selectedUser) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const result = await apiDelete(`/system/user/${selectedUser.userId}`)
|
||||
|
||||
if (result.code === 200) {
|
||||
alert('删除用户成功')
|
||||
setIsDeleteDialogOpen(false)
|
||||
setSelectedUser(null)
|
||||
// 刷新数据
|
||||
if (roleFilter === "all") {
|
||||
fetchAllUsers()
|
||||
} else {
|
||||
const role = roles.find(r => r.roleKey === roleFilter)
|
||||
if (role) {
|
||||
fetchUsersByRole(role.roleId, currentPage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(result.msg || '删除用户失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
alert('网络错误,请稍后重试')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const UserTable = ({ users }: { users: UserData[] }) => (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
@@ -216,19 +287,19 @@ export default function CompanyPermissionsPage() {
|
||||
<TableHead>联系方式</TableHead>
|
||||
<TableHead>最后登录</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
{isAdmin && <TableHead>操作</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8">
|
||||
<TableCell colSpan={isAdmin ? 8 : 7} className="text-center py-8">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : users.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8">
|
||||
<TableCell colSpan={isAdmin ? 8 : 7} className="text-center py-8">
|
||||
<div className="text-gray-500">暂无数据</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -270,16 +341,37 @@ export default function CompanyPermissionsPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(user.status)}</TableCell>
|
||||
{isAdmin && (
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleViewUser(user)}
|
||||
title="查看"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditUser(user)}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUser(user)}
|
||||
title="删除"
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
@@ -297,6 +389,7 @@ export default function CompanyPermissionsPage() {
|
||||
<h1 className="text-3xl font-bold text-gray-900">公司权限管理</h1>
|
||||
<p className="text-gray-600">管理公司内部用户账户和权限分配</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
@@ -312,6 +405,7 @@ export default function CompanyPermissionsPage() {
|
||||
<CreateUserForm roles={roles} onClose={() => setIsCreateDialogOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -425,6 +519,133 @@ export default function CompanyPermissionsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 查看用户详情对话框 */}
|
||||
<Dialog open={isViewDialogOpen} onOpenChange={setIsViewDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>用户详情</DialogTitle>
|
||||
<DialogDescription>查看用户详细信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedUser && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-gray-600">用户名</Label>
|
||||
<div className="mt-1 text-sm">{selectedUser.userName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">姓名</Label>
|
||||
<div className="mt-1 text-sm">{selectedUser.nickName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">手机号</Label>
|
||||
<div className="mt-1 text-sm">{selectedUser.phonenumber || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">状态</Label>
|
||||
<div className="mt-1">{getStatusBadge(selectedUser.status)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">角色</Label>
|
||||
<div className="mt-1">
|
||||
{selectedUser.roles.map((role, idx) => (
|
||||
<Badge key={idx} className="mr-1">
|
||||
{role.roleName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">所属省份</Label>
|
||||
<div className="mt-1 text-sm">
|
||||
{selectedUser.provinceName || '总公司'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">部门/机构</Label>
|
||||
<div className="mt-1 text-sm">{selectedUser.dept?.deptName || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-600">最后登录</Label>
|
||||
<div className="mt-1 text-sm">
|
||||
{selectedUser.loginDate
|
||||
? new Date(selectedUser.loginDate).toLocaleString('zh-CN')
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={() => setIsViewDialogOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 编辑用户对话框 */}
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑用户</DialogTitle>
|
||||
<DialogDescription>修改用户信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedUser && (
|
||||
<EditUserForm
|
||||
user={selectedUser}
|
||||
roles={roles}
|
||||
onClose={() => {
|
||||
setIsEditDialogOpen(false)
|
||||
setSelectedUser(null)
|
||||
}}
|
||||
onSuccess={() => {
|
||||
// 刷新数据
|
||||
if (roleFilter === "all") {
|
||||
fetchAllUsers()
|
||||
} else {
|
||||
const role = roles.find(r => r.roleKey === roleFilter)
|
||||
if (role) {
|
||||
fetchUsersByRole(role.roleId, currentPage)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">确认删除用户</DialogTitle>
|
||||
<DialogDescription>
|
||||
您确定要删除用户 <span className="font-medium text-red-600">{selectedUser?.nickName || selectedUser?.userName}</span> 吗?此操作不可撤销。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsDeleteDialogOpen(false)
|
||||
setSelectedUser(null)
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "删除中..." : "确认删除"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -667,3 +888,297 @@ function CreateUserForm({ roles, onClose }: { roles: Role[]; onClose: () => void
|
||||
)
|
||||
}
|
||||
|
||||
function EditUserForm({
|
||||
user,
|
||||
roles,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
user: UserData
|
||||
roles: Role[]
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}) {
|
||||
const [formData, setFormData] = useState({
|
||||
username: user.userName || "",
|
||||
name: user.nickName || "",
|
||||
phone: user.phonenumber || "",
|
||||
role: "",
|
||||
department: user.dept?.deptName || "",
|
||||
provinces: [] as string[],
|
||||
password: "",
|
||||
status: user.status || "0",
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [availableProvinces, setAvailableProvinces] = useState<any[]>([])
|
||||
const [provincesLoading, setProvincesLoading] = useState(false)
|
||||
|
||||
// 根据角色获取省份数据
|
||||
const fetchProvinces = async (roleKey: string) => {
|
||||
try {
|
||||
setProvincesLoading(true)
|
||||
|
||||
const endpoint = roleKey === "common"
|
||||
? "/back/region/deProvinces"
|
||||
: "/back/region/provinces"
|
||||
|
||||
const result = await apiGet(endpoint)
|
||||
|
||||
if (result.code === 200) {
|
||||
setAvailableProvinces(result.data || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取省份数据失败:', error)
|
||||
} finally {
|
||||
setProvincesLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
const userRole = user.roles[0]
|
||||
const role = roles.find(r => r.roleName === userRole?.roleName)
|
||||
if (role) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
role: role.roleKey
|
||||
}))
|
||||
// 如果有省份信息,解析省份名称
|
||||
if (user.provinceName) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
provinces: [user.provinceName]
|
||||
}))
|
||||
}
|
||||
// 加载省份数据
|
||||
if (role.roleKey && role.roleKey !== "admin" && role.roleKey !== "merchant") {
|
||||
fetchProvinces(role.roleKey)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, roles])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!formData.role) {
|
||||
alert('请选择角色')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const selectedRole = roles.find(r => r.roleKey === formData.role)
|
||||
if (!selectedRole) {
|
||||
alert('选择的角色无效')
|
||||
return
|
||||
}
|
||||
|
||||
const selectedProvinceCodes = formData.provinces
|
||||
.map(provinceName => {
|
||||
const province = availableProvinces.find(p => p.name === provinceName)
|
||||
return province ? province.code : null
|
||||
})
|
||||
.filter(code => code !== null)
|
||||
.join(',')
|
||||
|
||||
const requestData: any = {
|
||||
userId: user.userId,
|
||||
userName: formData.username,
|
||||
nickName: formData.name,
|
||||
phonenumber: formData.phone,
|
||||
sex: "0",
|
||||
status: formData.status,
|
||||
remark: formData.department || "",
|
||||
postIds: [],
|
||||
roleIds: [selectedRole.roleId],
|
||||
provinceCode: selectedProvinceCodes || ""
|
||||
}
|
||||
|
||||
// 如果修改了密码,才包含密码字段
|
||||
if (formData.password.trim()) {
|
||||
requestData.password = formData.password
|
||||
}
|
||||
|
||||
console.log('更新用户请求参数:', requestData)
|
||||
|
||||
const result = await apiPut('/system/user', requestData)
|
||||
|
||||
if (result.code === 200) {
|
||||
alert('用户更新成功')
|
||||
onClose()
|
||||
onSuccess()
|
||||
} else {
|
||||
alert(result.msg || '更新用户失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
alert('网络错误,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleProvinceToggle = (province: string) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
provinces: prev.provinces.includes(province)
|
||||
? prev.provinces.filter((p) => p !== province)
|
||||
: [...prev.provinces, province],
|
||||
}))
|
||||
}
|
||||
|
||||
const shouldShowProvinces = formData.role && formData.role !== "admin" && formData.role !== "merchant"
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-username">用户名</Label>
|
||||
<Input
|
||||
id="edit-username"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-name">姓名</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="请输入真实姓名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-phone">手机号</Label>
|
||||
<Input
|
||||
id="edit-phone"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
placeholder="请输入手机号"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-role">角色</Label>
|
||||
<Select
|
||||
value={formData.role}
|
||||
onValueChange={(value) => {
|
||||
setFormData({ ...formData, role: value, provinces: [] })
|
||||
if (value && value !== "admin") {
|
||||
fetchProvinces(value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.roleId} value={role.roleKey}>
|
||||
{role.roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-status">状态</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(value) => setFormData({ ...formData, status: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">正常</SelectItem>
|
||||
<SelectItem value="1">停用</SelectItem>
|
||||
<SelectItem value="2">锁定</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{shouldShowProvinces && (
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label htmlFor="edit-provinces">归属省份</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-500">
|
||||
{formData.role === "common" ? "经销商可选择多个省份" : "其他角色只能选择一个省份"}
|
||||
</div>
|
||||
{provincesLoading ? (
|
||||
<div className="text-sm text-gray-500">正在加载省份数据...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{availableProvinces.map((province) => (
|
||||
<div key={province.code} className="flex items-center space-x-2">
|
||||
<input
|
||||
type={formData.role === "common" ? "checkbox" : "radio"}
|
||||
id={`edit-province-${province.code}`}
|
||||
name="edit-provinces"
|
||||
checked={formData.provinces.includes(province.name)}
|
||||
onChange={() => {
|
||||
if (formData.role === "common") {
|
||||
handleProvinceToggle(province.name)
|
||||
} else {
|
||||
setFormData({ ...formData, provinces: [province.name] })
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<Label htmlFor={`edit-province-${province.code}`} className="text-sm">
|
||||
{province.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{formData.provinces.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{formData.provinces.map((province) => (
|
||||
<Badge key={province} variant="outline" className="text-xs">
|
||||
{province}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label htmlFor="edit-password">新密码(留空则不修改)</Label>
|
||||
<Input
|
||||
id="edit-password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="留空则不修改密码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !formData.username || !formData.name || !formData.role}
|
||||
>
|
||||
{loading ? '更新中...' : '更新用户'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ export default function EquipmentPage() {
|
||||
const [statusFilter, setStatusFilter] = useState("all")
|
||||
const [isAddEquipmentOpen, setIsAddEquipmentOpen] = useState(false)
|
||||
const [isEditEquipmentOpen, setIsEditEquipmentOpen] = useState(false)
|
||||
const [isViewEquipmentOpen, setIsViewEquipmentOpen] = useState(false)
|
||||
const [viewEquipment, setViewEquipment] = useState<Equipment | null>(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [merchants, setMerchants] = useState<ProvinceMerchant[]>([])
|
||||
const [loadingMerchants, setLoadingMerchants] = useState(false)
|
||||
@@ -316,6 +318,16 @@ export default function EquipmentPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开查看对话框
|
||||
const handleViewEquipment = (equipment: Equipment) => {
|
||||
setViewEquipment(equipment)
|
||||
setIsViewEquipmentOpen(true)
|
||||
// 加载设备类型数据用于显示
|
||||
if (Object.keys(equipmentTypes).length === 0) {
|
||||
fetchEquipmentTypes()
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑对话框
|
||||
const handleEditEquipment = (equipment: Equipment) => {
|
||||
setEditEquipment({
|
||||
@@ -891,6 +903,146 @@ export default function EquipmentPage() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 查看设备对话框 */}
|
||||
<Dialog open={isViewEquipmentOpen} onOpenChange={setIsViewEquipmentOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col w-[95vw] sm:w-full p-0 sm:p-6">
|
||||
<DialogHeader className="px-4 sm:px-0 pt-4 sm:pt-0 pb-3 sm:pb-4 border-b sm:border-b-0">
|
||||
<DialogTitle className="text-base sm:text-lg">设备详情</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">查看设备完整信息</DialogDescription>
|
||||
</DialogHeader>
|
||||
{viewEquipment && (
|
||||
<div className="flex-1 overflow-y-auto px-4 sm:px-0 py-3 sm:py-4 space-y-4 sm:space-y-6">
|
||||
{/* 设备基本信息 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 sm:gap-x-6">
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">设备编号</Label>
|
||||
<div className="text-sm sm:text-base font-medium text-gray-900 flex items-center gap-2 min-w-0">
|
||||
<Shield className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||
<span className="truncate">{viewEquipment.equipmentId}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">设备名称</Label>
|
||||
<div className="text-sm sm:text-base font-medium text-gray-900 break-words">{viewEquipment.equipmentName}</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">设备类型</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">
|
||||
{equipmentTypes[viewEquipment.equipmentType] || viewEquipment.equipmentType}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">设备型号</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">{viewEquipment.equipmentModel || '-'}</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">设备状态</Label>
|
||||
<div className="flex items-center">{getStatusBadge(viewEquipment.status)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 商户信息 */}
|
||||
<div className="border-t pt-4 sm:pt-6">
|
||||
<h3 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4">商户信息</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 sm:gap-x-6">
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">商户名称</Label>
|
||||
<div className="text-sm sm:text-base font-medium text-gray-900 break-words">{viewEquipment.merchantName}</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">商场名称</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">{viewEquipment.mallName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-1 sm:col-span-2 space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">安装位置</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 flex items-start gap-2 min-w-0">
|
||||
<MapPin className="h-4 w-4 text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="break-words flex-1">{viewEquipment.installationLocation || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 检测信息 */}
|
||||
<div className="border-t pt-4 sm:pt-6">
|
||||
<h3 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4">检测信息</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 sm:gap-x-6">
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">安装日期</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
<span>{viewEquipment.installationDate || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">下一次检测时间</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||
<span className={viewEquipment.status === "3" ? "text-red-600 font-medium" : viewEquipment.status === "2" ? "text-yellow-600 font-medium" : ""}>
|
||||
{viewEquipment.nextInspectionDate || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 其他信息 */}
|
||||
<div className="border-t pt-4 sm:pt-6">
|
||||
<h3 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4">其他信息</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 sm:gap-x-6">
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">创建人</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">{viewEquipment.createdBy || '-'}</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">创建时间</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">{viewEquipment.createdAt || '-'}</div>
|
||||
</div>
|
||||
{viewEquipment.updatedBy && (
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">更新人</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">{viewEquipment.updatedBy}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">更新时间</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 break-words">{viewEquipment.updatedAt || '-'}</div>
|
||||
</div>
|
||||
{viewEquipment.remarks && (
|
||||
<div className="col-span-1 sm:col-span-2 space-y-1.5 sm:space-y-2">
|
||||
<Label className="text-xs sm:text-sm text-gray-500 block">备注</Label>
|
||||
<div className="text-sm sm:text-base text-gray-900 bg-gray-50 p-3 rounded-md whitespace-pre-wrap break-words">
|
||||
{viewEquipment.remarks}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 px-4 sm:px-0 py-3 sm:py-0 mt-auto border-t pt-3 sm:pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsViewEquipmentOpen(false)}
|
||||
className="w-full sm:w-auto text-sm sm:text-base h-9 sm:h-10"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
{viewEquipment && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsViewEquipmentOpen(false)
|
||||
handleEditEquipment(viewEquipment)
|
||||
}}
|
||||
className="w-full sm:w-auto text-sm sm:text-base bg-black text-white hover:bg-gray-800 h-9 sm:h-10"
|
||||
>
|
||||
编辑设备
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -956,31 +1108,30 @@ export default function EquipmentPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 筛选区域 */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-col gap-3 mb-4 sm:mb-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="relative">
|
||||
<Shield className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Shield className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4 z-10" />
|
||||
<Input
|
||||
placeholder="搜索设备编号..."
|
||||
value={equipmentIdSearch}
|
||||
onChange={(e) => setEquipmentIdSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
className="pl-10 h-9 sm:h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4 z-10" />
|
||||
<Input
|
||||
placeholder="搜索设备名称..."
|
||||
value={equipmentNameSearch}
|
||||
onChange={(e) => setEquipmentNameSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
className="pl-10 h-9 sm:h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full sm:w-40">
|
||||
<SelectTrigger className="w-full sm:w-40 h-9 sm:h-10 text-sm">
|
||||
<SelectValue placeholder="筛选状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -990,11 +1141,12 @@ export default function EquipmentPage() {
|
||||
<SelectItem value="3">已到期</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" className="w-full sm:w-auto">
|
||||
<Button variant="outline" className="w-full sm:w-auto h-9 sm:h-10 text-sm">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden md:block rounded-md border overflow-x-auto">
|
||||
@@ -1080,13 +1232,24 @@ export default function EquipmentPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewEquipment(item)}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditEquipment(item)}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
@@ -1097,70 +1260,94 @@ export default function EquipmentPage() {
|
||||
</div>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden space-y-4">
|
||||
<div className="md:hidden space-y-3">
|
||||
{loadingEquipmentList ? (
|
||||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||||
<div className="text-center py-8 text-gray-500 text-sm">加载中...</div>
|
||||
) : filteredEquipment.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">暂无数据</div>
|
||||
<div className="text-center py-8 text-gray-500 text-sm">暂无数据</div>
|
||||
) : (
|
||||
filteredEquipment.map((item) => {
|
||||
const equipmentTypeDisplay = equipmentTypes[item.equipmentType] || item.equipmentType
|
||||
return (
|
||||
<Card key={item.equipmentId}>
|
||||
<CardContent className="p-3 sm:p-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<Card key={item.equipmentId} className="overflow-hidden">
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{/* 设备基本信息 */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center flex-wrap gap-1.5 mb-1">
|
||||
<div className="flex items-center flex-wrap gap-1.5 mb-2">
|
||||
<Shield className="h-4 w-4 text-blue-600 flex-shrink-0" />
|
||||
<span className="text-blue-600 font-medium text-sm truncate">{item.equipmentId}</span>
|
||||
{getStatusBadge(item.status)}
|
||||
<span className="text-blue-600 font-medium text-sm break-all">{item.equipmentId}</span>
|
||||
<div className="flex-shrink-0">{getStatusBadge(item.status)}</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 truncate">{item.equipmentName}</div>
|
||||
<div className="text-xs text-gray-500">类型: {equipmentTypeDisplay}</div>
|
||||
<div className="text-xs text-gray-500">型号: {item.equipmentModel}</div>
|
||||
<div className="text-sm font-medium text-gray-900 break-words mb-1">{item.equipmentName}</div>
|
||||
<div className="text-xs text-gray-500 break-words">类型: {equipmentTypeDisplay}</div>
|
||||
{item.equipmentModel && (
|
||||
<div className="text-xs text-gray-500 break-words">型号: {item.equipmentModel}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewEquipment(item)}
|
||||
className="h-8 px-3 text-xs whitespace-nowrap"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditEquipment(item)}
|
||||
className="flex-shrink-0 h-8 px-2 sm:px-3 text-xs sm:text-sm"
|
||||
className="h-8 px-3 text-xs whitespace-nowrap"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3 space-y-2">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">商户信息</div>
|
||||
<div className="font-medium text-sm">{item.merchantName}</div>
|
||||
<div className="text-xs text-gray-600">{item.mallName}</div>
|
||||
<div className="flex items-center text-xs text-gray-500 mt-1">
|
||||
<MapPin className="h-3 w-3 mr-1" />
|
||||
{item.installationLocation}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">检测信息</div>
|
||||
{/* 详细信息 */}
|
||||
<div className="border-t pt-3 space-y-3">
|
||||
{/* 商户信息 */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs font-medium text-gray-700">商户信息</div>
|
||||
<div className="font-medium text-sm text-gray-900 break-words">{item.merchantName}</div>
|
||||
{item.mallName && (
|
||||
<div className="text-xs text-gray-600 break-words">{item.mallName}</div>
|
||||
)}
|
||||
{item.installationLocation && (
|
||||
<div className="flex items-start gap-1.5 text-xs text-gray-500 mt-1">
|
||||
<MapPin className="h-3.5 w-3.5 text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
<span className="break-words flex-1">{item.installationLocation}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 检测信息 */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs font-medium text-gray-700">检测信息</div>
|
||||
{item.installationDate && (
|
||||
<div className="flex items-center text-xs mb-1">
|
||||
<Calendar className="h-3 w-3 mr-1 text-orange-600" />
|
||||
<span className="text-orange-600">上次检测: {item.installationDate}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<Calendar className="h-3.5 w-3.5 text-orange-600 flex-shrink-0" />
|
||||
<span className="text-orange-600">安装日期: {item.installationDate}</span>
|
||||
</div>
|
||||
)}
|
||||
{item.nextInspectionDate && (
|
||||
<div className="flex items-center text-xs mb-1">
|
||||
<Calendar className="h-3 w-3 mr-1 text-green-600" />
|
||||
<span className="text-green-600">下次检测: {item.nextInspectionDate}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<Calendar className="h-3.5 w-3.5 text-green-600 flex-shrink-0" />
|
||||
<span className={`${item.status === "3" ? "text-red-600" : item.status === "2" ? "text-yellow-600" : "text-green-600"} font-medium`}>
|
||||
下次检测: {item.nextInspectionDate}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500">安装: {item.installationDate}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">负责经销商</div>
|
||||
<div className="font-medium text-sm">{item.createdBy}</div>
|
||||
{/* 负责经销商 */}
|
||||
{item.createdBy && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs font-medium text-gray-700">负责经销商</div>
|
||||
<div className="font-medium text-sm text-gray-900 break-words">{item.createdBy}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1189,37 +1189,42 @@ export default function MallsPage() {
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{loadingEquipments ? (
|
||||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||||
<div className="text-center py-8 text-gray-500 text-sm">加载中...</div>
|
||||
) : merchantEquipments.length > 0 ? (
|
||||
merchantEquipments.map((equipment: any) => (
|
||||
<Card key={equipment.id}>
|
||||
<CardContent className="p-3 space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<Card key={equipment.id} className="overflow-hidden">
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{/* 设备基本信息 */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm mb-1">{equipment.equipmentName}</div>
|
||||
<div className="text-xs text-gray-500 mb-2">编号: {equipment.equipmentId}</div>
|
||||
<div className="font-medium text-sm text-gray-900 mb-1 break-words">{equipment.equipmentName}</div>
|
||||
<div className="text-xs text-gray-500">编号: {equipment.equipmentId}</div>
|
||||
</div>
|
||||
{getEquipmentStatusBadge(equipment.status)}
|
||||
<div className="flex-shrink-0">{getEquipmentStatusBadge(equipment.status)}</div>
|
||||
</div>
|
||||
<div className="border-t pt-2 space-y-1.5 text-xs">
|
||||
<div>
|
||||
<span className="text-gray-500">类型:</span>
|
||||
<span className="ml-1">{equipment.equipmentType}</span>
|
||||
|
||||
{/* 详细信息 - 竖着显示 */}
|
||||
<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>
|
||||
<div>
|
||||
<span className="text-gray-500">安装日期:</span>
|
||||
<span className="ml-1">{equipment.installationDate}</span>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium text-gray-700">安装日期</div>
|
||||
<div className="text-sm text-gray-900">{equipment.installationDate || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">安装位置:</span>
|
||||
<span className="ml-1">{equipment.installationLocation}</span>
|
||||
{equipment.installationLocation && (
|
||||
<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.installationLocation}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">暂无设备数据</div>
|
||||
<div className="text-center py-8 text-gray-500 text-sm">暂无设备数据</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1182,40 +1182,41 @@ export default function MerchantsPage() {
|
||||
|
||||
{/* 设备详情对话框 */}
|
||||
<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>
|
||||
<DialogContent className="w-[95vw] sm:w-[90vw] md:!w-[1400px] md:!max-w-[1400px] max-h-[95vh] overflow-hidden flex flex-col p-3 sm:p-6">
|
||||
<DialogHeader className="flex-shrink-0 pb-2 sm:pb-4">
|
||||
<DialogTitle className="text-base sm:text-lg">{selectedMerchant?.merchantName} - 设备详情</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">查看该商户的所有设备信息和状态</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="flex-1 overflow-y-auto space-y-3 sm:space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="p-3 sm: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>
|
||||
<p className="text-xl sm:text-2xl font-bold text-green-600">{selectedMerchant?.normalCount || 0}</p>
|
||||
<p className="text-xs sm:text-sm text-gray-600">正常设备</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="p-3 sm: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>
|
||||
<p className="text-xl sm:text-2xl font-bold text-yellow-600">{selectedMerchant?.expiringCount || 0}</p>
|
||||
<p className="text-xs sm:text-sm text-gray-600">即将到期</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="p-3 sm: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>
|
||||
<p className="text-xl sm:text-2xl font-bold text-red-600">{selectedMerchant?.expiredCount || 0}</p>
|
||||
<p className="text-xs sm:text-sm text-gray-600">已过期</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
{/* Desktop Table View */}
|
||||
<div className="hidden md:block rounded-md border overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -1269,6 +1270,48 @@ export default function MerchantsPage() {
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{loadingMerchantEquipments[selectedMerchant?.merchantsId || selectedMerchant?.id] ? (
|
||||
<div className="text-center py-8 text-gray-500 text-sm">加载中...</div>
|
||||
) : (merchantEquipments[selectedMerchant?.merchantsId || selectedMerchant?.id] || []).length > 0 ? (
|
||||
(merchantEquipments[selectedMerchant?.merchantsId || selectedMerchant?.id] || []).map((equipment: any) => (
|
||||
<Card key={equipment.id} className="overflow-hidden">
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{/* 设备基本信息 */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-gray-900 mb-1 break-words">{equipment.equipmentName}</div>
|
||||
<div className="text-xs text-gray-500">编号: {equipment.equipmentId}</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">{getStatusBadge(equipment.status)}</div>
|
||||
</div>
|
||||
|
||||
{/* 详细信息 - 竖着显示 */}
|
||||
<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>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium text-gray-700">安装日期</div>
|
||||
<div className="text-sm text-gray-900">{equipment.installationDate || '-'}</div>
|
||||
</div>
|
||||
{equipment.installationLocation && (
|
||||
<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.installationLocation}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500 text-sm">暂无设备数据</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user