refactor(layout): 重构应用布局结构和全局副作用处理
- 移除 AppShell 组件,将布局逻辑直接集成到各层级 layout - 提取全局 Provider 到 AppProviders 组件统一管理 - 移除独立的 ThemeSync 和 QueryProvider 组件 - 将 AntThemeProvider 功能整合到 AppProviders - 更新用户状态和主题相关的 prop 传递方式 - 优化管理后台菜单结构为全局常量定义 - 迁移页面私有 hooks 到对应页面目录下 - 提取通用 UI 副作用动作为全局 hooks 以减少重复代码
This commit is contained in:
@@ -5,8 +5,8 @@ import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { App, Button, Card, Drawer, Empty, Image, Input, Pagination, Spin, Tag, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
import { useCopyText } from "@/hooks/use-copy-text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { fetchAssetLibrary, type AssetLibraryItem } from "@/services/api/assets";
|
||||
@@ -16,6 +16,7 @@ const PAGE_SIZE = 12;
|
||||
|
||||
export default function AssetLibraryPage() {
|
||||
const { message } = App.useApp();
|
||||
const copyText = useCopyText();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [selectedType, setSelectedType] = useState("");
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
@@ -77,11 +78,6 @@ export default function AssetLibraryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const copyText = async (value: string) => {
|
||||
copy(value);
|
||||
message.success("已复制");
|
||||
};
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -164,8 +160,8 @@ export default function AssetLibraryPage() {
|
||||
</div>
|
||||
{selectedAsset.description ? <Typography.Paragraph type="secondary">{selectedAsset.description}</Typography.Paragraph> : null}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAsset.type === "text" ? <Button type="primary" icon={<Copy className="size-4" />} onClick={() => void copyText(selectedAsset.content)}>复制文本</Button> : null}
|
||||
{selectedAsset.type === "image" ? <Button type="primary" icon={<Copy className="size-4" />} onClick={() => void copyText(selectedAsset.url)}>复制链接</Button> : null}
|
||||
{selectedAsset.type === "text" ? <Button type="primary" icon={<Copy className="size-4" />} onClick={() => copyText(selectedAsset.content)}>复制文本</Button> : null}
|
||||
{selectedAsset.type === "image" ? <Button type="primary" icon={<Copy className="size-4" />} onClick={() => copyText(selectedAsset.url)}>复制链接</Button> : null}
|
||||
<Button icon={<FolderPlus className="size-4" />} onClick={() => void saveToMyAssets(selectedAsset)}>加入我的素材</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { Copy, Download, PencilLine, Search, Trash2, Upload } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { App, Button, Card, Drawer, Empty, Form, Image, Input, Modal, Pagination, Select, Space, Tag, Typography } from "antd";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
import { useCopyText } from "@/hooks/use-copy-text";
|
||||
import { formatBytes, readFileAsDataUrl } from "@/lib/image-utils";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -30,6 +30,7 @@ const kindOptions = [
|
||||
|
||||
export default function AssetsPage() {
|
||||
const { message } = App.useApp();
|
||||
const copyText = useCopyText();
|
||||
const [form] = Form.useForm<AssetFormValues>();
|
||||
const coverInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -138,10 +139,9 @@ export default function AssetsPage() {
|
||||
if (!form.getFieldValue("title")) form.setFieldValue("title", file.name);
|
||||
};
|
||||
|
||||
const copyText = async (asset: Asset) => {
|
||||
const copyAssetText = async (asset: Asset) => {
|
||||
if (asset.kind !== "text") return;
|
||||
copy(asset.data.content);
|
||||
message.success("文本已复制");
|
||||
copyText(asset.data.content, "文本已复制");
|
||||
};
|
||||
|
||||
const downloadImage = (asset: Asset) => {
|
||||
@@ -199,7 +199,7 @@ export default function AssetsPage() {
|
||||
asset={asset}
|
||||
onOpen={() => setPreviewAsset(asset)}
|
||||
onEdit={() => openEdit(asset)}
|
||||
onCopy={copyText}
|
||||
onCopy={copyAssetText}
|
||||
onDownload={downloadImage}
|
||||
onDelete={() => setDeletingAsset(asset)}
|
||||
/>
|
||||
@@ -286,7 +286,7 @@ export default function AssetsPage() {
|
||||
}} />
|
||||
</Modal>
|
||||
|
||||
<AssetDrawer asset={previewAsset} onClose={() => setPreviewAsset(null)} onCopy={copyText} onDownload={downloadImage} />
|
||||
<AssetDrawer asset={previewAsset} onClose={() => setPreviewAsset(null)} onCopy={copyAssetText} onDownload={downloadImage} />
|
||||
|
||||
<Modal title="删除素材" open={Boolean(deletingAsset)} onCancel={() => setDeletingAsset(null)} onOk={confirmDelete} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
确定删除「{deletingAsset?.title}」吗?删除后会从我的素材中移除。
|
||||
|
||||
@@ -2093,9 +2093,7 @@ function CanvasTopBar({
|
||||
onExpandAssistant: () => void;
|
||||
}) {
|
||||
const colorTheme = useThemeStore((state) => state.theme);
|
||||
const setTheme = useThemeStore((state) => state.setTheme);
|
||||
const theme = canvasThemes[colorTheme];
|
||||
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
const initial = (userName.trim()[0] || "U").toUpperCase();
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const accountRef = useRef<HTMLDivElement>(null);
|
||||
@@ -2175,9 +2173,6 @@ function CanvasTopBar({
|
||||
|
||||
<div className="pointer-events-auto flex items-center gap-1.5">
|
||||
<UserStatusActions
|
||||
version={appVersion}
|
||||
theme={colorTheme}
|
||||
onThemeChange={setTheme}
|
||||
onOpenConfig={onOpenConfig}
|
||||
userName={userName}
|
||||
initial={initial}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { ReactNode } from "react";
|
||||
"use client";
|
||||
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import type { ReactNode } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { AppTopNav } from "@/components/app-top-nav";
|
||||
import { type NavigationToolSlug, navigationTools } from "@/lib/navigation-tools";
|
||||
import { useAiConfigStore } from "@/stores/use-ai-config-store";
|
||||
|
||||
export default function UserLayout({ children }: { children: ReactNode }) {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
const pathname = usePathname();
|
||||
const config = useAiConfigStore((state) => state.config);
|
||||
const updateConfig = useAiConfigStore((state) => state.updateConfig);
|
||||
const slug = pathname.split("/").filter(Boolean)[0];
|
||||
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
|
||||
<AppTopNav activeToolSlug={activeToolSlug} config={config} onConfigChange={updateConfig} hideHeader={/^\/canvas\/[^/]+/.test(pathname)} />
|
||||
<div className="min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { FolderPlus, Search } from "lucide-react";
|
||||
import { type UIEvent, useEffect, useState } from "react";
|
||||
import { App, Button, Empty, Input, Spin, Tag } from "antd";
|
||||
import copy from "copy-to-clipboard";
|
||||
|
||||
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";
|
||||
@@ -19,6 +19,7 @@ export default function PromptsPage() {
|
||||
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(() => {
|
||||
@@ -32,11 +33,6 @@ export default function PromptsPage() {
|
||||
setSelectedTags((items) => items.includes(tag) ? items.filter((item) => item !== tag) : [...items, tag]);
|
||||
};
|
||||
|
||||
const copyPrompt = async (prompt: string) => {
|
||||
copy(prompt);
|
||||
message.success("提示词已复制");
|
||||
};
|
||||
|
||||
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("已加入我的素材");
|
||||
@@ -93,7 +89,7 @@ export default function PromptsPage() {
|
||||
<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={() => void copyPrompt(item.prompt)} extraAction={<Button size="small" icon={<FolderPlus className="size-3.5" />} onClick={() => savePromptAsset(item)}>加入我的素材</Button>} />
|
||||
<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}
|
||||
@@ -102,7 +98,7 @@ export default function PromptsPage() {
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<PromptDetailDialog prompt={selectedPrompt} onClose={() => setSelectedPrompt(null)} onCopy={(prompt) => void copyPrompt(prompt)} onSaveAsset={savePromptAsset} />
|
||||
<PromptDetailDialog prompt={selectedPrompt} onClose={() => setSelectedPrompt(null)} onCopy={(prompt) => copyText(prompt, "提示词已复制")} onSaveAsset={savePromptAsset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user