240 lines
11 KiB
TypeScript
240 lines
11 KiB
TypeScript
/**
|
||
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载三个相互独立的能力,多由 URL
|
||
* 路由参数开启(独立会话 `isolate` 另可由「外观」设置里的开关在运行时切换,见 `setIsolatedSession`),
|
||
* 存进同一份 sessionStorage 会话、跨登录跳转/刷新/页内导航存活:
|
||
*
|
||
* 1. `embed=1`(可带 `&theme=`)—— **嵌入模式**:去掉全局导航栏(Header)+ 侧边栏(PageSidebar),
|
||
* 并按外部主题色强制系统明/暗(离散切换)。
|
||
* 2. `hideJump=1` —— **隐藏深链底部「快捷跳转」栏**:去掉 `TaskJumpBar` 的两部分——底部的
|
||
* 「下一步」等按钮 + 右侧垂直居中的悬浮菜单(单智能体问答 / 多智能体会商深链共用该栏)。
|
||
* 3. `isolate=1` —— **独立会话登录**:本标签页的鉴权身份按标签页隔离(见 [[scoped-storage]]
|
||
* `authStorage()`,嵌入态与独立态同走 sessionStorage)。用于同一浏览器里**另开一个标签页登
|
||
* 录不同账号**而**不覆盖**当前标签页(localStorage)的登录态——`embed` 已隐含此隔离,`isolate`
|
||
* 则是把同一套隔离单独提供给**普通(带 chrome)标签页**,不去 chrome、不强制主题、不动跳转栏。
|
||
*
|
||
* 三者可单独使用,也可叠加:`hideJump` **只**隐藏跳转栏,不动 chrome / 主题;`embed` 不影响跳转栏;
|
||
* `isolate` 只切换鉴权存储,不影响 chrome / 主题 / 跳转栏。
|
||
*
|
||
* 隔离要点(核心诉求:同一浏览器另开咱们正常系统页,**绝不能**被这些开关影响):
|
||
* - 用 **sessionStorage**(按标签页隔离、绝不跨标签)持有这些 flag,而**不用** localStorage
|
||
* (跨标签共享 → 会污染正常页)。
|
||
* - 主题走 next-themes 的 `forcedTheme`(见 App.tsx `AppProviders`)——只在本标签页生效、
|
||
* 不写 localStorage `strategy-theme`;嵌入侧绝不调 `setTheme`。
|
||
* - 去 chrome / 去跳转栏均由本模块的 sessionStorage flag 驱动,不碰 `persistSidebarEnabled` 等
|
||
* localStorage 偏好。
|
||
*
|
||
* iframe 的浏览器上下文就是它自己的标签页,sessionStorage 天然「一个 iframe 一份、随关闭消失」;
|
||
* 正常页标签页里既无这些参数也无这份会话 → 所有 `isXxx()` 恒为 false,完全不受影响。
|
||
*/
|
||
|
||
const EMBED_SESSION_KEY = "embed.session";
|
||
|
||
export type EmbedTheme = "light" | "dark";
|
||
|
||
/** 本标签页的嵌入会话(按需置位的若干独立开关)。 */
|
||
export interface EmbedSession {
|
||
/** 嵌入模式:去全局 chrome + 强制主题。 */
|
||
embed: boolean;
|
||
/** 嵌入模式下强制的主题(仅 embed 为真时有意义)。 */
|
||
theme: EmbedTheme;
|
||
/** 隐藏深链底部「快捷跳转」栏(TaskJumpBar:底部按钮 + 右侧悬浮菜单)。 */
|
||
hideJump: boolean;
|
||
/**
|
||
* 仅去全局 chrome(Header 导航栏 + PageSidebar 侧边栏),**不**强制主题、**不**隔离鉴权。
|
||
* 区别于 `embed`:嵌入态会额外强制主题 + 走 sessionStorage 隔离登录身份;`hideChrome` 只想要
|
||
* 一个干净的全屏页面(如「大屏绘制」`?hideChrome=1`),其余行为与正常标签页完全一致。
|
||
*/
|
||
hideChrome: boolean;
|
||
/** 独立会话:鉴权身份按本标签页隔离(走 sessionStorage),用于同浏览器登录不同账号互不覆盖。 */
|
||
isolate: boolean;
|
||
}
|
||
|
||
/** URL 布尔参数的三态:显式开 / 显式关 / 未带。 */
|
||
type FlagParam = "on" | "off" | null;
|
||
|
||
function toSearchParams(search: string): URLSearchParams {
|
||
return new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
|
||
}
|
||
|
||
/** 读某个布尔型 query 参数的三态。`1/true/yes` → on;`0/false/no` → off;缺省/其它 → null。 */
|
||
function readFlagParam(search: string, key: string): FlagParam {
|
||
const v = (toSearchParams(search).get(key) ?? "").trim().toLowerCase();
|
||
if (!v) return null;
|
||
if (v === "1" || v === "true" || v === "yes") return "on";
|
||
if (v === "0" || v === "false" || v === "no") return "off";
|
||
return null;
|
||
}
|
||
|
||
/** 读 URL 里的 `embed` 三态。 */
|
||
export function readEmbedParam(search: string): FlagParam {
|
||
return readFlagParam(search, "embed");
|
||
}
|
||
|
||
/** 读 URL 里的 `hideJump` 三态。 */
|
||
export function readHideJumpParam(search: string): FlagParam {
|
||
return readFlagParam(search, "hideJump");
|
||
}
|
||
|
||
/** 读 URL 里的 `hideChrome` 三态(仅去全局导航栏 + 侧边栏)。 */
|
||
export function readHideChromeParam(search: string): FlagParam {
|
||
return readFlagParam(search, "hideChrome");
|
||
}
|
||
|
||
/** 读 URL 里的 `isolate` 三态(独立会话登录)。 */
|
||
export function readIsolateParam(search: string): FlagParam {
|
||
return readFlagParam(search, "isolate");
|
||
}
|
||
|
||
/**
|
||
* 把外部传入的主题色映射成离散明/暗。与 `resolveThemeFromQuery` 同源约定(dark-blue → 深色),
|
||
* 另外宽松接受 `dark` / 中文「深色/深蓝」直传;其余(含空)一律浅色。
|
||
*/
|
||
export function resolveEmbedThemeMode(raw: string | null | undefined): EmbedTheme {
|
||
const v = (raw ?? "").trim().toLowerCase();
|
||
if (v === "dark" || v === "dark-blue" || v === "深色" || v === "深蓝") return "dark";
|
||
return "light";
|
||
}
|
||
|
||
/** 取 URL query 里的 `theme` 值(embed 主题来源)。 */
|
||
export function readEmbedThemeParam(search: string): string {
|
||
return (toSearchParams(search).get("theme") ?? "").trim();
|
||
}
|
||
|
||
/** 读本标签页的嵌入会话;无 → null。 */
|
||
export function readEmbedSession(): EmbedSession | null {
|
||
try {
|
||
const raw = sessionStorage.getItem(EMBED_SESSION_KEY);
|
||
if (!raw) return null;
|
||
const p = JSON.parse(raw) as Partial<EmbedSession>;
|
||
return {
|
||
embed: p.embed === true,
|
||
theme: p.theme === "dark" ? "dark" : "light",
|
||
hideJump: p.hideJump === true,
|
||
hideChrome: p.hideChrome === true,
|
||
isolate: p.isolate === true,
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** 写会话;无任何开关置位时直接清除(保持 sessionStorage 干净)。隐私模式/quota 异常静默失败。 */
|
||
function writeEmbedSession(next: EmbedSession): void {
|
||
try {
|
||
if (!next.embed && !next.hideJump && !next.hideChrome && !next.isolate) {
|
||
sessionStorage.removeItem(EMBED_SESSION_KEY);
|
||
return;
|
||
}
|
||
sessionStorage.setItem(EMBED_SESSION_KEY, JSON.stringify(next));
|
||
} catch {
|
||
/* noop */
|
||
}
|
||
}
|
||
|
||
/** 本标签页是否处于嵌入模式(驱动去全局 chrome + 强制主题)。 */
|
||
export function isEmbedActive(): boolean {
|
||
return readEmbedSession()?.embed === true;
|
||
}
|
||
|
||
/** 本标签页是否要隐藏深链底部「快捷跳转」栏(TaskJumpBar)。 */
|
||
export function isJumpBarHidden(): boolean {
|
||
return readEmbedSession()?.hideJump === true;
|
||
}
|
||
|
||
/**
|
||
* 本标签页是否要去全局 chrome(Header 导航栏 + PageSidebar 侧边栏)。
|
||
* `embed` 已隐含去 chrome,故这里取「embed || hideChrome」之并——任一置位都隐藏。
|
||
* 与 `isEmbedActive()` 的差别:`hideChrome` 不强制主题、不隔离鉴权,只去 chrome。
|
||
*/
|
||
export function isChromeHidden(): boolean {
|
||
const s = readEmbedSession();
|
||
return s?.embed === true || s?.hideChrome === true;
|
||
}
|
||
|
||
/**
|
||
* 本标签页是否处于独立会话态:鉴权身份按标签页隔离(走 sessionStorage)。
|
||
* `embed` 已隐含同样的隔离,故 `authStorage()` 取「embed || isolate」之并;这里只判 `isolate`,
|
||
* 供需要区分「纯独立会话(带 chrome)」与「嵌入态(去 chrome)」的场合使用。
|
||
*/
|
||
export function isIsolatedSession(): boolean {
|
||
return readEmbedSession()?.isolate === true;
|
||
}
|
||
|
||
/**
|
||
* 按 URL 参数同步 sessionStorage 会话(AppProviders 在每次 search 变化时调用一次)。
|
||
* **合并语义**:on 置位、off 清位、未带则沿用既有值——这样登录后跳到不带参数的干净路由时,
|
||
* 之前置位的开关不会被抹掉。
|
||
*/
|
||
export function syncEmbedSessionFromSearch(search: string): void {
|
||
const cur =
|
||
readEmbedSession() ??
|
||
{ embed: false, theme: "light" as EmbedTheme, hideJump: false, hideChrome: false, isolate: false };
|
||
const next: EmbedSession = { ...cur };
|
||
|
||
const embedParam = readEmbedParam(search);
|
||
if (embedParam === "on") {
|
||
next.embed = true;
|
||
next.theme = resolveEmbedThemeMode(readEmbedThemeParam(search));
|
||
} else if (embedParam === "off") {
|
||
next.embed = false;
|
||
}
|
||
|
||
const hideJumpParam = readHideJumpParam(search);
|
||
if (hideJumpParam === "on") next.hideJump = true;
|
||
else if (hideJumpParam === "off") next.hideJump = false;
|
||
|
||
const hideChromeParam = readHideChromeParam(search);
|
||
if (hideChromeParam === "on") next.hideChrome = true;
|
||
else if (hideChromeParam === "off") next.hideChrome = false;
|
||
|
||
const isolateParam = readIsolateParam(search);
|
||
if (isolateParam === "on") next.isolate = true;
|
||
else if (isolateParam === "off") next.isolate = false;
|
||
|
||
writeEmbedSession(next);
|
||
}
|
||
|
||
/**
|
||
* 从当前 HashRouter URL(`#/path?a=b`)取出 query 串。
|
||
* 供「在 React 渲染之前」同步启用嵌入会话(main.tsx)——必须早于任何鉴权写入,
|
||
* 否则首帧 LoginPage 的 effect 可能抢在 AppProviders 同步 effect 之前把登录参数写进 localStorage。
|
||
*/
|
||
function currentHashSearch(): string {
|
||
if (typeof window === "undefined") return "";
|
||
const hash = window.location.hash || "";
|
||
const i = hash.indexOf("?");
|
||
return i >= 0 ? hash.slice(i) : "";
|
||
}
|
||
|
||
/** main.tsx 在渲染前同步调用一次:据当前 URL 启用/关闭本标签页嵌入会话。 */
|
||
export function syncEmbedSessionFromCurrentUrl(): void {
|
||
syncEmbedSessionFromSearch(currentHashSearch());
|
||
}
|
||
|
||
/**
|
||
* 解析「应当强制的嵌入主题」供 `AppProviders` 的 `forcedTheme` 用:
|
||
* - URL 显式 `embed=1` → 用 URL `theme`(首帧即生效,登录页转场不闪烁);
|
||
* - URL 显式 `embed=0` → 不强制(undefined);
|
||
* - URL 未带 → 回退本标签页嵌入会话主题(仅 embed 置位时)。
|
||
* 非嵌入标签页恒返回 undefined → 完全不影响正常系统页的主题。
|
||
*/
|
||
export function resolveForcedEmbedTheme(search: string): EmbedTheme | undefined {
|
||
const param = readEmbedParam(search);
|
||
if (param === "on") return resolveEmbedThemeMode(readEmbedThemeParam(search));
|
||
if (param === "off") return undefined;
|
||
const session = readEmbedSession();
|
||
return session?.embed ? session.theme : undefined;
|
||
}
|
||
|
||
/**
|
||
* 运行时直接置位/清除本标签页的独立会话标志(供「外观」设置里的开关用,区别于 URL 参数驱动)。
|
||
* 仅切换标志;登录态的「localStorage 快照搬进 sessionStorage」由 [[scoped-storage]]
|
||
* `migrateAuthToSession()` 负责,调用顺序见 appearance 设置页。
|
||
*/
|
||
export function setIsolatedSession(on: boolean): void {
|
||
const cur =
|
||
readEmbedSession() ??
|
||
{ embed: false, theme: "light" as EmbedTheme, hideJump: false, hideChrome: false, isolate: false };
|
||
writeEmbedSession({ ...cur, isolate: on });
|
||
}
|