生产管理系统前端 1开发分类字典 2.适配input框灰色背景 3.适配textarea灰色背景.

This commit is contained in:
2025-10-23 18:04:05 +08:00
parent d254790901
commit dbbdf1f2d7
13 changed files with 788 additions and 12 deletions

1
.gitignore vendored
View File

@@ -146,3 +146,4 @@ Thumbs.db
# Temporary folders # Temporary folders
tmp/ tmp/
temp/ temp/
nul

View File

@@ -4,7 +4,6 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite",
"next:dev": "next dev --turbopack", "next:dev": "next dev --turbopack",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",

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

@@ -1,14 +1,306 @@
'use client'; '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() { 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 ( 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/category</p> <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> </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> </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

@@ -90,7 +90,7 @@ export function RoleFormDialog({
<h4 className="text-green-800"></h4> <h4 className="text-green-800"></h4>
<p className="text-xs text-muted-foreground"></p> <p className="text-xs text-muted-foreground"></p>
</div> </div>
<Card className="p-4 bg-gray-50"> <Card className="p-4 bg-gray-50 bg-input-background">
<div className="space-y-6"> <div className="space-y-6">
{allSystemMenus.map((system) => ( {allSystemMenus.map((system) => (
<div key={system.id} className="space-y-3"> <div key={system.id} className="space-y-3">

View File

@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( 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]", "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", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className

View File

@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return ( return (
<textarea <textarea
className={cn( 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 className
)} )}
ref={ref} ref={ref}

View File

@@ -3,10 +3,12 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@config "../../tailwind.config.js"; @config "../../tailwind.config.js";
@import "./body.css";
/* CSS变量定义 - 农业管理系统主题 */ /* CSS变量定义 - 农业管理系统主题 */
:root { :root {
/* 基础色彩系统 */ /* 基础色彩系统 */
--input-background: #f3f3f5;
--background: 240 10% 98%; --background: 240 10% 98%;
--foreground: 240 10% 10%; --foreground: 240 10% 10%;
--card: 0 0% 100%; --card: 0 0% 100%;
@@ -291,7 +293,7 @@
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out;
--color-input-background: var(--input-background);
@keyframes accordion-down { @keyframes accordion-down {
from { from {
height: 0; 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);
}
}