Add skill for gameplay entry type workflows
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import {PowerOff, Save} from 'lucide-react';
|
||||
import {FormEvent, useState} from 'react';
|
||||
import {PowerOff, RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useState} from 'react';
|
||||
|
||||
import {
|
||||
disableProfileRedeemCode,
|
||||
listProfileRedeemCodes,
|
||||
upsertProfileRedeemCode,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
@@ -40,8 +41,29 @@ export function AdminRedeemCodePage({
|
||||
const [disableCode, setDisableCode] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [disableErrorMessage, setDisableErrorMessage] = useState('');
|
||||
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||
const [entries, setEntries] = useState<ProfileRedeemCodeAdminResponse[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDisabling, setIsDisabling] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshRedeemCodes();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
async function refreshRedeemCodes() {
|
||||
setIsLoading(true);
|
||||
setListErrorMessage('');
|
||||
try {
|
||||
const response = await listProfileRedeemCodes(token);
|
||||
setEntries(response.entries);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setListErrorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
@@ -63,6 +85,8 @@ export function AdminRedeemCodePage({
|
||||
mode === 'private' ? splitLines(allowedPublicUserCodes) : [],
|
||||
});
|
||||
onResultChange(response);
|
||||
upsertEntry(response);
|
||||
fillForm(response);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
@@ -83,6 +107,8 @@ export function AdminRedeemCodePage({
|
||||
code: disableCode.trim(),
|
||||
});
|
||||
onResultChange(response);
|
||||
upsertEntry(response);
|
||||
fillForm(response);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setDisableErrorMessage);
|
||||
} finally {
|
||||
@@ -90,6 +116,34 @@ export function AdminRedeemCodePage({
|
||||
}
|
||||
}
|
||||
|
||||
function upsertEntry(next: ProfileRedeemCodeAdminResponse) {
|
||||
setEntries((current) => {
|
||||
const rest = current.filter((entry) => entry.code !== next.code);
|
||||
return [...rest, next].sort((left, right) => {
|
||||
const leftUpdatedAt = Date.parse(left.updatedAt);
|
||||
const rightUpdatedAt = Date.parse(right.updatedAt);
|
||||
if (Number.isFinite(leftUpdatedAt) && Number.isFinite(rightUpdatedAt)) {
|
||||
const updatedCompare = rightUpdatedAt - leftUpdatedAt;
|
||||
if (updatedCompare !== 0) {
|
||||
return updatedCompare;
|
||||
}
|
||||
}
|
||||
return left.code.localeCompare(right.code);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fillForm(entry: ProfileRedeemCodeAdminResponse) {
|
||||
setCode(entry.code);
|
||||
setMode(entry.mode);
|
||||
setRewardPoints(String(entry.rewardPoints));
|
||||
setMaxUses(String(entry.maxUses));
|
||||
setEnabled(entry.enabled);
|
||||
setAllowedUserIds(entry.allowedUserIds.join('\n'));
|
||||
setAllowedPublicUserCodes('');
|
||||
setDisableCode(entry.code);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-heading">
|
||||
@@ -97,8 +151,23 @@ export function AdminRedeemCodePage({
|
||||
<h2>兑换码</h2>
|
||||
<p>创建、更新与停用</p>
|
||||
</div>
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
onClick={refreshRedeemCodes}
|
||||
>
|
||||
<RefreshCcw size={17} aria-hidden="true" />
|
||||
<span>{isLoading ? '刷新中' : '刷新'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{listErrorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{listErrorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-two-column admin-two-column-wide">
|
||||
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
||||
<div className="admin-form-row">
|
||||
@@ -200,6 +269,48 @@ export function AdminRedeemCodePage({
|
||||
</form>
|
||||
|
||||
<div className="admin-stack">
|
||||
<section className="admin-panel">
|
||||
<div className="admin-panel-heading">
|
||||
<h3>兑换码列表</h3>
|
||||
<span>{entries.length}</span>
|
||||
</div>
|
||||
{entries.length ? (
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table admin-table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>奖励</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry) => (
|
||||
<tr key={entry.code}>
|
||||
<td>
|
||||
<button
|
||||
className="admin-text-button"
|
||||
type="button"
|
||||
onClick={() => fillForm(entry)}
|
||||
>
|
||||
{entry.code}
|
||||
</button>
|
||||
<small>{redeemModeLabel(entry.mode)}</small>
|
||||
</td>
|
||||
<td>{entry.rewardPoints}</td>
|
||||
<td>{entry.enabled ? '启用' : '停用'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-empty-state">
|
||||
{isLoading ? '加载中' : '暂无兑换码'}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<form className="admin-panel admin-form" onSubmit={handleDisable}>
|
||||
<label className="admin-field">
|
||||
<span>停用 Code</span>
|
||||
@@ -273,3 +384,7 @@ function parsePositiveInteger(value: string) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function redeemModeLabel(value: ProfileRedeemCodeMode) {
|
||||
return redeemModes.find((item) => item.value === value)?.label ?? value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user