Merge branch 'dhk-1' into 'master'
fix(sandbox): 修复 Markdown 编辑保存后内容被重置回原值 See merge request knowledge-base-bj/deerflow-server!121
This commit is contained in:
commit
5dc1ed5685
30 files changed
+2646
-959
No files matched your search
@@ -99,11 +99,15 @@ export function App() {
|
||||
<Route path="/public/leaderboard" element={<PublicLeaderboardPage />} />
|
||||
<Route
|
||||
path="/roundtable-share/:shareCode"
|
||||
element={<PublicRoundtableSharePage />}
|
||||
element={<PublicRoundtableSharePage view="landing" />}
|
||||
/>
|
||||
<Route
|
||||
path="/roundtable-share/:shareCode/report"
|
||||
element={<PublicRoundtableSharePage report />}
|
||||
element={<PublicRoundtableSharePage view="report" />}
|
||||
/>
|
||||
<Route
|
||||
path="/roundtable-share/:shareCode/dashboard"
|
||||
element={<PublicRoundtableSharePage view="dashboard" />}
|
||||
/>
|
||||
<Route
|
||||
path="/public/scheduled-tasks/:taskId"
|
||||
|
||||
@@ -498,3 +498,248 @@
|
||||
background: linear-gradient(90deg, rgba(62, 178, 255, 0) 0%, #4ee6ff 50%, rgba(62, 178, 255, 0) 100%);
|
||||
box-shadow: 0 0 10px rgba(78, 230, 255, 0.72);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
浅色主题导航(红/蓝两套,颜色经 --brand-* 变量自动切换)
|
||||
逻辑同深色:一级导航在顶栏、二级导航在侧边栏、三级导航在顶栏下方横条。
|
||||
但一级导航不做深色那种围绕基座的特殊处理,正常横排即可。
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── 顶栏一级导航(白字落在 red/blue-header banner 上)──────────────────── */
|
||||
.light-topnav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
/* 整体往右移(用百分比,随容器宽度自适应;约等于宽屏下的 200px 量级) */
|
||||
margin-left: 10%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.light-topnav-item {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
/* 复用全局字体栈(与「多智能体会商」等页面一致):Source Han Sans SC 在 Windows 上
|
||||
常按字重分家族,放栈首会导致 font-weight 选不到粗体;用 --font-sans(Noto Sans SC
|
||||
领先)才能正常加粗。 */
|
||||
font-family: var(--font-sans);
|
||||
font-size: 16px;
|
||||
/* 普通项用 500(雅黑兜底下渲染为常规字面),选中项 700 才能看出粗体对比 */
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.light-topnav-item:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 选中态:不改背景,文字加粗变亮 + 贴近导航栏底部一条粗白线 */
|
||||
.light-topnav-item--active,
|
||||
.light-topnav-item--active:hover {
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.light-topnav-item--active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 0;
|
||||
height: 4px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ── 侧边栏:二级图标卡片列(96px)+ 右侧三级列(展开时整体变宽,带过渡)──────── */
|
||||
.light-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 4rem;
|
||||
z-index: 40;
|
||||
width: 96px;
|
||||
height: calc(100vh - 4rem);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid var(--border);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* 展开三级列时整体变宽的过渡动画 */
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* 展开三级列:96(二级) + 140(三级) = 236px */
|
||||
.light-sidebar--expanded {
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
/* 二级列:固定 96px,竖排图标卡片 + 底部设置 */
|
||||
.light-sidebar__primary {
|
||||
flex: 0 0 96px;
|
||||
width: 96px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 横向 padding 置 0:让选中态浅红背景能铺满与列宽一致 */
|
||||
padding: 10px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 三级列:固定 140px,随侧边栏展开淡入 */
|
||||
.light-sidebar__secondary {
|
||||
flex: 0 0 140px;
|
||||
width: 140px;
|
||||
height: 100%;
|
||||
border-left: 1px solid var(--border);
|
||||
/* 横向 padding 置 0:让三级选中背景能铺满与列宽一致(文字内缩由 subitem 自身 padding 保证) */
|
||||
padding: 12px 0;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: #ffffff;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.light-sidebar--expanded .light-sidebar__secondary {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.light-sidebar__list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.light-sidebar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 14px 4px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #222222;
|
||||
/* 复用全局字体栈,确保加粗生效(详见 .light-topnav-item 注释) */
|
||||
font-family: var(--font-sans);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.light-sidebar-item__icon {
|
||||
flex-shrink: 0;
|
||||
/* 图标统一用主题色(红主题红、蓝主题蓝) */
|
||||
color: var(--brand-900);
|
||||
transition: color 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.light-sidebar-item__label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.light-sidebar-item:hover {
|
||||
background: var(--brand-200);
|
||||
}
|
||||
|
||||
.light-sidebar-item:hover .light-sidebar-item__icon {
|
||||
color: var(--brand-900);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.light-sidebar-item--active {
|
||||
background: var(--brand-200);
|
||||
color: var(--brand-900);
|
||||
font-weight: 600;
|
||||
/* 选中态:去圆角去边框,浅红底铺满父元素宽度,右侧一条粗主题色边(用 inset 阴影避免挤压内容) */
|
||||
box-shadow: inset -4px 0 0 0 var(--brand-900);
|
||||
}
|
||||
|
||||
.light-sidebar-item--active .light-sidebar-item__icon {
|
||||
color: var(--brand-900);
|
||||
}
|
||||
|
||||
/* 侧边栏底部:设置入口(浅色模式不支持收起,故无收起按钮) */
|
||||
.light-sidebar__footer {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── 三级列条目(左对齐图标 + 文字,参考 xiugai-1.png)─────────────────────── */
|
||||
.light-sidebar-subnav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.light-sidebar-subitem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #222222;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.light-sidebar-subitem__icon {
|
||||
flex-shrink: 0;
|
||||
/* 图标用主题色(红主题红、蓝主题蓝) */
|
||||
color: var(--brand-900);
|
||||
}
|
||||
|
||||
.light-sidebar-subitem__label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.light-sidebar-subitem:hover {
|
||||
background: var(--brand-200);
|
||||
}
|
||||
|
||||
.light-sidebar-subitem--active {
|
||||
background: var(--brand-200);
|
||||
color: var(--brand-900);
|
||||
font-weight: 700;
|
||||
box-shadow: inset -4px 0 0 0 var(--brand-900);
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Bell,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Settings,
|
||||
User,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
||||
import { SettingsDialog } from "@/components/workspace/settings";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useUnreadCount } from "@/core/notifications";
|
||||
import {
|
||||
defaultExpandedIds,
|
||||
type MenuItem,
|
||||
} from "@/core/page-layout/sidebar-menu";
|
||||
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
|
||||
|
||||
type PageSidebarProps = {
|
||||
collapsed: boolean;
|
||||
onToggleCollapsed: () => void;
|
||||
};
|
||||
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
||||
|
||||
export function PageSidebar({
|
||||
collapsed,
|
||||
onToggleCollapsed,
|
||||
}: PageSidebarProps) {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const visibleMenuItems = useSidebarMenuItems(!!isAdmin);
|
||||
const { t } = useI18n();
|
||||
const { data: unreadCount = 0 } = useUnreadCount();
|
||||
// defaultExpandedIds 是共享的 ReadonlySet,复制一份给 useState(避免外部突变)
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set(defaultExpandedIds));
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [settingsDefaultSection, setSettingsDefaultSection] =
|
||||
useState<SettingsSection>("appearance");
|
||||
const [notificationOpen, setNotificationOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const openSettings = (section: SettingsSection) => {
|
||||
setSettingsDefaultSection(section);
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const iconButtonClass = isDark
|
||||
? "rounded-lg p-2 text-slate-400 transition-all duration-300 ease-out hover:scale-105 hover:bg-slate-800/50 hover:text-white"
|
||||
: "rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600";
|
||||
const panelClass = isDark
|
||||
? "ml-4 space-y-1 border-l border-blue-800/30 pl-4"
|
||||
: "ml-4 space-y-1 border-l border-red-100 pl-4";
|
||||
const labelClass = (compact: boolean, small = false) =>
|
||||
`overflow-hidden whitespace-nowrap transition-all duration-300 ease-out ${
|
||||
compact ? "max-w-0 opacity-0" : "max-w-[220px] opacity-100"
|
||||
} ${small ? "text-xs" : "text-sm"} flex-1 font-medium`;
|
||||
const menuButtonClass = (isActive: boolean, depth = 0) =>
|
||||
`group relative flex w-full items-center rounded-[2px] py-2 text-left transition-all duration-300 ease-out theme-transition ${
|
||||
collapsed && depth === 0 ? "justify-center px-2" : "gap-3 px-3"
|
||||
} ${
|
||||
isActive
|
||||
? isDark
|
||||
? "bg-blue-500/15 text-cyan-300 font-semibold"
|
||||
: "bg-red-50 text-red-600 font-semibold"
|
||||
: isDark
|
||||
? "text-slate-400 hover:bg-slate-800/50 hover:text-white"
|
||||
: "text-gray-600 hover:bg-red-50 hover:text-red-600"
|
||||
}`;
|
||||
const footerButtonClass = menuButtonClass(false, 0);
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const renderLeaf = (item: MenuItem, depth = 0) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = !!item.path && pathname === item.path;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
title={collapsed && depth === 0 ? item.label : undefined}
|
||||
onClick={() => item.path && navigate(item.path)}
|
||||
className={menuButtonClass(isActive, depth)}
|
||||
>
|
||||
{isActive && (
|
||||
<span
|
||||
className={`absolute left-0 inset-y-0 w-[3px] rounded-r ${
|
||||
isDark ? "bg-cyan-300" : "bg-red-600"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<Icon className={`${depth === 0 ? "h-[18px] w-[18px]" : "h-4 w-4"} flex-shrink-0 transition-transform duration-300 ease-out group-hover:scale-105`} />
|
||||
{(!collapsed || depth > 0) && (
|
||||
<span className={labelClass(false, depth > 0)}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBranch = (item: MenuItem, depth = 0) => {
|
||||
const expanded = expandedIds.has(item.id);
|
||||
const isTopLevel = depth === 0;
|
||||
const Icon = depth > 0 && item.icon === Folder && expanded ? FolderOpen : item.icon;
|
||||
|
||||
return (
|
||||
<div key={item.id} className="space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (collapsed && isTopLevel) {
|
||||
onToggleCollapsed();
|
||||
return;
|
||||
}
|
||||
toggleExpanded(item.id);
|
||||
}}
|
||||
title={collapsed && isTopLevel ? item.label : undefined}
|
||||
className={menuButtonClass(false, depth)}
|
||||
>
|
||||
<Icon
|
||||
className={`${isTopLevel ? "h-[18px] w-[18px]" : "h-4 w-4"} flex-shrink-0 transition-transform duration-300 ease-out group-hover:scale-105 ${
|
||||
expanded && depth > 0 ? "scale-105" : ""
|
||||
}`}
|
||||
/>
|
||||
{(!collapsed || !isTopLevel) && (
|
||||
<span className={labelClass(false, depth > 0)}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
{(!collapsed || !isTopLevel) && (
|
||||
<ChevronRight
|
||||
className={`h-4 w-4 transition-transform duration-300 ease-out ${expanded ? "rotate-90" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{(!collapsed || !isTopLevel) && item.children && (
|
||||
<div
|
||||
className={`${panelClass} overflow-hidden transition-all duration-300 ease-out ${
|
||||
expanded ? "max-h-[1200px] opacity-100" : "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
{item.children.map((child) =>
|
||||
child.children ? renderBranch(child, depth + 1) : renderLeaf(child, depth + 1),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsDialog
|
||||
open={settingsOpen}
|
||||
onOpenChange={setSettingsOpen}
|
||||
defaultSection={settingsDefaultSection}
|
||||
/>
|
||||
<NotificationDialog
|
||||
open={notificationOpen}
|
||||
onOpenChange={setNotificationOpen}
|
||||
/>
|
||||
<aside
|
||||
className={`fixed left-0 top-16 z-40 flex h-[calc(100vh-4rem)] flex-col overflow-hidden shadow-sm theme-transition transition-all duration-300 ${
|
||||
isDark
|
||||
? "border-r border-blue-800/30 bg-slate-900"
|
||||
: "border-r border-red-100 bg-white"
|
||||
} ${collapsed ? "w-16" : "w-64"}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapsed}
|
||||
title={collapsed ? "展开侧边栏" : "收起侧边栏"}
|
||||
className={`absolute -right-3 top-6 z-50 flex h-6 w-6 items-center justify-center rounded-full shadow-md theme-transition ${
|
||||
isDark
|
||||
? "bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||||
: "bg-white text-gray-500 hover:bg-red-50 hover:text-red-600 border border-red-100"
|
||||
}`}
|
||||
>
|
||||
{collapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
|
||||
</button>
|
||||
|
||||
<nav className={`flex-1 overflow-y-auto ${collapsed ? "p-2" : "p-4"} space-y-2 transition-all duration-300 ease-out`}>
|
||||
{visibleMenuItems.map((item) =>
|
||||
item.children ? renderBranch(item) : renderLeaf(item),
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div
|
||||
className={`mt-auto shrink-0 theme-transition ${
|
||||
isDark ? "border-t border-blue-800/30" : "border-t border-red-100"
|
||||
} ${collapsed ? "p-2" : "p-4"}`}
|
||||
>
|
||||
{mounted ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
title={collapsed ? t.workspace.settingsAndMore : undefined}
|
||||
className={footerButtonClass}
|
||||
>
|
||||
<Settings className="h-5 w-5 flex-shrink-0" />
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className={labelClass(false)}>
|
||||
{t.workspace.settingsAndMore}
|
||||
</span>
|
||||
<ChevronsUpDown className="h-4 w-4 flex-shrink-0" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align={collapsed ? "start" : "end"}
|
||||
side={collapsed ? "right" : "top"}
|
||||
sideOffset={4}
|
||||
className="min-w-56 rounded-lg"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setNotificationOpen(true)}>
|
||||
<Bell className="size-4" />
|
||||
{t.notifications.title}
|
||||
{unreadCount > 0 && (
|
||||
<span
|
||||
className={`ml-auto flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium leading-none text-white ${
|
||||
isDark ? "bg-cyan-500" : "bg-red-500"
|
||||
}`}
|
||||
>
|
||||
{unreadCount > 99 ? "99+" : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => openSettings("tools")}>
|
||||
<Wrench className="size-4" />
|
||||
{t.settings.sections.tools}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => openSettings("account")}>
|
||||
<User className="size-4" />
|
||||
个人信息
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`${footerButtonClass} pointer-events-none`}
|
||||
>
|
||||
<Settings className="h-5 w-5 flex-shrink-0" />
|
||||
{!collapsed && (
|
||||
<span className={labelClass(false)}>
|
||||
{t.workspace.settingsAndMore}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`pointer-events-none absolute right-0 top-0 h-32 w-32 rounded-full blur-3xl theme-transition ${
|
||||
isDark ? "bg-blue-500/10" : "bg-red-500/5"
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
className={`pointer-events-none absolute bottom-20 right-0 h-24 w-24 rounded-full blur-3xl theme-transition ${
|
||||
isDark ? "bg-cyan-400/5" : "bg-red-400/5"
|
||||
}`}
|
||||
/>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -22,14 +22,18 @@ import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
||||
import { SettingsDialog } from "@/components/workspace/settings";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { isWujieEnv } from "@/core/embed/wujie";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useUnreadCount } from "@/core/notifications";
|
||||
import {
|
||||
defaultExpandedIds,
|
||||
getActiveThirdLevelItems,
|
||||
type MenuItem,
|
||||
} from "@/core/page-layout/sidebar-menu";
|
||||
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
|
||||
|
||||
import "@/components/page-sdebar.css";
|
||||
|
||||
type PageSidebarProps = {
|
||||
collapsed: boolean;
|
||||
onToggleCollapsed: () => void;
|
||||
@@ -37,7 +41,201 @@ type PageSidebarProps = {
|
||||
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
||||
|
||||
export function PageSidebar({
|
||||
const hasActiveChild = (item: MenuItem, pathname: string): boolean => {
|
||||
if (item.path && pathname === item.path) {
|
||||
return true;
|
||||
}
|
||||
return item.children?.some((child) => hasActiveChild(child, pathname)) ?? false;
|
||||
};
|
||||
|
||||
const findFirstRoutePath = (items?: MenuItem[]): string | null => {
|
||||
if (!items?.length) {
|
||||
return null;
|
||||
}
|
||||
for (const child of items) {
|
||||
if (child.path) {
|
||||
return child.path;
|
||||
}
|
||||
const nested = findFirstRoutePath(child.children);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 浅色侧边栏入口。
|
||||
*
|
||||
* - 普通桌面环境:渲染「二级导航图标卡片」(一级在顶栏、二级在侧边栏,参考 导航-1.png)。
|
||||
* - wujie 嵌入环境:顶栏被宿主隐藏,一级导航无处安放,故回落到原手风琴整树菜单
|
||||
* (保证嵌入模式下所有层级仍可达,零回归)。
|
||||
*/
|
||||
export function PageSidebar({ collapsed, onToggleCollapsed }: PageSidebarProps) {
|
||||
if (isWujieEnv()) {
|
||||
return (
|
||||
<LegacyAccordionSidebar
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={onToggleCollapsed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <SecondaryNavSidebar />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浅色二级导航侧边栏(图标卡片,红/蓝主题色经 --brand-* 变量自动切换)。
|
||||
* 浅色模式不支持侧边栏收起,故无收起按钮、不接收 collapsed 相关 props。
|
||||
*/
|
||||
function SecondaryNavSidebar() {
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const visibleMenuItems = useSidebarMenuItems(!!isAdmin);
|
||||
const { t } = useI18n();
|
||||
const { data: unreadCount = 0 } = useUnreadCount();
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [settingsDefaultSection, setSettingsDefaultSection] =
|
||||
useState<SettingsSection>("appearance");
|
||||
const [notificationOpen, setNotificationOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const openSettings = (section: SettingsSection) => {
|
||||
setSettingsDefaultSection(section);
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
// 当前激活的一级菜单(与顶栏一级导航联动);其 children 即侧边栏要渲染的二级导航。
|
||||
const activeTopItem =
|
||||
visibleMenuItems.find((item) => hasActiveChild(item, pathname)) ??
|
||||
visibleMenuItems[0];
|
||||
const secondaryItems = activeTopItem?.children ?? [];
|
||||
|
||||
const isItemActive = (item: MenuItem) =>
|
||||
item.path ? pathname === item.path : hasActiveChild(item, pathname);
|
||||
|
||||
const handleItemClick = (item: MenuItem) => {
|
||||
const path = item.path ?? findFirstRoutePath(item.children);
|
||||
if (path) {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
|
||||
// 当前激活二级项的三级子项;非空时侧边栏在右侧展开「三级列」并整体变宽(见 css 过渡)。
|
||||
const thirdItems = getActiveThirdLevelItems(visibleMenuItems, pathname);
|
||||
const expanded = thirdItems.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsDialog
|
||||
open={settingsOpen}
|
||||
onOpenChange={setSettingsOpen}
|
||||
defaultSection={settingsDefaultSection}
|
||||
/>
|
||||
<NotificationDialog
|
||||
open={notificationOpen}
|
||||
onOpenChange={setNotificationOpen}
|
||||
/>
|
||||
<aside className={`light-sidebar ${expanded ? "light-sidebar--expanded" : ""}`}>
|
||||
{/* 一级列:二级图标卡片 + 底部设置 */}
|
||||
<div className="light-sidebar__primary">
|
||||
<nav className="light-sidebar__list" aria-label="二级菜单">
|
||||
{secondaryItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = isItemActive(item);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
title={item.label}
|
||||
onClick={() => handleItemClick(item)}
|
||||
className={`light-sidebar-item ${active ? "light-sidebar-item--active" : ""}`}
|
||||
>
|
||||
<Icon className="light-sidebar-item__icon h-7 w-7" />
|
||||
<span className="light-sidebar-item__label">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="light-sidebar__footer">
|
||||
{mounted ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
title={t.workspace.settingsAndMore}
|
||||
className="light-sidebar-item"
|
||||
>
|
||||
<Settings className="light-sidebar-item__icon h-7 w-7" />
|
||||
<span className="light-sidebar-item__label">设置</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
side="right"
|
||||
sideOffset={8}
|
||||
className="min-w-56 rounded-lg"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setNotificationOpen(true)}>
|
||||
<Bell className="size-4" />
|
||||
{t.notifications.title}
|
||||
{unreadCount > 0 && (
|
||||
<span className="ml-auto flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--brand-900)] px-1 text-[10px] font-medium leading-none text-white">
|
||||
{unreadCount > 99 ? "99+" : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => openSettings("tools")}>
|
||||
<Wrench className="size-4" />
|
||||
{t.settings.sections.tools}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => openSettings("account")}>
|
||||
<User className="size-4" />
|
||||
个人信息
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 二级列:三级导航(激活二级项有三级子项时展开,整体动画见 css width transition) */}
|
||||
<div className="light-sidebar__secondary" aria-hidden={!expanded}>
|
||||
<nav className="light-sidebar-subnav" aria-label="三级菜单">
|
||||
{thirdItems.map((child) => {
|
||||
const ChildIcon = child.icon;
|
||||
const childActive = child.path
|
||||
? pathname === child.path
|
||||
: hasActiveChild(child, pathname);
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
title={child.label}
|
||||
onClick={() => handleItemClick(child)}
|
||||
className={`light-sidebar-subitem ${childActive ? "light-sidebar-subitem--active" : ""}`}
|
||||
>
|
||||
<ChildIcon className="light-sidebar-subitem__icon h-4 w-4" />
|
||||
<span className="light-sidebar-subitem__label">{child.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧版浅色整树手风琴侧边栏。仅用于 wujie 嵌入环境(顶栏被宿主隐藏时保证所有层级可达)。
|
||||
*/
|
||||
function LegacyAccordionSidebar({
|
||||
collapsed,
|
||||
onToggleCollapsed,
|
||||
}: PageSidebarProps) {
|
||||
@@ -66,9 +264,6 @@ export function PageSidebar({
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const iconButtonClass = isDark
|
||||
? "rounded-lg p-2 text-slate-400 transition-all duration-300 ease-out hover:scale-105 hover:bg-slate-800/50 hover:text-white"
|
||||
: "rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600";
|
||||
const panelClass = isDark
|
||||
? "ml-4 space-y-1 border-l border-blue-800/30 pl-4"
|
||||
: "ml-4 space-y-1 border-l border-red-100 pl-4";
|
||||
|
||||
@@ -121,12 +121,19 @@ export function ArtifactFileDetail({
|
||||
filepath: filepathFromProps,
|
||||
threadId,
|
||||
onClose,
|
||||
allowVirtualMarkdownEdit = false,
|
||||
}: {
|
||||
className?: string;
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
/** 覆盖默认关闭行为(仅 setOpen(false));圆桌 Step3 文件预览用于退出沙箱模式。 */
|
||||
onClose?: () => void;
|
||||
/**
|
||||
* 圆桌沙箱(Step2/Step3)用:流式结束后,把 `write-file:` 虚拟 md 预览切回真实
|
||||
* artifacts API(可加载 + PATCH 保存),从而支持用户编辑(对齐主聊天页 md 可编辑)。
|
||||
* 默认 false —— 主聊天页等其它调用方行为不变。
|
||||
*/
|
||||
allowVirtualMarkdownEdit?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
@@ -141,10 +148,6 @@ export function ArtifactFileDetail({
|
||||
}
|
||||
return filepathFromProps;
|
||||
}, [filepathFromProps, isWriteFileVirtual]);
|
||||
/** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API。 */
|
||||
const useWriteFileStreaming = useMemo(() => {
|
||||
return isWriteFileVirtual && !isImageFile(filepath);
|
||||
}, [filepath, isWriteFileVirtual]);
|
||||
const isSkillFile = useMemo(() => {
|
||||
return filepath.endsWith(".skill");
|
||||
}, [filepath]);
|
||||
@@ -153,6 +156,24 @@ export function ArtifactFileDetail({
|
||||
const isSpreadsheetFile = useMemo(() => {
|
||||
return /\.xlsx$/i.test(filepath);
|
||||
}, [filepath]);
|
||||
// 圆桌沙箱:write_file 虚拟预览的 md 在流式结束(thread 非 loading)后,切回真实
|
||||
// artifacts API 加载并允许编辑/保存。流式进行中仍走内存 stub 只读预览;仅
|
||||
// allowVirtualMarkdownEdit 开启的调用方(Step2/Step3 沙箱)生效,主聊天页不受影响。
|
||||
const editVirtualMarkdown = useMemo(() => {
|
||||
return (
|
||||
allowVirtualMarkdownEdit &&
|
||||
isWriteFileVirtual &&
|
||||
!isImageFile(filepath) &&
|
||||
!isMock &&
|
||||
!thread.isLoading &&
|
||||
checkCodeFile(filepath).language === "markdown"
|
||||
);
|
||||
}, [allowVirtualMarkdownEdit, isWriteFileVirtual, filepath, isMock, thread.isLoading]);
|
||||
/** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API;
|
||||
* 圆桌结束后可编辑的 md(editVirtualMarkdown)亦回到真实 API(可加载 + PATCH 保存)。 */
|
||||
const useWriteFileStreaming = useMemo(() => {
|
||||
return isWriteFileVirtual && !isImageFile(filepath) && !editVirtualMarkdown;
|
||||
}, [filepath, isWriteFileVirtual, editVirtualMarkdown]);
|
||||
const { isCodeFile, language } = useMemo(() => {
|
||||
if (useWriteFileStreaming) {
|
||||
let nextLanguage = checkCodeFile(filepath).language;
|
||||
@@ -173,7 +194,10 @@ export function ArtifactFileDetail({
|
||||
}, [language]);
|
||||
const { content, isApplyingEdit } = useArtifactContent({
|
||||
threadId,
|
||||
filepath: filepathFromProps,
|
||||
// editVirtualMarkdown:用解码后的真实路径走 artifacts API(而非 write-file 虚拟
|
||||
// URL),这样保存后 useUpdateArtifactContent 写入的查询缓存才能命中,编辑内容
|
||||
// 不会被旧的内存 stub 值覆盖回退。
|
||||
filepath: editVirtualMarkdown ? filepath : filepathFromProps,
|
||||
enabled: isCodeFile && !useWriteFileStreaming,
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useLocalSettings } from "@/core/settings";
|
||||
import {
|
||||
useAppearanceSettings,
|
||||
useCitationDisplaySettings,
|
||||
useLayoutMode,
|
||||
useUpdateAppearanceSettings,
|
||||
useUpdateCitationDisplaySettings,
|
||||
} from "@/core/system-settings/hooks";
|
||||
@@ -57,6 +58,9 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
||||
const globalDefault = appearance?.default_theme ?? "light";
|
||||
const globalSkillView = appearance?.default_skill_view ?? "list";
|
||||
const globalAgentView = appearance?.default_agent_view ?? "card";
|
||||
const globalLayoutMode = appearance?.default_layout_mode ?? "full";
|
||||
// 有效界面模式:用户自己选过则以其为准,否则跟随管理员系统默认(再否则会话级回退)。
|
||||
const [effectiveLayoutMode, setLayoutMode] = useLayoutMode();
|
||||
const cleanupWords =
|
||||
citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? [];
|
||||
|
||||
@@ -74,6 +78,18 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefaultLayoutMode = async (value: "full" | "compact") => {
|
||||
if (value === globalLayoutMode) return;
|
||||
try {
|
||||
await updateAppearance.mutateAsync({ default_layout_mode: value });
|
||||
toast.success(
|
||||
`已将系统默认界面模式设置为${value === "full" ? "全量模式" : "简洁模式"}`,
|
||||
);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefaultView = async (
|
||||
kind: "skill" | "agent",
|
||||
value: "card" | "list",
|
||||
@@ -156,10 +172,10 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={settings.display.layoutMode !== "compact" ? "default" : "outline"}
|
||||
variant={effectiveLayoutMode !== "compact" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSettings("display", { layoutMode: "full" });
|
||||
setLayoutMode("full");
|
||||
onClose?.();
|
||||
navigate("/page/workspace/chats/new");
|
||||
}}
|
||||
@@ -169,10 +185,10 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={settings.display.layoutMode === "compact" ? "default" : "outline"}
|
||||
variant={effectiveLayoutMode === "compact" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSettings("display", { layoutMode: "compact" });
|
||||
setLayoutMode("compact");
|
||||
onClose?.();
|
||||
navigate("/page/workspace/chats/new");
|
||||
}}
|
||||
@@ -183,6 +199,43 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Separator />
|
||||
<SettingsSection
|
||||
title="系统默认界面模式(管理员)"
|
||||
description="给所有还没有手动切换过界面模式的用户的默认;已经自己切换过的用户保留其个人选择不受影响。"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={globalLayoutMode === "full" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => void handleSetDefaultLayoutMode("full")}
|
||||
disabled={updateAppearance.isPending}
|
||||
>
|
||||
<PanelLeftIcon className="mr-1.5 size-4" />
|
||||
全量模式
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={globalLayoutMode === "compact" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => void handleSetDefaultLayoutMode("compact")}
|
||||
disabled={updateAppearance.isPending}
|
||||
>
|
||||
<LayoutIcon className="mr-1.5 size-4" />
|
||||
简洁模式
|
||||
</Button>
|
||||
<p className="text-muted-foreground ml-2 text-xs">
|
||||
当前系统默认:
|
||||
{globalLayoutMode === "full" ? "全量模式" : "简洁模式"}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
@@ -133,8 +133,18 @@ export function useUpdateArtifactContent() {
|
||||
return useMutation({
|
||||
mutationFn: updateArtifactContent,
|
||||
onSuccess: (content, variables) => {
|
||||
queryClient.setQueryData(
|
||||
["artifact", variables.filepath, variables.threadId, variables.isMock],
|
||||
// useArtifactContent 的查询键带第 5 个元素 publicShareCode,这里用前缀
|
||||
// 过滤(setQueriesData)匹配,避免精确键缺少 publicShareCode 写到幽灵缓存、
|
||||
// 导致保存后内容被旧缓存重置回原值。
|
||||
queryClient.setQueriesData(
|
||||
{
|
||||
queryKey: [
|
||||
"artifact",
|
||||
variables.filepath,
|
||||
variables.threadId,
|
||||
variables.isMock,
|
||||
],
|
||||
},
|
||||
{
|
||||
content,
|
||||
url: undefined,
|
||||
|
||||
@@ -31,15 +31,44 @@ export async function loginByYUserId(
|
||||
{ yuserId: yUserId },
|
||||
)) as UserDetailResponse;
|
||||
|
||||
const result = res.data;
|
||||
if (!result?.id) {
|
||||
throw new Error("无法根据 yUserId 获取用户信息。");
|
||||
}
|
||||
|
||||
localStorage.setItem(EXTERNAL_USER_ID_STORAGE_KEY, result.id);
|
||||
localStorage.setItem(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(result));
|
||||
// consumer 返回 { accessToken, gateDepartName, typeName, userName }。
|
||||
// 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与
|
||||
// source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。
|
||||
// 注意:接口也会返回 accessToken,但 XK/PG 及 source=accessToken 要用路由透传的
|
||||
// access_token,故登录流程随后会用 persistAccessToken 覆盖 userInfo.accessToken。
|
||||
const result = res.data ?? {};
|
||||
localStorage.setItem(
|
||||
EXTERNAL_USER_INFO_STORAGE_KEY,
|
||||
JSON.stringify({ ...result, yUserId }),
|
||||
);
|
||||
}
|
||||
|
||||
export function persistToolsToken(token: string): void {
|
||||
localStorage.setItem(TOOLS_TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.accessToken(覆盖接口返回的同名值)。
|
||||
* 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读
|
||||
* external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里
|
||||
* source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。
|
||||
* 用合并而非覆盖整对象,避免抹掉 loginByYUserId 从 consumer 拿到的用户信息。
|
||||
*/
|
||||
export function persistAccessToken(token: string): void {
|
||||
const trimmed = token?.trim();
|
||||
if (!trimmed) return;
|
||||
let info: Record<string, unknown> = {};
|
||||
try {
|
||||
const raw = localStorage.getItem(EXTERNAL_USER_INFO_STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
info = parsed as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
info = {};
|
||||
}
|
||||
info.accessToken = trimmed;
|
||||
localStorage.setItem(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(info));
|
||||
}
|
||||
@@ -9,15 +9,16 @@
|
||||
*/
|
||||
import type { ElementType } from "react";
|
||||
import {
|
||||
Activity,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Brain,
|
||||
CalendarClock,
|
||||
CircleHelp,
|
||||
ClipboardList,
|
||||
Database,
|
||||
FileBox,
|
||||
FileText,
|
||||
Folder,
|
||||
Gauge,
|
||||
Grid3X3,
|
||||
HelpCircle,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
ListChecks,
|
||||
ListTodo,
|
||||
Network,
|
||||
Radar,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Target,
|
||||
@@ -77,7 +79,7 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "xk-center-task",
|
||||
label: "XK中任务",
|
||||
icon: Folder,
|
||||
icon: ClipboardList,
|
||||
children: [
|
||||
{
|
||||
id: "xk-task-jc",
|
||||
@@ -96,7 +98,7 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "coh-center-task",
|
||||
label: "COH中任务",
|
||||
icon: Folder,
|
||||
icon: Activity,
|
||||
children: [
|
||||
{
|
||||
id: "coh-task-3qfx",
|
||||
@@ -121,7 +123,7 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "pg-center-task",
|
||||
label: "PG中任务",
|
||||
icon: Folder,
|
||||
icon: Gauge,
|
||||
children: [
|
||||
{
|
||||
id: "pg-task-pg",
|
||||
@@ -146,7 +148,7 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "ty-center-task",
|
||||
label: "TY中任务",
|
||||
icon: Folder,
|
||||
icon: Radar,
|
||||
children: [
|
||||
{
|
||||
id: "ty-task-xdty",
|
||||
@@ -296,6 +298,31 @@ export const defaultExpandedIds: ReadonlySet<string> = new Set([
|
||||
"comprehensive-management",
|
||||
]);
|
||||
|
||||
/** 该菜单项自身或任意后代的 path 是否等于当前路由 */
|
||||
export function menuItemContainsPath(item: MenuItem, pathname: string): boolean {
|
||||
if (item.path && item.path === pathname) {
|
||||
return true;
|
||||
}
|
||||
return item.children?.some((child) => menuItemContainsPath(child, pathname)) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在「一级 → 二级 → 三级」菜单中,取当前路由所在的「激活二级项」的三级子项。
|
||||
* 浅色侧边栏据此在二级右侧展开三级列;无三级则返回空数组(侧边栏保持窄栏)。
|
||||
*/
|
||||
export function getActiveThirdLevelItems(
|
||||
items: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem[] {
|
||||
const activeTop =
|
||||
items.find((item) => menuItemContainsPath(item, pathname)) ?? items[0];
|
||||
const secondary = activeTop?.children ?? [];
|
||||
const activeSecond = secondary.find(
|
||||
(item) => (item.children?.length ?? 0) > 0 && menuItemContainsPath(item, pathname),
|
||||
);
|
||||
return activeSecond?.children ?? [];
|
||||
}
|
||||
|
||||
/** 按 admin 角色过滤菜单:非 admin 不展示 ``adminOnly: true`` 的项及其子项 */
|
||||
export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
|
||||
return items
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
* 浅色 PageSidebar 与深色 PageSidebarDark 共用此 hook,保证两套皮肤同步。
|
||||
*/
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Layout } from "lucide-react";
|
||||
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
|
||||
|
||||
import {
|
||||
filterMenuItemsByRole,
|
||||
getActiveThirdLevelItems,
|
||||
sidebarMenuItems,
|
||||
type MenuItem,
|
||||
} from "./sidebar-menu";
|
||||
@@ -61,3 +64,14 @@ export function useSidebarMenuItems(isAdmin: boolean): MenuItem[] {
|
||||
[iframeApps, isAdmin],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前路由所在「激活二级项」的三级子项(浅色侧边栏右侧列用)。
|
||||
* 供 PageRoutes(主内容左内边距随展开自适应)与浅色侧边栏共用同一份判定逻辑。
|
||||
*/
|
||||
export function useActiveThirdLevel(): MenuItem[] {
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const items = useSidebarMenuItems(!!isAdmin);
|
||||
const { pathname } = useLocation();
|
||||
return useMemo(() => getActiveThirdLevelItems(items, pathname), [items, pathname]);
|
||||
}
|
||||
@@ -37,11 +37,17 @@ export interface PublicDraftShareSnapshot {
|
||||
consensus_text: string;
|
||||
/** 各席位最终交付. */
|
||||
seat_deliveries: PublicDraftSeatDelivery[];
|
||||
/** Self-contained Step 3 result report HTML (empty when no report exists). */
|
||||
/** Self-contained Step 3 result report HTML — 经典 HTML 看板 (empty when none). */
|
||||
html: string;
|
||||
summary: string | null;
|
||||
generated_at: string | null;
|
||||
has_report: boolean;
|
||||
/** 方案总结报告 (Markdown,roundtable-summary 产出). */
|
||||
summary_report_md: string;
|
||||
has_summary_report: boolean;
|
||||
/** 大屏多页 (模式二) 结构化数据 (report-json 字符串);前端按主题渲染成自包含 HTML。 */
|
||||
dashboard_json: string | null;
|
||||
has_dashboard: boolean;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,17 +137,20 @@ export async function updatePublishSquares(
|
||||
|
||||
export type ThemeMode = "light" | "dark" | "lightblue";
|
||||
export type ViewMode = "card" | "list";
|
||||
export type LayoutMode = "full" | "compact";
|
||||
|
||||
export interface AppearanceSettings {
|
||||
default_theme: ThemeMode;
|
||||
default_skill_view: ViewMode;
|
||||
default_agent_view: ViewMode;
|
||||
default_layout_mode: LayoutMode;
|
||||
}
|
||||
|
||||
export interface AppearanceUpdate {
|
||||
default_theme?: ThemeMode;
|
||||
default_skill_view?: ViewMode;
|
||||
default_agent_view?: ViewMode;
|
||||
default_layout_mode?: LayoutMode;
|
||||
}
|
||||
|
||||
export async function loadAppearanceSettings(): Promise<AppearanceSettings> {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type PrivateSquareUpdate,
|
||||
type PromptPrefixUpdate,
|
||||
type PublishSquaresUpdate,
|
||||
type LayoutMode,
|
||||
type ViewMode,
|
||||
} from "./api";
|
||||
|
||||
@@ -143,6 +144,27 @@ export function useViewMode(
|
||||
return [mode, setMode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective interface layout mode (full chrome vs compact). A user's own choice
|
||||
* (persisted in local settings) wins; otherwise the admin-configured system
|
||||
* default applies; otherwise `undefined` (callers fall back to the session-level
|
||||
* `sidebarEnabled` flag set by portal / yUserId login). `setMode` persists the
|
||||
* user's override locally.
|
||||
*/
|
||||
export function useLayoutMode(): [
|
||||
LayoutMode | undefined,
|
||||
(mode: LayoutMode) => void,
|
||||
] {
|
||||
const [settings, setSettings] = useLocalSettings();
|
||||
const { data: appearance } = useAppearanceSettings();
|
||||
const userOverride = settings.display.layoutMode;
|
||||
const adminDefault = appearance?.default_layout_mode;
|
||||
const mode = userOverride ?? adminDefault;
|
||||
const setMode = (next: LayoutMode) =>
|
||||
setSettings("display", { layoutMode: next });
|
||||
return [mode, setMode];
|
||||
}
|
||||
|
||||
export function useCitationDisplaySettings() {
|
||||
return useQuery({
|
||||
queryKey: CITATION_DISPLAY_KEY,
|
||||
|
||||
@@ -42,10 +42,10 @@ import {
|
||||
getLastAgentId,
|
||||
saveLastAgentId,
|
||||
useKnowledgeRagEnabled,
|
||||
useLocalSettings,
|
||||
useThreadSettings,
|
||||
} from "@/core/settings";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { env } from "@/env";
|
||||
@@ -96,7 +96,8 @@ export default function ChatPage() {
|
||||
const { open: historySidebarOpen, toggleSidebar: toggleHistorySidebar } =
|
||||
useSidebar();
|
||||
const { t } = useI18n();
|
||||
const [localSettings] = useLocalSettings();
|
||||
// 有效界面模式:用户自选 > 管理员系统默认 > (下方再回退会话级 sidebarEnabled)
|
||||
const [effectiveLayoutMode] = useLayoutMode();
|
||||
const [showFollowups, setShowFollowups] = useState(false);
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
||||
@@ -418,7 +419,7 @@ export default function ChatPage() {
|
||||
// 优先看外观设置的「全量/简洁」,未设置过才回退到会话级 sidebarEnabled。否则用户显式
|
||||
// 选「全量」时 PageRoutes 已挂出顶栏+侧栏,这里却还按旧 sidebarEnabled 判定,导致深蓝
|
||||
// 主题底图(workspace-chat-bg)不生效。非紧凑(嵌入/standalone)保持原样。
|
||||
const layoutMode = localSettings.display.layoutMode;
|
||||
const layoutMode = effectiveLayoutMode;
|
||||
const chromeByPreference =
|
||||
layoutMode === "full"
|
||||
? true
|
||||
@@ -589,11 +590,6 @@ export default function ChatPage() {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{compactLanding && (
|
||||
<div className="text-muted-foreground/70 w-full -translate-y-2 text-center text-[11px]">
|
||||
CM Assistant V1.2 · 深度推理可能会涉及数据同步 · 信息由AI自动生成,请审慎参考
|
||||
</div>
|
||||
)}
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
||||
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
||||
{t.common.notAvailableInDemoMode}
|
||||
|
||||
@@ -37,6 +37,8 @@ import { cn } from "@/lib/utils";
|
||||
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
|
||||
import { buildLightAppUrl } from "@/strategy-components/lib/light-app";
|
||||
|
||||
import "./light-app-pages.css";
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
|
||||
/** 卡片图标池(按 appId 哈希取一个,仅作视觉区分)。 */
|
||||
@@ -107,7 +109,7 @@ export default function LightAppListPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto bg-zinc-50/60 font-sans text-zinc-900 antialiased dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
||||
<div className="light-app-list-page h-full overflow-y-auto bg-zinc-50/60 font-sans text-zinc-900 antialiased dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
||||
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-6 px-4 py-7 sm:px-6 lg:px-8">
|
||||
{/* 标题 */}
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 应用管理页 —— 注册 / 编辑轻应用(iframe 与 window.open)。
|
||||
*
|
||||
* 深蓝主题适配见 `./light-app-management.css`(《深蓝色 UI 规范》tokens.css)。
|
||||
*/
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -43,6 +48,8 @@ import {
|
||||
stringifyThemeMap,
|
||||
} from "@/strategy-components/lib/light-app";
|
||||
|
||||
import "./light-app-management.css";
|
||||
|
||||
interface FormState {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -63,7 +70,10 @@ const PARAM_TYPE_OPTIONS: { label: string; value: "login" | "theme" }[] = [
|
||||
{ label: "主题参数", value: "theme" },
|
||||
];
|
||||
|
||||
const DIALOG_SELECT_POPUP = { attach: () => document.body };
|
||||
const DIALOG_SELECT_POPUP = {
|
||||
attach: () => document.body,
|
||||
overlayClassName: "lam-select-popup",
|
||||
};
|
||||
|
||||
const emptyForm = (): FormState => ({
|
||||
name: "",
|
||||
@@ -213,14 +223,14 @@ export default function LightAppManagementPage() {
|
||||
const tableApps = useMemo(() => apps, [apps]);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto bg-neutral-50">
|
||||
<div className="light-app-management-page h-full overflow-y-auto bg-neutral-50 dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
||||
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col space-y-7 px-8 py-8">
|
||||
<div className="flex flex-col justify-between gap-6 md:flex-row md:items-center">
|
||||
<div className="space-y-1.5">
|
||||
<h1 className="flex items-center text-xl font-black tracking-tight text-neutral-900">
|
||||
<h1 className="lam-title flex items-center text-xl font-black tracking-tight text-neutral-900">
|
||||
应用管理
|
||||
</h1>
|
||||
<p className="max-w-2xl text-xs leading-relaxed text-neutral-500">
|
||||
<p className="lam-desc max-w-2xl text-xs leading-relaxed text-neutral-500">
|
||||
注册轻应用。
|
||||
<code className="mx-1 rounded-[5px] bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">
|
||||
window.open
|
||||
@@ -235,14 +245,14 @@ export default function LightAppManagementPage() {
|
||||
|
||||
<button
|
||||
onClick={openCreate}
|
||||
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]"
|
||||
className="lam-primary-btn 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-[5px] bg-white p-3 shadow-[0_1px_3px_rgba(0,0,0,0.01)] md:flex-row">
|
||||
<div className="lam-toolbar 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
|
||||
@@ -250,7 +260,7 @@ export default function LightAppManagementPage() {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="搜索应用名称 / 描述..."
|
||||
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"
|
||||
className="lam-search-input 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
|
||||
@@ -262,12 +272,12 @@ export default function LightAppManagementPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="lam-view-toggle 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-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
|
||||
className={`lam-view-toggle-btn cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
|
||||
viewMode === "table"
|
||||
? "bg-white text-[#F5222D] shadow-sm"
|
||||
? "is-active bg-white text-[#F5222D] shadow-sm"
|
||||
: "text-neutral-500 hover:text-neutral-700"
|
||||
}`}
|
||||
>
|
||||
@@ -275,9 +285,9 @@ export default function LightAppManagementPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("cards")}
|
||||
className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
|
||||
className={`lam-view-toggle-btn cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${
|
||||
viewMode === "cards"
|
||||
? "bg-white text-[#F5222D] shadow-sm"
|
||||
? "is-active bg-white text-[#F5222D] shadow-sm"
|
||||
: "text-neutral-500 hover:text-neutral-700"
|
||||
}`}
|
||||
>
|
||||
@@ -287,14 +297,14 @@ export default function LightAppManagementPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-xs font-medium text-neutral-500">
|
||||
搜寻到 <span className="font-bold text-[#F5222D]">{tableApps.length}</span>{" "}
|
||||
<div className="lam-count text-xs font-medium text-neutral-500">
|
||||
搜寻到 <strong className="font-bold text-[#F5222D]">{tableApps.length}</strong>{" "}
|
||||
个注册完成的子模块轻应用
|
||||
</div>
|
||||
|
||||
{viewMode === "table" ? (
|
||||
<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">
|
||||
<div className="lam-table-wrap overflow-visible rounded-[5px] bg-white shadow-[0_1px_3px_0_rgba(0,0,0,0.01)]">
|
||||
<table className="lam-table 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">
|
||||
<th className="w-[15%] px-6 py-4">名称</th>
|
||||
@@ -310,19 +320,19 @@ export default function LightAppManagementPage() {
|
||||
{tableApps.map((app) => (
|
||||
<tr key={app.appId} className="group align-middle transition-colors hover:bg-neutral-50/40">
|
||||
<td className="px-6 py-[18px]">
|
||||
<span className="text-[14px] font-bold text-neutral-800 transition-colors group-hover:text-[#F5222D]">
|
||||
<span className="lam-row-name text-[14px] font-bold text-neutral-800 transition-colors group-hover:text-[#F5222D]">
|
||||
{app.name}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-[18px]">
|
||||
{app.appType === "window_open" ? (
|
||||
<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">
|
||||
<span className="lam-tag-window 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-[5px] border border-sky-100 bg-sky-50 px-3 py-1 text-[12px] font-semibold text-sky-800">
|
||||
<span className="lam-tag-iframe 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>
|
||||
@@ -330,7 +340,7 @@ export default function LightAppManagementPage() {
|
||||
</td>
|
||||
|
||||
<td className="overflow-hidden px-4 py-[18px]">
|
||||
<div className="flex max-w-full items-center space-x-1.5 font-mono text-[12px] text-neutral-600 transition-colors group-hover:text-neutral-800">
|
||||
<div className="lam-url flex max-w-full items-center space-x-1.5 font-mono text-[12px] text-neutral-600 transition-colors group-hover:text-neutral-800">
|
||||
<a
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
@@ -342,7 +352,7 @@ export default function LightAppManagementPage() {
|
||||
</a>
|
||||
<button
|
||||
onClick={() => handleCopyUrl(app.url)}
|
||||
className="shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
|
||||
className="lam-icon-btn 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" />
|
||||
@@ -352,8 +362,8 @@ 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-[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="lam-mount-tag 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="lam-mount-dot h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||
<span className="truncate">{mountLabel(app.mountLocation)}</span>
|
||||
</span>
|
||||
) : (
|
||||
@@ -370,7 +380,7 @@ export default function LightAppManagementPage() {
|
||||
activeParamPopover === app.appId ? null : app.appId
|
||||
)
|
||||
}
|
||||
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"
|
||||
className="lam-param-btn 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 ? (
|
||||
@@ -389,14 +399,14 @@ 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-[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">
|
||||
<div className="lam-param-popover 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="lam-param-popover-head 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">
|
||||
注入的路由参数
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setActiveParamPopover(null)}
|
||||
className="text-[10px] font-semibold text-rose-700 hover:underline"
|
||||
className="lam-param-popover-close text-[10px] font-semibold text-rose-700 hover:underline"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
@@ -405,11 +415,11 @@ export default function LightAppManagementPage() {
|
||||
{app.routeParams.map((p, idx) => (
|
||||
<div
|
||||
key={`${app.appId}-${idx}`}
|
||||
className="rounded-[5px] border border-neutral-100 bg-neutral-50/60 p-2 text-[11px] leading-relaxed"
|
||||
className="lam-param-item 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>
|
||||
<span className="truncate text-[10px] text-neutral-500">
|
||||
<span className="lam-param-key truncate font-bold text-rose-700">{p.key}</span>
|
||||
<span className="lam-param-val truncate text-[10px] text-neutral-500">
|
||||
{getParamSourceLabel(p)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -427,15 +437,15 @@ export default function LightAppManagementPage() {
|
||||
{app.enabled ? (
|
||||
<>
|
||||
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
||||
<span className="lam-status-on-ping absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="lam-status-on-dot relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
||||
</span>
|
||||
<span className="text-[12.5px] font-bold text-emerald-700">已启用</span>
|
||||
<span className="lam-status-on text-[12.5px] font-bold text-emerald-700">已启用</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="h-2.5 w-2.5 shrink-0 rounded-full bg-neutral-300" />
|
||||
<span className="text-[12.5px] font-medium text-neutral-400">已停用</span>
|
||||
<span className="lam-status-off-dot h-2.5 w-2.5 shrink-0 rounded-full bg-neutral-300" />
|
||||
<span className="lam-status-off text-[12.5px] font-medium text-neutral-400">已停用</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -445,14 +455,14 @@ export default function LightAppManagementPage() {
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
<button
|
||||
onClick={() => openEdit(app)}
|
||||
className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
|
||||
className="lam-icon-btn 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-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
|
||||
className="lam-icon-btn lam-icon-btn-danger 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" />
|
||||
@@ -466,15 +476,15 @@ export default function LightAppManagementPage() {
|
||||
<tr>
|
||||
<td colSpan={7} className="py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center space-y-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-neutral-100 text-neutral-300">
|
||||
<div className="lam-empty-icon flex h-12 w-12 items-center justify-center rounded-full bg-neutral-100 text-neutral-300">
|
||||
<Info className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-semibold text-neutral-400">
|
||||
<p className="lam-empty-title text-[14px] font-semibold text-neutral-400">
|
||||
{isLoading ? "加载中..." : "暂无匹配的组件应用"}
|
||||
</p>
|
||||
{!isLoading && (
|
||||
<p className="text-[12px] text-neutral-300">
|
||||
<p className="lam-empty-sub text-[12px] text-neutral-300">
|
||||
{search ? "请确认搜索关键词输入是否正确" : "点击右上角“注册应用”添加第一款轻应用"}
|
||||
</p>
|
||||
)}
|
||||
@@ -487,15 +497,15 @@ export default function LightAppManagementPage() {
|
||||
</table>
|
||||
</div>
|
||||
) : tableApps.length === 0 ? (
|
||||
<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">
|
||||
<div className="lam-table-wrap rounded-[5px] bg-white py-20 text-center">
|
||||
<div className="lam-empty-icon 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>
|
||||
<p className="text-[14px] font-semibold text-neutral-400">
|
||||
<p className="lam-empty-title text-[14px] font-semibold text-neutral-400">
|
||||
{isLoading ? "加载中..." : "暂无匹配的组件应用"}
|
||||
</p>
|
||||
{!isLoading && (
|
||||
<p className="mt-1 text-[12px] text-neutral-300">
|
||||
<p className="lam-empty-sub mt-1 text-[12px] text-neutral-300">
|
||||
{search ? "请确认搜索关键词输入是否正确" : "点击右上角“注册应用”添加第一款轻应用"}
|
||||
</p>
|
||||
)}
|
||||
@@ -505,7 +515,7 @@ export default function LightAppManagementPage() {
|
||||
{tableApps.map((app) => (
|
||||
<div
|
||||
key={app.appId}
|
||||
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"
|
||||
className="lam-card 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 ${
|
||||
@@ -518,38 +528,38 @@ 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-[5px] border border-rose-100 bg-rose-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-rose-700">
|
||||
<span className="lam-tag-window 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-[5px] border border-sky-100 bg-sky-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-sky-700">
|
||||
<span className="lam-tag-iframe 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>
|
||||
)}
|
||||
<h4 className="line-clamp-1 text-sm font-bold leading-snug text-neutral-800 transition-colors group-hover:text-[#F5222D]">
|
||||
<h4 className="lam-card-title line-clamp-1 text-sm font-bold leading-snug text-neutral-800 transition-colors group-hover:text-[#F5222D]">
|
||||
{app.name}
|
||||
</h4>
|
||||
</div>
|
||||
{app.enabled ? (
|
||||
<span className="inline-flex shrink-0 items-center space-x-1 text-[11px] font-bold text-emerald-700">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-500" />
|
||||
<span className="lam-status-on inline-flex shrink-0 items-center space-x-1 text-[11px] font-bold text-emerald-700">
|
||||
<span className="lam-status-on-dot h-2 w-2 rounded-full bg-emerald-500" />
|
||||
<span>已启用</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex shrink-0 items-center space-x-1 text-[11px] font-medium text-neutral-400">
|
||||
<span className="h-2 w-2 rounded-full bg-neutral-300" />
|
||||
<span className="lam-status-off inline-flex shrink-0 items-center space-x-1 text-[11px] font-medium text-neutral-400">
|
||||
<span className="lam-status-off-dot h-2 w-2 rounded-full bg-neutral-300" />
|
||||
<span>已停用</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="line-clamp-2 min-h-[2.25rem] text-xs leading-relaxed text-neutral-500">
|
||||
<p className="lam-card-desc line-clamp-2 min-h-[2.25rem] text-xs leading-relaxed text-neutral-500">
|
||||
{app.description || "暂无应用详细描述说明。"}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-1.5 rounded-[5px] bg-neutral-50/60 p-2 font-mono text-[11px] text-neutral-600">
|
||||
<div className="lam-card-url-box 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}
|
||||
@@ -568,7 +578,7 @@ export default function LightAppManagementPage() {
|
||||
{app.routeParams.map((p, idx) => (
|
||||
<span
|
||||
key={`${app.appId}-card-${idx}`}
|
||||
className="rounded-[5px] border border-neutral-200/40 bg-neutral-100 px-1.5 py-0.5 font-mono text-[10px] text-neutral-500"
|
||||
className="lam-card-param 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>
|
||||
@@ -577,24 +587,24 @@ export default function LightAppManagementPage() {
|
||||
)}
|
||||
|
||||
{app.appType === "iframe" && app.mountLocation && (
|
||||
<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">
|
||||
<div className="lam-card-mount 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-1 border-t border-neutral-100/60 bg-neutral-50/30 px-5 py-3">
|
||||
<div className="lam-card-footer 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-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600"
|
||||
className="lam-icon-btn 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-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600"
|
||||
className="lam-icon-btn lam-icon-btn-danger 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" />
|
||||
@@ -607,21 +617,21 @@ export default function LightAppManagementPage() {
|
||||
</div>
|
||||
|
||||
{viewMode === "table" && (
|
||||
<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">
|
||||
<div className="lam-tip-box select-none rounded-[5px] bg-amber-50/40 p-5 shadow-[0_1px_2px_rgba(0,0,0,0.005)]">
|
||||
<div className="lam-tip-head 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>
|
||||
</div>
|
||||
<ul className="mt-3.5 space-y-3 pl-1 text-[12px] leading-relaxed text-neutral-600">
|
||||
<ul className="lam-tip-list mt-3.5 space-y-3 pl-1 text-[12px] leading-relaxed text-neutral-600">
|
||||
<li className="flex items-start space-x-2">
|
||||
<span className="mt-1 shrink-0 text-amber-600">•</span>
|
||||
<span className="lam-tip-bullet mt-1 shrink-0 text-amber-600">•</span>
|
||||
<span>
|
||||
当您注册 <strong className="font-bold text-neutral-800">iframe 嵌套</strong>{" "}
|
||||
应用时,您需要在菜单一栏指定分类(如“资源管理”),组件将直接作为新导航菜单项加入该分类。
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start space-x-2">
|
||||
<span className="mt-1 shrink-0 text-amber-600">•</span>
|
||||
<span className="lam-tip-bullet mt-1 shrink-0 text-amber-600">•</span>
|
||||
<span>
|
||||
当路由挂载成功,点击左侧注入的子栏名称可在主区域调起{" "}
|
||||
<strong className="font-bold text-neutral-800">真实 Iframe 预览器</strong>
|
||||
@@ -636,23 +646,23 @@ export default function LightAppManagementPage() {
|
||||
{dialogOpen && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center overflow-y-auto select-none">
|
||||
<div
|
||||
className="fixed inset-0 bg-[#3D3D3D]/40 backdrop-blur-[1px]"
|
||||
className="lam-dialog-overlay fixed inset-0 bg-[#3D3D3D]/40 backdrop-blur-[1px]"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
/>
|
||||
|
||||
<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="lam-dialog 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="lam-dialog-header 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-[5px] bg-rose-50 p-1.5 text-[#A8071A]">
|
||||
<span className="lam-dialog-icon 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]">
|
||||
<h2 className="lam-dialog-title text-[17px] font-bold text-[#222222]">
|
||||
{editing ? "修改轻应用" : "注册应用"}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setDialogOpen(false)}
|
||||
className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
|
||||
className="lam-dialog-close cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -660,8 +670,8 @@ export default function LightAppManagementPage() {
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-y-auto py-4 pr-1 text-[#222222]">
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="font-bold text-[#F5222D]">*</span>
|
||||
<label className="lam-form-label flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="lam-required font-bold text-[#F5222D]">*</span>
|
||||
<Layers className="h-4 w-4 text-[#A6A6A6]" />
|
||||
<span>应用类型</span>
|
||||
</label>
|
||||
@@ -672,7 +682,7 @@ export default function LightAppManagementPage() {
|
||||
popupProps={DIALOG_SELECT_POPUP}
|
||||
onChange={(v) => patchForm({ appType: v as LightAppType })}
|
||||
/>
|
||||
<p className="text-[11.5px] leading-relaxed text-[#A6A6A6]">
|
||||
<p className="lam-form-hint text-[11.5px] leading-relaxed text-[#A6A6A6]">
|
||||
{form.appType === "window_open"
|
||||
? "在新标签页或卡片式路由容器打开。多用于外部高度复杂的系统跳转。"
|
||||
: "通过标准 iframe 嵌入页面,可直接挂载到左侧导航树相应分类内。"}
|
||||
@@ -680,8 +690,8 @@ export default function LightAppManagementPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="font-bold text-[#F5222D]">*</span>
|
||||
<label className="lam-form-label flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="lam-required font-bold text-[#F5222D]">*</span>
|
||||
<Tag className="h-4 w-4 text-[#A6A6A6]" />
|
||||
<span>应用名称</span>
|
||||
</label>
|
||||
@@ -689,13 +699,13 @@ export default function LightAppManagementPage() {
|
||||
value={form.name}
|
||||
onChange={(e) => patchForm({ name: e.target.value })}
|
||||
placeholder="如: 舆情看板"
|
||||
className="h-9 rounded-[5px] border-[#DBDBDB] text-[14px]"
|
||||
className="lam-input h-9 rounded-[5px] border-[#DBDBDB] text-[14px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="font-bold text-[#F5222D]">*</span>
|
||||
<label className="lam-form-label flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="lam-required font-bold text-[#F5222D]">*</span>
|
||||
<Link2 className="h-4 w-4 text-[#A6A6A6]" />
|
||||
<span>应用地址</span>
|
||||
</label>
|
||||
@@ -703,14 +713,14 @@ 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-[5px] border-[#DBDBDB] font-mono text-[14px]"
|
||||
className="lam-input h-9 rounded-[5px] border-[#DBDBDB] font-mono text-[14px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{form.appType === "iframe" && (
|
||||
<div className="animate-in fade-in space-y-1.5">
|
||||
<label className="flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="font-bold text-[#F5222D]">*</span>
|
||||
<label className="lam-form-label flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="lam-required font-bold text-[#F5222D]">*</span>
|
||||
<FolderTree className="h-4 w-4 text-[#A6A6A6]" />
|
||||
<span>挂载菜单</span>
|
||||
</label>
|
||||
@@ -726,7 +736,7 @@ export default function LightAppManagementPage() {
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<label className="lam-form-label flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="w-2 shrink-0" />
|
||||
<AlignLeft className="h-4 w-4 text-[#A6A6A6]" />
|
||||
<span>应用描述 (可选)</span>
|
||||
@@ -736,13 +746,13 @@ export default function LightAppManagementPage() {
|
||||
onChange={(e) => patchForm({ description: e.target.value })}
|
||||
placeholder="简要说明该应用的用途"
|
||||
rows={2}
|
||||
className="resize-none rounded-[5px] border-[#DBDBDB] text-[14px]"
|
||||
className="lam-input resize-none rounded-[5px] border-[#DBDBDB] text-[14px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5 pt-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<span className="lam-form-label flex items-center space-x-1.5 text-[14px] font-medium">
|
||||
<Sliders className="h-4 w-4 text-[#A6A6A6]" />
|
||||
<span>路由参数</span>
|
||||
<span
|
||||
@@ -755,7 +765,7 @@ export default function LightAppManagementPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={addParam}
|
||||
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]"
|
||||
className="lam-add-param-btn 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>
|
||||
@@ -763,14 +773,14 @@ export default function LightAppManagementPage() {
|
||||
</div>
|
||||
|
||||
{form.routeParams.length > 0 ? (
|
||||
<div className="max-h-[220px] space-y-2 overflow-y-auto rounded-[5px] border border-[#E8E8E8] bg-[#FAFAFA] p-1">
|
||||
<div className="lam-param-panel 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-[5px] border border-[#E8E8E8] bg-white p-2"
|
||||
className="lam-param-row relative rounded-[5px] border border-[#E8E8E8] bg-white p-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
@@ -779,7 +789,7 @@ export default function LightAppManagementPage() {
|
||||
value={p.key}
|
||||
onChange={(e) => patchParam(idx, { key: e.target.value })}
|
||||
placeholder="参数名 key"
|
||||
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]"
|
||||
className="lam-input 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>
|
||||
|
||||
@@ -817,7 +827,7 @@ export default function LightAppManagementPage() {
|
||||
patchParam(idx, { customValue: e.target.value })
|
||||
}
|
||||
placeholder="自定义值"
|
||||
className="h-8 w-[120px] rounded-[5px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none"
|
||||
className="lam-input h-8 w-[120px] rounded-[5px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -826,7 +836,7 @@ export default function LightAppManagementPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeParam(idx)}
|
||||
className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
|
||||
className="lam-icon-btn lam-icon-btn-danger cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -834,8 +844,8 @@ export default function LightAppManagementPage() {
|
||||
|
||||
{/* 主题参数:逐主题色配置传给目标应用的值(左=系统主题色,右=目标应用值,可改) */}
|
||||
{p.paramType === "theme" && (
|
||||
<div className="mt-2 space-y-1.5 rounded-[5px] bg-[#FAFAFA] p-2">
|
||||
<p className="text-[11px] leading-relaxed text-[#A6A6A6]">
|
||||
<div className="lam-param-theme-box mt-2 space-y-1.5 rounded-[5px] bg-[#FAFAFA] p-2">
|
||||
<p className="lam-form-hint text-[11px] leading-relaxed text-[#A6A6A6]">
|
||||
各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改)
|
||||
</p>
|
||||
{APP_THEME_COLORS.map((c) => (
|
||||
@@ -862,7 +872,7 @@ export default function LightAppManagementPage() {
|
||||
})
|
||||
}
|
||||
placeholder={c.key}
|
||||
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]"
|
||||
className="lam-input 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>
|
||||
))}
|
||||
@@ -873,19 +883,19 @@ export default function LightAppManagementPage() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[5px] border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
|
||||
<div className="lam-param-empty rounded-[5px] border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
|
||||
暂无注入的路由参数,加载地址时不传递多余拼接。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-[#E8E8E8] py-2.5">
|
||||
<span className="text-[14px] font-medium text-[#222222]">立即启用该应用</span>
|
||||
<div className="lam-dialog-footer flex items-center justify-between border-t border-[#E8E8E8] py-2.5">
|
||||
<span className="lam-form-label text-[14px] font-medium text-[#222222]">立即启用该应用</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => patchForm({ enabled: !form.enabled })}
|
||||
className={`relative h-[22px] w-10 cursor-pointer rounded-full p-0.5 transition-colors duration-200 ${
|
||||
form.enabled ? "bg-[#F5222D]" : "bg-[#DBDBDB]"
|
||||
form.enabled ? "lam-switch-on bg-[#F5222D]" : "lam-switch-off bg-[#DBDBDB]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
@@ -897,12 +907,12 @@ export default function LightAppManagementPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center justify-end space-x-3 border-t border-[#E8E8E8] pt-4">
|
||||
<div className="lam-dialog-footer flex flex-shrink-0 items-center justify-end space-x-3 border-t border-[#E8E8E8] pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={saving}
|
||||
onClick={() => setDialogOpen(false)}
|
||||
className="h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
|
||||
className="lam-dialog-cancel h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
@@ -910,7 +920,7 @@ export default function LightAppManagementPage() {
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={handleSave}
|
||||
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"
|
||||
className="lam-dialog-save 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>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
|
||||
import {
|
||||
GENERAL_QA_ROUTE,
|
||||
loginByYUserId,
|
||||
persistAccessToken,
|
||||
persistToolsToken,
|
||||
} from '@/core/auth/yuser-login';
|
||||
import {
|
||||
@@ -53,6 +54,8 @@ export default function LoginPage() {
|
||||
// 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage
|
||||
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
||||
const toolToken = searchParams.get('token')?.trim() ?? '';
|
||||
// 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参
|
||||
const accessToken = searchParams.get('access_token')?.trim() ?? '';
|
||||
// 用户名直登口令门:开启后端开关时,口令从 ?password= 透传
|
||||
const gatePassword = searchParams.get('password') ?? '';
|
||||
// token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开)
|
||||
@@ -137,6 +140,9 @@ export default function LoginPage() {
|
||||
setStoredAuth(auth);
|
||||
// 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖)
|
||||
setStoredUserName(loginUserName);
|
||||
// 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后),
|
||||
// XK/PG 中任务 iframe 据此取 accessToken
|
||||
persistAccessToken(accessToken);
|
||||
persistSidebarEnabled(sidebarEnabled);
|
||||
setStatus('success');
|
||||
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
|
||||
@@ -161,6 +167,7 @@ export default function LoginPage() {
|
||||
sidebarEnabled,
|
||||
yUserId,
|
||||
toolToken,
|
||||
accessToken,
|
||||
onlineTicket,
|
||||
authToken,
|
||||
gatePassword,
|
||||
|
||||
@@ -5,9 +5,11 @@ import { PageSidebar } from "@/components/page-sidebar";
|
||||
import { PageSidebarDark } from "@/components/page-sidebar-dark";
|
||||
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
|
||||
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
|
||||
import { isWujieEnv } from "@/core/embed/wujie";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu";
|
||||
import { useLayoutMode } from "@/core/system-settings/hooks";
|
||||
|
||||
import WorkspaceRoutes from "./WorkspaceRoutes";
|
||||
import StrategyRoutes from "./StrategyRoutes";
|
||||
@@ -22,11 +24,16 @@ function PageLayout() {
|
||||
const location = useLocation();
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [settings] = useLocalSettings();
|
||||
// 界面模式开关(外观设置):用户显式选「全量/简洁」时以其为准;未设置过则回退到
|
||||
// 会话级 sidebarEnabled(外部门户/yUserId 登录时置位)这一旧默认。这样无论是否走门户
|
||||
// 登录,「全量↔简洁」切换都能真正控制顶栏+侧边栏的显示。
|
||||
const layoutMode = settings.display.layoutMode;
|
||||
// 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。
|
||||
const inWujie = useMemo(() => isWujieEnv(), []);
|
||||
const lightNarrowSidebar = !isDark && !inWujie;
|
||||
// 浅色窄栏在右侧展开「三级列」时整体变宽(96→236px),主内容左内边距随之过渡。
|
||||
const thirdLevel = useActiveThirdLevel();
|
||||
const lightExpanded = lightNarrowSidebar && thirdLevel.length > 0;
|
||||
// 界面模式(外观设置):用户显式选「全量/简洁」时以其为准;否则跟随管理员系统默认
|
||||
// (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId
|
||||
// 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。
|
||||
const [layoutMode] = useLayoutMode();
|
||||
const chromeByPreference =
|
||||
layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled;
|
||||
const pageSidebarAllowed =
|
||||
@@ -61,8 +68,11 @@ function PageLayout() {
|
||||
// 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。
|
||||
// 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐
|
||||
? (collapsed ? "pl-0" : "pl-[118px]")
|
||||
// 浅色仍保留过渡动画(pl-16 ↔ pl-64 平滑收展)
|
||||
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}`
|
||||
: lightNarrowSidebar
|
||||
// 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡
|
||||
? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}`
|
||||
// 浅色旧版手风琴(wujie 嵌入):pl-16 ↔ pl-64 平滑收展
|
||||
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}`
|
||||
}`}
|
||||
>
|
||||
<Routes>
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
LayoutDashboard,
|
||||
Loader2,
|
||||
ScrollText,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useParams } from "react-router-dom";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -21,6 +23,12 @@ import {
|
||||
} from "@/core/roundtable-draft-shares/api";
|
||||
import { usePublicBackground } from "@/core/embed/background";
|
||||
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||
import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp";
|
||||
import { adaptReportData } from "@/roundtable-planning/dashboard/data-context";
|
||||
import { parseReportJson } from "@/roundtable-planning/lib/report-template";
|
||||
|
||||
/** 该分享页支持的三种视图:落地汇总 / 结果绘制(经典 HTML) / 大屏多页(模式二)。 */
|
||||
export type RoundtableShareView = "landing" | "report" | "dashboard";
|
||||
|
||||
/** 各席位最终交付 —— 折叠手风琴(仿 HighFidelityReport 第二区块)。 */
|
||||
function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) {
|
||||
@@ -81,20 +89,22 @@ function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) {
|
||||
* Public, read-only roundtable-result viewer.
|
||||
*
|
||||
* Reachable at `#/roundtable-share/<code>` with NO authentication — anyone with
|
||||
* the link can open it. Two views, driven by the `report` prop / route:
|
||||
* the link can open it. Three views, driven by the `view` prop / route:
|
||||
*
|
||||
* - landing (`#/roundtable-share/<code>`): a read-only result page mirroring the
|
||||
* in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」
|
||||
* button that opens the full report in a NEW TAB (when one was generated).
|
||||
* - report (`#/roundtable-share/<code>/report`): the Step 3 report (a
|
||||
* in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终
|
||||
* 结论 + 各席位最终交付.
|
||||
* - report (`#/roundtable-share/<code>/report`): the Step 3 经典 HTML 看板 (a
|
||||
* self-contained HTML document) rendered full-screen inside a sandboxed
|
||||
* `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute
|
||||
* code in the app origin.
|
||||
* `iframe` (no `allow-scripts`).
|
||||
* - dashboard (`#/roundtable-share/<code>/dashboard`): the Step 3 大屏多页 (模式二)
|
||||
* — rendered on the fly from the stored structured data via `renderDashboardHtml`
|
||||
* into the same sandboxed `iframe`.
|
||||
*/
|
||||
export default function PublicRoundtableSharePage({
|
||||
report = false,
|
||||
view = "landing",
|
||||
}: {
|
||||
report?: boolean;
|
||||
view?: RoundtableShareView;
|
||||
}) {
|
||||
const { shareCode } = useParams<{ shareCode: string }>();
|
||||
// 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。
|
||||
@@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
// 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。
|
||||
// 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与
|
||||
// Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。
|
||||
const dashboardData = useMemo(() => {
|
||||
const json = data?.dashboard_json;
|
||||
if (!json) return undefined;
|
||||
const parsed = parseReportJson(json);
|
||||
if (!parsed) return undefined;
|
||||
return adaptReportData(parsed, data?.generated_at ?? "");
|
||||
}, [data?.dashboard_json, data?.generated_at]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-background text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm" style={pageBg}>
|
||||
@@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({
|
||||
);
|
||||
}
|
||||
|
||||
// ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ──────────────────────────────
|
||||
if (report) {
|
||||
// ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ──
|
||||
if (view === "dashboard") {
|
||||
return (
|
||||
<div className="bg-background h-screen w-screen overflow-auto" style={pageBg}>
|
||||
{dashboardData ? (
|
||||
<DashboardApp data={dashboardData} />
|
||||
) : (
|
||||
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm">
|
||||
<p className="text-base font-medium text-foreground">暂无大屏多页</p>
|
||||
<p>这次会商还没有生成第三步「大屏多页」的方案报告。</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ─────────────────
|
||||
if (view === "report") {
|
||||
return (
|
||||
<div className="bg-background flex h-screen flex-col" style={pageBg}>
|
||||
<header className="flex shrink-0 items-center gap-2 border-b px-6 py-3">
|
||||
@@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({
|
||||
);
|
||||
}
|
||||
|
||||
// ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)─────
|
||||
const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`;
|
||||
// ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ──
|
||||
const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`;
|
||||
const reportUrl = `${shareBase}/report`;
|
||||
const dashboardUrl = `${shareBase}/dashboard`;
|
||||
|
||||
return (
|
||||
<div className="bg-[#fef6f6] flex h-screen flex-col text-slate-800" style={pageBg}>
|
||||
@@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-6xl space-y-6 p-6">
|
||||
{/* 1. 结果绘制 —— 新标签打开报告页 */}
|
||||
{/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-primary" />
|
||||
结果绘制
|
||||
</h4>
|
||||
{data.has_report ? (
|
||||
<a
|
||||
href={reportUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-primary text-white hover:bg-[var(--brand-900)] inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm shadow-primary/10"
|
||||
>
|
||||
<BarChart3 className="size-4" />
|
||||
查看结果绘制
|
||||
<ExternalLink className="size-3.5 opacity-80" />
|
||||
</a>
|
||||
{data.has_report || data.has_dashboard ? (
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{data.has_report && (
|
||||
<a
|
||||
href={reportUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-primary text-white hover:bg-[var(--brand-900)] inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm shadow-primary/10"
|
||||
>
|
||||
<BarChart3 className="size-4" />
|
||||
查看结果绘制
|
||||
<ExternalLink className="size-3.5 opacity-80" />
|
||||
</a>
|
||||
)}
|
||||
{data.has_dashboard && (
|
||||
<a
|
||||
href={dashboardUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-white text-primary border border-primary/30 hover:bg-primary/5 inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm"
|
||||
>
|
||||
<LayoutDashboard className="size-4" />
|
||||
查看大屏多页
|
||||
<ExternalLink className="size-3.5 opacity-80" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-slate-400 italic">
|
||||
这次会商还没有生成第三步「结果绘制」的方案报告。
|
||||
@@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 2. 总控最终结论 */}
|
||||
{/* 2. 方案总结报告(Markdown) */}
|
||||
{data.has_summary_report && (
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<ScrollText className="w-4 h-4 text-primary" />
|
||||
方案总结报告
|
||||
</h4>
|
||||
<div className="text-xs text-slate-700 leading-relaxed">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{data.summary_report_md}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 3. 总控最终结论 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||
@@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 3. 各席位最终交付 */}
|
||||
{/* 4. 各席位最终交付 */}
|
||||
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-primary" />
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
@import "../../docs/ui规范-深蓝/tokens.css";
|
||||
@import "./light-app-pages.css";
|
||||
|
||||
/**
|
||||
* 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》)
|
||||
* 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。
|
||||
*/
|
||||
.dark .light-app-management-page {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-title {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-desc {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-desc code {
|
||||
background: rgba(22, 119, 255, 0.12);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-primary-btn {
|
||||
background: var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-primary-btn:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-primary-btn:active {
|
||||
background: var(--color-primary-active);
|
||||
}
|
||||
|
||||
/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-toolbar {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input:hover {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input:focus {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle-btn.is-active {
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-link);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-count {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-count strong {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
/* ── 表格(§28)──────────────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-table-wrap {
|
||||
background: var(--color-bg-page);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table thead tr {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-row-name {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tag-window {
|
||||
border-color: rgba(245, 34, 45, 0.35);
|
||||
background: var(--func-error-bg);
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tag-iframe {
|
||||
border-color: rgba(22, 119, 255, 0.35);
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-url {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table tbody tr:hover .lam-url {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-url a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-url a:hover {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-mount-tag {
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-mount-dot {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-btn {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-btn:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-popover {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-card);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-popover-head {
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-popover-close {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-item {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-key {
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-val {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-on {
|
||||
color: var(--func-success);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-on-dot {
|
||||
background: var(--func-success);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-on-ping {
|
||||
background: var(--func-success);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-off {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-status-off-dot {
|
||||
background: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-icon-btn {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-icon-btn:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-icon-btn-danger:hover {
|
||||
background: var(--func-error-bg);
|
||||
color: var(--func-error);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-empty-icon {
|
||||
background: var(--color-bg-page);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-empty-title {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-empty-sub {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card:hover {
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-title {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card:hover .lam-card-title {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-desc {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-url-box {
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-param {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-mount {
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-footer {
|
||||
border-color: var(--color-border);
|
||||
background: rgba(12, 24, 54, 0.5);
|
||||
}
|
||||
|
||||
/* ── 底部提示(§33 警告)────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-tip-box {
|
||||
background: var(--func-warning-bg);
|
||||
border: 1px solid rgba(250, 173, 20, 0.25);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-head {
|
||||
border-color: rgba(250, 173, 20, 0.2);
|
||||
color: var(--func-warning);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-list {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-list strong {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-tip-bullet {
|
||||
color: var(--func-warning);
|
||||
}
|
||||
|
||||
/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .lam-dialog-overlay {
|
||||
background: rgba(6, 14, 36, 0.6);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog {
|
||||
border-color: var(--color-border);
|
||||
background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page));
|
||||
box-shadow: var(--shadow-3);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-header {
|
||||
border-color: var(--color-border);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-header::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
background: var(--gradient-title-line);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-icon {
|
||||
background: var(--func-info-bg);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-title {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-close {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-close:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-label .lam-required {
|
||||
color: var(--func-error);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-label svg {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-form-hint {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog .lam-input,
|
||||
.dark .light-app-management-page .lam-dialog input[type="text"] {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog .lam-input::placeholder,
|
||||
.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog .lam-input:focus,
|
||||
.dark .light-app-management-page .lam-dialog input[type="text"]:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-panel {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-row {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-theme-box {
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-empty {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-bg-page);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-add-param-btn {
|
||||
border-color: var(--color-border-strong);
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-add-param-btn:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-footer {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-cancel {
|
||||
border-color: var(--color-border-strong);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-cancel:hover {
|
||||
border-color: var(--color-link);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-save {
|
||||
background: var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-save:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog-save:active {
|
||||
background: var(--color-primary-active);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-switch-on {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-switch-off {
|
||||
background: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
/* ── TDesign Select(§15)──────────────────────────────────────────── */
|
||||
.dark .light-app-management-page .t-select .t-input,
|
||||
.dark .light-app-management-page .t-select .t-input__wrap {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
border-radius: var(--radius-input);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select .t-input__inner {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select .t-input__inner::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select:hover .t-input,
|
||||
.dark .light-app-management-page .t-select:hover .t-input__wrap {
|
||||
border-color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select.t-is-focused .t-input,
|
||||
.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select .t-fake-arrow {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select__dropdown {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select-option {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select-option:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .t-select-option.t-is-selected {
|
||||
background: rgba(22, 119, 255, 0.16);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
/* shadcn Input / Textarea in dialog */
|
||||
.dark .light-app-management-page .lam-dialog [data-slot="input"],
|
||||
.dark .light-app-management-page .lam-dialog textarea {
|
||||
background: var(--color-bg-page);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible,
|
||||
.dark .light-app-management-page .lam-dialog textarea:focus-visible {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-param-theme-box span {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* TDesign 下拉挂载到 body 时的深蓝样式 */
|
||||
.dark .lam-select-popup.t-popup {
|
||||
background: var(--color-bg-card);
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.dark .lam-select-popup .t-select-option {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dark .lam-select-popup .t-select-option:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .lam-select-popup .t-select-option.t-is-selected {
|
||||
background: rgba(22, 119, 255, 0.16);
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-search-input + button,
|
||||
.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-table .text-neutral-300 {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-url-box button {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dark .light-app-management-page .lam-card-url-box button:hover {
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
color: var(--color-link);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@import "../../docs/ui规范-深蓝/tokens.css";
|
||||
|
||||
/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */
|
||||
.dark .light-app-management-page,
|
||||
.dark .light-app-list-page {
|
||||
background-color: var(--color-bg-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;
|
||||
}
|
||||
@@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
||||
filepath={fileSandboxVirtualUrl}
|
||||
threadId={fileSandboxTarget.threadId}
|
||||
onClose={handleCloseFileSandbox}
|
||||
allowVirtualMarkdownEdit
|
||||
/>
|
||||
</ThreadContext.Provider>
|
||||
)}
|
||||
|
||||
@@ -115,6 +115,7 @@ export function Step2SandboxLayout({
|
||||
filepath={selectedArtifact}
|
||||
threadId={previewThreadId}
|
||||
onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined}
|
||||
allowVirtualMarkdownEdit
|
||||
/>
|
||||
) : (
|
||||
<div className="relative flex size-full justify-center">
|
||||
|
||||
@@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
return email ? email.split("@")[0] : "用户";
|
||||
}, [auth?.email]);
|
||||
const showTopNav = isDark && !collapsed && !isWujieEnv();
|
||||
// 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。
|
||||
// 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏,
|
||||
// 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。
|
||||
const showLightTopNav = !isDark && !isWujieEnv();
|
||||
|
||||
const hasActiveChild = (item: MenuItem): boolean => {
|
||||
if (item.path && pathname === item.path) {
|
||||
@@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderLightTopItem = (item: MenuItem) => {
|
||||
const isSelected = item.path ? pathname === item.path : hasActiveChild(item);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`light-topnav-item ${isSelected ? "light-topnav-item--active" : ""}`}
|
||||
onClick={() => handleNavItemClick(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用
|
||||
const actionCluster = (
|
||||
<>
|
||||
@@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : showLightTopNav ? (
|
||||
<>
|
||||
{/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
|
||||
<div className="flex h-full min-w-0 flex-1 items-center">
|
||||
<div className="flex flex-shrink-0 items-center gap-2 pr-1">
|
||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
|
||||
<FileText className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="whitespace-nowrap text-base font-semibold text-white">
|
||||
CM助手
|
||||
</span>
|
||||
</div>
|
||||
<nav className="light-topnav" aria-label="一级菜单">
|
||||
{visibleMenuItems.map(renderLightTopItem)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2 pl-2">
|
||||
{actionCluster}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex h-full min-w-0 flex-1 items-center gap-3">
|
||||
@@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
{/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */}
|
||||
{showTopNav && thirdSubNavItems.length ? (
|
||||
<nav className="third-submenu-bar" aria-label="三级菜单">
|
||||
<div className="third-submenu-list">
|
||||
|
||||
@@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string {
|
||||
return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value;
|
||||
}
|
||||
|
||||
/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */
|
||||
/**
|
||||
* 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。
|
||||
* accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键);
|
||||
* userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。
|
||||
*/
|
||||
export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [
|
||||
{ label: "accessToken", value: "accessToken" },
|
||||
{ label: "yUserId", value: "yUserId" },
|
||||
{ label: "userName", value: "userName" },
|
||||
{ label: "gateDepartName", value: "gateDepartName" },
|
||||
{ label: "typeName", value: "typeName" },
|
||||
{ label: "其他(自定义值)", value: "other" },
|
||||
];
|
||||
|
||||
|
||||
@@ -932,7 +932,7 @@ body {
|
||||
.strategy-app-header {
|
||||
background-color: transparent;
|
||||
background-image: url("../assets/img/red-header.png");
|
||||
background-size: 100% 100%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -115,10 +115,17 @@ class PublicDraftShareSnapshotResponse(BaseModel):
|
||||
seat_deliveries: list[SeatDeliveryItem] = Field(default_factory=list)
|
||||
# The Step 3「结果绘制」report — a self-contained HTML document. Empty when the
|
||||
# shared draft never reached / generated a result report.
|
||||
html: str = Field(default="", description="Self-contained Step 3 result report HTML")
|
||||
html: str = Field(default="", description="Self-contained Step 3 result report HTML (经典 HTML 看板)")
|
||||
summary: str | None = Field(default=None, description="The report's closing one-liner")
|
||||
generated_at: str | None = Field(default=None, description="When the report was generated (ISO-8601)")
|
||||
has_report: bool = Field(default=False, description="Whether a Step 3 report exists")
|
||||
has_report: bool = Field(default=False, description="Whether a Step 3 HTML report exists")
|
||||
# Step 3「方案总结报告」—— the Markdown report from roundtable-summary (step3.summaryReport.md).
|
||||
summary_report_md: str = Field(default="", description="Step 3 Markdown 方案总结报告")
|
||||
has_summary_report: bool = Field(default=False, description="Whether a Step 3 Markdown summary report exists")
|
||||
# Step 3「大屏多页」(模式二) —— structured dashboard data (step3.dashboardJson); the
|
||||
# frontend renders it to self-contained HTML via renderDashboardHtml at view time.
|
||||
dashboard_json: str | None = Field(default=None, description="Step 3 大屏多页 structured data (report-json)")
|
||||
has_dashboard: bool = Field(default=False, description="Whether a Step 3 大屏多页 dashboard exists")
|
||||
created_at: str | None = Field(default=None, description="When the share was created (ISO-8601)")
|
||||
|
||||
|
||||
@@ -259,29 +266,32 @@ async def import_share(
|
||||
def _result_sections(draft: dict) -> tuple[str, list[dict[str, str]]]:
|
||||
"""Derive (总控最终结论, 各席位最终交付) from a draft's Step 2 snapshot.
|
||||
|
||||
Mirrors the frontend `Step3Panel`: the consensus is `step2.lastLeaderContent`
|
||||
and each seat delivery is a Step 2 dialogue bubble tagged
|
||||
``confidence == "子智能体交付"``. Falls back to the active run's snapshot when
|
||||
the top-level Step 2 fields are empty (multi-run drafts).
|
||||
Mirrors the frontend draft hydration (`RoundtablePlanningPage.normalizeStep2Runs`):
|
||||
when the draft has ``runs[]`` the rendered content is the **active run**
|
||||
(``activeRunId``), NOT the top-level ``step2`` fields — those can be a stale
|
||||
foreground seed (e.g. only the 系统消息/开场 2 条气泡) left over after a later
|
||||
background job wrote its results into ``runs[]``. Reading the top level there
|
||||
yields an empty consensus and no deliveries (the reported bug). Only drafts
|
||||
with no ``runs[]`` (legacy/foreground-only) fall back to the top-level fields.
|
||||
|
||||
The consensus is ``lastLeaderContent`` and each seat delivery is a dialogue
|
||||
bubble tagged ``confidence == "子智能体交付"``.
|
||||
"""
|
||||
step2 = draft.get("step2") if isinstance(draft.get("step2"), dict) else {}
|
||||
step2 = step2 or {}
|
||||
consensus = step2.get("lastLeaderContent") or ""
|
||||
dialogues = step2.get("step2RoundtableDialogues")
|
||||
|
||||
runs = [r for r in step2.get("runs", []) if isinstance(r, dict)] if isinstance(step2.get("runs"), list) else []
|
||||
if runs:
|
||||
active_id = step2.get("activeRunId")
|
||||
source = next((r for r in runs if r.get("id") == active_id), None) or runs[0]
|
||||
else:
|
||||
source = step2
|
||||
|
||||
consensus = source.get("lastLeaderContent") or ""
|
||||
dialogues = source.get("step2RoundtableDialogues")
|
||||
if not isinstance(dialogues, list):
|
||||
dialogues = []
|
||||
|
||||
# Multi-run drafts keep the freshest content on the active run; use it when
|
||||
# the top-level snapshot carries nothing.
|
||||
if not consensus and not dialogues and isinstance(step2.get("runs"), list):
|
||||
runs = [r for r in step2["runs"] if isinstance(r, dict)]
|
||||
active_id = step2.get("activeRunId")
|
||||
active = next((r for r in runs if r.get("id") == active_id), None) or (runs[0] if runs else None)
|
||||
if active:
|
||||
consensus = active.get("lastLeaderContent") or ""
|
||||
d = active.get("step2RoundtableDialogues")
|
||||
dialogues = d if isinstance(d, list) else []
|
||||
|
||||
seat_deliveries = [
|
||||
{"sender": str(d.get("sender") or ""), "content": str(d.get("content") or "")}
|
||||
for d in dialogues
|
||||
@@ -313,8 +323,14 @@ async def get_public_draft_share_snapshot(
|
||||
if draft is None:
|
||||
raise HTTPException(status_code=404, detail="The shared draft no longer exists")
|
||||
|
||||
step3 = draft.get("step3") if isinstance(draft.get("step3"), dict) else None
|
||||
html = (step3 or {}).get("html") or ""
|
||||
step3 = draft.get("step3") if isinstance(draft.get("step3"), dict) else {}
|
||||
step3 = step3 or {}
|
||||
html = step3.get("html") or ""
|
||||
# 方案总结报告(Markdown,模式:roundtable-summary)—— 嵌在 step3.summaryReport.md。
|
||||
summary_report = step3.get("summaryReport") if isinstance(step3.get("summaryReport"), dict) else {}
|
||||
summary_report_md = (summary_report or {}).get("md") or ""
|
||||
# 大屏多页(模式二)—— 只存结构化数据 step3.dashboardJson,前端按主题渲染成自包含 HTML。
|
||||
dashboard_json = step3.get("dashboardJson") or None
|
||||
consensus_text, seat_deliveries = _result_sections(draft)
|
||||
return PublicDraftShareSnapshotResponse(
|
||||
share_code=share_code,
|
||||
@@ -323,8 +339,12 @@ async def get_public_draft_share_snapshot(
|
||||
consensus_text=consensus_text,
|
||||
seat_deliveries=[SeatDeliveryItem(**s) for s in seat_deliveries],
|
||||
html=html,
|
||||
summary=(step3 or {}).get("summary"),
|
||||
generated_at=(step3 or {}).get("generatedAt"),
|
||||
summary=step3.get("summary"),
|
||||
generated_at=step3.get("generatedAt"),
|
||||
has_report=bool(html),
|
||||
summary_report_md=summary_report_md,
|
||||
has_summary_report=bool(summary_report_md),
|
||||
dashboard_json=dashboard_json,
|
||||
has_dashboard=bool(dashboard_json),
|
||||
created_at=share.get("created_at"),
|
||||
)
|
||||
@@ -311,12 +311,14 @@ class AppearanceResponse(BaseModel):
|
||||
default_theme: Literal["light", "dark", "lightblue"] = Field(..., description="System default theme — light, dark or lightblue")
|
||||
default_skill_view: Literal["card", "list"] = Field(..., description="System default skill list display mode — card or list")
|
||||
default_agent_view: Literal["card", "list"] = Field(..., description="System default agent list display mode — card or list")
|
||||
default_layout_mode: Literal["full", "compact"] = Field(..., description="System default interface layout — full (sidebar + header) or compact (hidden)")
|
||||
|
||||
|
||||
class AppearanceUpdateRequest(BaseModel):
|
||||
default_theme: Literal["light", "dark", "lightblue"] | None = Field(default=None)
|
||||
default_skill_view: Literal["card", "list"] | None = Field(default=None)
|
||||
default_agent_view: Literal["card", "list"] | None = Field(default=None)
|
||||
default_layout_mode: Literal["full", "compact"] | None = Field(default=None)
|
||||
|
||||
|
||||
def _appearance_response(appearance) -> AppearanceResponse:
|
||||
@@ -324,6 +326,7 @@ def _appearance_response(appearance) -> AppearanceResponse:
|
||||
default_theme=appearance.default_theme,
|
||||
default_skill_view=appearance.default_skill_view,
|
||||
default_agent_view=appearance.default_agent_view,
|
||||
default_layout_mode=appearance.default_layout_mode,
|
||||
)
|
||||
|
||||
|
||||
@@ -343,7 +346,7 @@ async def update_appearance_settings(
|
||||
current = load_system_settings()
|
||||
fields = body.model_dump(exclude_unset=True)
|
||||
update: dict[str, object] = {}
|
||||
for key in ("default_theme", "default_skill_view", "default_agent_view"):
|
||||
for key in ("default_theme", "default_skill_view", "default_agent_view", "default_layout_mode"):
|
||||
if key in fields and fields[key] is not None:
|
||||
update[key] = fields[key]
|
||||
if update:
|
||||
|
||||
@@ -59,6 +59,10 @@ class AppearanceSettings(BaseModel):
|
||||
default="card",
|
||||
description="Default agent list display mode for users who haven't picked their own (card / list)",
|
||||
)
|
||||
default_layout_mode: Literal["full", "compact"] = Field(
|
||||
default="full",
|
||||
description="Default interface layout for users who haven't picked their own — full (sidebar + header) / compact (hidden)",
|
||||
)
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user