样式修改
This commit is contained in:
1 parent
c890ae5a15
commit
27cfd53c06
12 files changed
+321
-56
No files matched your search
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
.item-nav {
|
.item-nav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 70px;
|
height: 75px;
|
||||||
margin-top: 13px;
|
margin-top: 13px;
|
||||||
font-family: Noto Sans S Chinese;
|
font-family: Noto Sans S Chinese;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 24px;
|
top: 29px;
|
||||||
width: 77px;
|
width: 77px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
background: url('../assets/icon/nav-line-logo.svg') no-repeat;
|
background: url('../assets/icon/nav-line-logo.svg') no-repeat;
|
||||||
@@ -238,3 +238,85 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn__icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex: 0 0 16px;
|
||||||
|
color: #80e9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn__label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--inline {
|
||||||
|
width: calc(100% - 6px);
|
||||||
|
min-height: 58px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
border: 1px solid #55a6fe33;
|
||||||
|
background: #000c1c26;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
box-shadow: inset 0 0 16px #00b9ff1f;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--inline .sidebar-collapse-btn__label {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--inline:hover {
|
||||||
|
background: rgba(0, 98, 217, 0.3);
|
||||||
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--floating {
|
||||||
|
position: fixed;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
z-index: 60;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(128, 233, 255, 0.35);
|
||||||
|
background: rgba(3, 38, 80, 0.82);
|
||||||
|
color: rgba(205, 245, 255, 0.95);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
transition: width 0.22s ease, opacity 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--floating:hover {
|
||||||
|
width: 104px;
|
||||||
|
background: rgba(7, 52, 110, 0.9);
|
||||||
|
border-color: rgba(128, 233, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--floating .sidebar-collapse-btn__label {
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
transition: max-width 0.22s ease, opacity 0.16s ease, transform 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapse-btn--floating:hover .sidebar-collapse-btn__label {
|
||||||
|
max-width: 64px;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { PanelLeftOpen, PanelLeftClose } from "lucide-react";
|
||||||
|
|
||||||
import { getStoredAuth } from "@/core/auth";
|
import { getStoredAuth } from "@/core/auth";
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,13 @@ import {
|
|||||||
} from "@/core/page-layout/sidebar-menu";
|
} from "@/core/page-layout/sidebar-menu";
|
||||||
import './page-sdebar.css'
|
import './page-sdebar.css'
|
||||||
|
|
||||||
export function PageSidebarDark() {
|
type PageSidebarDarkProps = {
|
||||||
|
/** 是否收起(true 时只渲染左下角浮动入口,sidebar 主体隐藏) */
|
||||||
|
collapsed: boolean;
|
||||||
|
onToggleCollapsed: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDarkProps) {
|
||||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||||
const visibleMenuItems = useMemo(
|
const visibleMenuItems = useMemo(
|
||||||
() => filterMenuItemsByRole(sidebarMenuItems, !!isAdmin),
|
() => filterMenuItemsByRole(sidebarMenuItems, !!isAdmin),
|
||||||
@@ -91,10 +98,37 @@ export function PageSidebarDark() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findFirstRoutePath = (items?: MenuItem[]): string | null => {
|
||||||
|
if (!items?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of items) {
|
||||||
|
if (child.path) {
|
||||||
|
return child.path;
|
||||||
|
}
|
||||||
|
const nestedPath = findFirstRoutePath(child.children);
|
||||||
|
if (nestedPath) {
|
||||||
|
return nestedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const handleTopItemClick = (item: MenuItem) => {
|
const handleTopItemClick = (item: MenuItem) => {
|
||||||
setActiveTopId(item.id);
|
setActiveTopId(item.id);
|
||||||
|
|
||||||
if (!item.children?.length && item.path) {
|
if (item.children?.length) {
|
||||||
|
const firstPath = findFirstRoutePath(item.children);
|
||||||
|
if (firstPath) {
|
||||||
|
closeNavPanel();
|
||||||
|
navigate(firstPath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.path) {
|
||||||
closeNavPanel();
|
closeNavPanel();
|
||||||
navigate(item.path);
|
navigate(item.path);
|
||||||
}
|
}
|
||||||
@@ -138,6 +172,22 @@ export function PageSidebarDark() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
// collapsed 状态:sidebar 主体完全消失(包括占据的宽度),只渲染左下角浮动入口
|
||||||
|
if (collapsed) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="展开侧边栏"
|
||||||
|
aria-label="展开侧边栏"
|
||||||
|
className="sidebar-collapse-btn sidebar-collapse-btn--floating"
|
||||||
|
onClick={onToggleCollapsed}
|
||||||
|
>
|
||||||
|
<PanelLeftOpen className="sidebar-collapse-btn__icon" aria-hidden="true" />
|
||||||
|
<span className="sidebar-collapse-btn__label">收起面板</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sidebar-contnet">
|
<aside className="sidebar-contnet">
|
||||||
<div className="sidebar-contents">
|
<div className="sidebar-contents">
|
||||||
@@ -169,6 +219,18 @@ export function PageSidebarDark() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* 底部"收起面板"按钮:独立样式,mt-auto 推到底 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="收起侧边栏"
|
||||||
|
aria-label="收起侧边栏"
|
||||||
|
className="sidebar-collapse-btn sidebar-collapse-btn--inline"
|
||||||
|
onClick={onToggleCollapsed}
|
||||||
|
>
|
||||||
|
<PanelLeftClose className="sidebar-collapse-btn__icon" aria-hidden="true" />
|
||||||
|
<span className="sidebar-collapse-btn__label">收起面板</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 外部系统跳转登录时透传的参数:
|
||||||
|
* theme —— 主题色,"dark-blue" => 深色,其他 => 浅色(在 LoginPage 调 setTheme 直接生效)
|
||||||
|
* ticket —— 第三方 ticket,持久化到 localStorage 供后续请求使用
|
||||||
|
* userId —— 外部用户 id,同上
|
||||||
|
* env —— 环境标识,同上
|
||||||
|
*
|
||||||
|
* 这里只暴露 ``ticket / userId / env`` 三个 storage key 和读写工具;
|
||||||
|
* theme 不存(直接走 ThemeContext,避免双源)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const EXTERNAL_LOGIN_STORAGE_KEYS = {
|
||||||
|
ticket: "login.ticket",
|
||||||
|
userId: "login.userId",
|
||||||
|
env: "login.env",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ExternalLoginParams = {
|
||||||
|
ticket?: string;
|
||||||
|
userId?: string;
|
||||||
|
env?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 把外部跳转带来的三个参数写到 localStorage;隐私模式 / quota 异常时静默失败 */
|
||||||
|
export function persistExternalLoginParams(params: ExternalLoginParams): void {
|
||||||
|
try {
|
||||||
|
if (params.ticket) {
|
||||||
|
localStorage.setItem(EXTERNAL_LOGIN_STORAGE_KEYS.ticket, params.ticket);
|
||||||
|
}
|
||||||
|
if (params.userId) {
|
||||||
|
localStorage.setItem(EXTERNAL_LOGIN_STORAGE_KEYS.userId, params.userId);
|
||||||
|
}
|
||||||
|
if (params.env) {
|
||||||
|
localStorage.setItem(EXTERNAL_LOGIN_STORAGE_KEYS.env, params.env);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// localStorage 不可用时不阻断登录流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取持久化的外部登录参数;不可用时返回空字符串 */
|
||||||
|
export function readExternalLoginParams(): Required<ExternalLoginParams> {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
ticket: localStorage.getItem(EXTERNAL_LOGIN_STORAGE_KEYS.ticket) ?? "",
|
||||||
|
userId: localStorage.getItem(EXTERNAL_LOGIN_STORAGE_KEYS.userId) ?? "",
|
||||||
|
env: localStorage.getItem(EXTERNAL_LOGIN_STORAGE_KEYS.env) ?? "",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { ticket: "", userId: "", env: "" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把 ``theme`` query 值映射成 ThemeContext 的 ``'dark' | 'light'`` */
|
||||||
|
export function resolveThemeFromQuery(themeParam: string | null | undefined): "dark" | "light" {
|
||||||
|
return themeParam === "dark-blue" ? "dark" : "light";
|
||||||
|
}
|
||||||
@@ -68,40 +68,40 @@ export const sidebarMenuItems: MenuItem[] = [
|
|||||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: "light-app-center",
|
// id: "light-app-center",
|
||||||
label: "轻应用",
|
// label: "轻应用",
|
||||||
icon: Grid3X3,
|
// icon: Grid3X3,
|
||||||
children: [
|
// children: [
|
||||||
{
|
// {
|
||||||
id: "light-app-note-management",
|
// id: "light-app-note-management",
|
||||||
label: "笔记管理",
|
// label: "笔记管理",
|
||||||
icon: NotebookPen,
|
// icon: NotebookPen,
|
||||||
children: [
|
// children: [
|
||||||
{
|
// {
|
||||||
id: "light-app-new-note",
|
// id: "light-app-new-note",
|
||||||
label: "新建笔记",
|
// label: "新建笔记",
|
||||||
icon: Plus,
|
// icon: Plus,
|
||||||
path: "/page/strategy/light-app/new-note",
|
// path: "/page/strategy/light-app/new-note",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "light-app-note-list",
|
// id: "light-app-note-list",
|
||||||
label: "笔记列表",
|
// label: "笔记列表",
|
||||||
icon: List,
|
// icon: List,
|
||||||
path: "/page/strategy/light-app/notes",
|
// path: "/page/strategy/light-app/notes",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "light-app-note-qa",
|
// id: "light-app-note-qa",
|
||||||
label: "笔记问答",
|
// label: "笔记问答",
|
||||||
icon: MessageSquareCode,
|
// icon: MessageSquareCode,
|
||||||
path: "/page/strategy/light-app/note-qa",
|
// path: "/page/strategy/light-app/note-qa",
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
{ id: "light-app-square", label: "应用广场", icon: Layout },
|
// { id: "light-app-square", label: "应用广场", icon: Layout },
|
||||||
{ id: "light-app-records", label: "运行记录", icon: History },
|
// { id: "light-app-records", label: "运行记录", icon: History },
|
||||||
],
|
// ],
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: "task-management",
|
id: "task-management",
|
||||||
label: "任务管理",
|
label: "任务管理",
|
||||||
|
|||||||
@@ -463,8 +463,9 @@ export function AIWritingPanel({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 研究中 + 已有技能步骤:实时流式展示技能检索过程,避免 skeleton 卡片掩盖步骤 */}
|
{/* 技能检索 pending:用户点击瞬间到 awaiting 之间,始终显示 SkillStepsCard 占位
|
||||||
{state.status === 'researching' && state.skillSearchSteps.length > 0 && (
|
steps.length === 0 时 SkillStepsCard 内部会渲染骨架行,不让用户干等空白 */}
|
||||||
|
{state.skillSearchPending && (
|
||||||
<div className="px-4 pb-2">
|
<div className="px-4 pb-2">
|
||||||
<SkillStepsCard steps={state.skillSearchSteps} inProgress />
|
<SkillStepsCard steps={state.skillSearchSteps} inProgress />
|
||||||
</div>
|
</div>
|
||||||
@@ -474,8 +475,8 @@ export function AIWritingPanel({
|
|||||||
state.status !== 'writing_draft' &&
|
state.status !== 'writing_draft' &&
|
||||||
!(state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
!(state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
||||||
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) &&
|
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) &&
|
||||||
/* 技能检索已经在流式展示步骤 / 选择打字机 / 等待 chunk 中,不再叠加 skeleton */
|
/* 技能检索占位卡 / 选择打字机 已经在流式展示,不再叠加 skeleton */
|
||||||
!(state.status === 'researching' && state.skillSearchSteps.length > 0) &&
|
!state.skillSearchPending &&
|
||||||
!(state.status === 'researching' && (state.skillSelectStreamingText || state.skillSelectThinkingText)) &&
|
!(state.status === 'researching' && (state.skillSelectStreamingText || state.skillSelectThinkingText)) &&
|
||||||
/* 意图解析流式过程中(LLM 还在吐 JSON)不要显示 skeleton */
|
/* 意图解析流式过程中(LLM 还在吐 JSON)不要显示 skeleton */
|
||||||
!state.intentStreamingText && !state.intentThinkingText && (
|
!state.intentStreamingText && !state.intentThinkingText && (
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function MaterialsToolbar({
|
|||||||
value={userQuery}
|
value={userQuery}
|
||||||
onChange={handleInput}
|
onChange={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={placeholder ?? '例如:想查看各大平台销量 / 2025 年财报 / 行业头部公司近期动态…'}
|
placeholder={placeholder ?? '输入文字后点击“重新搜索”或“技能检索”按钮…'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="w-full resize-none rounded-md border border-input bg-background px-2.5 py-1.5 text-xs leading-5 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 min-h-[28px] max-h-24"
|
className="w-full resize-none rounded-md border border-input bg-background px-2.5 py-1.5 text-xs leading-5 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 min-h-[28px] max-h-24"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -118,9 +118,28 @@ export function SkillStepsCard({ steps, inProgress }: SkillStepsCardProps) {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{inProgress && (
|
{inProgress && steps.length === 0 && (
|
||||||
|
// 用户刚点击"技能检索",还没收到任何步骤事件:用骨架行 + spinner 占位,
|
||||||
|
// 避免用户在等待后端 emit 时干等空白
|
||||||
|
<>
|
||||||
|
<li className="flex items-center gap-2 text-[11px] text-violet-700/80 dark:text-violet-300/70 italic">
|
||||||
|
<Loader2 className="size-3 animate-spin shrink-0" />
|
||||||
|
正在准备技能检索…
|
||||||
|
</li>
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<li key={`skel-${i}`} className="flex items-start gap-2 animate-pulse" style={{ animationDelay: `${i * 120}ms` }}>
|
||||||
|
<span className="shrink-0 mt-0.5 size-4 rounded-full bg-violet-100 dark:bg-violet-900/60" />
|
||||||
|
<div className="flex-1 min-w-0 space-y-1">
|
||||||
|
<div className="h-2.5 w-24 rounded bg-violet-100 dark:bg-violet-900/60" />
|
||||||
|
<div className="h-2 w-40 rounded bg-violet-50 dark:bg-violet-900/30" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{inProgress && steps.length > 0 && (
|
||||||
<li className="flex items-center gap-2 text-[11px] text-muted-foreground italic">
|
<li className="flex items-center gap-2 text-[11px] text-muted-foreground italic">
|
||||||
<Loader2 className="size-3 animate-spin" />
|
<Loader2 className="size-3 animate-spin shrink-0" />
|
||||||
更多步骤生成中…
|
更多步骤生成中…
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const INITIAL_STATE: AIWritingState = {
|
|||||||
skillSearchSteps: [],
|
skillSearchSteps: [],
|
||||||
skillSelectStreamingText: '',
|
skillSelectStreamingText: '',
|
||||||
skillSelectThinkingText: '',
|
skillSelectThinkingText: '',
|
||||||
|
skillSearchPending: false,
|
||||||
streamingReviewItems: [],
|
streamingReviewItems: [],
|
||||||
revisionSummary: null,
|
revisionSummary: null,
|
||||||
pendingRevisionContext: null,
|
pendingRevisionContext: null,
|
||||||
@@ -105,10 +106,13 @@ function reducer(prev: AIWritingState, action: Action): AIWritingState {
|
|||||||
|
|
||||||
case 'intervention_submitted': {
|
case 'intervention_submitted': {
|
||||||
const ns = action.nextStatus;
|
const ns = action.nextStatus;
|
||||||
|
// 点击瞬间标记技能检索 pending(动机 = 用户的 action 类型),让 SkillStepsCard 占位卡立即挂载
|
||||||
|
const enablingSkillSearch = action.record.action === 'enable_skill_search';
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
currentPause: null,
|
currentPause: null,
|
||||||
status: ns,
|
status: ns,
|
||||||
|
skillSearchPending: enablingSkillSearch ? true : prev.skillSearchPending,
|
||||||
...(ns === 'researching' ? { researchProgress: '', skillSearchSteps: [], skillSelectStreamingText: '', skillSelectThinkingText: '' } : {}),
|
...(ns === 'researching' ? { researchProgress: '', skillSearchSteps: [], skillSelectStreamingText: '', skillSelectThinkingText: '' } : {}),
|
||||||
...(ns === 'writing_outline' ? { outlineStreamingText: '', outlineThinkingText: '' } : {}),
|
...(ns === 'writing_outline' ? { outlineStreamingText: '', outlineThinkingText: '' } : {}),
|
||||||
...(ns === 'writing_draft' ? { streamingDraftSections: [], streamingDraftTitle: '' } : {}),
|
...(ns === 'writing_draft' ? { streamingDraftSections: [], streamingDraftTitle: '' } : {}),
|
||||||
@@ -341,7 +345,9 @@ function handleEvent(prev: AIWritingState, event: ProgressEvent): AIWritingState
|
|||||||
const extra: Partial<AIWritingState> = {};
|
const extra: Partial<AIWritingState> = {};
|
||||||
if (pp === 'material_confirm' && event.materials?.length) extra.materials = event.materials;
|
if (pp === 'material_confirm' && event.materials?.length) extra.materials = event.materials;
|
||||||
if (pp === 'outline_confirm' && event.outline) extra.outline = event.outline;
|
if (pp === 'outline_confirm' && event.outline) extra.outline = event.outline;
|
||||||
return { ...base, ...extra, status: pauseStatus, currentPause: event };
|
// 进入素材确认(material_confirm)= 技能检索(如果在进行)已经完成,清掉 pending 占位
|
||||||
|
const clearPending = pp === 'material_confirm' ? { skillSearchPending: false } : {};
|
||||||
|
return { ...base, ...extra, ...clearPending, status: pauseStatus, currentPause: event };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'done':
|
case 'done':
|
||||||
|
|||||||
@@ -321,6 +321,9 @@ export interface AIWritingState {
|
|||||||
// 技能选择 LLM 流式打字内容(正文 / 思考分开),select_skills 完成后清空
|
// 技能选择 LLM 流式打字内容(正文 / 思考分开),select_skills 完成后清空
|
||||||
skillSelectStreamingText: string;
|
skillSelectStreamingText: string;
|
||||||
skillSelectThinkingText: string;
|
skillSelectThinkingText: string;
|
||||||
|
/** 技能检索是否正在进行:用户点击"技能检索"瞬间 true,进入 awaiting 时清掉。
|
||||||
|
* 用于在 LangGraph 还没推任何 skill_search_step 事件时给前端展示占位卡。 */
|
||||||
|
skillSearchPending: boolean;
|
||||||
// 流式审核(逐条到达)
|
// 流式审核(逐条到达)
|
||||||
streamingReviewItems: StreamingReviewItem[];
|
streamingReviewItems: StreamingReviewItem[];
|
||||||
// 修改摘要
|
// 修改摘要
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { loginByUsername } from "@/core/auth/api";
|
import { loginByUsername } from "@/core/auth/api";
|
||||||
import { setStoredAuth } from "@/core/auth";
|
import { setStoredAuth } from "@/core/auth";
|
||||||
|
import {
|
||||||
|
persistExternalLoginParams,
|
||||||
|
resolveThemeFromQuery,
|
||||||
|
} from "@/core/auth/external-login";
|
||||||
import {
|
import {
|
||||||
isSidebarEnabledFromSearch,
|
isSidebarEnabledFromSearch,
|
||||||
persistSidebarEnabled,
|
persistSidebarEnabled,
|
||||||
@@ -18,6 +23,7 @@ export default function LoginPage() {
|
|||||||
const { username } = useParams<{ username?: string }>();
|
const { username } = useParams<{ username?: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const { setTheme } = useTheme();
|
||||||
const sidebarEnabled = useMemo(
|
const sidebarEnabled = useMemo(
|
||||||
() => isSidebarEnabledFromSearch(searchParams.toString()),
|
() => isSidebarEnabledFromSearch(searchParams.toString()),
|
||||||
[searchParams],
|
[searchParams],
|
||||||
@@ -26,9 +32,31 @@ export default function LoginPage() {
|
|||||||
const raw = username?.trim();
|
const raw = username?.trim();
|
||||||
return raw && raw.length > 0 ? raw : DEFAULT_USERNAME;
|
return raw && raw.length > 0 ? raw : DEFAULT_USERNAME;
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
// 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage
|
||||||
|
const externalParams = useMemo(() => ({
|
||||||
|
theme: searchParams.get("theme") ?? "",
|
||||||
|
ticket: searchParams.get("ticket") ?? "",
|
||||||
|
userId: searchParams.get("userId") ?? "",
|
||||||
|
env: searchParams.get("env") ?? "",
|
||||||
|
}), [searchParams]);
|
||||||
const [status, setStatus] = useState<LoginStatus>("loading");
|
const [status, setStatus] = useState<LoginStatus>("loading");
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||||
|
|
||||||
|
// 主题切换:theme=dark-blue → 深色,其他值或缺省 → 浅色
|
||||||
|
useEffect(() => {
|
||||||
|
if (!externalParams.theme) return;
|
||||||
|
setTheme(resolveThemeFromQuery(externalParams.theme));
|
||||||
|
}, [externalParams.theme, setTheme]);
|
||||||
|
|
||||||
|
// ticket / userId / env 持久化(与 login 请求并行做,互不阻塞)
|
||||||
|
useEffect(() => {
|
||||||
|
persistExternalLoginParams({
|
||||||
|
ticket: externalParams.ticket,
|
||||||
|
userId: externalParams.userId,
|
||||||
|
env: externalParams.env,
|
||||||
|
});
|
||||||
|
}, [externalParams.ticket, externalParams.userId, externalParams.env]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ function PageLayout() {
|
|||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{sidebarEnabled && (
|
{sidebarEnabled && (
|
||||||
isDark ? (
|
isDark ? (
|
||||||
<PageSidebarDark />
|
<PageSidebarDark
|
||||||
|
collapsed={collapsed}
|
||||||
|
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PageSidebar
|
<PageSidebar
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
@@ -33,14 +36,16 @@ function PageLayout() {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<main
|
<main
|
||||||
className={`flex-1 overflow-hidden transition-[padding] duration-300 ${
|
className={`flex-1 overflow-hidden ${
|
||||||
!sidebarEnabled
|
!sidebarEnabled
|
||||||
? ""
|
? ""
|
||||||
: isDark
|
: isDark
|
||||||
? "pl-[118px]"
|
// 深色 sidebar:展开 118px(10 + 98 + 10 gap),收起 = 0 sidebar 完全消失,
|
||||||
: collapsed
|
// 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。
|
||||||
? "pl-16"
|
// 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐
|
||||||
: "pl-64"
|
? (collapsed ? "pl-0" : "pl-[118px]")
|
||||||
|
// 浅色仍保留过渡动画(pl-16 ↔ pl-64 平滑收展)
|
||||||
|
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}`
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ export function WorkspaceLayout({
|
|||||||
const pageSidebarWidth = pageSidebarHidden
|
const pageSidebarWidth = pageSidebarHidden
|
||||||
? "0rem"
|
? "0rem"
|
||||||
: pageSidebarDark
|
: pageSidebarDark
|
||||||
// dark page sidebar 视觉右边界是 108px(外框 98 + 内层 translateX 10),
|
// dark page sidebar:
|
||||||
// 再 +10px gap 让 workspace sidebar 跟它之间留一条间隔;
|
// 展开: 视觉右边界 108px(外框 98 + 内层 translateX 10)+ 10px gap = 118px
|
||||||
// 与 PageRoutes.tsx 的 main pl-[118px] 保持一致
|
// 收起: sidebar 主体完全消失(连同其占据的宽度),workspace 内容铺到屏幕左边;
|
||||||
? "118px"
|
// 左下角浮动按钮是 fixed,叠在主内容上不影响 layout
|
||||||
|
// 与 PageRoutes.tsx 的 main pl-[118px] / pl-0 保持一致
|
||||||
|
? (pageSidebarCollapsed ? "0rem" : "118px")
|
||||||
: pageSidebarCollapsed
|
: pageSidebarCollapsed
|
||||||
? "4rem"
|
? "4rem"
|
||||||
: "16rem";
|
: "16rem";
|
||||||
|
|||||||
Reference in new issue
Block a user