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

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.

214
public/sse-test.html Normal file
View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.status.connected { background: #d4edda; color: #155724; }
.status.connecting { background: #fff3cd; color: #856404; }
.status.disconnected { background: #f8d7da; color: #721c24; }
.events {
border: 1px solid #ddd;
padding: 10px;
height: 400px;
overflow-y: auto;
background: #f9f9f9;
}
.event-item {
padding: 8px;
margin: 5px 0;
background: #fff;
border: 1px solid #eee;
border-radius: 4px;
}
.event-item .time {
color: #888;
font-size: 12px;
}
.event-item .data {
white-space: pre-wrap;
word-break: break-all;
}
</style>
</head>
<body>
<h1>SSE / HTTP Stream 测试</h1>
<div>
<label>URL: </label>
<input type="text" id="url" value="http://localhost:4005/root/v3/" style="width: 400px;">
<label>类型: </label>
<select id="streamType">
<option value="sse">SSE (EventSource)</option>
<option value="stream">HTTP Stream (Fetch)</option>
</select>
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
</div>
<div id="status" class="status disconnected">未连接</div>
<h3>事件日志</h3>
<div id="events" class="events"></div>
<script>
let eventSource = null;
let abortController = null;
async function connect() {
const url = document.getElementById('url').value;
const type = document.getElementById('streamType').value;
if (!url) {
alert('请输入 URL');
return;
}
disconnect();
const eventsDiv = document.getElementById('events');
eventsDiv.innerHTML = '';
updateStatus('connecting', '正在连接...');
if (type === 'sse') {
connectSSE(url);
} else {
connectStream(url);
}
}
function connectSSE(url) {
try {
eventSource = new EventSource(url);
eventSource.onopen = function() {
updateStatus('connected', '已连接 (SSE)');
addEvent('系统', 'SSE 连接已建立');
};
eventSource.onmessage = function(event) {
addEvent('消息', event.data, 'message');
};
eventSource.onerror = function(error) {
// 如果 readyState 是 CLOSED说明是后端主动关闭不重连
if (eventSource.readyState === EventSource.CLOSED) {
updateStatus('disconnected', '连接已关闭');
addEvent('系统', '后端已关闭连接,无须重连');
return;
}
// 如果是其他错误状态,显示错误信息,不自动重连
updateStatus('disconnected', '连接错误');
addEvent('错误', 'SSE 连接发生错误');
};
eventSource.addEventListener('data', function(event) {
addEvent('自定义事件', event.data, 'data');
});
} catch (e) {
updateStatus('disconnected', '连接失败: ' + e.message);
addEvent('错误', e.message);
}
}
async function connectStream(url) {
try {
abortController = new AbortController();
updateStatus('connected', '已连接 (Stream)');
addEvent('系统', 'HTTP Stream 连接已建立');
const response = await fetch(url, {
signal: abortController.signal,
headers: {
'Accept': 'text/plain, text/event-stream'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
addEvent('系统', 'Stream 连接已关闭');
break;
}
// 解码并处理数据
buffer += decoder.decode(value, { stream: true });
// 按行处理
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
addEvent('数据', line, 'stream');
}
}
}
} catch (e) {
if (e.name !== 'AbortError') {
updateStatus('disconnected', '连接错误: ' + e.message);
addEvent('错误', e.message);
}
}
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
if (abortController) {
abortController.abort();
abortController = null;
}
updateStatus('disconnected', '已断开');
addEvent('系统', '连接已关闭');
}
function updateStatus(type, message) {
const statusDiv = document.getElementById('status');
statusDiv.className = 'status ' + type;
statusDiv.textContent = message;
}
function addEvent(type, data, eventType = '') {
const eventsDiv = document.getElementById('events');
const item = document.createElement('div');
item.className = 'event-item';
const time = new Date().toLocaleTimeString();
item.innerHTML = `
<div class="time">[${time}] ${eventType ? '[' + eventType + '] ' : ''}${type}</div>
<div class="data">${escapeHtml(data)}</div>
`;
eventsDiv.insertBefore(item, eventsDiv.firstChild);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>

9
skills/page/SKILL.md Normal file
View File

@@ -0,0 +1,9 @@
---
name: new-page
description: 创建一个新页面
---
## 参考当前的文档
`./references/*.ts`

View File

@@ -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 (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead>UID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map((domain) => (
<TableRow key={domain.id}>
<TableCell>{domain.id}</TableCell>
<TableCell>{domain.domain}</TableCell>
<TableCell>{domain.appId}</TableCell>
<TableCell>{domain.uid}</TableCell>
<TableCell>{domain.status}</TableCell>
<TableCell className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setShowEditModal(true);
setFormData(domain);
}}>
<Pencil className="w-4 h-4 mr-1" />
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => deleteDomain(domain)}>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
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 (
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
<DialogContent>
<DialogHeader>
<DialogTitle></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('domain', { required: '请输入域名' })}
placeholder="请输入域名"
className={errors.domain ? "border-red-500" : ""}
/>
{errors.domain && <span className="text-xs text-red-500">{errors.domain.message as string}</span>}
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">ID</label>
<Input
{...control.register('appId', { required: '请输入应用ID' })}
placeholder="请输入应用ID"
className={errors.appId ? "border-red-500" : ""}
/>
{errors.appId && <span className="text-xs text-red-500">{errors.appId.message as string}</span>}
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium"></label>
<Controller
name="status"
control={control}
defaultValue=""
render={({ field }) => (
<Select value={field.value || ''} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="请选择状态" />
</SelectTrigger>
<SelectContent>
{appDomainStatus.map((item) => (
<SelectItem key={item} value={item}>{item}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</div>
<Button type="submit"></Button>
</form>
</div>
</DialogContent>
</Dialog>
);
};
export const List = () => {
const { getDomainList, setShowEditModal, setFormData } = useDomainStore();
useEffect(() => {
getDomainList();
}, [getDomainList]);
return (
<div className="p-4 w-full h-full">
<div className="flex mb-4">
<Dialog>
<DialogTrigger asChild>
<Button
onClick={() => {
setShowEditModal(true);
setFormData({});
}}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</DialogTrigger>
</Dialog>
</div>
<TableList />
<FomeModal />
</div>
);
};
export default () => {
return <LayoutMain><List /></LayoutMain>;
}

View File

@@ -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<any>;
updateDomain: (data: { domain: string; id: string; [key: string]: any }, opts?: { refresh?: boolean }) => Promise<any>;
deleteDomain: (data: { id: string }) => Promise<any>;
getDomainDetail: (data: { domain?: string; id?: string }) => Promise<any>;
list: Domain[];
setList: (list: Domain[]) => void;
formData: any;
setFormData: (formData: any) => void;
showEditModal: boolean;
setShowEditModal: (showEditModal: boolean) => void;
}
export const useDomainStore = create<Store>((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 }),
}));

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