Files
Genarrative/apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

205 lines
6.3 KiB
TypeScript

import { RefreshCcw, Save } from 'lucide-react';
import { FormEvent, useEffect, useState } from 'react';
import {
getProfileWalletConfig,
upsertProfileWalletConfig,
} from '../api/adminApiClient';
import type { ProfileWalletConfigAdminResponse } from '../api/adminApiTypes';
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
import { handlePageError } from './pageUtils';
interface AdminProfileWalletConfigPageProps {
token: string;
result: ProfileWalletConfigAdminResponse | null;
onUnauthorized: (message?: string) => void;
onResultChange: (result: ProfileWalletConfigAdminResponse) => void;
}
export function AdminProfileWalletConfigPage({
token,
result,
onUnauthorized,
onResultChange,
}: AdminProfileWalletConfigPageProps) {
const [initialMudPoints, setInitialMudPoints] = useState('100');
const [dailyFreePointsPerDay, setDailyFreePointsPerDay] = useState('20');
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [loadErrorMessage, setLoadErrorMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
useEffect(() => {
void refreshConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
async function refreshConfig() {
setIsLoading(true);
setLoadErrorMessage('');
try {
const response = await getProfileWalletConfig(token);
onResultChange(response);
setInitialMudPoints(String(response.initialMudPoints));
setDailyFreePointsPerDay(String(response.dailyFreePointsPerDay));
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setLoadErrorMessage);
} finally {
setIsLoading(false);
}
}
async function handleSave(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isSaving) {
return;
}
const normalizedDailyFreePointsPerDay = parsePositiveInteger(
dailyFreePointsPerDay,
);
if (!normalizedDailyFreePointsPerDay) {
setErrorMessage('每日免费泥点数必须是大于 0 的整数');
return;
}
const normalizedInitialMudPoints = parsePositiveInteger(initialMudPoints);
if (!normalizedInitialMudPoints) {
setErrorMessage('账号初始泥点数必须是大于 0 的整数');
return;
}
setErrorMessage('');
const confirmed = await confirmWrite({
action: '保存账号配置',
target: `初始 ${normalizedInitialMudPoints} 泥点,每日免费 ${normalizedDailyFreePointsPerDay} 泥点`,
});
if (!confirmed) {
return;
}
setIsSaving(true);
try {
const response = await upsertProfileWalletConfig(token, {
initialMudPoints: normalizedInitialMudPoints,
dailyFreePointsPerDay: normalizedDailyFreePointsPerDay,
});
onResultChange(response);
setInitialMudPoints(String(response.initialMudPoints));
setDailyFreePointsPerDay(String(response.dailyFreePointsPerDay));
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsSaving(false);
}
}
return (
<section className="admin-page">
<div className="admin-page-heading">
<div>
<h2>账号配置</h2>
<p>泥点钱包</p>
</div>
<button
className="admin-secondary-button"
disabled={isLoading}
type="button"
onClick={refreshConfig}
>
<RefreshCcw size={17} aria-hidden="true" />
<span>{isLoading ? '刷新中' : '刷新'}</span>
</button>
</div>
{loadErrorMessage ? (
<div className="admin-alert" role="status">
{loadErrorMessage}
</div>
) : null}
<div className="admin-two-column">
<form className="admin-panel admin-form" onSubmit={handleSave}>
<label className="admin-field">
<span>账号初始泥点数</span>
<input
min={1}
step={1}
type="number"
value={initialMudPoints}
onChange={(event) => setInitialMudPoints(event.target.value)}
/>
</label>
<label className="admin-field">
<span>每日免费泥点数</span>
<input
min={1}
step={1}
type="number"
value={dailyFreePointsPerDay}
onChange={(event) => setDailyFreePointsPerDay(event.target.value)}
/>
</label>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
</div>
) : null}
<button
className="admin-primary-button"
disabled={
isSaving ||
!parsePositiveInteger(initialMudPoints) ||
!parsePositiveInteger(dailyFreePointsPerDay)
}
type="submit"
>
<Save size={17} aria-hidden="true" />
<span>{isSaving ? '保存中' : '保存'}</span>
</button>
</form>
<section className="admin-panel admin-result-panel">
<div className="admin-panel-heading">
<h3>当前配置</h3>
<span>{result?.configId ?? '-'}</span>
</div>
{result ? (
<dl className="admin-info-list">
<div>
<dt>初始泥点</dt>
<dd>{result.initialMudPoints}</dd>
</div>
<div>
<dt>每日免费泥点</dt>
<dd>{result.dailyFreePointsPerDay}</dd>
</div>
<div>
<dt>更新人</dt>
<dd>{result.updatedByDisplayName || '-'}</dd>
</div>
<div>
<dt>更新</dt>
<dd>{result.updatedAt}</dd>
</div>
</dl>
) : (
<div className="admin-empty-state">
{isLoading ? '加载中' : '暂无记录'}
</div>
)}
</section>
</div>
{confirmDialog}
</section>
);
}
function parsePositiveInteger(value: string) {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
}