@@ -17,30 +17,46 @@ export type SCUMPageContext = {
} ;
type SCUMWorkspaceActions = {
listSCUMPlayers ? : ( ) = > Promise < unknown > ;
listSCUMUsers ? : ( ) = > Promise < unknown > ;
listSCUMPlayers ? : ( ) = > Promise < unknown > ;
listSCUMSquads ? : ( ) = > Promise < unknown > ;
listSCUMSquadMembers ? : ( ) = > Promise < unknown > ;
listSCUMVehicles ? : ( ) = > Promise < unknown > ;
listSCUMFlags ? : ( ) = > Promise < unknown > ;
listSCUMPositions ? : ( ) = > Promise < unknown > ;
listSCUMPositions ? : ( ) = > Promise < unknown > ;
listSCUMMapPoints ? : ( ) = > Promise < unknown > ;
listSCUMGifts ? : ( ) = > Promise < unknown > ;
getSCUMMapGeometry ? : ( ) = > unknown ;
listSCUMOperations ? : ( ) = > Promise < unknown > ;
createSCUMOperation ? : ( request : unknown ) = > Promise < unknown > ;
listSCUMWorkflows ? : ( ) = > Promise < unknown > ;
createSCUMWorkflow ? : ( request : unknown ) = > Promise < unknown > ;
listSCUMWorkflowSteps ? : ( workflowId? : string ) = > Promise < unknown > ;
listSCUMWorkflowSteps ? : ( workflowId? : string ) = > Promise < unknown > ;
listGameGiftCatalogs ? : ( ) = > Promise < unknown > ;
saveGameGiftCatalog ? : ( request : unknown ) = > Promise < unknown > ;
publishGameGiftCatalog ? : ( catalogId : string ) = > Promise < unknown > ;
listGameGiftGrants ? : ( ) = > Promise < unknown > ;
requestGameGiftGrant ? : ( request : unknown ) = > Promise < unknown > ;
approveGameGiftGrant ? : ( grantId : string ) = > Promise < unknown > ;
} ;
type RecordMap = Record < string , unknown > ;
type DataState = { status : "loading" } | { status : "error" ; reason : string } | { status : "ready" ; data : SCUMSurfaceData } ;
type ActionState = { status : "idle" | "pending" | "ok" | "error" ; message? : string } ;
type SCUMSurfaceData = { players : RecordMap [ ] ; squads : RecordMap [ ] ; members : RecordMap [ ] ; vehicles : RecordMap [ ] ; flags : RecordMap [ ] ; positions : RecordMap [ ] ; operations : RecordMap [ ] ; workflows : RecordMap [ ] ; steps : RecordMap [ ] } ;
type SCUMSurfaceData = { players : RecordMap [ ] ; squads : RecordMap [ ] ; members : RecordMap [ ] ; vehicles : RecordMap [ ] ; flags : RecordMap [ ] ; positions : RecordMap [ ] ; mapPoints : RecordMap [ ] ; giftEvents : RecordMap [ ] ; catalogs : RecordMap [ ] ; grants : RecordMap [ ] ; operations : RecordMap [ ] ; workflows : RecordMap [ ] ; steps : RecordMap [ ] ; mapGeometry? : RecordMap } ;
const emptyData : SCUMSurfaceData = { players : [ ] , squads : [ ] , members : [ ] , vehicles : [ ] , flags : [ ] , positions : [ ] , operations : [ ] , workflows : [ ] , steps : [ ] } ;
const emptyData : SCUMSurfaceData = { players : [ ] , squads : [ ] , members : [ ] , vehicles : [ ] , flags : [ ] , positions : [ ] , mapPoints : [ ] , giftEvents : [ ] , catalogs : [ ] , grants : [ ] , operations : [ ] , workflows : [ ] , steps : [ ] } ;
export function renderSCUMFeaturePage ( react : ReactLike , input : SCUMPageContext ) {
const e = react . createElement ;
const [ state , setState ] = usePluginState < DataState > ( react , { status : "loading" } ) ;
const [ action , setAction ] = usePluginState < ActionState > ( react , { status : "idle" } ) ;
const [ playerQuery , setPlayerQuery ] = usePluginState ( react , "" ) ;
const [ squadFilter , setSquadFilter ] = usePluginState ( react , "" ) ;
const [ selectedSquad , setSelectedSquad ] = usePluginState ( react , "" ) ;
const [ layers , setLayers ] = usePluginState < Record < string , boolean > > ( react , { player : true , vehicle : true , base : true , flag : true } ) ;
const [ selectedMarker , setSelectedMarker ] = usePluginState < RecordMap | undefined > ( react , undefined ) ;
const [ giftTab , setGiftTab ] = usePluginState ( react , "catalogs" ) ;
const pageKey = input . pageKey ? ? "players" ;
const refresh = ( ) = > {
@@ -51,9 +67,9 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
}
setState ( { status : "loading" } ) ;
void Promise . all ( [
safeList ( actions . listSCUMPlayers ) , safeList ( actions . listSCUMSquads ) , safeList ( actions . listSCUMSquadMembers ) , safeList ( actions . listSCUMVehicles ) ,
safeList ( actions . listSCUMFlags ) , safeList ( actions . listSCUMPositions ) , safeList ( actions . listSCUMOperations ) , safeList ( actions . listSCUMWorkflows ) , safeList ( actions . listSCUMWorkflowSteps )
] ) . then ( ( [ players , squads , members , vehicles , flags , positions , operations , workflows , steps ] ) = > setState ( { status : "ready" , data : { players , squads , members , vehicles , flags , positions , operations , workflows , steps } } ) )
safeList ( actions . listSCUMPlayers ) , safeList ( actions . listSCUMSquads ) , safeList ( actions . listSCUMSquadMembers ) , safeList ( actions . listSCUMVehicles ) , safeList ( actions . listSCUMFlags ) , safeList ( actions . listSCUMPositions ) ,
safeList ( actions . listSCUMMapPoints ) , safeList ( actions . listSCUMGifts ) , safeList ( actions . listGameGiftCatalogs ) , safeList ( actions . listGameGiftGrants ) , safeList ( actions . listSCUMOperations ) , safeList ( actions . listSCUMWorkflows ) , safeList ( actions . listSCUMWorkflowSteps )
] ) . then ( ( [ players , squads , members , vehicles , flags , positions , mapPoints , giftEvents , catalogs , grants , operations , workflows , steps ] ) = > setState ( { status : "ready" , data : { players , squads , members , vehicles , flags , positions , mapPoints , giftEvents , catalogs , grants , operations , workflows , steps , mapGeometry : asRecord ( actions . getSCUMMapGeometry ? . ( ) ) } } ) )
. catch ( ( error ) = > setState ( { status : "error" , reason : error instanceof Error ? error . message : "SCUM 投影读取失败。" } ) ) ;
} ;
@@ -72,25 +88,30 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
action . status !== "idle" ? e ( "p" , { className : "page-status" , "data-state" : action . status } , action . message ) : null ,
state . status === "loading" ? e ( "p" , { className : "page-status" } , "正在读取平台本地 SCUM 投影…" ) : null ,
state . status === "error" ? e ( "p" , { className : "page-status" , "data-state" : "error" } , state . reason ) : null ,
state . status === "ready" ? renderSurfaceBody ( e , pageKey , data , input , setAction , refresh ) : null
state . status === "ready" ? renderSurfaceBody ( e , pageKey , data , input , setAction , refresh , { playerQuery , setPlayerQuery , squadFilter , setSquadFilter , selectedSquad , setSelectedSquad , layers , setLayers , selectedMarker , setSelectedMarker , giftTab , setGiftTab } ) : null
) ;
}
function renderSurfaceBody ( e : ReactLike [ "createElement" ] , pageKey : string , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void ) {
type SurfaceControls = { playerQuery : string ; setPlayerQuery : StateSetter < string > ; squadFilter : string ; setSquadFilter : StateSetter < string > ; selectedSquad : string ; setSelectedSquad : StateSetter < string > ; layers : Record < string , boolean > ; setLayers : StateSetter < Record < string , boolean > > ; selectedMarker? : RecordMap ; setSelectedMarker : StateSetter < RecordMap | undefined > ; giftTab : string ; setGiftTab : StateSetter < string > } ;
function renderSurfaceBody ( e : ReactLike [ "createElement" ] , pageKey : string , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void , controls : SurfaceControls ) {
switch ( pageKey ) {
case "players" : return playersSurface ( e , data , input , setAction , refresh ) ;
case "squads" : return squadsSurface ( e , data ) ;
case "live-map" : return mapSurface ( e , data ) ;
case "gifts" : return giftsSurface ( e , data , input , setAction , refresh ) ;
case "players" : return playersSurface ( e , data , input , setAction , refresh , controls ) ;
case "squads" : return squadsSurface ( e , data , controls ) ;
case "live-map" : return mapSurface ( e , data , controls ) ;
case "gifts" : return giftsSurface ( e , data , input , setAction , refresh , controls ) ;
case "workflows" : return workflowsSurface ( e , data ) ;
default : return playersSurface ( e , data , input , setAction , refresh ) ;
default : return playersSurface ( e , data , input , setAction , refresh , controls ) ;
}
}
function playersSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void ) {
function playersSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void , controls : SurfaceControls ) {
const squads = uniqueStrings ( data . players . map ( ( player ) = > textField ( player , "SquadName" , "squadName" ) || textField ( player , "SquadID" , "squadId" ) ) ) ;
const query = controls . playerQuery . trim ( ) . toLowerCase ( ) ;
const players = data . players . filter ( ( player ) = > ( ! controls . squadFilter || controls . squadFilter === ( textField ( player , "SquadName" , "squadName" ) || textField ( player , "SquadID" , "squadId" ) ) ) && ( ! query || [ "DisplayName" , "displayName" , "SteamID" , "steamId" , "SquadName" , "squadName" ] . some ( ( key ) = > textField ( player , key ) . toLowerCase ( ) . includes ( query ) ) ) ) ;
return e ( "div" , { className : "console-record-list" } ,
statsStrip ( e , [ [ "玩家投影" , data . players . length ] , [ "在线" , data . players . filter ( ( p ) = > boolField ( p , "Online" , "online" ) ) . length ] , [ "坐标" , data . positions . length ] , [ "待审操作" , data . operations . filter ( ( op ) = > field ( op , "Status" , "status" ) === "waiting" ) . length ] ] ) ,
data . players . length ? data . players . slice ( 0 , 80 ) . map ( ( player ) = > e ( "article" , { key : idOf ( player ) , className : "console-record" } ,
e ( "div" , { className : "server-toolbar" } , e ( "input" , { type : "search" , value : controls.playerQuery , placeholder : "搜索昵称、Steam 或队伍" , onChange : ( event : { target : { value : string } } ) = > controls . setPlayerQuery ( event . target . value ) } ) , e ( "select" , { value : controls.squadFilter , onChange : ( event : { target : { value : string } } ) = > controls . setSquadFilter ( event . target . value ) } , e ( "option" , { value : "" } , "全部队伍" ) , squads . map ( ( squad ) = > e ( "option" , { key : squad , value : squad } , squad ) ) ) ) ,
players . length ? players . slice ( 0 , 80 ) . map ( ( player ) = > e ( "article" , { key : idOf ( player ) , className : "console-record" } ,
e ( "div" , { className : "console-record-head" } , e ( "strong" , null , textField ( player , "DisplayName" , "displayName" ) || textField ( player , "GamePlayerID" , "gamePlayerId" ) || "未知玩家" ) , e ( "span" , { className : ` status-pill ${ boolField ( player , "Online" , "online" ) ? "status-active" : "status-disabled" } ` } , boolField ( player , "Online" , "online" ) ? "在线" : "离线/未知" ) ) ,
e ( "div" , { className : "console-record-meta" } , e ( "span" , null , ` Steam ${ textField ( player , "SteamID" , "steamId" ) || "unknown" } ` ) , e ( "span" , null , ` Profile ${ textField ( player , "UserProfileID" , "userProfileId" ) || "unknown" } ` ) , e ( "span" , null , ` 队伍 ${ textField ( player , "SquadName" , "squadName" ) || textField ( player , "SquadID" , "squadId" ) || "unknown" } ` ) , e ( "span" , null , freshness ( player ) ) ) ,
e ( "span" , { className : "provider-id" } , ` Fame ${ numField ( player , "FamePoints" , "famePoints" ) } · Cash ${ numField ( player , "NormalBalance" , "normalBalance" ) } · Gold ${ numField ( player , "GoldBalance" , "goldBalance" ) } · ${ coords ( field ( player , "Position" , "position" ) as RecordMap | undefined ) } ` ) ,
@@ -99,37 +120,57 @@ function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, in
operationButton ( e , input , setAction , refresh , player , "player.currency.normal.set" , "amount" , "现金 +1000" , 1000 ) ,
operationButton ( e , input , setAction , refresh , player , "player.attribute.855.set" , "after" , "855 审批" , Number ( numField ( player , "855" , "855" ) ) || 1 , true )
)
) ) : e ( "p" , { className : "page-status" } , "暂无玩家投影。先运行 player/world refresh workflow;不会显示假玩家。" )
) ) : e ( "p" , { className : "page-status" } , data . players . length ? "没有符合筛选条件的真实玩家投影。" : "暂无玩家投影。先运行 player/world refresh workflow;不会显示假玩家。" )
) ;
}
function squadsSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData ) {
return e ( "div" , { className : "overview-two-col" } ,
tablePanel ( e , "队伍" , data . squads , ( squad ) = > [ textField ( squad , "Name" , "name" ) || textField ( squad , "SquadID" , "squadId" ) , ` 成员 ${ numField ( squad , "MemberCount" , "memberCount" ) } ` , ` 队长 ${ textField ( squad , "LeaderProfileID" , "leaderProfileId" ) || "unknown" } ` , freshness ( squad ) ] ) ,
tablePanel ( e , "成员 / 旗帜" , [ . . . data . members . slice ( 0 , 40 ) , . . . data . flags . slice ( 0 , 40 ) ] , ( item ) = > [ textField ( item , "DisplayName" , "displayName" ) || textField ( item , "FlagID" , "flagId" ) || "unknown" , textField ( item , "Rank" , "rank" ) || textField ( item , "OwnershipConfidence" , "ownershipConfidence" ) || "unknown" , textField ( item , "SquadID" , "squadId" ) || textField ( item , "OwnerSquadID" , "ownerSquadId" ) || "unknown" , freshness ( item ) ] )
) ;
function squadsSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , controls : SurfaceControls ) {
const selected = controls . selectedSquad || textField ( data . squads [ 0 ] , "SquadID" , "squadId" ) ;
const roster = data . members . filter ( ( member ) = > textField ( member , "SquadID" , "squadId" ) === selected ) ;
const flags = data . flags . filter ( ( flag ) = > textField ( flag , "OwnerSquadID" , "ownerSquadId" ) === selected ) ;
const squadRows = e ( "div" , { className : "console-row-list" } , data . squads . map ( ( squad ) = > {
const id = textField ( squad , "SquadID" , "squadId" ) ;
return e ( "button" , { type : "button" , key : idOf ( squad ) , className : ` console-row ${ selected === id ? "console-row-selected" : "" } ` , onClick : ( ) = > controls . setSelectedSquad ( id ) } , e ( "span" , null , textField ( squad , "Name" , "name" ) || id ) , e ( "strong" , null , ` 成员 ${ numField ( squad , "MemberCount" , "memberCount" ) } ` ) , e ( "strong" , null , ` 分数 ${ numField ( squad , "Score" , "score" ) } ` ) , e ( "strong" , null , freshness ( squad ) ) ) ;
} ) ) ;
const rosterRows = roster . length ? e ( "div" , { className : "console-row-list" } , roster . map ( ( member ) = > e ( "div" , { key : idOf ( member ) , className : "console-row" } , e ( "span" , null , textField ( member , "DisplayName" , "displayName" ) || "未知成员" ) , e ( "strong" , null , textField ( member , "IsLeader" , "isLeader" ) === "true" ? "队长" : ` 军衔 ${ textField ( member , "Rank" , "rank" ) || "--" } ` ) , e ( "strong" , null , ` Steam ${ textField ( member , "SteamID" , "steamId" ) || "--" } ` ) , e ( "strong" , null , freshness ( member ) ) ) ) ) : e ( "p" , { className : "page-status" } , "此队伍暂无成员投影。" ) ;
const flagRows = e ( "div" , { className : "console-row-list" } , flags . map ( ( flag ) = > e ( "div" , { key : idOf ( flag ) , className : "console-row" } , e ( "span" , null , ` 旗帜 ${ textField ( flag , "FlagID" , "flagId" ) } ` ) , e ( "strong" , null , textField ( flag , "OwnershipConfidence" , "ownershipConfidence" ) || "verified" ) , e ( "strong" , null , coords ( field ( flag , "Position" , "position" ) as RecordMap ) ) , e ( "strong" , null , freshness ( flag ) ) ) ) ) ;
return e ( "div" , { className : "overview-two-col" } , e ( "article" , { className : "console-module" } , e ( "div" , { className : "panel-header" } , e ( "h2" , null , "队伍" ) , e ( "span" , { className : "page-status" } , ` ${ data . squads . length } 条 ` ) ) , squadRows ) , e ( "article" , { className : "console-module" } , e ( "div" , { className : "panel-header" } , e ( "h2" , null , "成员 / 旗帜" ) , e ( "span" , { className : "page-status" } , selected || "未选择队伍" ) ) , rosterRows , flagRows ) ) ;
}
function mapSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData ) {
const overlays = [ . . . data . positions , . . . data . vehicles . map ( ( v ) = > field ( v , "Position" , "position" ) as RecordMap ) . filter ( Boolean ) , . . . data . flags . map ( ( f ) = > field ( f , "Position" , "position" ) as RecordMap ) . filter ( Boolean ) ] ;
function mapSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , controls : SurfaceControls ) {
const overlays = mapOverlays ( data ) ;
const visible = overlays . filter ( ( point ) = > controls . layers [ textField ( point , "SubjectType" , "subjectType" ) ] !== false ) ;
return e ( "div" , { className : "console-record-list" } ,
statsStrip ( e , [ [ "玩家" , data . players . length ] , [ "载具" , data . vehicles . length ] , [ "旗帜" , data . flags . length ] , [ "坐标点" , overlays . length ] ] ) ,
e ( "div" , { className : "map-projection-board" } , overlays . slice ( 0 , 120 ) . map ( ( point , index ) = > e ( "span " , { key : ` ${ idOf ( point ) } : ${ index } ` , className : "map-projection-dot" , title : ` ${ textField ( point , "SubjectType" , "subjectType" ) || "point" } ${ coords ( point ) } ` , style : dotStyle ( point ) } , "" ) ) ) ,
tablePanel ( e , "地图覆盖物" , overlays , ( point ) = > [ textField ( point , "SubjectType" , "subjectType" ) || "unknown" , textField ( point , "SubjectID " , "subjectId " ) || textField ( point , "GamePlayerID" , "gamePlayerId" ) || textField ( point , "VehicleID" , "vehicleId" ) || "unknown" , coords ( point ) , freshness ( point ) ] )
statsStrip ( e , [ [ "玩家" , overlays . filter ( ( point ) = > textField ( point , "SubjectType" , "subjectType" ) === "player" ) . length ] , [ "载具" , overlays . filter ( ( point ) = > textField ( point , "SubjectType" , "subjectType" ) === "vehicle" ) . length ] , [ "旗帜" , overlays . filter ( ( point ) = > textField ( point , "SubjectType" , "subjectType" ) === "flag" ) . length ] , [ "坐标点" , visible . length ] ] ) ,
e ( "div" , { className : "map-layer-controls" } , [ "player" , "vehicle" , "base" , "flag" ] . map ( ( kind ) = > e ( "label " , { key : kind } , e ( "input" , { type : "checkbox" , checked : controls.layers [ kind ] !== false , onChange : ( ) = > controls . setLayers ( ( previous ) = > ( { . . . previous , [ kind ] : previous [ kind ] === false } ) ) } ) , mapKindLabel ( kind ) ) ) ) ,
e ( "div" , { className : "map-projection-board" } , visible . slice ( 0 , 500 ) . map ( ( point , index ) = > e ( "button" , { type : "button" , key : ` ${ idOf ( point ) } : ${ index } ` , className : ` map-projection-dot map-projection-dot- ${ textField ( point , "SubjectType" , "subjectType" ) || "unknown" } ` , title : ` ${ mapKindLabel ( textField ( point , "SubjectType " , "subjectType " ) ) } ${ coords ( point ) } ` , style : dotStyle ( point , data . mapGeometry ) , onClick : ( ) = > controls . setSelectedMarker ( point ) } , "" ) ) ) ,
controls . selectedMarker ? e ( "article" , { className : "console-record" } , e ( "div" , { className : "console-record-head" } , e ( "strong" , null , mapKindLabel ( textField ( controls . selectedMarker , "SubjectType" , "subjectType" ) ) ) , e ( "span" , { className : "status-pill status-active" } , freshness ( controls . selectedMarker ) ) ) , e ( "div" , { className : "console-record-meta" } , e ( "span" , null , ` 对象 ${ textField ( controls . selectedMarker , "SubjectID" , "subjectId" ) || "--" } ` ) , e ( "span" , null , coords ( controls . selectedMarker ) ) , e ( "span" , null , ` 观察 ${ dateField ( controls . selectedMarker , "ObservedAt" , "observedAt" ) } ` ) ) ) : null ,
tablePanel ( e , "地图覆盖物" , visible , ( point ) = > [ mapKindLabel ( textField ( point , "SubjectType" , "subjectType" ) ) , textField ( point , "Label" , "label" ) || textField ( point , "SubjectID" , "subjectId" ) || textField ( point , "GamePlayerID" , "gamePlayerId" ) || textField ( point , "VehicleID" , "vehicleId" ) || "unknown" , coords ( point ) , freshness ( point ) ] )
) ;
}
function giftsSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void ) {
function giftsSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void , controls : SurfaceControls ) {
const tab = controls . giftTab ;
return e ( "div" , { className : "console-record-list" } ,
statsStrip ( e , [ [ "可选玩家 " , data . players . length ] , [ "发放操作 " , data . operations . filter ( ( op ) = > textField ( op , "TemplateKey" , "templateKey" ) === "reward.deliver" ) . length ] , [ "未知态" , data . operations . filter ( ( op ) = > field ( op , "Status" , "status" ) === "unknown" ) . length ] ] ) ,
e ( "p" , { className : "page-status" } , "礼包只创建 typed delivery workflow; 确认 结果未知时不会重复发放。" ) ,
data . players . slice ( 0 , 40 ) . map ( ( player ) = > e ( "article " , { key : idOf ( player ) , className : "console-record" } ,
e ( "div" , { className : "console-record-head" } , e ( "strong" , null , textField ( player , "DisplayName" , "displayName" ) || idOf ( player ) ) , e ( "span" , { className : "status-pill status-disabled" } , freshness ( player ) ) ) ,
e ( "div" , { className : "console-row-actions" } , operationButton ( e , input , setAction , refresh , player , "reward.deliver" , "rewardKey" , "创建礼包发放" , "starter-pack" ) , operationButton ( e , input , setAction , refresh , player , "player.notify" , "message" , "发送通知" , "你的礼包正在审核发放。" ) )
) )
statsStrip ( e , [ [ "礼包定义 " , ( data . catalogs ? ? [ ] ) . length ] , [ "发放单 " , ( data . grants ? ? [ ] ) . length ] , [ "定时礼包完成" , ( data . giftEvents ? ? [ ] ) . length ] , [ "可选玩家" , data . players . length ] ] ) ,
e ( "p" , { className : "page-status" } , "礼包只走 typed delivery workflow;结果未知时不会重复发放。" ) ,
e ( "div" , { className : "section-tabs" } , [ [ "catalogs" , "礼包定义" ] , [ "grants" , "发放记录" ] , [ "events" , "定时礼包完成" ] ] . map ( ( [ key , label ] ) = > e ( "button " , { type : "button" , key , className : ` section-tab ${ tab === key ? "section-tab-active" : "" } ` , onClick : ( ) = > controls . setGiftTab ( key ) } , label ) ) ) ,
tab === "catalogs" ? giftCatalogSurface ( e , data , input , setAction , refresh ) : null ,
tab === "grants" ? giftGrantSurface ( e , data , input , setAction , refresh ) : null ,
tab === "events" ? tablePanel ( e , "游戏内已完成定时礼包" , data . giftEvents ? ? [ ] , ( event ) = > [ textField ( event , "displayName" , "DisplayName" ) || "未知玩家" , textField ( event , "giftType" , "GiftType" ) || "finished-timed-gift" , dateField ( event , "spawnAt" , "SpawnAt" ) , textField ( event , "mapId" , "MapID" ) || "--" ] ) : null
) ;
}
function giftCatalogSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void ) {
const catalogs = data . catalogs ? ? [ ] ;
return e ( "div" , { className : "console-row-list" } , catalogs . length ? catalogs . map ( ( catalog ) = > e ( "article" , { key : idOf ( catalog ) , className : "console-record" } , e ( "div" , { className : "console-record-head" } , e ( "strong" , null , textField ( catalog , "Name" , "name" ) ) , e ( "span" , { className : "status-pill status-active" } , textField ( catalog , "GameVersion" , "gameVersion" ) ) ) , e ( "span" , { className : "provider-id" } , giftItemsText ( field ( catalog , "DraftItems" , "draftItems" ) as RecordMap [ ] ) ) , e ( "div" , { className : "console-row-actions" } , e ( "button" , { type : "button" , className : "icon-command" , onClick : ( ) = > publishCatalog ( input , setAction , refresh , idOf ( catalog ) ) } , "发布版本" ) ) ) ) : e ( "p" , { className : "page-status" } , "暂无平台运营礼包定义。" ) ) ;
}
function giftGrantSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData , input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void ) {
const grants = data . grants ? ? [ ] ;
return e ( "div" , { className : "console-row-list" } , grants . length ? grants . map ( ( grant ) = > e ( "article" , { key : idOf ( grant ) , className : "console-record" } , e ( "div" , { className : "console-record-head" } , e ( "strong" , null , textField ( grant , "PlayerDisplayName" , "playerDisplayName" ) || "未知玩家" ) , e ( "span" , { className : "status-pill status-disabled" } , textField ( grant , "Status" , "status" ) ) ) , e ( "span" , { className : "provider-id" } , giftItemsText ( field ( grant , "Items" , "items" ) as RecordMap [ ] ) ) , textField ( grant , "Status" , "status" ) === "pending-approval" ? e ( "div" , { className : "console-row-actions" } , e ( "button" , { type : "button" , className : "icon-command" , onClick : ( ) = > approveGrant ( input , setAction , refresh , idOf ( grant ) ) } , "批准发放" ) ) : null ) ) : e ( "p" , { className : "page-status" } , "暂无礼包发放记录。" ) ) ;
}
function workflowsSurface ( e : ReactLike [ "createElement" ] , data : SCUMSurfaceData ) {
return e ( "div" , { className : "console-record-list" } ,
data . workflows . length ? data . workflows . map ( ( wf ) = > e ( "article" , { key : idOf ( wf ) , className : "console-record" } ,
@@ -184,7 +225,25 @@ function workflowLabel(templateKey: string): string { return templateKey.include
function statsStrip ( e : ReactLike [ "createElement" ] , items : Array < [ string , number ] > ) { return e ( "div" , { className : "console-stat-strip" } , items . map ( ( [ label , value ] ) = > e ( "span" , { key : label , className : "server-card-stat" } , e ( "span" , null , label ) , e ( "strong" , null , String ( value ) ) ) ) ) ; }
function tablePanel ( e : ReactLike [ "createElement" ] , title : string , rows : RecordMap [ ] , render : ( row : RecordMap ) = > unknown [ ] ) { return e ( "article" , { className : "console-module" } , e ( "div" , { className : "panel-header" } , e ( "h2" , null , title ) , e ( "span" , { className : "page-status" } , ` ${ rows . length } 条 ` ) ) , e ( "div" , { className : "console-row-list" } , rows . length ? rows . slice ( 0 , 100 ) . map ( ( row ) = > e ( "div" , { key : idOf ( row ) , className : "console-row" } , render ( row ) . map ( ( part , i ) = > i === 0 ? e ( "span" , { key : i } , String ( part ? ? "unknown" ) ) : e ( "strong" , { key : i } , String ( part ? ? "unknown" ) ) ) ) ) : e ( "p" , { className : "page-status" } , "暂无真实投影数据。" ) ) ) ; }
function dotStyle ( point : RecordMap ) : Record < string , string > { const x = Number ( field ( point , "X" , "x" ) ? ? 0 ) ; const y = Number ( field ( point , "Y" , "y" ) ? ? 0 ) ; return { left : ` ${ Math . max ( 2 , Math . min ( 98 , 50 + x / 10000 ) ) } % ` , top : ` ${ Math . max ( 2 , Math . min ( 98 , 50 - y / 10000 ) ) } % ` } ; }
function mapOverlays ( data : SCUMSurfaceData ) : RecordMap [ ] {
const mapPoints = ( data . mapPoints ? ? data . positions ? ? [ ] ) . map ( ( point ) = > ( { . . . ( field ( point , "Fields" , "fields" ) as RecordMap ) , . . . point , SubjectType : textField ( field ( point , "Fields" , "fields" ) as RecordMap , "subjectType" , "SubjectType" ) || textField ( point , "SubjectType" , "subjectType" ) || "player" } ) ) ;
if ( mapPoints . length ) return mapPoints ;
const vehicles = data . vehicles . map ( ( vehicle ) = > ( { . . . ( field ( vehicle , "Position" , "position" ) as RecordMap ) , SubjectType : "vehicle" , Label : textField ( vehicle , "Label" , "label" ) || textField ( vehicle , "ClassName" , "className" ) , VehicleID : textField ( vehicle , "VehicleID" , "vehicleId" ) } ) ) . filter ( ( point ) = > Boolean ( point ) ) ;
const flags = data . flags . map ( ( flag ) = > ( { . . . ( field ( flag , "Position" , "position" ) as RecordMap ) , SubjectType : "flag" , Label : textField ( flag , "OwnerSquadName" , "ownerSquadName" ) , FlagID : textField ( flag , "FlagID" , "flagId" ) } ) ) . filter ( ( point ) = > Boolean ( point ) ) ;
return [ . . . mapPoints , . . . vehicles , . . . flags ] ;
}
function dotStyle ( point : RecordMap , geometry? : RecordMap ) : Record < string , string > {
const x = Number ( field ( point , "X" , "x" ) ? ? 0 ) ; const y = Number ( field ( point , "Y" , "y" ) ? ? 0 ) ;
const minX = Number ( field ( geometry , "WorldMinX" , "worldMinX" ) ) ; const maxX = Number ( field ( geometry , "WorldMaxX" , "worldMaxX" ) ) ; const minY = Number ( field ( geometry , "WorldMinY" , "worldMinY" ) ) ; const maxY = Number ( field ( geometry , "WorldMaxY" , "worldMaxY" ) ) ;
if ( [ minX , maxX , minY , maxY ] . every ( Number . isFinite ) && maxX > minX && maxY > minY ) return { left : ` ${ Math . max ( 1 , Math . min ( 99 , ( x - minX ) / ( maxX - minX ) * 100 ) ) } % ` , top : ` ${ Math . max ( 1 , Math . min ( 99 , 100 - ( y - minY ) / ( maxY - minY ) * 100 ) ) } % ` } ;
return { left : "50%" , top : "50%" } ;
}
function mapKindLabel ( kind : string ) : string { return kind === "player" ? "玩家" : kind === "vehicle" ? "载具" : kind === "base" ? "基地" : kind === "flag" ? "旗帜" : "实体" ; }
function uniqueStrings ( values : string [ ] ) : string [ ] { return [ . . . new Set ( values . filter ( Boolean ) ) ] . sort ( ( a , b ) = > a . localeCompare ( b , "zh-CN" ) ) ; }
function asRecord ( value : unknown ) : RecordMap | undefined { return value && typeof value === "object" ? value as RecordMap : undefined ; }
function giftItemsText ( items : RecordMap [ ] | undefined ) : string { return Array . isArray ( items ) && items . length ? items . map ( ( item ) = > ` ${ textField ( item , "Label" , "label" , "CatalogItemKey" , "catalogItemKey" ) } x ${ textField ( item , "Quantity" , "quantity" ) } ` ) . join ( " · " ) : "未声明物品" ; }
function publishCatalog ( input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void , catalogId : string ) { setAction ( { status : "pending" , message : "正在发布礼包版本…" } ) ; void input . workspaceActions ? . publishGameGiftCatalog ? . ( catalogId ) . then ( ( ) = > { setAction ( { status : "ok" , message : "礼包版本已发布。" } ) ; refresh ( ) ; } ) . catch ( ( error ) = > setAction ( { status : "error" , message : error instanceof Error ? error . message : "礼包发布失败。" } ) ) ; }
function approveGrant ( input : SCUMPageContext , setAction : StateSetter < ActionState > , refresh : ( ) = > void , grantId : string ) { setAction ( { status : "pending" , message : "正在批准礼包发放…" } ) ; void input . workspaceActions ? . approveGameGiftGrant ? . ( grantId ) . then ( ( ) = > { setAction ( { status : "ok" , message : "礼包已进入发放队列。" } ) ; refresh ( ) ; } ) . catch ( ( error ) = > setAction ( { status : "error" , message : error instanceof Error ? error . message : "礼包批准失败。" } ) ) ; }
function safeList ( fn ? : ( ) = > Promise < unknown > ) : Promise < RecordMap [ ] > { return fn ? fn ( ) . then ( ( value ) = > Array . isArray ( ( value as RecordMap ) ? . items ) ? ( value as { items : RecordMap [ ] } ) . items : [ ] ) : Promise . resolve ( [ ] ) ; }
function usePluginState < T > ( react : ReactLike , initial : T ) : [ T , StateSetter < T > ] { return react . useState ? react . useState < T > ( initial ) : [ initial , ( ) = > undefined ] ; }
function field ( row : RecordMap | undefined , . . . keys : string [ ] ) : unknown { if ( ! row ) return undefined ; for ( const key of keys ) if ( row [ key ] !== undefined ) return row [ key ] ; return undefined ; }