Compare commits

...
2 Commits
Author SHA1 Message Date
xuhdandClaude Sonnet 4.6 719ff6f210 merge: 合并 master 冲突,保留暗色主题 + 内嵌传参支持
- StudioNotebookDetailPage: 保留 notebookIdProp 内嵌传参 + useTheme 透传
- StudioNotebooksPage: 保留 onOpen 回调 + 暗色主题卡片样式

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 15:12:08 +08:00
xuhdandClaude Sonnet 4.6 1f74586032 feat(frontend): 来源弹窗加loading + 笔记页暗色适配 + 主题透传
- 来源上传弹窗 A系统基因库 tab 搜索时显示 loading spinner
- NetworkActor 组件初始加载时显示 loading spinner(初始状态设为 true)
- StudioNotebooksPage 笔记卡片支持暗色主题样式
- StudioNotebookDetailPage / NotebookAIWritingPage 跳转时透传 themekey 参数

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 15:00:50 +08:00
5 changed files with 49 additions and 13 deletions

No files matched your search

@@ -826,7 +826,13 @@ const DocumentUploadPopupContent: FC<{
</Button> </Button>
</div> </div>
)} )}
{listData.length === 0 && <Empty />} {isSearching ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
listData.length === 0 && <Empty />
)}
{searchType === "human" && ( {searchType === "human" && (
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]"> <div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
{listData.map((item: listDataItem, index: number) => { {listData.map((item: listDataItem, index: number) => {
@@ -545,7 +545,12 @@ export function NetworkActor({ inputValue, searchSpaceId, onSuccess }: NetworkAc
</div> </div>
)} )}
<div className="p-[10px]"> <div className="p-[10px]">
<div className="grid grid-cols-4 gap-[10px] user-list"> {isSearching ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : null}
<div className={`grid grid-cols-4 gap-[10px] user-list${isSearching ? ' hidden' : ''}`}>
{userList.map((item, index) => ( {userList.map((item, index) => (
<div key={index} className="user-item p-[10px] relative"> <div key={index} className="user-item p-[10px] relative">
<div className="absolute top-[5px] right-[10px]"> <div className="absolute top-[5px] right-[10px]">
@@ -2,6 +2,9 @@ import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import AIWritingPage from '@/open-canvas/pages/AIWritingPage'; import AIWritingPage from '@/open-canvas/pages/AIWritingPage';
import { getNotebookApiBaseURL } from '@/core/config'; import { getNotebookApiBaseURL } from '@/core/config';
import { useTheme, type ThemeType } from '@/contexts/ThemeContext';
const VALID_THEMES: ThemeType[] = ['dark', 'light', 'lightblue'];
interface NotebookTokenData { interface NotebookTokenData {
doc_ids: number[]; doc_ids: number[];
@@ -13,6 +16,17 @@ interface NotebookTokenData {
export default function NotebookAIWritingPage() { export default function NotebookAIWritingPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const token = searchParams.get('token'); const token = searchParams.get('token');
const themekey = searchParams.get('themekey');
const { setTheme } = useTheme();
// 跟随调用方传入的 themekey 切换主题;缺省或非法值兜底为 light
useEffect(() => {
const next = themekey && VALID_THEMES.includes(themekey as ThemeType)
? (themekey as ThemeType)
: 'light';
setTheme(next);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themekey]);
const [data, setData] = useState<NotebookTokenData | null>(null); const [data, setData] = useState<NotebookTokenData | null>(null);
const [spaceName, setSpaceName] = useState<string | null>(null); const [spaceName, setSpaceName] = useState<string | null>(null);
@@ -27,6 +27,7 @@ import { fetchDocumentByChunk } from "@/core/studio/api";
import type { DocumentWithChunks } from "@/core/studio/api"; import type { DocumentWithChunks } from "@/core/studio/api";
import type { SourceItem } from "@/core/studio/types"; import type { SourceItem } from "@/core/studio/types";
import { getStoredAuth } from "@/core/auth"; import { getStoredAuth } from "@/core/auth";
import { useTheme } from "@/contexts/ThemeContext";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
import { import {
Select, Select,
@@ -218,6 +219,7 @@ export default function StudioNotebookDetailPage({
const params = useParams<{ notebookId: string }>(); const params = useParams<{ notebookId: string }>();
const notebookId = const notebookId =
notebookIdProp != null ? String(notebookIdProp) : params.notebookId; notebookIdProp != null ? String(notebookIdProp) : params.notebookId;
const { theme } = useTheme(); // 跳转「我的空间」时带上当前主题 key
const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback
const { models } = useModels(); const { models } = useModels();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -508,7 +510,7 @@ export default function StudioNotebookDetailPage({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<a <a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`} href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70" className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/70"
@@ -532,7 +534,7 @@ export default function StudioNotebookDetailPage({
<div className="relative z-10 flex flex-1 flex-col overflow-hidden"> <div className="relative z-10 flex flex-1 flex-col overflow-hidden">
{/* 右上角跳转按钮 */} {/* 右上角跳转按钮 */}
<a <a
href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat`} href={`${import.meta.env.VITE_AIPI_FRONTEND_URL}/auth/callback?token=${studioToken ?? ""}&redirect=/dashboard/${notebookId}/new-chat&themekey=${theme}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="absolute right-6 top-4 z-10 flex items-center gap-1.5 rounded-full bg-muted/70 px-3 py-1.5 text-xs text-muted-foreground backdrop-blur transition-colors hover:bg-muted" className="absolute right-6 top-4 z-10 flex items-center gap-1.5 rounded-full bg-muted/70 px-3 py-1.5 text-xs text-muted-foreground backdrop-blur transition-colors hover:bg-muted"
+18 -9
View File
@@ -8,6 +8,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useStudioNotebooks } from "@/core/studio/hooks"; import { useStudioNotebooks } from "@/core/studio/hooks";
import { createSearchSpace, deleteSearchSpace, renameSearchSpace } from "@/core/studio/api"; import { createSearchSpace, deleteSearchSpace, renameSearchSpace } from "@/core/studio/api";
import { getStoredAuth } from "@/core/auth"; import { getStoredAuth } from "@/core/auth";
import { useTheme } from "@/contexts/ThemeContext";
import type { StudioNotebook } from "@/core/studio/types"; import type { StudioNotebook } from "@/core/studio/types";
import { import {
Dialog, Dialog,
@@ -23,13 +24,16 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
function NotebookCardSkeleton() { function NotebookCardSkeleton({ isDark }: { isDark: boolean }) {
return ( return (
<div className="flex flex-col rounded-[20px] bg-[#f2f2f2] p-5" style={{ minHeight: 180 }}> <div
<div className="size-10 animate-pulse rounded-xl bg-[#e0e0e0]" /> className={`flex flex-col rounded-[20px] p-5 ${isDark ? "bg-[#1e2330]" : "bg-[#f2f2f2]"}`}
style={{ minHeight: 180 }}
>
<div className={`size-10 animate-pulse rounded-xl ${isDark ? "bg-[#2c3346]" : "bg-[#e0e0e0]"}`} />
<div className="mt-auto flex flex-col gap-1.5 pt-8"> <div className="mt-auto flex flex-col gap-1.5 pt-8">
<div className="h-4 w-3/4 animate-pulse rounded bg-[#e0e0e0]" /> <div className={`h-4 w-3/4 animate-pulse rounded ${isDark ? "bg-[#2c3346]" : "bg-[#e0e0e0]"}`} />
<div className="h-3 w-1/3 animate-pulse rounded bg-[#e0e0e0]" /> <div className={`h-3 w-1/3 animate-pulse rounded ${isDark ? "bg-[#2c3346]" : "bg-[#e0e0e0]"}`} />
</div> </div>
</div> </div>
); );
@@ -37,11 +41,13 @@ function NotebookCardSkeleton() {
function NotebookCard({ function NotebookCard({
nb, nb,
isDark,
onRename, onRename,
onDelete, onDelete,
onOpen, onOpen,
}: { }: {
nb: StudioNotebook; nb: StudioNotebook;
isDark: boolean;
onRename: (nb: StudioNotebook) => void; onRename: (nb: StudioNotebook) => void;
onDelete: (nb: StudioNotebook) => void; onDelete: (nb: StudioNotebook) => void;
/** 传入则点击卡片回调(内嵌打开),不再路由跳转。 */ /** 传入则点击卡片回调(内嵌打开),不再路由跳转。 */
@@ -61,16 +67,16 @@ function NotebookCard({
<div className="size-7" /> <div className="size-7" />
</div> </div>
<div className="mt-auto flex flex-col gap-1 pt-8"> <div className="mt-auto flex flex-col gap-1 pt-8">
<span className="line-clamp-2 text-[15px] font-medium leading-snug text-[#1f1f1f]"> <span className={`line-clamp-2 text-[15px] font-medium leading-snug ${isDark ? "text-[#e8eaf0]" : "text-[#1f1f1f]"}`}>
{nb.name} {nb.name}
</span> </span>
<span className="text-[13px] text-[#666]">{nb.source_count} 个来源</span> <span className={`text-[13px] ${isDark ? "text-[#8b93a7]" : "text-[#666]"}`}>{nb.source_count} 个来源</span>
</div> </div>
</> </>
); );
return ( return (
<div className="group relative flex flex-col rounded-[20px] bg-[#f2f2f2] transition-colors hover:bg-[#e8e8e8]" style={{ minHeight: 180 }}> <div className={`group relative flex flex-col rounded-[20px] transition-colors ${isDark ? "bg-[#1e2330] hover:bg-[#262c3b]" : "bg-[#f2f2f2] hover:bg-[#e8e8e8]"}`} style={{ minHeight: 180 }}>
{onOpen ? ( {onOpen ? (
<button <button
type="button" type="button"
@@ -125,6 +131,8 @@ export default function StudioNotebooksPage({
onOpenNotebook?: (id: number) => void; onOpenNotebook?: (id: number) => void;
} = {}) { } = {}) {
const { notebooks, isLoading, isError } = useStudioNotebooks(); const { notebooks, isLoading, isError } = useStudioNotebooks();
const { theme } = useTheme();
const isDark = theme === "dark";
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// ── 新建 ── // ── 新建 ──
@@ -221,11 +229,12 @@ export default function StudioNotebooksPage({
{/* Grid */} {/* Grid */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{isLoading {isLoading
? Array.from({ length: 8 }).map((_, i) => <NotebookCardSkeleton key={i} />) ? Array.from({ length: 8 }).map((_, i) => <NotebookCardSkeleton key={i} isDark={isDark} />)
: notebooks.map((nb) => ( : notebooks.map((nb) => (
<NotebookCard <NotebookCard
key={nb.id} key={nb.id}
nb={nb} nb={nb}
isDark={isDark}
onRename={(nb) => { setRenameTarget(nb); setRenameName(nb.name); setRenameError(""); }} onRename={(nb) => { setRenameTarget(nb); setRenameName(nb.name); setRenameError(""); }}
onDelete={(nb) => { setDeleteTarget(nb); setDeleteError(""); }} onDelete={(nb) => { setDeleteTarget(nb); setDeleteError(""); }}
onOpen={onOpenNotebook} onOpen={onOpenNotebook}