【码源】智能无人仓库管理系统(详细码源下~基于React+TypeScript+Vite):
详细码源下:
InventoryManagement.tsx #库存管理:
bash
复制代码
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Database, Search, Plus, Edit, Eye, Filter, X,
AlertCircle, ChevronDown, RefreshCw, BarChart2,
PackageCheck, PackageOpen, MoveHorizontal, Calendar,
CheckCircle, TrendingUp, TrendingDown, ArrowUp, ArrowDown
} from 'lucide-react';
import {
BarChart, Bar, LineChart, Line, PieChart, Pie,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
Cell
} from 'recharts';
import { useTheme } from '@/hooks/useTheme';
import { toast } from 'sonner';
// 库存警报类型定义
interface InventoryAlert {
id: string;
goodsId: string;
goodsName: string;
category: string;
currentStock: number;
threshold: number;
location: string;
alertType: 'low-stock' | 'expiring-soon' | 'excess-stock';
alertTime: string;
status: 'pending' | 'processed';
}
// 库存移动记录类型定义
interface InventoryMovement {
id: string;
goodsId: string;
goodsName: string;
movementType: 'in' | 'out' | 'transfer';
quantity: number;
fromLocation: string;
toLocation: string;
operator: string;
movementTime: string;
reason: string;
}
// 库存盘点类型定义
interface InventoryCheck {
id: string;
name: string;
status: 'pending' | 'in-progress' | 'completed' | 'cancelled';
startTime: string;
endTime: string | null;
checkedBy: string;
discrepancyCount: number;
totalItems: number;
}
// 库存分类统计类型定义
interface CategoryStat {
name: string;
value: number;
inStock: number;
lowStock: number;
outOfStock: number;
}
// 模拟库存警报数据
const generateMockAlerts = (): InventoryAlert[] => {
const categories = ['电子产品', '服装鞋帽', '食品饮料', '家居用品', '其他'];
const alertTypes: Array<'low-stock' | 'expiring-soon' | 'excess-stock'> = ['low-stock', 'expiring-soon', 'excess-stock'];
const locations = ['A区-01-01', 'A区-01-02', 'A区-02-01', 'B区-01-01', 'B区-02-01', 'C区-01-01'];
return Array.from({ length: 15 }, (_, index) => {
const alertType = alertTypes[Math.floor(Math.random() * alertTypes.length)];
const threshold = Math.floor(Math.random() * 50) + 10;
const currentStock = alertType === 'low-stock'
? Math.floor(Math.random() * threshold)
: alertType === 'excess-stock'
? threshold * (Math.floor(Math.random() * 5) + 3)
: Math.floor(Math.random() * 200) + 50;
return {
id: `ALERT-${String(index + 1).padStart(4, '0')}`,
goodsId: `GOODS-${String(Math.floor(Math.random() * 100)).padStart(4, '0')}`,
goodsName: `货物名称 ${Math.floor(Math.random() * 100) + 1}`,
category: categories[Math.floor(Math.random() * categories.length)],
currentStock,
threshold,
location: locations[Math.floor(Math.random() * locations.length)],
alertType,
alertTime: new Date(Date.now() - Math.floor(Math.random() * 7) * 24 * 60 * 60 * 1000).toLocaleString('zh-CN'),
status: Math.random() > 0.5 ? 'pending' : 'processed'
};
});
};
// 模拟库存移动记录数据
const generateMockMovements = (): InventoryMovement[] => {
const movementTypes: Array<'in' | 'out' | 'transfer'> = ['in', 'out', 'transfer'];
const locations = ['A区-01-01', 'A区-01-02', 'A区-02-01', 'B区-01-01', 'B区-02-01', 'C区-01-01'];
const reasons = ['采购入库', '销售出库', '内部移库', '盘点调整', '质量问题退货', '客户订单发货'];
const operators = ['系统自动', '管理员', '操作员A', '操作员B', '操作员C'];
return Array.from({ length: 50 }, (_, index) => {
const movementType = movementTypes[Math.floor(Math.random() * movementTypes.length)];
return {
id: `MOVE-${String(index + 1).padStart(4, '0')}`,
goodsId: `GOODS-${String(Math.floor(Math.random() * 100)).padStart(4, '0')}`,
goodsName: `货物名称 ${Math.floor(Math.random() * 100) + 1}`,
movementType,
quantity: Math.floor(Math.random() * 200) + 10,
fromLocation: movementType === 'in' ? '外部' : locations[Math.floor(Math.random() * locations.length)],
toLocation: movementType === 'out' ? '外部' : locations[Math.floor(Math.random() * locations.length)],
operator: operators[Math.floor(Math.random() * operators.length)],
movementTime: new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000).toLocaleString('zh-CN'),
reason: reasons[Math.floor(Math.random() * reasons.length)]
};
});
};
// 模拟库存盘点数据
const generateMockChecks = (): InventoryCheck[] => {
const statuses: Array<'pending' | 'in-progress' | 'completed' | 'cancelled'> = ['pending', 'in-progress', 'completed', 'cancelled'];
const checkers = ['管理员', '操作员A', '操作员B', '系统自动'];
return Array.from({ length: 10 }, (_, index) => {
const status = statuses[Math.floor(Math.random() * statuses.length)];
const startDate = new Date(Date.now() - Math.floor(Math.random() * 90) * 24 * 60 * 60 * 1000);
const endDate = status === 'completed'
? new Date(startDate.getTime() + Math.floor(Math.random() * 3) * 24 * 60 * 60 * 1000)
: null;
const totalItems = Math.floor(Math.random() * 300) + 50;
return {
id: `CHECK-${String(index + 1).padStart(4, '0')}`,
name: `${index % 3 === 0 ? '月度' : index % 3 === 1 ? '季度' : '年度'}盘点 ${index + 1}`,
status,
startTime: startDate.toLocaleString('zh-CN'),
endTime: endDate ? endDate.toLocaleString('zh-CN') : null,
checkedBy: checkers[Math.floor(Math.random() * checkers.length)],
discrepancyCount: status === 'completed' ? Math.floor(Math.random() * 10) : 0,
totalItems
};
});
};
// 模拟库存分类统计数据
const generateCategoryStats = (): CategoryStat[] => {
const categories = ['电子产品', '服装鞋帽', '食品饮料', '家居用品', '其他'];
return categories.map((category, index) => {
const value = Math.floor(Math.random() * 1000) + 500;
const lowStock = Math.floor(value * 0.15);
const outOfStock = Math.floor(value * 0.05);
const inStock = value - lowStock - outOfStock;
return {
name: category,
value,
inStock,
lowStock,
outOfStock
};
});
};
// 模拟库存趋势数据
const generateInventoryTrendData = () => {
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月'];
let inventory = 12000;
return months.map(month => {
const inQuantity = Math.floor(Math.random() * 2000) + 1000;
const outQuantity = Math.floor(Math.random() * 2000) + 1000;
inventory = Math.max(8000, inventory + inQuantity - outQuantity);
return {
month,
inventory,
in: inQuantity,
out: outQuantity
};
});
};
// 渲染警报类型标签
const renderAlertTypeBadge = (type: string) => {
switch (type) {
case 'low-stock':
return <span className="px-2 py-1 text-xs rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">库存不足</span>;
case 'expiring-soon':
return <span className="px-2 py-1 text-xs rounded-full bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200">即将过期</span>;
case 'excess-stock':
return <span className="px-2 py-1 text-xs rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">库存过剩</span>;
default:
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未知</span>;
}
};
// 渲染警报状态标签
const renderAlertStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <span className="px-2 py-1 text-xs rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">待处理</span>;
case 'processed':
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">已处理</span>;
default:
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未知</span>;
}
};
// 渲染移动类型标签
const renderMovementTypeBadge = (type: string) => {
switch (type) {
case 'in':
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">入库</span>;
case 'out':
return <span className="px-2 py-1 text-xs rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">出库</span>;
case 'transfer':
return <span className="px-2 py-1 text-xs rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">移库</span>;
default:
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未知</span>;
}
};
// 渲染盘点状态标签
const renderCheckStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">待开始</span>;
case 'in-progress':
return <span className="px-2 py-1 text-xs rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">进行中</span>;
case 'completed':
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">已完成</span>;
case 'cancelled':
return <span className="px-2 py-1 text-xs rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">已取消</span>;
default:
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未知</span>;
}
};
const InventoryManagement: React.FC = () => {
const { theme } = useTheme();
const [alerts, setAlerts] = useState<InventoryAlert[]>([]);
const [filteredAlerts, setFilteredAlerts] = useState<InventoryAlert[]>([]);
const [movements, setMovements] = useState<InventoryMovement[]>([]);
const [filteredMovements, setFilteredMovements] = useState<InventoryMovement[]>([]);
const [checks, setChecks] = useState<InventoryCheck[]>([]);
const [categoryStats, setCategoryStats] = useState<CategoryStat[]>([]);
const [inventoryTrend, setInventoryTrend] = useState(generateInventoryTrendData());
const [searchTerm, setSearchTerm] = useState('');
const [selectedAlertType, setSelectedAlertType] = useState('all');
const [selectedAlertStatus, setSelectedAlertStatus] = useState('all');
const [selectedMovementType, setSelectedMovementType] = useState('all');
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isCheckModalOpen, setIsCheckModalOpen] = useState(false);
const [currentAlert, setCurrentAlert] = useState<InventoryAlert | null>(null);
const [currentMovement, setCurrentMovement] = useState<InventoryMovement | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
const [activeTab, setActiveTab] = useState<'alerts' | 'movements' | 'checks'>('alerts');
const [newCheck, setNewCheck] = useState<{
name: string;
startTime: string;
checkedBy: string;
}>({
name: '',
startTime: new Date().toLocaleString('zh-CN'),
checkedBy: '管理员'
});
// 图表颜色
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'];
// 加载数据
useEffect(() => {
// 模拟API请求延迟
const timer = setTimeout(() => {
const mockAlertsData = generateMockAlerts();
const mockMovementsData = generateMockMovements();
const mockChecksData = generateMockChecks();
const mockCategoryStatsData = generateCategoryStats();
setAlerts(mockAlertsData);
setFilteredAlerts(mockAlertsData);
setMovements(mockMovementsData);
setFilteredMovements(mockMovementsData);
setChecks(mockChecksData);
setCategoryStats(mockCategoryStatsData);
setIsLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
// 搜索和筛选警报
useEffect(() => {
let result = [...alerts];
// 搜索筛选
if (searchTerm) {
result = result.filter(item =>
item.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.goodsId.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.goodsName.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// 警报类型筛选
if (selectedAlertType !== 'all') {
result = result.filter(item => item.alertType === selectedAlertType);
}
// 警报状态筛选
if (selectedAlertStatus !== 'all') {
result = result.filter(item => item.status === selectedAlertStatus);
}
setFilteredAlerts(result);
setCurrentPage(1); // 重置到第一页
}, [searchTerm, selectedAlertType, selectedAlertStatus, alerts, activeTab]);
// 搜索和筛选移动记录
useEffect(() => {
let result = [...movements];
// 搜索筛选
if (searchTerm) {
result = result.filter(item =>
item.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.goodsId.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.goodsName.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// 移动类型筛选
if (selectedMovementType !== 'all') {
result = result.filter(item => item.movementType === selectedMovementType);
}
setFilteredMovements(result);
setCurrentPage(1); // 重置到第一页
}, [searchTerm, selectedMovementType, movements, activeTab]);
// 分页功能
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = activeTab === 'alerts'
? filteredAlerts.slice(indexOfFirstItem, indexOfLastItem)
: activeTab === 'movements'
? filteredMovements.slice(indexOfFirstItem, indexOfLastItem)
: checks.slice(indexOfFirstItem, indexOfLastItem);
const totalPages = Math.ceil(
activeTab === 'alerts'
? filteredAlerts.length
: activeTab === 'movements'
? filteredMovements.length
: checks.length
/ itemsPerPage
);
// 打开警报详情模态框
const openAlertDetailModal = (alert: InventoryAlert) => {
setCurrentAlert(alert);
setIsDetailModalOpen(true);
};
// 打开移动记录详情模态框
const openMovementDetailModal = (movement: InventoryMovement) => {
setCurrentMovement(movement);
setIsDetailModalOpen(true);
};
// 刷新数据
const handleRefresh = () => {
setIsLoading(true);
// 模拟API请求延迟
const timer = setTimeout(() => {
const mockAlertsData = generateMockAlerts();
const mockMovementsData = generateMockMovements();
const mockChecksData = generateMockChecks();
const mockCategoryStatsData = generateCategoryStats();
const mockInventoryTrendData = generateInventoryTrendData();
setAlerts(mockAlertsData);
setFilteredAlerts(mockAlertsData);
setMovements(mockMovementsData);
setFilteredMovements(mockMovementsData);
setChecks(mockChecksData);
setCategoryStats(mockCategoryStatsData);
setInventoryTrend(mockInventoryTrendData);
setIsLoading(false);
toast.success('数据刷新成功');
}, 1000);
return () => clearTimeout(timer);
};
// 处理警报标记为已处理
const handleProcessAlert = (alertId: string) => {
const updatedAlerts = alerts.map(alert =>
alert.id === alertId ? { ...alert, status: 'processed' } : alert
);
setAlerts(updatedAlerts);
toast.success('警报已标记为已处理');
};
// 处理创建新盘点任务
const handleCreateCheck = () => {
if (!newCheck.name || !newCheck.startTime || !newCheck.checkedBy) {
toast.error('请填写完整的盘点信息');
return;
}
const checkToAdd: InventoryCheck = {
id: `CHECK-${String(checks.length + 1).padStart(4, '0')}`,
name: newCheck.name,
status: 'pending',
startTime: newCheck.startTime,
endTime: null,
checkedBy: newCheck.checkedBy,
discrepancyCount: 0,
totalItems: Math.floor(Math.random() * 300) + 50
};
setChecks([checkToAdd, ...checks]);
setIsCheckModalOpen(false);
setNewCheck({
name: '',
startTime: new Date().toLocaleString('zh-CN'),
checkedBy: '管理员'
});
toast.success('新盘点任务已创建');
};
// 分页控制
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 获取统计数据
const getInventoryStats = () => {
const totalItems = categoryStats.reduce((sum, category) => sum + category.value, 0);
const inStockItems = categoryStats.reduce((sum, category) => sum + category.inStock, 0);
const lowStockItems = categoryStats.reduce((sum, category) => sum + category.lowStock, 0);
const outOfStockItems = categoryStats.reduce((sum, category) => sum + category.outOfStock, 0);
const pendingAlerts = alerts.filter(alert => alert.status === 'pending').length;
return {
totalItems,
inStockItems,
lowStockItems,
outOfStockItems,
pendingAlerts
};
};
const stats = getInventoryStats();
return (
<div className="p-4 md:p-6">
{/* 页面标题和操作区 */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
<div>
<h2 className="text-2xl font-bold">库存管理</h2>
<p className="text-gray-500 dark:text-gray-400">监控和管理仓库库存状态</p>
</div>
<div className="flex space-x-3 mt-4 md:mt-0">
<button
onClick={handleRefresh}
className="flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
<RefreshCw size={16} className="mr-2" />
刷新
</button>
{activeTab === 'checks' && (
<button
onClick={() => setIsCheckModalOpen(true)}
className="flex items-center px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg shadow-sm hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors duration-200"
>
<Plus size={16} className="mr-2" />
新建盘点
</button>
)}
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow duration-300"
>
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">库存总量</p>
<h3 className="text-2xl font-bold mt-1">{stats.totalItems}</h3>
</div>
<div className="bg-blue-500 text-white p-2 rounded-lg">
<Database size={20} />
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow duration-300"
>
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">充足库存</p>
<h3 className="text-2xl font-bold mt-1">{stats.inStockItems}</h3>
</div>
<div className="bg-green-500 text-white p-2 rounded-lg">
<PackageCheck size={20} />
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow duration-300"
>
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">库存不足</p>
<h3 className="text-2xl font-bold mt-1">{stats.lowStockItems}</h3>
</div>
<div className="bg-yellow-500 text-white p-2 rounded-lg">
<PackageOpen size={20} />
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow duration-300"
>
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">缺货商品</p>
<h3 className="text-2xl font-bold mt-1">{stats.outOfStockItems}</h3>
</div>
<div className="bg-red-500 text-white p-2 rounded-lg">
<AlertCircle size={20} />
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.4 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow duration-300"
>
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 dark:text-gray-400 text-sm">待处理警报</p>
<h3 className="text-2xl font-bold mt-1">{stats.pendingAlerts}</h3>
</div>
<div className="bg-purple-500 text-white p-2 rounded-lg">
<AlertCircle size={20} />
</div>
</div>
</motion.div>
</div>
{/* 标签切换 */}
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-6">
<button
onClick={() => setActiveTab('alerts')}
className={`px-4 py-3 font-medium text-sm border-b-2 ${
activeTab === 'alerts'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
库存警报
</button>
<button
onClick={() => setActiveTab('movements')}
className={`px-4 py-3 font-medium text-sm border-b-2 ${
activeTab === 'movements'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
库存移动记录
</button>
<button
onClick={() => setActiveTab('checks')}
className={`px-4 py-3 font-medium text-sm border-b-2 ${
activeTab === 'checks'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
库存盘点
</button>
</div>
{/* 搜索和筛选区 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4 mb-6">
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<div className="relative flex-1">
<Search size={18} className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder={
activeTab === 'alerts'
? "搜索警报ID、货物ID或名称..."
: activeTab === 'movements'
? "搜索记录ID、货物ID或名称..."
: "搜索盘点ID或名称..."
}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X size={16} />
</button>
)}
</div>
{(activeTab === 'alerts' || activeTab === 'movements') && (
<div className="relative">
<button
onClick={() => setIsFilterDropdownOpen(!isFilterDropdownOpen)}
className="flex items-center px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
<Filter size={16} className="mr-2" />
筛选
<ChevronDown size={16} className={`ml-2 transition-transform duration-200 ${isFilterDropdownOpen ? 'transform rotate-180' : ''}`} />
</button>
<AnimatePresence>
{isFilterDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg z-10 p-4"
>
{activeTab === 'alerts' && (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">警报类型</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedAlertType}
onChange={(e) => setSelectedAlertType(e.target.value)}
>
<option value="all">全部类型</option>
<option value="low-stock">库存不足</option>
<option value="expiring-soon">即将过期</option>
<option value="excess-stock">库存过剩</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">警报状态</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedAlertStatus}
onChange={(e) => setSelectedAlertStatus(e.target.value)}
>
<option value="all">全部状态</option>
<option value="pending">待处理</option>
<option value="processed">已处理</option>
</select>
</div>
</>
)}
{activeTab === 'movements' && (
<div>
<label className="block text-sm font-medium mb-2">移动类型</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedMovementType}
onChange={(e) => setSelectedMovementType(e.target.value)}
>
<option value="all">全部类型</option>
<option value="in">入库</option>
<option value="out">出库</option>
<option value="transfer">移库</option>
</select>
</div>
)}
<div className="mt-4 flex justify-end">
<button
onClick={() => {
if (activeTab === 'alerts') {
setSelectedAlertType('all');
setSelectedAlertStatus('all');
} else if (activeTab === 'movements') {
setSelectedMovementType('all');
}
setIsFilterDropdownOpen(false);
}}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
重置筛选
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
</div>
{/* 主要内容区域 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
{/* 列表区域 */}
<div className="lg:col-span-2">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden">
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">
{activeTab === 'alerts' ? '库存警报列表' : activeTab === 'movements' ? '库存移动记录' : '库存盘点任务'}
</h3>
<div className="text-sm text-gray-500 dark:text-gray-400">
{activeTab === 'alerts'
? `共 ${filteredAlerts.length} 条警报`
: activeTab === 'movements'
? `共 ${filteredMovements.length} 条记录`
: `共 ${checks.length} 个任务`
}
</div>
</div>
{isLoading ? (
<div className="h-96 flex items-center justify-center">
<div className="flex flex-col items-center">
<RefreshCw size={32} className="text-blue-500 animate-spin" />
<p className="mt-2 text-gray-500 dark:text-gray-400">加载中...</p>
</div>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
{activeTab === 'alerts' && (
<>
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">货物名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">分类</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">当前库存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">预警阈值</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">位置</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">警报类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">状态</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredAlerts.length === 0 ? (
<tr>
<td colSpan={9} className="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
没有找到匹配的警报
</td>
</tr>
) : (
currentItems.map((alert) => (
<tr key={alert.id} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200">
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium">{alert.id}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{alert.goodsName}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{alert.category}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{alert.currentStock}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{alert.threshold}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{alert.location}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{renderAlertTypeBadge(alert.alertType)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{renderAlertStatusBadge(alert.status)}</td>
<td className="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<button
onClick={() => openAlertDetailModal(alert)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
<Eye size={16} />
</button>
{alert.status === 'pending' && (
<button
onClick={() => handleProcessAlert(alert.id)}
className="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300"
>
<CheckCircle size={16} />
</button>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</>
)}
{activeTab === 'movements' && (
<>
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">货物名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">移动类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">数量</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">从</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">到</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">操作人</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">时间</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredMovements.length === 0 ? (
<tr>
<td colSpan={9} className="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
没有找到匹配的记录
</td>
</tr>
) : (
currentItems.map((movement) => (
<tr key={movement.id} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200">
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium">{movement.id}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{movement.goodsName}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{renderMovementTypeBadge(movement.movementType)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
<div className="flex items-center">
{movement.movementType === 'in' ? (
<ArrowUp size={14} className="text-green-500 mr-1" />
) : movement.movementType === 'out' ? (
<ArrowDown size={14} className="text-red-500 mr-1" />
) : (
<MoveHorizontal size={14} className="text-blue-500 mr-1" />
)}
{movement.quantity}
</div>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{movement.fromLocation}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{movement.toLocation}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{movement.operator}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{movement.movementTime}</td>
<td className="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => openMovementDetailModal(movement)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
<Eye size={16} />
</button>
</td>
</tr>
))
)}
</tbody>
</>
)}
{activeTab === 'checks' && (
<>
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">开始时间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">结束时间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">盘点人</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">差异数</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{checks.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
没有找到匹配的盘点任务
</td>
</tr>
) : (
currentItems.map((check) => (
<tr key={check.id} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200">
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium">{check.id}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{check.name}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{renderCheckStatusBadge(check.status)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{check.startTime}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{check.endTime || '-'}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{check.checkedBy}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{check.discrepancyCount}</td>
</tr>
))
)}
</tbody>
</>
)}
</table>
</div>
)}
{/* 分页控件 */}
{(activeTab === 'alerts' && filteredAlerts.length > 0) ||
(activeTab === 'movements' && filteredMovements.length > 0) ||
(activeTab === 'checks' && checks.length > 0) ? (
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-gray-700">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className={`relative inline-flex items-center px-4 py-2 border rounded-md text-sm font-medium ${
currentPage === 1
? 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
上一页
</button>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className={`ml-3 relative inline-flex items-center px-4 py-2 border rounded-md text-sm font-medium ${
currentPage === totalPages
? 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
下一页
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700 dark:text-gray-300">
显示第 <span className="font-medium">{indexOfFirstItem + 1}</span> 到 <span className="font-medium">{Math.min(indexOfLastItem,
activeTab === 'alerts' ? filteredAlerts.length :
activeTab === 'movements' ? filteredMovements.length : checks.length)}</span> 条,共 <span className="font-medium">{
activeTab === 'alerts' ? filteredAlerts.length :
activeTab === 'movements' ? filteredMovements.length : checks.length}</span> 条记录
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{[...Array(totalPages)].map((_, index) => {
// 只显示当前页、首页、末页以及前后各一页
if (
index === 0 ||
index === totalPages - 1 ||
Math.abs(index - (currentPage - 1)) <= 1
) {
return (
<button
key={index}
onClick={() => handlePageChange(index + 1)}
className={`relative inline-flex items-center px-2 py-2 rounded-md text-sm font-medium ${
currentPage === index + 1
? 'bg-blue-50 dark:bg-blue-900 border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{index + 1}
</button>
);
}
// 添加省略号
if (
(index === 2 && currentPage > 4) ||
(index === totalPages - 3 && currentPage < totalPages - 3)
) {
return (
<span key={index} className="relative inline-flex items-center px-2 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-300">
...
</span>
);
}
return null;
})}
</nav>
</div>
</div>
</div>
) : null}
</div>
</div>
{/* 数据分析区域 */}
<div className="space-y-6">
{/* 库存分类统计 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5"
>
<h3 className="font-bold text-lg mb-4">库存分类统计</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={categoryStats}
margin={{ top: 5, right: 5, left: 5, bottom: 40 }}
>
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#374151' : '#e5e7eb'} />
<XAxis
dataKey="name"
stroke={theme === 'dark' ? '#9ca3af' : '#6b7280'}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis stroke={theme === 'dark' ? '#9ca3af' : '#6b7280'} />
<Tooltip
contentStyle={{
backgroundColor: theme === 'dark' ? '#1f2937' : '#ffffff',
border: `1px solid ${theme === 'dark' ? '#374151' : '#e5e7eb'}`,
color: theme === 'dark' ? '#ffffff' : '#000000'
}}
/>
<Legend />
<Bar dataKey="inStock" name="充足库存" stackId="a" fill="#10b981" />
<Bar dataKey="lowStock" name="库存不足" stackId="a" fill="#f59e0b" />
<Bar dataKey="outOfStock" name="缺货" stackId="a" fill="#ef4444" />
</BarChart>
</ResponsiveContainer>
</div>
</motion.div>
{/* 库存趋势图 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5"
>
<h3 className="font-bold text-lg mb-4">库存趋势</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={inventoryTrend}
margin={{ top: 5, right: 5, left: 5, bottom: 20 }}
>
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#374151' : '#e5e7eb'} />
<XAxis dataKey="month" stroke={theme === 'dark' ? '#9ca3af' : '#6b7280'} />
<YAxis yAxisId="left" stroke={theme === 'dark' ? '#9ca3af' : '#6b7280'} />
<YAxis yAxisId="right" orientation="right" stroke={theme === 'dark' ? '#9ca3af' : '#6b7280'} />
<Tooltip
contentStyle={{
backgroundColor: theme === 'dark' ? '#1f2937' : '#ffffff',
border: `1px solid ${theme === 'dark' ? '#374151' : '#e5e7eb'}`,
color: theme === 'dark' ? '#ffffff' : '#000000'
}}
/>
<Legend />
<Line
yAxisId="left"
type="monotone"
dataKey="inventory"
name="库存总量"
stroke="#3b82f6"
strokeWidth={2}
dot={{ r: 3 }}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="in"
name="入库量"
stroke="#10b981"
strokeWidth={2}
dot={{ r: 3 }}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="out"
name="出库量"
stroke="#ef4444"
strokeWidth={2}
dot={{ r: 3 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</motion.div>
{/* 库存状态饼图 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.4 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5"
>
<h3 className="font-bold text-lg mb-4">库存状态分布</h3>
<div className="h-64 flex justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={[
{ name: '充足库存', value: stats.inStockItems },
{ name: '库存不足', value: stats.lowStockItems },
{ name: '缺货', value: stats.outOfStockItems }
]}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
fill="#8884d8"
paddingAngle={2}
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
labelLine={false}
>
<Cell fill="#10b981" />
<Cell fill="#f59e0b" />
<Cell fill="#ef4444" />
</Pie>
<Tooltip
contentStyle={{
backgroundColor: theme === 'dark' ? '#1f2937' : '#ffffff',
border: `1px solid ${theme === 'dark' ? '#374151' : '#e5e7eb'}`,
color: theme === 'dark' ? '#ffffff' : '#000000'
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
</motion.div>
</div>
</div>
{/* 详情模态框 */}
<AnimatePresence>
{isDetailModalOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsDetailModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-lg max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{currentAlert ? (
<>
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">警报详情</h3>
<button onClick={() => setIsDetailModalOpen(false)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="p-5">
<div className="mb-6">
<div className="flex justify-center mb-4">
<div className="w-16 h-16 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center text-red-600 dark:text-red-400">
<AlertCircle size={24} />
</div>
</div>
<h4 className="text-xl font-bold text-center">{currentAlert.goodsName}</h4>
<p className="text-center text-gray-500 dark:text-gray-400">{currentAlert.id}</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">货物ID</p>
<p className="font-medium">{currentAlert.goodsId}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">货物分类</p>
<p className="font-medium">{currentAlert.category}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">当前库存</p>
<p className="font-medium">{currentAlert.currentStock}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">预警阈值</p>
<p className="font-medium">{currentAlert.threshold}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">存放位置</p>
<p className="font-medium">{currentAlert.location}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">警报类型</p>
<p className="font-medium">{renderAlertTypeBadge(currentAlert.alertType)}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">警报状态</p>
<p className="font-medium">{renderAlertStatusBadge(currentAlert.status)}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">警报时间</p>
<p className="font-medium">{currentAlert.alertTime}</p>
</div>
</div>
</div>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-end">
{currentAlert.status === 'pending' && (
<button
onClick={() => {
handleProcessAlert(currentAlert.id);
setIsDetailModalOpen(false);
}}
className="mr-3 px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg shadow-sm hover:bg-green-700 dark:hover:bg-green-800 transition-colors duration-200"
>
标记为已处理
</button>
)}
<button
onClick={() => setIsDetailModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
关闭
</button>
</div>
</>
) : currentMovement ? (
<>
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">移动记录详情</h3>
<button onClick={() => setIsDetailModalOpen(false)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="p-5">
<div className="mb-6">
<div className="flex justify-center mb-4">
<div className="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400"><MoveHorizontal size={24} />
</div>
</div>
<h4 className="text-xl font-bold text-center">{currentMovement.goodsName}</h4>
<p className="text-center text-gray-500 dark:text-gray-400">{currentMovement.id}</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">货物ID</p>
<p className="font-medium">{currentMovement.goodsId}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">移动类型</p>
<p className="font-medium">{renderMovementTypeBadge(currentMovement.movementType)}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">移动数量</p>
<p className="font-medium">{currentMovement.quantity}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">操作人</p>
<p className="font-medium">{currentMovement.operator}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">起始位置</p>
<p className="font-medium">{currentMovement.fromLocation}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">目标位置</p>
<p className="font-medium">{currentMovement.toLocation}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">移动时间</p>
<p className="font-medium">{currentMovement.movementTime}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">移动原因</p>
<p className="font-medium">{currentMovement.reason}</p>
</div>
</div>
</div>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<button
onClick={() => setIsDetailModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
关闭
</button>
</div>
</>
) : null}
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 创建盘点任务模态框 */}
<AnimatePresence>
{isCheckModalOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsCheckModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-md max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">新建盘点任务</h3>
<button onClick={() => setIsCheckModalOpen(false)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="p-5">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">盘点名称 <span className="text-red-500">*</span></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newCheck.name}
onChange={(e) => setNewCheck({ ...newCheck, name: e.target.value })}
placeholder="例如:月度盘点"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">开始时间 <span className="text-red-500">*</span></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newCheck.startTime}
onChange={(e) => setNewCheck({ ...newCheck, startTime: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">盘点人 <span className="text-red-500">*</span></label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newCheck.checkedBy}
onChange={(e) => setNewCheck({ ...newCheck, checkedBy: e.target.value })}
>
<option value="管理员">管理员</option>
<option value="操作员A">操作员A</option>
<option value="操作员B">操作员B</option>
<option value="操作员C">操作员C</option>
</select>
</div>
</div>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
<button
onClick={() => setIsCheckModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
取消
</button>
<button
onClick={handleCreateCheck}
className="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg shadow-sm hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors duration-200"
>
创建
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default InventoryManagement;
UserManagement.tsx #用户管理:
bash
复制代码
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Users, Search, Plus, Edit, Trash2, Eye, Filter, X,
CheckCircle, AlertCircle, ChevronDown, RefreshCw,
UserPlus, Lock, Unlock, UserX, UserCheck
} from 'lucide-react';
import { useTheme } from '@/hooks/useTheme';
import { toast } from 'sonner';
// 用户数据类型定义
interface User {
id: string;
username: string;
realName: string;
email: string;
phone: string;
role: 'admin' | 'operator' | 'viewer';
status: 'active' | 'inactive' | 'locked';
createdAt: string;
lastLogin: string;
permissions: {
canManageGoods: boolean;
canManageRobots: boolean;
canManageInventory: boolean;
canManageUsers: boolean;
canViewAnalytics: boolean;
canManageSettings: boolean;
};
}
// 模拟用户数据
const generateMockUsers = (): User[] => {
const roles: Array<'admin' | 'operator' | 'viewer'> = ['admin', 'operator', 'viewer'];
const statuses: Array<'active' | 'inactive' | 'locked'> = ['active', 'inactive', 'locked'];
return Array.from({ length: 20 }, (_, index) => {
const role = roles[Math.floor(Math.random() * roles.length)];
const status = statuses[Math.floor(Math.random() * statuses.length)];
const createDate = new Date(Date.now() - Math.floor(Math.random() * 365) * 24 * 60 * 60 * 1000);
const lastLoginDate = status === 'active'
? new Date(createDate.getTime() + Math.floor(Math.random() * 180) * 24 * 60 * 60 * 1000)
: null;
// 基于角色设置权限
const basePermissions = {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: false,
canManageSettings: false
};
let permissions = { ...basePermissions };
if (role === 'admin') {
permissions = {
canManageGoods: true,
canManageRobots: true,
canManageInventory: true,
canManageUsers: true,
canViewAnalytics: true,
canManageSettings: true
};
} else if (role === 'operator') {
permissions = {
canManageGoods: true,
canManageRobots: true,
canManageInventory: true,
canManageUsers: false,
canViewAnalytics: true,
canManageSettings: false
};
} else if (role === 'viewer') {
permissions = {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: true,
canManageSettings: false
};
}
return {
id: `USER-${String(index + 1).padStart(4, '0')}`,
username: `user${index + 1}`,
realName: `用户${index + 1}`,
email: `user${index + 1}@example.com`,
phone: `138${String(Math.floor(Math.random() * 100000000)).padStart(8, '0')}`,
role,
status,
createdAt: createDate.toLocaleDateString('zh-CN'),
lastLogin: lastLoginDate ? lastLoginDate.toLocaleString('zh-CN') : '-',
permissions
};
});
};
// 渲染角色标签
const renderRoleBadge = (role: string) => {
switch (role) {
case 'admin':
return <span className="px-2 py-1 text-xs rounded-full bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200">管理员</span>;
case 'operator':
return <span className="px-2 py-1 text-xs rounded-full bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">操作员</span>;
case 'viewer':
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">查看者</span>;
default:
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未知</span>;
}
};
// 渲染状态标签
const renderStatusBadge = (status: string) => {
switch (status) {
case 'active':
return <span className="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">活跃</span>;
case 'inactive':
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未激活</span>;
case 'locked':
return <span className="px-2 py-1 text-xs rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">已锁定</span>;
default:
return <span className="px-2 py-1 text-xs rounded-full bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">未知</span>;
}
};
// 渲染权限状态
const renderPermissionStatus = (hasPermission: boolean) => {
return hasPermission ? (
<CheckCircle size={18} className="text-green-500" />
) : (
<X size={18} className="text-red-500" />
);
};
const UserManagement: React.FC = () => {
const { theme } = useTheme();
const [users, setUsers] = useState<User[]>([]);
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedRole, setSelectedRole] = useState('all');
const [selectedStatus, setSelectedStatus] = useState('all');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [newUser, setNewUser] = useState<Partial<User>>({
username: '',
realName: '',
email: '',
phone: '',
role: 'viewer',
status: 'active',
permissions: {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: false,
canManageSettings: false
}
});
const [isLoading, setIsLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
// 加载用户数据
useEffect(() => {
// 模拟API请求延迟
const timer = setTimeout(() => {
const mockData = generateMockUsers();
setUsers(mockData);
setFilteredUsers(mockData);
setIsLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
// 搜索和筛选功能
useEffect(() => {
let result = [...users];
// 搜索筛选
if (searchTerm) {
result = result.filter(item =>
item.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.realName.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.email.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// 角色筛选
if (selectedRole !== 'all') {
result = result.filter(item => item.role === selectedRole);
}
// 状态筛选
if (selectedStatus !== 'all') {
result = result.filter(item => item.status === selectedStatus);
}
setFilteredUsers(result);
setCurrentPage(1); // 重置到第一页
}, [searchTerm, selectedRole, selectedStatus, users]);
// 分页功能
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredUsers.slice(indexOfFirstItem, indexOfLastItem);
const totalPages = Math.ceil(filteredUsers.length / itemsPerPage);
// 处理添加用户
const handleAddUser = () => {
if (!newUser.username || !newUser.realName || !newUser.email) {
toast.error('请填写必要的用户信息');
return;
}
const userToAdd: User = {
id: `USER-${String(users.length + 1).padStart(4, '0')}`,
username: newUser.username!,
realName: newUser.realName!,
email: newUser.email!,
phone: newUser.phone || '',
role: newUser.role || 'viewer',
status: newUser.status || 'active',
createdAt: new Date().toLocaleDateString('zh-CN'),
lastLogin: '-',
permissions: newUser.permissions || {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: false,
canManageSettings: false
}
};
setUsers([...users, userToAdd]);
setIsAddModalOpen(false);
setNewUser({
username: '',
realName: '',
email: '',
phone: '',
role: 'viewer',
status: 'active',
permissions: {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: false,
canManageSettings: false
}
});
toast.success('用户添加成功');
};
// 处理编辑用户
const handleEditUser = () => {
if (!currentUser) return;
const updatedUsers = users.map(user =>
user.id === currentUser.id ? currentUser : user
);
setUsers(updatedUsers);
setIsEditModalOpen(false);
setCurrentUser(null);
toast.success('用户信息更新成功');
};
// 处理删除用户
const handleDeleteUser = () => {
if (!currentUser) return;
const updatedUsers = users.filter(user => user.id !== currentUser.id);
setUsers(updatedUsers);
setIsDeleteModalOpen(false);
setCurrentUser(null);
toast.success('用户删除成功');
};
// 处理用户状态变更
const handleStatusChange = (userId: string, newStatus: 'active' | 'inactive' | 'locked') => {
const updatedUsers = users.map(user =>
user.id === userId ? { ...user, status: newStatus } : user
);
setUsers(updatedUsers);
toast.success('用户状态已更新');
};
// 打开编辑模态框
const openEditModal = (user: User) => {
setCurrentUser({ ...user });
setIsEditModalOpen(true);
};
// 打开详情模态框
const openDetailModal = (user: User) => {
setCurrentUser({ ...user });
setIsDetailModalOpen(true);
};
// 打开删除确认模态框
const openDeleteModal = (user: User) => {
setCurrentUser({ ...user });
setIsDeleteModalOpen(true);
};
// 刷新数据
const handleRefresh = () => {
setIsLoading(true);
// 模拟API请求延迟
const timer = setTimeout(() => {
const mockData = generateMockUsers();
setUsers(mockData);
setFilteredUsers(mockData);
setIsLoading(false);
toast.success('数据刷新成功');
}, 1000);
return () => clearTimeout(timer);
};
// 分页控制
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 根据角色更新权限
const handleRoleChange = (role: 'admin' | 'operator' | 'viewer') => {
if (isAddModalOpen) {
let newPermissions = {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: false,
canManageSettings: false
};
if (role === 'admin') {
newPermissions = {
canManageGoods: true,
canManageRobots: true,
canManageInventory: true,
canManageUsers: true,
canViewAnalytics: true,
canManageSettings: true
};
} else if (role === 'operator') {
newPermissions = {
canManageGoods: true,
canManageRobots: true,
canManageInventory: true,
canManageUsers: false,
canViewAnalytics: true,
canManageSettings: false};
} else if (role === 'viewer') {
newPermissions = {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: true,
canManageSettings: false
};
}
setNewUser({
...newUser,
role,
permissions: newPermissions
});
} else if (isEditModalOpen && currentUser) {
let newPermissions = {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: false,
canManageSettings: false
};
if (role === 'admin') {
newPermissions = {
canManageGoods: true,
canManageRobots: true,
canManageInventory: true,
canManageUsers: true,
canViewAnalytics: true,
canManageSettings: true
};
} else if (role === 'operator') {
newPermissions = {
canManageGoods: true,
canManageRobots: true,
canManageInventory: true,
canManageUsers: false,
canViewAnalytics: true,
canManageSettings: false
};
} else if (role === 'viewer') {
newPermissions = {
canManageGoods: false,
canManageRobots: false,
canManageInventory: false,
canManageUsers: false,
canViewAnalytics: true,
canManageSettings: false
};
}
setCurrentUser({
...currentUser,
role,
permissions: newPermissions
});
}
};
return (
<div className="p-4 md:p-6">
{/* 页面标题和操作区 */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
<div>
<h2 className="text-2xl font-bold">用户管理</h2>
<p className="text-gray-500 dark:text-gray-400">管理系统用户和权限</p>
</div>
<div className="flex space-x-3 mt-4 md:mt-0">
<button
onClick={handleRefresh}
className="flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
<RefreshCw size={16} className="mr-2" />
刷新
</button>
<button
onClick={() => setIsAddModalOpen(true)}
className="flex items-center px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg shadow-sm hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors duration-200"
>
<Plus size={16} className="mr-2" />
添加用户
</button>
</div>
</div>
{/* 搜索和筛选区 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4 mb-6">
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<div className="relative flex-1">
<Search size={18} className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="搜索用户ID、用户名、姓名或邮箱..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X size={16} />
</button>
)}
</div>
<div className="relative">
<button
onClick={() => setIsFilterDropdownOpen(!isFilterDropdownOpen)}
className="flex items-center px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
<Filter size={16} className="mr-2" />
筛选
<ChevronDown size={16} className={`ml-2 transition-transform duration-200 ${isFilterDropdownOpen ? 'transform rotate-180' : ''}`} />
</button>
<AnimatePresence>
{isFilterDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg z-10 p-4"
>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">用户角色</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)}
>
<option value="all">全部角色</option>
<option value="admin">管理员</option>
<option value="operator">操作员</option>
<option value="viewer">查看者</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">用户状态</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
>
<option value="all">全部状态</option>
<option value="active">活跃</option>
<option value="inactive">未激活</option>
<option value="locked">已锁定</option>
</select>
</div>
<div className="mt-4 flex justify-end">
<button
onClick={() => {
setSelectedRole('all');
setSelectedStatus('all');
setIsFilterDropdownOpen(false);
}}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
重置筛选
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
{/* 用户列表 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden mb-6">
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">用户列表</h3>
<div className="text-sm text-gray-500 dark:text-gray-400">
共 {filteredUsers.length} 个用户
</div>
</div>
{isLoading ? (
<div className="h-96 flex items-center justify-center">
<div className="flex flex-col items-center">
<RefreshCw size={32} className="text-blue-500 animate-spin" />
<p className="mt-2 text-gray-500 dark:text-gray-400">加载中...</p>
</div>
</div>
) : filteredUsers.length === 0 ? (
<div className="h-96 flex items-center justify-center">
<div className="text-center">
<Users size={48} className="mx-auto text-gray-300 dark:text-gray-600 mb-2" />
<p className="text-gray-500 dark:text-gray-400">没有找到匹配的用户</p>
<button
onClick={() => {
setSearchTerm('');
setSelectedRole('all');
setSelectedStatus('all');
}}
className="mt-2 text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
清除筛选条件
</button>
</div>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">用户名</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">姓名</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">邮箱</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">角色</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">创建日期</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">最后登录</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{currentItems.map((user) => (
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200">
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium">{user.id}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{user.username}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{user.realName}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{user.email}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{renderRoleBadge(user.role)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{renderStatusBadge(user.status)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{user.createdAt}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">{user.lastLogin}</td>
<td className="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<button
onClick={() => openDetailModal(user)}
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
<Eye size={16} />
</button>
<button
onClick={() => openEditModal(user)}
className="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300"
>
<Edit size={16} />
</button>
<button
onClick={() => openDeleteModal(user)}
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页控件 */}
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-gray-700">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className={`relative inline-flex items-center px-4 py-2 border rounded-md text-sm font-medium ${
currentPage === 1
? 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
上一页
</button>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className={`ml-3 relative inline-flex items-center px-4 py-2 border rounded-md text-sm font-medium ${
currentPage === totalPages
? 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
下一页
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700 dark:text-gray-300">
显示第 <span className="font-medium">{indexOfFirstItem + 1}</span> 到 <span className="font-medium">{Math.min(indexOfLastItem, filteredUsers.length)}</span> 条,共 <span className="font-medium">{filteredUsers.length}</span> 条记录
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{[...Array(totalPages)].map((_, index) => {
// 只显示当前页、首页、末页以及前后各一页
if (
index === 0 ||
index === totalPages - 1 ||
Math.abs(index - (currentPage - 1)) <= 1
) {
return (
<button
key={index}
onClick={() => handlePageChange(index + 1)}
className={`relative inline-flex items-center px-2 py-2 rounded-md text-sm font-medium ${
currentPage === index + 1
? 'bg-blue-50 dark:bg-blue-900 border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{index + 1}
</button>
);
}
// 添加省略号
if (
(index === 2 && currentPage > 4) ||
(index === totalPages - 3 && currentPage < totalPages - 3)
) {
return (
<span key={index} className="relative inline-flex items-center px-2 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-300">
...
</span>
);
}
return null;
})}
</nav>
</div>
</div>
</div>
</>
)}
</div>
{/* 添加用户模态框 */}
<AnimatePresence>
{isAddModalOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsAddModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">添加新用户</h3>
<button onClick={() => setIsAddModalOpen(false)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="p-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">用户名 <span className="text-red-500">*</span></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newUser.username || ''}
onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">姓名 <span className="text-red-500">*</span></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newUser.realName || ''}
onChange={(e) => setNewUser({ ...newUser, realName: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">邮箱 <span className="text-red-500">*</span></label>
<input
type="email"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newUser.email || ''}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">电话</label>
<input
type="tel"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newUser.phone || ''}
onChange={(e) => setNewUser({ ...newUser, phone: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">角色 <span className="text-red-500">*</span></label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newUser.role || 'viewer'}
onChange={(e) => handleRoleChange(e.target.value as 'admin' | 'operator' | 'viewer')}
>
<option value="admin">管理员</option>
<option value="operator">操作员</option>
<option value="viewer">查看者</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">状态</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={newUser.status || 'active'}
onChange={(e) => setNewUser({ ...newUser, status: e.target.value as 'active' | 'inactive' | 'locked' })}
>
<option value="active">活跃</option>
<option value="inactive">未激活</option>
<option value="locked">已锁定</option>
</select>
</div>
</div>
<div className="mt-6">
<label className="block text-sm font-medium mb-3">权限设置</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-center">
<input
type="checkbox"
id="canManageGoods"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={newUser.permissions?.canManageGoods || false}
onChange={(e) => setNewUser({
...newUser,
permissions: {
...(newUser.permissions || {}),
canManageGoods: e.target.checked
}
})}
/>
<label htmlFor="canManageGoods" className="ml-2 block text-sm">管理货物</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="canManageRobots"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={newUser.permissions?.canManageRobots || false}
onChange={(e) => setNewUser({
...newUser,
permissions: {
...(newUser.permissions || {}),
canManageRobots: e.target.checked
}
})}
/>
<label htmlFor="canManageRobots" className="ml-2 block text-sm">管理机器人</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="canManageInventory"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={newUser.permissions?.canManageInventory || false}
onChange={(e) => setNewUser({
...newUser,
permissions: {
...(newUser.permissions || {}),
canManageInventory: e.target.checked
}
})}
/>
<label htmlFor="canManageInventory" className="ml-2 block text-sm">管理库存</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="canManageUsers"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={newUser.permissions?.canManageUsers || false}
onChange={(e) => setNewUser({
...newUser,
permissions: {
...(newUser.permissions || {}),
canManageUsers: e.target.checked
}
})}
/>
<label htmlFor="canManageUsers" className="ml-2 block text-sm">管理用户</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="canViewAnalytics"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={newUser.permissions?.canViewAnalytics || false}
onChange={(e) => setNewUser({
...newUser,
permissions: {
...(newUser.permissions || {}),
canViewAnalytics: e.target.checked
}
})}
/>
<label htmlFor="canViewAnalytics" className="ml-2 block text-sm">查看分析</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="canManageSettings"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={newUser.permissions?.canManageSettings || false}
onChange={(e) => setNewUser({
...newUser,
permissions: {
...(newUser.permissions || {}),
canManageSettings: e.target.checked
}
})}
/>
<label htmlFor="canManageSettings" className="ml-2 block text-sm">管理设置</label>
</div>
</div>
</div>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
<button
onClick={() => setIsAddModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
取消
</button>
<button
onClick={handleAddUser}
className="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg shadow-sm hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors duration-200"
>
添加
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 编辑用户模态框 */}
<AnimatePresence>
{isEditModalOpen && currentUser && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsEditModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">编辑用户</h3>
<button onClick={() => setIsEditModalOpen(false)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="p-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">用户ID</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-100 dark:bg-gray-900 text-gray-500 dark:text-gray-400 cursor-not-allowed"
value={currentUser.id}
disabled
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">用户名 <span className="text-red-500">*</span></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={currentUser.username}
onChange={(e) => setCurrentUser({ ...currentUser, username: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">姓名 <span className="text-red-500">*</span></label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={currentUser.realName}
onChange={(e) => setCurrentUser({ ...currentUser, realName: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">邮箱 <span className="text-red-500">*</span></label>
<input
type="email"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={currentUser.email}
onChange={(e) => setCurrentUser({ ...currentUser, email: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">电话</label>
<input
type="tel"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={currentUser.phone}
onChange={(e) => setCurrentUser({ ...currentUser, phone: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">角色 <span className="text-red-500">*</span></label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={currentUser.role}
onChange={(e) => handleRoleChange(e.target.value as 'admin' | 'operator' | 'viewer')}
>
<option value="admin">管理员</option>
<option value="operator">操作员</option>
<option value="viewer">查看者</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">状态</label>
<select
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={currentUser.status}
onChange={(e) => setCurrentUser({ ...currentUser, status: e.target.value as 'active' | 'inactive' | 'locked' })}
>
<option value="active">活跃</option>
<option value="inactive">未激活</option>
<option value="locked">已锁定</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">创建日期</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-100 dark:bg-gray-900 text-gray-500 dark:text-gray-400 cursor-not-allowed"
value={currentUser.createdAt}
disabled
/>
</div>
</div>
<div className="mt-6">
<label className="block text-sm font-medium mb-3">权限设置</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex items-center">
<input
type="checkbox"
id="edit-canManageGoods"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={currentUser.permissions.canManageGoods}
onChange={(e) => setCurrentUser({
...currentUser,
permissions: {
...currentUser.permissions,
canManageGoods: e.target.checked
}
})}
/>
<label htmlFor="edit-canManageGoods" className="ml-2 block text-sm">管理货物</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="edit-canManageRobots"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={currentUser.permissions.canManageRobots}
onChange={(e) => setCurrentUser({
...currentUser,
permissions: {
...currentUser.permissions,
canManageRobots: e.target.checked
}
})}
/>
<label htmlFor="edit-canManageRobots" className="ml-2 block text-sm">管理机器人</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="edit-canManageInventory"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={currentUser.permissions.canManageInventory}
onChange={(e) => setCurrentUser({
...currentUser,
permissions: {
...currentUser.permissions,
canManageInventory: e.target.checked
}
})}
/>
<label htmlFor="edit-canManageInventory" className="ml-2 block text-sm">管理库存</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="edit-canManageUsers"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={currentUser.permissions.canManageUsers}
onChange={(e) => setCurrentUser({
...currentUser,
permissions: {
...currentUser.permissions,
canManageUsers: e.target.checked
}
})}
/>
<label htmlFor="edit-canManageUsers" className="ml-2 block text-sm">管理用户</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="edit-canViewAnalytics"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={currentUser.permissions.canViewAnalytics}
onChange={(e) => setCurrentUser({
...currentUser,
permissions: {
...currentUser.permissions,
canViewAnalytics: e.target.checked
}
})}
/>
<label htmlFor="edit-canViewAnalytics" className="ml-2 block text-sm">查看分析</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="edit-canManageSettings"
className="w-4 h-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-700 rounded"
checked={currentUser.permissions.canManageSettings}
onChange={(e) => setCurrentUser({
...currentUser,
permissions: {
...currentUser.permissions,
canManageSettings: e.target.checked
}
})}
/>
<label htmlFor="edit-canManageSettings" className="ml-2 block text-sm">管理设置</label>
</div>
</div>
</div>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
<button
onClick={() => setIsEditModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
取消
</button>
<button
onClick={handleEditUser}
className="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg shadow-sm hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors duration-200"
>
保存
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 用户详情模态框 */}
<AnimatePresence>
{isDetailModalOpen && currentUser && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsDetailModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-xl max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="font-bold text-lg">用户详情</h3>
<button onClick={() => setIsDetailModalOpen(false)} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="p-5">
<div className="mb-6">
<div className="flex justify-center mb-4">
<div className="w-20 h-20 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400">
<Users size={32} />
</div>
</div>
<h4 className="text-xl font-bold text-center">{currentUser.realName}</h4>
<p className="text-center text-gray-500 dark:text-gray-400">{currentUser.username}</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">用户ID</p>
<p className="font-medium">{currentUser.id}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">邮箱</p>
<p className="font-medium">{currentUser.email}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">电话</p>
<p className="font-medium">{currentUser.phone || '-'}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">角色</p>
<p className="font-medium">{renderRoleBadge(currentUser.role)}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">状态</p>
<p className="font-medium">{renderStatusBadge(currentUser.status)}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
<p className="text-sm text-gray-500 dark:text-gray-400">创建日期</p>
<p className="font-medium">{currentUser.createdAt}</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg md:col-span-2">
<p className="text-sm text-gray-500 dark:text-gray-400">最后登录</p>
<p className="font-medium">{currentUser.lastLogin}</p>
</div>
</div>
<div className="mt-6">
<h4 className="font-medium mb-3">权限信息</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<span className="text-sm">管理货物</span>
{renderPermissionStatus(currentUser.permissions.canManageGoods)}
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<span className="text-sm">管理机器人</span>
{renderPermissionStatus(currentUser.permissions.canManageRobots)}
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<span className="text-sm">管理库存</span>
{renderPermissionStatus(currentUser.permissions.canManageInventory)}
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<span className="text-sm">管理用户</span>
{renderPermissionStatus(currentUser.permissions.canManageUsers)}
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<span className="text-sm">查看分析</span>
{renderPermissionStatus(currentUser.permissions.canViewAnalytics)}
</div>
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<span className="text-sm">管理设置</span>
{renderPermissionStatus(currentUser.permissions.canManageSettings)}
</div>
</div>
</div>
</div>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
{currentUser.status === 'active' && (
<button
onClick={() => {
handleStatusChange(currentUser.id, 'locked');
setIsDetailModalOpen(false);
}}
className="px-4 py-2 border border-red-300 dark:border-red-700 rounded-lg bg-white dark:bg-gray-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors duration-200"
>
<Lock size={16} className="inline-block mr-2" /> 锁定用户
</button>
)}
{currentUser.status === 'locked' && (
<button
onClick={() => {
handleStatusChange(currentUser.id, 'active');
setIsDetailModalOpen(false);
}}
className="px-4 py-2 border border-green-300 dark:border-green-700 rounded-lg bg-white dark:bg-gray-800 text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/30 transition-colors duration-200"
>
<Unlock size={16} className="inline-block mr-2" /> 解锁用户
</button>
)}
<button
onClick={() => setIsDetailModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
关闭
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 删除确认模态框 */}
<AnimatePresence>
{isDeleteModalOpen && currentUser && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsDeleteModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-md"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 text-center">
<div className="flex justify-center mb-4">
<div className="w-16 h-16 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center text-red-600 dark:text-red-400">
<AlertCircle size={24} />
</div>
</div>
<h3 className="font-bold text-lg mb-2">确认删除</h3>
<p className="text-gray-500 dark:text-gray-400">
您确定要删除用户 <span className="font-medium">{currentUser.realName} ({currentUser.username})</span> 吗?此操作无法撤销。
</p>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-center space-x-3">
<button
onClick={() => setIsDeleteModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
取消
</button>
<button
onClick={handleDeleteUser}
className="px-4 py-2 bg-red-600 dark:bg-red-700 text-white rounded-lg shadow-sm hover:bg-red-700 dark:hover:bg-red-800 transition-colors duration-200"
>
删除
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default UserManagement;
SystemSettings.tsx #系统设置:
bash
复制代码
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Settings, Save, RefreshCw, Database, Server, Bell,
User, Lock, Palette, Info, HelpCircle, LogOut,
ChevronDown, ToggleLeft, ToggleRight, FileText,
Activity, AlertTriangle, Eye, X
} from 'lucide-react';
import { useTheme } from '@/hooks/useTheme';
import { toast } from 'sonner';
// 系统设置类型定义
interface SystemSettingsType {
general: {
siteName: string;
defaultTheme: 'light' | 'dark' | 'system';
language: string;
dateFormat: string;
timeFormat: string;
};
notifications: {
inventoryAlerts: boolean;
robotStatusAlerts: boolean;
systemUpdates: boolean;
emailNotifications: boolean;
smsNotifications: boolean;
};
security: {
twoFactorAuth: boolean;
passwordPolicy: {
minLength: number;
requireUppercase: boolean;
requireLowercase: boolean;
requireNumbers: boolean;
requireSpecialChars: boolean;
passwordExpiryDays: number;
};
sessionTimeout: number;
};
backup: {
autoBackup: boolean;
backupFrequency: 'daily' | 'weekly' | 'monthly';
backupTime: string;
backupRetentionDays: number;
lastBackup: string;
nextBackup: string;
};
warehouse: {
layout: string;
zones: number;
aisles: number;
rowsPerAisle: number;
shelvesPerRow: number;
};
}
// 模拟系统设置数据
const generateMockSettings = (): SystemSettingsType => {
const now = new Date();
const lastBackupDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 昨天
const nextBackupDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 明天
return {
general: {
siteName: '智能无人仓库管理系统',
defaultTheme: 'system',
language: '中文',
dateFormat: 'YYYY-MM-DD',
timeFormat: '24小时制'
},
notifications: {
inventoryAlerts: true,
robotStatusAlerts: true,
systemUpdates: true,
emailNotifications: true,
smsNotifications: false
},
security: {
twoFactorAuth: false,
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: false,
passwordExpiryDays: 90
},
sessionTimeout: 30
},
backup: {
autoBackup: true,
backupFrequency: 'daily',
backupTime: '02:00',
backupRetentionDays: 30,
lastBackup: lastBackupDate.toLocaleString('zh-CN'),
nextBackup: nextBackupDate.toLocaleString('zh-CN')
},
warehouse: {
layout: '标准布局',
zones: 5,
aisles: 20,
rowsPerAisle: 8,
shelvesPerRow: 12
}
};
};
const SystemSettings: React.FC = () => {
const { theme } = useTheme();
const [settings, setSettings] = useState<SystemSettingsType>(generateMockSettings());
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'general' | 'notifications' | 'security' | 'backup' | 'warehouse'>('general');
const [isEditMode, setIsEditMode] = useState(false);
const [originalSettings, setOriginalSettings] = useState<SystemSettingsType>(generateMockSettings());
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
// 加载设置数据
useEffect(() => {
// 模拟API请求延迟
const timer = setTimeout(() => {
const mockData = generateMockSettings();
setSettings(mockData);
setOriginalSettings(mockData);
setIsLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
// 刷新数据
const handleRefresh = () => {
setIsLoading(true);
// 模拟API请求延迟
const timer = setTimeout(() => {
const mockData = generateMockSettings();
setSettings(mockData);
setOriginalSettings(mockData);
setIsEditMode(false);
setIsLoading(false);
toast.success('设置已刷新');
}, 1000);
return () => clearTimeout(timer);
};
// 开始编辑
const handleStartEdit = () => {
setIsEditMode(true);
};
// 取消编辑
const handleCancelEdit = () => {
setSettings({ ...originalSettings });
setIsEditMode(false);
};
// 保存设置
const handleSaveSettings = () => {
setOriginalSettings({ ...settings });
setIsEditMode(false);
toast.success('设置已保存');
// 更新备份日期信息
if (activeTab === 'backup') {
const now = new Date();
const nextBackupDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 明天
setSettings(prev => ({
...prev,
backup: {
...prev.backup,
nextBackup: nextBackupDate.toLocaleString('zh-CN')
}
}));
}
};
// 重置所有设置
const handleResetSettings = () => {
setIsConfirmModalOpen(true);
};
// 确认重置设置
const confirmResetSettings = () => {
const defaultSettings = generateMockSettings();
setSettings(defaultSettings);
setOriginalSettings(defaultSettings);
setIsEditMode(false);
setIsConfirmModalOpen(false);
toast.success('所有设置已重置为默认值');
};
// 切换开关状态
const toggleSetting = (path: string) => {
setSettings(prev => {
const newSettings = { ...prev };
// 简单路径解析,实际项目中可能需要更复杂的处理
const parts = path.split('.');
if (parts.length === 2) {
newSettings[parts[0]][parts[1]] = !newSettings[parts[0]][parts[1]];
} else if (parts.length === 3) {
newSettings[parts[0]][parts[1]][parts[2]] = !newSettings[parts[0]][parts[1]][parts[2]];
}
return newSettings;
});
};
// 更新数值设置
const updateNumberSetting = (path: string, value: number) => {
setSettings(prev => {
const newSettings = { ...prev };
const parts = path.split('.');
if (parts.length === 2) {
newSettings[parts[0]][parts[1]] = value;
} else if (parts.length === 3) {
newSettings[parts[0]][parts[1]][parts[2]] = value;
}
return newSettings;
});
};
// 更新文本设置
const updateTextSetting = (path: string, value: string) => {
setSettings(prev => {
const newSettings = { ...prev };
const parts = path.split('.');
if (parts.length === 2) {
newSettings[parts[0]][parts[1]] = value;
} else if (parts.length === 3) {
newSettings[parts[0]][parts[1]][parts[2]] = value;
}
return newSettings;
});
};
return (
<div className="p-4 md:p-6">
{/* 页面标题和操作区 */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
<div>
<h2 className="text-2xl font-bold">系统设置</h2>
<p className="text-gray-500 dark:text-gray-400">配置系统的各项参数和选项</p>
</div>
<div className="flex space-x-3 mt-4 md:mt-0">
<button
onClick={handleRefresh}
className="flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
<RefreshCw size={16} className="mr-2" />
刷新
</button>
{!isEditMode ? (
<button
onClick={handleStartEdit}
className="flex items-center px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg shadow-sm hover:bg-blue-700 dark:hover:bg-blue-800 transition-colors duration-200"
>
<Settings size={16} className="mr-2" />
编辑设置
</button>
) : (
<>
<button
onClick={handleCancelEdit}
className="flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
<X size={16} className="mr-2" />
取消
</button>
<button
onClick={handleSaveSettings}
className="flex items-center px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg shadow-sm hover:bg-green-700 dark:hover:bg-green-800 transition-colors duration-200"
>
<Save size={16} className="mr-2" />
保存设置
</button>
</>
)}
</div>
</div>
{/* 标签切换 */}
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-6 overflow-x-auto">
<button
onClick={() => setActiveTab('general')}
className={`px-4 py-3 font-medium text-sm border-b-2 whitespace-nowrap ${
activeTab === 'general'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<Settings size={16} className="inline-block mr-2" />
基本设置
</button>
<button
onClick={() => setActiveTab('notifications')}
className={`px-4 py-3 font-medium text-sm border-b-2 whitespace-nowrap ${
activeTab === 'notifications'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<Bell size={16} className="inline-block mr-2" />
通知设置
</button>
<button
onClick={() => setActiveTab('security')}
className={`px-4 py-3 font-medium text-sm border-b-2 whitespace-nowrap ${
activeTab === 'security'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<Lock size={16} className="inline-block mr-2" />
安全设置
</button>
<button
onClick={() => setActiveTab('backup')}
className={`px-4 py-3 font-medium text-sm border-b-2 whitespace-nowrap ${
activeTab === 'backup'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<Database size={16} className="inline-block mr-2" />
备份设置
</button>
<button
onClick={() => setActiveTab('warehouse')}
className={`px-4 py-3 font-medium text-sm border-b-2 whitespace-nowrap ${
activeTab === 'warehouse'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<Server size={16} className="inline-block mr-2" />
仓库设置
</button>
</div>
{/* 主要内容区域 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-5 mb-6">
{isLoading ? (
<div className="h-96 flex items-center justify-center">
<div className="flex flex-col items-center">
<RefreshCw size={32} className="text-blue-500 animate-spin" />
<p className="mt-2 text-gray-500 dark:text-gray-400">加载设置中...</p>
</div>
</div>
) : (
<>
{/* 基本设置 */}
{activeTab === 'general' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<h3 className="text-lg font-medium mb-4">基本设置</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium mb-2">系统名称</label>
<input
type="text"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.general.siteName}
onChange={(e) => updateTextSetting('general.siteName', e.target.value)}
disabled={!isEditMode}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">默认主题</label>
<select
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.general.defaultTheme}
onChange={(e) => updateTextSetting('general.defaultTheme', e.target.value)}
disabled={!isEditMode}
>
<option value="light">浅色模式</option>
<option value="dark">深色模式</option>
<option value="system">跟随系统</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">语言</label>
<select className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.general.language}
onChange={(e) => updateTextSetting('general.language', e.target.value)}
disabled={!isEditMode}
>
<option value="中文">中文</option>
<option value="English">English</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">日期格式</label>
<select
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.general.dateFormat}
onChange={(e) => updateTextSetting('general.dateFormat', e.target.value)}
disabled={!isEditMode}
>
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">时间格式</label>
<select
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.general.timeFormat}
onChange={(e) => updateTextSetting('general.timeFormat', e.target.value)}
disabled={!isEditMode}
>
<option value="24小时制">24小时制</option>
<option value="12小时制">12小时制</option>
</select>
</div>
</div>
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-lg border border-blue-100 dark:border-blue-800">
<div className="flex">
<Info size={20} className="text-blue-600 dark:text-blue-400 mr-3 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-700 dark:text-blue-300">
基本设置将影响整个系统的外观和行为。更改系统名称、默认主题和语言等设置后,所有用户都会受到影响。
</p>
</div>
</div>
</motion.div>
)}
{/* 通知设置 */}
{activeTab === 'notifications' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<h3 className="text-lg font-medium mb-4">通知设置</h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">库存警报</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">当库存水平达到预警阈值时发送通知</p>
</div>
<button
onClick={() => toggleSetting('notifications.inventoryAlerts')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.notifications.inventoryAlerts ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">机器人状态警报</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">当机器人出现故障或需要维护时发送通知</p>
</div>
<button
onClick={() => toggleSetting('notifications.robotStatusAlerts')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.notifications.robotStatusAlerts ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">系统更新通知</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">当系统有新的更新可用时发送通知</p>
</div>
<button
onClick={() => toggleSetting('notifications.systemUpdates')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.notifications.systemUpdates ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">电子邮件通知</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">通过电子邮件发送通知</p>
</div>
<button
onClick={() => toggleSetting('notifications.emailNotifications')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.notifications.emailNotifications ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">短信通知</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">通过短信发送重要通知</p>
</div>
<button
onClick={() => toggleSetting('notifications.smsNotifications')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.notifications.smsNotifications ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
</div>
<div className="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/30 rounded-lg border border-yellow-100 dark:border-yellow-800">
<div className="flex">
<AlertTriangle size={20} className="text-yellow-600 dark:text-yellow-400 mr-3 flex-shrink-0 mt-0.5" />
<p className="text-sm text-yellow-700 dark:text-yellow-300">
请注意,短信通知可能产生额外费用。建议仅为重要警报启用短信通知。
</p>
</div>
</div>
</motion.div>
)}
{/* 安全设置 */}
{activeTab === 'security' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<h3 className="text-lg font-medium mb-4">安全设置</h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">两步验证</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">登录时需要额外的验证步骤</p>
</div>
<button
onClick={() => toggleSetting('security.twoFactorAuth')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.security.twoFactorAuth ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 className="font-medium mb-3">密码策略</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">最小长度</label>
<input
type="number"
min="6"
max="32"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.security.passwordPolicy.minLength}
onChange={(e) => updateNumberSetting('security.passwordPolicy.minLength', parseInt(e.target.value) || 8)}
disabled={!isEditMode}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">密码过期天数</label>
<input
type="number"
min="0"
max="365"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.security.passwordPolicy.passwordExpiryDays}
onChange={(e) => updateNumberSetting('security.passwordPolicy.passwordExpiryDays', parseInt(e.target.value) || 90)}
disabled={!isEditMode}
/>
</div>
<div className="flex items-center justify-between">
<label className="block text-sm font-medium">需要大写字母</label>
<button
onClick={() => toggleSetting('security.passwordPolicy.requireUppercase')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.security.passwordPolicy.requireUppercase ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between">
<label className="block text-sm font-medium">需要小写字母</label>
<button
onClick={() => toggleSetting('security.passwordPolicy.requireLowercase')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.security.passwordPolicy.requireLowercase ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between">
<label className="block text-sm font-medium">需要数字</label>
<button
onClick={() => toggleSetting('security.passwordPolicy.requireNumbers')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.security.passwordPolicy.requireNumbers ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="flex items-center justify-between">
<label className="block text-sm font-medium">需要特殊字符</label>
<button
onClick={() => toggleSetting('security.passwordPolicy.requireSpecialChars')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.security.passwordPolicy.requireSpecialChars ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 className="font-medium mb-3">会话设置</h4>
<div>
<label className="block text-sm font-medium mb-2">会话超时时间(分钟)</label>
<input
type="number"
min="5"
max="120"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.security.sessionTimeout}
onChange={(e) => updateNumberSetting('security.sessionTimeout', parseInt(e.target.value) || 30)}
disabled={!isEditMode}
/>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">用户在指定时间内无操作将自动登出</p>
</div>
</div>
</div>
<div className="mt-6 p-4 bg-red-50 dark:bg-red-900/30 rounded-lg border border-red-100 dark:border-red-800">
<div className="flex">
<Lock size={20} className="text-red-600 dark:text-red-400 mr-3 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700 dark:text-red-300">
安全设置对系统安全性至关重要。建议启用两步验证并设置强密码策略以保护系统免受未授权访问。
</p>
</div>
</div>
</motion.div>
)}
{/* 备份设置 */}
{activeTab === 'backup' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<h3 className="text-lg font-medium mb-4">备份设置</h3>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div>
<h4 className="font-medium">自动备份</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">定期自动备份系统数据</p>
</div>
<button
onClick={() => toggleSetting('backup.autoBackup')}
disabled={!isEditMode}
className={`p-1 rounded-full ${isEditMode ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'}`}
>
{settings.backup.autoBackup ? (
<ToggleRight size={24} className="text-green-500" />
) : (
<ToggleLeft size={24} className="text-gray-400" />
)}
</button>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 className="font-medium mb-3">备份计划</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">备份频率</label>
<select
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.backup.backupFrequency}
onChange={(e) => updateTextSetting('backup.backupFrequency', e.target.value)}
disabled={!isEditMode}
>
<option value="daily">每日</option>
<option value="weekly">每周</option>
<option value="monthly">每月</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">备份时间</label>
<input
type="time"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.backup.backupTime}
onChange={(e) => updateTextSetting('backup.backupTime', e.target.value)}
disabled={!isEditMode}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">备份保留天数</label>
<input
type="number"
min="1"
max="365"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.backup.backupRetentionDays}
onChange={(e) => updateNumberSetting('backup.backupRetentionDays', parseInt(e.target.value) || 30)}
disabled={!isEditMode}
/>
</div>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<h4 className="font-medium mb-3">备份状态</h4>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">上次备份</label>
<p className="text-sm mt-1">{settings.backup.lastBackup}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">下次备份</label>
<p className="text-sm mt-1">{settings.backup.nextBackup}</p>
</div>
</div>
</div>
</div>
<div className="mt-6 p-4 bg-green-50 dark:bg-green-900/30 rounded-lg border border-green-100 dark:border-green-800">
<div className="flex">
<Database size={20} className="text-green-600 dark:text-green-400 mr-3 flex-shrink-0 mt-0.5" />
<p className="text-sm text-green-700 dark:text-green-300">
定期备份是数据安全的重要保障。建议设置合理的备份频率和保留时间,确保数据可以在意外情况下恢复。
</p>
</div>
</div>
</motion.div>
)}
{/* 仓库设置 */}
{activeTab === 'warehouse' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<h3 className="text-lg font-medium mb-4">仓库设置</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">仓库布局</label>
<select
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.warehouse.layout}
onChange={(e) => updateTextSetting('warehouse.layout', e.target.value)}
disabled={!isEditMode}
>
<option value="标准布局">标准布局</option>
<option value="优化布局">优化布局</option>
<option value="自定义布局">自定义布局</option>
</select>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium mb-2">区域数量</label>
<input
type="number"
min="1"
max="20"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.warehouse.zones}
onChange={(e) => updateNumberSetting('warehouse.zones', parseInt(e.target.value) || 5)}
disabled={!isEditMode}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">通道数量</label>
<input
type="number"
min="1"
max="50"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.warehouse.aisles}
onChange={(e) => updateNumberSetting('warehouse.aisles', parseInt(e.target.value) || 20)}
disabled={!isEditMode}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">每通道行数</label>
<input
type="number"
min="1"
max="20"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.warehouse.rowsPerAisle}
onChange={(e) => updateNumberSetting('warehouse.rowsPerAisle', parseInt(e.target.value) || 8)}
disabled={!isEditMode}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">每行货架数</label>
<input
type="number"
min="1"
max="30"
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 ${isEditMode ? '' : 'cursor-not-allowed'}`}
value={settings.warehouse.shelvesPerRow}
onChange={(e) => updateNumberSetting('warehouse.shelvesPerRow', parseInt(e.target.value) || 12)}
disabled={!isEditMode}
/>
</div>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<h4 className="font-medium mb-3">仓库容量计算</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">总货架数量</label>
<p className="text-sm font-medium mt-1">
{settings.warehouse.zones * settings.warehouse.aisles * settings.warehouse.rowsPerAisle * settings.warehouse.shelvesPerRow}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 dark:text-gray-400">估算容量</label>
<p className="text-sm font-medium mt-1">
{settings.warehouse.zones * settings.warehouse.aisles * settings.warehouse.rowsPerAisle * settings.warehouse.shelvesPerRow * 50} 个货物单元
</p>
</div>
</div>
</div>
</div>
<div className="mt-6 p-4 bg-orange-50 dark:bg-orange-900/30 rounded-lg border border-orange-100 dark:border-orange-800">
<div className="flex">
<Activity size={20} className="text-orange-600 dark:text-orange-400 mr-3 flex-shrink-0 mt-0.5" />
<p className="text-sm text-orange-700 dark:text-orange-300">
仓库设置会影响机器人路径规划和货物存放策略。更改这些设置后,建议重新校准机器人系统以获得最佳性能。
</p>
</div>
</div>
</motion.div>
)}
{/* 底部操作按钮 */}
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<button
onClick={handleResetSettings}
className="px-4 py-2 border border-red-300 dark:border-red-700 rounded-lg bg-white dark:bg-gray-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors duration-200"
>
重置为默认设置
</button>
</div>
</>
)}
</div>
{/* 系统信息卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4"
>
<div className="flex items-center mb-2">
<Info size={18} className="mr-2 text-blue-500" />
<h3 className="font-medium">系统版本</h3>
</div>
<p className="text-sm">v1.2.0</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">最后更新: 2025-09-15</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4"
>
<div className="flex items-center mb-2">
<FileText size={18} className="mr-2 text-green-500" />
<h3 className="font-medium">文档</h3>
</div>
<a href="#" className="text-sm text-blue-600 dark:text-blue-400 hover:underline">用户手册</a>
<br />
<a href="#" className="text-sm text-blue-600 dark:text-blue-400 hover:underline mt-1">API文档</a>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4"
>
<div className="flex items-center mb-2">
<HelpCircle size={18} className="mr-2 text-purple-500" />
<h3 className="font-medium">支持</h3>
</div>
<p className="text-sm">support@example.com</p>
<p className="text-sm mt-1">+86 400-123-4567</p>
</motion.div>
</div>
{/* 确认重置模态框 */}
<AnimatePresence>
{isConfirmModalOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={() => setIsConfirmModalOpen(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-lg w-full max-w-md"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 text-center">
<div className="flex justify-center mb-4">
<div className="w-16 h-16 rounded-full bg-red-100 dark:bg-red-900 flex items-center justify-center text-red-600 dark:text-red-400">
<AlertTriangle size={24} />
</div>
</div>
<h3 className="font-bold text-lg mb-2">确认重置</h3>
<p className="text-gray-500 dark:text-gray-400">
您确定要将所有系统设置重置为默认值吗?此操作无法撤销,会影响所有用户的使用体验。
</p>
</div>
<div className="p-5 border-t border-gray-200 dark:border-gray-700 flex justify-center space-x-3">
<button
onClick={() => setIsConfirmModalOpen(false)}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
>
取消
</button>
<button
onClick={confirmResetSettings}
className="px-4 py-2 bg-red-600 dark:bg-red-700 text-white rounded-lg shadow-sm hover:bg-red-700 dark:hover:bg-red-800 transition-colors duration-200"
>
确认重置
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default SystemSettings;