This commit is contained in:
menxipeng
2025-11-20 23:28:07 +08:00
parent bf26caee84
commit 7f095044cb
4 changed files with 887 additions and 137 deletions

View File

@@ -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>
<TableCell>
<div className="flex space-x-2">
<Button variant="outline" size="sm">
<Eye className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Edit className="h-4 w-4" />
</Button>
</div>
</TableCell>
{isAdmin && (
<TableCell>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => handleViewUser(user)}
title="查看"
>
<Eye className="h-4 w-4" />
</Button>
<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,21 +389,23 @@ export default function CompanyPermissionsPage() {
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="text-gray-600"></p>
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<CreateUserForm roles={roles} onClose={() => setIsCreateDialogOpen(false)} />
</DialogContent>
</Dialog>
{isAdmin && (
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<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>
)
}

View File

@@ -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,44 +1108,44 @@ 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>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="筛选状态" />
</SelectTrigger>
<SelectContent>
<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>
<div className="flex flex-col sm:flex-row gap-3">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-40 h-9 sm:h-10 text-sm">
<SelectValue placeholder="筛选状态" />
</SelectTrigger>
<SelectContent>
<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 h-9 sm:h-10 text-sm">
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* Desktop Table View */}
@@ -1080,13 +1232,24 @@ export default function EquipmentPage() {
</div>
</TableCell>
<TableCell className="text-center">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditEquipment(item)}
>
</Button>
<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="h-8 px-3 text-xs whitespace-nowrap"
>
</Button>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleEditEquipment(item)}
className="flex-shrink-0 h-8 px-2 sm:px-3 text-xs sm:text-sm"
>
</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 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>
<div className="text-xs text-gray-500 mb-1"></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>
</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>

View File

@@ -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>
<div>
<span className="text-gray-500">:</span>
<span className="ml-1">{equipment.installationLocation}</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>
{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>

View File

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