603deee962
- 移除 AppShell 组件,将布局逻辑直接集成到各层级 layout - 提取全局 Provider 到 AppProviders 组件统一管理 - 移除独立的 ThemeSync 和 QueryProvider 组件 - 将 AntThemeProvider 功能整合到 AppProviders - 更新用户状态和主题相关的 prop 传递方式 - 优化管理后台菜单结构为全局常量定义 - 迁移页面私有 hooks 到对应页面目录下 - 提取通用 UI 副作用动作为全局 hooks 以减少重复代码
105 lines
6.2 KiB
TypeScript
105 lines
6.2 KiB
TypeScript
"use client";
|
||
|
||
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 { PromptDetailDialog } from "@/components/prompts/prompt-detail-dialog";
|
||
import { usePromptList } from "@/components/prompts/use-prompt-list";
|
||
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>
|
||
);
|
||
}
|