子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
'use client';
import React from 'react';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Search } from 'lucide-react';
interface CategoryFiltersProps {
searchKeyword: string;
typeFilter: string;
onSearchChange: (value: string) => void;
onTypeFilterChange: (value: string) => void;
}
export function CategoryFilters({
searchKeyword,
typeFilter,
onSearchChange,
onTypeFilterChange,
}: CategoryFiltersProps) {
return (
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索分类名称、编码..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={typeFilter} onValueChange={onTypeFilterChange}>
<SelectTrigger>
<SelectValue placeholder="分类类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="industry"></SelectItem>
<SelectItem value="equipment"></SelectItem>
<SelectItem value="crop"></SelectItem>
<SelectItem value="operation"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
</Card>
);
}

View File

@@ -0,0 +1,137 @@
'use client';
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { CategoryDictionary, CategoryFormData } from '../types';
interface CategoryFormDialogProps {
open: boolean;
editing?: CategoryDictionary;
parent?: CategoryDictionary | null;
formData: CategoryFormData;
onOpenChange: (open: boolean) => void;
onFormDataChange: (data: Partial<CategoryFormData>) => void;
onSave: () => void;
}
export function CategoryFormDialog({
open,
editing,
parent,
formData,
onOpenChange,
onFormDataChange,
onSave,
}: CategoryFormDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
{editing ? '编辑分类' : '新增分类'}
</DialogTitle>
<DialogDescription className="sr-only">
{editing ? '编辑分类信息' : '添加新分类'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{parent && (
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<Label className="text-sm text-blue-900 dark:text-blue-100"></Label>
<p className="mt-1 dark:text-gray-100">{parent.name}</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="code"> *</Label>
<Input
id="code"
value={formData.code}
onChange={(e) => onFormDataChange({ code: e.target.value })}
placeholder="IND001"
/>
</div>
<div>
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => onFormDataChange({ name: e.target.value })}
placeholder="请输入名称"
/>
</div>
</div>
<div>
<Label htmlFor="type"></Label>
<Select
value={formData.type}
onValueChange={(value) => onFormDataChange({ type: value })}
disabled={!!parent}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="industry"></SelectItem>
<SelectItem value="equipment"></SelectItem>
<SelectItem value="crop"></SelectItem>
<SelectItem value="operation"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => onFormDataChange({ description: e.target.value })}
placeholder="请输入描述"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="sortOrder"></Label>
<Input
id="sortOrder"
type="number"
value={formData.sortOrder}
onChange={(e) => onFormDataChange({ sortOrder: parseInt(e.target.value) || 0 })}
/>
</div>
<div className="flex items-center justify-between pt-6">
<Label htmlFor="isActive"></Label>
<Switch
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => onFormDataChange({ isActive: checked })}
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={onSave}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import React from 'react';
import { Card } from '@/components/ui/card';
import { FolderTree } from 'lucide-react';
export function CategoryInstructions() {
return (
<Card className="p-4 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<h4 className="text-blue-900 dark:text-blue-100 mb-2">
<FolderTree className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800 dark:text-blue-200">
<li> </li>
<li> /</li>
<li> </li>
<li> </li>
<li> 使 IND001-01</li>
</ul>
</Card>
);
}

View File

@@ -0,0 +1,114 @@
'use client';
import React from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
ChevronRight,
ChevronDown,
Folder,
File,
Plus,
Edit,
Trash2
} from 'lucide-react';
import { CategoryDictionary } from '../types';
interface CategoryTreeProps {
categories: CategoryDictionary[];
expandedIds: Set<string>;
onToggleExpand: (id: string) => void;
onAdd: (parent?: CategoryDictionary) => void;
onEdit: (category: CategoryDictionary) => void;
onDelete: (id: string) => void;
}
export function CategoryTree({
categories,
expandedIds,
onToggleExpand,
onAdd,
onEdit,
onDelete,
}: CategoryTreeProps) {
const renderTree = (nodes: CategoryDictionary[], level: number = 0) => {
return nodes.map(node => (
<div key={node.id} style={{ marginLeft: `${level * 24}px` }}>
<div className="flex items-center gap-2 py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg group">
<div className="flex-1 flex items-center gap-2">
{node.children && node.children.length > 0 ? (
<button
onClick={() => onToggleExpand(node.id)}
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
>
{expandedIds.has(node.id) ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
) : (
<div className="w-6" />
)}
{node.children && node.children.length > 0 ? (
<Folder className="w-4 h-4 text-yellow-600 dark:text-yellow-500" />
) : (
<File className="w-4 h-4 text-gray-400 dark:text-gray-500" />
)}
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="dark:text-gray-100">{node.name}</span>
<Badge variant="outline" className="text-xs">{node.code}</Badge>
{!node.isActive && (
<Badge variant="outline" className="text-xs text-red-600 dark:text-red-400"></Badge>
)}
</div>
{node.description && (
<p className="text-xs text-muted-foreground dark:text-gray-400">{node.description}</p>
)}
</div>
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
onClick={() => onAdd(node)}
>
<Plus className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(node)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(node.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
{expandedIds.has(node.id) && node.children && renderTree(node.children, level + 1)}
</div>
));
};
return (
<Card className="p-4">
<div className="min-h-[400px]">
{categories.length === 0 ? (
<div className="text-center text-muted-foreground py-12">
</div>
) : (
renderTree(categories)
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,306 @@
'use client';
import React, { useReducer, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
import { toast } from 'sonner';
import { CategoryDictionary, CategoryAction } from './types';
import { categoryReducer, initialState } from './reducer';
import { CategoryFilters } from './components/CategoryFilters';
import { CategoryTree } from './components/CategoryTree';
import { CategoryFormDialog } from './components/CategoryFormDialog';
import { CategoryInstructions } from './components/CategoryInstructions';
export default function CategoryDictionaryPage() {
const [state, dispatch] = useReducer(categoryReducer, initialState);
// 模拟数据加载
useEffect(() => {
const mockData: CategoryDictionary[] = [
{
id: 'cat-1',
code: 'IND001',
name: '种植业',
type: 'industry',
level: 1,
sortOrder: 1,
description: '农作物种植相关行业',
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-2',
code: 'IND001-01',
name: '粮食作物',
type: 'industry',
parentId: 'cat-1',
level: 2,
sortOrder: 1,
description: '小麦、水稻、玉米等粮食作物',
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-3',
code: 'IND001-02',
name: '经济作物',
type: 'industry',
parentId: 'cat-1',
level: 2,
sortOrder: 2,
description: '棉花、油料、糖料等经济作物',
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-4',
code: 'IND002',
name: '畜牧业',
type: 'industry',
level: 1,
sortOrder: 2,
description: '牲畜饲养相关行业',
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-5',
code: 'EQP001',
name: '动力机械',
type: 'equipment',
level: 1,
sortOrder: 1,
description: '拖拉机等动力设备',
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-6',
code: 'EQP001-01',
name: '轮式拖拉机',
type: 'equipment',
parentId: 'cat-5',
level: 2,
sortOrder: 1,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-7',
code: 'EQP001-02',
name: '履带式拖拉机',
type: 'equipment',
parentId: 'cat-5',
level: 2,
sortOrder: 2,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'cat-8',
code: 'EQP002',
name: '收获机械',
type: 'equipment',
level: 1,
sortOrder: 2,
description: '收割机、采摘机等',
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
];
// 尝试从 localStorage 加载数据
const storedData = localStorage.getItem('smart_agriculture_category_dictionary');
if (storedData) {
try {
const data = JSON.parse(storedData);
dispatch({ type: 'SET_CATEGORIES', payload: data });
} catch (error) {
console.error('Failed to parse stored data:', error);
dispatch({ type: 'SET_CATEGORIES', payload: mockData });
}
} else {
dispatch({ type: 'SET_CATEGORIES', payload: mockData });
}
}, []);
// 保存数据到 localStorage
const saveCategories = (categories: CategoryDictionary[]) => {
localStorage.setItem('smart_agriculture_category_dictionary', JSON.stringify(categories));
dispatch({ type: 'SET_CATEGORIES', payload: categories });
};
// 构建树形结构
const buildTree = (items: CategoryDictionary[]): CategoryDictionary[] => {
const map = new Map<string, CategoryDictionary>();
const roots: CategoryDictionary[] = [];
// 创建映射
items.forEach(item => {
map.set(item.id, { ...item, children: [] });
});
// 构建树
items.forEach(item => {
const node = map.get(item.id)!;
if (item.parentId) {
const parent = map.get(item.parentId);
if (parent) {
parent.children = parent.children || [];
parent.children.push(node);
}
} else {
roots.push(node);
}
});
return roots;
};
// 过滤分类数据
const filteredCategories = state.categories.filter(cat => {
const matchKeyword = !state.searchKeyword ||
cat.name.includes(state.searchKeyword) ||
cat.code.includes(state.searchKeyword);
const matchType = state.typeFilter === 'all' || cat.type === state.typeFilter;
return matchKeyword && matchType;
});
const treeData = buildTree(filteredCategories);
// 处理新增
const handleAdd = (parent?: CategoryDictionary) => {
dispatch({
type: 'SET_DIALOG_STATE',
payload: {
open: true,
editing: undefined,
parent: parent || null,
},
});
};
// 处理编辑
const handleEdit = (category: CategoryDictionary) => {
dispatch({
type: 'SET_DIALOG_STATE',
payload: {
open: true,
editing: category,
parent: undefined,
},
});
};
// 处理删除
const handleDelete = (id: string) => {
// 检查是否有子分类
const hasChildren = state.categories.some(cat => cat.parentId === id);
if (hasChildren) {
toast.error('请先删除子分类');
return;
}
const updated = state.categories.filter(cat => cat.id !== id);
saveCategories(updated);
toast.success('删除成功');
};
// 处理保存
const handleSave = () => {
if (!state.formData.code.trim() || !state.formData.name.trim()) {
toast.error('请填写编码和名称');
return;
}
if (state.dialogState.editing) {
// 编辑
dispatch({
type: 'UPDATE_CATEGORY',
payload: {
id: state.dialogState.editing.id,
updates: state.formData,
},
});
saveCategories(state.categories);
toast.success('更新成功');
} else {
// 新增
const newCategory: CategoryDictionary = {
id: `cat-${Date.now()}`,
...state.formData,
parentId: state.dialogState.parent?.id,
level: state.dialogState.parent ? state.dialogState.parent.level + 1 : 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
dispatch({ type: 'ADD_CATEGORY', payload: newCategory });
saveCategories([...state.categories, newCategory]);
toast.success('添加成功');
}
dispatch({
type: 'SET_DIALOG_STATE',
payload: { open: false, editing: undefined, parent: undefined },
});
};
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-green-800 dark:text-green-600"></h2>
<p className="text-muted-foreground dark:text-gray-400"></p>
</div>
<Button onClick={() => handleAdd()}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 搜索和筛选 */}
<CategoryFilters
searchKeyword={state.searchKeyword}
typeFilter={state.typeFilter}
onSearchChange={(value) => dispatch({ type: 'SET_SEARCH_KEYWORD', payload: value })}
onTypeFilterChange={(value) => dispatch({ type: 'SET_TYPE_FILTER', payload: value })}
/>
{/* 分类树 */}
<CategoryTree
categories={treeData}
expandedIds={state.expandedIds}
onToggleExpand={(id) => dispatch({ type: 'TOGGLE_EXPAND', payload: id })}
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
/>
{/* 编辑对话框 */}
<CategoryFormDialog
open={state.dialogState.open}
editing={state.dialogState.editing}
parent={state.dialogState.parent}
formData={state.formData}
onOpenChange={(open) => dispatch({
type: 'SET_DIALOG_STATE',
payload: { open, editing: undefined, parent: undefined },
})}
onFormDataChange={(data) => dispatch({ type: 'SET_FORM_DATA', payload: data })}
onSave={handleSave}
/>
{/* 使用说明 */}
<CategoryInstructions />
</div>
);
}

View File

@@ -0,0 +1,94 @@
import { CategoryDictionary, CategoryAction, CategoryState, CategoryFormData } from './types';
const initialFormData: CategoryFormData = {
code: '',
name: '',
type: 'industry',
description: '',
sortOrder: 0,
isActive: true,
};
export const initialState: CategoryState = {
categories: [],
searchKeyword: '',
typeFilter: 'all',
expandedIds: new Set(),
dialogState: {
open: false,
editing: undefined,
parent: undefined,
},
formData: initialFormData,
};
export function categoryReducer(state: CategoryState, action: CategoryAction): CategoryState {
switch (action.type) {
case 'SET_CATEGORIES':
return { ...state, categories: action.payload };
case 'ADD_CATEGORY':
return { ...state, categories: [...state.categories, action.payload] };
case 'UPDATE_CATEGORY':
return {
...state,
categories: state.categories.map(cat =>
cat.id === action.payload.id
? { ...cat, ...action.payload.updates, updatedAt: new Date().toISOString() }
: cat
),
};
case 'DELETE_CATEGORY':
return {
...state,
categories: state.categories.filter(cat => cat.id !== action.payload),
};
case 'SET_SEARCH_KEYWORD':
return { ...state, searchKeyword: action.payload };
case 'SET_TYPE_FILTER':
return { ...state, typeFilter: action.payload };
case 'TOGGLE_EXPAND':
const newExpanded = new Set(state.expandedIds);
if (newExpanded.has(action.payload)) {
newExpanded.delete(action.payload);
} else {
newExpanded.add(action.payload);
}
return { ...state, expandedIds: newExpanded };
case 'SET_DIALOG_STATE':
return {
...state,
dialogState: action.payload,
formData: action.payload.editing
? {
code: action.payload.editing.code,
name: action.payload.editing.name,
type: action.payload.editing.type,
description: action.payload.editing.description || '',
sortOrder: action.payload.editing.sortOrder,
isActive: action.payload.editing.isActive,
}
: action.payload.parent
? {
...initialFormData,
type: action.payload.parent.type,
}
: initialFormData,
};
case 'SET_FORM_DATA':
return {
...state,
formData: { ...state.formData, ...action.payload },
};
default:
return state;
}
}

View File

@@ -0,0 +1,53 @@
// 分类字典类型定义
export interface CategoryDictionary {
id: string;
code: string;
name: string;
type: string; // 分类类型industry, equipment, crop等
parentId?: string;
level: number;
sortOrder: number;
description?: string;
isActive: boolean;
children?: CategoryDictionary[];
createdAt: string;
updatedAt: string;
}
export type CategoryType = 'industry' | 'equipment' | 'crop' | 'operation' | 'other';
// 分类表单数据
export interface CategoryFormData {
code: string;
name: string;
type: string;
description: string;
sortOrder: number;
isActive: boolean;
}
// 分类操作类型
export type CategoryAction =
| { type: 'SET_CATEGORIES'; payload: CategoryDictionary[] }
| { type: 'ADD_CATEGORY'; payload: CategoryDictionary }
| { type: 'UPDATE_CATEGORY'; payload: { id: string; updates: Partial<CategoryDictionary> } }
| { type: 'DELETE_CATEGORY'; payload: string }
| { type: 'SET_SEARCH_KEYWORD'; payload: string }
| { type: 'SET_TYPE_FILTER'; payload: string }
| { type: 'TOGGLE_EXPAND'; payload: string }
| { type: 'SET_DIALOG_STATE'; payload: { open: boolean; editing?: CategoryDictionary; parent?: CategoryDictionary | null } }
| { type: 'SET_FORM_DATA'; payload: Partial<CategoryFormData> };
// 分类状态
export interface CategoryState {
categories: CategoryDictionary[];
searchKeyword: string;
typeFilter: string;
expandedIds: Set<string>;
dialogState: {
open: boolean;
editing?: CategoryDictionary;
parent?: CategoryDictionary | null;
};
formData: CategoryFormData;
}

View File

@@ -0,0 +1,159 @@
'use client';
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { CategoryFormData, CategoryDictionary } from '../types';
interface CategoryFormProps {
open: boolean;
editing: CategoryDictionary | null;
formData: CategoryFormData;
onFormDataChange: (data: Partial<CategoryFormData>) => void;
onOpenChange: (open: boolean) => void;
onSave: () => void;
}
export function CategoryForm({
open,
editing,
formData,
onFormDataChange,
onOpenChange,
onSave,
}: CategoryFormProps) {
const handleSave = () => {
if (!formData.code.trim() || !formData.name.trim() || !formData.value.trim() || !formData.label.trim()) {
return false;
}
onSave();
return true;
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
{editing ? '编辑字典' : '新增字典'}
</DialogTitle>
<DialogDescription className="sr-only">
{editing ? '编辑数据字典' : '添加新数据字典'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={formData.code}
onChange={(e) => onFormDataChange({ code: e.target.value })}
placeholder="GENDER_MALE"
disabled={editing?.isSystem}
/>
</div>
<div>
<Label> *</Label>
<Input
value={formData.name}
onChange={(e) => onFormDataChange({ name: e.target.value })}
placeholder="性别-男"
/>
</div>
</div>
<div>
<Label> *</Label>
<Select
value={formData.category}
onValueChange={(value) => onFormDataChange({ category: value })}
disabled={editing?.isSystem}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gender"></SelectItem>
<SelectItem value="status"></SelectItem>
<SelectItem value="unit"></SelectItem>
<SelectItem value="weather"></SelectItem>
<SelectItem value="soil_type"></SelectItem>
<SelectItem value="irrigation_method"></SelectItem>
<SelectItem value="fertilizer_type"></SelectItem>
<SelectItem value="pesticide_type"></SelectItem>
<SelectItem value="task_status"></SelectItem>
<SelectItem value="task_priority"></SelectItem>
<SelectItem value="approval_status"></SelectItem>
<SelectItem value="operation_type"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={formData.value}
onChange={(e) => onFormDataChange({ value: e.target.value })}
placeholder="male"
disabled={editing?.isSystem}
/>
<p className="text-xs text-muted-foreground mt-1">
使使
</p>
</div>
<div>
<Label> *</Label>
<Input
value={formData.label}
onChange={(e) => onFormDataChange({ label: e.target.value })}
placeholder="男"
/>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
</div>
<div>
<Label></Label>
<Textarea
value={formData.description}
onChange={(e) => onFormDataChange({ description: e.target.value })}
placeholder="请输入描述"
rows={2}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
type="number"
value={formData.sortOrder}
onChange={(e) => onFormDataChange({ sortOrder: parseInt(e.target.value) || 0 })}
/>
</div>
<div className="flex items-center justify-between pt-6">
<Label></Label>
<Switch
checked={formData.isActive}
onCheckedChange={(checked) => onFormDataChange({ isActive: checked })}
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,211 @@
'use client';
import React from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Search, BookOpen, Edit, Trash2 } from 'lucide-react';
import { CategoryDictionary } from '../types';
interface CategoryListProps {
categories: CategoryDictionary[];
searchKeyword: string;
categoryFilter: string;
onSearchChange: (keyword: string) => void;
onCategoryFilterChange: (category: string) => void;
onEdit: (category: CategoryDictionary) => void;
onDelete: (id: string) => void;
}
export function CategoryList({
categories,
searchKeyword,
categoryFilter,
onSearchChange,
onCategoryFilterChange,
onEdit,
onDelete,
}: CategoryListProps) {
// 过滤字典
const filteredCategories = categories.filter(category => {
const matchKeyword = !searchKeyword ||
category.name.includes(searchKeyword) ||
category.code.includes(searchKeyword) ||
category.label.includes(searchKeyword) ||
category.value.includes(searchKeyword);
const matchCategory = categoryFilter === 'all' || category.category === categoryFilter;
return matchKeyword && matchCategory;
});
// 按分类分组
const groupedCategories = filteredCategories.reduce((acc, category) => {
if (!acc[category.category]) {
acc[category.category] = [];
}
acc[category.category].push(category);
return acc;
}, {} as Record<string, CategoryDictionary[]>);
const getCategoryLabel = (category: string) => {
const labels: Record<string, string> = {
gender: '性别',
status: '状态',
unit: '单位',
weather: '天气',
soil_type: '土壤类型',
irrigation_method: '灌溉方式',
fertilizer_type: '肥料类型',
pesticide_type: '农药类型',
task_status: '任务状态',
task_priority: '任务优先级',
approval_status: '审批状态',
operation_type: '作业类型',
other: '其他',
};
return labels[category] || category;
};
return (
<div className="space-y-6">
{/* 搜索和筛选 */}
<Card className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索编码、名称、标签、值..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<Select value={categoryFilter} onValueChange={onCategoryFilterChange}>
<SelectTrigger>
<SelectValue placeholder="字典分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="gender"></SelectItem>
<SelectItem value="status"></SelectItem>
<SelectItem value="unit"></SelectItem>
<SelectItem value="weather"></SelectItem>
<SelectItem value="soil_type"></SelectItem>
<SelectItem value="irrigation_method"></SelectItem>
<SelectItem value="fertilizer_type"></SelectItem>
<SelectItem value="pesticide_type"></SelectItem>
<SelectItem value="task_status"></SelectItem>
<SelectItem value="task_priority"></SelectItem>
<SelectItem value="approval_status"></SelectItem>
<SelectItem value="operation_type"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
</Card>
{/* 字典列表 */}
{Object.entries(groupedCategories).map(([category, items]) => (
<Card key={category}>
<div className="p-4 border-b bg-muted/50">
<h3 className="flex items-center gap-2">
<BookOpen className="w-5 h-5 text-green-600" />
{getCategoryLabel(category)}
<Badge variant="outline">{items.length}</Badge>
</h3>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.sort((a, b) => a.sortOrder - b.sortOrder).map((category) => (
<TableRow key={category.id}>
<TableCell>
<div className="flex items-center gap-2">
<code className="text-xs bg-muted px-2 py-1 rounded">{category.code}</code>
{category.isSystem && (
<Badge variant="outline" className="text-xs"></Badge>
)}
</div>
</TableCell>
<TableCell>
<div>
<div>{category.name}</div>
{category.description && (
<p className="text-xs text-muted-foreground">{category.description}</p>
)}
</div>
</TableCell>
<TableCell>
<code className="text-xs">{category.value}</code>
</TableCell>
<TableCell>{category.label}</TableCell>
<TableCell>{category.sortOrder}</TableCell>
<TableCell>
{category.isActive ? (
<Badge className="bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"></Badge>
) : (
<Badge variant="outline"></Badge>
)}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(category)}
>
<Edit className="w-4 h-4" />
</Button>
{!category.isSystem && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(category.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
))}
{filteredCategories.length === 0 && (
<Card className="p-12 text-center text-muted-foreground">
</Card>
)}
{/* 使用说明 */}
<Card className="p-4 bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800">
<h4 className="text-blue-900 dark:text-blue-100 mb-2">
<BookOpen className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800 dark:text-blue-200">
<li> </li>
<li> 使线 GENDER_MALE</li>
<li> valuelabel</li>
<li> </li>
<li> 便</li>
</ul>
</Card>
</div>
);
}

View File

@@ -0,0 +1,84 @@
'use client';
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { AlertTriangle } from 'lucide-react';
import { CategoryDictionary } from '../types';
interface DeleteConfirmDialogProps {
open: boolean;
category: CategoryDictionary | null;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
}
export function DeleteConfirmDialog({
open,
category,
onOpenChange,
onConfirm,
}: DeleteConfirmDialogProps) {
const handleConfirm = () => {
onConfirm();
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<div className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-destructive" />
<DialogTitle></DialogTitle>
</div>
<DialogDescription>
"{category?.name}"
</DialogDescription>
</DialogHeader>
<div className="py-4">
{category && (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<code className="text-xs bg-muted px-2 py-1 rounded">{category.code}</code>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span>{category.name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span>{category.category}</span>
</div>
{category.isSystem && (
<div className="mt-2 p-2 bg-destructive/10 border border-destructive/20 rounded text-destructive text-xs">
</div>
)}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
disabled={category?.isSystem}
>
{category?.isSystem ? '系统字典不可删除' : '确认删除'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,385 @@
'use client';
import React, { useReducer, useLayoutEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Plus, Download } from 'lucide-react';
import { toast } from 'sonner';
import { CategoryList } from './components/CategoryList';
import { CategoryForm } from './components/CategoryForm';
import { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
import { CategoryDictionary } from './types';
import { categoryReducer, initialCategoryState } from './reducer';
// 模拟数据
const mockData: CategoryDictionary[] = [
// 性别
{
id: 'dict-1',
code: 'GENDER_MALE',
name: '性别-男',
category: 'gender',
value: 'male',
label: '男',
sortOrder: 1,
isSystem: true,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-2',
code: 'GENDER_FEMALE',
name: '性别-女',
category: 'gender',
value: 'female',
label: '女',
sortOrder: 2,
isSystem: true,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
// 状态
{
id: 'dict-3',
code: 'STATUS_ACTIVE',
name: '状态-激活',
category: 'status',
value: 'active',
label: '激活',
sortOrder: 1,
isSystem: true,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-4',
code: 'STATUS_INACTIVE',
name: '状态-停用',
category: 'status',
value: 'inactive',
label: '停用',
sortOrder: 2,
isSystem: true,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
// 单位类型
{
id: 'dict-5',
code: 'UNIT_AREA_MU',
name: '面积单位-亩',
category: 'unit',
value: 'mu',
label: '亩',
sortOrder: 1,
description: '中国传统面积单位',
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-6',
code: 'UNIT_AREA_HECTARE',
name: '面积单位-公顷',
category: 'unit',
value: 'hectare',
label: '公顷',
sortOrder: 2,
description: '国际通用面积单位',
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-7',
code: 'UNIT_WEIGHT_KG',
name: '重量单位-千克',
category: 'unit',
value: 'kg',
label: '千克',
sortOrder: 3,
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-8',
code: 'UNIT_WEIGHT_TON',
name: '重量单位-吨',
category: 'unit',
value: 'ton',
label: '吨',
sortOrder: 4,
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
// 天气
{
id: 'dict-9',
code: 'WEATHER_SUNNY',
name: '天气-晴',
category: 'weather',
value: 'sunny',
label: '晴',
sortOrder: 1,
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-10',
code: 'WEATHER_CLOUDY',
name: '天气-多云',
category: 'weather',
value: 'cloudy',
label: '多云',
sortOrder: 2,
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-11',
code: 'WEATHER_RAINY',
name: '天气-雨',
category: 'weather',
value: 'rainy',
label: '雨',
sortOrder: 3,
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
// 土壤类型
{
id: 'dict-12',
code: 'SOIL_SANDY',
name: '土壤-砂土',
category: 'soil_type',
value: 'sandy',
label: '砂土',
sortOrder: 1,
description: '含砂粒较多的土壤',
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-13',
code: 'SOIL_LOAMY',
name: '土壤-壤土',
category: 'soil_type',
value: 'loamy',
label: '壤土',
sortOrder: 2,
description: '砂粘适中的土壤',
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
{
id: 'dict-14',
code: 'SOIL_CLAY',
name: '土壤-黏土',
category: 'soil_type',
value: 'clay',
label: '黏土',
sortOrder: 3,
description: '含黏粒较多的土壤',
isSystem: false,
isActive: true,
createdAt: '2024-01-01T00:00:00',
updatedAt: '2024-01-01T00:00:00',
},
];
export default function DataDictionaryPage() {
const [state, dispatch] = useReducer(categoryReducer, initialCategoryState);
const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
const [categoryToDelete, setCategoryToDelete] = React.useState<CategoryDictionary | null>(null);
// 加载数据
useLayoutEffect(() => {
const data = localStorage.getItem('smart_agriculture_category_dictionary');
if (data) {
try {
const categories = JSON.parse(data);
dispatch({ type: 'SET_CATEGORIES', payload: categories });
} catch (error) {
console.error('Failed to parse category dictionary data:', error);
loadMockData();
}
} else {
loadMockData();
}
}, []);
const loadMockData = () => {
localStorage.setItem('smart_agriculture_category_dictionary', JSON.stringify(mockData));
dispatch({ type: 'SET_CATEGORIES', payload: mockData });
};
const saveCategories = (categories: CategoryDictionary[]) => {
localStorage.setItem('smart_agriculture_category_dictionary', JSON.stringify(categories));
dispatch({ type: 'SET_CATEGORIES', payload: categories });
};
// 处理新增
const handleAdd = () => {
dispatch({ type: 'SET_DIALOG_STATE', payload: { open: true, editing: null } });
};
// 处理编辑
const handleEdit = (category: CategoryDictionary) => {
dispatch({ type: 'SET_DIALOG_STATE', payload: { open: true, editing: category } });
};
// 处理删除
const handleDelete = (id: string) => {
const category = state.categories.find(c => c.id === id);
if (!category) return;
if (category.isSystem) {
toast.error('系统内置字典不能删除');
return;
}
setCategoryToDelete(category);
setDeleteConfirmOpen(true);
};
// 确认删除
const confirmDelete = () => {
if (!categoryToDelete) return;
const updated = state.categories.filter(c => c.id !== categoryToDelete.id);
saveCategories(updated);
toast.success('删除成功');
setCategoryToDelete(null);
};
// 处理保存
const handleSave = () => {
const { formData, dialogState } = state;
if (!formData.code.trim() || !formData.name.trim() || !formData.value.trim() || !formData.label.trim()) {
toast.error('请填写必填项');
return;
}
const now = new Date().toISOString();
if (dialogState.editing) {
// 编辑
const updated = state.categories.map(category =>
category.id === dialogState.editing!.id
? {
...category,
...formData,
updatedAt: now,
}
: category
);
saveCategories(updated);
toast.success('更新成功');
} else {
// 新增
const newCategory: CategoryDictionary = {
id: `dict-${Date.now()}`,
...formData,
isSystem: false,
createdAt: now,
updatedAt: now,
};
saveCategories([...state.categories, newCategory]);
toast.success('添加成功');
}
dispatch({ type: 'SET_DIALOG_STATE', payload: { open: false, editing: null } });
};
// 处理导出
const handleExport = () => {
const filteredCategories = state.categories.filter(category => {
const matchKeyword = !state.searchKeyword ||
category.name.includes(state.searchKeyword) ||
category.code.includes(state.searchKeyword) ||
category.label.includes(state.searchKeyword) ||
category.value.includes(state.searchKeyword);
const matchCategory = state.categoryFilter === 'all' || category.category === state.categoryFilter;
return matchKeyword && matchCategory;
});
const dataStr = JSON.stringify(filteredCategories, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `category_dictionary_${new Date().getTime()}.json`;
link.click();
toast.success('导出成功');
};
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800 dark:text-green-600"></h2>
<p className="text-muted-foreground"></p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleAdd}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 字典列表 */}
<CategoryList
categories={state.categories}
searchKeyword={state.searchKeyword}
categoryFilter={state.categoryFilter}
onSearchChange={(keyword) => dispatch({ type: 'SET_SEARCH_KEYWORD', payload: keyword })}
onCategoryFilterChange={(category) => dispatch({ type: 'SET_CATEGORY_FILTER', payload: category })}
onEdit={handleEdit}
onDelete={handleDelete}
/>
{/* 编辑表单 */}
<CategoryForm
open={state.dialogState.open}
editing={state.dialogState.editing}
formData={state.formData}
onFormDataChange={(data) => dispatch({ type: 'SET_FORM_DATA', payload: data })}
onOpenChange={(open) => dispatch({ type: 'SET_DIALOG_STATE', payload: { open, editing: null } })}
onSave={handleSave}
/>
{/* 删除确认对话框 */}
<DeleteConfirmDialog
open={deleteConfirmOpen}
category={categoryToDelete}
onOpenChange={setDeleteConfirmOpen}
onConfirm={confirmDelete}
/>
</div>
);
}

View File

@@ -0,0 +1,109 @@
import { CategoryState, CategoryAction, CategoryFormData } from './types';
// 初始状态
export const initialCategoryState: CategoryState = {
categories: [],
searchKeyword: '',
categoryFilter: 'all',
dialogState: {
open: false,
editing: null,
},
formData: {
code: '',
name: '',
category: 'other',
value: '',
label: '',
sortOrder: 0,
description: '',
isActive: true,
},
};
// 初始表单数据
export const initialFormData: CategoryFormData = {
code: '',
name: '',
category: 'other',
value: '',
label: '',
sortOrder: 0,
description: '',
isActive: true,
};
// Reducer
export function categoryReducer(state: CategoryState, action: CategoryAction): CategoryState {
switch (action.type) {
case 'SET_CATEGORIES':
return {
...state,
categories: action.payload,
};
case 'ADD_CATEGORY':
return {
...state,
categories: [...state.categories, action.payload],
};
case 'UPDATE_CATEGORY':
return {
...state,
categories: state.categories.map(category =>
category.id === action.payload.id
? { ...category, ...action.payload.updates, updatedAt: new Date().toISOString() }
: category
),
};
case 'DELETE_CATEGORY':
return {
...state,
categories: state.categories.filter(category => category.id !== action.payload),
};
case 'SET_SEARCH_KEYWORD':
return {
...state,
searchKeyword: action.payload,
};
case 'SET_CATEGORY_FILTER':
return {
...state,
categoryFilter: action.payload,
};
case 'SET_DIALOG_STATE':
return {
...state,
dialogState: action.payload,
formData: action.payload.editing
? {
code: action.payload.editing.code,
name: action.payload.editing.name,
category: action.payload.editing.category,
value: action.payload.editing.value,
label: action.payload.editing.label,
sortOrder: action.payload.editing.sortOrder,
description: action.payload.editing.description || '',
isActive: action.payload.editing.isActive,
}
: initialFormData,
};
case 'SET_FORM_DATA':
return {
...state,
formData: {
...state.formData,
...action.payload,
},
};
default:
return state;
}
}

View File

@@ -0,0 +1,67 @@
// 分类字典类型定义
export interface CategoryDictionary {
id: string;
code: string;
name: string;
category: string; // 字典分类
value: string;
label: string;
sortOrder: number;
description?: string;
isSystem: boolean; // 是否系统内置
isActive: boolean;
extendData?: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export type DictionaryCategory =
| 'gender'
| 'status'
| 'unit'
| 'weather'
| 'soil_type'
| 'irrigation_method'
| 'fertilizer_type'
| 'pesticide_type'
| 'task_status'
| 'task_priority'
| 'approval_status'
| 'operation_type'
| 'other';
// 分类表单数据
export interface CategoryFormData {
code: string;
name: string;
category: string;
value: string;
label: string;
sortOrder: number;
description: string;
isActive: boolean;
}
// 分类操作类型
export type CategoryAction =
| { type: 'SET_CATEGORIES'; payload: CategoryDictionary[] }
| { type: 'ADD_CATEGORY'; payload: CategoryDictionary }
| { type: 'UPDATE_CATEGORY'; payload: { id: string; updates: Partial<CategoryDictionary> } }
| { type: 'DELETE_CATEGORY'; payload: string }
| { type: 'SET_SEARCH_KEYWORD'; payload: string }
| { type: 'SET_CATEGORY_FILTER'; payload: string }
| { type: 'SET_DIALOG_STATE'; payload: { open: boolean; editing?: CategoryDictionary | null } }
| { type: 'SET_FORM_DATA'; payload: Partial<CategoryFormData> };
// 分类状态
export interface CategoryState {
categories: CategoryDictionary[];
searchKeyword: string;
categoryFilter: string;
dialogState: {
open: boolean;
editing?: CategoryDictionary | null;
};
formData: CategoryFormData;
}

View File

@@ -0,0 +1,26 @@
'use client';
import React from 'react';
import Link from 'next/link';
export default function SystemPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4"></h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Link href="/central-config/system/settings" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm"></p>
</Link>
<Link href="/central-config/system/category" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm"></p>
</Link>
<Link href="/central-config/system/dictionary" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 text-sm"></p>
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface CopyrightInfoCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function CopyrightInfoCard({ settings, onSettingsChange }: CopyrightInfoCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>ICP备案号</Label>
<Input
value={settings.icp || ''}
onChange={(e) => onSettingsChange({ icp: e.target.value })}
placeholder="京ICP备12345678号"
/>
</div>
<div>
<Label></Label>
<Input
value={settings.copyright || ''}
onChange={(e) => onSettingsChange({ copyright: e.target.value })}
placeholder="© 2024 公司名称 版权所有"
/>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,39 @@
import { Card } from '@/components/ui/card'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface FeatureToggleCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function FeatureToggleCard({ settings, onSettingsChange }: FeatureToggleCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={settings.enableRegistration}
onCheckedChange={(checked) => onSettingsChange({ enableRegistration: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>访访</Label>
<p className="text-sm text-muted-foreground">访</p>
</div>
<Switch
checked={settings.enableGuestAccess}
onCheckedChange={(checked) => onSettingsChange({ enableGuestAccess: checked })}
/>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,67 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { SystemSettings } from '@/types/system-params'
interface PasswordPolicyCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function PasswordPolicyCard({ settings, onSettingsChange }: PasswordPolicyCardProps) {
const updatePasswordPolicy = (updates: Partial<SystemSettings['passwordPolicy']>) => {
onSettingsChange({
passwordPolicy: { ...settings.passwordPolicy, ...updates }
})
}
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="space-y-4">
<div>
<Label></Label>
<Input
type="number"
value={settings.passwordPolicy.minLength}
onChange={(e) => updatePasswordPolicy({ minLength: parseInt(e.target.value) || 8 })}
min={6}
max={32}
/>
</div>
<div className="space-y-3">
<Label></Label>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireUppercase}
onCheckedChange={(checked) => updatePasswordPolicy({ requireUppercase: checked })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireLowercase}
onCheckedChange={(checked) => updatePasswordPolicy({ requireLowercase: checked })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireNumbers}
onCheckedChange={(checked) => updatePasswordPolicy({ requireNumbers: checked })}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm"></span>
<Switch
checked={settings.passwordPolicy.requireSpecialChars}
onCheckedChange={(checked) => updatePasswordPolicy({ requireSpecialChars: checked })}
/>
</div>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,60 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface PlatformInfoCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function PlatformInfoCard({ settings, onSettingsChange }: PlatformInfoCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Input
value={settings.platformName}
onChange={(e) => onSettingsChange({ platformName: e.target.value })}
placeholder="请输入平台名称"
/>
</div>
<div>
<Label></Label>
<Input
value={settings.companyName || ''}
onChange={(e) => onSettingsChange({ companyName: e.target.value })}
placeholder="请输入公司名称"
/>
</div>
<div>
<Label></Label>
<Input
type="email"
value={settings.contactEmail || ''}
onChange={(e) => onSettingsChange({ contactEmail: e.target.value })}
placeholder="support@example.com"
/>
</div>
<div>
<Label></Label>
<Input
value={settings.contactPhone || ''}
onChange={(e) => onSettingsChange({ contactPhone: e.target.value })}
placeholder="400-888-8888"
/>
</div>
<div className="md:col-span-2">
<Label></Label>
<Input
value={settings.address || ''}
onChange={(e) => onSettingsChange({ address: e.target.value })}
placeholder="请输入公司地址"
/>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,69 @@
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { SystemSettings } from '@/types/system-params'
interface RegionalSettingsCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function RegionalSettingsCard({ settings, onSettingsChange }: RegionalSettingsCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label></Label>
<Select
value={settings.language}
onValueChange={(value) => onSettingsChange({ language: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-CN"></SelectItem>
<SelectItem value="zh-TW"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select
value={settings.timezone}
onValueChange={(value) => onSettingsChange({ timezone: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Asia/Shanghai"> (UTC+8)</SelectItem>
<SelectItem value="Asia/Tokyo"> (UTC+9)</SelectItem>
<SelectItem value="America/New_York"> (UTC-5)</SelectItem>
<SelectItem value="Europe/London"> (UTC+0)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Select
value={settings.dateFormat}
onValueChange={(value) => onSettingsChange({ dateFormat: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="YYYY-MM-DD">2024-10-14</SelectItem>
<SelectItem value="DD/MM/YYYY">14/10/2024</SelectItem>
<SelectItem value="MM/DD/YYYY">10/14/2024</SelectItem>
<SelectItem value="YYYY年MM月DD日">20241014</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,45 @@
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SystemSettings } from '@/types/system-params'
interface SessionManagementCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function SessionManagementCard({ settings, onSettingsChange }: SessionManagementCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label></Label>
<Input
type="number"
value={settings.sessionTimeout}
onChange={(e) => onSettingsChange({ sessionTimeout: parseInt(e.target.value) || 30 })}
min={5}
max={1440}
/>
<p className="text-sm text-muted-foreground mt-1">
退
</p>
</div>
<div>
<Label></Label>
<Input
type="number"
value={settings.maxLoginAttempts}
onChange={(e) => onSettingsChange({ maxLoginAttempts: parseInt(e.target.value) || 5 })}
min={3}
max={10}
/>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card } from '@/components/ui/card'
import { Settings } from 'lucide-react'
export function SettingsInfoCard() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2">
<Settings className="w-4 h-4 inline mr-2" />
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> Logo将显示在系统导航栏和登录页面</li>
<li> </li>
<li> </li>
<li> </li>
<li> "保存设置"</li>
</ul>
</Card>
)
}

View File

@@ -0,0 +1,22 @@
import { Card } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import { SystemSettings } from '@/types/system-params'
interface SystemAnnouncementCardProps {
settings: SystemSettings
onSettingsChange: (updates: Partial<SystemSettings>) => void
}
export function SystemAnnouncementCard({ settings, onSettingsChange }: SystemAnnouncementCardProps) {
return (
<Card className="p-6">
<h3 className="mb-4"></h3>
<Textarea
value={settings.systemAnnouncement || ''}
onChange={(e) => onSettingsChange({ systemAnnouncement: e.target.value })}
placeholder="输入系统公告内容,将显示在登录页面"
rows={4}
/>
</Card>
)
}

View File

@@ -0,0 +1,8 @@
export { PlatformInfoCard } from './PlatformInfoCard'
export { SystemAnnouncementCard } from './SystemAnnouncementCard'
export { CopyrightInfoCard } from './CopyrightInfoCard'
export { FeatureToggleCard } from './FeatureToggleCard'
export { SessionManagementCard } from './SessionManagementCard'
export { PasswordPolicyCard } from './PasswordPolicyCard'
export { RegionalSettingsCard } from './RegionalSettingsCard'
export { SettingsInfoCard } from './SettingsInfoCard'

View File

@@ -0,0 +1,168 @@
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { SystemSettings } from '@/types/system-params'
import { Save, RefreshCw, Info, Shield, Globe } from 'lucide-react'
import { toast } from 'sonner'
// Import modular components
import {
PlatformInfoCard,
SystemAnnouncementCard,
CopyrightInfoCard,
FeatureToggleCard,
SessionManagementCard,
PasswordPolicyCard,
RegionalSettingsCard,
SettingsInfoCard
} from './components'
export default function SystemSettingsPage() {
const [settings, setSettings] = useState<SystemSettings>({
platformName: '智慧农业生产管理系统',
platformLogo: '',
systemAnnouncement: '欢迎使用智慧农业生产管理系统!',
contactEmail: 'support@smart-agriculture.com',
contactPhone: '400-888-8888',
address: '北京市海淀区中关村大街1号',
companyName: '智慧农业科技有限公司',
icp: '京ICP备12345678号',
copyright: '© 2024 智慧农业科技有限公司 版权所有',
enableRegistration: true,
enableGuestAccess: false,
sessionTimeout: 30,
maxLoginAttempts: 5,
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: false,
},
dateFormat: 'YYYY-MM-DD',
timezone: 'Asia/Shanghai',
language: 'zh-CN',
})
const [hasChanges, setHasChanges] = useState(false)
useEffect(() => {
loadSettings()
}, [])
const loadSettings = () => {
const data = localStorage.getItem('smart_agriculture_system_settings')
if (data) {
setSettings(JSON.parse(data))
} else {
saveSettings(settings)
}
}
const saveSettings = (newSettings: SystemSettings) => {
localStorage.setItem('smart_agriculture_system_settings', JSON.stringify(newSettings))
setSettings(newSettings)
setHasChanges(false)
toast.success('系统设置已保存')
}
const handleSave = () => {
saveSettings(settings)
}
const handleReset = () => {
loadSettings()
setHasChanges(false)
toast.info('已恢复到上次保存的设置')
}
const updateSettings = (updates: Partial<SystemSettings>) => {
setSettings({ ...settings, ...updates })
setHasChanges(true)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800"></h2>
<p className="text-muted-foreground"></p>
</div>
<div className="flex gap-2">
{hasChanges && (
<Button variant="outline" onClick={handleReset}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
)}
<Button onClick={handleSave} disabled={!hasChanges}>
<Save className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<Tabs defaultValue="basic" className="space-y-4">
<TabsList>
<TabsTrigger value="basic">
<Info className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="security">
<Shield className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="regional">
<Globe className="w-4 h-4 mr-2" />
</TabsTrigger>
</TabsList>
{/* 基本设置 */}
<TabsContent value="basic" className="space-y-4">
<PlatformInfoCard
settings={settings}
onSettingsChange={updateSettings}
/>
<SystemAnnouncementCard
settings={settings}
onSettingsChange={updateSettings}
/>
<CopyrightInfoCard
settings={settings}
onSettingsChange={updateSettings}
/>
<FeatureToggleCard
settings={settings}
onSettingsChange={updateSettings}
/>
</TabsContent>
{/* 安全设置 */}
<TabsContent value="security" className="space-y-4">
<SessionManagementCard
settings={settings}
onSettingsChange={updateSettings}
/>
<PasswordPolicyCard
settings={settings}
onSettingsChange={updateSettings}
/>
</TabsContent>
{/* 区域设置 */}
<TabsContent value="regional" className="space-y-4">
<RegionalSettingsCard
settings={settings}
onSettingsChange={updateSettings}
/>
</TabsContent>
</Tabs>
{/* 设置预览 */}
<SettingsInfoCard />
</div>
)
}