feat: add new Flowme and FlowmeChannel management with CRUD operations and UI components
This commit is contained in:
@@ -62,7 +62,7 @@ export const AppDeleteModal = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tips</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='w-[400px]'>
|
||||
<div className=''>
|
||||
<p className='text-sm text-gray-500'>Delete App Introduce</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -197,7 +197,7 @@ const ShareModal = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>分享</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='flex flex-col gap-2 w-[400px] '>
|
||||
<div className='flex flex-col gap-2 '>
|
||||
<PermissionManager
|
||||
value={permission}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -123,7 +123,7 @@ const FomeModal = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加域名</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 w-[500px]">
|
||||
<div className="p-4">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">域名</label>
|
||||
|
||||
187
src/app/flowme/channel/page.tsx
Normal file
187
src/app/flowme/channel/page.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import { useFlowmeChannelStore } from '../store/channel';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { pick } from 'es-toolkit';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { LayoutMain } from '@/modules/layout';
|
||||
|
||||
const TableList = () => {
|
||||
const { list, setShowEdit, setFormData, deleteData } = useFlowmeChannelStore();
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>标题</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
<TableHead>颜色</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.title}</TableCell>
|
||||
<TableCell>{item.description}</TableCell>
|
||||
<TableCell>{item.tags?.join(', ')}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: item.color }} />
|
||||
{item.color}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData(item);
|
||||
}}>
|
||||
<Pencil className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteData(item.id)}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FormModal = () => {
|
||||
const { showEdit, setShowEdit, formData, updateData } = useFlowmeChannelStore();
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
} = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (!showEdit) return;
|
||||
if (formData?.id) {
|
||||
reset(formData);
|
||||
} else {
|
||||
reset({
|
||||
title: '',
|
||||
description: '',
|
||||
color: '#007bff',
|
||||
});
|
||||
}
|
||||
}, [formData, showEdit, reset]);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
const _formData: any = pick(data, ['title', 'description', 'tags', 'link', 'data', 'color']);
|
||||
if (formData.id) {
|
||||
_formData.id = formData.id;
|
||||
}
|
||||
const res = await updateData(_formData);
|
||||
if (res.code === 200) {
|
||||
setShowEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={showEdit} onOpenChange={setShowEdit}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formData?.id ? '编辑' : '添加'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">标题</label>
|
||||
<Input
|
||||
{...control.register('title')}
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Input
|
||||
{...control.register('description')}
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">颜色</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
{...control.register('color')}
|
||||
placeholder="请输入颜色值"
|
||||
/>
|
||||
<Input
|
||||
type="color"
|
||||
{...control.register('color')}
|
||||
className="w-12 h-10 p-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit">提交</Button>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const { getList, setShowEdit, setFormData } = useFlowmeChannelStore();
|
||||
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, [getList]);
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full h-full">
|
||||
<div className="flex mb-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData({});
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Dialog>
|
||||
</div>
|
||||
<TableList />
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain><List /></LayoutMain>;
|
||||
}
|
||||
196
src/app/flowme/page.tsx
Normal file
196
src/app/flowme/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import { useFlowmeStore } from './store/index';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { pick } from 'es-toolkit';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { LayoutMain } from '@/modules/layout';
|
||||
|
||||
const TableList = () => {
|
||||
const { list, setShowEdit, setFormData, deleteData } = useFlowmeStore();
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>标题</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>重要性</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.title}</TableCell>
|
||||
<TableCell>{item.description}</TableCell>
|
||||
<TableCell>{item.tags?.join(', ')}</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
<TableCell>{item.source}</TableCell>
|
||||
<TableCell>{item.importance}</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData(item);
|
||||
}}>
|
||||
<Pencil className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteData(item.id)}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FormModal = () => {
|
||||
const { showEdit, setShowEdit, formData, updateData } = useFlowmeStore();
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
} = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (!showEdit) return;
|
||||
if (formData?.id) {
|
||||
reset(formData);
|
||||
} else {
|
||||
reset({
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
source: '',
|
||||
importance: 0,
|
||||
});
|
||||
}
|
||||
}, [formData, showEdit, reset]);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
const _formData: any = pick(data, ['title', 'description', 'type', 'source', 'importance', 'tags', 'link', 'data', 'channelId']);
|
||||
if (formData.id) {
|
||||
_formData.id = formData.id;
|
||||
}
|
||||
const res = await updateData(_formData);
|
||||
if (res.code === 200) {
|
||||
setShowEdit(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={showEdit} onOpenChange={setShowEdit}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formData?.id ? '编辑' : '添加'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">标题</label>
|
||||
<Input
|
||||
{...control.register('title')}
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Input
|
||||
{...control.register('description')}
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">类型</label>
|
||||
<Input
|
||||
{...control.register('type')}
|
||||
placeholder="请输入类型"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">来源</label>
|
||||
<Input
|
||||
{...control.register('source')}
|
||||
placeholder="请输入来源"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">重要性</label>
|
||||
<Input
|
||||
type="number"
|
||||
{...control.register('importance', { valueAsNumber: true })}
|
||||
placeholder="请输入重要性等级"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">提交</Button>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const { getList, setShowEdit, setFormData } = useFlowmeStore();
|
||||
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, [getList]);
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full h-full">
|
||||
<div className="flex mb-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData({});
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Dialog>
|
||||
</div>
|
||||
<TableList />
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain><List /></LayoutMain>;
|
||||
}
|
||||
103
src/app/flowme/store/channel.ts
Normal file
103
src/app/flowme/store/channel.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast as message } from 'sonner';
|
||||
|
||||
export type FlowmeChannelType = {
|
||||
id: string;
|
||||
uid?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
link: string;
|
||||
data: Record<string, any>;
|
||||
color: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type FlowmeChannelStore = {
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
formData: Partial<FlowmeChannelType>;
|
||||
setFormData: (formData: Partial<FlowmeChannelType>) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: FlowmeChannelType[];
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: Partial<FlowmeChannelType>) => Promise<any>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
detail: FlowmeChannelType | null;
|
||||
setDetail: (detail: FlowmeChannelType | null) => void;
|
||||
getDetail: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useFlowmeChannelStore = create<FlowmeChannelStore>((set, get) => {
|
||||
return {
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
formData: {},
|
||||
setFormData: (formData) => set({ formData }),
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
list: [],
|
||||
getList: async () => {
|
||||
set({ loading: true });
|
||||
const res = await query.post({
|
||||
path: 'flowme-channel',
|
||||
key: 'list',
|
||||
});
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data.list });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'flowme-channel',
|
||||
key: 'update',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, formData: res.data });
|
||||
getList();
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
deleteData: async (id) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'flowme-channel',
|
||||
key: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
if (res.code === 200) {
|
||||
getList();
|
||||
message.success('Success');
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
detail: null,
|
||||
setDetail: (detail) => set({ detail }),
|
||||
getDetail: async (id) => {
|
||||
set({ detail: null });
|
||||
const res = await query.post({
|
||||
path: 'flowme-channel',
|
||||
key: 'get',
|
||||
data: { id },
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ detail: res.data });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
107
src/app/flowme/store/index.ts
Normal file
107
src/app/flowme/store/index.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast as message } from 'sonner';
|
||||
|
||||
export type FlowmeType = {
|
||||
id: string;
|
||||
uid?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
link: string;
|
||||
data: Record<string, any>;
|
||||
channelId?: string;
|
||||
type: string;
|
||||
source: string;
|
||||
importance: number;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type FlowmeStore = {
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
formData: Partial<FlowmeType>;
|
||||
setFormData: (formData: Partial<FlowmeType>) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: FlowmeType[];
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: Partial<FlowmeType>) => Promise<any>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
detail: FlowmeType | null;
|
||||
setDetail: (detail: FlowmeType | null) => void;
|
||||
getDetail: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useFlowmeStore = create<FlowmeStore>((set, get) => {
|
||||
return {
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
formData: {},
|
||||
setFormData: (formData) => set({ formData }),
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
list: [],
|
||||
getList: async () => {
|
||||
set({ loading: true });
|
||||
const res = await query.post({
|
||||
path: 'flowme',
|
||||
key: 'list',
|
||||
});
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data.list });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'flowme',
|
||||
key: 'update',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, formData: res.data });
|
||||
getList();
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
deleteData: async (id) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'flowme',
|
||||
key: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
if (res.code === 200) {
|
||||
getList();
|
||||
message.success('Success');
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
detail: null,
|
||||
setDetail: (detail) => set({ detail }),
|
||||
getDetail: async (id) => {
|
||||
set({ detail: null });
|
||||
const res = await query.post({
|
||||
path: 'flowme',
|
||||
key: 'get',
|
||||
data: { id },
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ detail: res.data });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user