生产管理系统前端 开发中心配置系统 所有页面
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Employee, UserStatus } from '../types';
|
||||
|
||||
interface EmployeeDetailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedEmployee: Employee | null;
|
||||
}
|
||||
|
||||
export function EmployeeDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedEmployee
|
||||
}: EmployeeDetailDialogProps) {
|
||||
const getStatusBadge = (status: UserStatus) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
||||
case 'frozen':
|
||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
||||
case 'inactive':
|
||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedEmployee) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>员工详情</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
查看员工的详细信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>姓名</Label>
|
||||
<div className="mt-1">{selectedEmployee.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<div className="mt-1">{selectedEmployee.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>电话</Label>
|
||||
<div className="mt-1">{selectedEmployee.phone}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱</Label>
|
||||
<div className="mt-1">{selectedEmployee.email || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>部门</Label>
|
||||
<div className="mt-1">{selectedEmployee.department || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>职位</Label>
|
||||
<div className="mt-1">{selectedEmployee.position || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>状态</Label>
|
||||
<div className="mt-1">{getStatusBadge(selectedEmployee.status)}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>角色</Label>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
{selectedEmployee.roles && selectedEmployee.roles.length > 0 ? (
|
||||
selectedEmployee.roles.map((role, index) => (
|
||||
<Badge key={index} className="bg-purple-100 text-purple-700">
|
||||
{role}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">未分配角色</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedEmployee.lastLoginTime && (
|
||||
<div>
|
||||
<Label>最后登录</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedEmployee.lastLoginTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>创建时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedEmployee.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>更新时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedEmployee.updatedAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Employee, Role, EmployeeFormData } from '../types';
|
||||
|
||||
interface EmployeeFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingEmployee: Employee | null;
|
||||
formData: EmployeeFormData;
|
||||
onFormDataChange: (data: EmployeeFormData) => void;
|
||||
onSave: () => void;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
export function EmployeeFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
editingEmployee,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
onSave,
|
||||
roles
|
||||
}: EmployeeFormDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingEmployee ? '编辑员工' : '添加员工'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{editingEmployee ? '编辑员工信息' : '添加新员工'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">用户名 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="name">姓名 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
||||
placeholder="真实姓名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">电话 *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
|
||||
placeholder="手机号码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
|
||||
placeholder="电子邮箱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="department">部门</Label>
|
||||
<Input
|
||||
id="department"
|
||||
value={formData.department || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, department: e.target.value })}
|
||||
placeholder="所属部门"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="position">职位</Label>
|
||||
<Input
|
||||
id="position"
|
||||
value={formData.position || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, position: e.target.value })}
|
||||
placeholder="职位名称"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 角色选择 */}
|
||||
<div>
|
||||
<Label>角色配置 *</Label>
|
||||
<Card className="mt-2 p-4 bg-gray-50">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{roles
|
||||
.filter(role => role.status === 'active')
|
||||
.map((role) => (
|
||||
<div key={role.id} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={`role-${role.id}`}
|
||||
checked={formData.roleIds?.includes(role.id) || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
roleIds: [...(formData.roleIds || []), role.id],
|
||||
});
|
||||
} else {
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
roleIds: (formData.roleIds || []).filter(id => id !== role.id),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`role-${role.id}`} className="cursor-pointer text-foreground">
|
||||
{role.name}
|
||||
</Label>
|
||||
{role.description && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{role.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{roles.filter(r => r.status === 'active').length === 0 && (
|
||||
<p className="text-sm text-muted-foreground text-center py-2">
|
||||
暂无可用角色,请先在角色管理中创建角色
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onSave}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Eye, Edit, Lock, Trash2, UserX, UserCheck } from 'lucide-react';
|
||||
import { Employee, UserStatus } from '../types';
|
||||
|
||||
interface EmployeeListProps {
|
||||
employees: Employee[];
|
||||
onViewDetail: (employee: Employee) => void;
|
||||
onEdit: (employee: Employee) => void;
|
||||
onResetPassword: (employee: Employee) => void;
|
||||
onToggleStatus: (employee: Employee) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export function EmployeeList({
|
||||
employees,
|
||||
onViewDetail,
|
||||
onEdit,
|
||||
onResetPassword,
|
||||
onToggleStatus,
|
||||
onDelete
|
||||
}: EmployeeListProps) {
|
||||
const getStatusBadge = (status: UserStatus) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
||||
case 'frozen':
|
||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
||||
case 'inactive':
|
||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>电话</TableHead>
|
||||
<TableHead>部门</TableHead>
|
||||
<TableHead>职位</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{employees.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
employees.map((employee) => (
|
||||
<TableRow key={employee.id}>
|
||||
<TableCell>{employee.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{employee.username}</TableCell>
|
||||
<TableCell>{employee.phone}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{employee.department || '-'}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{employee.position || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{employee.roles && employee.roles.length > 0
|
||||
? employee.roles.join(', ')
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(employee.status)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(employee)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(employee)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onResetPassword(employee)}
|
||||
>
|
||||
<Lock className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleStatus(employee)}
|
||||
>
|
||||
{employee.status === 'active' ? (
|
||||
<UserX className="w-4 h-4 text-orange-600" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4 text-green-600" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(employee.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Search } from 'lucide-react';
|
||||
import { EmployeeFilters } from '../types';
|
||||
|
||||
interface EmployeeManagementFiltersProps {
|
||||
filters: EmployeeFilters;
|
||||
onFiltersChange: (filters: EmployeeFilters) => void;
|
||||
}
|
||||
|
||||
export function EmployeeManagementFilters({
|
||||
filters,
|
||||
onFiltersChange
|
||||
}: EmployeeManagementFiltersProps) {
|
||||
const updateFilter = (key: keyof EmployeeFilters, value: string) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索员工姓名、用户名、电话、部门..."
|
||||
value={filters.searchKeyword}
|
||||
onChange={(e) => updateFilter('searchKeyword', e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={filters.statusFilter} onValueChange={(value) => updateFilter('statusFilter', value)}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="active">正常</SelectItem>
|
||||
<SelectItem value="frozen">已冻结</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
interface EmployeeManagementHeaderProps {
|
||||
onAddEmployee: () => void;
|
||||
}
|
||||
|
||||
export function EmployeeManagementHeader({ onAddEmployee }: EmployeeManagementHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">员工管理</h2>
|
||||
<p className="text-muted-foreground">管理本企业员工账户体系</p>
|
||||
</div>
|
||||
<Button onClick={onAddEmployee}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加员工
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { EmployeeManagementStats, Employee } from '../types';
|
||||
|
||||
interface EmployeeManagementStatsCardsProps {
|
||||
employees: Employee[];
|
||||
}
|
||||
|
||||
export function EmployeeManagementStatsCards({ employees }: EmployeeManagementStatsCardsProps) {
|
||||
const stats: EmployeeManagementStats[] = [
|
||||
{
|
||||
label: '总员工数',
|
||||
value: employees.length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '正常员工',
|
||||
value: employees.filter(e => e.status === 'active').length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
{
|
||||
label: '冻结员工',
|
||||
value: employees.filter(e => e.status === 'frozen').length,
|
||||
color: 'text-gray-600',
|
||||
bg: 'bg-gray-100',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
259
crop-x/src/app/(app)/central-config/user/employee/page.tsx
Normal file
259
crop-x/src/app/(app)/central-config/user/employee/page.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { EmployeeManagementHeader } from './components/EmployeeManagementHeader';
|
||||
import { EmployeeManagementStatsCards } from './components/EmployeeManagementStatsCards';
|
||||
import { EmployeeManagementFilters } from './components/EmployeeManagementFilters';
|
||||
import { EmployeeList } from './components/EmployeeList';
|
||||
import { EmployeeFormDialog } from './components/EmployeeFormDialog';
|
||||
import { EmployeeDetailDialog } from './components/EmployeeDetailDialog';
|
||||
import { Employee, Role, EmployeeFilters, EmployeeFormData } from './types';
|
||||
|
||||
export default function EmployeeManagementPage() {
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [filters, setFilters] = useState<EmployeeFilters>({
|
||||
searchKeyword: '',
|
||||
statusFilter: 'all'
|
||||
});
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
|
||||
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
|
||||
const [formData, setFormData] = useState<EmployeeFormData>({
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
status: 'active',
|
||||
roleIds: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadEmployees();
|
||||
loadRoles();
|
||||
}, []);
|
||||
|
||||
const loadRoles = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_roles');
|
||||
if (data) {
|
||||
setRoles(JSON.parse(data));
|
||||
}
|
||||
};
|
||||
|
||||
const loadEmployees = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_employees');
|
||||
if (data) {
|
||||
setEmployees(JSON.parse(data));
|
||||
} else {
|
||||
// 初始化示例数据
|
||||
const mockEmployees: Employee[] = [
|
||||
{
|
||||
id: 'emp-1',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
username: 'zhangsan',
|
||||
name: '张三',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@example.com',
|
||||
department: '技术部',
|
||||
position: '农机操作员',
|
||||
roleIds: ['role-3'],
|
||||
roles: ['操作员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-01T08:00:00',
|
||||
updatedAt: '2024-10-01T08:00:00',
|
||||
lastLoginTime: '2024-10-14T09:30:00',
|
||||
},
|
||||
{
|
||||
id: 'emp-2',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
username: 'lisi',
|
||||
name: '李四',
|
||||
phone: '13900139002',
|
||||
email: 'lisi@example.com',
|
||||
department: '管理部',
|
||||
position: '部门主管',
|
||||
roleIds: ['role-2'],
|
||||
roles: ['企业管理员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-02T10:00:00',
|
||||
updatedAt: '2024-10-02T10:00:00',
|
||||
lastLoginTime: '2024-10-14T08:15:00',
|
||||
},
|
||||
{
|
||||
id: 'emp-3',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
username: 'wangwu',
|
||||
name: '王五',
|
||||
phone: '13700137003',
|
||||
department: '维修部',
|
||||
position: '维修技师',
|
||||
roleIds: ['role-3'],
|
||||
roles: ['操作员'],
|
||||
status: 'frozen',
|
||||
createdAt: '2024-09-28T14:00:00',
|
||||
updatedAt: '2024-10-10T16:00:00',
|
||||
},
|
||||
];
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(mockEmployees));
|
||||
setEmployees(mockEmployees);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEmployees = employees.filter(emp => {
|
||||
const matchKeyword = !filters.searchKeyword ||
|
||||
emp.name.includes(filters.searchKeyword) ||
|
||||
emp.username.includes(filters.searchKeyword) ||
|
||||
emp.phone.includes(filters.searchKeyword) ||
|
||||
(emp.department && emp.department.includes(filters.searchKeyword));
|
||||
|
||||
const matchStatus = filters.statusFilter === 'all' || emp.status === filters.statusFilter;
|
||||
|
||||
return matchKeyword && matchStatus;
|
||||
});
|
||||
|
||||
const handleAddEmployee = () => {
|
||||
setEditingEmployee(null);
|
||||
setFormData({
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
status: 'active',
|
||||
roleIds: [],
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEdit = (employee: Employee) => {
|
||||
setEditingEmployee(employee);
|
||||
setFormData(employee);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.username || !formData.name || !formData.phone) {
|
||||
toast.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证角色选择
|
||||
if (!formData.roleIds || formData.roleIds.length === 0) {
|
||||
toast.error('请至少选择一个角色');
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据角色ID设置角色名称
|
||||
const selectedRoles = roles.filter(r => formData.roleIds?.includes(r.id));
|
||||
const roleNames = selectedRoles.map(r => r.name);
|
||||
|
||||
if (editingEmployee) {
|
||||
// 更新
|
||||
const updated = employees.map(emp =>
|
||||
emp.id === editingEmployee.id
|
||||
? {
|
||||
...emp,
|
||||
...formData,
|
||||
roles: roleNames,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: emp
|
||||
);
|
||||
setEmployees(updated);
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
|
||||
toast.success('员工信息更新成功');
|
||||
} else {
|
||||
// 新增
|
||||
const newEmployee: Employee = {
|
||||
id: `emp-${Date.now()}`,
|
||||
...formData as Employee,
|
||||
roles: roleNames,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...employees, newEmployee];
|
||||
setEmployees(updated);
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
|
||||
toast.success('员工添加成功');
|
||||
}
|
||||
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (!confirm('确定要删除该员工吗?')) return;
|
||||
|
||||
const updated = employees.filter(emp => emp.id !== id);
|
||||
setEmployees(updated);
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
|
||||
toast.success('员工删除成功');
|
||||
};
|
||||
|
||||
const handleToggleStatus = (employee: Employee) => {
|
||||
const newStatus = employee.status === 'active' ? 'frozen' : 'active';
|
||||
const updated = employees.map(emp =>
|
||||
emp.id === employee.id
|
||||
? { ...emp, status: newStatus, updatedAt: new Date().toISOString() }
|
||||
: emp
|
||||
);
|
||||
setEmployees(updated);
|
||||
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
|
||||
toast.success(newStatus === 'active' ? '账户已激活' : '账户已冻结');
|
||||
};
|
||||
|
||||
const handleResetPassword = (employee: Employee) => {
|
||||
if (!confirm(`确定要重置 ${employee.name} 的密码吗?`)) return;
|
||||
toast.success('密码已重置为:123456');
|
||||
};
|
||||
|
||||
const handleViewDetail = (employee: Employee) => {
|
||||
setSelectedEmployee(employee);
|
||||
setShowDetailDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<EmployeeManagementHeader
|
||||
onAddEmployee={handleAddEmployee}
|
||||
/>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<EmployeeManagementStatsCards employees={employees} />
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<EmployeeManagementFilters
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
|
||||
{/* 员工列表 */}
|
||||
<EmployeeList
|
||||
employees={filteredEmployees}
|
||||
onViewDetail={handleViewDetail}
|
||||
onEdit={handleEdit}
|
||||
onResetPassword={handleResetPassword}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
{/* 添加/编辑表单 */}
|
||||
<EmployeeFormDialog
|
||||
open={showForm}
|
||||
onOpenChange={setShowForm}
|
||||
editingEmployee={editingEmployee}
|
||||
formData={formData}
|
||||
onFormDataChange={setFormData}
|
||||
onSave={handleSave}
|
||||
roles={roles}
|
||||
/>
|
||||
|
||||
{/* 详情对话框 */}
|
||||
<EmployeeDetailDialog
|
||||
open={showDetailDialog}
|
||||
onOpenChange={setShowDetailDialog}
|
||||
selectedEmployee={selectedEmployee}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
crop-x/src/app/(app)/central-config/user/employee/types.ts
Normal file
65
crop-x/src/app/(app)/central-config/user/employee/types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// 员工管理相关类型定义
|
||||
|
||||
export interface Employee {
|
||||
id: string;
|
||||
enterpriseId: string;
|
||||
enterpriseName: string;
|
||||
username: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
department?: string;
|
||||
position?: string;
|
||||
roleIds: string[];
|
||||
roles?: string[];
|
||||
status: UserStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginTime?: string;
|
||||
}
|
||||
|
||||
export type UserStatus = 'active' | 'inactive' | 'frozen';
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
type: RoleType;
|
||||
menuIds: string[];
|
||||
permissionIds: string[];
|
||||
defaultHomePage?: string;
|
||||
status: 'active' | 'inactive';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type RoleType = 'system' | 'custom';
|
||||
|
||||
// 统计数据
|
||||
export interface EmployeeManagementStats {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
export interface EmployeeFilters {
|
||||
searchKeyword: string;
|
||||
statusFilter: string;
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
export interface EmployeeFormData {
|
||||
username?: string;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
department?: string;
|
||||
position?: string;
|
||||
enterpriseId?: string;
|
||||
enterpriseName?: string;
|
||||
status?: UserStatus;
|
||||
roleIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Menu, MenuType } from '../types';
|
||||
|
||||
interface MenuFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingMenu: Menu | null;
|
||||
parentMenu: Menu | null;
|
||||
formData: Partial<Menu>;
|
||||
onFormDataChange: (data: Partial<Menu>) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export function MenuFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
editingMenu,
|
||||
parentMenu,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
onSave
|
||||
}: MenuFormDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingMenu ? '编辑菜单' : parentMenu ? `添加子菜单(父级:${parentMenu.name})` : '添加一级菜单'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{editingMenu ? '编辑菜单信息' : '添加新菜单'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">菜单名称 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
||||
placeholder="请输入菜单名称"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="code">菜单编码 *</Label>
|
||||
<Input
|
||||
id="code"
|
||||
value={formData.code || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, code: e.target.value })}
|
||||
placeholder="请输入菜单编码,如:system:menu:add"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="type">菜单类型 *</Label>
|
||||
<Select
|
||||
value={formData.type}
|
||||
onValueChange={(value: MenuType) => onFormDataChange({ ...formData, type: value })}
|
||||
>
|
||||
<SelectTrigger id="type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="directory">目录</SelectItem>
|
||||
<SelectItem value="menu">菜单</SelectItem>
|
||||
<SelectItem value="button">按钮</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
目录:可包含子菜单的分组 / 菜单:具体页面 / 按钮:页面内的操作权限
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="icon">图标名称</Label>
|
||||
<Input
|
||||
id="icon"
|
||||
value={formData.icon || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, icon: e.target.value })}
|
||||
placeholder="如:Settings, Users"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="path">路由路径 {formData.type === 'menu' ? '*' : ''}</Label>
|
||||
<Input
|
||||
id="path"
|
||||
value={formData.path || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, path: e.target.value })}
|
||||
placeholder="/config/user/menu"
|
||||
/>
|
||||
{formData.type === 'menu' && (
|
||||
<p className="text-xs text-red-500 mt-1">菜单类型必须填写路由路径</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="component">组件路径</Label>
|
||||
<Input
|
||||
id="component"
|
||||
value={formData.component || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, component: e.target.value })}
|
||||
placeholder="@/views/config/MenuManagement"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="sort">排序号 *</Label>
|
||||
<Input
|
||||
id="sort"
|
||||
type="number"
|
||||
value={formData.sort || 0}
|
||||
onChange={(e) => onFormDataChange({ ...formData, sort: parseInt(e.target.value) || 0 })}
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="status">状态 *</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(value: 'active' | 'inactive') => onFormDataChange({ ...formData, status: value })}
|
||||
>
|
||||
<SelectTrigger id="status">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">启用</SelectItem>
|
||||
<SelectItem value="inactive">停用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg bg-gray-50">
|
||||
<div>
|
||||
<Label>是否可见</Label>
|
||||
<p className="text-xs text-gray-500 mt-1">控制菜单是否在导航中显示</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={formData.visible}
|
||||
onCheckedChange={(checked) => onFormDataChange({ ...formData, visible: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onSave} className="bg-green-600 hover:bg-green-700">
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Menu } from '../types';
|
||||
|
||||
interface MenuManagementHeaderProps {
|
||||
onAddMenu: () => void;
|
||||
}
|
||||
|
||||
export function MenuManagementHeader({ onAddMenu }: MenuManagementHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">菜单管理</h2>
|
||||
<p className="text-muted-foreground">树形结构管理系统菜单体系(最多支持三级菜单)</p>
|
||||
</div>
|
||||
<Button onClick={onAddMenu} className="bg-green-600 hover:bg-green-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加一级菜单
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function MenuManagementInstructions() {
|
||||
return (
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">菜单管理功能说明</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 支持三级菜单树形结构管理(一级目录 → 二级目录 → 三级菜单)</li>
|
||||
<li>• 目录类型可以包含子菜单,菜单类型为具体页面,按钮类型为页面内的操作权限</li>
|
||||
<li>• 菜单编码用于权限控制,建议使用冒号分隔的层级结构,如:system:user:add</li>
|
||||
<li>• 路由路径对应前端路由配置,菜单类型必须填写路径</li>
|
||||
<li>• 排序号越小越靠前,可以通过调整排序号改变菜单顺序</li>
|
||||
<li>• 菜单与角色权限关联,实现灵活的访问控制</li>
|
||||
<li>• 删除菜单前请先删除其所有子菜单</li>
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Menu } from '../types';
|
||||
|
||||
interface MenuManagementStatsCardsProps {
|
||||
menus: Menu[];
|
||||
}
|
||||
|
||||
export function MenuManagementStatsCards({ menus }: MenuManagementStatsCardsProps) {
|
||||
// 统计菜单数量
|
||||
const countMenus = (menus: Menu[]): { level1: number; level2: number; level3: number } => {
|
||||
let level1 = 0;
|
||||
let level2 = 0;
|
||||
let level3 = 0;
|
||||
|
||||
menus.forEach(menu => {
|
||||
if (!menu.parentId) {
|
||||
level1++;
|
||||
if (menu.children) {
|
||||
menu.children.forEach(child => {
|
||||
level2++;
|
||||
if (child.children) {
|
||||
level3 += child.children.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { level1, level2, level3 };
|
||||
};
|
||||
|
||||
const stats = countMenus(menus);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-muted-foreground">一级菜单</div>
|
||||
<div className="mt-2 text-blue-600">{stats.level1}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-muted-foreground">二级菜单</div>
|
||||
<div className="mt-2 text-green-600">{stats.level2}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-muted-foreground">三级菜单</div>
|
||||
<div className="mt-2 text-purple-600">{stats.level3}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="text-sm text-muted-foreground">菜单总数</div>
|
||||
<div className="mt-2 text-orange-600">{stats.level1 + stats.level2 + stats.level3}</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Menu as MenuIcon,
|
||||
Folder,
|
||||
FileCode,
|
||||
FileText,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { Menu, MenuType } from '../types';
|
||||
|
||||
interface MenuTreeProps {
|
||||
menus: Menu[];
|
||||
expandedIds: Set<string>;
|
||||
onToggleExpand: (id: string) => void;
|
||||
onExpandAll: () => void;
|
||||
onCollapseAll: () => void;
|
||||
onAdd: (parent?: Menu) => void;
|
||||
onEdit: (menu: Menu) => void;
|
||||
onDelete: (menu: Menu) => void;
|
||||
}
|
||||
|
||||
export function MenuTree({
|
||||
menus,
|
||||
expandedIds,
|
||||
onToggleExpand,
|
||||
onExpandAll,
|
||||
onCollapseAll,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: MenuTreeProps) {
|
||||
const getMenuIcon = (type: MenuType) => {
|
||||
switch (type) {
|
||||
case 'directory':
|
||||
return Folder;
|
||||
case 'menu':
|
||||
return FileCode;
|
||||
case 'button':
|
||||
return FileText;
|
||||
default:
|
||||
return MenuIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: MenuType) => {
|
||||
switch (type) {
|
||||
case 'directory':
|
||||
return <Badge className="bg-blue-100 text-blue-700">目录</Badge>;
|
||||
case 'menu':
|
||||
return <Badge className="bg-green-100 text-green-700">菜单</Badge>;
|
||||
case 'button':
|
||||
return <Badge className="bg-purple-100 text-purple-700">按钮</Badge>;
|
||||
default:
|
||||
return <Badge>未知</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
return status === 'active' ? (
|
||||
<Badge className="bg-green-100 text-green-700">启用</Badge>
|
||||
) : (
|
||||
<Badge className="bg-gray-100 text-gray-700">停用</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染菜单树
|
||||
const renderMenuTree = (items: Menu[], level: number = 0) => {
|
||||
return items.map((menu) => {
|
||||
const isExpanded = expandedIds.has(menu.id);
|
||||
const hasChildren = menu.children && menu.children.length > 0;
|
||||
const Icon = getMenuIcon(menu.type);
|
||||
const indent = level * 24;
|
||||
|
||||
return (
|
||||
<div key={menu.id}>
|
||||
<div
|
||||
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 mb-2"
|
||||
style={{ marginLeft: `${indent}px` }}
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
{/* 展开/收起图标 */}
|
||||
<div className="w-5 flex items-center justify-center">
|
||||
{hasChildren ? (
|
||||
<button
|
||||
onClick={() => onToggleExpand(menu.id)}
|
||||
className="hover:bg-gray-200 rounded p-0.5"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-600" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-600" />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-4" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 菜单图标 */}
|
||||
<Icon className="w-5 h-5 text-green-600" />
|
||||
|
||||
{/* 菜单信息 */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{menu.name}</span>
|
||||
<span className="text-xs text-gray-500">({menu.code})</span>
|
||||
</div>
|
||||
{menu.path && (
|
||||
<div className="text-xs text-gray-500 mt-0.5">{menu.path}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 状态标签 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{getTypeBadge(menu.type)}
|
||||
{getStatusBadge(menu.status)}
|
||||
{menu.visible ? (
|
||||
<Eye className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<EyeOff className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
<span className="text-xs text-gray-500">排序: {menu.sort}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
{/* 只有目录类型可以添加子菜单,且限制最多三级 */}
|
||||
{menu.type === 'directory' && level < 2 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onAdd(menu)}
|
||||
title="添加子菜单"
|
||||
>
|
||||
<Plus className="w-4 h-4 text-green-600" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(menu)}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className="w-4 h-4 text-blue-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(menu)}
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 递归渲染子菜单 */}
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="mt-2">
|
||||
{renderMenuTree(menu.children!, level + 1)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3>菜单结构</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onExpandAll}
|
||||
>
|
||||
展开全部
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCollapseAll}
|
||||
>
|
||||
收起全部
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{menus.length > 0 ? (
|
||||
renderMenuTree(menus)
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无菜单数据,点击上方按钮添加一级菜单
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
463
crop-x/src/app/(app)/central-config/user/menu/page.tsx
Normal file
463
crop-x/src/app/(app)/central-config/user/menu/page.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { MenuManagementHeader } from './components/MenuManagementHeader';
|
||||
import { MenuManagementStatsCards } from './components/MenuManagementStatsCards';
|
||||
import { MenuTree } from './components/MenuTree';
|
||||
import { MenuFormDialog } from './components/MenuFormDialog';
|
||||
import { MenuManagementInstructions } from './components/MenuManagementInstructions';
|
||||
import { Menu, MenuType } from './types';
|
||||
|
||||
/**
|
||||
* 菜单管理页面组件
|
||||
* 提供菜单的增删改查、展开折叠等功能
|
||||
*/
|
||||
export default function MenuManagementPage() {
|
||||
// 菜单列表状态
|
||||
const [menus, setMenus] = useState<Menu[]>([]);
|
||||
// 展开的菜单ID集合
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
// 控制表单显示状态
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
// 当前编辑的菜单
|
||||
const [editingMenu, setEditingMenu] = useState<Menu | null>(null);
|
||||
// 父级菜单
|
||||
const [parentMenu, setParentMenu] = useState<Menu | null>(null);
|
||||
// 表单数据
|
||||
const [formData, setFormData] = useState<Partial<Menu>>({
|
||||
type: 'menu',
|
||||
visible: true,
|
||||
status: 'active',
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
// 组件挂载时加载菜单数据
|
||||
useEffect(() => {
|
||||
loadMenus();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 加载菜单数据
|
||||
* 优先从localStorage读取,若不存在则使用示例数据
|
||||
*/
|
||||
const loadMenus = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_menus');
|
||||
if (data) {
|
||||
setMenus(JSON.parse(data));
|
||||
} else {
|
||||
// 初始化示例数据 - 三级菜单结构
|
||||
const mockMenus: Menu[] = [
|
||||
{
|
||||
id: 'menu-1',
|
||||
name: '智能农机管理系统',
|
||||
code: 'machinery',
|
||||
icon: 'Tractor',
|
||||
type: 'directory',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
children: [
|
||||
{
|
||||
id: 'menu-1-1',
|
||||
parentId: 'menu-1',
|
||||
name: '农机档案',
|
||||
code: 'machinery:archive',
|
||||
icon: 'Archive',
|
||||
type: 'directory',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
children: [
|
||||
{
|
||||
id: 'menu-1-1-1',
|
||||
parentId: 'menu-1-1',
|
||||
name: '农机录入',
|
||||
code: 'machinery:archive:entry',
|
||||
path: '/machinery/archive/entry',
|
||||
type: 'menu',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-1-1-2',
|
||||
parentId: 'menu-1-1',
|
||||
name: '农机分类',
|
||||
code: 'machinery:archive:classification',
|
||||
path: '/machinery/archive/classification',
|
||||
type: 'menu',
|
||||
sort: 2,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-1-1-3',
|
||||
parentId: 'menu-1-1',
|
||||
name: '二维码管理',
|
||||
code: 'machinery:archive:qrcode',
|
||||
path: '/machinery/archive/qrcode',
|
||||
type: 'menu',
|
||||
sort: 3,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'menu-1-2',
|
||||
parentId: 'menu-1',
|
||||
name: '驾驶员管理',
|
||||
code: 'machinery:driver',
|
||||
icon: 'User',
|
||||
type: 'directory',
|
||||
sort: 2,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
children: [
|
||||
{
|
||||
id: 'menu-1-2-1',
|
||||
parentId: 'menu-1-2',
|
||||
name: '驾驶员信息',
|
||||
code: 'machinery:driver:info',
|
||||
path: '/machinery/driver/info',
|
||||
type: 'menu',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-1-2-2',
|
||||
parentId: 'menu-1-2',
|
||||
name: '任务管理',
|
||||
code: 'machinery:driver:task',
|
||||
path: '/machinery/driver/task',
|
||||
type: 'menu',
|
||||
sort: 2,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'menu-2',
|
||||
name: '中心配置管理系统',
|
||||
code: 'config',
|
||||
icon: 'Settings',
|
||||
type: 'directory',
|
||||
sort: 7,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
children: [
|
||||
{
|
||||
id: 'menu-2-1',
|
||||
parentId: 'menu-2',
|
||||
name: '租户管理',
|
||||
code: 'config:tenant',
|
||||
icon: 'Building2',
|
||||
type: 'directory',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
children: [
|
||||
{
|
||||
id: 'menu-2-1-1',
|
||||
parentId: 'menu-2-1',
|
||||
name: '企业信息',
|
||||
code: 'config:tenant:enterprise',
|
||||
path: '/config/tenant/enterprise',
|
||||
type: 'menu',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-2-1-2',
|
||||
parentId: 'menu-2-1',
|
||||
name: '企业审核',
|
||||
code: 'config:tenant:audit',
|
||||
path: '/config/tenant/audit',
|
||||
type: 'menu',
|
||||
sort: 2,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'menu-2-2',
|
||||
parentId: 'menu-2',
|
||||
name: '用户管理',
|
||||
code: 'config:user',
|
||||
icon: 'Users',
|
||||
type: 'directory',
|
||||
sort: 2,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
children: [
|
||||
{
|
||||
id: 'menu-2-2-1',
|
||||
parentId: 'menu-2-2',
|
||||
name: '员工管理',
|
||||
code: 'config:user:employee',
|
||||
path: '/config/user/employee',
|
||||
type: 'menu',
|
||||
sort: 1,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-2-2-2',
|
||||
parentId: 'menu-2-2',
|
||||
name: '角色管理',
|
||||
code: 'config:user:role',
|
||||
path: '/config/user/role',
|
||||
type: 'menu',
|
||||
sort: 2,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-2-2-3',
|
||||
parentId: 'menu-2-2',
|
||||
name: '菜单管理',
|
||||
code: 'config:user:menu',
|
||||
path: '/config/user/menu',
|
||||
type: 'menu',
|
||||
sort: 3,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'menu-2-2-4',
|
||||
parentId: 'menu-2-2',
|
||||
name: '权限管理',
|
||||
code: 'config:user:permission',
|
||||
path: '/config/user/permission',
|
||||
type: 'menu',
|
||||
sort: 4,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
localStorage.setItem('smart_agriculture_menus', JSON.stringify(mockMenus));
|
||||
setMenus(mockMenus);
|
||||
// 默认展开所有一级菜单
|
||||
setExpandedIds(new Set(mockMenus.map(m => m.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
const newExpanded = new Set(expandedIds);
|
||||
if (newExpanded.has(id)) {
|
||||
newExpanded.delete(id);
|
||||
} else {
|
||||
newExpanded.add(id);
|
||||
}
|
||||
setExpandedIds(newExpanded);
|
||||
};
|
||||
|
||||
const expandAll = () => {
|
||||
const getAllMenuIds = (menus: Menu[]): string[] => {
|
||||
let ids: string[] = [];
|
||||
menus.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
ids.push(menu.id);
|
||||
ids.push(...getAllMenuIds(menu.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
setExpandedIds(new Set(getAllMenuIds(menus)));
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
setExpandedIds(new Set());
|
||||
};
|
||||
|
||||
const handleAdd = (parent?: Menu) => {
|
||||
setEditingMenu(null);
|
||||
setParentMenu(parent || null);
|
||||
|
||||
// 根据父菜单决定默认类型
|
||||
let defaultType: MenuType = 'menu';
|
||||
if (!parent) {
|
||||
defaultType = 'directory'; // 一级菜单默认为目录
|
||||
} else if (parent.type === 'directory' && !parent.parentId) {
|
||||
defaultType = 'directory'; // 二级菜单默认为目录
|
||||
} else {
|
||||
defaultType = 'menu'; // 三级菜单默认为菜单
|
||||
}
|
||||
|
||||
setFormData({
|
||||
parentId: parent?.id,
|
||||
type: defaultType,
|
||||
visible: true,
|
||||
status: 'active',
|
||||
sort: 0,
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEdit = (menu: Menu) => {
|
||||
setEditingMenu(menu);
|
||||
setParentMenu(null);
|
||||
setFormData({
|
||||
...menu,
|
||||
children: undefined, // 不包含子菜单
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.name || !formData.code) {
|
||||
toast.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证路径:三级菜单必须填写路径
|
||||
if (formData.type === 'menu' && !formData.path) {
|
||||
toast.error('菜单类型必须填写路径');
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingMenu) {
|
||||
// 更新菜单
|
||||
const updateMenuInTree = (items: Menu[]): Menu[] => {
|
||||
return items.map(item => {
|
||||
if (item.id === editingMenu.id) {
|
||||
return {
|
||||
...item,
|
||||
...formData,
|
||||
children: item.children, // 保留子菜单
|
||||
} as Menu;
|
||||
}
|
||||
if (item.children) {
|
||||
return {
|
||||
...item,
|
||||
children: updateMenuInTree(item.children),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const updated = updateMenuInTree(menus);
|
||||
setMenus(updated);
|
||||
localStorage.setItem('smart_agriculture_menus', JSON.stringify(updated));
|
||||
toast.success('菜单更新成功');
|
||||
} else {
|
||||
// 新增菜单
|
||||
const newMenu: Menu = {
|
||||
id: `menu-${Date.now()}`,
|
||||
...formData as Menu,
|
||||
};
|
||||
|
||||
if (parentMenu) {
|
||||
// 添加到父菜单下
|
||||
const addToParent = (items: Menu[]): Menu[] => {
|
||||
return items.map(item => {
|
||||
if (item.id === parentMenu.id) {
|
||||
return {
|
||||
...item,
|
||||
children: [...(item.children || []), newMenu],
|
||||
};
|
||||
}
|
||||
if (item.children) {
|
||||
return {
|
||||
...item,
|
||||
children: addToParent(item.children),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const updated = addToParent(menus);
|
||||
setMenus(updated);
|
||||
localStorage.setItem('smart_agriculture_menus', JSON.stringify(updated));
|
||||
// 自动展开父菜单
|
||||
setExpandedIds(prev => new Set([...prev, parentMenu.id]));
|
||||
} else {
|
||||
// 添加为一级菜单
|
||||
const updated = [...menus, newMenu];
|
||||
setMenus(updated);
|
||||
localStorage.setItem('smart_agriculture_menus', JSON.stringify(updated));
|
||||
}
|
||||
|
||||
toast.success('菜单添加成功');
|
||||
}
|
||||
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleDelete = (menu: Menu) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
toast.error('请先删除该菜单下的子菜单');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除菜单"${menu.name}"吗?`)) return;
|
||||
|
||||
const deleteFromTree = (items: Menu[]): Menu[] => {
|
||||
return items
|
||||
.filter(item => item.id !== menu.id)
|
||||
.map(item => {
|
||||
if (item.children) {
|
||||
return {
|
||||
...item,
|
||||
children: deleteFromTree(item.children),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const updated = deleteFromTree(menus);
|
||||
setMenus(updated);
|
||||
localStorage.setItem('smart_agriculture_menus', JSON.stringify(updated));
|
||||
toast.success('菜单删除成功');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<MenuManagementHeader onAddMenu={() => handleAdd()} />
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<MenuManagementStatsCards menus={menus} />
|
||||
|
||||
{/* 菜单树 */}
|
||||
<MenuTree
|
||||
menus={menus}
|
||||
expandedIds={expandedIds}
|
||||
onToggleExpand={toggleExpand}
|
||||
onExpandAll={expandAll}
|
||||
onCollapseAll={collapseAll}
|
||||
onAdd={handleAdd}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
{/* 添加/编辑表单 */}
|
||||
<MenuFormDialog
|
||||
open={showForm}
|
||||
onOpenChange={setShowForm}
|
||||
editingMenu={editingMenu}
|
||||
parentMenu={parentMenu}
|
||||
formData={formData}
|
||||
onFormDataChange={setFormData}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
{/* 功能说明 */}
|
||||
<MenuManagementInstructions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
crop-x/src/app/(app)/central-config/user/menu/types.ts
Normal file
16
crop-x/src/app/(app)/central-config/user/menu/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface Menu {
|
||||
id: string;
|
||||
parentId?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
icon?: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
type: MenuType;
|
||||
sort: number;
|
||||
visible: boolean;
|
||||
status: 'active' | 'inactive';
|
||||
children?: Menu[];
|
||||
}
|
||||
|
||||
export type MenuType = 'directory' | 'menu' | 'button';
|
||||
30
crop-x/src/app/(app)/central-config/user/page.tsx
Normal file
30
crop-x/src/app/(app)/central-config/user/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function UserPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">用户管理</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Link href="/central-config/user/employee" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">员工管理</h3>
|
||||
<p className="text-gray-600 text-sm">管理员工信息</p>
|
||||
</Link>
|
||||
<Link href="/central-config/user/role" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">角色管理</h3>
|
||||
<p className="text-gray-600 text-sm">管理系统角色</p>
|
||||
</Link>
|
||||
<Link href="/central-config/user/menu" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">菜单管理</h3>
|
||||
<p className="text-gray-600 text-sm">管理系统菜单</p>
|
||||
</Link>
|
||||
<Link href="/central-config/user/permission" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">权限配置</h3>
|
||||
<p className="text-gray-600 text-sm">配置系统权限</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
interface PermissionManagementHeaderProps {
|
||||
onAddPermission: () => void;
|
||||
}
|
||||
|
||||
export function PermissionManagementHeader({ onAddPermission }: PermissionManagementHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">权限配置管理</h2>
|
||||
<p className="text-muted-foreground">精细化管理系统功能权限</p>
|
||||
</div>
|
||||
<Button onClick={onAddPermission} className="bg-green-600 hover:bg-green-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加权限
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
414
crop-x/src/app/(app)/central-config/user/permission/page.tsx
Normal file
414
crop-x/src/app/(app)/central-config/user/permission/page.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Shield,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { PermissionManagementHeader } from './components/PermissionManagementHeader';
|
||||
import { Permission, PermissionType, Menu } from './types';
|
||||
|
||||
export default function PermissionConfigPage() {
|
||||
const [permissions, setPermissions] = useState<Permission[]>([]);
|
||||
const [menus, setMenus] = useState<Menu[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingPermission, setEditingPermission] = useState<Permission | null>(null);
|
||||
const [formData, setFormData] = useState<Partial<Permission>>({
|
||||
type: 'view',
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadPermissions();
|
||||
loadMenus();
|
||||
}, []);
|
||||
|
||||
const loadMenus = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_menus');
|
||||
if (data) {
|
||||
setMenus(JSON.parse(data));
|
||||
}
|
||||
};
|
||||
|
||||
const loadPermissions = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_permissions');
|
||||
if (data) {
|
||||
setPermissions(JSON.parse(data));
|
||||
} else {
|
||||
const mockPermissions: Permission[] = [
|
||||
{
|
||||
id: 'perm-1',
|
||||
name: '查看农机',
|
||||
code: 'machinery:view',
|
||||
type: 'view',
|
||||
description: '查看农机档案和基本信息',
|
||||
menuId: 'menu-1-1-1',
|
||||
menuPath: '智能农机管理系统 > 农机档案 > 农机录入',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
updatedAt: '2024-01-01T00:00:00',
|
||||
},
|
||||
{
|
||||
id: 'perm-2',
|
||||
name: '添加农机',
|
||||
code: 'machinery:add',
|
||||
type: 'add',
|
||||
description: '添加新的农机设备',
|
||||
menuId: 'menu-1-1-1',
|
||||
menuPath: '智能农机管理系统 > 农机档案 > 农机录入',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
updatedAt: '2024-01-01T00:00:00',
|
||||
},
|
||||
];
|
||||
localStorage.setItem('smart_agriculture_permissions', JSON.stringify(mockPermissions));
|
||||
setPermissions(mockPermissions);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredPermissions = permissions.filter((perm) => {
|
||||
const matchKeyword =
|
||||
!searchKeyword ||
|
||||
perm.name.includes(searchKeyword) ||
|
||||
perm.code.includes(searchKeyword) ||
|
||||
(perm.menuPath && perm.menuPath.includes(searchKeyword));
|
||||
|
||||
const matchType = typeFilter === 'all' || perm.type === typeFilter;
|
||||
|
||||
return matchKeyword && matchType;
|
||||
});
|
||||
|
||||
const getPermissionTypeBadge = (type: PermissionType) => {
|
||||
const config: Record<PermissionType, { label: string; className: string }> = {
|
||||
view: { label: '查看', className: 'bg-blue-100 text-blue-700' },
|
||||
add: { label: '新增', className: 'bg-green-100 text-green-700' },
|
||||
edit: { label: '编辑', className: 'bg-yellow-100 text-yellow-700' },
|
||||
delete: { label: '删除', className: 'bg-red-100 text-red-700' },
|
||||
export: { label: '导出', className: 'bg-purple-100 text-purple-700' },
|
||||
import: { label: '导入', className: 'bg-indigo-100 text-indigo-700' },
|
||||
approve: { label: '审核', className: 'bg-orange-100 text-orange-700' },
|
||||
control: { label: '控制', className: 'bg-pink-100 text-pink-700' },
|
||||
};
|
||||
|
||||
const { label, className } = config[type] || { label: type, className: '' };
|
||||
return <Badge className={className}>{label}</Badge>;
|
||||
};
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: '总权限数',
|
||||
value: permissions.length,
|
||||
color: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
label: '查看权限',
|
||||
value: permissions.filter((p) => p.type === 'view').length,
|
||||
color: 'text-green-600',
|
||||
},
|
||||
{
|
||||
label: '操作权限',
|
||||
value: permissions.filter((p) => ['add', 'edit', 'delete'].includes(p.type)).length,
|
||||
color: 'text-orange-600',
|
||||
},
|
||||
{
|
||||
label: '特殊权限',
|
||||
value: permissions.filter((p) => ['control', 'approve', 'export', 'import'].includes(p.type)).length,
|
||||
color: 'text-purple-600',
|
||||
},
|
||||
];
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingPermission(null);
|
||||
setFormData({
|
||||
type: 'view',
|
||||
status: 'active',
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEdit = (permission: Permission) => {
|
||||
setEditingPermission(permission);
|
||||
setFormData(permission);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.name || !formData.code) {
|
||||
toast.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingPermission) {
|
||||
const updated = permissions.map((perm) =>
|
||||
perm.id === editingPermission.id
|
||||
? {
|
||||
...perm,
|
||||
...formData,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: perm,
|
||||
);
|
||||
setPermissions(updated);
|
||||
localStorage.setItem('smart_agriculture_permissions', JSON.stringify(updated));
|
||||
toast.success('权限更新成功');
|
||||
} else {
|
||||
const newPermission: Permission = {
|
||||
id: `perm-${Date.now()}`,
|
||||
...formData as Permission,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...permissions, newPermission];
|
||||
setPermissions(updated);
|
||||
localStorage.setItem('smart_agriculture_permissions', JSON.stringify(updated));
|
||||
toast.success('权限添加成功');
|
||||
}
|
||||
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (!confirm('确定要删除该权限吗?')) return;
|
||||
|
||||
const updated = permissions.filter((perm) => perm.id !== id);
|
||||
setPermissions(updated);
|
||||
localStorage.setItem('smart_agriculture_permissions', JSON.stringify(updated));
|
||||
toast.success('权限删除成功');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PermissionManagementHeader onAddPermission={handleAdd} />
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 ${stat.color}`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<Card className="p-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索权限名称、代码、所属菜单..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="view">查看</SelectItem>
|
||||
<SelectItem value="add">新增</SelectItem>
|
||||
<SelectItem value="edit">编辑</SelectItem>
|
||||
<SelectItem value="delete">删除</SelectItem>
|
||||
<SelectItem value="export">导出</SelectItem>
|
||||
<SelectItem value="import">导入</SelectItem>
|
||||
<SelectItem value="approve">审核</SelectItem>
|
||||
<SelectItem value="control">控制</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 权限列表 */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>权限名称</TableHead>
|
||||
<TableHead>权限代码</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>关联菜单</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPermissions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredPermissions.map((permission) => (
|
||||
<TableRow key={permission.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-muted-foreground" />
|
||||
{permission.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{permission.code}</TableCell>
|
||||
<TableCell>{getPermissionTypeBadge(permission.type)}</TableCell>
|
||||
<TableCell className="text-muted-foreground max-w-xs">
|
||||
{permission.menuPath ? (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{permission.menuPath.split(' > ').map((part, index, arr) => (
|
||||
<span key={index} className="flex items-center gap-1">
|
||||
<span className={index === arr.length - 1 ? 'text-green-600' : ''}>
|
||||
{part}
|
||||
</span>
|
||||
{index < arr.length - 1 && (
|
||||
<ChevronRight className="w-3 h-3 text-gray-400" />
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground max-w-xs truncate">
|
||||
{permission.description || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(permission)}>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDelete(permission.id)}>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* 添加/编辑表单 */}
|
||||
<Dialog open={showForm} onOpenChange={setShowForm}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingPermission ? '编辑权限' : '添加权限'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{editingPermission ? '编辑权限信息' : '添加新权限'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">权限名称 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="如:查看农机"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="code">权限代码 *</Label>
|
||||
<Input
|
||||
id="code"
|
||||
value={formData.code || ''}
|
||||
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
|
||||
placeholder="如:machinery:view"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="type">权限类型 *</Label>
|
||||
<Select value={formData.type || 'view'} onValueChange={(value: PermissionType) => setFormData({ ...formData, type: value })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="view">查看</SelectItem>
|
||||
<SelectItem value="add">新增</SelectItem>
|
||||
<SelectItem value="edit">编辑</SelectItem>
|
||||
<SelectItem value="delete">删除</SelectItem>
|
||||
<SelectItem value="export">导出</SelectItem>
|
||||
<SelectItem value="import">导入</SelectItem>
|
||||
<SelectItem value="approve">审核</SelectItem>
|
||||
<SelectItem value="control">控制</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="description">权限描述</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="描述该权限的作用..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowForm(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-green-600 hover:bg-green-700">
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">权限管理说明</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 权限是系统功能的最小控制单元,精确到按钮级别</li>
|
||||
<li>• 权限代码格式建议:模块:操作,如 machinery:view</li>
|
||||
<li>• 每个权限可以关联到具体的菜单(支持一级、二级、三级菜单)</li>
|
||||
<li>• 权限通过角色分配给用户,实现灵活的访问控制</li>
|
||||
<li>• 支持8种权限类型:查看、新增、编辑、删除、导出、导入、审核、控制</li>
|
||||
<li>• 合理规划权限体系,确保系统安全性和易用性</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
crop-x/src/app/(app)/central-config/user/permission/types.ts
Normal file
37
crop-x/src/app/(app)/central-config/user/permission/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface Permission {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: PermissionType;
|
||||
description?: string;
|
||||
menuId?: string;
|
||||
menuPath?: string;
|
||||
status: 'active' | 'inactive';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type PermissionType =
|
||||
| 'view'
|
||||
| 'add'
|
||||
| 'edit'
|
||||
| 'delete'
|
||||
| 'export'
|
||||
| 'import'
|
||||
| 'approve'
|
||||
| 'control';
|
||||
|
||||
export interface Menu {
|
||||
id: string;
|
||||
parentId?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
icon?: string;
|
||||
path?: string;
|
||||
component?: string;
|
||||
type: 'directory' | 'menu' | 'button';
|
||||
sort: number;
|
||||
visible: boolean;
|
||||
status: 'active' | 'inactive';
|
||||
children?: Menu[];
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Role, RoleType } from '../types';
|
||||
|
||||
interface RoleDetailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedRole: Role | null;
|
||||
}
|
||||
|
||||
export function RoleDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedRole
|
||||
}: RoleDetailDialogProps) {
|
||||
const getRoleTypeBadge = (type: RoleType) => {
|
||||
return type === 'system' ? (
|
||||
<Badge className="bg-blue-100 text-blue-700">系统角色</Badge>
|
||||
) : (
|
||||
<Badge className="bg-green-100 text-green-700">自定义</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
return status === 'active' ? (
|
||||
<Badge className="bg-green-100 text-green-700">启用</Badge>
|
||||
) : (
|
||||
<Badge className="bg-gray-100 text-gray-700">禁用</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
if (!selectedRole) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>角色详情</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
查看角色的详细信息和权限
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>角色名称</Label>
|
||||
<div className="mt-1">{selectedRole.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>角色代码</Label>
|
||||
<div className="mt-1">{selectedRole.code}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>角色描述</Label>
|
||||
<div className="mt-1">{selectedRole.description || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>角色类型</Label>
|
||||
<div className="mt-1">{getRoleTypeBadge(selectedRole.type)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>状态</Label>
|
||||
<div className="mt-1">{getStatusBadge(selectedRole.status)}</div>
|
||||
</div>
|
||||
{selectedRole.defaultHomePage && (
|
||||
<div className="col-span-2">
|
||||
<Label>默认首页</Label>
|
||||
<div className="mt-1">{selectedRole.defaultHomePage}</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>菜单数量</Label>
|
||||
<div className="mt-1">
|
||||
{selectedRole.menuIds.includes('*') ? '全部' : selectedRole.menuIds.length}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>权限数量</Label>
|
||||
<div className="mt-1">
|
||||
{selectedRole.permissionIds.includes('*') ? '全部' : selectedRole.permissionIds.length}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>创建时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedRole.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>更新时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedRole.updatedAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { Role, RoleFormData, allSystemMenus } from '../types';
|
||||
|
||||
interface RoleFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingRole: Role | null;
|
||||
formData: RoleFormData;
|
||||
onFormDataChange: (data: RoleFormData) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export function RoleFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
editingRole,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
onSave
|
||||
}: RoleFormDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingRole ? '编辑角色' : '添加角色'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{editingRole ? '编辑角色信息' : '添加新角色'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[calc(90vh-180px)]">
|
||||
<div className="space-y-6 pr-4">
|
||||
{/* 基本信息 */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-green-800">基本信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">角色名称 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
||||
placeholder="如:数据分析员"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="code">角色代码 *</Label>
|
||||
<Input
|
||||
id="code"
|
||||
value={formData.code || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, code: e.target.value })}
|
||||
placeholder="如:data_analyst"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="description">角色描述</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, description: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="描述角色的职责和权限范围..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="defaultHomePage">默认首页</Label>
|
||||
<Input
|
||||
id="defaultHomePage"
|
||||
value={formData.defaultHomePage || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, defaultHomePage: e.target.value })}
|
||||
placeholder="/machinery/monitoring/location"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 菜单与操作权限 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-green-800">菜单与操作权限</h4>
|
||||
<p className="text-xs text-muted-foreground">选择菜单后可配置该菜单下的操作权限</p>
|
||||
</div>
|
||||
<Card className="p-4 bg-gray-50">
|
||||
<div className="space-y-6">
|
||||
{allSystemMenus.map((system) => (
|
||||
<div key={system.id} className="space-y-3">
|
||||
{/* 系统级别 */}
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-green-200">
|
||||
<Checkbox
|
||||
id={`system-${system.id}`}
|
||||
checked={
|
||||
formData.menuIds?.includes('*') ||
|
||||
(system.menus.every(menu =>
|
||||
formData.menuIds?.includes(menu.id)
|
||||
) && system.menus.length > 0)
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
// 选中所有一级菜单
|
||||
const allMenuIds = system.menus.map(m => m.id);
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
menuIds: Array.from(new Set([...(formData.menuIds || []), ...allMenuIds])),
|
||||
});
|
||||
} else {
|
||||
// 取消选中所有一级菜单
|
||||
const menuIdsToRemove = system.menus.map(m => m.id);
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
menuIds: (formData.menuIds || []).filter(id => !menuIdsToRemove.includes(id)),
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={formData.menuIds?.includes('*')}
|
||||
/>
|
||||
<Label htmlFor={`system-${system.id}`} className="cursor-pointer text-green-700">
|
||||
{system.label}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* 一级菜单 */}
|
||||
<div className="ml-6 space-y-4">
|
||||
{system.menus.map((menu) => (
|
||||
<div key={menu.id} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`menu-${menu.id}`}
|
||||
checked={formData.menuIds?.includes(menu.id) || formData.menuIds?.includes('*')}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
menuIds: [...(formData.menuIds || []), menu.id],
|
||||
});
|
||||
} else {
|
||||
// 取消菜单时,同时取消该菜单下的所有权限
|
||||
const childrenIds = menu.children?.map(c => c.id) || [];
|
||||
const permissionsToRemove = menu.children?.flatMap(c =>
|
||||
c.operations.map(op => op.id)
|
||||
) || [];
|
||||
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
menuIds: (formData.menuIds || []).filter(id =>
|
||||
id !== menu.id && !childrenIds.includes(id)
|
||||
),
|
||||
permissionIds: (formData.permissionIds || []).filter(id =>
|
||||
!permissionsToRemove.includes(id)
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={formData.menuIds?.includes('*')}
|
||||
/>
|
||||
<Label htmlFor={`menu-${menu.id}`} className="cursor-pointer">
|
||||
{menu.label}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* 二级菜单及其操作权限 */}
|
||||
{menu.children && (formData.menuIds?.includes(menu.id) || formData.menuIds?.includes('*')) && (
|
||||
<div className="ml-6 space-y-3 pl-4 border-l-2 border-green-100">
|
||||
{menu.children.map((child) => (
|
||||
<div key={child.id} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronRight className="w-3 h-3 text-green-600" />
|
||||
<span className="text-sm text-muted-foreground">{child.label}</span>
|
||||
</div>
|
||||
<div className="ml-5 flex flex-wrap gap-3">
|
||||
{child.operations.map((operation) => (
|
||||
<div key={operation.id} className="flex items-center gap-1.5">
|
||||
<Checkbox
|
||||
id={`permission-${operation.id}`}
|
||||
checked={
|
||||
formData.permissionIds?.includes(operation.id) ||
|
||||
formData.permissionIds?.includes('*')
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
permissionIds: [...(formData.permissionIds || []), operation.id],
|
||||
});
|
||||
} else {
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
permissionIds: (formData.permissionIds || []).filter(id => id !== operation.id),
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={formData.permissionIds?.includes('*')}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`permission-${operation.id}`}
|
||||
className="text-xs cursor-pointer text-foreground"
|
||||
>
|
||||
{operation.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onSave}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Eye, Edit, Trash2, Shield } from 'lucide-react';
|
||||
import { Role, RoleType } from '../types';
|
||||
|
||||
interface RoleListProps {
|
||||
roles: Role[];
|
||||
onViewDetail: (role: Role) => void;
|
||||
onEdit: (role: Role) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export function RoleList({
|
||||
roles,
|
||||
onViewDetail,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: RoleListProps) {
|
||||
const getRoleTypeBadge = (type: RoleType) => {
|
||||
return type === 'system' ? (
|
||||
<Badge className="bg-blue-100 text-blue-700">系统角色</Badge>
|
||||
) : (
|
||||
<Badge className="bg-green-100 text-green-700">自定义</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
return status === 'active' ? (
|
||||
<Badge className="bg-green-100 text-green-700">启用</Badge>
|
||||
) : (
|
||||
<Badge className="bg-gray-100 text-gray-700">禁用</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>角色名称</TableHead>
|
||||
<TableHead>角色代码</TableHead>
|
||||
<TableHead>角色描述</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{roles.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
roles.map((role) => (
|
||||
<TableRow key={role.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-muted-foreground" />
|
||||
{role.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{role.code}</TableCell>
|
||||
<TableCell className="text-muted-foreground max-w-xs truncate">
|
||||
{role.description}
|
||||
</TableCell>
|
||||
<TableCell>{getRoleTypeBadge(role.type)}</TableCell>
|
||||
<TableCell>{getStatusBadge(role.status)}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(role.createdAt).toLocaleDateString('zh-CN')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(role)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(role)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
{role.type === 'custom' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(role.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
interface RoleManagementHeaderProps {
|
||||
onAddRole: () => void;
|
||||
}
|
||||
|
||||
export function RoleManagementHeader({ onAddRole }: RoleManagementHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">角色管理</h2>
|
||||
<p className="text-muted-foreground">基于RBAC的角色访问控制管理</p>
|
||||
</div>
|
||||
<Button onClick={onAddRole}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加角色
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function RoleManagementInstructions() {
|
||||
return (
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">角色管理说明</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 系统角色是预定义的角色,不能删除,只能修改描述和权限</li>
|
||||
<li>• 自定义角色可根据业务需求灵活创建</li>
|
||||
<li>• 每个角色可配置可访问的菜单和功能权限</li>
|
||||
<li>• 用户通过分配角色获得相应的系统访问权限</li>
|
||||
<li>• 建议按照职能划分角色,便于权限管理</li>
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { RoleManagementStats, Role } from '../types';
|
||||
|
||||
interface RoleManagementStatsCardsProps {
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
export function RoleManagementStatsCards({ roles }: RoleManagementStatsCardsProps) {
|
||||
const stats: RoleManagementStats[] = [
|
||||
{
|
||||
label: '总角色数',
|
||||
value: roles.length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '系统角色',
|
||||
value: roles.filter(r => r.type === 'system').length,
|
||||
color: 'text-purple-600',
|
||||
bg: 'bg-purple-100',
|
||||
},
|
||||
{
|
||||
label: '自定义角色',
|
||||
value: roles.filter(r => r.type === 'custom').length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
interface RoleSearchProps {
|
||||
searchKeyword: string;
|
||||
onSearchChange: (keyword: string) => void;
|
||||
}
|
||||
|
||||
export function RoleSearch({ searchKeyword, onSearchChange }: RoleSearchProps) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索角色名称、代码、描述..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
219
crop-x/src/app/(app)/central-config/user/role/page.tsx
Normal file
219
crop-x/src/app/(app)/central-config/user/role/page.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { RoleManagementHeader } from './components/RoleManagementHeader';
|
||||
import { RoleManagementStatsCards } from './components/RoleManagementStatsCards';
|
||||
import { RoleSearch } from './components/RoleSearch';
|
||||
import { RoleList } from './components/RoleList';
|
||||
import { RoleFormDialog } from './components/RoleFormDialog';
|
||||
import { RoleDetailDialog } from './components/RoleDetailDialog';
|
||||
import { RoleManagementInstructions } from './components/RoleManagementInstructions';
|
||||
import { Role, RoleFormData } from './types';
|
||||
|
||||
export default function RoleManagementPage() {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
const [formData, setFormData] = useState<RoleFormData>({
|
||||
type: 'custom',
|
||||
status: 'active',
|
||||
menuIds: [],
|
||||
permissionIds: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles();
|
||||
}, []);
|
||||
|
||||
const loadRoles = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_roles');
|
||||
if (data) {
|
||||
setRoles(JSON.parse(data));
|
||||
} else {
|
||||
const mockRoles: Role[] = [
|
||||
{
|
||||
id: 'role-1',
|
||||
name: '超级管理员',
|
||||
code: 'super_admin',
|
||||
description: '系统最高权限,可管理所有功能和数据',
|
||||
type: 'system',
|
||||
menuIds: ['*'],
|
||||
permissionIds: ['*'],
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
updatedAt: '2024-01-01T00:00:00',
|
||||
},
|
||||
{
|
||||
id: 'role-2',
|
||||
name: '企业管理员',
|
||||
code: 'enterprise_admin',
|
||||
description: '企业管理员,可管理本企业的员工和数据',
|
||||
type: 'system',
|
||||
menuIds: ['config-user', 'machinery', 'field'],
|
||||
permissionIds: ['user:view', 'user:add', 'user:edit', 'machinery:*'],
|
||||
defaultHomePage: '/machinery/archive/entry',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
updatedAt: '2024-01-01T00:00:00',
|
||||
},
|
||||
{
|
||||
id: 'role-3',
|
||||
name: '操作员',
|
||||
code: 'operator',
|
||||
description: '一般操作员,可查看和操作农机设备',
|
||||
type: 'system',
|
||||
menuIds: ['machinery', 'field'],
|
||||
permissionIds: ['machinery:view', 'machinery:control', 'field:view'],
|
||||
defaultHomePage: '/machinery/monitoring/location',
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
updatedAt: '2024-01-01T00:00:00',
|
||||
},
|
||||
{
|
||||
id: 'role-4',
|
||||
name: '维修员',
|
||||
code: 'maintenance',
|
||||
description: '农机维修人员,负责设备维护和故障诊断',
|
||||
type: 'custom',
|
||||
menuIds: ['machinery-fault', 'machinery-archive'],
|
||||
permissionIds: ['machinery:view', 'fault:view', 'fault:handle'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-01T00:00:00',
|
||||
updatedAt: '2024-10-01T00:00:00',
|
||||
},
|
||||
];
|
||||
localStorage.setItem('smart_agriculture_roles', JSON.stringify(mockRoles));
|
||||
setRoles(mockRoles);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRoles = roles.filter(role => {
|
||||
const matchKeyword = !searchKeyword ||
|
||||
role.name.includes(searchKeyword) ||
|
||||
role.code.includes(searchKeyword) ||
|
||||
(role.description && role.description.includes(searchKeyword));
|
||||
return matchKeyword;
|
||||
});
|
||||
|
||||
const handleAddRole = () => {
|
||||
setEditingRole(null);
|
||||
setFormData({
|
||||
type: 'custom',
|
||||
status: 'active',
|
||||
menuIds: [],
|
||||
permissionIds: [],
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEdit = (role: Role) => {
|
||||
setEditingRole(role);
|
||||
setFormData(role);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.name || !formData.code) {
|
||||
toast.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingRole) {
|
||||
const updated = roles.map(role =>
|
||||
role.id === editingRole.id
|
||||
? {
|
||||
...role,
|
||||
...formData,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: role
|
||||
);
|
||||
setRoles(updated);
|
||||
localStorage.setItem('smart_agriculture_roles', JSON.stringify(updated));
|
||||
toast.success('角色更新成功');
|
||||
} else {
|
||||
const newRole: Role = {
|
||||
id: `role-${Date.now()}`,
|
||||
...formData as Role,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...roles, newRole];
|
||||
setRoles(updated);
|
||||
localStorage.setItem('smart_agriculture_roles', JSON.stringify(updated));
|
||||
toast.success('角色添加成功');
|
||||
}
|
||||
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
const role = roles.find(r => r.id === id);
|
||||
if (role && role.type === 'system') {
|
||||
toast.error('系统角色不能删除');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('确定要删除该角色吗?')) return;
|
||||
|
||||
const updated = roles.filter(role => role.id !== id);
|
||||
setRoles(updated);
|
||||
localStorage.setItem('smart_agriculture_roles', JSON.stringify(updated));
|
||||
toast.success('角色删除成功');
|
||||
};
|
||||
|
||||
const handleViewDetail = (role: Role) => {
|
||||
setSelectedRole(role);
|
||||
setShowDetailDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<RoleManagementHeader
|
||||
onAddRole={handleAddRole}
|
||||
/>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<RoleManagementStatsCards roles={roles} />
|
||||
|
||||
{/* 搜索 */}
|
||||
<RoleSearch
|
||||
searchKeyword={searchKeyword}
|
||||
onSearchChange={setSearchKeyword}
|
||||
/>
|
||||
|
||||
{/* 角色列表 */}
|
||||
<RoleList
|
||||
roles={filteredRoles}
|
||||
onViewDetail={handleViewDetail}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
{/* 添加/编辑表单 */}
|
||||
<RoleFormDialog
|
||||
open={showForm}
|
||||
onOpenChange={setShowForm}
|
||||
editingRole={editingRole}
|
||||
formData={formData}
|
||||
onFormDataChange={setFormData}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
{/* 详情对话框 */}
|
||||
<RoleDetailDialog
|
||||
open={showDetailDialog}
|
||||
onOpenChange={setShowDetailDialog}
|
||||
selectedRole={selectedRole}
|
||||
/>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<RoleManagementInstructions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
358
crop-x/src/app/(app)/central-config/user/role/types.ts
Normal file
358
crop-x/src/app/(app)/central-config/user/role/types.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
// 角色管理相关类型定义
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
type: RoleType;
|
||||
menuIds: string[];
|
||||
permissionIds: string[];
|
||||
defaultHomePage?: string;
|
||||
status: 'active' | 'inactive';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type RoleType = 'system' | 'custom';
|
||||
|
||||
// 定义菜单权限结构(菜单与操作权限关联)
|
||||
export interface MenuWithPermissions {
|
||||
id: string;
|
||||
label: string;
|
||||
children?: {
|
||||
id: string;
|
||||
label: string;
|
||||
operations: {
|
||||
id: string;
|
||||
label: string;
|
||||
key: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
// 所有系统菜单及其操作权限
|
||||
export const allSystemMenus: { id: string; label: string; menus: MenuWithPermissions[] }[] = [
|
||||
{
|
||||
id: 'machinery',
|
||||
label: '智能农机管理系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'machinery-archive',
|
||||
label: '农机档案',
|
||||
children: [
|
||||
{
|
||||
id: 'machinery-entry',
|
||||
label: '农机档案录入与维护',
|
||||
operations: [
|
||||
{ id: 'machinery-entry:view', label: '查看', key: 'view' },
|
||||
{ id: 'machinery-entry:add', label: '新增', key: 'add' },
|
||||
{ id: 'machinery-entry:edit', label: '编辑', key: 'edit' },
|
||||
{ id: 'machinery-entry:delete', label: '删除', key: 'delete' },
|
||||
{ id: 'machinery-entry:export', label: '导出', key: 'export' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'machinery-classification',
|
||||
label: '农机分类与标签管理',
|
||||
operations: [
|
||||
{ id: 'machinery-classification:view', label: '查看', key: 'view' },
|
||||
{ id: 'machinery-classification:edit', label: '编辑', key: 'edit' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'machinery-qrcode',
|
||||
label: '农机二维码管理',
|
||||
operations: [
|
||||
{ id: 'machinery-qrcode:view', label: '查看', key: 'view' },
|
||||
{ id: 'machinery-qrcode:generate', label: '生成', key: 'generate' },
|
||||
{ id: 'machinery-qrcode:print', label: '打印', key: 'print' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'driver-archive',
|
||||
label: '驾驶员档案',
|
||||
children: [
|
||||
{
|
||||
id: 'driver-info',
|
||||
label: '驾驶员信息管理',
|
||||
operations: [
|
||||
{ id: 'driver-info:view', label: '查看', key: 'view' },
|
||||
{ id: 'driver-info:add', label: '新增', key: 'add' },
|
||||
{ id: 'driver-info:edit', label: '编辑', key: 'edit' },
|
||||
{ id: 'driver-info:delete', label: '删除', key: 'delete' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'driver-task',
|
||||
label: '驾驶员任务管理',
|
||||
operations: [
|
||||
{ id: 'driver-task:view', label: '查看', key: 'view' },
|
||||
{ id: 'driver-task:assign', label: '分配任务', key: 'assign' },
|
||||
{ id: 'driver-task:cancel', label: '取消任务', key: 'cancel' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'monitoring',
|
||||
label: '设备实时监控与定位',
|
||||
children: [
|
||||
{
|
||||
id: 'realtime-location',
|
||||
label: '实时位置追踪',
|
||||
operations: [
|
||||
{ id: 'realtime-location:view', label: '查看', key: 'view' },
|
||||
{ id: 'realtime-location:track', label: '追踪', key: 'track' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'work-status',
|
||||
label: '工作状态监控',
|
||||
operations: [
|
||||
{ id: 'work-status:view', label: '查看', key: 'view' },
|
||||
{ id: 'work-status:control', label: '远程控制', key: 'control' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'fault-diagnosis',
|
||||
label: '远程诊断与故障预警',
|
||||
children: [
|
||||
{
|
||||
id: 'fault-warning',
|
||||
label: '故障诊断与预警',
|
||||
operations: [
|
||||
{ id: 'fault-warning:view', label: '查看', key: 'view' },
|
||||
{ id: 'fault-warning:handle', label: '处理', key: 'handle' },
|
||||
{ id: 'fault-warning:export', label: '导出', key: 'export' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'field',
|
||||
label: '地块信息管理系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'field-info',
|
||||
label: '地块信息',
|
||||
children: [
|
||||
{
|
||||
id: 'field-list',
|
||||
label: '地块列表',
|
||||
operations: [
|
||||
{ id: 'field-list:view', label: '查看', key: 'view' },
|
||||
{ id: 'field-list:add', label: '新增', key: 'add' },
|
||||
{ id: 'field-list:edit', label: '编辑', key: 'edit' },
|
||||
{ id: 'field-list:delete', label: '删除', key: 'delete' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'field-map',
|
||||
label: '地块地图',
|
||||
operations: [
|
||||
{ id: 'field-map:view', label: '查看', key: 'view' },
|
||||
{ id: 'field-map:draw', label: '绘制', key: 'draw' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'operation',
|
||||
label: '农事操作管理系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'operation-plan',
|
||||
label: '作业计划',
|
||||
children: [
|
||||
{
|
||||
id: 'plan-list',
|
||||
label: '计划列表',
|
||||
operations: [
|
||||
{ id: 'plan-list:view', label: '查看', key: 'view' },
|
||||
{ id: 'plan-list:add', label: '新增', key: 'add' },
|
||||
{ id: 'plan-list:edit', label: '编辑', key: 'edit' },
|
||||
{ id: 'plan-list:delete', label: '删除', key: 'delete' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'asset',
|
||||
label: '农业资产管理系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'asset-inventory',
|
||||
label: '资产清单',
|
||||
children: [
|
||||
{
|
||||
id: 'inventory-list',
|
||||
label: '清单列表',
|
||||
operations: [
|
||||
{ id: 'inventory-list:view', label: '查看', key: 'view' },
|
||||
{ id: 'inventory-list:manage', label: '管理', key: 'manage' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai-model',
|
||||
label: 'AI作物模型精准决策系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'ai-models',
|
||||
label: 'AI模型',
|
||||
children: [
|
||||
{
|
||||
id: 'model-list',
|
||||
label: '模型列表',
|
||||
operations: [
|
||||
{ id: 'model-list:view', label: '查看', key: 'view' },
|
||||
{ id: 'model-list:run', label: '运行', key: 'run' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'irrigation',
|
||||
label: '水肥一体化控制系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'irrigation-control',
|
||||
label: '灌溉控制',
|
||||
children: [
|
||||
{
|
||||
id: 'zone-control',
|
||||
label: '区域控制',
|
||||
operations: [
|
||||
{ id: 'zone-control:view', label: '查看', key: 'view' },
|
||||
{ id: 'zone-control:control', label: '控制', key: 'control' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
label: '中心配置管理系统',
|
||||
menus: [
|
||||
{
|
||||
id: 'tenant-management',
|
||||
label: '租户管理',
|
||||
children: [
|
||||
{
|
||||
id: 'enterprise-audit',
|
||||
label: '企业审核',
|
||||
operations: [
|
||||
{ id: 'enterprise-audit:view', label: '查看', key: 'view' },
|
||||
{ id: 'enterprise-audit:audit', label: '审核', key: 'audit' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'enterprise-info',
|
||||
label: '企业信息',
|
||||
operations: [
|
||||
{ id: 'enterprise-info:view', label: '查看', key: 'view' },
|
||||
{ id: 'enterprise-info:edit', label: '编辑', key: 'edit' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'user-management',
|
||||
label: '用户管理',
|
||||
children: [
|
||||
{
|
||||
id: 'employee-management',
|
||||
label: '员工管理',
|
||||
operations: [
|
||||
{ id: 'employee-management:view', label: '查看', key: 'view' },
|
||||
{ id: 'employee-management:add', label: '新增', key: 'add' },
|
||||
{ id: 'employee-management:edit', label: '编辑', key: 'edit' },
|
||||
{ id: 'employee-management:delete', label: '删除', key: 'delete' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'role-management',
|
||||
label: '角色管理',
|
||||
operations: [
|
||||
{ id: 'role-management:view', label: '查看', key: 'view' },
|
||||
{ id: 'role-management:manage', label: '管理', key: 'manage' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system-params',
|
||||
label: '系统参数',
|
||||
children: [
|
||||
{
|
||||
id: 'system-settings',
|
||||
label: '系统设置',
|
||||
operations: [
|
||||
{ id: 'system-settings:view', label: '查看', key: 'view' },
|
||||
{ id: 'system-settings:edit', label: '编辑', key: 'edit' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system-monitor',
|
||||
label: '系统监控',
|
||||
children: [
|
||||
{
|
||||
id: 'login-log',
|
||||
label: '登录日志',
|
||||
operations: [
|
||||
{ id: 'login-log:view', label: '查看', key: 'view' },
|
||||
{ id: 'login-log:export', label: '导出', key: 'export' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'operation-log',
|
||||
label: '操作日志',
|
||||
operations: [
|
||||
{ id: 'operation-log:view', label: '查看', key: 'view' },
|
||||
{ id: 'operation-log:export', label: '导出', key: 'export' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 统计数据
|
||||
export interface RoleManagementStats {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
export interface RoleFormData {
|
||||
name?: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
type?: RoleType;
|
||||
status?: 'active' | 'inactive';
|
||||
defaultHomePage?: string;
|
||||
menuIds?: string[];
|
||||
permissionIds?: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user