refactor(layout): 重构应用布局结构和全局副作用处理

- 移除 AppShell 组件,将布局逻辑直接集成到各层级 layout
- 提取全局 Provider 到 AppProviders 组件统一管理
- 移除独立的 ThemeSync 和 QueryProvider 组件
- 将 AntThemeProvider 功能整合到 AppProviders
- 更新用户状态和主题相关的 prop 传递方式
- 优化管理后台菜单结构为全局常量定义
- 迁移页面私有 hooks 到对应页面目录下
- 提取通用 UI 副作用动作为全局 hooks 以减少重复代码
This commit is contained in:
HouYunFei
2026-05-21 11:42:22 +08:00
parent dce6ab2282
commit 603deee962
25 changed files with 151 additions and 257 deletions
+4 -8
View File
@@ -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>
+6 -6
View File
@@ -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}
+19 -3
View File
@@ -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>
);
}
+4 -8
View File
@@ -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>
);
}