generated from kevisual/vite-react-template
Auto commit: 2026-03-19 01:46
This commit is contained in:
@@ -1,30 +1,421 @@
|
||||
import { useWorkspaceStore, type WorkspaceState } from "./store";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { useMarkStore, useWorkspaceStore } from "./store";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const App = () => {
|
||||
const markStore = useMarkStore(useShallow(state => {
|
||||
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
||||
return {
|
||||
init: state.init,
|
||||
list: state.list,
|
||||
loading: state.loading,
|
||||
getList: state.getList,
|
||||
createItem: state.createItem,
|
||||
updateItem: state.updateItem,
|
||||
deleteItem: state.deleteItem,
|
||||
showCreateDialog: state.showCreateDialog,
|
||||
setShowCreateDialog: state.setShowCreateDialog,
|
||||
showEditDialog: state.showEditDialog,
|
||||
setShowEditDialog: state.setShowEditDialog,
|
||||
editingItem: state.editingItem,
|
||||
setEditingItem: state.setEditingItem,
|
||||
}
|
||||
}));
|
||||
const workspaceStore = useWorkspaceStore(useShallow(state => {
|
||||
return {
|
||||
edit: state.edit,
|
||||
setEdit: state.setEdit,
|
||||
}
|
||||
}));
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
markStore.init('cnb');
|
||||
}, []);
|
||||
console.log('markStore.list', markStore.list);
|
||||
workspaceStore.getList({ search });
|
||||
}, [search]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('确定要删除这个workspace吗?')) {
|
||||
await workspaceStore.deleteItem(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
workspaceStore.getList({ search });
|
||||
};
|
||||
|
||||
const handleEdit = (item: any) => {
|
||||
workspaceStore.setEditingItem(item);
|
||||
workspaceStore.setShowEditDialog(true);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
workspaceStore.setShowCreateDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Workspaces</h1>
|
||||
<p>This is the workspaces page.</p>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h1>Workspaces</h1>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索workspace..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ padding: '8px 12px', border: '1px solid #ddd', borderRadius: '4px', width: '200px' }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
style={{ padding: '8px 16px', background: '#666', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
style={{ padding: '8px 16px', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
||||
>
|
||||
创建Workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{workspaceStore.loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>加载中...</div>
|
||||
) : workspaceStore.list.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>暂无workspace数据</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
|
||||
{workspaceStore.list.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
background: '#fff',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{item.name || '未命名'}</h3>
|
||||
<p style={{ margin: '0 0 8px 0', color: '#666', fontSize: '14px' }}>ID: {item.id}</p>
|
||||
<p style={{ margin: '0 0 8px 0', color: '#999', fontSize: '12px' }}>
|
||||
创建时间: {item.created_at ? new Date(item.created_at).toLocaleString() : '-'}
|
||||
</p>
|
||||
<p style={{ margin: '0 0 8px 0', color: '#999', fontSize: '12px' }}>
|
||||
更新时间: {item.updated_at ? new Date(item.updated_at).toLocaleString() : '-'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
||||
<button
|
||||
onClick={() => handleEdit(item)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#0070f3',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#ff4d4f',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建弹窗 */}
|
||||
<CreateDialog
|
||||
open={workspaceStore.showCreateDialog}
|
||||
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
||||
onSubmit={workspaceStore.createItem}
|
||||
/>
|
||||
|
||||
{/* 编辑弹窗 */}
|
||||
<EditDialog
|
||||
open={workspaceStore.showEditDialog}
|
||||
item={workspaceStore.editingItem}
|
||||
onClose={() => {
|
||||
workspaceStore.setShowEditDialog(false);
|
||||
workspaceStore.setEditingItem(null);
|
||||
}}
|
||||
onSubmit={workspaceStore.updateItem}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
// 创建弹窗组件
|
||||
function CreateDialog({ open, onClose, onSubmit }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { name: string, data?: any }) => Promise<void>;
|
||||
}) {
|
||||
const [name, setName] = useState('');
|
||||
const [data, setData] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName('');
|
||||
setData('');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) {
|
||||
alert('请输入名称');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
let parsedData = {};
|
||||
if (data.trim()) {
|
||||
try {
|
||||
parsedData = JSON.parse(data);
|
||||
} catch {
|
||||
alert('JSON格式不正确');
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await onSubmit({ name: name.trim(), data: parsedData });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
width: '480px',
|
||||
maxWidth: '90%'
|
||||
}} onClick={e => e.stopPropagation()}>
|
||||
<h2 style={{ margin: '0 0 20px 0' }}>创建Workspace</h2>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="请输入名称"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>数据 (JSON)</label>
|
||||
<textarea
|
||||
value={data}
|
||||
onChange={e => setData(e.target.value)}
|
||||
placeholder='{"key": "value"}'
|
||||
rows={4}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#fff',
|
||||
color: '#666',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#0070f3',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: submitting ? 'not-allowed' : 'pointer',
|
||||
opacity: submitting ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{submitting ? '创建中...' : '创建'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 编辑弹窗组件
|
||||
function EditDialog({ open, item, onClose, onSubmit }: {
|
||||
open: boolean;
|
||||
item: any;
|
||||
onClose: () => void;
|
||||
onSubmit: (id: string, data: { name?: string, data?: any }) => Promise<void>;
|
||||
}) {
|
||||
const [name, setName] = useState('');
|
||||
const [data, setData] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && item) {
|
||||
setName(item.name || '');
|
||||
setData(item.data ? JSON.stringify(item.data, null, 2) : '');
|
||||
}
|
||||
}, [open, item]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) {
|
||||
alert('请输入名称');
|
||||
return;
|
||||
}
|
||||
if (!item?.id) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
let parsedData: any = undefined;
|
||||
if (data.trim()) {
|
||||
try {
|
||||
parsedData = JSON.parse(data);
|
||||
} catch {
|
||||
alert('JSON格式不正确');
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await onSubmit(item.id, { name: name.trim(), data: parsedData });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000
|
||||
}} onClick={onClose}>
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
width: '480px',
|
||||
maxWidth: '90%'
|
||||
}} onClick={e => e.stopPropagation()}>
|
||||
<h2 style={{ margin: '0 0 20px 0' }}>编辑Workspace</h2>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="请输入名称"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>数据 (JSON)</label>
|
||||
<textarea
|
||||
value={data}
|
||||
onChange={e => setData(e.target.value)}
|
||||
placeholder='{"key": "value"}'
|
||||
rows={4}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#fff',
|
||||
color: '#666',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#0070f3',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: submitting ? 'not-allowed' : 'pointer',
|
||||
opacity: submitting ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{submitting ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -2,13 +2,143 @@ import { useMarkStore } from '@kevisual/api/store-mark';
|
||||
export { useMarkStore }
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { queryApi } from '@/modules/mark-api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type WorkspaceItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
type WorkspaceState = {
|
||||
edit: boolean;
|
||||
setEdit: (edit: boolean) => void;
|
||||
list: WorkspaceItem[];
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
// 弹窗状态
|
||||
showCreateDialog: boolean;
|
||||
setShowCreateDialog: (show: boolean) => void;
|
||||
showEditDialog: boolean;
|
||||
setShowEditDialog: (show: boolean) => void;
|
||||
editingItem: WorkspaceItem | null;
|
||||
setEditingItem: (item: WorkspaceItem | null) => void;
|
||||
// 数据操作
|
||||
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
||||
createItem: (data: { name: string, data?: any }) => Promise<void>;
|
||||
updateItem: (id: string, data: { name?: string, data?: any }) => Promise<void>;
|
||||
deleteItem: (id: string) => Promise<void>;
|
||||
getItem: (id: string) => Promise<WorkspaceItem | null>;
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = create<WorkspaceState>((set) => ({
|
||||
export type { WorkspaceState, WorkspaceItem };
|
||||
|
||||
export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
|
||||
edit: false,
|
||||
setEdit: (edit) => set({ edit }),
|
||||
}));
|
||||
list: [],
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
showCreateDialog: false,
|
||||
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
|
||||
showEditDialog: false,
|
||||
setShowEditDialog: (show) => set({ showEditDialog: show }),
|
||||
editingItem: null,
|
||||
setEditingItem: (item) => set({ editingItem: item }),
|
||||
|
||||
getList: async (params = {}) => {
|
||||
const { page = 1, pageSize = 20, search } = params;
|
||||
set({ loading: true });
|
||||
try {
|
||||
const res = await queryApi.mark.list({
|
||||
markType: 'cnb',
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
sort: 'DESC'
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data?.list || [] });
|
||||
} else {
|
||||
toast.error(res.message || '获取列表失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取workspace列表失败', e);
|
||||
toast.error('获取列表失败');
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createItem: async (data) => {
|
||||
try {
|
||||
const res = await queryApi.mark.create({
|
||||
name: data.name,
|
||||
markType: 'cnb',
|
||||
data: data.data || {}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('创建成功');
|
||||
get().getList();
|
||||
set({ showCreateDialog: false });
|
||||
} else {
|
||||
toast.error(res.message || '创建失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('创建失败', e);
|
||||
toast.error('创建失败');
|
||||
}
|
||||
},
|
||||
|
||||
updateItem: async (id, data) => {
|
||||
try {
|
||||
const res = await queryApi.mark.update({
|
||||
id,
|
||||
...data
|
||||
});
|
||||
if (res.code === 200) {
|
||||
toast.success('更新成功');
|
||||
get().getList();
|
||||
set({ showEditDialog: false, editingItem: null });
|
||||
} else {
|
||||
toast.error(res.message || '更新失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('更新失败', e);
|
||||
toast.error('更新失败');
|
||||
}
|
||||
},
|
||||
|
||||
deleteItem: async (id) => {
|
||||
try {
|
||||
const res = await queryApi.mark.delete({ id });
|
||||
if (res.code === 200) {
|
||||
toast.success('删除成功');
|
||||
get().getList();
|
||||
} else {
|
||||
toast.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('删除失败', e);
|
||||
toast.error('删除失败');
|
||||
}
|
||||
},
|
||||
|
||||
getItem: async (id) => {
|
||||
try {
|
||||
const res = await queryApi.mark.get({ id });
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
} else {
|
||||
toast.error(res.message || '获取详情失败');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取详情失败', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user