mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 01:14:36 +08:00
refactor: remove "use client" directive from multiple files and update project structure to align with Vite and React Router
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { Copy, FolderPlus } from "lucide-react";
|
||||
import { Button, Modal, Space, Tag } from "antd";
|
||||
|
||||
import { formatPromptDate, type Prompt } from "@/services/api/prompts";
|
||||
|
||||
export function PromptDetailDialog({ prompt, onClose, onCopy, onSaveAsset }: { prompt: Prompt | null; onClose: () => void; onCopy: (prompt: string) => void; onSaveAsset?: (prompt: Prompt) => void }) {
|
||||
return (
|
||||
<>
|
||||
<Modal title={prompt?.title} open={Boolean(prompt)} onCancel={onClose} footer={null} width={860}>
|
||||
{prompt ? (
|
||||
<>
|
||||
<div className="grid gap-5 md:grid-cols-[300px_minmax(0,1fr)]">
|
||||
<div className="space-y-3">
|
||||
<img src={prompt.coverUrl} alt={prompt.title} className="aspect-[4/3] w-full rounded-lg object-cover" />
|
||||
{prompt.preview ? <pre className="max-h-60 overflow-auto whitespace-pre-wrap rounded-lg bg-stone-100 p-3 text-xs leading-5 text-stone-600 dark:bg-stone-900 dark:text-stone-300">{prompt.preview}</pre> : null}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{prompt.tags.map((tag) => (
|
||||
<Tag key={tag} className="m-0">
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 whitespace-pre-wrap text-sm leading-7 text-stone-800 dark:text-stone-300">{prompt.prompt}</p>
|
||||
<div className="mt-4 text-xs text-stone-500 dark:text-stone-400">
|
||||
创建:{formatPromptDate(prompt.createdAt)} · 更新:{formatPromptDate(prompt.updatedAt)}
|
||||
</div>
|
||||
<Space wrap className="mt-5">
|
||||
<Button type="primary" icon={<Copy className="size-4" />} onClick={() => onCopy(prompt.prompt)}>
|
||||
复制提示词
|
||||
</Button>
|
||||
{onSaveAsset ? (
|
||||
<Button icon={<FolderPlus className="size-4" />} onClick={() => onSaveAsset(prompt)}>
|
||||
加入我的素材
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { FolderPlus, Search } from "lucide-react";
|
||||
import { type UIEvent, useEffect, useState } from "react";
|
||||
import { App, Button, Empty, Input, Spin, Tag } from "antd";
|
||||
|
||||
import { PromptCard } from "@/components/prompts/prompt-card";
|
||||
import { usePromptList } from "@/components/prompts/use-prompt-list";
|
||||
import { PromptDetailDialog } from "./components/prompt-detail-dialog";
|
||||
import { useCopyText } from "@/hooks/use-copy-text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { ALL_PROMPTS_OPTION, type Prompt } from "@/services/api/prompts";
|
||||
|
||||
export default function PromptsPage() {
|
||||
const { message } = App.useApp();
|
||||
const [titleKeyword, setTitleKeyword] = useState("");
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState(ALL_PROMPTS_OPTION);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||
const addAsset = useAssetStore((state) => state.addAsset);
|
||||
const copyText = useCopyText();
|
||||
const { query, items: promptItems, tags: promptTags, categories: promptCategoryOptions, total: totalPrompts } = usePromptList({ keyword: titleKeyword, tags: selectedTags, category: selectedCategory });
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isError) {
|
||||
message.error(query.error instanceof Error ? query.error.message : "获取提示词失败");
|
||||
}
|
||||
}, [message, query.error, query.isError]);
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
if (tag === ALL_PROMPTS_OPTION) return setSelectedTags([]);
|
||||
setSelectedTags((items) => (items.includes(tag) ? items.filter((item) => item !== tag) : [...items, tag]));
|
||||
};
|
||||
|
||||
const savePromptAsset = (item: Prompt) => {
|
||||
addAsset({ kind: "text", title: item.title, coverUrl: item.coverUrl, tags: item.tags, source: item.category, data: { content: item.prompt }, metadata: { source: "prompt-library", promptId: item.id, githubUrl: item.githubUrl } });
|
||||
message.success("已加入我的素材");
|
||||
};
|
||||
|
||||
const handleListScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||
const target = event.currentTarget;
|
||||
if (query.hasNextPage && !query.isFetchingNextPage && target.scrollTop + target.clientHeight >= target.scrollHeight - 160) {
|
||||
void query.fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-background text-stone-800 dark:text-stone-100">
|
||||
<main
|
||||
className="min-h-0 flex-1 overflow-y-auto bg-background bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] px-6 py-8 [background-size:16px_16px] dark:bg-[radial-gradient(rgba(245,245,244,.16)_1px,transparent_1px)]"
|
||||
onScroll={handleListScroll}
|
||||
>
|
||||
<div className="pb-8">
|
||||
<div className="mx-auto max-w-5xl text-center">
|
||||
<h1 className="text-4xl font-semibold tracking-tight text-stone-950 dark:text-stone-100">提示词中心</h1>
|
||||
<p className="mt-3 text-sm text-stone-500 dark:text-stone-400">共 {totalPrompts} 条提示词,按标题、标签与分类快速查找灵感。</p>
|
||||
</div>
|
||||
{query.isLoading ? (
|
||||
<div className="flex h-60 items-center justify-center">
|
||||
<Spin />
|
||||
</div>
|
||||
) : null}
|
||||
{!query.isLoading ? (
|
||||
<>
|
||||
<div className="mx-auto mt-8 w-full max-w-2xl">
|
||||
<Input size="large" className="w-full" prefix={<Search className="size-4 text-stone-400" />} value={titleKeyword} placeholder="按标题查询" onChange={(event) => setTitleKeyword(event.target.value)} />
|
||||
</div>
|
||||
<div className="mx-auto mt-6 grid max-w-6xl gap-3 text-left">
|
||||
<div className="grid gap-2 sm:grid-cols-[56px_minmax(0,1fr)] sm:items-start">
|
||||
<div className="pt-2 text-xs font-medium text-stone-500 dark:text-stone-400">分类</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{promptCategoryOptions.map((category) => (
|
||||
<Tag.CheckableTag key={category} checked={selectedCategory === category} className={cn("prompt-filter-tag", selectedCategory === category && "is-active")} onChange={() => setSelectedCategory(category)}>
|
||||
{category}
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-[56px_minmax(0,1fr)] sm:items-start">
|
||||
<div className="pt-2 text-xs font-medium text-stone-500 dark:text-stone-400">标签</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{promptTags.map((tag) => (
|
||||
<Tag.CheckableTag
|
||||
key={tag}
|
||||
checked={tag === ALL_PROMPTS_OPTION ? selectedTags.length === 0 : selectedTags.includes(tag)}
|
||||
className={cn("prompt-filter-tag", (tag === ALL_PROMPTS_OPTION ? selectedTags.length === 0 : selectedTags.includes(tag)) && "is-active")}
|
||||
onChange={() => toggleTag(tag)}
|
||||
>
|
||||
{tag}
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!query.isLoading ? (
|
||||
<div>
|
||||
<div className="mx-auto grid max-w-7xl gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{promptItems.map((item) => (
|
||||
<PromptCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onOpen={() => setSelectedPrompt(item)}
|
||||
onCopy={() => copyText(item.prompt, "提示词已复制")}
|
||||
extraAction={
|
||||
<Button size="small" icon={<FolderPlus className="size-3.5" />} onClick={() => savePromptAsset(item)}>
|
||||
加入我的素材
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{promptItems.length === 0 ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有找到匹配的提示词" className="py-16" /> : null}
|
||||
<div className="mx-auto mt-6 max-w-7xl text-center text-xs text-stone-500 dark:text-stone-400">
|
||||
{query.isFetchingNextPage ? "加载中..." : query.hasNextPage ? "继续向下滚动加载更多" : promptItems.length > 0 ? "已经到底了" : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<PromptDetailDialog prompt={selectedPrompt} onClose={() => setSelectedPrompt(null)} onCopy={(prompt) => copyText(prompt, "提示词已复制")} onSaveAsset={savePromptAsset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user