071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
205 lines
6.3 KiB
TypeScript
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;
|
|
}
|