fix(people): 本人 can edit their own birth data in 星盘档案 (S1, BUG-1081)
The owner had no edit entry anywhere once /people replaced the settings chart pane. 本人 now gets 编辑 with the same form; the save goes through PATCH /api/account via a shared lib/self-profile-save.ts that the home onboarding also uses. The dead saveProfile is removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
adbb205440
commit
8415fdca72
@@ -317,7 +317,6 @@ export default function Home() {
|
||||
closeAccountDialog,
|
||||
persistAvatar,
|
||||
persistGender,
|
||||
saveProfile,
|
||||
saveOnboardingName,
|
||||
saveOnboardingBirth,
|
||||
saveOnboardingPlace,
|
||||
@@ -328,9 +327,8 @@ export default function Home() {
|
||||
birthTimeRevisionPendingRef, dialogReturnTargetRef,
|
||||
setAccount, setAccountError, setAccountMenuOpen, setActiveAccountDialog,
|
||||
setBirthTimeAssessmentPhase,
|
||||
setBirthTimeConsultationConsent, setBirthTimeError, setBirthTimeJourney, setBillingPane, setDraft,
|
||||
setBirthTimeError, setBirthTimeJourney, setBillingPane, setDraft,
|
||||
setOnboardingJustCompleted, setOnboardingStep, setPresetMessageLength,
|
||||
setRectificationError,
|
||||
setSigningOut, setStartGreeting, signingOut, uiPreview,
|
||||
initialProfile: () => warmStart()?.profile ?? null,
|
||||
onOwnBirthPlaceSaved: birthSkyReveal.begin,
|
||||
|
||||
@@ -1923,7 +1923,6 @@ button:disabled:where(:not([data-slot="button"])) { cursor: default; opacity: .4
|
||||
.profile-gender-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); }
|
||||
.profile-gender-option:disabled { cursor: default; opacity: 0.6; }
|
||||
.profile-gender-hint { margin: 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
|
||||
.people-archive-fact-note { display: block; margin-top: var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); }
|
||||
.account-menu-danger, .account-menu-danger > svg { color: var(--color-danger); }
|
||||
.account-menu-danger[data-highlighted] { background: var(--color-danger-muted); }
|
||||
|
||||
@@ -6087,7 +6086,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
cursor: pointer;
|
||||
}
|
||||
.people-archive-secondary:hover { background: var(--color-canvas-muted); }
|
||||
.people-archive-foot { display: grid; text-align: center; }
|
||||
.people-archive-foot { display: grid; gap: var(--space-2); justify-items: center; text-align: center; }
|
||||
.people-archive-confirm {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
|
||||
@@ -9,6 +9,10 @@ import { ChartProfileForm } from "@/components/chart-profile-form";
|
||||
import { SecondaryPageShell } from "@/components/secondary-page-shell";
|
||||
import { bindCurrentSubjectAccount, invalidateSubjectCatalog, setCurrentSubject, useCurrentSubject } from "@/lib/current-subject";
|
||||
import { fetchAccount } from "@/lib/home-cloud-sync";
|
||||
import { clearHomeWarmSnapshot } from "@/lib/home-warm-snapshot";
|
||||
import { invalidateEphemerisPage, pinChartSnapshotIdentity } from "@/lib/secondary-page-data";
|
||||
import { saveSelfGender, saveSelfProfile } from "@/lib/self-profile-save";
|
||||
import { useSessionList } from "@/hooks/use-session-list";
|
||||
import {
|
||||
missingOtherProfileStep,
|
||||
profileBirthTimeLabel,
|
||||
@@ -41,8 +45,6 @@ const DELETE_FAILED = "删除失败,请重试";
|
||||
const NEW_PERSON_PARAM = "new";
|
||||
|
||||
const LIMIT_COPY = "最多保存 5 个人的星盘";
|
||||
/** 本人 has no edit view here; the owner's gender is set in 设置 → 个人资料. */
|
||||
const SELF_GENDER_NOTE = "在「设置 → 个人资料」里改";
|
||||
|
||||
/** D1: below this width the page is list → detail; at and above it, two columns. */
|
||||
const PEOPLE_TWO_COLUMN_QUERY = "(min-width: 1024px)";
|
||||
@@ -82,6 +84,8 @@ export function PeoplePage() {
|
||||
const [pendingDelete, setPendingDelete] = useState<Usage | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const pushedDetail = useRef(false);
|
||||
const accountIdRef = useRef("");
|
||||
const { setAccount } = useSessionList();
|
||||
|
||||
async function load() {
|
||||
setStatus("loading");
|
||||
@@ -98,6 +102,7 @@ export function PeoplePage() {
|
||||
const profile = readProfile(row.profile);
|
||||
return [{ id: row.id, role: "other" as const, profile, incomplete: incompleteProfile(profile) }];
|
||||
});
|
||||
accountIdRef.current = account.user.id;
|
||||
bindCurrentSubjectAccount(account.user.id, others.map((person) => person.id));
|
||||
setPeople([
|
||||
{ id: "self", role: "self", profile: self, incomplete: incompleteProfile(self) },
|
||||
@@ -220,6 +225,42 @@ export function PeoplePage() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本人 (BUG-1081). The owner is never a `chart_profiles` row: birth data goes
|
||||
* through the one account write (`PATCH /api/account`, `birth_time_source`
|
||||
* checked there), gender through its own one-field PATCH. Then everything
|
||||
* that painted the old birth data forgets it: the layout's account, Home's
|
||||
* warm snapshot and today's card, the chart and ephemeris caches.
|
||||
*/
|
||||
async function saveSelf(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const current = people.find((person) => person.role === "self");
|
||||
if (!current || missingOtherProfileStep(draft) || saving) return;
|
||||
setSaving(true);
|
||||
setNotice("");
|
||||
try {
|
||||
const saved = await saveSelfProfile(draft);
|
||||
const gender = draft.gender ?? null;
|
||||
if (gender !== (current.profile.gender ?? null)) await saveSelfGender(gender);
|
||||
const profile: Profile = { ...saved.profile, gender };
|
||||
if (saved.fingerprint) pinChartSnapshotIdentity({ accountId: accountIdRef.current, fingerprint: saved.fingerprint });
|
||||
clearHomeWarmSnapshot();
|
||||
invalidateEphemerisPage();
|
||||
setAccount((account) => account ? {
|
||||
...account,
|
||||
profile,
|
||||
...(saved.fingerprint ? { chartBirth: { fingerprint: saved.fingerprint } } : {}),
|
||||
} : account);
|
||||
setEditing(false);
|
||||
invalidateSubjectCatalog();
|
||||
await load();
|
||||
} catch (caught) {
|
||||
setNotice(caught instanceof Error && caught.message ? caught.message : "保存失败,请重试");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function askDelete() {
|
||||
if (!selected || selected.role !== "other") return;
|
||||
setPendingDelete(null);
|
||||
@@ -285,6 +326,7 @@ export function PeoplePage() {
|
||||
onCancelEdit={() => setEditing(false)}
|
||||
onDraftChange={setDraft}
|
||||
saveOther={saveOther}
|
||||
saveSelf={saveSelf}
|
||||
askDelete={askDelete}
|
||||
onCancelDelete={() => setPendingDelete(null)}
|
||||
confirmDelete={confirmDelete}
|
||||
@@ -323,6 +365,7 @@ export type PeopleArchiveViewProps = {
|
||||
readonly onCancelEdit: () => void;
|
||||
readonly onDraftChange: Dispatch<SetStateAction<Profile>>;
|
||||
readonly saveOther: (event: FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
readonly saveSelf: (event: FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
readonly askDelete: () => Promise<void>;
|
||||
readonly onCancelDelete: () => void;
|
||||
readonly confirmDelete: () => Promise<void>;
|
||||
@@ -352,6 +395,7 @@ export function PeopleArchiveView({
|
||||
onCancelEdit,
|
||||
onDraftChange,
|
||||
saveOther,
|
||||
saveSelf,
|
||||
askDelete,
|
||||
onCancelDelete,
|
||||
confirmDelete,
|
||||
@@ -359,6 +403,7 @@ export function PeopleArchiveView({
|
||||
}: PeopleArchiveViewProps) {
|
||||
const remaining = MAX_SAVED_SUBJECTS - people.length;
|
||||
const deleteNotice = notice === DELETE_USAGE_UNAVAILABLE || notice === DELETE_FAILED;
|
||||
const editingSelf = editing && selectedId !== "" && selected?.role === "self";
|
||||
return (
|
||||
<div className="people-archive" data-view={view}>
|
||||
<section className="people-archive-list" aria-label="星盘档案">
|
||||
@@ -399,14 +444,14 @@ export function PeopleArchiveView({
|
||||
</button>
|
||||
{editing ? (
|
||||
<ChartProfileForm
|
||||
title={selectedId ? "编辑人物" : "添加一个人"}
|
||||
description="称呼和出生资料。填报的出生时间保存后不能再改。"
|
||||
title={editingSelf ? "编辑我的资料" : selectedId ? "编辑人物" : "添加一个人"}
|
||||
description={editingSelf ? "称呼和出生资料。" : "称呼和出生资料。填报的出生时间保存后不能再改。"}
|
||||
value={draft}
|
||||
onChange={onDraftChange}
|
||||
nameInputId="people-archive-name"
|
||||
showAyanamsa
|
||||
showGender
|
||||
onSubmit={(event) => void saveOther(event)}
|
||||
onSubmit={(event) => void (editingSelf ? saveSelf(event) : saveOther(event))}
|
||||
onCancel={onCancelEdit}
|
||||
submitLabel={saving ? "保存中" : "保存"}
|
||||
submitDisabled={saving || Boolean(missingOtherProfileStep(draft))}
|
||||
@@ -438,10 +483,7 @@ export function PeopleArchiveView({
|
||||
<dt>岁差</dt>
|
||||
<dd>{peopleAyanamsaLabel(selected.profile)}</dd>
|
||||
<dt>性别</dt>
|
||||
<dd>
|
||||
{profileGenderLabel(selected.profile.gender)}
|
||||
{selected.role === "self" ? <small className="people-archive-fact-note">{SELF_GENDER_NOTE}</small> : null}
|
||||
</dd>
|
||||
<dd>{profileGenderLabel(selected.profile.gender)}</dd>
|
||||
</dl>
|
||||
<div className="people-archive-actions">
|
||||
<button type="button" onClick={() => openPerson(selected.id, newChatHref())}>
|
||||
@@ -457,10 +499,10 @@ export function PeopleArchiveView({
|
||||
{selected.role === "other" ? (
|
||||
<button type="button" className="people-archive-secondary" onClick={() => window.location.assign(`/?synastry=${selected.id}`)}>和我合盘</button>
|
||||
) : null}
|
||||
{/* BUG-1081: 本人 is edited here too; only deleting stays off. */}
|
||||
<div className="people-archive-foot">
|
||||
{selected.role === "self" ? <p>本人不能删除</p> : (
|
||||
<button type="button" className="people-archive-secondary" onClick={() => onEdit(selected.profile)}>编辑</button>
|
||||
)}
|
||||
{selected.role === "self" ? <p>本人不能删除</p> : null}
|
||||
<button type="button" className="people-archive-secondary" onClick={() => onEdit(selected.profile)}>编辑</button>
|
||||
</div>
|
||||
{notice ? <p className="form-error" role="alert">{notice}</p> : null}
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,9 @@ import {
|
||||
type BeamAvatarPatch,
|
||||
} from "@/lib/beam-avatar";
|
||||
import {
|
||||
applyPersistedBirthTime,
|
||||
birthTimePersistenceValues,
|
||||
isBirthTimeDraftReady,
|
||||
} from "@/lib/birth-time-intake-model";
|
||||
import {
|
||||
birthTimeConsultationOptionsCopy,
|
||||
createBirthTimeConsultationConsentState,
|
||||
type BirthTimeConsultationConsentState,
|
||||
type LatestAccountRequestGuard,
|
||||
} from "@/lib/birth-time-consultation-consent";
|
||||
import { readChartBirthFingerprint } from "@/lib/chart-birth-truth";
|
||||
@@ -30,13 +25,12 @@ import {
|
||||
pinChartSnapshotIdentity,
|
||||
} from "@/lib/secondary-page-data";
|
||||
import {
|
||||
birthProfileDeclarationChanged,
|
||||
missingProfileStep,
|
||||
readProfile,
|
||||
selectedBirthPlace,
|
||||
} from "@/lib/home-profile";
|
||||
import { resolveAyanamsa } from "@/lib/ayanamsa";
|
||||
import { withSavedGender, type ProfileGender } from "@/lib/profile-gender";
|
||||
import { saveSelfProfile } from "@/lib/self-profile-save";
|
||||
import { createStartGreeting } from "@/lib/starter-greeting";
|
||||
import { clearHomeWarmSnapshot } from "@/lib/home-warm-snapshot";
|
||||
import {
|
||||
@@ -68,7 +62,6 @@ export type ProfileOnboardingParams = {
|
||||
setAccountMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setActiveAccountDialog: Dispatch<SetStateAction<AccountDialog | null>>;
|
||||
setBirthTimeAssessmentPhase: Dispatch<SetStateAction<BirthTimeAssessmentPhase | null>>;
|
||||
setBirthTimeConsultationConsent: Dispatch<SetStateAction<BirthTimeConsultationConsentState>>;
|
||||
setBirthTimeError: Dispatch<SetStateAction<string>>;
|
||||
setBirthTimeJourney: Dispatch<SetStateAction<JourneyClientResponse | null>>;
|
||||
setBillingPane: Dispatch<SetStateAction<OpenAccountDialogOptions>>;
|
||||
@@ -76,7 +69,6 @@ export type ProfileOnboardingParams = {
|
||||
setOnboardingJustCompleted: Dispatch<SetStateAction<boolean>>;
|
||||
setOnboardingStep: Dispatch<SetStateAction<OnboardingStep>>;
|
||||
setPresetMessageLength: Dispatch<SetStateAction<number>>;
|
||||
setRectificationError: Dispatch<SetStateAction<string>>;
|
||||
setSigningOut: Dispatch<SetStateAction<boolean>>;
|
||||
setStartGreeting: Dispatch<SetStateAction<string>>;
|
||||
signingOut: boolean;
|
||||
@@ -99,7 +91,6 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
setAccountMenuOpen,
|
||||
setActiveAccountDialog,
|
||||
setBirthTimeAssessmentPhase,
|
||||
setBirthTimeConsultationConsent,
|
||||
setBirthTimeError,
|
||||
setBirthTimeJourney,
|
||||
setBillingPane,
|
||||
@@ -107,7 +98,6 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
setOnboardingJustCompleted,
|
||||
setOnboardingStep,
|
||||
setPresetMessageLength,
|
||||
setRectificationError,
|
||||
setSigningOut,
|
||||
setStartGreeting,
|
||||
signingOut,
|
||||
@@ -227,42 +217,9 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
async function persistProfile(nextProfile: Profile): Promise<Profile> {
|
||||
if (!account) throw new Error("账户尚未加载完成");
|
||||
if (process.env.NODE_ENV === "development" && uiPreview.current) return nextProfile;
|
||||
const birthPlace = selectedBirthPlace(nextProfile);
|
||||
const response = await fetch("/api/account", {
|
||||
method: "PATCH",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: nextProfile.name.trim() || null,
|
||||
birth_date: nextProfile.date || null,
|
||||
...birthTimePersistenceValues(nextProfile),
|
||||
country_code: nextProfile.countryCode,
|
||||
province_code: nextProfile.provinceCode || null,
|
||||
city_code: nextProfile.cityCode || null,
|
||||
district_code: nextProfile.districtCode || null,
|
||||
birth_place_label: nextProfile.birthPlaceLabel || null,
|
||||
birth_place_type: nextProfile.birthPlaceType || null,
|
||||
birth_place_provider: nextProfile.birthPlaceProvider || null,
|
||||
birth_place_provider_id: nextProfile.birthPlaceProviderId || null,
|
||||
latitude: birthPlace?.lat ?? null,
|
||||
longitude: birthPlace?.lon ?? null,
|
||||
timezone_id: nextProfile.timezoneId || null,
|
||||
timezone_offset: birthPlace?.tz ?? null,
|
||||
timezone_source: nextProfile.timezoneSource || null,
|
||||
ayanamsa: resolveAyanamsa(nextProfile),
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as {
|
||||
error?: string;
|
||||
birthTime?: unknown;
|
||||
chartBirth?: { fingerprint?: unknown };
|
||||
} | null;
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error || "账户资料暂时无法保存。");
|
||||
}
|
||||
const fingerprint = readChartBirthFingerprint(payload);
|
||||
// One write path for the owner's birth data, shared with 星盘档案 (BUG-1081).
|
||||
const { profile: savedProfile, fingerprint } = await saveSelfProfile(nextProfile);
|
||||
if (fingerprint) pinChartSnapshotIdentity({ accountId: account.user.id, fingerprint });
|
||||
const savedProfile = applyPersistedBirthTime(nextProfile, payload?.birthTime);
|
||||
setAccount((current) => current ? { ...current, profile: savedProfile } : current);
|
||||
return savedProfile;
|
||||
}
|
||||
@@ -290,32 +247,6 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
return assessedProfile;
|
||||
}
|
||||
|
||||
async function saveProfile(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!profileDraft.name.trim() || !isBirthTimeDraftReady(profileDraft) || !selectedBirthPlace(profileDraft) || !account || profileSaving) return;
|
||||
setProfileSaving(true);
|
||||
setProfileNotice("");
|
||||
setAccountError("");
|
||||
try {
|
||||
const declarationChanged = birthProfileDeclarationChanged(profile, profileDraft);
|
||||
const savedProfile = await persistProfile(profileDraft);
|
||||
setProfile(savedProfile);
|
||||
setProfileDraft(savedProfile);
|
||||
setRectificationError("");
|
||||
if (declarationChanged) {
|
||||
setBirthTimeConsultationConsent(createBirthTimeConsultationConsentState());
|
||||
void refreshAccount();
|
||||
}
|
||||
setProfileNotice(savedProfile.birthTimeStatus === "confirmed"
|
||||
? "出生资料已保存到云端,可在同一账号的其他设备使用。"
|
||||
: `出生资料已保存。${birthTimeConsultationOptionsCopy(savedProfile)}`);
|
||||
} catch (caught) {
|
||||
setAccountError(friendlyError(caught instanceof Error ? caught.message : "出生资料保存失败"));
|
||||
} finally {
|
||||
setProfileSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOnboardingName() {
|
||||
const name = composerDraftSnapshot().replace(/\s+/g, " ").trim().slice(0, 80);
|
||||
if (!name || !account || profileSaving) return;
|
||||
@@ -456,7 +387,6 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
|
||||
persistGender,
|
||||
persistProfile,
|
||||
assessSavedBirthTime,
|
||||
saveProfile,
|
||||
saveOnboardingName,
|
||||
saveOnboardingBirth,
|
||||
editDeclaredBirthTimeDetails,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { resolveAyanamsa } from "./ayanamsa.ts";
|
||||
import { applyPersistedBirthTime, birthTimePersistenceValues } from "./birth-time-intake-model.ts";
|
||||
import { readChartBirthFingerprint } from "./chart-birth-truth.ts";
|
||||
import { selectedBirthPlace } from "./home-profile.ts";
|
||||
import type { Profile } from "./home-types.ts";
|
||||
import type { ProfileGender } from "./profile-gender.ts";
|
||||
|
||||
/**
|
||||
* The owner's own profile write (`PATCH /api/account`), shared by the home
|
||||
* onboarding and the 星盘档案 edit view (TASK-self-edit-avatar-menu-20260928
|
||||
* S1, BUG-1081). The owner is never a `chart_profiles` row; every birth-data
|
||||
* save goes through the account route, which checks `birth_time_source` and
|
||||
* derives the birth-time status on the server.
|
||||
*
|
||||
* Birth-data saves never carry `gender` (TASK-consult-gender-optional): the
|
||||
* owner's gender is its own one-field PATCH, so a birth-data save can never
|
||||
* clear it.
|
||||
*/
|
||||
export function selfProfilePatchBody(nextProfile: Profile) {
|
||||
const birthPlace = selectedBirthPlace(nextProfile);
|
||||
return {
|
||||
name: nextProfile.name.trim() || null,
|
||||
birth_date: nextProfile.date || null,
|
||||
...birthTimePersistenceValues(nextProfile),
|
||||
country_code: nextProfile.countryCode,
|
||||
province_code: nextProfile.provinceCode || null,
|
||||
city_code: nextProfile.cityCode || null,
|
||||
district_code: nextProfile.districtCode || null,
|
||||
birth_place_label: nextProfile.birthPlaceLabel || null,
|
||||
birth_place_type: nextProfile.birthPlaceType || null,
|
||||
birth_place_provider: nextProfile.birthPlaceProvider || null,
|
||||
birth_place_provider_id: nextProfile.birthPlaceProviderId || null,
|
||||
latitude: birthPlace?.lat ?? null,
|
||||
longitude: birthPlace?.lon ?? null,
|
||||
timezone_id: nextProfile.timezoneId || null,
|
||||
timezone_offset: birthPlace?.tz ?? null,
|
||||
timezone_source: nextProfile.timezoneSource || null,
|
||||
ayanamsa: resolveAyanamsa(nextProfile),
|
||||
};
|
||||
}
|
||||
|
||||
export type SelfProfileSaved = Readonly<{
|
||||
/** The draft with the birth-time truth the server returned applied. */
|
||||
profile: Profile;
|
||||
/** The chart page's `profileFingerprint` of the saved row, when the route returned one. */
|
||||
fingerprint: string | null;
|
||||
}>;
|
||||
|
||||
type Fetch = typeof fetch;
|
||||
|
||||
export async function saveSelfProfile(nextProfile: Profile, fetchImpl: Fetch = fetch): Promise<SelfProfileSaved> {
|
||||
const response = await fetchImpl("/api/account", {
|
||||
method: "PATCH",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(selfProfilePatchBody(nextProfile)),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as {
|
||||
error?: string;
|
||||
birthTime?: unknown;
|
||||
chartBirth?: { fingerprint?: unknown };
|
||||
} | null;
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error || "账户资料暂时无法保存。");
|
||||
}
|
||||
const savedProfile = applyPersistedBirthTime(nextProfile, payload?.birthTime);
|
||||
return { profile: savedProfile, fingerprint: readChartBirthFingerprint(payload) };
|
||||
}
|
||||
|
||||
/** The owner's optional gender, on its own; null clears it. */
|
||||
export async function saveSelfGender(gender: ProfileGender | null, fetchImpl: Fetch = fetch): Promise<void> {
|
||||
const response = await fetchImpl("/api/account", {
|
||||
method: "PATCH",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ gender }),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as { error?: string } | null;
|
||||
if (!response.ok) throw new Error(payload?.error || "性别没保存上,再试一次。");
|
||||
}
|
||||
Reference in New Issue
Block a user