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:
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 |
@@ -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 |
@@ -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 />}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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="三级菜单">
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+9
-1
@@ -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) ─────────────────────────────── */
|
||||
|
||||
@@ -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 变量管不到的地方)────────── */
|
||||
|
||||
Reference in new issue
Block a user