生产管理系统前端 1开发分类字典 2.适配input框灰色背景 3.适配textarea灰色背景.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,306 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
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="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">分类字典</h1>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<p>分类字典页面 - 路径: /config/system/category</p>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
53
crop-x/src/app/(app)/central-config/system/category/types.ts
Normal file
53
crop-x/src/app/(app)/central-config/system/category/types.ts
Normal 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;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export function RoleFormDialog({
|
||||
<h4 className="text-green-800">菜单与操作权限</h4>
|
||||
<p className="text-xs text-muted-foreground">选择菜单后可配置该菜单下的操作权限</p>
|
||||
</div>
|
||||
<Card className="p-4 bg-gray-50">
|
||||
<Card className="p-4 bg-gray-50 bg-input-background">
|
||||
<div className="space-y-6">
|
||||
{allSystemMenus.map((system) => (
|
||||
<div key={system.id} className="space-y-3">
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm bg-input-background",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
|
||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm bg-input-background",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@config "../../tailwind.config.js";
|
||||
@import "./body.css";
|
||||
|
||||
/* CSS变量定义 - 农业管理系统主题 */
|
||||
:root {
|
||||
/* 基础色彩系统 */
|
||||
--input-background: #f3f3f5;
|
||||
--background: 240 10% 98%;
|
||||
--foreground: 240 10% 10%;
|
||||
--card: 0 0% 100%;
|
||||
@@ -291,7 +293,7 @@
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
--color-input-background: var(--input-background);
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
@@ -325,4 +327,15 @@
|
||||
}
|
||||
|
||||
}
|
||||
@import "./body.css";
|
||||
|
||||
@layer utilities {
|
||||
.\@container\/card-header {
|
||||
container: card-header / inline-size;
|
||||
}
|
||||
.bg-input-background {
|
||||
background-color: var(--input-background);
|
||||
}
|
||||
.focus-visible\:ring-ring\/50:focus-visible {
|
||||
--tw-ring-color: var(--ring);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user