Files
browser/platform_web/components/GameGiftCatalogPanel.tsx
T

26 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CheckCircle2, Gift, RefreshCw, Send } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { GameGiftCatalogResponse, GameGiftGrantResponse, GameGiftRevisionResponse, GamePlayerResponse } from "../api/types";
import { ErrorState, LoadingState } from "./StateViews";
type Ready = { catalogs: GameGiftCatalogResponse[]; revisions: GameGiftRevisionResponse[]; grants: GameGiftGrantResponse[]; players: GamePlayerResponse[]; selectedCatalog?: string; selectedRevision?: string; selectedPlayer?: string; name: string; notice: string; item: "bandage" | "water-bottle" | "improvised-spear"; quantity: number; note?: string };
type State = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; value: Ready };
const itemLabels = { bandage: "绷带", "water-bottle": "饮用水", "improvised-spear": "简易长矛" } as const;
export function GameGiftCatalogPanel({ serverInstanceId }: { serverInstanceId: string }) {
const [state, setState] = useState<State>({ status: "loading" });
const load = useCallback(async () => { setState({ status: "loading" }); try { const [catalogs, grants, players] = await Promise.all([platformApiClient.listGameGiftCatalogs(serverInstanceId), platformApiClient.listGameGiftGrants(serverInstanceId), platformApiClient.listGamePlayers(serverInstanceId)]); const selectedCatalog = catalogs.items[0]?.id; const revisions = selectedCatalog ? await platformApiClient.listGameGiftRevisions(serverInstanceId, selectedCatalog) : { items: [] }; setState({ status: "ready", value: { catalogs: catalogs.items, revisions: revisions.items, grants: grants.items, players: players.items, selectedCatalog, selectedRevision: revisions.items[0]?.id, selectedPlayer: players.items[0]?.id, name: "新手补给", notice: "已向你发放运营礼包,请查收。", item: "bandage", quantity: 2 } }); } catch (error) { setState({ status: "error", reason: error instanceof Error ? error.message : "礼包目录读取失败" }); } }, [serverInstanceId]);
useEffect(() => { void load(); }, [load]);
const edit = (patch: Partial<Ready>) => setState((current) => current.status === "ready" ? { ...current, value: { ...current.value, ...patch } } : current);
async function createDraft() { if (state.status !== "ready") return; const v = state.value; try { const saved = await platformApiClient.saveGameGiftCatalog(serverInstanceId, { id: v.selectedCatalog, name: v.name, gameVersion: "0.9.700.90357", items: [{ catalogItemKey: v.item, quantity: v.quantity }] }); edit({ catalogs: [saved, ...v.catalogs.filter((catalog) => catalog.id !== saved.id)], selectedCatalog: saved.id, revisions: [], selectedRevision: undefined, note: "草稿已保存;发布后才可定向发放。" }); } catch (error) { edit({ note: error instanceof Error ? error.message : "草稿保存失败" }); } }
async function publish() { if (state.status !== "ready" || !state.value.selectedCatalog) return; try { const revision = await platformApiClient.publishGameGiftCatalog(serverInstanceId, state.value.selectedCatalog); edit({ revisions: [revision, ...state.value.revisions], selectedRevision: revision.id, note: "已发布不可变版本,可预览并选择目标玩家。" }); } catch (error) { edit({ note: error instanceof Error ? error.message : "发布失败" }); } }
async function requestGrant() { if (state.status !== "ready") return; const v=state.value; if (!v.selectedRevision || !v.selectedPlayer) return; try { const grant=await platformApiClient.requestGameGiftGrant(serverInstanceId,{revisionId:v.selectedRevision,gamePlayerRecordId:v.selectedPlayer,notice:v.notice,idempotencyKey:`gift-${v.selectedRevision}-${v.selectedPlayer}`}); edit({grants:[grant,...v.grants],note:"发放申请已冻结礼包版本和目标身份,等待平台管理员审批。"}); } catch(error){edit({note:error instanceof Error?error.message:"发放申请失败"});} }
async function approve(grant: GameGiftGrantResponse) { if(state.status!=="ready")return;try{const next=await platformApiClient.approveGameGiftGrant(serverInstanceId,grant.id);edit({grants:state.value.grants.map((item)=>item.id===next.id?next:item),note:"已通过受控游戏通道排队发放。"});}catch(error){edit({note:error instanceof Error?error.message:"审批失败"});} }
if(state.status==="loading")return <LoadingState label="正在加载版本化 SCUM 礼包目录…"/>;
if(state.status==="error")return <ErrorState title="礼包目录不可用" reason={state.reason} onRetry={()=>void load()}/>;
const v=state.value; const selectedRevision=v.revisions.find((item)=>item.id===v.selectedRevision);
return <section className="console-panel" aria-label="SCUM 版本化礼包目录与定向发放"><div className="panel-header"><div><h2><Gift size={16}/> 版本化礼包目录与定向发放</h2><p className="provider-id">仅可选择此 SCUM 版本已验证的目录项;网页不提交游戏生成指令、物品代码或 RCON 文本。</p></div><button className="icon-command" type="button" onClick={()=>void load()}><RefreshCw size={14}/><span>刷新</span></button></div><div className="console-module"><h3>礼包草稿与版本</h3><div className="console-row-list"><label className="console-row"><strong>礼包名称</strong><input value={v.name} maxLength={80} onChange={(event)=>edit({name:event.target.value})}/></label><label className="console-row"><strong>已验证物品</strong><select value={v.item} onChange={(event)=>edit({item:event.target.value as Ready["item"]})}>{Object.entries(itemLabels).map(([key,label])=><option key={key} value={key}>{label}</option>)}</select></label><label className="console-row"><strong>数量</strong><input type="number" min={1} max={20} value={v.quantity} onChange={(event)=>edit({quantity:Number(event.target.value)})}/></label></div><div className="action-strip"><button className="command-button" type="button" onClick={()=>void createDraft()}>保存草稿</button><select aria-label="选择礼包草稿" value={v.selectedCatalog||""} onChange={async(event)=>{const catalogId=event.target.value;const response=await platformApiClient.listGameGiftRevisions(serverInstanceId,catalogId);edit({selectedCatalog:catalogId,revisions:response.items,selectedRevision:response.items[0]?.id});}}><option value="">选择草稿</option>{v.catalogs.map((catalog)=><option key={catalog.id} value={catalog.id}>{catalog.name} · {catalog.gameVersion}</option>)}</select><button className="primary-command" type="button" disabled={!v.selectedCatalog} onClick={()=>void publish()}>发布不可变版本</button></div></div><div className="console-module"><h3>预览、玩家选择与发放确认</h3><p className="page-status">{selectedRevision ? `版本 #${selectedRevision.revision}${selectedRevision.items.map((item)=>`${item.label} ×${item.quantity}`).join("、")}` : "请先发布一个礼包版本。"}</p><div className="console-row-list"><label className="console-row"><strong>目标本地玩家</strong><select value={v.selectedPlayer||""} onChange={(event)=>edit({selectedPlayer:event.target.value})}>{v.players.map((player)=><option key={player.id} value={player.id}>{player.displayName}{player.gamePlayerId}</option>)}</select></label><label className="console-row"><strong>定向通知</strong><input maxLength={200} value={v.notice} onChange={(event)=>edit({notice:event.target.value})}/></label></div><button className="primary-command" type="button" disabled={!selectedRevision||!v.selectedPlayer} onClick={()=>void requestGrant()}><Send size={14}/>确认并提交发放申请</button></div>{v.note&&<p className="page-status">{v.note}</p>}<div className="console-record-list">{v.grants.map((grant)=><div className="console-record" key={grant.id}><strong>{grantStatus(grant.status)}</strong><span>{grant.playerDisplayName} · {grant.items.map((item)=>`${item.label} ×${item.quantity}`).join("、")}</span><small>版本 #{grant.revisionNumber} · {grant.deliverySummary||"等待投递结果"}{grant.notificationSummary?` · 通知:${grant.notificationSummary}`:""}</small>{grant.status==="pending-approval"&&<button className="icon-command" type="button" onClick={()=>void approve(grant)}><CheckCircle2 size={14}/><span>平台管理员审批</span></button>}</div>)}</div></section>;
}
function grantStatus(status: GameGiftGrantResponse["status"]) { return ({"pending-approval":"等待平台管理员审批",queued:"已排队投递",delivered:"物品已投递,通知处理中",notification_failed:"物品已投递,定向通知失败",failed:"投递失败",unknown:"投递结果未知,绝不自动重试"})[status]; }