diff --git a/src/app/flowme/channel/page.tsx b/src/app/flowme/channel/page.tsx
new file mode 100644
index 0000000..5d71165
--- /dev/null
+++ b/src/app/flowme/channel/page.tsx
@@ -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 (
+
+
+
+
+ 标题
+ 描述
+ 标签
+ 颜色
+ 操作
+
+
+
+ {list.map((item) => (
+
+ {item.title}
+ {item.description}
+ {item.tags?.join(', ')}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+export const List = () => {
+ const { getList, setShowEdit, setFormData } = useFlowmeChannelStore();
+
+ useEffect(() => {
+ getList();
+ }, [getList]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default () => {
+ return
;
+}
diff --git a/src/app/flowme/page.tsx b/src/app/flowme/page.tsx
new file mode 100644
index 0000000..2d321d7
--- /dev/null
+++ b/src/app/flowme/page.tsx
@@ -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 (
+
+
+
+
+ 标题
+ 描述
+ 标签
+ 类型
+ 来源
+ 重要性
+ 操作
+
+
+
+ {list.map((item) => (
+
+ {item.title}
+ {item.description}
+ {item.tags?.join(', ')}
+ {item.type}
+ {item.source}
+ {item.importance}
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+export const List = () => {
+ const { getList, setShowEdit, setFormData } = useFlowmeStore();
+
+ useEffect(() => {
+ getList();
+ }, [getList]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default () => {
+ return
;
+}
diff --git a/src/app/flowme/store/channel.ts b/src/app/flowme/store/channel.ts
new file mode 100644
index 0000000..8384740
--- /dev/null
+++ b/src/app/flowme/store/channel.ts
@@ -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
;
+ color: string;
+ createdAt: string;
+ updatedAt: string;
+};
+
+type FlowmeChannelStore = {
+ showEdit: boolean;
+ setShowEdit: (showEdit: boolean) => void;
+ formData: Partial;
+ setFormData: (formData: Partial) => void;
+ loading: boolean;
+ setLoading: (loading: boolean) => void;
+ list: FlowmeChannelType[];
+ getList: () => Promise;
+ updateData: (data: Partial) => Promise;
+ deleteData: (id: string) => Promise;
+ detail: FlowmeChannelType | null;
+ setDetail: (detail: FlowmeChannelType | null) => void;
+ getDetail: (id: string) => Promise;
+};
+
+export const useFlowmeChannelStore = create((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');
+ }
+ },
+ };
+});
diff --git a/src/app/flowme/store/index.ts b/src/app/flowme/store/index.ts
new file mode 100644
index 0000000..b8e4c25
--- /dev/null
+++ b/src/app/flowme/store/index.ts
@@ -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;
+ channelId?: string;
+ type: string;
+ source: string;
+ importance: number;
+ isArchived: boolean;
+ createdAt: string;
+ updatedAt: string;
+};
+
+type FlowmeStore = {
+ showEdit: boolean;
+ setShowEdit: (showEdit: boolean) => void;
+ formData: Partial;
+ setFormData: (formData: Partial) => void;
+ loading: boolean;
+ setLoading: (loading: boolean) => void;
+ list: FlowmeType[];
+ getList: () => Promise;
+ updateData: (data: Partial) => Promise;
+ deleteData: (id: string) => Promise;
+ detail: FlowmeType | null;
+ setDetail: (detail: FlowmeType | null) => void;
+ getDetail: (id: string) => Promise;
+};
+
+export const useFlowmeStore = create((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');
+ }
+ },
+ };
+});