生产管理系统前端 开发中心配置系统 所有页面

This commit is contained in:
2025-10-21 18:04:39 +08:00
parent 4a5d278d89
commit 9afc680833
185 changed files with 13677 additions and 4487 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View 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[];
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View 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';

View 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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View 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[];
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View 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[];
}