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

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