feat: add new Flowme and FlowmeChannel management with CRUD operations and UI components

This commit is contained in:
2026-02-01 03:57:20 +08:00
parent a4e17023d0
commit cc466f7bd4
12 changed files with 1117 additions and 4 deletions

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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>

View 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
View 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>;
}

View 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');
}
},
};
});

View 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');
}
},
};
});