feat(ui): theme & layout improvements

- Dark-blue theme background assets and styling integration
- Appearance settings with layoutMode (full/compact) preference
- Settings dialog and auth context updates
- AI writing form and panel refinements
- Open Canvas context updates
- Roundtable planning styling and dashboard enhancements
- Strategy components and header updates
- Light app management and routing improvements
- Global style enhancements for dark theme
- Various page layout and workspace routing optimizations
This commit is contained in:
董宏凯 committed 2026-06-14 19:30:54 +08:00
1 parent da2864f422
commit 56449ed129
33 files changed
+1133 -491

No files matched your search

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><image width="1920" height="1080" xlink:href="data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMwaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMCAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUVERkMwNjI0Q0RCMTFGMDg5MTZCMkY2RDg1RkVEQkUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUVERkMwNjM0Q0RCMTFGMDg5MTZCMkY2RDg1RkVEQkUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5RURGQzA2MDRDREIxMUYwODkxNkIyRjZEODVGRURCRSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5RURGQzA2MTRDREIxMUYwODkxNkIyRjZEODVGRURCRSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAYEBAQFBAYFBQYJBgUG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 truncated

After

Width:  |  Height:  |  Size: 431 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="152" height="42.359" viewBox="0 0 152 42.359"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="none" width="100%" height="100%" viewBox="0 0 536 150"><image width="536" height="150" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 33 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="64" viewBox="0 0 1920 64"><defs><style>.a{fill:url(#a);}.b{fill:url(#b);}.c{clip-path:url(#c);}.d{fill:url(#d);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#131f3d"/><stop offset="1" stop-color="#094593"/></linearGradient><linearGradient id="b" x1="0.473" x2="0.473" y2="0.95" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#131f3d"/><stop offset="1" stop-color="#094593" stop-opacity="0"/></linearGradient><clipPath id="c"><rect class="a" width="1920" height="64" transform="translate(-2 5)"/></clipPath><pattern id="d" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 1917 104"><image width="1917" height="104" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 98 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="30" viewBox="0 0 32 30"><defs><style>.a{fill:rgba(255,255,255,0.05);}.b{fill:url(#a);}</style><linearGradient id="a" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#a2e1f9"/><stop offset="1" stop-color="#45c3f3"/></linearGradient></defs><g transform="translate(1.411 -9.285)"><rect class="a" width="32" height="30" rx="4" transform="translate(-1.411 9.285)"/><path class="b" d="M4618.588,4844.6a1.257,1.257,0,1,1,1.258,1.256A1.258,1.258,0,0,1,4618.588,4844.6Zm4.609.838v-1.676h12.569v1.676Zm-4.609-5.867a1.257,1.257,0,1,1,1.258,1.257A1.257,1.257,0,0,1,4618.588,4839.569Zm4.609.838v-1.676h12.569v1.676Zm-4.609-5.865a1.257,1.257,0,1,1,1.258,1.257A1.257,1.257,0,0,1,4618.588,4834.542Zm4.609.838V4833.7h12.569v1.676Z" transform="translate(-4613 -4815)"/></g></svg>

After

Width:  |  Height:  |  Size: 892 B

+23 -33
View File
@@ -379,6 +379,13 @@
overflow-y: hidden;
}
/* 左右分组:不撑满、不裁切选中高光 */
.top-submenu-bar--group {
flex: 0 0 auto;
margin-left: 0;
overflow: visible;
}
.top-submenu-list {
min-width: max-content;
height: 64px;
@@ -398,48 +405,31 @@
border: 0;
padding: 0 18px;
background: transparent;
color: rgba(224, 242, 255, 0.9);
color: rgba(255, 255, 255, 0.8);
font-family: "Source Han Sans SC", "Noto Sans SC", "Noto Sans S Chinese", "Microsoft YaHei", sans-serif;
font-size: 16px;
font-size: 18px;
line-height: 1;
font-weight: 700;
letter-spacing: 0;
white-space: nowrap;
cursor: pointer;
overflow: hidden;
transition: color 0.18s ease, text-shadow 0.18s ease, background 0.18s ease;
overflow: visible;
transition: color 0.18s ease, text-shadow 0.18s ease;
}
.top-submenu-item:hover,
.top-submenu-item:hover {
color: #9fe3ff;
text-shadow: 0 0 10px rgba(91, 210, 255, 0.4);
}
/* 选中态:设计稿提供的 nav-active.svg 发光高光 + 青色文字 */
.top-submenu-item--active {
color: #ffffff;
text-shadow: 0 0 10px rgba(91, 210, 255, 0.45);
background:
linear-gradient(135deg, rgba(15, 84, 169, 0.52) 0%, rgba(5, 43, 103, 0.34) 76%),
linear-gradient(180deg, rgba(39, 126, 230, 0.2) 0%, rgba(0, 50, 122, 0.26) 100%);
box-shadow: inset 0 1px 0 rgba(98, 180, 255, 0.16), inset 0 -18px 22px rgba(0, 31, 79, 0.24);
}
.top-submenu-item--active::before {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
background: linear-gradient(90deg, rgba(59, 162, 255, 0) 0%, #35d7ff 50%, rgba(59, 162, 255, 0) 100%);
box-shadow: 0 0 12px rgba(53, 215, 255, 0.9), 0 -6px 18px rgba(33, 143, 255, 0.36);
}
.top-submenu-item--active::after {
content: '';
position: absolute;
left: 16px;
right: 16px;
bottom: 0;
height: 16px;
background: radial-gradient(ellipse at bottom, rgba(37, 189, 255, 0.34) 0%, rgba(37, 189, 255, 0) 68%);
pointer-events: none;
color: #64cdf5;
text-shadow: 0 0 12px rgba(100, 205, 245, 0.5);
background-image: url("../assets/img/nav-active.svg");
background-repeat: no-repeat;
background-position: center calc(50% + 7px);
background-size: 152px 42px;
}
.third-submenu-bar {
@@ -1,7 +1,8 @@
"use client";
import { DropletIcon, LayoutGridIcon, ListIcon, MoonIcon, SunIcon } from "lucide-react";
import { DropletIcon, LayoutGridIcon, LayoutIcon, ListIcon, MoonIcon, PanelLeftIcon, SunIcon } from "lucide-react";
import { useMemo, type ComponentType, type SVGProps } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useTheme } from "@/contexts/ThemeContext";
@@ -42,9 +43,10 @@ const languageOptions: { value: Locale; label: string }[] = [
{ value: "zh-CN", label: zhCN.locale.localName },
];
export function AppearanceSettingsPage() {
export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
const { t, locale, changeLocale } = useI18n();
const { theme, setTheme } = useTheme();
const navigate = useNavigate();
const currentTheme = theme;
const [settings, setSettings] = useLocalSettings();
const isAdmin = getStoredAuth()?.system_role === "admin";
@@ -145,6 +147,42 @@ export function AppearanceSettingsPage() {
</div>
</SettingsSection>
<Separator />
<SettingsSection
title="界面模式"
description="全量模式展示侧边栏与顶栏导航;简洁模式隐藏侧边栏与顶栏,获得更大的工作区。"
>
<div className="flex gap-2">
<Button
type="button"
variant={settings.display.layoutMode !== "compact" ? "default" : "outline"}
size="sm"
onClick={() => {
setSettings("display", { layoutMode: "full" });
onClose?.();
navigate("/page/workspace/chats/new");
}}
>
<PanelLeftIcon className="mr-1.5 size-4" />
全量模式
</Button>
<Button
type="button"
variant={settings.display.layoutMode === "compact" ? "default" : "outline"}
size="sm"
onClick={() => {
setSettings("display", { layoutMode: "compact" });
onClose?.();
navigate("/page/workspace/chats/new");
}}
>
<LayoutIcon className="mr-1.5 size-4" />
简洁模式
</Button>
</div>
</SettingsSection>
{isAdmin && (
<>
<Separator />
@@ -121,7 +121,9 @@ export function SettingsDialog(props: SettingsDialogProps) {
<ScrollArea className="h-full min-h-0 min-w-0 rounded-lg border">
<div className="min-w-0 space-y-8 p-6">
{activeSection === "account" && <AccountSettingsPage />}
{activeSection === "appearance" && <AppearanceSettingsPage />}
{activeSection === "appearance" && (
<AppearanceSettingsPage onClose={() => props.onOpenChange?.(false)} />
)}
{activeSection === "memory" && <MemorySettingsPage />}
{activeSection === "tools" && <ToolSettingsPage />}
{activeSection === "about" && <AboutSettingsPage />}
+31
View File
@@ -44,6 +44,37 @@ export function clearStoredAuth() {
window.localStorage.removeItem(AUTH_STORAGE_KEY);
}
/**
* 把登录用户名写入 localStorage.userInfo.userName(顶栏用户区据此显示)。
* 仅在尚未设置时写入,避免覆盖外部登录(yUserId)从 consumer 拿到的用户名。
*/
export function setStoredUserName(name: string | null | undefined) {
if (!isBrowser()) {
return;
}
const trimmed = name?.trim();
if (!trimmed) {
return;
}
let info: Record<string, unknown> = {};
try {
const raw = window.localStorage.getItem("userInfo");
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === "object") {
info = parsed as Record<string, unknown>;
}
}
} catch {
info = {};
}
if (typeof info.userName === "string" && info.userName.trim()) {
return;
}
info.userName = trimmed;
window.localStorage.setItem("userInfo", JSON.stringify(info));
}
export function getAccessToken(): string | undefined {
const auth = getStoredAuth();
const token = auth?.access_token?.trim();
+5
View File
@@ -62,6 +62,11 @@ export interface LocalSettings {
*/
skillViewMode?: "card" | "list";
agentViewMode?: "card" | "list";
/**
* Layout mode: "full" shows the page sidebar + header; "compact" hides them
* for a cleaner work area. Defaults to "full" (undefined treated as "full").
*/
layoutMode?: "full" | "compact";
};
context: Omit<
AgentThreadContext,
@@ -47,7 +47,7 @@ const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; des
{ value: 'notebook', label: '我的空间', desc: '从空间中检索素材' },
];
const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222]';
const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222] dark:text-[#AACEFF]';
const FIELD_TRIGGER_CLASS =
'h-12 w-full rounded-xl border-[#e8dedd] bg-white text-sm shadow-sm';
@@ -254,7 +254,7 @@ export function AIWritingPanel({
const showNewButton = isHistoryMode || state.status !== 'idle';
return (
<div className="flex flex-col h-full overflow-hidden bg-background">
<div className="flex flex-col h-full overflow-hidden bg-background dark:bg-transparent">
{!isHistoryMode && (
<DraftSync status={state.status} markdown={state.currentDraftMarkdown} title={state.currentDraftTitle} />
)}
@@ -114,7 +114,7 @@ export function WritingSetupChat() {
return (
<ThreadContext.Provider value={{ thread }}>
<ArtifactsProvider>
<div className="flex size-full flex-col bg-background">
<div className="flex size-full flex-col bg-background dark:bg-transparent">
{/* 空状态隐藏对话头部,直接展示欢迎屏(与原型图一致);有消息后再显示头部 */}
{!showWelcome && (
<header className="flex shrink-0 items-center gap-2 border-b px-4 py-3">
@@ -139,7 +139,7 @@ export function WritingSetupChat() {
)}
</div>
<div className="bg-background shrink-0 border-t px-4 py-4">
<div className="bg-background dark:bg-transparent shrink-0 border-t px-4 py-4">
{/* 与原型图一致:白色圆角卡片包裹「模型选择行 + 输入框」 */}
<div className="space-y-3 rounded-2xl border border-[#ece5e4] bg-card p-3 shadow-sm dark:border-[#1B477E] dark:bg-[#022248]/40">
<div className="flex items-center justify-between gap-3">
@@ -59,7 +59,7 @@ export function useWritingFormOptional(): WritingFormValue | null {
export function WritingFormProvider({ children, initialMaterialSource }: { children: ReactNode; initialMaterialSource?: 'general' | 'notebook' }) {
const [intent, setIntent] = useState('');
const [formExpanded, setFormExpanded] = useState(false);
const [showOutline, setShowOutline] = useState(false);
const [showOutline, setShowOutline] = useState(true);
const [outlineText, setOutlineText] = useState('');
const [articleType, setArticleType] = useState('analysis');
const [wordCountInput, setWordCountInput] = useState('800');
@@ -229,7 +229,7 @@ export default function AIWritingPage({
<AIWritingProvider>
<WritingFormProvider initialMaterialSource={studioToken ?? initialNotebookId != null ? 'notebook' : undefined}>
<TooltipProvider delayDuration={300}>
<div className="h-full flex flex-col overflow-hidden bg-background">
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
<div className="flex-1 min-h-0 overflow-hidden">
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
</div>
+1 -1
View File
@@ -180,7 +180,7 @@ export default function AgentChatPage() {
separate flex sibling OUTSIDE ChatBox, so the artifact preview
always opens between the chat and the detail panel — the detail
panel stays rightmost. */}
<div className="flex size-full min-h-0 overflow-hidden">
<div className="flex size-full min-h-0 overflow-hidden workspace-chat-bg">
<div className="min-h-0 min-w-0 flex-1">
<ChatBox threadId={threadId}>
{/* Main conversation column. `relative` hosts the floating
@@ -1,14 +1,23 @@
/**
* 配置管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
* 配置管理页 —— 把固定业务码绑定到「智能体」+「业务链」。
*
* 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉:
* - 智能体来自 `/api/agents`
* - 业务链来自 `/api/roundtable-chains?scope=public`(只列公共链,保证全局共享时各账号都能解析)
* 智能体来自 `/api/agents`,业务链来自 `/api/roundtable-chains?scope=public`。
* 保存调 `PUT /api/business-mapping/{code}`。映射全局共享、无用户隔离。
*/
import { useEffect, useMemo, useState } from "react";
import { Loading, Select } from "tdesign-react";
import { Link2, Pencil, Save, Users, X } from "lucide-react";
import { motion } from "motion/react";
import { Select } from "tdesign-react";
import {
AlertCircle,
HelpCircle,
Info,
Link2,
Pencil,
Save,
Search,
Users,
X,
} from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "@/contexts/ThemeContext";
@@ -21,11 +30,7 @@ import {
updateBusinessMapping,
type BusinessMapping,
} from "@/strategy-components/api/business-mapping";
interface DraftSelection {
agentId: string | null;
chainId: string | null;
}
import { cn } from "@/lib/utils";
export default function BusinessChainMappingPage() {
const { theme } = useTheme();
@@ -36,8 +41,13 @@ export default function BusinessChainMappingPage() {
const [chains, setChains] = useState<BusinessChain[]>([]);
const [loading, setLoading] = useState(true);
const [editingCode, setEditingCode] = useState<string | null>(null);
const [draft, setDraft] = useState<DraftSelection>({ agentId: null, chainId: null });
const [draft, setDraft] = useState<{ agentId: string | null; chainId: string | null }>({
agentId: null,
chainId: null,
});
const [saving, setSaving] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | "configured" | "pending">("all");
useEffect(() => {
let cancelled = false;
@@ -58,9 +68,7 @@ export default function BusinessChainMappingPage() {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
return () => { cancelled = true; };
}, []);
const agentOptions = useMemo(
@@ -72,6 +80,24 @@ export default function BusinessChainMappingPage() {
[chains],
);
const stats = useMemo(() => {
const total = rows.length;
const boundAgents = rows.filter((r) => r.agentId !== null).length;
return { total, boundAgents };
}, [rows]);
const filteredRows = useMemo(() => {
return rows.filter((r) => {
const q = searchQuery.toLowerCase();
const matchesSearch =
r.label.toLowerCase().includes(q) || r.businessCode.toLowerCase().includes(q);
const isConfigured = r.agentId !== null && r.chainId !== null;
if (statusFilter === "configured") return matchesSearch && isConfigured;
if (statusFilter === "pending") return matchesSearch && !isConfigured;
return matchesSearch;
});
}, [rows, searchQuery, statusFilter]);
const beginEdit = (row: BusinessMapping) => {
setEditingCode(row.businessCode);
setDraft({ agentId: row.agentId, chainId: row.chainId });
@@ -86,10 +112,10 @@ export default function BusinessChainMappingPage() {
setSaving(true);
try {
const agentName = draft.agentId
? agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null
? (agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null)
: null;
const chainTitle = draft.chainId
? chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null
? (chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null)
: null;
const updated = await updateBusinessMapping(row.businessCode, {
agentId: draft.agentId,
@@ -111,162 +137,416 @@ export default function BusinessChainMappingPage() {
}
};
/* ── 样式 ── */
const shellClass = isDark
? "h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100"
: "h-full overflow-y-auto bg-[linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]";
? "h-full overflow-y-auto text-slate-100 workspace-chat-bg"
: "h-full overflow-y-auto bg-white text-[#222]";
// 卡片:无边框,纯阴影
const cardClass = isDark
? "border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] shadow-[0_16px_36px_rgba(3,10,22,0.2)]"
: "border border-[#E8E8E8] bg-white shadow-[0_14px_30px_rgba(245,34,45,0.06)]";
const headRowClass = isDark ? "text-slate-400" : "text-[#8C8C8C]";
const rowBorder = isDark ? "border-[rgba(67,89,124,0.32)]" : "border-[#F0F0F0]";
? "bg-[rgba(12,29,54,0.88)] shadow-[0_8px_32px_rgba(3,10,22,0.28)] backdrop-blur-sm"
: "bg-white shadow-[0_8px_32px_rgba(0,0,0,0.07)]";
// 行分隔:极淡,不带色
const rowDivider = isDark ? "border-white/[0.04]" : "border-black/[0.04]";
const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]";
const codeBadge = isDark
? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]"
: "bg-[#FFF1F0] text-[#CF1322]";
const subText = isDark ? "text-slate-400" : "text-slate-500";
const filterTabs: { id: "all" | "configured" | "pending"; label: string }[] = [
{ id: "all", label: "全部业务" },
{ id: "configured", label: "已绑定" },
{ id: "pending", label: "待配置" },
];
return (
<div className={shellClass}>
<div className="mx-auto flex min-h-full max-w-[1280px] flex-col gap-5 px-6 py-6 lg:px-8">
<div className="space-y-1">
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? "text-slate-50" : "text-[#222222]"}`}>
配置管理
</h1>
<p className={`text-sm ${isDark ? "text-slate-300/80" : "text-slate-500"}`}>
为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。
</p>
</div>
<div className={cn(shellClass, "[&_.t-input]:rounded-[5px] [&_.t-input__wrap]:rounded-[5px]")}>
<div className="mx-auto flex min-h-full max-w-7xl flex-col gap-6 px-6 py-7 lg:px-8">
<Loading loading={loading} showOverlay={false}>
<div className={`overflow-hidden rounded-3xl ${cardClass}`}>
<table className="w-full border-collapse text-sm">
<thead>
<tr className={`text-left text-xs uppercase tracking-widest ${headRowClass}`}>
<th className="px-6 py-4 font-semibold">业务</th>
<th className="px-6 py-4 font-semibold">智能体</th>
<th className="px-6 py-4 font-semibold">业务链</th>
<th className="px-6 py-4 text-right font-semibold">操作</th>
</tr>
</thead>
<tbody>
{rows.length === 0 && !loading ? (
<tr>
<td colSpan={4} className={`px-6 py-12 text-center ${mutedText}`}>
暂无业务数据
</td>
</tr>
) : (
rows.map((row) => {
const editing = editingCode === row.businessCode;
return (
<tr key={row.businessCode} className={`border-t ${rowBorder}`}>
{/* 业务(只读) */}
<td className="px-6 py-4 align-middle">
<div className="flex items-center gap-2">
<span className={`text-[15px] font-semibold ${isDark ? "text-white" : "text-[#222222]"}`}>
{row.label}
</span>
<span className={`rounded-full px-2 py-0.5 text-[11px] font-mono ${codeBadge}`}>
{row.businessCode}
</span>
</div>
</td>
{/* ── 页头 banner ── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className={cn("rounded-[5px] p-6 lg:p-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-6 relative overflow-hidden", cardClass)}
>
<div
className="absolute right-0 top-0 w-72 h-32 pointer-events-none"
style={{
background: isDark
? "radial-gradient(ellipse at top right, rgba(77,141,255,0.07), transparent)"
: "radial-gradient(ellipse at top right, rgba(245,34,45,0.04), transparent)",
}}
/>
{/* 智能体 */}
<td className="px-6 py-4 align-middle">
{editing ? (
<Select
className="min-w-[200px]"
value={draft.agentId ?? undefined}
options={agentOptions}
filterable
clearable
placeholder="搜索并选择智能体"
onChange={(v) => setDraft((d) => ({ ...d, agentId: (v as string) ?? null }))}
/>
) : (
<span className="inline-flex items-center gap-1.5">
<Users className={`h-3.5 w-3.5 ${mutedText}`} />
{row.agentName || row.agentId ? (
<span>{row.agentName || row.agentId}</span>
) : (
<span className={mutedText}>未配置</span>
)}
</span>
)}
</td>
{/* 业务链 */}
<td className="px-6 py-4 align-middle">
{editing ? (
<Select
className="min-w-[220px]"
value={draft.chainId ?? undefined}
options={chainOptions}
filterable
clearable
placeholder="搜索并选择业务链(公共)"
onChange={(v) => setDraft((d) => ({ ...d, chainId: (v as string) ?? null }))}
/>
) : (
<span className="inline-flex items-center gap-1.5">
<Link2 className={`h-3.5 w-3.5 ${mutedText}`} />
{row.chainTitle || row.chainId ? (
<span>{row.chainTitle || row.chainId}</span>
) : (
<span className={mutedText}>未配置</span>
)}
</span>
)}
</td>
{/* 操作 */}
<td className="px-6 py-4 text-right align-middle">
{editing ? (
<div className="inline-flex items-center gap-2">
<button
type="button"
disabled={saving}
onClick={() => handleSave(row)}
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
isDark ? "bg-[rgba(34,197,94,0.18)] text-[#86EFAC]" : "bg-[#EEFCF7] text-[#0F9D6C]"
}`}
>
<Save className="h-3.5 w-3.5" />
保存
</button>
<button
type="button"
disabled={saving}
onClick={cancelEdit}
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
isDark ? "bg-[rgba(110,136,176,0.18)] text-slate-300" : "bg-[#F5F5F5] text-[#575757]"
}`}
>
<X className="h-3.5 w-3.5" />
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => beginEdit(row)}
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium ${
isDark ? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]" : "bg-[#FFF1F0] text-[#CF1322]"
}`}
>
<Pencil className="h-3.5 w-3.5" />
编辑
</button>
)}
</td>
</tr>
);
})
<div className="space-y-3 relative z-10">
<div className="flex flex-wrap items-center gap-2.5">
<h2 className={cn("text-xl font-bold tracking-tight", isDark ? "text-slate-50" : "text-slate-900")}>
配置管理
</h2>
<span
className={cn(
"text-[10px] uppercase font-mono tracking-wider font-bold px-2.5 py-1 rounded-[5px] flex items-center gap-1.5",
isDark
? "bg-[rgba(77,141,255,0.12)] text-[#9CCBFF]"
: "bg-red-50/80 text-red-500",
)}
</tbody>
</table>
>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse", isDark ? "bg-[#55A6FE]" : "bg-red-500")} />
智能会商神经中枢
</span>
<span
className={cn(
"text-[10px] font-mono tracking-wider px-2 py-0.5 rounded-[5px]",
isDark ? "text-slate-500 bg-white/5" : "text-slate-400 bg-slate-50",
)}
>
v1.2.5
</span>
</div>
<p className={cn("text-xs max-w-2xl leading-relaxed", subText)}>
业务流转与交叉验证的高级控制中心。在此注册高频业务模块并绑定专职执行智能体(Agent)及公共流转决策链(Chain),决策信息将基于该拓扑自动触发多智能体自诊断及会商组织。
</p>
<div className={cn("flex flex-wrap items-center gap-x-4 gap-y-1.5 pt-1 text-[11px] font-medium", mutedText)}>
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-green-500" />
会商引擎: 正常运行中
</span>
<span className="opacity-30">•</span>
<span>
全局配置项:{" "}
<strong className={isDark ? "text-slate-200 font-semibold" : "text-slate-700 font-semibold"}>
{stats.total} 项注册
</strong>
</span>
<span className="opacity-30">•</span>
<span>
指定专家节点:{" "}
<strong className={isDark ? "text-[#9CCBFF] font-semibold" : "text-red-500 font-semibold"}>
{stats.boundAgents} / {stats.total}
</strong>
</span>
</div>
</div>
</Loading>
</motion.div>
{/* ── 表格区 ── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
className={cn("rounded-[5px] overflow-hidden", cardClass)}
>
{/* 搜索 + 筛选 */}
<div className={cn("px-6 py-5 border-b flex flex-col sm:flex-row sm:items-center justify-between gap-4", rowDivider)}>
<div className="relative w-full max-w-sm">
<Search className={cn("absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4", mutedText)} />
<input
type="text"
placeholder="搜索业务名称、ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={cn(
"w-full pl-10 pr-4 py-2.5 rounded-[5px] text-xs transition-all placeholder:text-slate-400 focus:outline-none",
isDark
? "text-slate-200 bg-white/5 focus:bg-white/8 focus:ring-1 focus:ring-[#4D8DFF]/30"
: "text-slate-700 bg-slate-50/60 hover:bg-slate-50 focus:bg-white focus:ring-1 focus:ring-red-400/30",
)}
/>
</div>
<div className={cn("flex items-center gap-1 p-1 rounded-[5px] self-end sm:self-auto shrink-0", isDark ? "bg-white/5" : "bg-slate-100/70")}>
{filterTabs.map((tab) => (
<button
key={tab.id}
onClick={() => setStatusFilter(tab.id)}
className={cn(
"px-3.5 py-1.5 rounded-[5px] text-xs font-semibold transition-all cursor-pointer",
statusFilter === tab.id
? isDark
? "bg-[rgba(77,141,255,0.18)] text-[#9CCBFF] shadow-sm"
: "bg-white text-gray-900 shadow-sm"
: isDark
? "text-slate-500 hover:text-slate-300"
: "text-gray-400 hover:text-gray-700",
)}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 表格 */}
<div className="overflow-x-auto">
{loading ? (
<div className={cn("py-16 text-center text-sm", mutedText)}>加载中…</div>
) : (
<table className="w-full table-fixed text-left border-collapse">
<thead>
<tr className={cn("border-b", rowDivider, isDark ? "bg-white/[0.02]" : "bg-black/[0.02]")}>
<th className={cn("pl-8 pr-4 py-3.5 text-xs font-semibold uppercase tracking-wider w-[20%]", mutedText)}>业务</th>
<th className={cn("px-4 py-3.5 text-xs font-semibold uppercase tracking-wider w-[26%]", mutedText)}>智能体</th>
<th className={cn("px-4 py-3.5 text-xs font-semibold uppercase tracking-wider w-[26%]", mutedText)}>业务链</th>
<th className={cn("pl-4 pr-8 py-3.5 text-xs font-semibold uppercase tracking-wider w-[28%] text-right", mutedText)}>操作</th>
</tr>
</thead>
<tbody>
{filteredRows.length > 0 ? (
filteredRows.map((row) => {
const editing = editingCode === row.businessCode;
return (
<tr
key={row.businessCode}
className={cn(
"border-b last:border-0 transition-colors duration-150",
rowDivider,
!editing && (isDark ? "hover:bg-white/[0.025]" : "hover:bg-black/[0.015]"),
editing && (isDark ? "bg-[rgba(77,141,255,0.04)]" : "bg-red-50/30"),
)}
>
{/* 业务 */}
<td className="pl-8 pr-4 py-5 align-middle">
<div className="flex items-center gap-2.5">
<span className={cn("font-semibold tracking-tight text-sm", isDark ? "text-slate-100" : "text-gray-800")}>
{row.label}
</span>
<span
className={cn(
"text-[10px] font-mono font-bold px-2 py-0.5 rounded-[5px] leading-none",
isDark
? "bg-[rgba(77,141,255,0.12)] text-[#9CCBFF]"
: "bg-red-50 text-red-400",
)}
>
{row.businessCode}
</span>
</div>
</td>
{/* 智能体 */}
<td className="px-4 py-4 align-middle">
{editing ? (
<Select
className="min-w-[200px]"
value={draft.agentId ?? undefined}
options={agentOptions}
filterable
clearable
placeholder="搜索并选择智能体"
onChange={(v) =>
setDraft((d) => ({ ...d, agentId: (v as string) ?? null }))
}
/>
) : (
<div className="flex items-center gap-2.5">
<div
className={cn(
"p-1.5 rounded-[5px] transition-colors",
row.agentId
? isDark
? "bg-[rgba(77,141,255,0.1)] text-[#9CCBFF]"
: "bg-red-50 text-red-400"
: isDark
? "bg-white/5 text-slate-600"
: "bg-slate-50 text-gray-300",
)}
>
<Users className="w-3.5 h-3.5 shrink-0" />
</div>
{row.agentName || row.agentId ? (
<div className="space-y-0.5">
<span className={cn("text-xs font-semibold", isDark ? "text-slate-200" : "text-gray-800")}>
{row.agentName || row.agentId}
</span>
{row.agentId && agents.find((a) => a.id === row.agentId)?.description && (
<span className={cn("block text-[10px]", mutedText)}>
{(agents.find((a) => a.id === row.agentId)?.description ?? "").slice(0, 40)}
{(agents.find((a) => a.id === row.agentId)?.description?.length ?? 0) > 40 ? "…" : ""}
</span>
)}
</div>
) : (
<span className={cn("text-xs", mutedText)}>未配置</span>
)}
</div>
)}
</td>
{/* 业务链 */}
<td className="px-4 py-4 align-middle">
{editing ? (
<Select
className="min-w-[220px]"
value={draft.chainId ?? undefined}
options={chainOptions}
filterable
clearable
placeholder="搜索并选择业务链(公共)"
onChange={(v) =>
setDraft((d) => ({ ...d, chainId: (v as string) ?? null }))
}
/>
) : (
<div className="flex items-center gap-2.5">
<div
className={cn(
"p-1.5 rounded-[5px] transition-colors",
row.chainId
? isDark
? "bg-[rgba(34,197,94,0.08)] text-green-400"
: "bg-blue-50 text-blue-400"
: isDark
? "bg-white/5 text-slate-600"
: "bg-slate-50 text-gray-300",
)}
>
<Link2 className="w-3.5 h-3.5 shrink-0" />
</div>
{row.chainTitle || row.chainId ? (
<div className="space-y-0.5">
<span className={cn("text-xs font-semibold", isDark ? "text-slate-200" : "text-gray-800")}>
{row.chainTitle || row.chainId}
</span>
{row.chainId && (
<span className={cn("flex items-center gap-1 text-[10px] leading-none", mutedText)}>
<span
className={cn(
"px-1 rounded-[5px] text-[9px] font-medium",
isDark
? "bg-[rgba(34,197,94,0.08)] text-green-400"
: "bg-green-50 text-green-600",
)}
>
公共
</span>
节点数: {chains.find((c) => c.id === row.chainId)?.seats?.length ?? "—"}
</span>
)}
</div>
) : (
<span className={cn("text-xs", mutedText)}>未配置</span>
)}
</div>
)}
</td>
{/* 操作 */}
<td className="pl-4 pr-8 py-4 text-right align-middle whitespace-nowrap">
{editing ? (
<div className="inline-flex items-center gap-2 justify-end">
<button
type="button"
disabled={saving}
onClick={() => handleSave(row)}
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 py-1.5 text-xs font-medium transition-all active:scale-95 disabled:opacity-50",
isDark
? "bg-[rgba(34,197,94,0.12)] text-green-400 hover:bg-[rgba(34,197,94,0.2)]"
: "bg-green-50 text-green-600 hover:bg-green-100",
)}
>
<Save className="h-3.5 w-3.5" />
保存
</button>
<button
type="button"
disabled={saving}
onClick={cancelEdit}
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 py-1.5 text-xs font-medium transition-all active:scale-95 disabled:opacity-50",
isDark
? "bg-white/5 text-slate-400 hover:bg-white/10 hover:text-slate-200"
: "bg-slate-50 text-slate-500 hover:bg-slate-100",
)}
>
<X className="h-3.5 w-3.5" />
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => beginEdit(row)}
title="编辑配置"
className={cn(
"inline-flex items-center gap-1.5 rounded-[5px] px-3 py-1.5 text-xs font-medium transition-all duration-200 active:scale-95",
isDark
? "text-[#9CCBFF] bg-[rgba(77,141,255,0.12)] hover:bg-[rgba(77,141,255,0.18)]"
: "text-red-500 bg-red-50 hover:bg-red-100",
)}
>
<Pencil className="h-3.5 w-3.5" />
<span>编辑</span>
</button>
)}
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={4} className="py-14 text-center">
<div className="flex flex-col items-center justify-center gap-2">
<AlertCircle className={cn("w-7 h-7", isDark ? "text-slate-700" : "text-gray-200")} />
<p className={cn("text-xs font-medium", mutedText)}>未找到符合筛选条件的业务配置</p>
<button
onClick={() => { setSearchQuery(""); setStatusFilter("all"); }}
className={cn("text-[11px] font-semibold mt-1 hover:underline", isDark ? "text-[#9CCBFF]" : "text-red-500")}
>
重置筛选条件
</button>
</div>
</td>
</tr>
)}
</tbody>
</table>
)}
</div>
{/* 表格尾栏 */}
<div className={cn("px-6 py-4 border-t flex items-center justify-between text-xs font-medium", rowDivider, isDark ? "bg-white/[0.02]" : "bg-black/[0.015]", mutedText)}>
<span className="flex items-center gap-1.5">
<Info className="w-3.5 h-3.5" />
会商程序会在对应业务的业务链生效时开启多体交叉核验证。
</span>
<span className="font-mono text-[10px]">
显示 {filteredRows.length} / {rows.length} 条
</span>
</div>
</motion.div>
{/* ── 使用说明 ── */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.12 }}
className={cn(
"rounded-[5px] p-5 flex gap-4 text-xs leading-relaxed",
isDark
? "bg-[rgba(245,158,11,0.05)] text-amber-300/70"
: "bg-amber-50/50 text-amber-800",
)}
>
<div className={cn("w-8 h-8 rounded-[5px] shrink-0 flex items-center justify-center", isDark ? "bg-[rgba(245,158,11,0.08)] text-amber-400" : "bg-amber-100/60 text-amber-500")}>
<HelpCircle className="w-4 h-4" />
</div>
<div>
<h4 className={cn("font-bold mb-0.5", isDark ? "text-amber-300" : "text-amber-900")}>如何配置业务流转?</h4>
<p className={isDark ? "text-amber-400/60" : "text-amber-700/80"}>
1. 将鼠标悬停在任一业务行右侧,点击出现的{" "}
<span className={cn("font-semibold", isDark ? "text-[#9CCBFF]" : "text-red-500")}>「编辑」</span>。
<br />
2. 在该行内直接搜索并选择 <strong>执行智能体</strong> 与 <strong>业务链</strong>,点击「保存」即时生效。
<br />
3. 若无需绑定,可搜索框清空后保存,置为「未配置」状态。
</p>
</div>
</motion.div>
</div>
</div>
);
+10 -10
View File
@@ -119,7 +119,7 @@ export default function LightAppListPage() {
<h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-[#E8EDF5]">
应用列表
</h1>
<span className="rounded-full border border-[#CF1322]/15 bg-[#FFF1F0] px-2 py-0.5 font-mono text-xs font-medium text-[#CF1322] dark:border-[#1677FF]/30 dark:bg-[#1677FF]/15 dark:text-[#4096FF]">
<span className="rounded-[5px] border border-[#CF1322]/15 bg-[#FFF1F0] px-2 py-0.5 font-mono text-xs font-medium text-[#CF1322] dark:border-[#1677FF]/30 dark:bg-[#1677FF]/15 dark:text-[#4096FF]">
导航工作台
</span>
</div>
@@ -129,7 +129,7 @@ export default function LightAppListPage() {
</div>
{/* 搜索 */}
<div className="rounded-2xl border border-zinc-200/80 bg-white p-5 shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347]">
<div className="rounded-[5px] bg-white p-5 shadow-sm dark:bg-[#122347]">
<div className="relative w-full">
<Search className="pointer-events-none absolute left-3.5 top-1/2 size-4.5 -translate-y-1/2 text-zinc-400 dark:text-[#6E88B0]" />
<input
@@ -137,7 +137,7 @@ export default function LightAppListPage() {
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="键入应用名称、目标地址、或描述,实时过滤检索…"
className="w-full rounded-xl border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25"
className="w-full rounded-[5px] border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25"
/>
{search && (
<button
@@ -171,7 +171,7 @@ export default function LightAppListPage() {
{/* 卡片网格 / 空态 */}
{apps.length === 0 ? (
<div className="rounded-2xl border border-dashed border-zinc-200 bg-white p-16 text-center shadow-sm dark:border-[#1A2F5A] dark:bg-[#122347]">
<div className="rounded-[5px] bg-white p-16 text-center shadow-sm dark:bg-[#122347]">
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full border border-zinc-100 bg-zinc-50 text-zinc-300 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#6E88B0]">
<Search className="size-6" />
</div>
@@ -205,7 +205,7 @@ export default function LightAppListPage() {
pageSize={PAGE_SIZE}
total={apps.length}
onPageChange={setPage}
className="pt-2"
className="pt-2 [&_button]:rounded-[5px]"
/>
)}
</>
@@ -240,11 +240,11 @@ function AppCard({
const hasParams = (app.routeParams ?? []).some((p) => p.key) && parsedUrl !== app.url;
return (
<div className="group relative flex flex-col justify-between overflow-hidden rounded-2xl border border-zinc-100 bg-white p-5 shadow-[0_2px_8px_rgba(0,0,0,0.01)] transition-all duration-300 hover:-translate-y-1 hover:border-zinc-200/90 hover:shadow-[0_12px_24px_rgba(0,0,0,0.04)] dark:border-[#1A2F5A] dark:bg-[#122347] dark:shadow-none dark:hover:border-[#2D4473]">
<div className="group relative flex flex-col justify-between overflow-hidden rounded-[5px] bg-white p-5 shadow-[0_2px_8px_rgba(0,0,0,0.01)] transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_12px_24px_rgba(0,0,0,0.04)] dark:bg-[#122347] dark:shadow-none">
{/* 上:图标 + 标题 + 描述 */}
<div>
<div className="mb-3.5 flex items-start gap-4">
<div className={cn("rounded-xl border border-white/60 p-3 shadow-sm dark:border-transparent", theme.iconBg)}>
<div className={cn("rounded-[5px] p-3 shadow-sm", theme.iconBg)}>
<Icon className="size-5.5" />
</div>
</div>
@@ -260,7 +260,7 @@ function AppCard({
{/* 下:URL 预览 + 动作 */}
<div className="mt-auto border-t border-zinc-100 pt-4 dark:border-[#1A2F5A]">
<div className="mb-4 rounded-xl border border-zinc-100 bg-zinc-50/70 p-3 dark:border-[#1A2F5A] dark:bg-[#0C1836]">
<div className="mb-4 rounded-[5px] bg-zinc-50/70 p-3 dark:bg-[#0C1836]">
<div className="mb-1 flex items-center justify-between text-[10px] font-semibold text-zinc-400 dark:text-[#6E88B0]">
<span>拼接后目标路由终端:</span>
<span className="text-[9px] font-bold uppercase tracking-wider text-[#CF1322] dark:text-[#4096FF]">
@@ -283,7 +283,7 @@ function AppCard({
type="button"
onClick={onCopy}
className={cn(
"flex items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-semibold transition",
"flex items-center justify-center gap-1.5 rounded-[5px] border px-3 py-2 text-xs font-semibold transition",
copied
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-[#52C41A]/30 dark:bg-[#52C41A]/15 dark:text-[#52C41A]"
: "border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 hover:text-zinc-800 dark:border-[#1A2F5A] dark:bg-[#122347] dark:text-[#C5CFE0] dark:hover:bg-[#1A2F5A]",
@@ -305,7 +305,7 @@ function AppCard({
<button
type="button"
onClick={onOpen}
className="flex items-center justify-center gap-1.5 rounded-lg bg-[#CF1322] px-3 py-2 text-xs font-semibold text-white shadow-[0_1px_3px_rgba(207,19,34,0.2)] transition hover:bg-[#A8071A] active:scale-[0.98] dark:bg-[#1677FF] dark:shadow-none dark:hover:bg-[#0958D9]"
className="flex items-center justify-center gap-1.5 rounded-[5px] bg-[#CF1322] px-3 py-2 text-xs font-semibold text-white shadow-[0_1px_3px_rgba(207,19,34,0.2)] transition hover:bg-[#A8071A] active:scale-[0.98] dark:bg-[#1677FF] dark:shadow-none dark:hover:bg-[#0958D9]"
>
<span>进入应用</span>
<ArrowUpRight className="size-3.5" />
@@ -22,6 +22,7 @@ import {
X,
} from "lucide-react";
import { toast } from "sonner";
import { Select } from "tdesign-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -52,6 +53,18 @@ interface FormState {
routeParams: LightAppRouteParam[];
}
const APP_TYPE_OPTIONS: { label: string; value: LightAppType }[] = [
{ label: "iframe 嵌套 (挂载到菜单)", value: "iframe" },
{ label: "window.open (新页跳转)", value: "window_open" },
];
const PARAM_TYPE_OPTIONS: { label: string; value: "login" | "theme" }[] = [
{ label: "登录参数", value: "login" },
{ label: "主题参数", value: "theme" },
];
const DIALOG_SELECT_POPUP = { attach: () => document.body };
const emptyForm = (): FormState => ({
name: "",
description: "",
@@ -209,11 +222,11 @@ export default function LightAppManagementPage() {
</h1>
<p className="max-w-2xl text-xs leading-relaxed text-neutral-500">
注册轻应用。
<code className="mx-1 rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
<code className="mx-1 rounded-[5px] bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
window.open
</code>
类型在「应用列表」以卡片展示;
<code className="mx-1 rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
<code className="mx-1 rounded-[5px] bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
iframe
</code>
类型挂载到所选菜单下并注入侧栏。
@@ -222,14 +235,14 @@ export default function LightAppManagementPage() {
<button
onClick={openCreate}
className="flex cursor-pointer items-center justify-center space-x-1.5 rounded-lg bg-[#A8071A] px-5 py-2.5 text-xs font-semibold text-white shadow-md shadow-rose-950/10 transition-all hover:bg-[#BC0D1E] hover:shadow-lg hover:shadow-rose-900/15 active:bg-[#820014]"
className="flex cursor-pointer items-center justify-center space-x-1.5 rounded-[5px] bg-[#A8071A] px-5 py-2.5 text-xs font-semibold text-white shadow-md shadow-rose-950/10 transition-all hover:bg-[#BC0D1E] hover:shadow-lg hover:shadow-rose-900/15 active:bg-[#820014]"
>
<Plus className="h-4 w-4" />
<span>注册应用</span>
</button>
</div>
<div className="flex flex-col items-center gap-3.5 rounded-xl border border-neutral-100 bg-white p-3 shadow-[0_1px_3px_rgba(0,0,0,0.01)] md:flex-row">
<div className="flex flex-col items-center gap-3.5 rounded-[5px] bg-white p-3 shadow-[0_1px_3px_rgba(0,0,0,0.01)] md:flex-row">
<div className="relative w-full flex-1">
<Search className="absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-400" />
<input
@@ -237,7 +250,7 @@ export default function LightAppManagementPage() {
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索应用名称 / 描述..."
className="w-full rounded-lg border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10"
className="w-full rounded-[5px] border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10"
/>
{search && (
<button
@@ -249,10 +262,10 @@ export default function LightAppManagementPage() {
)}
</div>
<div className="flex shrink-0 items-center space-x-0.5 self-end rounded-lg border border-neutral-200/50 bg-neutral-100/60 p-0.5 md:self-auto">
<div className="flex shrink-0 items-center space-x-0.5 self-end rounded-[5px] border border-neutral-200/50 bg-neutral-100/60 p-0.5 md:self-auto">
<button
onClick={() => setViewMode("table")}
className={`cursor-pointer rounded-md px-4 py-1.5 text-xs font-bold transition-all ${
className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
viewMode === "table"
? "bg-white text-[#F5222D] shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
@@ -262,7 +275,7 @@ export default function LightAppManagementPage() {
</button>
<button
onClick={() => setViewMode("cards")}
className={`cursor-pointer rounded-md px-4 py-1.5 text-xs font-bold transition-all ${
className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
viewMode === "cards"
? "bg-white text-[#F5222D] shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
@@ -280,7 +293,7 @@ export default function LightAppManagementPage() {
</div>
{viewMode === "table" ? (
<div className="overflow-visible rounded-xl border border-neutral-100 bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)]">
<div className="overflow-visible rounded-[5px] bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)]">
<table className="w-full table-fixed border-collapse text-left">
<thead>
<tr className="select-none border-b border-neutral-100/80 bg-neutral-50/40 text-[12.5px] font-semibold text-neutral-500">
@@ -304,12 +317,12 @@ export default function LightAppManagementPage() {
<td className="px-4 py-[18px]">
{app.appType === "window_open" ? (
<span className="inline-flex select-none items-center space-x-1.5 rounded-lg border border-rose-100 bg-rose-50 px-3 py-1 text-[12px] font-semibold text-rose-800">
<span className="inline-flex select-none items-center space-x-1.5 rounded-[5px] border border-rose-100 bg-rose-50 px-3 py-1 text-[12px] font-semibold text-rose-800">
<ExternalLink className="h-3.5 w-3.5" />
<span>新页跳转</span>
</span>
) : (
<span className="inline-flex select-none items-center space-x-1.5 rounded-lg border border-sky-100 bg-sky-50 px-3 py-1 text-[12px] font-semibold text-sky-800">
<span className="inline-flex select-none items-center space-x-1.5 rounded-[5px] border border-sky-100 bg-sky-50 px-3 py-1 text-[12px] font-semibold text-sky-800">
<Monitor className="h-3.5 w-3.5" />
<span>iframe 嵌套</span>
</span>
@@ -329,7 +342,7 @@ export default function LightAppManagementPage() {
</a>
<button
onClick={() => handleCopyUrl(app.url)}
className="shrink-0 rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
className="shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
title="复制网址"
>
<Copy className="h-3 w-3" />
@@ -339,7 +352,7 @@ export default function LightAppManagementPage() {
<td className="px-4 py-[18px]">
{app.appType === "iframe" && app.mountLocation ? (
<span className="inline-flex max-w-full items-center space-x-1.5 rounded bg-sky-50 px-2 py-0.5 text-[11px] font-medium text-sky-700">
<span className="inline-flex max-w-full items-center space-x-1.5 rounded-[5px] bg-sky-50 px-2 py-0.5 text-[11px] font-medium text-sky-700">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
<span className="truncate">{mountLabel(app.mountLocation)}</span>
</span>
@@ -357,7 +370,7 @@ export default function LightAppManagementPage() {
activeParamPopover === app.appId ? null : app.appId
)
}
className="inline-flex cursor-pointer items-center space-x-1.5 rounded border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100"
className="inline-flex cursor-pointer items-center space-x-1.5 rounded-[5px] border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100"
>
<span>{app.routeParams.length}个</span>
{activeParamPopover === app.appId ? (
@@ -376,7 +389,7 @@ export default function LightAppManagementPage() {
className="fixed inset-0 z-40"
onClick={() => setActiveParamPopover(null)}
/>
<div className="absolute left-4 top-12 z-50 w-[280px] animate-in fade-in rounded-xl border border-neutral-100 bg-white p-3.5 text-[12px] shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="absolute left-4 top-12 z-50 w-[280px] animate-in fade-in rounded-[5px] border border-neutral-100 bg-white p-3.5 text-[12px] shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="mb-2.5 flex items-center justify-between border-b border-neutral-100 pb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-neutral-400">
注入的路由参数
@@ -392,7 +405,7 @@ export default function LightAppManagementPage() {
{app.routeParams.map((p, idx) => (
<div
key={`${app.appId}-${idx}`}
className="rounded-lg border border-neutral-100 bg-neutral-50/60 p-2 text-[11px] leading-relaxed"
className="rounded-[5px] border border-neutral-100 bg-neutral-50/60 p-2 text-[11px] leading-relaxed"
>
<div className="flex items-center justify-between gap-1 font-mono">
<span className="truncate font-bold text-rose-700">{p.key}</span>
@@ -432,14 +445,14 @@ export default function LightAppManagementPage() {
<div className="flex items-center justify-center space-x-1">
<button
onClick={() => openEdit(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
title="编辑配置"
>
<Pencil className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
title="删除配置"
>
<Trash2 className="h-4 w-4" />
@@ -474,7 +487,7 @@ export default function LightAppManagementPage() {
</table>
</div>
) : tableApps.length === 0 ? (
<div className="rounded-xl border border-dashed border-neutral-200 bg-white py-20 text-center">
<div className="rounded-[5px] bg-white py-20 text-center">
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-neutral-100 text-neutral-300">
<Info className="h-6 w-6" />
</div>
@@ -492,7 +505,7 @@ export default function LightAppManagementPage() {
{tableApps.map((app) => (
<div
key={app.appId}
className="group flex flex-col justify-between overflow-hidden rounded-xl border border-neutral-100 bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)] transition-all hover:border-[#F5222D]/20 hover:shadow-md"
className="group flex flex-col justify-between overflow-hidden rounded-[5px] bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)] transition-all hover:shadow-md"
>
<div
className={`h-1.5 w-full bg-gradient-to-r ${
@@ -505,12 +518,12 @@ export default function LightAppManagementPage() {
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1.5">
{app.appType === "window_open" ? (
<span className="inline-flex items-center space-x-1 rounded border border-rose-100 bg-rose-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-rose-700">
<span className="inline-flex items-center space-x-1 rounded-[5px] border border-rose-100 bg-rose-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-rose-700">
<ExternalLink className="h-3 w-3" />
<span>新页跳转</span>
</span>
) : (
<span className="inline-flex items-center space-x-1 rounded border border-sky-100 bg-sky-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-sky-700">
<span className="inline-flex items-center space-x-1 rounded-[5px] border border-sky-100 bg-sky-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-sky-700">
<Monitor className="h-3 w-3" />
<span>iframe 嵌套</span>
</span>
@@ -536,14 +549,14 @@ export default function LightAppManagementPage() {
{app.description || "暂无应用详细描述说明。"}
</p>
<div className="flex items-center space-x-1.5 rounded-lg border border-neutral-200/40 bg-neutral-50/60 p-2 font-mono text-[11px] text-neutral-600">
<div className="flex items-center space-x-1.5 rounded-[5px] bg-neutral-50/60 p-2 font-mono text-[11px] text-neutral-600">
<Globe className="h-3.5 w-3.5 shrink-0 text-neutral-400" />
<span className="flex-1 truncate" title={app.url}>
{app.url}
</span>
<button
onClick={() => handleCopyUrl(app.url)}
className="shrink-0 rounded p-1 text-neutral-400 transition-colors hover:bg-neutral-200 hover:text-neutral-600"
className="shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-200 hover:text-neutral-600"
title="复制网址"
>
<Copy className="h-3.5 w-3.5" />
@@ -555,7 +568,7 @@ export default function LightAppManagementPage() {
{app.routeParams.map((p, idx) => (
<span
key={`${app.appId}-card-${idx}`}
className="rounded border border-neutral-200/40 bg-neutral-100 px-1.5 py-0.5 font-mono text-[10px] text-neutral-500"
className="rounded-[5px] border border-neutral-200/40 bg-neutral-100 px-1.5 py-0.5 font-mono text-[10px] text-neutral-500"
>
{p.key}
</span>
@@ -564,7 +577,7 @@ export default function LightAppManagementPage() {
)}
{app.appType === "iframe" && app.mountLocation && (
<div className="inline-flex items-center gap-1.5 rounded-lg border border-sky-100/60 bg-sky-50/60 p-2 text-[11px] font-medium leading-none text-sky-700">
<div className="inline-flex items-center gap-1.5 rounded-[5px] bg-sky-50/60 p-2 text-[11px] font-medium leading-none text-sky-700">
<span>挂载分类:</span>
<code className="font-semibold">{mountLabel(app.mountLocation)}</code>
</div>
@@ -574,14 +587,14 @@ export default function LightAppManagementPage() {
<div className="flex items-center justify-end space-x-1 border-t border-neutral-100/60 bg-neutral-50/30 px-5 py-3">
<button
onClick={() => openEdit(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
title="编辑配置"
>
<Pencil className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(app)}
className="cursor-pointer rounded-lg p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
title="删除配置"
>
<Trash2 className="h-4 w-4" />
@@ -594,7 +607,7 @@ export default function LightAppManagementPage() {
</div>
{viewMode === "table" && (
<div className="select-none rounded-xl border border-amber-200/40 bg-amber-50/40 p-5 shadow-[0_1px_2px_rgba(0,0,0,0.005)]">
<div className="select-none rounded-[5px] bg-amber-50/40 p-5 shadow-[0_1px_2px_rgba(0,0,0,0.005)]">
<div className="flex items-center space-x-2 border-b border-amber-100/50 pb-3 text-amber-800">
<AlertCircle className="h-4 w-4 shrink-0 text-amber-600" />
<h4 className="text-[13px] font-bold tracking-tight">智能多维挂载规则:</h4>
@@ -627,10 +640,10 @@ export default function LightAppManagementPage() {
onClick={() => setDialogOpen(false)}
/>
<div className="relative z-[110] m-4 flex max-h-[90vh] w-full max-w-[580px] flex-col rounded-[4px] border border-[#E8E8E8] bg-white p-6 shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="relative z-[110] m-4 flex max-h-[90vh] w-full max-w-[580px] flex-col rounded-[5px] border border-[#E8E8E8] bg-white p-6 shadow-[0_8px_32px_rgba(34,34,34,0.16)]">
<div className="flex flex-shrink-0 items-center justify-between border-b border-[#E8E8E8] pb-4">
<div className="flex items-center space-x-2.5">
<span className="rounded-lg bg-rose-50 p-1.5 text-[#A8071A]">
<span className="rounded-[5px] bg-rose-50 p-1.5 text-[#A8071A]">
<Layers className="h-5 w-5" />
</span>
<h2 className="text-[17px] font-bold text-[#222222]">
@@ -639,7 +652,7 @@ export default function LightAppManagementPage() {
</div>
<button
onClick={() => setDialogOpen(false)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
>
<X className="h-5 w-5" />
</button>
@@ -652,14 +665,13 @@ export default function LightAppManagementPage() {
<Layers className="h-4 w-4 text-[#A6A6A6]" />
<span>应用类型</span>
</label>
<select
<Select
className="w-full"
value={form.appType}
onChange={(e) => patchForm({ appType: e.target.value as LightAppType })}
className="h-9 w-full cursor-pointer rounded-[2px] border border-[#DBDBDB] bg-white px-3 text-[14px] focus:border-[#F5222D] focus:outline-none focus:ring-1 focus:ring-[#F5222D]"
>
<option value="iframe">iframe 嵌套 (挂载到菜单)</option>
<option value="window_open">window.open (新页跳转)</option>
</select>
options={APP_TYPE_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) => patchForm({ appType: v as LightAppType })}
/>
<p className="text-[11.5px] leading-relaxed text-[#A6A6A6]">
{form.appType === "window_open"
? "在新标签页或卡片式路由容器打开。多用于外部高度复杂的系统跳转。"
@@ -677,7 +689,7 @@ export default function LightAppManagementPage() {
value={form.name}
onChange={(e) => patchForm({ name: e.target.value })}
placeholder="如: 舆情看板"
className="h-9 rounded-[2px] border-[#DBDBDB] text-[14px]"
className="h-9 rounded-[5px] border-[#DBDBDB] text-[14px]"
/>
</div>
@@ -691,7 +703,7 @@ export default function LightAppManagementPage() {
value={form.url}
onChange={(e) => patchForm({ url: e.target.value })}
placeholder="https://example.com/page 或 http://47.88.25.99:3000/"
className="h-9 rounded-[2px] border-[#DBDBDB] font-mono text-[14px]"
className="h-9 rounded-[5px] border-[#DBDBDB] font-mono text-[14px]"
/>
</div>
@@ -702,18 +714,14 @@ export default function LightAppManagementPage() {
<FolderTree className="h-4 w-4 text-[#A6A6A6]" />
<span>挂载菜单</span>
</label>
<select
value={form.mountLocation}
onChange={(e) => patchForm({ mountLocation: e.target.value })}
className="h-9 w-full cursor-pointer rounded-[2px] border border-[#DBDBDB] bg-white px-3 text-[14px] focus:border-[#F5222D] focus:outline-none focus:ring-1 focus:ring-[#F5222D]"
>
<option value="">请选择挂载菜单</option>
{MOUNT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<Select
className="w-full"
value={form.mountLocation || undefined}
placeholder="请选择挂载菜单"
options={MOUNT_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) => patchForm({ mountLocation: (v as string) ?? "" })}
/>
</div>
)}
@@ -728,7 +736,7 @@ export default function LightAppManagementPage() {
onChange={(e) => patchForm({ description: e.target.value })}
placeholder="简要说明该应用的用途"
rows={2}
className="resize-none rounded-[2px] border-[#DBDBDB] text-[14px]"
className="resize-none rounded-[5px] border-[#DBDBDB] text-[14px]"
/>
</div>
@@ -747,7 +755,7 @@ export default function LightAppManagementPage() {
<button
type="button"
onClick={addParam}
className="flex h-7 cursor-pointer items-center space-x-1 rounded-[2px] border border-[#DBDBDB] bg-white px-2.5 text-[12px] font-bold text-[#575757] transition-colors hover:border-[#F5222D] hover:text-[#F5222D]"
className="flex h-7 cursor-pointer items-center space-x-1 rounded-[5px] border border-[#DBDBDB] bg-white px-2.5 text-[12px] font-bold text-[#575757] transition-colors hover:border-[#F5222D] hover:text-[#F5222D]"
>
<Plus className="h-3.5 w-3.5" />
<span>添加路由参数</span>
@@ -755,14 +763,14 @@ export default function LightAppManagementPage() {
</div>
{form.routeParams.length > 0 ? (
<div className="max-h-[220px] space-y-2 overflow-y-auto rounded-sm border border-[#E8E8E8] bg-[#FAFAFA] p-1">
<div className="max-h-[220px] space-y-2 overflow-y-auto rounded-[5px] border border-[#E8E8E8] bg-[#FAFAFA] p-1">
{form.routeParams.map((p, idx) => {
const themeMap =
p.paramType === "theme" ? parseThemeMap(p.themeValue) : {};
return (
<div
key={idx}
className="relative rounded-sm border border-[#E8E8E8] bg-white p-2"
className="relative rounded-[5px] border border-[#E8E8E8] bg-white p-2"
>
<div className="flex items-center space-x-2">
<div className="flex-1">
@@ -771,40 +779,36 @@ export default function LightAppManagementPage() {
value={p.key}
onChange={(e) => patchParam(idx, { key: e.target.value })}
placeholder="参数名 key"
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
className="h-8 w-full rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
</div>
<div className="w-[110px]">
<select
<Select
className="w-full"
size="small"
value={p.paramType}
onChange={(e) =>
options={PARAM_TYPE_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) =>
patchParam(idx, {
paramType: e.target.value as "login" | "theme",
source:
e.target.value === "login" ? (p.source || "accessToken") : undefined,
paramType: v as "login" | "theme",
source: v === "login" ? (p.source || "accessToken") : undefined,
})
}
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-[#FAFAFA] px-1 text-[12px] focus:border-[#F5222D] focus:outline-none"
>
<option value="login">登录参数</option>
<option value="theme">主题参数</option>
</select>
/>
</div>
{p.paramType === "login" && (
<div className="flex flex-[1.2] items-center gap-2 text-[11px]">
<select
<Select
className="w-full"
size="small"
value={p.source || "accessToken"}
onChange={(e) => patchParam(idx, { source: e.target.value })}
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-1 text-[12px] focus:border-[#F5222D] focus:outline-none"
>
{LOGIN_SOURCE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
options={LOGIN_SOURCE_OPTIONS}
popupProps={DIALOG_SELECT_POPUP}
onChange={(v) => patchParam(idx, { source: v as string })}
/>
{p.source === "other" && (
<input
type="text"
@@ -813,7 +817,7 @@ export default function LightAppManagementPage() {
patchParam(idx, { customValue: e.target.value })
}
placeholder="自定义值"
className="h-8 w-[120px] rounded-[2px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none"
className="h-8 w-[120px] rounded-[5px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none"
/>
)}
</div>
@@ -822,7 +826,7 @@ export default function LightAppManagementPage() {
<button
type="button"
onClick={() => removeParam(idx)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
>
<X className="h-4 w-4" />
</button>
@@ -830,7 +834,7 @@ export default function LightAppManagementPage() {
{/* 主题参数:逐主题色配置传给目标应用的值(左=系统主题色,右=目标应用值,可改) */}
{p.paramType === "theme" && (
<div className="mt-2 space-y-1.5 rounded-sm bg-[#FAFAFA] p-2">
<div className="mt-2 space-y-1.5 rounded-[5px] bg-[#FAFAFA] p-2">
<p className="text-[11px] leading-relaxed text-[#A6A6A6]">
各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改)
</p>
@@ -858,7 +862,7 @@ export default function LightAppManagementPage() {
})
}
placeholder={c.key}
className="h-7 flex-1 rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
className="h-7 flex-1 rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
</div>
))}
@@ -869,7 +873,7 @@ export default function LightAppManagementPage() {
})}
</div>
) : (
<div className="rounded-sm border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
<div className="rounded-[5px] border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
暂无注入的路由参数,加载地址时不传递多余拼接。
</div>
)}
@@ -898,7 +902,7 @@ export default function LightAppManagementPage() {
variant="outline"
disabled={saving}
onClick={() => setDialogOpen(false)}
className="h-9 rounded-[2px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
className="h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
>
取消
</Button>
@@ -906,7 +910,7 @@ export default function LightAppManagementPage() {
type="button"
disabled={saving}
onClick={handleSave}
className="h-9 cursor-pointer rounded-[2px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70"
className="h-9 cursor-pointer rounded-[5px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70"
>
{saving ? "保存中..." : editing ? "保存修改" : "立即注册"}
</button>
+5 -1
View File
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useTheme } from '@/contexts/ThemeContext';
import { loginByToken, loginByUsername } from '@/core/auth/api';
import { setStoredAuth } from '@/core/auth';
import { setStoredAuth, setStoredUserName } from '@/core/auth';
import {
persistExternalLoginParams,
readExternalLoginParams,
@@ -114,6 +114,7 @@ export default function LoginPage() {
setErrorMessage('');
try {
let auth;
let loginUserName = '';
if (authToken) {
// token 换登录:后端用 authToken 调上游接口换出用户名再登录
auth = await loginByToken(authToken);
@@ -128,11 +129,14 @@ export default function LoginPage() {
}
auth = await loginByUsername(deerflowUsername, gatePassword);
loginUserName = deerflowUsername;
}
if (cancelled) {
return;
}
setStoredAuth(auth);
// 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖)
setStoredUserName(loginUserName);
persistSidebarEnabled(sidebarEnabled);
setStatus('success');
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
+9 -1
View File
@@ -7,6 +7,7 @@ import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-rout
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { useTheme } from "@/contexts/ThemeContext";
import { useLocation } from "react-router-dom";
import { useLocalSettings } from "@/core/settings";
import WorkspaceRoutes from "./WorkspaceRoutes";
import StrategyRoutes from "./StrategyRoutes";
@@ -21,8 +22,15 @@ function PageLayout() {
const location = useLocation();
const { theme } = useTheme();
const isDark = theme === "dark";
const [settings] = useLocalSettings();
// 界面模式开关(外观设置):用户显式选「全量/简洁」时以其为准;未设置过则回退到
// 会话级 sidebarEnabled(外部门户/yUserId 登录时置位)这一旧默认。这样无论是否走门户
// 登录,「全量↔简洁」切换都能真正控制顶栏+侧边栏的显示。
const layoutMode = settings.display.layoutMode;
const chromeByPreference =
layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled;
const pageSidebarAllowed =
sidebarEnabled && !isChatsIframeWorkspacePath(location.pathname);
chromeByPreference && !isChatsIframeWorkspacePath(location.pathname);
// h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100%
// 高度链,根布局跟随容器高度。全屏下 100% 解析到视口,与 h-screen 等价;无界等嵌套
@@ -108,6 +108,7 @@ export function WorkspaceLayout({
'--header-height': headerHidden ? '0rem' : '4rem',
} as CSSProperties
}
key={String(compactWorkspaceSidebar)}
data-page-sidebar-collapsed={pageSidebarCollapsed}
// 紧凑模式下历史侧栏默认收起(聊天区更宽),由聊天页右上角图标钮展开
defaultOpen={!compactWorkspaceSidebar}
@@ -6,8 +6,8 @@
* - footer「配置」按钮 → 进入配置页(空白新建,等价于旧按钮的直接跳转)。
*
* 纯导航 / 浏览用途,不参与研讨席位选择(席位选择走 `BusinessChainPicker`)。
* 卡片席位名直接用落库快照(`seat.name`),无需实时校验,点「设置」进编辑页后
* 会看到实时数据。
* 卡片席位名优先用实时 agent 数据(`listAgents()`,与 `BusinessChainPicker` 一致),
* agent 改名后此弹窗也能同步显示;解析不到才回退到落库快照(`seat.name`)。
*/
import { Fragment, useCallback, useEffect, useState } from "react";
import {
@@ -30,6 +30,8 @@ import {
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { listAgents } from "@/core/agents";
import { listChains, setChainPublic, type ChainScope } from "../api/chains";
import { chainTagClasses, type BusinessChain } from "../lib/business-chain";
import { getAvatarTypeFor } from "../lib/roundtable-constants";
@@ -71,6 +73,27 @@ export function BusinessChainListBody({
// 筛选:我的 / 公共(已发布)。
const [scope, setScope] = useState<ChainScope>("mine");
const [publishingId, setPublishingId] = useState<string | null>(null);
// 实时名称表(id 小写 → 最新 name):用于把席位的落库快照名刷新成最新(agent 改名同步)。
const [nameById, setNameById] = useState<Map<string, string>>(new Map());
useEffect(() => {
if (!active) return;
let cancelled = false;
void (async () => {
try {
const agents = await listAgents();
if (cancelled) return;
const m = new Map<string, string>();
for (const a of agents) if (a?.id) m.set(a.id.toLowerCase(), a.name ?? a.id);
setNameById(m);
} catch {
// 静默失败:解析不到就回退到席位快照名。
}
})();
return () => {
cancelled = true;
};
}, [active]);
const refresh = useCallback(async (s: ChainScope) => {
setLoading(true);
@@ -171,6 +194,7 @@ export function BusinessChainListBody({
<ChainRow
key={chain.id}
chain={chain}
nameById={nameById}
publishing={publishingId === chain.id}
onSettings={() => onConfigure(chain.id)}
onTogglePublic={() => handleTogglePublic(chain)}
@@ -218,11 +242,13 @@ export function BusinessChainListDialog({
function ChainRow({
chain,
nameById,
publishing,
onSettings,
onTogglePublic,
}: {
chain: BusinessChain;
nameById: Map<string, string>;
publishing: boolean;
onSettings: () => void;
onTogglePublic: () => void;
@@ -292,14 +318,20 @@ function ChainRow({
)}
{/* 席位编排:按 stage 分组——并行阶段竖向并列在虚线框里,阶段之间用 → 串联。 */}
<ChainSeatsFlow chain={chain} />
<ChainSeatsFlow chain={chain} nameById={nameById} />
</div>
);
}
// ── 链条席位编排流(体现「同层并行 · 层间串行」)─────────────────────────────
function ChainSeatsFlow({ chain }: { chain: BusinessChain }) {
function ChainSeatsFlow({
chain,
nameById,
}: {
chain: BusinessChain;
nameById: Map<string, string>;
}) {
// 名称回退表:用落库快照(seats)解析 stage 里的 agent_id。
const seatById = new Map(chain.seats.map((s) => [s.agent_id, s] as const));
@@ -322,6 +354,8 @@ function ChainSeatsFlow({ chain }: { chain: BusinessChain }) {
const seatTag = (agentId: string) => {
const seat = seatById.get(agentId);
// 优先实时名称(agent 改名同步)→ 落库快照 → agent_id。
const label = nameById.get(agentId.toLowerCase()) ?? seat?.name ?? agentId;
return (
<span
key={agentId}
@@ -330,7 +364,7 @@ function ChainSeatsFlow({ chain }: { chain: BusinessChain }) {
chainTagClasses(getAvatarTypeFor(agentId, orderById.get(agentId) ?? 0)),
)}
>
{seat?.name || agentId}
{label}
</span>
);
};
@@ -46,7 +46,13 @@ import { DASHBOARD_VIEW_STORAGE_PREFIX } from "@/roundtable-planning/pages/Dashb
import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll";
import { useDialogueSpeedMetrics } from "@/roundtable-planning/hooks/useDialogueSpeedMetrics";
import type { UseStep3DashboardReturn } from "@/roundtable-planning/hooks/useStep3Dashboard";
import { parseReportJson, stripReportFence } from "@/roundtable-planning/lib/report-template";
import {
parseReportJson,
renderDashboardHtml,
stripReportFence,
type DashboardTheme,
} from "@/roundtable-planning/lib/report-template";
import { useTheme } from "@/contexts/ThemeContext";
interface Step3DashboardModeProps {
selectedModel: string;
@@ -94,13 +100,32 @@ export function Step3DashboardMode({
openDashboardPreview,
} = dashboard;
// 真实数据 → 大屏数据:优先本轮实时生成,其次草稿快照;都没有则 undefined(用 DEMO)。
const dashboardData = useMemo(() => {
const report = dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null);
return report ? adaptReportData(report, formatNow()) : undefined;
}, [dashboard.data, savedReport?.dashboardJson]);
// 真实数据(report-json):优先本轮实时生成,其次草稿快照。应用渲染/网页渲染共用。
const report = useMemo(
() => dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null),
[dashboard.data, savedReport?.dashboardJson],
);
const hasResult = !!dashboardData || !!html;
// 应用渲染用的大屏数据(不传则 DashboardApp 回落 DEMO)。
const dashboardData = useMemo(
() => (report ? adaptReportData(report, formatNow()) : undefined),
[report],
);
// 网页渲染主题跟随应用主题:renderDashboardHtml 是确定性的,主题切换时按 report
// 当场重渲(而非用 hook 在生成时烤死主题的 `html`),从而 iframe 大屏色随父元素变化。
// 仅 light/dark 两套(lightblue 归 light,与页面 themeMode 映射一致)。
const { theme } = useTheme();
const dashThemeMode: DashboardTheme = theme === "dark" ? "dark" : "light";
const dashboardHtml = useMemo(
() =>
report
? renderDashboardHtml(report, { generatedAt: formatNow(), theme: dashThemeMode })
: html,
[report, dashThemeMode, html],
);
const hasResult = !!dashboardData || !!dashboardHtml;
// 视图:有产物默认看「大屏页面」,否则进「交互区」看抽取过程。
const [view, setView] = useState<DashView>(() => (hasResult ? "page" : "chat"));
@@ -110,6 +135,27 @@ export function Step3DashboardMode({
// - "html" 网页渲染:直接把自包含 HTML 大屏塞进 iframe,与「新窗口打开 / 分享 / 下载」一致。
const [renderMode, setRenderMode] = useState<RenderMode>("app");
// 网页渲染:用 blob URL 作 iframe `src`(而非 `srcDoc`)。
// 原因:`srcdoc` 文档的 base URL 继承自父页面,大屏内的纯 CSS `:target` 导航锚点
// (#overview / #seat-x)会被解析成「父页面URL#seat-x」,导致点击导航时整个 iframe
// 跳去加载主应用并命中 HashRouter 的 404(Page not found)。blob: 文档有自己的 URL,
// 片段锚点在 iframe 内部解析,:target 切页正常 —— 与「新窗口打开」一致。
//
// 必须在 **effect 内部**创建/回收 blob:StrictMode(dev) 会 setup→cleanup→setup 双触发
// effect,若用 useMemo 跨两次 render 共享同一个 URL,cleanup 会把它 revoke 掉而 iframe
// 仍指向它 → 报「该文件可能已被移至别处」。effect 内每次 run 各自建 URL、cleanup 只回收
// 本 run 的,最后提交的那个存活,故 StrictMode-safe。dashboardHtml 随主题变 → blob 重建。
const [htmlBlobUrl, setHtmlBlobUrl] = useState<string | null>(null);
useEffect(() => {
if (typeof window === "undefined" || !dashboardHtml) {
setHtmlBlobUrl(null);
return;
}
const url = URL.createObjectURL(new Blob([dashboardHtml], { type: "text/html;charset=utf-8" }));
setHtmlBlobUrl(url);
return () => URL.revokeObjectURL(url);
}, [dashboardHtml]);
// 绘制完成**不再**自动切到大屏页面:用户正在交互区看抽取过程/继续问答时,强行跳走很打断。
// 想看成品由用户手动点顶栏「查看大屏」。没有产物时初始即落交互区(见上 useState 初值)。
@@ -126,8 +172,8 @@ export function Step3DashboardMode({
const handleOpenNewWindow = () => {
if (typeof window === "undefined") return;
if (renderMode === "html" && html) {
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
if (renderMode === "html" && dashboardHtml) {
const blob = new Blob([dashboardHtml], { type: "text/html;charset=utf-8" });
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank", "noopener,noreferrer");
// 延迟回收,给新标签页足够时间加载(过早 revoke 会打不开)。
@@ -195,9 +241,9 @@ export function Step3DashboardMode({
<button
type="button"
onClick={() => setRenderMode("html")}
disabled={!html}
disabled={!dashboardHtml}
title={
html
dashboardHtml
? "用自包含 HTML 大屏渲染(与新窗口打开 / 分享 / 下载一致)"
: "尚无已生成的 HTML 大屏,先完成一次绘制"
}
@@ -269,10 +315,10 @@ export function Step3DashboardMode({
{/* 主体:交互区(聊天 + 抽取沙箱) / 大屏页面(React) */}
{view === "page" ? (
renderMode === "html" && html ? (
renderMode === "html" && htmlBlobUrl ? (
<iframe
title="方案研讨大屏"
srcDoc={html}
src={htmlBlobUrl}
className="flex-1 min-h-0 w-full border-0 bg-white"
sandbox="allow-scripts allow-popups allow-downloads allow-same-origin"
/>
@@ -25,6 +25,19 @@
--dash-accent: #13c2c2; /* 辅色 zs-003-06 */
}
/* 深色主题下「大屏页面」整体背景换成设计稿深蓝主题底图(应用渲染模式)。
* 未分层规则优先级高于 @layer utilities,稳定覆盖 DashboardApp 根 className 上的
* `dark:bg-[var(--dash-page)]` 底色与 `dark:[background-image:radial-gradient(...)]`
* 深蓝点阵 —— 浅色模式不受影响(仅 .dark 生效)。底色保留作图片加载前/超出区域回落。 */
.dark .dash-root {
background-color: var(--dash-page);
background-image: url("../../assets/img/layout-bg-theme-dark-blue.svg");
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
background-attachment: fixed;
}
/* 深色下网格 overlay 改为极淡深蓝描线(浅色仍走上方 .bg-grid-slate-100)。 */
.dark .dash-root .bg-grid-slate-100 {
background-image:
@@ -33,6 +33,8 @@
* 文件头嵌入 `RT_DASHBOARD_MARKER` 注释,恢复草稿时据此识别「这是模板大屏」。
*/
import darkBgSvgRaw from "@/assets/img/layout-bg-theme-dark-blue.svg?raw";
// ── 契约类型 ─────────────────────────────────────────────────────────────────
export interface ReportKpi {
@@ -414,6 +416,12 @@ interface Palette {
link: string;
titleColor: string;
navBg: string;
/** 深色主题大屏背景图(自包含 HTML 内联 data URI);浅色为空串走纯色底。 */
bgImage: string;
/** 导航选中态 tab 背景(浅=白,深=深蓝卡片色)。 */
navActiveBg: string;
/** 导航 tab 悬停背景。 */
tabHoverBg: string;
trackBg: string;
/** 图表 / KPI / 席位强调色序(规范图表色序)。 */
accents: string[];
@@ -421,6 +429,16 @@ interface Palette {
modalMask: string;
}
/**
* 深色主题大屏背景图:从 SVG(内嵌 JPEG)里取出其 data URI,内联进自包含 HTML
* 当背景图用 —— 避免 iframe srcDoc / blob 新窗口 / 下载分享场景下相对路径取不到图
* (保持离线自包含;草稿存的是 report-json 不是 HTML,不会被这张图撑大)。
*/
const DARK_BG_DATA_URI: string = (() => {
const m = /href="(data:image\/[^"]+)"/.exec(darkBgSvgRaw);
return m ? m[1] : "";
})();
const PALETTES: Record<DashboardTheme, Palette> = {
dark: {
bgPage: "#0C1836",
@@ -438,7 +456,10 @@ const PALETTES: Record<DashboardTheme, Palette> = {
primary: "#1677FF",
link: "#4096FF",
titleColor: "linear-gradient(90deg,#91CAFF,#4096FF 45%,#13C2C2)",
navBg: "rgba(6,14,36,.88)",
navBg: "rgba(10,20,46,.78)",
bgImage: DARK_BG_DATA_URI,
navActiveBg: "#122347",
tabHoverBg: "rgba(64,150,255,.12)",
trackBg: "rgba(110,136,176,.22)",
accents: ["#4096FF", "#13C2C2", "#52C41A", "#FAAD14", "#FF7875", "#69B1FF"],
risk: { high: "#F5222D", mid: "#FAAD14", low: "#52C41A" },
@@ -462,7 +483,10 @@ const PALETTES: Record<DashboardTheme, Palette> = {
primary: "#E11D48",
link: "#E11D48",
titleColor: "linear-gradient(90deg,#1E293B,#334155)",
navBg: "rgba(255,255,255,.95)",
navBg: "#F1F5F9",
bgImage: "",
navActiveBg: "#FFFFFF",
tabHoverBg: "rgba(255,255,255,.6)",
trackBg: "#F1F5F9",
accents: ["#E11D48", "#4F46E5", "#059669", "#D97706", "#0284C7", "#7C3AED"],
risk: { high: "#DC2626", mid: "#D97706", low: "#059669" },
@@ -684,7 +708,7 @@ export function renderDashboardHtml(
const ovBase = `body:not(:has(.page:target)) ${sel("overview")},body:has(#overview:target) ${sel("overview")}`;
const seatBase = seats.map((s) => `body:has(#${esc(s.id)}:target) ${sel(s.id)}`).join(",");
const allActive = seatBase ? `${ovBase},${seatBase}` : ovBase;
const navActiveCss = `${allActive}{background:#fff;color:var(--primary);box-shadow:0 1px 5px rgba(15,23,42,.1);font-weight:700;border-color:${P.primary}33}`;
const navActiveCss = `${allActive}{background:var(--navactive);color:var(--primary);box-shadow:0 1px 5px rgba(15,23,42,.1);font-weight:700;border-color:${P.primary}33}`;
const KPI_ICONS = ["📊", "📈", "⚠️", "🧩", "🏆", "📅"];
const kpis = data.kpis
@@ -793,10 +817,11 @@ export function renderDashboardHtml(
<style>
/* 蓝图工程图风模板(light=参考大屏设计图 / dark=深蓝 token,渲染时注入 palette) */
:root{--line:${P.cardBorder};--txt:${P.text};--dim:${P.textSecondary};--strong:${P.textStrong};
--primary:${P.primary};--link:${P.link};--heading:${P.heading};--track:${P.trackBg};--bracket:${P.bracket}}
--primary:${P.primary};--link:${P.link};--heading:${P.heading};--track:${P.trackBg};--bracket:${P.bracket};
--navbg:${P.navBg};--navactive:${P.navActiveBg};--tabhover:${P.tabHoverBg}}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Inter","Noto Sans SC","Microsoft YaHei",system-ui,sans-serif;color:var(--txt);min-height:100vh;
font-size:13px;background:${P.bgPage}}
font-size:13px;background:${P.bgImage ? `url("${P.bgImage}") center top/cover no-repeat fixed,` : ""}${P.bgPage}}
/* 蓝图圆点网格底纹(对齐设计原图的 radial dots) */
body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
background-image:radial-gradient(${P.gridLine} 1.3px,transparent 1.3px);background-size:20px 20px}
@@ -812,10 +837,10 @@ main{position:relative;z-index:1;padding:20px 26px 48px;max-width:1500px;margin:
background:#fff;border:1px solid var(--line);border-radius:6px;padding:6px 12px;white-space:nowrap}
/* Nav:浅灰圆角胶囊容器 + 图标文字 tab(对齐设计原图) */
nav{position:relative;z-index:1;margin-bottom:22px}
.navwrap{display:inline-flex;flex-wrap:wrap;gap:6px;background:var(--track);border:1px solid var(--line);border-radius:10px;padding:5px}
.navwrap{display:inline-flex;flex-wrap:wrap;gap:6px;background:var(--navbg);border:1px solid var(--line);border-radius:10px;padding:5px;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.tab{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:6px 12px;
border:1px solid transparent;border-radius:7px;color:var(--dim);text-decoration:none;transition:.15s;background:transparent}
.tab:hover{color:var(--strong);background:rgba(255,255,255,.6)}
.tab:hover{color:var(--strong);background:var(--tabhover)}
.tab svg{width:14px;height:14px;flex:none}
.page{display:none;animation:fi .25s ease}
#overview{display:block}
@@ -140,6 +140,11 @@ export default function BusinessChainEditorPage() {
// ─── 数据 ────────────────────────────────────────────────────────────
const [pool, setPool] = useState<RecommendCandidate[]>([]);
const [poolLoading, setPoolLoading] = useState(true);
// 名称解析专用:一次性拉「全量」智能体(不受左栏搜索/标签过滤影响),用于把链条里
// 落库的名称快照刷新成最新名称——agent 改名后此页能同步显示。解析不到才回退到快照。
const [agentLiveById, setAgentLiveById] = useState<
Map<string, { name: string; description?: string }>
>(new Map());
const [chains, setChains] = useState<BusinessChain[]>([]);
// 左栏检索/筛选 —— 服务端过滤(listAgents 支持 search + tag_ids)。
const [search, setSearch] = useState("");
@@ -185,6 +190,22 @@ export default function BusinessChainEditorPage() {
);
const seatCount = useMemo(() => countSeats(stageGroups), [stageGroups]);
// 展示用 stageGroups:把每个席位的名称/描述刷新为最新(agent 改名同步),
// 解析不到再回退落库快照。仅用于显示,编辑操作仍作用于原始 stageGroups。
const displayStageGroups = useMemo<StageGroups>(
() =>
stageGroups.map((g) => ({
...g,
seats: g.seats.map((s) => {
const live = agentLiveById.get(s.agent_id.toLowerCase());
return live
? { ...s, name: live.name || s.name, description: live.description ?? s.description }
: s;
}),
})),
[stageGroups, agentLiveById],
);
// ─── Phase 7 只读流程图预览(§2.2 底部 / §7)──────────────────────────────
// 把当前分层(去掉空 stage)映射成 DagFlowChart 的 stages,并提供席位名/配色解析。
const previewStages = useMemo(
@@ -196,10 +217,10 @@ export default function BusinessChainEditorPage() {
);
const seatNameById = useMemo(() => {
const m = new Map<string, string>();
for (const g of stageGroups)
for (const g of displayStageGroups)
for (const s of g.seats) m.set(s.agent_id.toLowerCase(), s.name || s.agent_id);
return m;
}, [stageGroups]);
}, [displayStageGroups]);
const resolvePreviewSeat = useCallback(
(agentId: string, order: number) => ({
name: seatNameById.get(agentId.toLowerCase()) ?? agentId,
@@ -211,10 +232,10 @@ export default function BusinessChainEditorPage() {
// 步骤一右侧「精美流程图」的数据源:过滤空 stage,带上各阶段目标作为依赖标签。
const flowStages = useMemo(
() =>
stageGroups
displayStageGroups
.filter((g) => g.seats.length > 0)
.map((g) => ({ seats: g.seats, goal: g.goal ?? null })),
[stageGroups],
[displayStageGroups],
);
const hasFlowContent = flowStages.length > 0;
@@ -256,6 +277,28 @@ export default function BusinessChainEditorPage() {
};
}, [search, filterTagIds]);
// ─── 全量名称表(一次性,不受搜索过滤):用于刷新席位的快照名称 ──────────────
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const agents = filterRecommendCandidates(await listAgents());
if (cancelled) return;
const m = new Map<string, { name: string; description?: string }>();
// 大小写不敏感:席位落库的 agent_id 与 agent.id 大小写可能不一致(同页 seatNameById
// 也按 toLowerCase 匹配),统一小写键避免解析失败回退到旧快照名。
for (const a of agents)
m.set(a.agent_id.toLowerCase(), { name: a.name, description: a.description });
setAgentLiveById(m);
} catch {
// 静默失败:解析不到就回退到席位的名称快照。
}
})();
return () => {
cancelled = true;
};
}, []);
// ─── 链条选择 / 新建 ──────────────────────────────────────────────────
const loadChain = useCallback((chain: BusinessChain) => {
setCurrentChainId(chain.id);
@@ -523,11 +566,15 @@ export default function BusinessChainEditorPage() {
// 展平 + 折叠:seats 用候选池最新 name/description 覆盖快照;stages 在「全单席位且无
// 目标」时折叠为 null(线性链),有并行或填了「本阶段目标」时落显式分层。
const flat = stageGroupsToSeats(stageGroups);
const payloadSeats = flat.map((s) => ({
agent_id: s.agent_id,
name: poolById.get(s.agent_id)?.name ?? s.name,
description: poolById.get(s.agent_id)?.description ?? s.description ?? "",
}));
const payloadSeats = flat.map((s) => {
// 优先全量名称表(不受搜索过滤)→ 候选池 → 落库快照,保证改名后保存写入最新名。
const live = agentLiveById.get(s.agent_id.toLowerCase()) ?? poolById.get(s.agent_id);
return {
agent_id: s.agent_id,
name: live?.name ?? s.name,
description: live?.description ?? s.description ?? "",
};
});
// 总控编排提示:trim 后空串落 null(清空),与后端「空=不注入」一致。
const coordPrompt = coordinatorPrompt.trim() || null;
// 填了「总控编排提示」也强制落显式分层 —— 该提示仅 dag 模式注入,否则纯线性链里被静默忽略。
@@ -917,8 +964,8 @@ export default function BusinessChainEditorPage() {
{/* 步骤导航:① 基本信息 → ② 链条编排(点击可切换)。 */}
<nav className="flex shrink-0 items-center justify-center gap-3 border-b border-border bg-card px-6 py-2.5">
{([
{ n: 1 as const, label: "基本信息" },
{ n: 2 as const, label: "链条编排" },
{ n: 1 as const, label: "链条编排" },
{ n: 2 as const, label: "基本信息" },
]).map((s, i) => (
<Fragment key={s.n}>
{i > 0 && <span className="h-px w-10 bg-border" />}
@@ -947,8 +994,8 @@ export default function BusinessChainEditorPage() {
))}
</nav>
{/* 步骤一:左=基本信息,右=精美流程图预览(有编排内容时)。无内容则居中单列。 */}
{step === 1 && (
{/* 步骤二:左=基本信息,右=精美流程图预览(有编排内容时)。无内容则居中单列。 */}
{step === 2 && (
<div className="flex min-h-0 flex-1 overflow-hidden">
{hasFlowContent ? (
<>
@@ -969,15 +1016,15 @@ export default function BusinessChainEditorPage() {
<div className="mx-auto w-full max-w-3xl overflow-y-auto px-6 py-6">
{basicInfoForm}
<p className="mt-4 text-center text-xs text-muted-foreground">
还没有编排内容。点击上方「② 链条编排」去配置链条,保存后这里会显示流程图预览。
还没有编排内容。点击上方「① 链条编排」去配置链条,保存后这里会显示流程图预览。
</p>
</div>
)}
</div>
)}
{/* 双栏拖拽编辑器(步骤二)*/}
{step === 2 && (
{/* 双栏拖拽编辑器(步骤一)*/}
{step === 1 && (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
@@ -1046,9 +1093,9 @@ export default function BusinessChainEditorPage() {
</div>
</section>
{/* 右栏:分层 Stage */}
{/* 右栏:分层 Stage(用展示版,名称已按实时 agent 刷新) */}
<StagesColumn
stageGroups={stageGroups}
stageGroups={displayStageGroups}
seatCount={seatCount}
seatCountInvalid={seatCountInvalid}
hasEmptyStage={hasEmptyStage}
@@ -1083,7 +1130,7 @@ export default function BusinessChainEditorPage() {
{/* Phase 7:分层 DAG 只读流程图预览(§2.2 底部)。空编排时不渲染。
改为弹窗查看:底栏一个入口按钮,点击后在 Dialog 内展示完整流程图。 */}
{step === 2 && previewStages.length > 0 && (
{step === 1 && previewStages.length > 0 && (
<section className="shrink-0 flex items-center justify-between gap-2 border-t border-border bg-card px-6 py-2">
<span className="flex items-center gap-2 text-[11px] font-semibold text-muted-foreground">
<span>编排预览</span>
@@ -1133,8 +1180,8 @@ export default function BusinessChainEditorPage() {
}}
/>
{/* 页脚(步骤二):分层编排说明。步骤切换走顶部步骤导航。 */}
{step === 2 && (
{/* 页脚(步骤一):分层编排说明。步骤切换走顶部步骤导航。 */}
{step === 1 && (
<footer className="shrink-0 select-none border-t border-border bg-card px-6 py-3 text-center text-[11px] text-muted-foreground">
分层编排:同一 Stage 内的席位并行研讨,Stage 之间串行。全单席位 = 线性链。保存后自动同步至圆桌。
</footer>
@@ -1768,8 +1768,8 @@ function HistoryDropdown(props: HistoryDropdownProps) {
</button>
{isOpen && (
<div className="absolute right-0 top-full mt-2 w-[360px] max-h-[60vh] overflow-y-auto bg-white border border-slate-200 rounded-2xl shadow-xl z-50 dark:bg-secondary dark:border-border">
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between sticky top-0 bg-white z-10 dark:bg-secondary dark:border-border">
<div className="absolute right-0 top-full mt-2 w-[360px] max-h-[60vh] overflow-y-auto bg-white border border-slate-200 rounded-2xl shadow-xl z-50 dark:bg-[#0d1d3b] dark:border-border">
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between sticky top-0 bg-white z-10 dark:bg-[#0d1d3b] dark:border-border">
<span className="text-xs font-black text-slate-800 tracking-wide dark:text-foreground">历史草稿</span>
<span className="text-[10px] font-mono text-slate-400">{drafts.length} 条</span>
</div>
@@ -69,28 +69,28 @@
* 着色,所以这里统一覆盖即可整页换肤;浅色模式(无 .dark)保持红色主题不受影响。 */
.dark .roundtable-planning,
.roundtable-planning--dark {
/* —— 背景 / 容器(ZS-004 深蓝系) —— */
--background: #0c1836; /* zs-004-09 页面背景 */
--foreground: #c5cfe0; /* zs-004-02 正文字体 */
--card: #122347; /* zs-004-08 卡片背景 */
/* —— 背景 / 容器(ZS-004 深蓝系,半透明版 —— 让背景图透出)—— */
--background: rgba(6, 14, 36, 0.92); /* zs-004-09 页面底,半透明 */
--foreground: #c5cfe0; /* zs-004-02 正文字体 */
--card: rgba(3, 38, 80, 0.82); /* 卡片底:对齐 sidebar-contents 半透明深蓝 */
--card-foreground: #c5cfe0;
--popover: #122347; /* 下拉 / 浮层与卡片同底 */
--popover: rgba(3, 38, 80, 0.92); /* 下拉 / 浮层,略深一档 */
--popover-foreground: #c5cfe0;
--muted: #16294d; /* 次级容器(步骤条 / slate-50 表面) */
--muted-foreground: #6e88b0; /* zs-004-04 次级字体 */
--secondary: #1a2f5a; /* zs-004-07 次级按钮底 */
--muted: rgba(2, 24, 58, 0.75); /* 次级容器(步骤条 / slate-50 表面) */
--muted-foreground: #6e88b0; /* zs-004-04 次级字体 */
--secondary: rgba(26, 47, 90, 0.78); /* zs-004-07 次级按钮底,半透明 */
--secondary-foreground: #c5cfe0;
--accent: #1a2f5a; /* hover / 软高亮表面 */
--accent-foreground: #e8edf5; /* zs-004-01 */
--accent: rgba(26, 47, 90, 0.78); /* hover / 软高亮表面,半透明 */
--accent-foreground: #e8edf5; /* zs-004-01 */
/* —— 主色 / 链接(ZS-002 渐蓝系) —— */
--primary: #1677ff; /* zs-002-06 主操作 */
--primary-foreground: #ffffff;
--ring: rgba(22, 119, 255, 0.45);
/* —— 边框 / 控件描边(取较浅阶以保证深底上可见) —— */
--border: #2d4473; /* zs-004-06 容器边框 */
--input: #4a6491; /* zs-004-05 控件描边 */
/* —— 边框 / 控件描边:对齐 sidebar-contents 的蓝色发光边框 —— */
--border: rgba(43, 143, 254, 0.55); /* sidebar border-color 同款蓝边 */
--input: rgba(90, 169, 255, 0.35); /* 控件描边,略浅 */
/* —— 功能语义色 —— */
--destructive: #f5222d; /* zs-001-06 错误 */
@@ -110,7 +110,16 @@
--rt-primary-hover: var(--primary);
--rt-primary-soft: var(--accent);
color: var(--foreground);
/* 整页背景换深蓝主题底图(仅深色;浅色保持红白主题不受影响)。
* background-image 叠在 background-color 上 —— 底色作为图加载前/未覆盖区域的回落;
* cover 铺满、fixed 固定不随内容滚动(大屏背板观感,与应用渲染大屏一致)。
* url() 里的 SVG 由 Vite 当独立资源输出(非内联),浏览器缓存。 */
background-color: var(--background);
background-image: url("../../assets/img/layout-bg-theme-dark-blue.svg");
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
background-attachment: fixed;
}
.dark .roundtable-planning ::-webkit-scrollbar-thumb,
@@ -146,7 +155,7 @@
background-color: var(--background) !important;
}
/* ---- 表面:白 / 近白底 → 卡片底 ---- */
/* ---- 表面:白 / 近白底 → 卡片底(半透明,对齐 sidebar-contents 风格) ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(.bg-white, .bg-white\/70, .hover\:bg-white:hover, .bg-\[\#fcfcfd\], .bg-\[\#fffdfa\]) {
background-color: var(--card) !important;
+160 -117
View File
@@ -1,5 +1,5 @@
import { Bell, ChevronDown, FileText, Shirt, User } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
@@ -13,6 +13,7 @@ import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
import { cn } from "@/lib/utils";
import { useTheme, type ThemeType } from "../contexts/ThemeContext";
import { readUserInfo } from "./lib/light-app";
const iconButtonClass = (isDark: boolean) =>
cn(
@@ -22,6 +23,10 @@ const iconButtonClass = (isDark: boolean) =>
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
);
// 深色顶栏菜单左右分组(设计稿:左 问答中心/任务管理/应用中心,右 综合管理/资源管理)
const LEFT_MENU_IDS = ["qa-center", "task-management", "light-app-center"];
const RIGHT_MENU_IDS = ["comprehensive-management", "resource-management"];
// 换肤色板:红色=红白主题(light),蓝色=浅蓝主题(lightblue),深蓝色=深色主题(dark)
const THEME_SWATCHES: { id: ThemeType; label: string; color: string }[] = [
{ id: "light", label: "红色", color: "#cf1322" },
@@ -120,8 +125,19 @@ const Header = ({ collapsed }: HeaderProps) => {
const { data: unreadCount = 0 } = useUnreadCount();
const navigate = useNavigate();
const { pathname } = useLocation();
const isAdmin = getStoredAuth()?.system_role === "admin";
const auth = getStoredAuth();
const isAdmin = auth?.system_role === "admin";
const visibleMenuItems = useSidebarMenuItems(!!isAdmin);
// 登录时写入的真实用户名(外部登录存于 localStorage.userInfo.userName);
// 缺省回退到 stored auth 的邮箱前缀。
const displayName = useMemo(() => {
const info = readUserInfo();
const name = typeof info.userName === "string" ? info.userName.trim() : "";
if (name) return name;
const email = auth?.email?.trim();
return email ? email.split("@")[0] : "用户";
}, [auth?.email]);
const showTopNav = isDark && !collapsed && !isWujieEnv();
const hasActiveChild = (item: MenuItem): boolean => {
@@ -154,6 +170,9 @@ const Header = ({ collapsed }: HeaderProps) => {
const activeSecondItem = activeTopItem?.children?.find(hasActiveChild);
const thirdSubNavItems = activeSecondItem?.children ?? [];
const leftMenuItems = visibleMenuItems.filter((i) => LEFT_MENU_IDS.includes(i.id));
const rightMenuItems = visibleMenuItems.filter((i) => RIGHT_MENU_IDS.includes(i.id));
const handleNavItemClick = (item: MenuItem) => {
const path = item.path ?? findFirstRoutePath(item.children);
if (path) {
@@ -161,136 +180,160 @@ const Header = ({ collapsed }: HeaderProps) => {
}
};
const renderTopItem = (item: MenuItem) => {
const isSelected = item.path ? pathname === item.path : hasActiveChild(item);
return (
<button
key={item.id}
type="button"
className={`top-submenu-item ${isSelected ? "top-submenu-item--active" : ""}`}
onClick={() => handleNavItemClick(item)}
>
{item.label}
</button>
);
};
// 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用
const actionCluster = (
<>
<ThemeSwitcher isDark={isDark} />
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={() => setNotificationOpen(true)}
className={cn(iconButtonClass(isDark), "relative")}
title="通知"
>
<Bell className="size-4" />
{unreadCount > 0 && (
<span
className={cn(
"absolute top-1 right-1 flex h-4 min-w-4 items-center justify-center rounded-sm px-1 text-[10px] leading-none font-medium",
isDark
? "bg-cyan-500 text-white"
: "bg-white tabular-nums text-[var(--brand-900)]",
)}
>
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</Button>
<div
className={cn(
"mx-1 h-8 w-px",
isDark ? "bg-blue-800/30" : "bg-white/30",
)}
aria-hidden
/>
<Button
type="button"
variant="ghost"
className={cn(
"h-8 gap-2 rounded-sm px-2",
isDark
? "text-slate-200 hover:bg-slate-800/50"
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
)}
>
<div
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-sm",
isDark
? "bg-gradient-to-br from-blue-500 to-cyan-400"
: "bg-white",
)}
>
<User
className={cn(
"size-4",
isDark ? "text-white" : "text-[var(--brand-900)]",
)}
/>
</div>
<div className="hidden text-left sm:block">
<p className="text-sm leading-tight font-medium text-white">
{displayName}
</p>
</div>
<ChevronDown
className={cn(
"size-4 shrink-0",
isDark ? "text-slate-500" : "text-white/80",
)}
/>
</Button>
</>
);
return (
<>
<NotificationDialog
open={notificationOpen}
onOpenChange={setNotificationOpen}
/>
<header
className={cn(
"strategy-app-header theme-transition flex h-16 items-center justify-between border-b px-4",
isDark ? "border-blue-800/30" : "border-[var(--brand-950)]",
)}
>
<div className="flex h-full min-w-0 flex-1 items-center gap-3">
<div
className={cn(
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
isDark
? "bg-gradient-to-br from-blue-500 to-cyan-400"
: "bg-white/20",
)}
>
<FileText className="h-4 w-4 text-white" />
</div>
{!collapsed && (
<span className="text-sm font-semibold text-white">
CM助手
</span>
)}
{showTopNav && visibleMenuItems.length ? (
<nav className="top-submenu-bar" aria-label="一级菜单">
<div className="top-submenu-list">
{visibleMenuItems.map((item) => {
const isSelected = item.path ? pathname === item.path : hasActiveChild(item);
<header className="strategy-app-header theme-transition relative flex h-16 items-center justify-between px-4">
{showTopNav ? (
<>
{/* 左分区(flex-1):菜单靠右贴近中间基座 */}
<div className="flex h-full min-w-0 flex-1 items-center justify-end">
<nav
className="top-submenu-bar top-submenu-bar--group"
aria-label="左侧菜单"
>
<div className="top-submenu-list">
{leftMenuItems.map(renderTopItem)}
</div>
</nav>
</div>
return (
<button
key={item.id}
type="button"
className={`top-submenu-item ${isSelected ? "top-submenu-item--active" : ""}`}
onClick={() => handleNavItemClick(item)}
>
{item.label}
</button>
);
})}
{/* 中:CM助手(落在背景图发光基座上,纯文字无图标) */}
<div className="header-center-title">CM助手</div>
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
<nav
className="top-submenu-bar top-submenu-bar--group"
aria-label="右侧菜单"
>
<div className="top-submenu-list">
{rightMenuItems.map(renderTopItem)}
</div>
</nav>
<div className="flex shrink-0 items-center gap-2">
{actionCluster}
</div>
</nav>
) : null}
</div>
<div className="flex shrink-0 items-center gap-2">
<ThemeSwitcher isDark={isDark} />
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={() => setNotificationOpen(true)}
className={cn(iconButtonClass(isDark), "relative")}
title="通知"
>
<Bell className="size-4" />
{unreadCount > 0 && (
<span
</div>
</>
) : (
<>
<div className="flex h-full min-w-0 flex-1 items-center gap-3">
<div
className={cn(
"absolute top-1 right-1 flex h-4 min-w-4 items-center justify-center rounded-sm px-1 text-[10px] leading-none font-medium",
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
isDark
? "bg-cyan-500 text-white"
: "bg-white tabular-nums text-[var(--brand-900)]",
? "bg-gradient-to-br from-blue-500 to-cyan-400"
: "bg-white/20",
)}
>
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</Button>
<div
className={cn(
"mx-1 h-8 w-px",
isDark ? "bg-blue-800/30" : "bg-white/30",
)}
aria-hidden
/>
<Button
type="button"
variant="ghost"
className={cn(
"h-8 gap-2 rounded-sm px-2",
isDark
? "text-slate-200 hover:bg-slate-800/50"
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
)}
>
<div
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-sm",
isDark
? "bg-gradient-to-br from-blue-500 to-cyan-400"
: "bg-white",
<FileText className="h-4 w-4 text-white" />
</div>
{!collapsed && (
<span className="text-sm font-semibold text-white">
CM助手
</span>
)}
>
<User
className={cn(
"size-4",
isDark ? "text-white" : "text-[var(--brand-900)]",
)}
/>
</div>
<div className="hidden text-left sm:block">
<p className="text-sm leading-tight font-medium text-white">
张策略
</p>
<p
className={cn(
"text-xs leading-tight",
isDark ? "text-blue-400/60" : "text-white/75",
)}
>
策略分析师
</p>
<div className="flex shrink-0 items-center gap-2">
{actionCluster}
</div>
<ChevronDown
className={cn(
"size-4 shrink-0",
isDark ? "text-slate-500" : "text-white/80",
)}
/>
</Button>
</div>
</>
)}
</header>
{showTopNav && thirdSubNavItems.length ? (
<nav className="third-submenu-bar" aria-label="三级菜单">
@@ -221,15 +221,15 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
}, []);
const shellClass = isDark
? 'strategy-data h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)]'
? 'strategy-data h-full overflow-y-auto'
: 'strategy-data h-full overflow-y-auto bg-[linear-gradient(180deg,#FFFDFD_0%,#FFF7F7_46%,#FAFAFA_100%)]';
const toolbarClass = isDark
? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(10,23,42,0.94)] text-white shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
? 'border border-[rgba(43,143,254,0.55)] bg-[rgba(3,38,80,0.82)] text-white shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
: 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_2px_8px_rgba(34,34,34,0.08)]';
const metricClass = isDark
? 'border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)]'
? 'border border-[rgba(43,143,254,0.55)] bg-[rgba(3,38,80,0.82)]'
: 'border border-[#E8E8E8] bg-white';
const inputClass = isDark
@@ -221,15 +221,15 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
}, []);
const shellClass = isDark
? 'strategy-data h-full overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(27,94,168,0.28),transparent_32%),linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100'
? 'strategy-data h-full overflow-y-auto text-slate-100'
: 'strategy-data h-full overflow-y-auto bg-[radial-gradient(circle_at_top_left,rgba(245,34,45,0.08),transparent_26%),linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]';
const toolbarClass = isDark
? 'border border-[rgba(110,136,176,0.24)] bg-[rgba(10,23,42,0.94)] text-slate-100 shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
? 'border border-[rgba(43,143,254,0.55)] bg-[rgba(3,38,80,0.82)] text-slate-100 shadow-[0_16px_36px_rgba(3,10,22,0.2)]'
: 'border border-[#E8E8E8] bg-white text-[#222222] shadow-[0_2px_8px_rgba(34,34,34,0.08)]';
const metricCardClass = isDark
? 'border border-[rgba(110,136,176,0.24)] bg-[linear-gradient(180deg,rgba(13,31,55,0.98),rgba(8,19,34,0.98))] shadow-[0_14px_42px_rgba(2,8,20,0.30)]'
? 'border border-[rgba(43,143,254,0.55)] bg-[rgba(3,38,80,0.82)] shadow-[0_14px_42px_rgba(2,8,20,0.30)]'
: 'border border-[#E8E8E8] bg-white';
const inputClass = isDark
@@ -25,7 +25,7 @@
--color-main: var(--color-primary);
--td-brand-color: var(--color-primary);
--bg-color: rgba(22, 119, 255, 0.12);
--card-border: rgba(110, 136, 176, 0.32);
--card-border: rgba(43, 143, 254, 0.55);
--text-color: var(--color-text-strong);
--card-bg: 220 59% 16%;
@@ -33,6 +33,14 @@
--border-color: 220 54% 29%;
--input-bg: 220 71% 10%;
--input-border: 220 35% 38%;
/* 深色背景图:对齐多智能体会商页风格,fixed 固定不随内容滚动 */
background-image: url("../../../../assets/img/layout-bg-theme-dark-blue.svg");
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
background-attachment: fixed;
background-color: #0c1836;
}
/* ── Theme card (TaskCard, KanbanColumn) ─────────────────────────────── */
+51 -1
View File
@@ -502,6 +502,20 @@
background: var(--background);
}
/* 深色背景换成设计稿深蓝主题底图。background-color 作为图片加载前/超出
* 区域的回落底色。仅 .dark 生效,浅色模式不受影响。
* - .workspace-chat-bg:紧凑模式聊天页(ChatPage 在 compactChrome 下挂)
* - .canvas-writing-bg:AI 写作页(AIWritingPage 根容器)*/
.dark .workspace-chat-bg,
.dark .canvas-writing-bg {
background-color: var(--background);
background-image: url("../assets/img/layout-bg-theme-dark-blue.svg");
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
background-attachment: fixed;
}
.workspace-panel {
border: 1px solid #f0d7d0;
background: #ffffff;
@@ -924,7 +938,43 @@ body {
}
html.dark .strategy-app-header {
background-image: url("../assets/img/dark-blue-header.svg");
background-image: url("../assets/img/top-bg.svg");
background-size: cover;
}
/* 深色顶栏:居中「CM助手」标题,落在背景图中间的发光梯形基座上。
左右菜单分区各 flex-1 等宽,本元素居中即对齐背景图基座。 */
.header-center-title {
flex: 0 0 auto;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 155px;
/* 文字略微上移,贴合基座视觉中心 */
transform: translateY(-6px);
font-family: "Source Han Sans SC", "Noto Sans SC", "Noto Sans S Chinese",
"Microsoft YaHei", sans-serif;
font-size: 26px;
font-weight: 700;
letter-spacing: 4px;
white-space: nowrap;
pointer-events: none;
user-select: none;
/* 白→浅蓝→白 金属质感渐变文字 */
background: linear-gradient(
180deg,
#f9f9fa 0%,
#ffffff 15%,
#d8e5ff 49%,
#ffffff 89%,
#f8fafb 100%
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
filter: drop-shadow(0 0 14px rgba(120, 200, 255, 0.35));
}
/* ── 浅蓝主题:覆盖散落在各处的红色硬编码(CSS 变量管不到的地方)────────── */