diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index a7aaf31..c60ed5e 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -42,6 +42,7 @@ export function App() { const [projectModalOpen, setProjectModalOpen] = useState(false) const [profileModalOpen, setProfileModalOpen] = useState(false) const [experts, setExperts] = useState([]) + const workspaceRequestRef = useRef(0) useEffect(() => { if (!session) return @@ -51,21 +52,29 @@ export function App() { async function loadWorkspaces(activeSession = session) { if (!activeSession) return + const requestId = ++workspaceRequestRef.current setLoading(true) setError('') try { const nextProjects = await fetchProjects(activeSession) const items = await Promise.all(nextProjects.map(async (project) => [project.id, await fetchProjectWorkspace(activeSession, project.id)] as const)) + if (requestId !== workspaceRequestRef.current) return setProjects(nextProjects) setWorkspaces(Object.fromEntries(items)) setProjectId((current) => current && nextProjects.some((project) => project.id === current) ? current : '') } catch (requestError) { - setError(errorMessage(requestError)) + if (requestId === workspaceRequestRef.current) setError(errorMessage(requestError)) } finally { - setLoading(false) + if (requestId === workspaceRequestRef.current) setLoading(false) } } + async function refreshProjectWorkspace(targetProjectId: string, activeSession = session) { + if (!activeSession || !targetProjectId) return + const workspace = await fetchProjectWorkspace(activeSession, targetProjectId) + setWorkspaces((current) => ({ ...current, [targetProjectId]: workspace })) + } + async function loadAI() { if (!session || experts.length) return try { setExperts(await listAIExperts(session)) } catch (requestError) { setError(errorMessage(requestError)) } @@ -85,12 +94,13 @@ export function App() { } function logout() { + workspaceRequestRef.current += 1 localStorage.removeItem(SESSION_KEY) localStorage.removeItem('senlin-mini-session') setSession(null); setUser(null); setProjects([]); setWorkspaces({}); setProjectId(''); setView('tasks') } - if (!session) return { setSession(next); setUser(next.user); persistSession(next) }} /> + if (!session) return { workspaceRequestRef.current += 1; setProjects([]); setWorkspaces({}); setProjectId(''); setSession(next); setUser(next.user); persistSession(next) }} /> return (
@@ -109,13 +119,13 @@ export function App() { 全部项目 {projects.map((project) => {project.name})} - )} @@ -124,7 +134,7 @@ export function App() { workspace.tags.map((tag) => tag.name))} onClose={() => setTaskEditor(null)} onSave={async (draft) => { if (!taskEditor) return await updateTask(session, taskEditor.projectId, taskEditor.id, { title: draft.title, description: draft.summary, tag: draft.tag, completed: draft.completed }) - await loadWorkspaces() + await refreshProjectWorkspace(taskEditor.projectId) setTaskEditor(null) }} /> setProjectModalOpen(false)} onSave={async (name) => { @@ -271,24 +281,25 @@ function Panel({ title, subtitle, children }: { title: string; subtitle: string; function ProfileMenu({ user, onProfile, onLogout }: { user: CurrentUser | null; onProfile: () => void; onLogout: () => void }) { const menu = 修改资料退出登录 - return + return } function TaskEditor({ task, tags, onClose, onSave }: { task: TaskWithProject | null; tags: string[]; onClose: () => void; onSave: (draft: { title: string; summary: string; tag: string; completed: boolean }) => Promise }) { - const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); const [tag, setTag] = useState(''); const [completed, setCompleted] = useState(false); const [saving, setSaving] = useState(false) - useEffect(() => { if (task) { setTitle(task.title); setSummary(task.summary); setTag(task.tag); setCompleted(task.completed) } }, [task]) - return { if (!title.trim()) return; setSaving(true); try { await onSave({ title: title.trim(), summary: summary.trim(), tag, completed }) } finally { setSaving(false) } }}>
+ const [title, setTitle] = useState(''); const [summary, setSummary] = useState(''); const [tag, setTag] = useState(''); const [completed, setCompleted] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState('') + useEffect(() => { if (task) { setTitle(task.title); setSummary(task.summary); setTag(task.tag); setCompleted(task.completed); setError('') } }, [task]) + return { if (!title.trim()) return; setSaving(true); setError(''); try { await onSave({ title: title.trim(), summary: summary.trim(), tag, completed }) } catch (requestError) { setError(errorMessage(requestError)) } finally { setSaving(false) } }}>
{error ? : null} setCompleted(completed)} />
} function ProjectModal({ visible, onClose, onSave }: { visible: boolean; onClose: () => void; onSave: (name: string) => Promise }) { - const [name, setName] = useState(''); const [saving, setSaving] = useState(false) - return { if (!name.trim()) return; setSaving(true); try { await onSave(name.trim()); setName('') } finally { setSaving(false) } }}> + const [name, setName] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState('') + useEffect(() => { if (visible) setError('') }, [visible]) + return { if (!name.trim()) return; setSaving(true); setError(''); try { await onSave(name.trim()); setName('') } catch (requestError) { setError(errorMessage(requestError)) } finally { setSaving(false) } }}>{error ? : null} } function ProfileModal({ user, visible, onClose, onSave }: { user: CurrentUser | null; visible: boolean; onClose: () => void; onSave: (input: { displayName: string; currentPassword?: string; newPassword?: string }) => Promise }) { - const [displayName, setDisplayName] = useState(user?.displayName ?? ''); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [saving, setSaving] = useState(false) - useEffect(() => setDisplayName(user?.displayName ?? ''), [user]) - return { setSaving(true); try { await onSave({ displayName, currentPassword: currentPassword || undefined, newPassword: newPassword || undefined }); setCurrentPassword(''); setNewPassword('') } finally { setSaving(false) } }}>
+ const [displayName, setDisplayName] = useState(user?.displayName ?? ''); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState('') + useEffect(() => { setDisplayName(user?.displayName ?? ''); if (visible) setError('') }, [user, visible]) + return { if (!displayName.trim()) return; setSaving(true); setError(''); try { await onSave({ displayName: displayName.trim(), currentPassword: currentPassword || undefined, newPassword: newPassword || undefined }); setCurrentPassword(''); setNewPassword('') } catch (requestError) { setError(errorMessage(requestError)) } finally { setSaving(false) } }}>
{error ? : null}
} function Login({ onLogin }: { onLogin: (session: ApiSession & { user: CurrentUser }) => void }) { diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 869b6d3..d4e5ff8 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -94,4 +94,4 @@ button { font: inherit; } .login-page { display: grid; min-height: 100vh; place-items: center; padding: 20px; background: #edf2f9; }.login-card { display: grid; width: min(100%, 360px); gap: 12px; padding: 28px; border: 1px solid #e5e6eb; border-radius: 16px; background: #fff; box-shadow: 0 18px 48px rgba(31, 35, 41, .12); }.login-card > img { width: 34px; }.login-card .arco-typography { margin: 0; }.login-card label { display: grid; gap: 5px; color: #4e5969; font-size: 12px; } .theme-dark .project-strip, .theme-dark .app-header, .theme-dark .task-section, .theme-dark .detail-row, .theme-dark .tag-filter button { color: var(--text); background: var(--surface); }.theme-dark .project-pill { color: var(--muted); }.theme-dark .project-pill.active, .theme-dark .channel-rail button.active { color: #8ac7ff; background: #1d4d77; }.theme-dark .task-row:hover { background: #293847; border-color: #477da7; }.theme-dark .section-label span { color: #8ac7ff; background: #1d4d77; } -@media (max-width: 360px) { .profile-trigger > span { display: none; }.project-pill { max-width: 90px; }.project-strip { gap: 2px; padding-inline: 5px; }.app-main { padding: 13px 9px 20px; }.project-page-header { width: calc(100% + 18px); margin: -13px -9px 15px; padding: 17px 9px 14px; }.document-actions { width: 100%; justify-content: flex-start; }.document-actions .arco-btn { padding-inline: 8px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; }.stat-grid { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 360px) { .profile-trigger > span, .project-cycle { display: none; }.project-pill { max-width: 90px; }.project-strip { gap: 2px; padding-inline: 5px; }.app-main { padding: 13px 9px 20px; }.project-page-header { width: calc(100% + 18px); margin: -13px -9px 15px; padding: 17px 9px 14px; }.document-actions { width: 100%; justify-content: flex-start; }.document-actions .arco-btn { padding-inline: 8px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; }.stat-grid { grid-template-columns: repeat(2, 1fr); } }