fix(knowledge): align cards and remove extra guidance

This commit is contained in:
鲨鱼辣椒 committed 2026-10-13 15:43:08 +08:00
1 parent c8ce94b778
commit 2899009215
27 files changed
+516 -100

No files matched your search

@@ -0,0 +1,38 @@
import { forwardRef, type ButtonHTMLAttributes } from 'react';
import { ChevronRight } from 'lucide-react';
import { UserAvatar } from '@/components/profile/UserAvatar';
import { getAccountInitial } from '@/components/profile/user-avatar-utils';
import { cn } from '@/lib/utils';
export interface AccountMenuTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
accountName: string;
avatarUrl?: string | null;
collapsed?: boolean;
expandedWidth?: number;
open?: boolean;
avatarTestId?: string;
surfaceTestId?: string;
}
/** Shared Code account appearance; the containing menu owns behavior and width preferences. */
export const AccountMenuTrigger = forwardRef<HTMLButtonElement, AccountMenuTriggerProps>(
function AccountMenuTrigger({
accountName, avatarUrl, collapsed = false, expandedWidth = 150, open = false,
avatarTestId, surfaceTestId, className, style, type = 'button', ...props
}, ref) {
return <button {...props} ref={ref} type={type}
className={cn('group motion-press flex h-[42px] min-h-[42px] min-w-0 p-0 text-left text-xs font-medium leading-5 text-[#26384d]', collapsed && 'w-full', className)}
style={{ ...style, width: collapsed ? undefined : expandedWidth, maxWidth: '100%', flex: collapsed ? '0 1 auto' : `0 1 ${expandedWidth}px` }}>
<span data-testid={surfaceTestId}
className={cn('pointer-events-none flex h-full w-full min-w-0 items-center gap-2 rounded-[11px] border border-[#d9e3ee] bg-white px-2 py-1 text-xs font-medium leading-5 transition-colors group-hover:bg-[#eaf1f8]',
open && 'bg-[#eaf1f8]',
collapsed && 'justify-center px-0')}>
<UserAvatar src={avatarUrl} initial={getAccountInitial(accountName)} testId={avatarTestId} className="h-7 w-7 shrink-0" />
{!collapsed && <>
<span className="min-w-0 flex-1 truncate">{accountName}</span>
<ChevronRight aria-hidden="true" className={cn('h-3.5 w-3.5 shrink-0', open && 'rotate-90')} />
</>}
</span>
</button>;
},
);
+19 -15
View File
@@ -1,11 +1,16 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronUp, LogOut, Settings, UserCircle2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { LogOut, Settings, UserCircle2 } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { UserAvatar } from '@/components/profile/UserAvatar';
import { getAccountInitial } from '@/components/profile/user-avatar-utils';
import { DisclosureContent } from '@/components/ui/disclosure';
import { useAuthStore } from '@/stores/auth';
import { useSettingsStore } from '@/stores/settings';
import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout';
import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation';
import { PointWallet } from './PointWallet';
import { AccountMenuTrigger } from './AccountMenuTrigger';
export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: {
accountName: string;
@@ -14,7 +19,9 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: {
}) {
const user = useAuthStore(state => state.user);
const logout = useAuthStore(state => state.logout);
const expandedWidth = useSettingsStore(state => normalizeClassroomSidebarWidth(state.classroomSidebarWidth) - 21);
const navigate = useNavigate();
const location = useLocation();
const [open, setOpen] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
const [error, setError] = useState('');
@@ -48,26 +55,20 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: {
if (!user) return null;
return <div className="relative h-[3.125rem] w-32 max-w-full" data-testid="home-account-controls">
<button ref={trigger} type="button" data-testid="home-account-menu-trigger"
return <div className="relative h-[42px] max-w-full" style={{ width: expandedWidth }} data-testid="home-account-controls">
<AccountMenuTrigger ref={trigger} data-testid="home-account-menu-trigger"
accountName={accountName} avatarUrl={avatarUrl} expandedWidth={expandedWidth} open={open}
surfaceTestId="home-account-trigger-surface"
aria-label={`打开账户菜单:${accountName}`} aria-haspopup="menu" aria-expanded={open} aria-controls="home-account-menu"
className="group motion-press relative h-full w-full text-left"
onClick={() => setOpen(value => !value)}
onKeyDown={event => {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault(); setOpen(true); focusItem(event.key === 'ArrowUp');
}
}}>
<span data-testid="home-account-trigger-surface"
className="pointer-events-none absolute bottom-0 left-0 flex min-h-12 w-64 origin-bottom-left scale-50 items-center gap-3 rounded-2xl border border-border/80 bg-background px-4 py-2 text-sm font-medium shadow-sm group-hover:bg-surface-subtle">
<UserAvatar src={avatarUrl} initial={initial} className="h-8 w-8 shrink-0" />
<span className="min-w-0 flex-1 truncate">{accountName}</span>
<ChevronUp aria-hidden="true" className={`h-4 w-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
</span>
</button>
}} />
<DisclosureContent ref={menu} id="home-account-menu" open={open} role="menu" aria-label="账户菜单"
data-testid="home-account-menu"
className="glass-surface absolute bottom-[2.0625rem] left-0 z-50 w-72 max-w-[calc(100vw-2rem)] rounded-2xl border border-border/80 p-3 text-foreground shadow-float"
className="glass-surface absolute bottom-[calc(100%+0.5rem)] left-0 z-50 w-72 max-w-[calc(100vw-2rem)] rounded-2xl border border-border/80 p-3 text-foreground shadow-float"
innerClassName="max-h-[calc(100dvh-8rem)] overflow-y-auto"
onKeyDown={event => {
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
@@ -87,7 +88,10 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: {
<button type="button" role="menuitem" className="flex min-h-10 items-center gap-2 rounded-md px-1 text-left text-sm hover:bg-surface-subtle"
onClick={() => { setOpen(false); onOpenProfile(); }}><UserCircle2 className="h-4 w-4" />个人资料</button>
<button type="button" role="menuitem" className="flex min-h-10 items-center gap-2 rounded-md px-1 text-left text-sm hover:bg-surface-subtle"
onClick={() => { setOpen(false); navigate('/settings'); }}><Settings className="h-4 w-4" />设置</button>
onClick={() => {
if (!canLeaveCurrentPage()) { toast.info('请先保存当前修改,再打开设置。'); return; }
setOpen(false); navigate('/settings', { state: getSettingsReturnState(location) });
}}><Settings className="h-4 w-4" />设置</button>
<button type="button" role="menuitem" disabled={loggingOut} className="flex min-h-10 items-center gap-2 rounded-md px-1 text-left text-sm text-accent-strong hover:bg-accent-soft disabled:opacity-50"
onClick={async () => {
if (loggingOut) return;
@@ -0,0 +1,29 @@
import { useState } from 'react';
import { HomeAccountMenu } from './HomeAccountMenu';
import { UserProfileDialog } from '@/components/profile/UserProfileDialog';
import { useCurrentUserProfile } from '@/hooks/use-current-user-profile';
import { getAuthUserDisplayName } from '@/lib/auth-user-display';
import { useAuthStore } from '@/stores/auth';
import { getUserProfileDisplayName } from '@/stores/user-profile';
export function StandaloneAccountDock({ enforceProfileCompletion = true }: { enforceProfileCompletion?: boolean }) {
const user = useAuthStore(state => state.user);
const authenticated = useAuthStore(state => state.isAuthenticated());
const { userProfile, profileRequired, profileSyncError, syncProfileNow } = useCurrentUserProfile(authenticated);
const [profileOpen, setProfileOpen] = useState(false);
const [dismissedError, setDismissedError] = useState<string | null>(null);
if (!user || !authenticated) return null;
const name = getUserProfileDisplayName(userProfile) || getAuthUserDisplayName(user) || user.username;
return <>
<div data-testid="standalone-account-dock" className="fixed bottom-3 left-2.5 z-40">
<HomeAccountMenu key={user.userId ?? user.username} accountName={name} avatarUrl={userProfile?.avatarUrl}
onOpenProfile={() => { setDismissedError(null); setProfileOpen(true); void syncProfileNow().catch(() => undefined); }} />
</div>
<UserProfileDialog open={profileOpen || (enforceProfileCompletion && (profileRequired || Boolean(profileSyncError && dismissedError !== profileSyncError)))}
required={profileRequired} syncError={profileSyncError} onOpenChange={open => {
setProfileOpen(open);
if (open) setDismissedError(null);
else if (profileSyncError) setDismissedError(profileSyncError);
}} />
</>;
}
+6
View File
@@ -6,6 +6,8 @@ import { Outlet, useLocation } from 'react-router-dom';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Sidebar } from './Sidebar';
import { TitleBar } from './TitleBar';
import { SettingsLayout } from './SettingsLayout';
import { StandaloneAccountDock } from '@/components/account/StandaloneAccountDock';
import { getAiModuleForPath } from '@/lib/ai-modules';
import { useSettingsStore } from '@/stores/settings';
import { cn } from '@/lib/utils';
@@ -74,6 +76,8 @@ export function MainLayout() {
if (sidebarPeekCloseTimerRef.current) clearTimeout(sidebarPeekCloseTimerRef.current);
}, []);
if (location.pathname === '/settings' || location.pathname.startsWith('/settings/')) return <SettingsLayout />;
return (
<div data-testid="main-layout" data-classroom={isChatWorkspace} className="relative flex h-[100dvh] flex-col overflow-hidden bg-transparent font-sans">
{/* Title bar: drag region on macOS, icon + controls on Windows */}
@@ -103,11 +107,13 @@ export function MainLayout() {
'relative min-h-0 min-w-0 flex-1 overflow-auto bg-background',
(isPaintingModule || isCloudAgents || isChatWorkspace) ? 'basis-0 h-full overflow-hidden p-0' : 'p-5 sm:p-6',
(isChatWorkspace || isCloudAgents) && !isPaintingModule && 'basis-0 overflow-hidden p-0',
isCanvasWorkspace && 'pb-16 lg:pb-0',
)}
>
<Outlet />
</main>
</div>
{isCanvasWorkspace && <div className="lg:hidden"><StandaloneAccountDock enforceProfileCompletion={false} /></div>}
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import { TitleBar } from './TitleBar';
import { StandaloneAccountDock } from '@/components/account/StandaloneAccountDock';
export function SettingsLayout() {
return <div data-testid="settings-layout" className="flex h-[100dvh] flex-col overflow-hidden bg-background text-foreground">
<TitleBar integrated pageTitle="设置" showSidebarControls={false} />
<div className="min-h-0 flex-1 overflow-auto pb-20"><Outlet /></div>
<StandaloneAccountDock />
</div>;
}
+22 -35
View File
@@ -36,6 +36,8 @@ import {
subscribeCodingProjectOpenRequest,
} from '@/lib/coding-project-entry';
import { PointWallet } from '@/components/account/PointWallet';
import { AccountMenuTrigger } from '@/components/account/AccountMenuTrigger';
import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation';
import { cn } from '@/lib/utils';
import { useCurrentUserProfile } from '@/hooks/use-current-user-profile';
import type { SidebarPeekSource } from './sidebar-peek';
@@ -361,8 +363,9 @@ export function Sidebar({
};
const navigateFromAccountMenu = (path: string) => {
if (path === '/settings' && !canLeaveCurrentPage()) { toast.info('请先保存当前修改,再打开设置。'); return; }
setAccountMenuOpen(false);
navigate(path);
navigate(path, path === '/settings' ? { state: getSettingsReturnState(location) } : undefined);
};
const openProfileDialog = () => {
@@ -810,42 +813,26 @@ export function Sidebar({
onOpenChange={setProfileDialogOpen}
/>
<div className="classroom-account-footer mt-auto shrink-0 px-2 pb-2 pt-1">
<div className="classroom-account-footer mt-auto shrink-0 px-[10px] pb-3 pt-2.5">
<div className="relative">
<div className={cn(
'flex items-center gap-2',
sidebarCollapsed && 'flex-col-reverse',
)}>
<button
ref={accountButtonRef}
type="button"
data-testid="sidebar-member-menu-trigger"
aria-label={`打开账户菜单:${accountName}`}
aria-haspopup="menu"
aria-expanded={accountMenuOpen}
onClick={() => setAccountMenuOpen((open) => !open)}
className={cn(
'motion-press flex min-h-9 min-w-0 flex-1 items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1 text-left text-sm font-medium text-foreground shadow-none transition-[background-color,color,transform] hover:bg-surface-subtle',
accountMenuOpen && 'bg-background text-foreground',
sidebarCollapsed && 'min-h-9 w-full justify-center px-0',
)}
>
<UserAvatar
src={accountAvatarUrl}
initial={accountInitial}
testId="sidebar-account-avatar"
className="h-7 w-7 shrink-0"
/>
{!sidebarCollapsed && (
<>
<span className="min-w-0 flex-1">
<span className="block truncate">{accountName}</span>
</span>
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0', accountMenuOpen && 'rotate-90')} />
</>
)}
</button>
<div className="flex flex-col items-start gap-2">
<SidebarUpdateButton collapsed={sidebarCollapsed} />
<div className="flex w-full items-center">
<AccountMenuTrigger
ref={accountButtonRef}
data-testid="sidebar-member-menu-trigger"
accountName={accountName}
avatarUrl={accountAvatarUrl}
collapsed={sidebarCollapsed}
expandedWidth={sidebarWidth - 21}
open={accountMenuOpen}
avatarTestId="sidebar-account-avatar"
aria-label={`打开账户菜单:${accountName}`}
aria-haspopup="menu"
aria-expanded={accountMenuOpen}
onClick={() => setAccountMenuOpen((open) => !open)}
/>
</div>
</div>
<DisclosureContent
@@ -57,7 +57,7 @@ export function SidebarUpdateButton({ collapsed }: { collapsed: boolean }) {
disabled={downloading}
onClick={() => void handleClick()}
className={cn(
'motion-press flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-accent/30 bg-accent text-primary-foreground shadow-soft hover:bg-accent-strong disabled:cursor-wait disabled:opacity-80',
'motion-press flex h-10 w-10 max-w-full shrink-0 items-center justify-center rounded-full border border-accent/30 bg-accent text-primary-foreground shadow-soft hover:bg-accent-strong disabled:cursor-wait disabled:opacity-80',
collapsed && 'shadow-none',
)}
>
+1
View File
@@ -212,6 +212,7 @@ function ProductTitleBar({
data-testid="titlebar-main-surface"
className={cn(
'relative flex min-w-0 flex-1 bg-background',
nativeTrafficLights && !nativeFullscreen && !showSidebarControls && !overlay && 'pl-[88px]',
overlay && 'pointer-events-none border-b-0 bg-transparent',
nativeTrafficLights && 'drag-region',
!overlay && (workspaceLayout || sidebarTitlebarHidden ? 'border-b-0' : 'border-b border-border/80'),
+30
View File
@@ -0,0 +1,30 @@
import { AI_MODULE_SELECTION_PATH, getGuardedAiModuleForPath } from './ai-modules';
type SettingsLocation = { pathname: string; search?: string; hash?: string; state?: unknown };
function localReturnPath(value: unknown): string | null {
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//') || value.includes('\\')) return null;
try {
const target = new URL(value, 'https://makelore.local');
if (target.origin !== 'https://makelore.local') return null;
if (target.pathname !== AI_MODULE_SELECTION_PATH && !getGuardedAiModuleForPath(target.pathname)) return null;
return `${target.pathname}${target.search}${target.hash}`;
} catch { return null; }
}
export function getSettingsReturnPath(state: unknown): string {
const candidate = state && typeof state === 'object' && 'settingsReturnTo' in state
? (state as { settingsReturnTo?: unknown }).settingsReturnTo : null;
return localReturnPath(candidate) ?? AI_MODULE_SELECTION_PATH;
}
export function getSettingsReturnState(location: SettingsLocation): { settingsReturnTo: string } {
const inSettings = location.pathname === '/settings' || location.pathname.startsWith('/settings/');
return { settingsReturnTo: inSettings ? getSettingsReturnPath(location.state)
: localReturnPath(`${location.pathname}${location.search ?? ''}${location.hash ?? ''}`) ?? AI_MODULE_SELECTION_PATH };
}
/** Honor the current page's existing unsaved-input protection before internal navigation. */
export function canLeaveCurrentPage(): boolean {
return typeof window === 'undefined' || window.dispatchEvent(new Event('beforeunload', { cancelable: true }));
}
+1 -2
View File
@@ -36,8 +36,7 @@
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row[data-selected="true"] { background: #e0ecfa; border-color: #cedef0; color: #304f75; }
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child { min-height: 35px; gap: 7px; }
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child > svg { color: #7891af; }
[data-testid="sidebar"][data-classroom="true"] .classroom-account-footer { padding: 10px 10px 12px; border-top: 1px solid #dde5ee; background: #edf3f9; }
[data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-member-menu-trigger"] { min-height: 42px; border: 1px solid #d9e3ee; border-radius: 11px; background: #fff; font-size: 12px; }
[data-testid="sidebar"][data-classroom="true"] .classroom-account-footer { border-top: 1px solid #dde5ee; background: #edf3f9; }
.classroom-workspace [data-testid="coding-conversation-header"] { border-bottom-color: #e8edf3; background: #fff; color: #304968; }
.classroom-tabs { display: flex; flex-shrink: 0; height: 46px; gap: 9px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
.classroom-workspace-toolbar { display: flex; flex-wrap: wrap; flex-shrink: 0; align-items: center; gap: 6px 12px; min-height: 46px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
-4
View File
@@ -120,10 +120,6 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
<Button type="button" variant="ghost" disabled={disabled || busy || catalogLoading} onClick={() => setRefresh(v => v + 1)}>刷新知识库与模型</Button>
</div>
{documentPending && selectedKnowledge && <p role="status" className="text-xs leading-5 text-amber-800">有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。</p>}
<details className="agent-knowledge-help">
<summary>使用说明与费用</summary>
<p className="mt-2 text-xs leading-5 text-muted-foreground">上传到知识库的文档可供获准使用此智能体的人检索。解析并建立索引后才会生效;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限。知识处理如触发对话模型调用,仍按原模型实际用量计费。</p>
</details>
{!catalogLoading && !catalogError && !manageable.length && <p className="text-xs leading-5 text-muted-foreground">还没有可管理的知识库,可以新建一个保存自己的文件。MakeLore知识仅供检索。</p>}
{selectedKnowledge && selectableIds !== undefined && !bindable(selectedKnowledge.kb_id) && <p role="status" className="text-xs leading-5 text-amber-800">
{selectableIds === null ? '知识库选用状态待确认。' : '这个知识库当前暂不可选用。'}你仍可管理自己的文件,刷新目录后再试。
@@ -34,7 +34,7 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load
</select>
<Button type="button" variant="ghost" size="sm" disabled={disabled || loading || owned.loading} onClick={onRefresh}>刷新知识库</Button>
</div>
<p className="text-xs leading-5 text-muted-foreground">{selectionFull ? '已选满100个知识库,请先移除一个再选择。' : '可多选,每次选择一个知识库加入配置。'}</p>
{selectionFull && <p className="text-xs leading-5 text-muted-foreground">已选满100个知识库,请先移除一个再选择。</p>}
{error ? <p role="alert" className="text-sm text-destructive">知识库目录读取失败,请刷新重试。</p>
: !loading && ownedIds !== null && options.every(option => ownedIds.includes(option.key)) && <p className="text-xs leading-5 text-muted-foreground">暂无可选的MakeLore知识。平台开放给你读取的知识库会出现在这里。</p>}
{owned.error && <p className="text-xs leading-5 text-muted-foreground">我的知识库目录暂不可用,仍可选用可读取的知识;上传文件请刷新重试。</p>}
@@ -56,7 +56,6 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load
</li>;
})}
</ul> : <p className="agent-resource-empty">还没有选择知识库</p>}
<p className="text-xs leading-5 text-muted-foreground">选择后保存并应用配置,智能体即可使用这些知识。MakeLore知识只可检索,无法在这里修改。</p>
</section>
</div>;
}
+1 -4
View File
@@ -43,16 +43,13 @@
.agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; }
.agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; }
.agent-knowledge-workspace { display: grid; gap: 18px; min-width: 0; }
.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: start; gap: 18px; min-width: 0; }
.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: stretch; gap: 18px; min-width: 0; }
.agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; }
.agent-knowledge-upload { border-color: #dce5f3; }
.agent-knowledge-panel-heading { display: flex; align-items: center; gap: 9px; color: #33485c; }
.agent-knowledge-panel-heading h3 { font-size: 16px; font-weight: 650; line-height: 1.5; }
.agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; }
.agent-knowledge-upload .agent-knowledge-panel-heading svg { color: #4a6e9c; }
.agent-knowledge-help { border-top: 1px solid #e8e6e1; padding-top: 12px; }
.agent-knowledge-help summary { min-height: 28px; width: fit-content; cursor: pointer; color: #606c7e; font-size: 12px; line-height: 1.7; }
.agent-knowledge-help summary:focus-visible { outline: 2px solid #9a7bb5; outline-offset: 3px; border-radius: 4px; }
.agent-knowledge-selection ul { max-height: 480px; overflow: auto; overscroll-behavior: contain; }
.agent-knowledge-selection button { flex-shrink: 0; }
@container (max-width: 620px) { .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } }
+1 -1
View File
@@ -116,7 +116,7 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
})}
</section>
</div>
{authenticated && authUser && <footer className="module-selection-account relative shrink-0 pb-2" aria-label="账户">
{authenticated && authUser && <footer className="module-selection-account relative h-[58px] shrink-0" aria-label="账户">
<HomeAccountMenu key={authUser.userId ?? authUser.username} accountName={userDisplayName || authUser.username}
avatarUrl={userProfile?.avatarUrl} onOpenProfile={() => {
setDismissedSyncError(null); setProfileDialogOpen(true);
+25 -3
View File
@@ -3,8 +3,9 @@
* Makelore application configuration.
*/
import { useEffect, useMemo, useState, type FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBlocker, useLocation, useNavigate } from 'react-router-dom';
import {
ArrowLeft,
ChevronDown,
ExternalLink,
FileText,
@@ -40,10 +41,12 @@ import { cn } from '@/lib/utils';
import { useSettingsStore } from '@/stores/settings';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { canLeaveCurrentPage, getSettingsReturnPath } from '@/lib/settings-navigation';
export function Settings() {
const { t } = useTranslation('settings');
const navigate = useNavigate();
const location = useLocation();
const {
setLanguage,
launchAtStartup,
@@ -146,6 +149,20 @@ export function Settings() {
proxyServer,
proxyServerDraft,
]);
const blocker = useBlocker(proxySettingsDirty || savingProxy);
useEffect(() => {
if (blocker.state !== 'blocked') return;
blocker.reset();
toast.info('请先保存或还原当前修改,再离开设置。');
}, [blocker]);
useEffect(() => {
if (!proxySettingsDirty && !savingProxy) return;
const protectUnsavedSettings = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ''; };
window.addEventListener('beforeunload', protectUnsavedSettings);
return () => window.removeEventListener('beforeunload', protectUnsavedSettings);
}, [proxySettingsDirty, savingProxy]);
const handleSaveProxySettings = async () => {
setSavingProxy(true);
@@ -237,9 +254,14 @@ export function Settings() {
};
return (
<main data-testid="settings-page" className="min-h-full bg-background">
<main data-testid="settings-page" data-settings-dirty={proxySettingsDirty || savingProxy} className="min-h-full bg-background">
<div className="mx-auto flex max-w-4xl flex-col gap-8 px-8 py-8">
<div>
<div className="flex items-center gap-3">
<Button type="button" variant="ghost" aria-label="返回上一页" data-testid="settings-back-button"
className="shrink-0 gap-2" onClick={() => {
if (!canLeaveCurrentPage()) { toast.info('请先保存或还原当前修改,再返回。'); return; }
navigate(getSettingsReturnPath(location.state));
}}><ArrowLeft className="h-4 w-4" />返回</Button>
<h1 className="text-2xl font-semibold tracking-normal">{t('title')}</h1>
</div>
+4 -10
View File
@@ -431,16 +431,10 @@
}
.module-selection-account [data-testid='home-account-controls'] {
transform: translateY(1rem);
}
@media (min-width: 640px) {
.module-selection-account [data-testid='home-account-controls'] {
transform: translate(
max(-1.5cm, calc(1rem - max(2rem, calc((100vw - 80rem) / 2)))),
2rem
);
}
position: fixed;
bottom: 12px;
left: 10px;
z-index: 40;
}
.module-option-card {