generated from kevisual/vite-react-template
refactor: 重构workspaces页面UI,使用组件化设计和Tailwind样式
This commit is contained in:
105
src/pages/workspaces/components/CreateDialog.tsx
Normal file
105
src/pages/workspaces/components/CreateDialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { TagInput } from "./TagInput";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export function CreateDialog({ open, onClose, onSubmit }: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
const [link, setLink] = useState('');
|
||||||
|
const [summary, setSummary] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!title.trim()) {
|
||||||
|
alert('请输入标题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onSubmit({
|
||||||
|
title: title.trim(),
|
||||||
|
tags,
|
||||||
|
link: link.trim(),
|
||||||
|
summary: summary.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="max-w-lg!">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>创建Workspace</DialogTitle>
|
||||||
|
<DialogDescription>填写以下信息创建一个新的Workspace</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-title">标题</Label>
|
||||||
|
<Input
|
||||||
|
id="create-title"
|
||||||
|
value={title}
|
||||||
|
onChange={e => setTitle(e.target.value)}
|
||||||
|
placeholder="请输入标题"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>标签</Label>
|
||||||
|
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-link">链接</Label>
|
||||||
|
<Input
|
||||||
|
id="create-link"
|
||||||
|
value={link}
|
||||||
|
onChange={e => setLink(e.target.value)}
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-summary">摘要</Label>
|
||||||
|
<Input
|
||||||
|
id="create-summary"
|
||||||
|
value={summary}
|
||||||
|
onChange={e => setSummary(e.target.value)}
|
||||||
|
placeholder="简要描述"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="create-description">描述</Label>
|
||||||
|
<Textarea
|
||||||
|
id="create-description"
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
placeholder="详细描述..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose} disabled={submitting}>取消</Button>
|
||||||
|
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
|
||||||
|
{submitting ? '创建中...' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
117
src/pages/workspaces/components/EditDialog.tsx
Normal file
117
src/pages/workspaces/components/EditDialog.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { TagInput } from "./TagInput";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export function EditDialog({ open, item, onClose, onSubmit }: {
|
||||||
|
open: boolean;
|
||||||
|
item: any;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
const [link, setLink] = useState('');
|
||||||
|
const [summary, setSummary] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && item) {
|
||||||
|
setTitle(item.title || '');
|
||||||
|
setTags(item.tags || []);
|
||||||
|
setLink(item.link || '');
|
||||||
|
setSummary(item.summary || '');
|
||||||
|
setDescription(item.description || '');
|
||||||
|
}
|
||||||
|
}, [open, item]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!title.trim()) {
|
||||||
|
alert('请输入标题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!item?.id) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onSubmit(item.id, {
|
||||||
|
title: title.trim(),
|
||||||
|
tags,
|
||||||
|
link: link.trim(),
|
||||||
|
summary: summary.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="max-w-lg!">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑Workspace</DialogTitle>
|
||||||
|
<DialogDescription>修改Workspace信息</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-title">标题</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-title"
|
||||||
|
value={title}
|
||||||
|
onChange={e => setTitle(e.target.value)}
|
||||||
|
placeholder="请输入标题"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>标签</Label>
|
||||||
|
<TagInput value={tags} onChange={setTags} placeholder="输入标签后按回车添加" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-link">链接</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-link"
|
||||||
|
value={link}
|
||||||
|
onChange={e => setLink(e.target.value)}
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-summary">摘要</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-summary"
|
||||||
|
value={summary}
|
||||||
|
onChange={e => setSummary(e.target.value)}
|
||||||
|
placeholder="简要描述"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-description">描述</Label>
|
||||||
|
<Textarea
|
||||||
|
id="edit-description"
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
placeholder="详细描述..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose} disabled={submitting}>取消</Button>
|
||||||
|
<Button variant="outline" onClick={handleSubmit} disabled={submitting}>
|
||||||
|
{submitting ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/pages/workspaces/components/TagInput.tsx
Normal file
53
src/pages/workspaces/components/TagInput.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export function TagInput({ value, onChange, placeholder }: {
|
||||||
|
value: string[];
|
||||||
|
onChange: (tags: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && input.trim()) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!value.includes(input.trim())) {
|
||||||
|
onChange([...value, input.trim()]);
|
||||||
|
}
|
||||||
|
setInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (tag: string) => {
|
||||||
|
onChange(value.filter(t => t !== tag));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder || '输入标签后按回车添加'}
|
||||||
|
/>
|
||||||
|
{value.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{value.map((tag) => (
|
||||||
|
<Badge key={tag} variant="outline" className="gap-1">
|
||||||
|
{tag}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRemove(tag)}
|
||||||
|
className="hover:text-destructive"
|
||||||
|
>
|
||||||
|
<XIcon className="size-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/pages/workspaces/components/index.ts
Normal file
3
src/pages/workspaces/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { CreateDialog } from "./CreateDialog";
|
||||||
|
export { EditDialog } from "./EditDialog";
|
||||||
|
export { TagInput } from "./TagInput";
|
||||||
@@ -1,6 +1,19 @@
|
|||||||
import { useWorkspaceStore, type WorkspaceState } from "./store";
|
import { useWorkspaceStore, type WorkspaceState } from "./store";
|
||||||
import { useShallow } from "zustand/shallow";
|
import { useShallow } from "zustand/shallow";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { CreateDialog, EditDialog } from "./components";
|
||||||
|
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
const workspaceStore = useWorkspaceStore(useShallow((state: WorkspaceState) => {
|
||||||
@@ -45,100 +58,82 @@ export const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px' }}>
|
<div className="p-5">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
<div className="flex justify-between items-center mb-5">
|
||||||
<h1>Workspaces</h1>
|
<h1 className="text-2xl font-semibold">Workspaces</h1>
|
||||||
<div style={{ display: 'flex', gap: '10px' }}>
|
<div className="flex gap-2">
|
||||||
<input
|
<div className="relative">
|
||||||
|
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索workspace..."
|
placeholder="搜索workspace..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
style={{ padding: '8px 12px', border: '1px solid #ddd', borderRadius: '4px', width: '200px' }}
|
className="pl-8 w-48"
|
||||||
/>
|
/>
|
||||||
<button
|
</div>
|
||||||
onClick={handleRefresh}
|
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||||
style={{ padding: '8px 16px', background: '#666', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
<RefreshCwIcon className="size-4" />
|
||||||
>
|
</Button>
|
||||||
刷新
|
<Button variant="outline" size="sm" onClick={handleCreate}>
|
||||||
</button>
|
<PlusIcon className="size-4" />
|
||||||
<button
|
|
||||||
onClick={handleCreate}
|
|
||||||
style={{ padding: '8px 16px', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
|
|
||||||
>
|
|
||||||
创建Workspace
|
创建Workspace
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{workspaceStore.loading ? (
|
{workspaceStore.loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '40px' }}>加载中...</div>
|
<div className="text-center py-10 text-muted-foreground">加载中...</div>
|
||||||
) : workspaceStore.list.length === 0 ? (
|
) : workspaceStore.list.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>暂无workspace数据</div>
|
<div className="text-center py-10 text-muted-foreground">暂无workspace数据</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{workspaceStore.list.map((item) => (
|
{workspaceStore.list.map((item) => (
|
||||||
<div
|
<Card key={item.id}>
|
||||||
key={item.id}
|
<CardHeader>
|
||||||
style={{
|
<CardTitle>{item.title || '未命名'}</CardTitle>
|
||||||
border: '1px solid #e0e0e0',
|
<CardDescription className="text-xs">ID: {item.id}</CardDescription>
|
||||||
borderRadius: '8px',
|
</CardHeader>
|
||||||
padding: '16px',
|
<CardContent className="space-y-2">
|
||||||
background: '#fff',
|
{item.tags && item.tags.length > 0 && (
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
<div className="flex flex-wrap gap-1">
|
||||||
}}
|
{item.tags.map((tag, index) => (
|
||||||
>
|
<Badge key={index} variant="outline">{tag}</Badge>
|
||||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{item.title || '未命名'}</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>
|
||||||
|
)}
|
||||||
|
{item.summary && (
|
||||||
|
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
||||||
|
)}
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
|
||||||
|
<PencilIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handleDelete(item.id)}>
|
||||||
|
<TrashIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 创建弹窗 */}
|
|
||||||
<CreateDialog
|
<CreateDialog
|
||||||
open={workspaceStore.showCreateDialog}
|
open={workspaceStore.showCreateDialog}
|
||||||
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
onClose={() => workspaceStore.setShowCreateDialog(false)}
|
||||||
onSubmit={workspaceStore.createItem}
|
onSubmit={workspaceStore.createItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
|
||||||
<EditDialog
|
<EditDialog
|
||||||
open={workspaceStore.showEditDialog}
|
open={workspaceStore.showEditDialog}
|
||||||
item={workspaceStore.editingItem}
|
item={workspaceStore.editingItem}
|
||||||
@@ -152,434 +147,4 @@ export const App = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建弹窗组件
|
|
||||||
function CreateDialog({ open, onClose, onSubmit }: {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string, data?: any }) => Promise<void>;
|
|
||||||
}) {
|
|
||||||
const [title, setTitle] = useState('');
|
|
||||||
const [tags, setTags] = useState('');
|
|
||||||
const [link, setLink] = useState('');
|
|
||||||
const [summary, setSummary] = useState('');
|
|
||||||
const [description, setDescription] = useState('');
|
|
||||||
const [data, setData] = useState('');
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setTitle('');
|
|
||||||
setTags('');
|
|
||||||
setLink('');
|
|
||||||
setSummary('');
|
|
||||||
setDescription('');
|
|
||||||
setData('');
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!title.trim()) {
|
|
||||||
alert('请输入标题');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
let parsedData = {};
|
|
||||||
if (data.trim()) {
|
|
||||||
try {
|
|
||||||
parsedData = JSON.parse(data);
|
|
||||||
} catch {
|
|
||||||
alert('JSON格式不正确');
|
|
||||||
setSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await onSubmit({
|
|
||||||
title: title.trim(),
|
|
||||||
tags: tags.split(',').map(t => t.trim()).filter(Boolean),
|
|
||||||
link: link.trim(),
|
|
||||||
summary: summary.trim(),
|
|
||||||
description: description.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: '520px',
|
|
||||||
maxWidth: '90%',
|
|
||||||
maxHeight: '90vh',
|
|
||||||
overflow: 'auto'
|
|
||||||
}} 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={title}
|
|
||||||
onChange={e => setTitle(e.target.value)}
|
|
||||||
placeholder="请输入标题"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>标签 (逗号分隔)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={tags}
|
|
||||||
onChange={e => setTags(e.target.value)}
|
|
||||||
placeholder="标签1, 标签2, 标签3"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>链接</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={link}
|
|
||||||
onChange={e => setLink(e.target.value)}
|
|
||||||
placeholder="https://..."
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>摘要</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={summary}
|
|
||||||
onChange={e => setSummary(e.target.value)}
|
|
||||||
placeholder="简要描述"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>描述</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
onChange={e => setDescription(e.target.value)}
|
|
||||||
placeholder="详细描述..."
|
|
||||||
rows={3}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
resize: 'vertical'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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: { title?: string, tags?: string[], link?: string, summary?: string, description?: string, data?: any }) => Promise<void>;
|
|
||||||
}) {
|
|
||||||
const [title, setTitle] = useState('');
|
|
||||||
const [tags, setTags] = useState('');
|
|
||||||
const [link, setLink] = useState('');
|
|
||||||
const [summary, setSummary] = useState('');
|
|
||||||
const [description, setDescription] = useState('');
|
|
||||||
const [data, setData] = useState('');
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open && item) {
|
|
||||||
setTitle(item.title || '');
|
|
||||||
setTags(item.tags ? item.tags.join(', ') : '');
|
|
||||||
setLink(item.link || '');
|
|
||||||
setSummary(item.summary || '');
|
|
||||||
setDescription(item.description || '');
|
|
||||||
setData(item.data ? JSON.stringify(item.data, null, 2) : '');
|
|
||||||
}
|
|
||||||
}, [open, item]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!title.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, {
|
|
||||||
title: title.trim(),
|
|
||||||
tags: tags.split(',').map(t => t.trim()).filter(Boolean),
|
|
||||||
link: link.trim(),
|
|
||||||
summary: summary.trim(),
|
|
||||||
description: description.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: '520px',
|
|
||||||
maxWidth: '90%',
|
|
||||||
maxHeight: '90vh',
|
|
||||||
overflow: 'auto'
|
|
||||||
}} 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={title}
|
|
||||||
onChange={e => setTitle(e.target.value)}
|
|
||||||
placeholder="请输入标题"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>标签 (逗号分隔)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={tags}
|
|
||||||
onChange={e => setTags(e.target.value)}
|
|
||||||
placeholder="标签1, 标签2, 标签3"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>链接</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={link}
|
|
||||||
onChange={e => setLink(e.target.value)}
|
|
||||||
placeholder="https://..."
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>摘要</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={summary}
|
|
||||||
onChange={e => setSummary(e.target.value)}
|
|
||||||
placeholder="简要描述"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: '16px' }}>
|
|
||||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>描述</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
onChange={e => setDescription(e.target.value)}
|
|
||||||
placeholder="详细描述..."
|
|
||||||
rows={3}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
resize: 'vertical'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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;
|
export default App;
|
||||||
@@ -12,9 +12,8 @@ type WorkspaceItem = {
|
|||||||
link?: string;
|
link?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
created_at: string;
|
createdAt: string;
|
||||||
updated_at: string;
|
updatedAt: string;
|
||||||
data?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceState = {
|
type WorkspaceState = {
|
||||||
@@ -32,8 +31,8 @@ type WorkspaceState = {
|
|||||||
setEditingItem: (item: WorkspaceItem | null) => void;
|
setEditingItem: (item: WorkspaceItem | null) => void;
|
||||||
// 数据操作
|
// 数据操作
|
||||||
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
||||||
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string, data?: any }) => Promise<void>;
|
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||||
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string, data?: any }) => Promise<void>;
|
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||||
deleteItem: (id: string) => Promise<void>;
|
deleteItem: (id: string) => Promise<void>;
|
||||||
getItem: (id: string) => Promise<WorkspaceItem | null>;
|
getItem: (id: string) => Promise<WorkspaceItem | null>;
|
||||||
}
|
}
|
||||||
@@ -85,8 +84,7 @@ export const useWorkspaceStore = create<WorkspaceState>((set, get) => ({
|
|||||||
tags: data.tags || [],
|
tags: data.tags || [],
|
||||||
link: data.link || '',
|
link: data.link || '',
|
||||||
summary: data.summary || '',
|
summary: data.summary || '',
|
||||||
description: data.description || '',
|
description: data.description || ''
|
||||||
data: data.data || {}
|
|
||||||
});
|
});
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
toast.success('创建成功');
|
toast.success('创建成功');
|
||||||
|
|||||||
Reference in New Issue
Block a user