|
|
|
@ -43,7 +43,6 @@ interface LayoutSideNavIconProps { |
|
|
|
|
|
|
|
const LAYOUT_NAV_ASSETS = { |
|
|
|
logo: new URL("@/assets/images/layout/nav/popi-logo.png", import.meta.url).href, |
|
|
|
collapse: new URL("@/assets/images/layout/nav/collapse.png", import.meta.url).href, |
|
|
|
explore: new URL("@/assets/images/layout/nav/explore.png", import.meta.url).href, |
|
|
|
exploreActive: new URL("@/assets/images/layout/nav/explore_active.png", import.meta.url).href, |
|
|
|
characters: new URL("@/assets/images/layout/nav/characters.png", import.meta.url).href, |
|
|
|
@ -81,6 +80,13 @@ const LAYOUT_SIDE_NAV_ITEMS: readonly LayoutSideNavItem[] = [ |
|
|
|
activeIconSrc: LAYOUT_NAV_ASSETS.charactersActive, |
|
|
|
to: ROUTES.CHARACTERS, |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: "voice", |
|
|
|
labelKey: "layout.nav.voice", |
|
|
|
iconSrc: LAYOUT_NAV_ASSETS.voice, |
|
|
|
activeIconSrc: LAYOUT_NAV_ASSETS.voiceActive, |
|
|
|
to: ROUTES.VOICE, |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: "create", |
|
|
|
labelKey: "layout.nav.create", |
|
|
|
@ -96,13 +102,6 @@ const LAYOUT_SIDE_NAV_ITEMS: readonly LayoutSideNavItem[] = [ |
|
|
|
to: CANVAS_BASE_URL ?? "", |
|
|
|
badgeKey: "layout.nav.canvasBadge", |
|
|
|
}, |
|
|
|
{ |
|
|
|
id: "voice", |
|
|
|
labelKey: "layout.nav.voice", |
|
|
|
iconSrc: LAYOUT_NAV_ASSETS.voice, |
|
|
|
activeIconSrc: LAYOUT_NAV_ASSETS.voiceActive, |
|
|
|
to: ROUTES.VOICE, |
|
|
|
}, |
|
|
|
]; |
|
|
|
|
|
|
|
const LANGUAGE_LABEL_KEYS: Record<AppLanguage, string> = { |
|
|
|
@ -188,7 +187,6 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
const [languageMenuOpen, setLanguageMenuOpen] = useState(false); |
|
|
|
const [footerPopoverOpen, setFooterPopoverOpen] = useState(false); |
|
|
|
const [topbarServiceQrOpen, setTopbarServiceQrOpen] = useState(false); |
|
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); |
|
|
|
const [promoBarVisible, setPromoBarVisible] = useState(() => !isLayoutPromoBarDismissed()); |
|
|
|
const activePromptPanelHost = useComposerStore((s) => s.activePromptPanelHost); |
|
|
|
|
|
|
|
@ -204,7 +202,6 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
const requestComposerCollapse = useComposerStore((s) => s.requestComposerCollapse); |
|
|
|
const requestComposerExpand = useComposerStore((s) => s.requestComposerExpand); |
|
|
|
const prevPathnameRef = useRef(pathname); |
|
|
|
const prevPathnameForCharactersCreateRef = useRef(pathname); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (!getToken()) { |
|
|
|
@ -254,11 +251,8 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** 角色创建页:侧栏固定收起且不可展开 */ |
|
|
|
const isCharactersCreateRoute = pathname === ROUTES.CHARACTERS_CREATE; |
|
|
|
const isVoiceArea = pathname === ROUTES.VOICE || pathname.startsWith(`${ROUTES.VOICE_MUSIC_SHARE}/`); |
|
|
|
const shouldShowFloatingComposer = pathname !== ROUTES.SUBSCRIBE && pathname !== ROUTES.CHARACTERS_CREATE && !isVoiceArea; |
|
|
|
const effectiveSidebarCollapsed = isCharactersCreateRoute || sidebarCollapsed; |
|
|
|
|
|
|
|
const handlePromoBarClose = useCallback((evt: ReactMouseEvent<HTMLButtonElement>) => { |
|
|
|
evt.stopPropagation(); |
|
|
|
@ -294,14 +288,6 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
window.scrollTo(0, 0); |
|
|
|
}, [pathname]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
const prev = prevPathnameForCharactersCreateRef.current; |
|
|
|
if (prev === ROUTES.CHARACTERS_CREATE && pathname !== ROUTES.CHARACTERS_CREATE) { |
|
|
|
setSidebarCollapsed(false); |
|
|
|
} |
|
|
|
prevPathnameForCharactersCreateRef.current = pathname; |
|
|
|
}, [pathname]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (prevPathnameRef.current === pathname) return; |
|
|
|
prevPathnameRef.current = pathname; |
|
|
|
@ -420,7 +406,7 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
<div className="layoutShellViewport" ref={layoutViewportRef}> |
|
|
|
<div className="layoutShellScaleSpacer" ref={scaleSpacerRef}> |
|
|
|
<div className="layoutShellScaleInner" ref={scaleInnerRef}> |
|
|
|
<div className={classNames("layoutShell", effectiveSidebarCollapsed ? "layoutShell_sidebarCollapsed" : "", !promoBarVisible ? "layoutShell_promoDismissed" : "")}> |
|
|
|
<div className={["layoutShell", !promoBarVisible ? "layoutShell_promoDismissed" : ""].filter(Boolean).join(" ")}> |
|
|
|
{promoBarVisible ? ( |
|
|
|
<div className="layoutShell__promoBar" role="banner" aria-label={t("layout.promo.aria")} onClick={() => navigate(ROUTES.SUBSCRIBE)}> |
|
|
|
<div className="layoutShell__promoBarInner"> |
|
|
|
@ -453,19 +439,6 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="layoutShell__sidebarCollapse" |
|
|
|
aria-disabled={isCharactersCreateRoute} |
|
|
|
onClick={() => { |
|
|
|
if (!isCharactersCreateRoute) setSidebarCollapsed((v) => !v); |
|
|
|
}} |
|
|
|
aria-label={effectiveSidebarCollapsed ? t("layout.sidebar.expand") : t("layout.sidebar.collapse")} |
|
|
|
aria-expanded={!effectiveSidebarCollapsed} |
|
|
|
> |
|
|
|
<span className="layoutShell__sidebarCollapseText">{t("layout.sidebar.menuLabel")}</span> |
|
|
|
<img src={LAYOUT_NAV_ASSETS.collapse} alt="" /> |
|
|
|
</button> |
|
|
|
<header className="layoutShell__topbar" aria-label={t("layout.aria.topbar")} onClick={dismissMainDetailModals}> |
|
|
|
<div className="layoutShell__topbarRight" aria-label={t("layout.aria.userArea")}> |
|
|
|
<div className="layoutShell__topbarTools"> |
|
|
|
@ -688,7 +661,7 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
const isActive = pathname === item.to || (item.to !== "/" && pathname.startsWith(`${item.to}/`)); |
|
|
|
const className = ["layoutShell__sideItem", isActive ? "layoutShell__sideItem_active" : ""].filter(Boolean).join(" "); |
|
|
|
return ( |
|
|
|
<Link key={item.id} to={item.to} className={className} title={effectiveSidebarCollapsed ? t(item.labelKey) : undefined} onClick={(evt) => handleLayoutNavClick(evt, item.to)}> |
|
|
|
<Link key={item.id} to={item.to} className={className} onClick={(evt) => handleLayoutNavClick(evt, item.to)}> |
|
|
|
<LayoutSideNavIcon item={item} /> |
|
|
|
<span className="layoutShell__sideText">{t(item.labelKey)}</span> |
|
|
|
{item.badgeKey ? <span className="layoutShell__sideBadge">{t(item.badgeKey)}</span> : null} |
|
|
|
@ -702,7 +675,7 @@ export function Layout({ children }: LayoutProps) { |
|
|
|
const isActive = pathname === item.to || (item.to !== "/" && pathname.startsWith(`${item.to}/`)); |
|
|
|
const className = ["layoutShell__sideItem", isActive ? "layoutShell__sideItem_active" : ""].filter(Boolean).join(" "); |
|
|
|
return ( |
|
|
|
<Link key={item.id} to={item.to} className={className} title={effectiveSidebarCollapsed ? t(item.labelKey) : undefined} onClick={(evt) => handleLayoutNavClick(evt, item.to)}> |
|
|
|
<Link key={item.id} to={item.to} className={className} onClick={(evt) => handleLayoutNavClick(evt, item.to)}> |
|
|
|
<LayoutSideNavIcon item={item} /> |
|
|
|
<span className="layoutShell__sideText">{t(item.labelKey)}</span> |
|
|
|
</Link> |
|
|
|
|