Add skill for gameplay entry type workflows

This commit is contained in:
2026-05-04 02:32:38 +08:00
parent 49aad7311c
commit 34aecdddf1
77 changed files with 5997 additions and 110 deletions

View File

@@ -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;
}