+
)}
{wsStatus === 'disconnected' && (
-
+
Connection lost.
safeGetClient()?.connect()} style={{ textDecoration: 'underline', marginLeft: 'auto', background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', padding: 0 }}>Reconnect now
)}
@@ -443,7 +446,7 @@ function ChannelPageInner() {
members={members.map((m: Member) => ({
uid: m.uid,
username: m.username,
- avatar_url: (m as any).avatar_url ?? null,
+ avatar_url: m.avatar_url ?? null,
}))}
agents={agents}
repos={repos}
@@ -484,7 +487,7 @@ function ChannelPageInner() {
thread={activeThread}
typingUsers={typingUsersList}
onClose={closeThread}
- sendMessage={(content: string, opts?: any) => sendMessage(content, opts)}
+ sendMessage={(content: string, opts?: { contentType?: string; thread?: string; inReplyTo?: string; attachmentIds?: string[] }) => sendMessage(content, opts)}
onTypingStart={() => { const c = safeGetClient(); if (c) c.sendTypingStart(roomIdParam); }}
onTypingStop={() => { const c = safeGetClient(); if (c) c.sendTypingStop(roomIdParam); }}
/>
diff --git a/src/app/project/channel/RoomSettingsModal.tsx b/src/app/project/channel/RoomSettingsModal.tsx
index 2bdb343..f883999 100644
--- a/src/app/project/channel/RoomSettingsModal.tsx
+++ b/src/app/project/channel/RoomSettingsModal.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState } from "react";
import { Settings, Trash2, Loader2, Globe, Lock } from "lucide-react";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
@@ -27,12 +27,13 @@ export function RoomSettingsModal({ open, onOpenChange }: RoomSettingsModalProps
const [isPublic, setIsPublic] = useState(true);
const [isSaving, setIsSaving] = useState(false);
- useEffect(() => {
- if (currentRoom && open) {
+ const handleOpenChange = (newOpen: boolean) => {
+ onOpenChange(newOpen);
+ if (newOpen && currentRoom) {
setName(currentRoom.room_name);
- setIsPublic(currentRoom.public);
+ setIsPublic(currentRoom.public ?? true);
}
- }, [currentRoom, open]);
+ };
const handleUpdateRoom = async () => {
if (!currentRoom || !name.trim()) return;
@@ -63,7 +64,7 @@ export function RoomSettingsModal({ open, onOpenChange }: RoomSettingsModalProps
};
return (
-
+
diff --git a/src/app/project/channel/settings/AiSettings.tsx b/src/app/project/channel/settings/AiSettings.tsx
index 95ef28d..7d1cc17 100644
--- a/src/app/project/channel/settings/AiSettings.tsx
+++ b/src/app/project/channel/settings/AiSettings.tsx
@@ -1,7 +1,7 @@
import { Loader2, Shield, Search, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { aiList, aiUpsert, aiDelete, modelCatalog } from "@/client/api";
-import type { RoomAiUpsertRequest, ModelWithPricingResponse } from "@/client/model";
+import type { RoomAiUpsertRequest, ModelWithPricingResponse, RoomAiResponse } from "@/client/model";
import { getModelIcon } from "@/lib/icons/modelIcons";
import { Plus, Trash2, Settings, X as XIcon } from "lucide-react";
import {
@@ -35,12 +35,13 @@ function ModelAvatar({ modelName, size = 36 }: { modelName: string; size?: numbe
}
return (
{modelName[0]?.toUpperCase() || "?"}
@@ -54,8 +55,8 @@ interface AiSettingsProps {
}
export function AiSettings({ roomId, onAiListChange }: AiSettingsProps) {
- const [roomAis, setRoomAis] = useState
([]);
- const [isLoadingAi, setIsLoadingAi] = useState(false);
+ const [roomAis, setRoomAis] = useState([]);
+ const [isLoadingAi, setIsLoadingAi] = useState(true);
const [showAddAi, setShowAddAi] = useState(false);
const [selectedModelFull, setSelectedModelFull] = useState(null);
const [aiParams, setAiParams] = useState({
@@ -98,7 +99,10 @@ export function AiSettings({ roomId, onAiListChange }: AiSettingsProps) {
};
useEffect(() => {
- fetchRoomAis();
+ aiList(roomId)
+ .then((res) => setRoomAis(res.data.data || []))
+ .catch((err) => console.error("Failed to fetch room AIs", err))
+ .finally(() => setIsLoadingAi(false));
}, [roomId]);
const handleAddAi = async () => {
diff --git a/src/app/project/components/ProjectCreateMenuModal.tsx b/src/app/project/components/ProjectCreateMenuModal.tsx
index 00e5b4a..6e019f7 100644
--- a/src/app/project/components/ProjectCreateMenuModal.tsx
+++ b/src/app/project/components/ProjectCreateMenuModal.tsx
@@ -77,8 +77,8 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
});
onClose();
navigate(`/${projectName}/repo/${repoForm.repo_name.trim()}`);
- } catch (err: any) {
- setError(err.response?.data?.message || "Failed to create repository.");
+ } catch (err: unknown) {
+ setError((err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to create repository.");
} finally {
setLoading(false);
}
@@ -98,8 +98,8 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
if (room) {
navigate(`/${projectName}/channel/${room.id}`);
}
- } catch (err: any) {
- setError(err.response?.data?.message || "Failed to create channel.");
+ } catch (err: unknown) {
+ setError((err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to create channel.");
} finally {
setLoading(false);
}
@@ -119,8 +119,8 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
if (res.data?.data) {
navigate(`/${projectName}/board/${res.data.data.id}`);
}
- } catch (err: any) {
- setError(err.response?.data?.message || "Failed to create board.");
+ } catch (err: unknown) {
+ setError((err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to create board.");
} finally {
setLoading(false);
}
@@ -142,8 +142,8 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
if (skill) {
navigate(`/${projectName}/skills/${skill.slug}`);
}
- } catch (err: any) {
- setError(err.response?.data?.message || "Failed to create skill.");
+ } catch (err: unknown) {
+ setError((err as { response?: { data?: { message?: string } } })?.response?.data?.message || "Failed to create skill.");
} finally {
setLoading(false);
}
@@ -179,7 +179,7 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
].map(tab => (
{ setActiveTab(tab.id as any); setError(null); }}
+ onClick={() => { setActiveTab(tab.id as typeof activeTab); setError(null); }}
className="flex-1 py-2.5 text-[12px] font-bold transition-all border-b-2 flex items-center justify-center gap-2"
style={{
borderColor: activeTab === tab.id ? "var(--accent)" : "transparent",
@@ -235,7 +235,7 @@ export function ProjectCreateMenuModal({ onClose, initialTab = "repo" }: Project
- {error &&
{error}
}
+ {error &&
{error}
}
- {error &&
{error}
}
+ {error &&
{error}
}
- {error &&
{error}
}
+ {error &&
{error}
}
- {error &&
{error}
}
+ {error &&
{error}
}
{
+ if (!projectName || !number) return;
+ if (!window.confirm("Are you sure you want to delete this comment?")) return;
+ deleteComment.mutate(
+ { projectName, issueNumber: number, commentId },
+ { onSuccess: () => refetchComments() }
+ );
+ };
+
const isOwnComment = (comment: IssueCommentResponse) => {
if (!currentUser) return false;
@@ -204,7 +214,7 @@ export function IssueDetailPage() {
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"
style={{
backgroundColor: issueDetail.state === "open" ? "var(--status-online)" : "var(--accent)",
- color: "#fff"
+ color: "var(--text-inverse)"
}}
>
{issueDetail.state}
@@ -248,7 +258,7 @@ export function IssueDetailPage() {
{issueDetail.body ? (
-
+
) : (
No description provided.
)}
@@ -294,6 +304,7 @@ export function IssueDetailPage() {
handleDeleteComment(comment.id)}
disabled={isMutating}>
@@ -326,7 +337,7 @@ export function IssueDetailPage() {
) : (
-
+
)}
diff --git a/src/app/project/issue-detail/IssueSidebar.tsx b/src/app/project/issue-detail/IssueSidebar.tsx
index 9a2356c..722f7ef 100644
--- a/src/app/project/issue-detail/IssueSidebar.tsx
+++ b/src/app/project/issue-detail/IssueSidebar.tsx
@@ -146,7 +146,7 @@ export function IssueSidebar({ projectName, issueNumber }: IssueSidebarProps) {
className="flex items-center gap-2"
onClick={() => addLabel.mutate({ projectName, issueNumber, labelId: l.id })}
>
-
+
{l.name}
{issueLabels.some(il => il.label_name === l.name) && }
@@ -167,7 +167,7 @@ export function IssueSidebar({ projectName, issueNumber }: IssueSidebarProps) {
key={l.label_name}
variant="outline"
className="text-[10px] px-1.5 py-0 h-5 border-none"
- style={{ backgroundColor: "var(--border-default)", color: "var(--text-primary)", borderLeft: `3px solid ${l.label_color ?? "#5865F2"}` }}
+ style={{ backgroundColor: "var(--border-default)", color: "var(--text-primary)", borderLeft: `3px solid ${l.label_color ?? "var(--accent)"}` }}
>
{l.label_name}
();
@@ -162,13 +163,13 @@ export function IssuesPage() {
) : (
- {filteredIssues.map((issue: any) => {
+ {filteredIssues.map((issue: IssueResponse) => {
// Find priority label if exists
- const priorityLabel = issue.labels?.find((l: any) => l.label_name?.toLowerCase().startsWith('priority:'));
- const priority = priorityLabel ? priorityLabel.label_name.split(':')[1].toLowerCase() : null;
-
+ const priorityLabel = issue.labels?.find((l: IssueLabelResponse) => l.label_name?.toLowerCase().startsWith('priority:'));
+ const priority = priorityLabel ? (priorityLabel.label_name ?? '').split(':')[1].toLowerCase() : null;
+
// Other labels
- const otherLabels = issue.labels?.filter((l: any) => !l.label_name?.toLowerCase().startsWith('priority:')) || [];
+ const otherLabels = issue.labels?.filter((l: IssueLabelResponse) => !l.label_name?.toLowerCase().startsWith('priority:')) || [];
return (
- {otherLabels.map((l: any) => (
+ {otherLabels.map((l: IssueLabelResponse) => (
({
setCurrentRoomName: () => {},
});
+// eslint-disable-next-line react-refresh/only-export-components
export const useProjectLayout = () => useContext(ProjectContext);
export function ProjectLayout() {
@@ -31,12 +32,15 @@ export function ProjectLayout() {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const { projectName } = useParams<{ projectName: string }>();
const channelMatch = useMatch("/:projectName/channel/:roomId");
+ const chatMatch = useMatch("/:projectName/chat/*");
const roomId = channelMatch?.params.roomId ?? null;
const isMobile = useIsMobile();
const isTablet = useIsTablet();
const canShowMembers = !isMobile && !isTablet;
+ const mainShouldOwnScroll = !channelMatch && !chatMatch;
+
return (
@@ -86,7 +90,7 @@ export function ProjectLayout() {
>
diff --git a/src/app/project/pulls/PullsPage.tsx b/src/app/project/pulls/PullsPage.tsx
index 60c1945..6487b9e 100644
--- a/src/app/project/pulls/PullsPage.tsx
+++ b/src/app/project/pulls/PullsPage.tsx
@@ -101,7 +101,7 @@ export function PullsPage() {
{pr.status}
diff --git a/src/app/project/repo/settings/BranchProtectionSettings.tsx b/src/app/project/repo/settings/BranchProtectionSettings.tsx
index 9a14f48..2bce7f2 100644
--- a/src/app/project/repo/settings/BranchProtectionSettings.tsx
+++ b/src/app/project/repo/settings/BranchProtectionSettings.tsx
@@ -89,7 +89,7 @@ export default function BranchProtectionSettings() {
const handleUpdate = async () => {
if (!editForm) return;
try {
- await updateMutation.mutateAsync({ ...editForm } as any);
+ await updateMutation.mutateAsync({ ...editForm });
setEditingId(null);
setEditForm(null);
setMsg({ type: "success", text: "Branch protection rule updated" });
@@ -162,7 +162,7 @@ export default function BranchProtectionSettings() {
- {branchOptions.map((b: any) => (
+ {branchOptions.map((b: { name: string }) => (
diff --git a/src/app/project/repo/settings/GeneralSettings.tsx b/src/app/project/repo/settings/GeneralSettings.tsx
index d11f168..9eeb4db 100644
--- a/src/app/project/repo/settings/GeneralSettings.tsx
+++ b/src/app/project/repo/settings/GeneralSettings.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { useParams } from "react-router-dom";
import { useRepoInfoQuery, useUpdateRepoSettingsMutation } from "@/hooks/useRepoDetailQuery";
import { Loader2, Save, Globe, EyeOff, GitBranch, Zap, RotateCcw } from "lucide-react";
@@ -27,7 +27,10 @@ export default function GeneralSettings() {
const [aiCodeReview, setAiCodeReview] = useState(false);
const [msg, setMsg] = useState<{ type: "success" | "error"; text: string } | null>(null);
- useEffect(() => {
+ // Sync form state when repoInfo changes (adjust during render, not in effect)
+ const [prevRepoInfo, setPrevRepoInfo] = useState
(undefined);
+ if (repoInfo !== prevRepoInfo) {
+ setPrevRepoInfo(repoInfo);
if (repoInfo) {
setName(repoInfo.repo_name);
setDescription(repoInfo.description ?? "");
@@ -35,7 +38,7 @@ export default function GeneralSettings() {
setIsPrivate(repoInfo.is_private);
setAiCodeReview(repoInfo.ai_code_review_enabled);
}
- }, [repoInfo]);
+ }
if (!projectName || !repoName) return null;
@@ -50,7 +53,7 @@ export default function GeneralSettings() {
ai_code_review_enabled: aiCodeReview,
});
setMsg({ type: "success", text: "Repository settings updated successfully" });
- } catch (err) {
+ } catch {
setMsg({ type: "error", text: "Failed to update repository settings" });
}
};
diff --git a/src/app/project/repos/ReposPage.tsx b/src/app/project/repos/ReposPage.tsx
index a66b8bb..3a6a3fd 100644
--- a/src/app/project/repos/ReposPage.tsx
+++ b/src/app/project/repos/ReposPage.tsx
@@ -20,6 +20,7 @@ import {
import type { ProjectRepositoryItem } from "@/client/model";
import { REPOS_PAGE } from "@/css/repo/styles";
import { useState, useMemo } from "react";
+import { ProjectCreateMenuModal } from "@/app/project/components/ProjectCreateMenuModal";
function getRelativeTime(dateStr: string | null) {
if (!dateStr) return "Never";
@@ -40,6 +41,7 @@ export function ReposPage() {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [searchQuery, setSearchQuery] = useState("");
+ const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
const { data: repos = [], isLoading, error, refetch } = useProjectReposQuery(projectName);
@@ -80,8 +82,8 @@ export function ReposPage() {
Repositories
Host and manage your project source code
- navigate(`/${projectName}/settings/repos/new`)}
+ setIsCreateMenuOpen(true)}
className={REPOS_PAGE.newBtn}
>
@@ -194,9 +196,9 @@ export function ReposPage() {
{/* New Repo Guided Card */}
{viewMode === 'grid' && !searchQuery && (
- navigate(`/${projectName}/settings/repos/new`)}
+ onClick={() => setIsCreateMenuOpen(true)}
>
Create a new repository
@@ -205,6 +207,9 @@ export function ReposPage() {
)}
)}
+ {isCreateMenuOpen && (
+ setIsCreateMenuOpen(false)} initialTab="repo" />
+ )}
);
}
diff --git a/src/app/project/settings/AccessSettings.tsx b/src/app/project/settings/AccessSettings.tsx
index 3fa72e0..0170942 100644
--- a/src/app/project/settings/AccessSettings.tsx
+++ b/src/app/project/settings/AccessSettings.tsx
@@ -6,7 +6,7 @@ import {
projectJoinSettings, projectUpdateJoinSettings,
projectJoinRequests, projectProcessJoinRequest,
} from "@/client/api";
-import type { InvitationResponse, JoinSettingsResponse, JoinRequestResponse, MemberRole } from "@/client/model";
+import type { InvitationResponse, JoinSettingsResponse, JoinRequestResponse, MemberRole, QuestionSchema } from "@/client/model";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Loader2, Mail, X, Check, Shield, User, EyeOff } from "lucide-react";
@@ -78,7 +78,7 @@ export function AccessSettings() {
const handleToggleApproval = async () => {
if (!joinSettings) return;
- try { setJsSaving(true); await projectUpdateJoinSettings(projectName!, { require_approval: !joinSettings.require_approval, require_questions: joinSettings.require_questions, questions: (joinSettings.questions as any[]) || [] }); setMsg({ type: "success", text: "Join settings updated" }); invalidateAll(); }
+ try { setJsSaving(true); await projectUpdateJoinSettings(projectName!, { require_approval: !joinSettings.require_approval, require_questions: joinSettings.require_questions, questions: (joinSettings.questions as QuestionSchema[]) || [] }); setMsg({ type: "success", text: "Join settings updated" }); invalidateAll(); }
catch { setMsg({ type: "error", text: "Failed to update join settings" }); }
finally { setJsSaving(false); }
};
diff --git a/src/app/project/settings/GeneralSettings.tsx b/src/app/project/settings/GeneralSettings.tsx
index 31678f4..ed70e77 100644
--- a/src/app/project/settings/GeneralSettings.tsx
+++ b/src/app/project/settings/GeneralSettings.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { useParams } from "react-router-dom";
import { useProjectInfo, useInvalidateProjectInfo } from "@/hooks/useProjectInfo";
import { projectExchangeName, projectExchangeTitle, projectExchangeVisibility } from "@/client/api";
@@ -35,7 +35,10 @@ export function GeneralSettings() {
const [msg, setMsg] = useState<{ type: "success" | "error"; text: string } | null>(null);
const [copied, setCopied] = useState(false);
- useEffect(() => {
+ // Sync form state when project info loads or changes
+ const [prevInfo, setPrevInfo] = useState
(undefined);
+ if (info !== prevInfo) {
+ setPrevInfo(info);
if (info) {
setForm({
name: info.name,
@@ -44,7 +47,7 @@ export function GeneralSettings() {
is_public: info.is_public,
});
}
- }, [info]);
+ }
if (!info || !projectName) return null;
@@ -84,7 +87,7 @@ export function GeneralSettings() {
await Promise.all(promises);
setMsg({ type: "success", text: "Project settings updated successfully" });
invalidateProjectInfo(projectName);
- } catch (err) {
+ } catch {
setMsg({ type: "error", text: "Failed to update project settings" });
} finally {
setSaving(null);
@@ -124,12 +127,12 @@ export function GeneralSettings() {
-
+
{(form.display_name || form.name)[0]?.toUpperCase()}
-
+
diff --git a/src/app/settings/AppearancePage.tsx b/src/app/settings/AppearancePage.tsx
index 54c344a..373995e 100644
--- a/src/app/settings/AppearancePage.tsx
+++ b/src/app/settings/AppearancePage.tsx
@@ -39,9 +39,55 @@ const TIMEZONES = [
{ value: "UTC", label: "UTC" },
];
+const SelectField = ({
+ label,
+ value,
+ onChange,
+ options,
+}: {
+ label: string;
+ value: string;
+ onChange: (v: string) => void;
+ options: { value: string; label: string }[];
+}) => (
+
+
+ {label}
+
+
+
+
+
+
+ {options.map((o) => (
+
+ {o.label}
+
+ ))}
+
+
+
+);
+
export function AppearancePage() {
const { preferences: cachedPrefs, setPreferences: setCachedPrefs } = useSettingsDataCache();
- const [_prefs, setPrefs] = useState
(cachedPrefs);
+ const [, setPrefs] = useState(cachedPrefs);
const [loading, setLoading] = useState(!cachedPrefs);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({
@@ -56,27 +102,24 @@ export function AppearancePage() {
useEffect(() => {
if (cachedPrefs) return;
- loadPrefs();
- }, []);
-
- const loadPrefs = async () => {
- try {
- setLoading(true);
- const res = await getPreferences();
- const d = res.data.data!;
- setPrefs(d);
- setCachedPrefs(d);
- setForm({
- language: d.language,
- theme: d.theme,
- timezone: d.timezone,
- });
- } catch {
- setMessage({ type: "error", text: "加载偏好设置失败" });
- } finally {
- setLoading(false);
- }
- };
+ (async () => {
+ try {
+ const res = await getPreferences();
+ const d = res.data.data!;
+ setPrefs(d);
+ setCachedPrefs(d);
+ setForm({
+ language: d.language,
+ theme: d.theme,
+ timezone: d.timezone,
+ });
+ } catch {
+ setMessage({ type: "error", text: "加载偏好设置失败" });
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [cachedPrefs, setCachedPrefs]);
const handleSave = async () => {
try {
@@ -106,52 +149,6 @@ export function AppearancePage() {
);
}
- const SelectField = ({
- label,
- value,
- onChange,
- options,
- }: {
- label: string;
- value: string;
- onChange: (v: string) => void;
- options: { value: string; label: string }[];
- }) => (
-
-
- {label}
-
-
-
-
-
-
- {options.map((o) => (
-
- {o.label}
-
- ))}
-
-
-
- );
-
return (
@@ -304,4 +301,4 @@ export function AppearancePage() {
);
-}
+}
\ No newline at end of file
diff --git a/src/app/settings/EmailPage.tsx b/src/app/settings/EmailPage.tsx
index 96e284f..fdec107 100644
--- a/src/app/settings/EmailPage.tsx
+++ b/src/app/settings/EmailPage.tsx
@@ -20,22 +20,19 @@ export function EmailPage() {
useEffect(() => {
if (cachedEmail !== null) return;
- loadEmail();
- }, []);
-
- const loadEmail = async () => {
- try {
- setLoading(true);
- const res = await apiEmailGet();
- const e = res.data.data?.email ?? null;
- setEmail(e);
- setCachedEmail(e);
- } catch {
- setMessage({ type: "error", text: "加载邮箱信息失败" });
- } finally {
- setLoading(false);
- }
- };
+ (async () => {
+ try {
+ const res = await apiEmailGet();
+ const e = res.data.data?.email ?? null;
+ setEmail(e);
+ setCachedEmail(e);
+ } catch {
+ setMessage({ type: "error", text: "加载邮箱信息失败" });
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [cachedEmail, setCachedEmail]);
const handleSave = async () => {
if (!form.new_email || !form.password) {
@@ -183,4 +180,4 @@ export function EmailPage() {
);
-}
+}
\ No newline at end of file
diff --git a/src/app/settings/MyAccountPage.tsx b/src/app/settings/MyAccountPage.tsx
index eb2c2c8..fd6b4e6 100644
--- a/src/app/settings/MyAccountPage.tsx
+++ b/src/app/settings/MyAccountPage.tsx
@@ -33,28 +33,25 @@ export function MyAccountPage() {
useEffect(() => {
if (cachedProfile) return;
- loadProfile();
- }, []);
-
- const loadProfile = async () => {
- try {
- setLoading(true);
- const res = await getMyProfile();
- const d = res.data.data!;
- setProfile(d);
- setCachedProfile(d);
- setForm({
- display_name: d.display_name ?? "",
- avatar_url: d.avatar_url ?? "",
- website_url: d.website_url ?? "",
- organization: d.organization ?? "",
- });
- } catch {
- setMessage({ type: "error", text: "加载个人信息失败" });
- } finally {
- setLoading(false);
- }
- };
+ (async () => {
+ try {
+ const res = await getMyProfile();
+ const d = res.data.data!;
+ setProfile(d);
+ setCachedProfile(d);
+ setForm({
+ display_name: d.display_name ?? "",
+ avatar_url: d.avatar_url ?? "",
+ website_url: d.website_url ?? "",
+ organization: d.organization ?? "",
+ });
+ } catch {
+ setMessage({ type: "error", text: "加载个人信息失败" });
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [cachedProfile, setCachedProfile]);
const handleSave = async () => {
try {
@@ -75,6 +72,27 @@ export function MyAccountPage() {
}
};
+ const loadProfile = async () => {
+ try {
+ setLoading(true);
+ if (cachedProfile) return;
+ const res = await getMyProfile();
+ const d = res.data.data!;
+ setProfile(d);
+ setCachedProfile(d);
+ setForm({
+ display_name: d.display_name ?? "",
+ avatar_url: d.avatar_url ?? "",
+ website_url: d.website_url ?? "",
+ organization: d.organization ?? "",
+ });
+ } catch {
+ setMessage({ type: "error", text: "加载个人信息失败" });
+ } finally {
+ setLoading(false);
+ }
+ };
+
const handleFileChange = async (e: React.ChangeEvent
) => {
const file = e.target.files?.[0];
if (!file) return;
@@ -89,14 +107,14 @@ export function MyAccountPage() {
setMessage(null);
const formData = new FormData();
formData.append("file", file);
-
- const res = await uploadAvatar(formData as any);
+
+ const res = await uploadAvatar(formData);
const newAvatarUrl = res.data.data?.avatar_url;
if (newAvatarUrl) {
setForm(f => ({ ...f, avatar_url: newAvatarUrl }));
setMessage({ type: "success", text: "头像上传成功,请保存更改" });
}
- } catch (err) {
+ } catch {
setMessage({ type: "error", text: "头像上传失败" });
} finally {
setUploading(false);
@@ -155,12 +173,12 @@ export function MyAccountPage() {
{profile?.username?.[0]?.toUpperCase() || "U"}
-
+
-
fileInputRef.current?.click()}
disabled={uploading}
@@ -169,9 +187,9 @@ export function MyAccountPage() {
上传新头像
{form.avatar_url && (
-
@@ -183,12 +201,12 @@ export function MyAccountPage() {
支持 JPG, PNG 或 GIF. 最大 2MB.
-
@@ -314,4 +332,4 @@ export function MyAccountPage() {