增加审批组件
This commit is contained in:
parent
e61b75f9e1
commit
0c09700980
78
frontend/src/components/DynamicIcon/README.md
Normal file
78
frontend/src/components/DynamicIcon/README.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# DynamicIcon 组件
|
||||||
|
|
||||||
|
动态图标渲染组件,支持所有 Lucide React 图标和 Emoji。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ 支持所有 Lucide React 图标(1000+ 个)
|
||||||
|
- ✅ 支持 Emoji 表情
|
||||||
|
- ✅ 自动识别图标类型
|
||||||
|
- ✅ 支持自定义默认图标
|
||||||
|
- ✅ 完整的 TypeScript 类型支持
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import DynamicIcon from '@/components/DynamicIcon';
|
||||||
|
|
||||||
|
// 使用 Lucide 图标名称
|
||||||
|
<DynamicIcon name="Calendar" className="h-4 w-4" />
|
||||||
|
<DynamicIcon name="Database" className="h-5 w-5" />
|
||||||
|
|
||||||
|
// 使用 Emoji
|
||||||
|
<DynamicIcon name="📅" />
|
||||||
|
<DynamicIcon name="💾" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义默认图标
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import DynamicIcon from '@/components/DynamicIcon';
|
||||||
|
import { Star } from 'lucide-react';
|
||||||
|
|
||||||
|
// 当图标不存在时显示 Star
|
||||||
|
<DynamicIcon name="NonExistent" fallback={Star} className="h-4 w-4" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| name | string | - | 图标名称(Lucide 图标名或 Emoji) |
|
||||||
|
| fallback | LucideIcon | FolderKanban | 默认图标组件 |
|
||||||
|
| className | string | "h-4 w-4" | CSS 类名 |
|
||||||
|
| ...其他 | LucideProps | - | 其他 Lucide 图标属性 |
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### 在表格中显示图标
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<TableCell>
|
||||||
|
{record.icon ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<DynamicIcon name={record.icon} className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在按钮中显示图标
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button>
|
||||||
|
<DynamicIcon name="Calendar" className="h-4 w-4 mr-2" />
|
||||||
|
选择日期
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 图标名称必须与 Lucide React 的图标名称完全匹配(区分大小写)
|
||||||
|
2. 查看所有可用图标:https://lucide.dev/icons
|
||||||
|
3. Emoji 会自动以 `text-lg` 大小渲染
|
||||||
|
|
||||||
60
frontend/src/components/DynamicIcon/index.tsx
Normal file
60
frontend/src/components/DynamicIcon/index.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { icons, LucideProps } from 'lucide-react';
|
||||||
|
import { FolderKanban } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DynamicIconProps extends Omit<LucideProps, 'ref'> {
|
||||||
|
/** 图标名称(Lucide React 图标名)或 emoji */
|
||||||
|
name?: string;
|
||||||
|
/** 默认图标(当找不到指定图标时显示) */
|
||||||
|
fallback?: React.ComponentType<LucideProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态图标组件
|
||||||
|
*
|
||||||
|
* 支持:
|
||||||
|
* 1. Lucide React 的所有图标(通过图标名称字符串)
|
||||||
|
* 2. Emoji 表情
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* // 使用 Lucide 图标名称
|
||||||
|
* <DynamicIcon name="Calendar" className="h-4 w-4" />
|
||||||
|
*
|
||||||
|
* // 使用 emoji
|
||||||
|
* <DynamicIcon name="📅" />
|
||||||
|
*
|
||||||
|
* // 自定义默认图标
|
||||||
|
* <DynamicIcon name="NonExistent" fallback={Star} />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const DynamicIcon: React.FC<DynamicIconProps> = ({
|
||||||
|
name,
|
||||||
|
fallback: FallbackIcon = FolderKanban,
|
||||||
|
className = "h-4 w-4",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
if (!name) {
|
||||||
|
return <FallbackIcon className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测是否为 emoji(包括各种 Unicode emoji 范围)
|
||||||
|
const isEmoji = name.length <= 4 && /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F1E0}-\u{1F1FF}]/u.test(name);
|
||||||
|
|
||||||
|
if (isEmoji) {
|
||||||
|
return <span className="inline-flex items-center justify-center text-lg">{name}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 lucide-react 的 icons 对象中动态获取图标组件
|
||||||
|
const IconComponent = icons[name as keyof typeof icons];
|
||||||
|
|
||||||
|
if (IconComponent) {
|
||||||
|
return <IconComponent className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到,显示默认图标
|
||||||
|
return <FallbackIcon className={className} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicIcon;
|
||||||
|
|
||||||
145
frontend/src/components/LucideIconSelect/README.md
Normal file
145
frontend/src/components/LucideIconSelect/README.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# LucideIconSelect 组件
|
||||||
|
|
||||||
|
Lucide React 图标选择器组件,提供可视化的图标选择界面。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- ✅ 支持所有 Lucide React 图标(1000+ 个)
|
||||||
|
- ✅ 搜索功能
|
||||||
|
- ✅ 分类浏览(12 个常用分类)
|
||||||
|
- ✅ 实时预览
|
||||||
|
- ✅ 响应式设计
|
||||||
|
- ✅ 完整的 TypeScript 类型支持
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import LucideIconSelect from '@/components/LucideIconSelect';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [iconName, setIconName] = useState('');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setOpen(true)}>
|
||||||
|
选择图标
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<LucideIconSelect
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
value={iconName}
|
||||||
|
onChange={setIconName}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在表单中使用
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import LucideIconSelect from '@/components/LucideIconSelect';
|
||||||
|
import DynamicIcon from '@/components/DynamicIcon';
|
||||||
|
|
||||||
|
function FormExample() {
|
||||||
|
const [iconSelectOpen, setIconSelectOpen] = useState(false);
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="icon"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>图标</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="点击选择图标"
|
||||||
|
value={field.value}
|
||||||
|
readOnly
|
||||||
|
onClick={() => setIconSelectOpen(true)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
{field.value && (
|
||||||
|
<div className="flex items-center justify-center w-10 h-10 border rounded-md">
|
||||||
|
<DynamicIcon name={field.value} className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<LucideIconSelect
|
||||||
|
open={iconSelectOpen}
|
||||||
|
onOpenChange={setIconSelectOpen}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| 属性 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| value | string | 否 | 当前选中的图标名称 |
|
||||||
|
| onChange | (iconName: string) => void | 否 | 图标变化回调 |
|
||||||
|
| open | boolean | 是 | 是否显示弹窗 |
|
||||||
|
| onOpenChange | (open: boolean) => void | 是 | 关闭弹窗回调 |
|
||||||
|
|
||||||
|
## 图标分类
|
||||||
|
|
||||||
|
组件内置了以下分类:
|
||||||
|
|
||||||
|
- **常用** - 常用业务图标
|
||||||
|
- **文件与文档** - 文件相关图标
|
||||||
|
- **用户与团队** - 用户管理图标
|
||||||
|
- **开发与代码** - 开发工具图标
|
||||||
|
- **数据与存储** - 数据库、存储图标
|
||||||
|
- **网络与服务** - 网络、服务器图标
|
||||||
|
- **时间与日历** - 时间、日期图标
|
||||||
|
- **操作与控制** - 操作按钮图标
|
||||||
|
- **通知与提醒** - 通知相关图标
|
||||||
|
- **编辑与格式** - 编辑工具图标
|
||||||
|
- **导航与箭头** - 导航、箭头图标
|
||||||
|
- **状态与标记** - 状态指示图标
|
||||||
|
|
||||||
|
## 自定义配置
|
||||||
|
|
||||||
|
如需添加或修改分类,可编辑 `src/config/lucideIcons.ts` 文件:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const ICON_CATEGORIES = {
|
||||||
|
'自定义分类': [
|
||||||
|
'Icon1', 'Icon2', 'Icon3'
|
||||||
|
],
|
||||||
|
// ... 其他分类
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例截图
|
||||||
|
|
||||||
|
选择器提供:
|
||||||
|
- 🔍 搜索框 - 快速查找图标
|
||||||
|
- 📑 分类标签 - 按类别浏览
|
||||||
|
- 🎨 图标网格 - 可视化选择
|
||||||
|
- ✅ 当前选中 - 实时预览
|
||||||
|
|
||||||
|
## 相关组件
|
||||||
|
|
||||||
|
配合 `DynamicIcon` 组件使用效果更佳:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import DynamicIcon from '@/components/DynamicIcon';
|
||||||
|
import LucideIconSelect from '@/components/LucideIconSelect';
|
||||||
|
```
|
||||||
|
|
||||||
141
frontend/src/components/LucideIconSelect/index.tsx
Normal file
141
frontend/src/components/LucideIconSelect/index.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { searchLucideIcons, ICON_CATEGORIES } from '@/config/lucideIcons';
|
||||||
|
import DynamicIcon from '@/components/DynamicIcon';
|
||||||
|
|
||||||
|
interface LucideIconSelectProps {
|
||||||
|
/** 当前选中的图标名称 */
|
||||||
|
value?: string;
|
||||||
|
/** 图标变化回调 */
|
||||||
|
onChange?: (iconName: string) => void;
|
||||||
|
/** 是否显示弹窗 */
|
||||||
|
open: boolean;
|
||||||
|
/** 关闭弹窗回调 */
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lucide 图标选择器组件
|
||||||
|
*
|
||||||
|
* 支持:
|
||||||
|
* - 搜索图标
|
||||||
|
* - 分类浏览
|
||||||
|
* - 点击选择
|
||||||
|
*/
|
||||||
|
const LucideIconSelect: React.FC<LucideIconSelectProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
open,
|
||||||
|
onOpenChange
|
||||||
|
}) => {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [category, setCategory] = useState('all');
|
||||||
|
|
||||||
|
// 过滤图标列表
|
||||||
|
const iconList = useMemo(() => {
|
||||||
|
return searchLucideIcons(search, category);
|
||||||
|
}, [search, category]);
|
||||||
|
|
||||||
|
// 选择图标
|
||||||
|
const handleSelect = (iconName: string) => {
|
||||||
|
onChange?.(iconName);
|
||||||
|
onOpenChange(false);
|
||||||
|
setSearch('');
|
||||||
|
setCategory('all');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>选择图标</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索图标名称..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分类标签 */}
|
||||||
|
<Tabs value={category} onValueChange={setCategory}>
|
||||||
|
<TabsList className="w-full flex-wrap h-auto">
|
||||||
|
<TabsTrigger value="all" className="text-xs">全部 ({iconList.length})</TabsTrigger>
|
||||||
|
{Object.keys(ICON_CATEGORIES).map((cat) => (
|
||||||
|
<TabsTrigger key={cat} value={cat} className="text-xs">
|
||||||
|
{cat}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value={category} className="mt-4">
|
||||||
|
{/* 图标网格 */}
|
||||||
|
<div className="grid grid-cols-8 gap-2 max-h-[400px] overflow-y-auto p-2 border rounded-md">
|
||||||
|
{iconList.length > 0 ? (
|
||||||
|
iconList.map(({ name, component: Icon }) => (
|
||||||
|
<Button
|
||||||
|
key={name}
|
||||||
|
variant={value === name ? "default" : "outline"}
|
||||||
|
className="h-20 flex flex-col items-center justify-center gap-2 p-2"
|
||||||
|
onClick={() => handleSelect(name)}
|
||||||
|
title={name}
|
||||||
|
>
|
||||||
|
<Icon className="h-6 w-6" />
|
||||||
|
<span className="text-[10px] truncate w-full text-center">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="col-span-8 text-center py-8 text-muted-foreground">
|
||||||
|
未找到匹配的图标
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* 当前选中 */}
|
||||||
|
{value && (
|
||||||
|
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
|
||||||
|
<span className="text-sm text-muted-foreground">当前选中:</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DynamicIcon name={value} className="h-5 w-5" />
|
||||||
|
<code className="text-sm">{value}</code>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="ml-auto"
|
||||||
|
onClick={() => {
|
||||||
|
onChange?.('');
|
||||||
|
onOpenChange(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LucideIconSelect;
|
||||||
|
|
||||||
104
frontend/src/config/lucideIcons.ts
Normal file
104
frontend/src/config/lucideIcons.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { icons, type LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有 Lucide React 图标列表
|
||||||
|
* @returns 图标名称和组件的数组
|
||||||
|
*/
|
||||||
|
export const getAllLucideIcons = () => {
|
||||||
|
return Object.entries(icons).map(([name, component]) => ({
|
||||||
|
name,
|
||||||
|
component: component as LucideIcon
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称获取 Lucide 图标组件
|
||||||
|
* @param iconName 图标名称
|
||||||
|
* @returns 图标组件或 null
|
||||||
|
*/
|
||||||
|
export const getLucideIcon = (iconName: string): LucideIcon | null => {
|
||||||
|
return (icons[iconName as keyof typeof icons] as LucideIcon) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 常用图标分类
|
||||||
|
*/
|
||||||
|
export const ICON_CATEGORIES = {
|
||||||
|
'常用': [
|
||||||
|
'FolderKanban', 'Workflow', 'Settings', 'Users', 'Calendar',
|
||||||
|
'Clock', 'Database', 'Server', 'Cloud', 'Package'
|
||||||
|
],
|
||||||
|
'文件与文档': [
|
||||||
|
'File', 'FileText', 'Folder', 'FolderOpen', 'Files',
|
||||||
|
'FileJson', 'FileCode', 'FilePlus', 'FileEdit'
|
||||||
|
],
|
||||||
|
'用户与团队': [
|
||||||
|
'User', 'Users', 'UserPlus', 'UserCheck', 'UserCog',
|
||||||
|
'Shield', 'ShieldCheck', 'Contact'
|
||||||
|
],
|
||||||
|
'开发与代码': [
|
||||||
|
'Code', 'Terminal', 'GitBranch', 'Github', 'GitFork',
|
||||||
|
'GitCommit', 'GitPullRequest', 'Bug', 'Braces'
|
||||||
|
],
|
||||||
|
'数据与存储': [
|
||||||
|
'Database', 'HardDrive', 'Save', 'Download', 'Upload',
|
||||||
|
'Archive', 'Package', 'Inbox', 'FolderSync'
|
||||||
|
],
|
||||||
|
'网络与服务': [
|
||||||
|
'Server', 'Cloud', 'CloudDownload', 'CloudUpload', 'Globe',
|
||||||
|
'Wifi', 'Zap', 'Activity', 'TrendingUp'
|
||||||
|
],
|
||||||
|
'时间与日历': [
|
||||||
|
'Calendar', 'CalendarDays', 'Clock', 'Timer', 'AlarmClock',
|
||||||
|
'History', 'CalendarCheck', 'CalendarPlus'
|
||||||
|
],
|
||||||
|
'操作与控制': [
|
||||||
|
'Play', 'Pause', 'Stop', 'RotateCw', 'RefreshCw',
|
||||||
|
'Power', 'Settings', 'Sliders', 'ToggleLeft'
|
||||||
|
],
|
||||||
|
'通知与提醒': [
|
||||||
|
'Bell', 'BellRing', 'MessageSquare', 'Mail', 'Inbox',
|
||||||
|
'AlertCircle', 'AlertTriangle', 'Info', 'CheckCircle'
|
||||||
|
],
|
||||||
|
'编辑与格式': [
|
||||||
|
'Edit', 'Edit2', 'Edit3', 'Pencil', 'Trash2',
|
||||||
|
'Copy', 'Clipboard', 'Scissors', 'FileEdit'
|
||||||
|
],
|
||||||
|
'导航与箭头': [
|
||||||
|
'ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'ChevronRight',
|
||||||
|
'ChevronLeft', 'ChevronUp', 'ChevronDown', 'Home', 'Menu'
|
||||||
|
],
|
||||||
|
'状态与标记': [
|
||||||
|
'Check', 'CheckCircle', 'CheckCircle2', 'X', 'XCircle',
|
||||||
|
'AlertCircle', 'AlertTriangle', 'HelpCircle', 'Plus', 'Minus'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索图标
|
||||||
|
* @param searchTerm 搜索关键词
|
||||||
|
* @param category 分类(可选)
|
||||||
|
* @returns 匹配的图标列表
|
||||||
|
*/
|
||||||
|
export const searchLucideIcons = (searchTerm: string, category?: string) => {
|
||||||
|
const allIcons = getAllLucideIcons();
|
||||||
|
|
||||||
|
let filtered = allIcons;
|
||||||
|
|
||||||
|
// 按分类过滤
|
||||||
|
if (category && category !== 'all') {
|
||||||
|
const categoryIcons = ICON_CATEGORIES[category as keyof typeof ICON_CATEGORIES] || [];
|
||||||
|
filtered = allIcons.filter(icon => categoryIcons.includes(icon.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按搜索词过滤
|
||||||
|
if (searchTerm) {
|
||||||
|
const search = searchTerm.toLowerCase();
|
||||||
|
filtered = filtered.filter(icon =>
|
||||||
|
icon.name.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
|
|
||||||
@ -0,0 +1,540 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
FolderKanban,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Calendar,
|
||||||
|
MousePointerClick,
|
||||||
|
Clock
|
||||||
|
} from "lucide-react";
|
||||||
|
import DynamicIcon from '@/components/DynamicIcon';
|
||||||
|
import LucideIconSelect from '@/components/LucideIconSelect';
|
||||||
|
import {
|
||||||
|
getWorkflowCategories,
|
||||||
|
createWorkflowCategory,
|
||||||
|
updateWorkflowCategory,
|
||||||
|
deleteWorkflowCategory
|
||||||
|
} from '../service';
|
||||||
|
import type {
|
||||||
|
WorkflowCategoryResponse,
|
||||||
|
WorkflowCategoryRequest,
|
||||||
|
WorkflowCategoryQuery
|
||||||
|
} from '../types';
|
||||||
|
import type { Page } from '@/types/base';
|
||||||
|
|
||||||
|
interface CategoryManageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<Page<WorkflowCategoryResponse> | null>(null);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [editRecord, setEditRecord] = useState<WorkflowCategoryResponse | null>(null);
|
||||||
|
const [iconSelectOpen, setIconSelectOpen] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<WorkflowCategoryRequest>({
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
description: '',
|
||||||
|
icon: '',
|
||||||
|
sort: 0,
|
||||||
|
supportedTriggers: [],
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载分类列表
|
||||||
|
const loadCategories = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const query: WorkflowCategoryQuery = {
|
||||||
|
pageNum: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
};
|
||||||
|
if (searchText) {
|
||||||
|
query.name = searchText;
|
||||||
|
}
|
||||||
|
const result = await getWorkflowCategories(query);
|
||||||
|
setData(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "加载失败",
|
||||||
|
description: "加载分类列表失败",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadCategories();
|
||||||
|
}
|
||||||
|
}, [open, searchText]);
|
||||||
|
|
||||||
|
// 触发方式选项
|
||||||
|
const triggerOptions = [
|
||||||
|
{ value: 'MANUAL', label: '手动触发', icon: MousePointerClick },
|
||||||
|
{ value: 'SCHEDULED', label: '定时触发', icon: Clock },
|
||||||
|
{ value: 'EVENT', label: '事件触发', icon: Calendar },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 新建
|
||||||
|
const handleCreate = () => {
|
||||||
|
setEditRecord(null);
|
||||||
|
form.reset({
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
description: '',
|
||||||
|
icon: '',
|
||||||
|
sort: 0,
|
||||||
|
supportedTriggers: [],
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (record: WorkflowCategoryResponse) => {
|
||||||
|
setEditRecord(record);
|
||||||
|
form.reset({
|
||||||
|
name: record.name,
|
||||||
|
code: record.code,
|
||||||
|
description: record.description || '',
|
||||||
|
icon: record.icon || '',
|
||||||
|
sort: record.sort,
|
||||||
|
supportedTriggers: record.supportedTriggers || [],
|
||||||
|
enabled: record.enabled,
|
||||||
|
});
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (record: WorkflowCategoryResponse) => {
|
||||||
|
try {
|
||||||
|
await deleteWorkflowCategory(record.id);
|
||||||
|
toast({
|
||||||
|
title: "删除成功",
|
||||||
|
description: `分类 "${record.name}" 已删除`,
|
||||||
|
});
|
||||||
|
loadCategories();
|
||||||
|
onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "删除失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
const handleSave = async (values: WorkflowCategoryRequest) => {
|
||||||
|
try {
|
||||||
|
if (editRecord) {
|
||||||
|
await updateWorkflowCategory(editRecord.id, values);
|
||||||
|
toast({
|
||||||
|
title: "更新成功",
|
||||||
|
description: `分类 "${values.name}" 已更新`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createWorkflowCategory(values);
|
||||||
|
toast({
|
||||||
|
title: "创建成功",
|
||||||
|
description: `分类 "${values.name}" 已创建`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setEditMode(false);
|
||||||
|
loadCategories();
|
||||||
|
onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "保存失败",
|
||||||
|
description: error instanceof Error ? error.message : '未知错误',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditMode(false);
|
||||||
|
setEditRecord(null);
|
||||||
|
form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染触发方式
|
||||||
|
const renderTriggers = (triggers?: string[]) => {
|
||||||
|
if (!triggers || triggers.length === 0) {
|
||||||
|
return <span className="text-muted-foreground text-sm">-</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 flex-wrap max-w-[240px]">
|
||||||
|
{triggers.map(trigger => {
|
||||||
|
const option = triggerOptions.find(o => o.value === trigger);
|
||||||
|
if (!option) return null;
|
||||||
|
const Icon = option.icon;
|
||||||
|
return (
|
||||||
|
<Badge key={trigger} variant="outline" className="inline-flex items-center gap-1 text-xs whitespace-nowrap">
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{option.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FolderKanban className="h-5 w-5" />
|
||||||
|
分类管理
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{!editMode ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 搜索和新建 */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索分类名称..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCreate}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
新建分类
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分类列表 */}
|
||||||
|
<div className="rounded-md border overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[160px]">分类名称</TableHead>
|
||||||
|
<TableHead className="w-[140px]">分类代码</TableHead>
|
||||||
|
<TableHead className="w-[60px]">图标</TableHead>
|
||||||
|
<TableHead className="w-[240px]">支持触发方式</TableHead>
|
||||||
|
<TableHead className="w-[80px] text-center">排序</TableHead>
|
||||||
|
<TableHead className="w-[80px]">状态</TableHead>
|
||||||
|
<TableHead className="min-w-[150px]">描述</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-right sticky right-0 bg-background shadow-[-2px_0_4px_rgba(0,0,0,0.05)]">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="h-24 text-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : data?.content && data.content.length > 0 ? (
|
||||||
|
data.content.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell className="font-medium text-sm">{record.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-xs bg-muted px-2 py-0.5 rounded whitespace-nowrap">
|
||||||
|
{record.code}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{record.icon ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<DynamicIcon name={record.icon} className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-sm">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{renderTriggers(record.supportedTriggers)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">{record.sort}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={record.enabled ? "success" : "secondary"}
|
||||||
|
className="inline-flex items-center gap-1 text-xs whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{record.enabled ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
启用
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="h-3 w-3" />
|
||||||
|
禁用
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="max-w-[200px] truncate text-sm" title={record.description}>
|
||||||
|
{record.description || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="sticky right-0 bg-background shadow-[-2px_0_4px_rgba(0,0,0,0.05)]">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
>
|
||||||
|
<Edit className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="h-24 text-center text-muted-foreground">
|
||||||
|
暂无分类数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 编辑表单 */
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSave)} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
rules={{ required: '请输入分类名称' }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>分类名称</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="输入分类名称" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="code"
|
||||||
|
rules={{ required: '请输入分类代码' }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>分类代码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="输入分类代码" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="icon"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>图标</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="点击选择图标"
|
||||||
|
value={field.value}
|
||||||
|
readOnly
|
||||||
|
onClick={() => setIconSelectOpen(true)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
{field.value && (
|
||||||
|
<div className="flex items-center justify-center w-10 h-10 border rounded-md">
|
||||||
|
<DynamicIcon name={field.value} className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<LucideIconSelect
|
||||||
|
open={iconSelectOpen}
|
||||||
|
onOpenChange={setIconSelectOpen}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>排序</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="输入排序值"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="supportedTriggers"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>支持的触发方式</FormLabel>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{triggerOptions.map((option) => {
|
||||||
|
const Icon = option.icon;
|
||||||
|
const isSelected = field.value?.includes(option.value);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
variant={isSelected ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const current = field.value || [];
|
||||||
|
if (isSelected) {
|
||||||
|
field.onChange(current.filter(v => v !== option.value));
|
||||||
|
} else {
|
||||||
|
field.onChange([...current, option.value]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>描述</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea placeholder="输入分类描述" rows={3} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">启用状态</FormLabel>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
控制此分类是否可用
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||||
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryManageDialog;
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { DataTablePagination } from '@/components/ui/pagination';
|
import { DataTablePagination } from '@/components/ui/pagination';
|
||||||
import {
|
import {
|
||||||
Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2,
|
Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2,
|
||||||
Clock, Activity, Workflow, Eye, Pencil
|
Clock, Activity, Workflow, Eye, Pencil, FolderKanban
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service';
|
import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service';
|
||||||
@ -19,6 +19,7 @@ import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
|||||||
import EditModal from './components/EditModal';
|
import EditModal from './components/EditModal';
|
||||||
import DeleteDialog from './components/DeleteDialog';
|
import DeleteDialog from './components/DeleteDialog';
|
||||||
import DeployDialog from './components/DeployDialog';
|
import DeployDialog from './components/DeployDialog';
|
||||||
|
import CategoryManageDialog from './components/CategoryManageDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流定义列表页
|
* 工作流定义列表页
|
||||||
@ -35,6 +36,7 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
const [deleteRecord, setDeleteRecord] = useState<WorkflowDefinition | null>(null);
|
const [deleteRecord, setDeleteRecord] = useState<WorkflowDefinition | null>(null);
|
||||||
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
|
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
|
||||||
const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null);
|
const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null);
|
||||||
|
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
|
||||||
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
||||||
pageNum: DEFAULT_CURRENT - 1,
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
pageSize: DEFAULT_PAGE_SIZE,
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
@ -256,10 +258,16 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>工作流列表</CardTitle>
|
<CardTitle>工作流列表</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
|
||||||
|
<FolderKanban className="h-4 w-4 mr-2" />
|
||||||
|
分类管理
|
||||||
|
</Button>
|
||||||
<Button onClick={handleCreate}>
|
<Button onClick={handleCreate}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
新建工作流
|
新建工作流
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
@ -480,6 +488,13 @@ const WorkflowDefinitionList: React.FC = () => {
|
|||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 分类管理弹窗 */}
|
||||||
|
<CategoryManageDialog
|
||||||
|
open={categoryDialogOpen}
|
||||||
|
onOpenChange={setCategoryDialogOpen}
|
||||||
|
onSuccess={loadCategories}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user