diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..b87975d 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./dist/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/public/sse-test.html b/public/sse-test.html new file mode 100644 index 0000000..5938573 --- /dev/null +++ b/public/sse-test.html @@ -0,0 +1,214 @@ + + + + + + SSE 测试 + + + +

SSE / HTTP Stream 测试

+
+ + + + + + +
+
未连接
+

事件日志

+
+ + + + diff --git a/skills/page/SKILL.md b/skills/page/SKILL.md new file mode 100644 index 0000000..4df4e1d --- /dev/null +++ b/skills/page/SKILL.md @@ -0,0 +1,9 @@ +--- +name: new-page +description: 创建一个新页面 +--- + +## 参考当前的文档 + + +`./references/*.ts` \ No newline at end of file diff --git a/skills/page/references/page.tsx b/skills/page/references/page.tsx new file mode 100644 index 0000000..9b4cbc7 --- /dev/null +++ b/skills/page/references/page.tsx @@ -0,0 +1,205 @@ +'use client'; +import { useEffect } from 'react'; +import { appDomainStatus, useDomainStore } from './store/index '; +import { useForm, Controller } 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Plus, Pencil, Trash2 } from 'lucide-react'; +import { LayoutUser } from '@/modules/layout/LayoutUser'; +import { LayoutMain } from '@/modules/layout'; + +const TableList = () => { + const { list, setShowEditModal, setFormData, deleteDomain } = useDomainStore(); + useEffect(() => { + // Initial load is handled by the parent component + }, []); + + return ( +
+ + + + ID + 域名 + 应用ID + UID + 状态 + 操作 + + + + {list.map((domain) => ( + + {domain.id} + {domain.domain} + {domain.appId} + {domain.uid} + {domain.status} + + + + + + ))} + +
+
+ ); +}; + +const FomeModal = () => { + const { showEditModal, setShowEditModal, formData, updateDomain } = useDomainStore(); + const { + handleSubmit, + formState: { errors }, + reset, + control, + setValue, + } = useForm(); + + useEffect(() => { + if (!showEditModal) return; + if (formData?.id) { + reset(formData); + } else { + reset({ + status: 'running', + }); + } + }, [formData, showEditModal, reset]); + + const onSubmit = async (data: any) => { + const _formData = pick(data, ['domain', 'appId', 'status', 'id']); + if (formData.id) { + _formData.id = formData.id; + } + const res = await updateDomain(_formData); + if (res.code === 200) { + setShowEditModal(false); + } + }; + + return ( + + + + 添加域名 + +
+
+
+ + + {errors.domain && {errors.domain.message as string}} +
+
+ + + {errors.appId && {errors.appId.message as string}} +
+
+ + ( + + )} + /> +
+ +
+
+
+
+ ); +}; + +export const List = () => { + const { getDomainList, setShowEditModal, setFormData } = useDomainStore(); + + useEffect(() => { + getDomainList(); + }, [getDomainList]); + + return ( +
+
+ + + + + +
+ + +
+ ); +}; + +export default () => { + return ; +} \ No newline at end of file diff --git a/skills/page/references/store/index .ts b/skills/page/references/store/index .ts new file mode 100644 index 0000000..fd1d43c --- /dev/null +++ b/skills/page/references/store/index .ts @@ -0,0 +1,92 @@ +'use strict'; +import { create } from 'zustand'; +import { query } from '@/modules/query'; +import { toast } from 'sonner'; + +// 审核,通过,驳回 +export const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const; + +type AppDomainStatus = (typeof appDomainStatus)[number]; +type Domain = { + id: string; + domain: string; + appId?: string; + status: AppDomainStatus; + data?: any; + uid?: string; + createdAt: string; + updatedAt: string; +}; +interface Store { + getDomainList: () => Promise; + updateDomain: (data: { domain: string; id: string; [key: string]: any }, opts?: { refresh?: boolean }) => Promise; + deleteDomain: (data: { id: string }) => Promise; + getDomainDetail: (data: { domain?: string; id?: string }) => Promise; + list: Domain[]; + setList: (list: Domain[]) => void; + formData: any; + setFormData: (formData: any) => void; + showEditModal: boolean; + setShowEditModal: (showEditModal: boolean) => void; +} + +export const useDomainStore = create((set, get) => ({ + getDomainList: async () => { + const res = await query.get({ + path: 'app.domain.manager', + key: 'list', + }); + if (res.code === 200) { + set({ list: res.data?.list || [] }); + } + return res; + }, + updateDomain: async (data: any, opts?: { refresh?: boolean }) => { + const res = await query.post({ + path: 'app.domain.manager', + key: 'update', + data, + }); + if (res.code === 200) { + const list = get().list; + set({ list: list.map((item) => (item.id === data.id ? res.data : item)) }); + toast.success('更新成功'); + if (opts?.refresh ?? true) { + get().getDomainList(); + } + } else { + toast.error(res.message || '更新失败'); + } + return res; + }, + deleteDomain: async (data: any) => { + const res = await query.post({ + path: 'app.domain.manager', + key: 'delete', + data, + }); + if (res.code === 200) { + const list = get().list; + set({ list: list.filter((item) => item.id !== data.id) }); + toast.success('删除成功'); + } + return res; + }, + getDomainDetail: async (data: any) => { + const res = await query.post({ + path: 'app.domain.manager', + key: 'get', + data, + }); + if (res.code === 200) { + set({ formData: res.data }); + } + return res; + }, + list: [], + setList: (list: any[]) => set({ list }), + formData: {}, + setFormData: (formData: any) => set({ formData }), + showEditModal: false, + setShowEditModal: (showEditModal: boolean) => set({ showEditModal }), +})); diff --git a/src/app/apps/modules/AppDeleteModal.tsx b/src/app/apps/modules/AppDeleteModal.tsx index baf5bda..f21b0e7 100644 --- a/src/app/apps/modules/AppDeleteModal.tsx +++ b/src/app/apps/modules/AppDeleteModal.tsx @@ -62,7 +62,7 @@ export const AppDeleteModal = () => { Tips -
+

Delete App Introduce

diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx index 67aeea7..11a107b 100644 --- a/src/app/apps/page.tsx +++ b/src/app/apps/page.tsx @@ -197,7 +197,7 @@ const ShareModal = () => { 分享 -
+
{ diff --git a/src/app/domain/page.tsx b/src/app/domain/page.tsx index 66b13d4..9b4cbc7 100644 --- a/src/app/domain/page.tsx +++ b/src/app/domain/page.tsx @@ -123,7 +123,7 @@ const FomeModal = () => { 添加域名 -
+
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(', ')} + +
+
+ {item.color} +
+ + + + + + + ))} + +
+
+ ); +}; + +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 ( + + + + {formData?.id ? '编辑' : '添加'} + +
+ +
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+
+ ); +}; + +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 ( + + + + {formData?.id ? '编辑' : '添加'} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ ); +}; + +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'); + } + }, + }; +});