Compare commits

...

2 Commits

8 changed files with 1008 additions and 7 deletions

View File

@@ -42,7 +42,7 @@ export function LoginLogTable({ logs }: LoginLogTableProps) {
{new Date(log.loginTime).toLocaleString('zh-CN')} {new Date(log.loginTime).toLocaleString('zh-CN')}
</TableCell> </TableCell>
<TableCell> <TableCell>
<code className="text-xs bg-gray-100 px-2 py-1 rounded"> <code className="text-xs px-2 py-1 rounded">
{log.ipAddress} {log.ipAddress}
</code> </code>
</TableCell> </TableCell>

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

@@ -1,14 +1,385 @@
'use client'; 'use client';
import React from 'react'; 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() { 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 ( return (
<div className="p-6"> <div className="space-y-6 p-6">
<h1 className="text-2xl font-bold mb-4"></h1> <div className="flex items-center justify-between">
<div className="bg-white rounded-lg shadow p-4"> <div>
<p> - : /config/system/dictionary</p> <h2 className="text-green-800 dark:text-green-600"></h2>
<p className="text-muted-foreground"></p>
</div> </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> </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

@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-10 w-full items-center justify-between rounded-md border border-input bg-input-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className className
)} )}
{...props} {...props}