1237 lines
48 KiB
Vue
1237 lines
48 KiB
Vue
<template>
|
||
<div class="app-container aslan-game-king">
|
||
<div class="filter-container">
|
||
<el-button class="filter-item" type="primary" icon="el-icon-plus" @click="openCreate">
|
||
新建活动
|
||
</el-button>
|
||
<el-button class="filter-item" icon="el-icon-refresh" :loading="listLoading" @click="loadActivities">
|
||
刷新
|
||
</el-button>
|
||
<el-alert
|
||
class="page-tip"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="消费金额、抽奖次数和排行榜均以后端金币流水为准;活动开始后,每次抽奖金币数、开始时间、平台及事件白名单不可修改。"
|
||
/>
|
||
</div>
|
||
|
||
<el-table v-loading="listLoading" :data="activities" border fit highlight-current-row>
|
||
<el-table-column label="活动" min-width="220">
|
||
<template slot-scope="scope">
|
||
<div class="primary-text">{{ scope.row.activityName }}</div>
|
||
<div class="secondary-text">{{ scope.row.activityCode }}</div>
|
||
<div class="secondary-text">ID: {{ scope.row.id }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="平台 / 时区" min-width="135" align="center">
|
||
<template slot-scope="scope">
|
||
<div>{{ scope.row.sysOrigin }}</div>
|
||
<div class="secondary-text">{{ scope.row.timeZone || 'Asia/Riyadh' }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="活动时间" min-width="205" align="center">
|
||
<template slot-scope="scope">
|
||
<div>{{ formatTime(scope.row.startTime) }}</div>
|
||
<div>{{ formatTime(scope.row.endTime) }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="抽奖门槛" min-width="125" align="right">
|
||
<template slot-scope="scope">
|
||
每 {{ formatNumber(scope.row.coinPerDraw) }} 金币
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="精确事件白名单" min-width="220">
|
||
<template slot-scope="scope">
|
||
<el-tag
|
||
v-for="eventType in scope.row.eventTypes || []"
|
||
:key="eventType"
|
||
class="event-tag"
|
||
size="mini"
|
||
>
|
||
{{ eventType }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="130" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="activityStatusTag(activityStatus(scope.row))">
|
||
{{ activityStatusText(activityStatus(scope.row)) }}
|
||
</el-tag>
|
||
<div class="settlement-status">结算:{{ settlementStatusText(scope.row.settlementStatus) }}</div>
|
||
<div class="secondary-text">可结算:{{ formatTime(scope.row.settlementTime) }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="启用" width="85" align="center">
|
||
<template slot-scope="scope">
|
||
<el-switch
|
||
v-model="scope.row.enabled"
|
||
:disabled="hasStarted(scope.row)"
|
||
@change="changeEnabled(scope.row)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="350" align="center" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" @click="openEdit(scope.row)">配置</el-button>
|
||
<el-button type="text" @click="openDrawRecords(scope.row)">抽奖发放</el-button>
|
||
<el-button type="text" @click="openSettlements(scope.row)">结算记录</el-button>
|
||
<el-button
|
||
type="text"
|
||
:disabled="scope.row.settlementStatus !== 'NOT_STARTED'"
|
||
@click="openBackfill(scope.row)"
|
||
>
|
||
消费补数
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-dialog
|
||
:title="form.id ? '编辑 Aslan 游戏王活动' : '新建 Aslan 游戏王活动'"
|
||
:visible.sync="editorVisible"
|
||
width="1180px"
|
||
top="4vh"
|
||
:close-on-click-modal="false"
|
||
@closed="resetEditor"
|
||
>
|
||
<div v-loading="editorLoading" class="editor-body">
|
||
<el-alert
|
||
v-if="startedLocked"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="后端判定活动已经开始:统计、结算、启用状态、七个奖项和六档排行奖励均已锁定;仅名称与说明可维护。"
|
||
/>
|
||
|
||
<div class="section-title">基础配置</div>
|
||
<el-form ref="activityForm" :model="form" :rules="formRules" label-width="135px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="活动编码" prop="activityCode">
|
||
<el-input
|
||
v-model.trim="form.activityCode"
|
||
maxlength="64"
|
||
show-word-limit
|
||
:disabled="startedLocked"
|
||
placeholder="例如 ASLAN_GAME_KING_S2"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="活动名称" prop="activityName">
|
||
<el-input v-model.trim="form.activityName" maxlength="128" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="所属平台" prop="sysOrigin">
|
||
<el-select v-model="form.sysOrigin" style="width: 100%" :disabled="startedLocked" filterable>
|
||
<el-option
|
||
v-for="item in permissionsSysOriginPlatforms"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="活动时区" prop="timeZone">
|
||
<el-select v-model="form.timeZone" style="width: 100%" filterable :disabled="startedLocked">
|
||
<el-option v-for="item in timeZoneOptions" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="活动时间" prop="timeRange">
|
||
<el-date-picker
|
||
v-model="form.timeRange"
|
||
type="datetimerange"
|
||
value-format="timestamp"
|
||
style="width: 100%"
|
||
range-separator="至"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
:disabled="startedLocked"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="每次抽奖金币数" prop="coinPerDraw">
|
||
<el-input-number
|
||
v-model="form.coinPerDraw"
|
||
:min="1"
|
||
:max="999999999999"
|
||
:precision="0"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
:disabled="startedLocked"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="结算等待(分钟)" prop="settlementDelayMinutes">
|
||
<el-input-number
|
||
v-model="form.settlementDelayMinutes"
|
||
:min="0"
|
||
:max="1440"
|
||
:precision="0"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
:disabled="startedLocked"
|
||
/>
|
||
<div class="form-help">活动结束后继续等待钱包异步流水到达,再冻结最终榜单;默认 30 分钟。</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="精确事件白名单" prop="eventTypes">
|
||
<el-select
|
||
v-model="form.eventTypes"
|
||
multiple
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
style="width: 100%"
|
||
:disabled="startedLocked"
|
||
placeholder="输入钱包流水的完整 eventType 后回车,例如 HOT_GAME_123;不支持前缀和通配符"
|
||
/>
|
||
<div class="form-help">
|
||
这里只做精确等值匹配。第三方游戏通常包含游戏 ID,请从真实金币支出流水复制完整 eventType,避免把礼物或其他支出计入活动。
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="排行口径">
|
||
<el-input value="消费金币榜(TYCOON)" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="排行周期">
|
||
<el-input value="活动总榜(OVERALL)" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="活动说明">
|
||
<el-input v-model.trim="form.activityDesc" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
|
||
<div class="section-title">抽奖奖项(固定 7 个且必须全部启用)</div>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="权重只表示相对概率;总权重必须大于 0。库存 -1 表示不限库存,0 表示不可再抽中。"
|
||
/>
|
||
<el-table :data="form.prizes" border class="config-table">
|
||
<el-table-column label="顺序" width="70" align="center" prop="sortOrder" />
|
||
<el-table-column label="启用" width="75" align="center">
|
||
<template slot-scope="scope">
|
||
<el-switch v-model="scope.row.enabled" :disabled="startedLocked" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖项名称" min-width="160">
|
||
<template slot-scope="scope">
|
||
<el-input v-model.trim="scope.row.prizeName" maxlength="128" placeholder="必填" :disabled="startedLocked" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖项图片" min-width="175" align="center">
|
||
<template slot-scope="scope">
|
||
<el-image
|
||
v-if="startedLocked"
|
||
class="readonly-prize-image"
|
||
:src="scope.row.prizeImage"
|
||
fit="contain"
|
||
/>
|
||
<upload-image
|
||
v-else
|
||
v-model="scope.row.prizeImage"
|
||
file-dir="activity/game-king"
|
||
tips="建议透明 PNG"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="权重" min-width="120">
|
||
<template slot-scope="scope">
|
||
<el-input-number v-model="scope.row.weight" :min="1" :max="999999999" :precision="0" controls-position="right" :disabled="startedLocked" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="库存" min-width="120">
|
||
<template slot-scope="scope">
|
||
<el-input-number v-model="scope.row.stock" :min="-1" :max="999999999" :precision="0" controls-position="right" :disabled="startedLocked" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励资源组" min-width="190">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" :disabled="startedLocked" @click="selectResourceGroup('prize', scope.$index)">
|
||
{{ scope.row.resourceGroupId ? `资源组 ${scope.row.resourceGroupId}` : '选择资源组' }}
|
||
</el-button>
|
||
<props-row v-if="scope.row.rewards && scope.row.rewards.length" :list="scope.row.rewards" />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="weight-summary">当前总权重:{{ totalPrizeWeight }}</div>
|
||
|
||
<div class="section-title">排行榜结算奖励(固定六档)</div>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="名次范围不可增删或修改;活动结束后按最终消费榜快照结算,失败记录可在结算记录中单独重试。"
|
||
/>
|
||
<el-table :data="form.rankRewards" border class="config-table">
|
||
<el-table-column label="名次范围" width="130" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag type="success">{{ rankRangeText(scope.row) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励名称" min-width="220">
|
||
<template slot-scope="scope">
|
||
<el-input v-model.trim="scope.row.rewardName" maxlength="128" placeholder="例如 Top 1 奖励" :disabled="startedLocked" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励资源组" min-width="360">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" :disabled="startedLocked" @click="selectResourceGroup('rank', scope.$index)">
|
||
{{ scope.row.resourceGroupId ? `资源组 ${scope.row.resourceGroupId}` : '选择资源组' }}
|
||
</el-button>
|
||
<props-row v-if="scope.row.rewards && scope.row.rewards.length" :list="scope.row.rewards" />
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<span slot="footer">
|
||
<el-button @click="editorVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="saveLoading" @click="submitActivity">保存全部配置</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="`${selectedActivity.activityName || ''} - 结算记录`"
|
||
:visible.sync="settlementsVisible"
|
||
width="1120px"
|
||
top="6vh"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="dialog-actions">
|
||
<el-button icon="el-icon-refresh" :loading="settlementsLoading" @click="loadSettlements">刷新</el-button>
|
||
<el-button
|
||
type="danger"
|
||
icon="el-icon-s-check"
|
||
:disabled="!settlementReady(selectedActivity)"
|
||
:loading="settleLoading"
|
||
@click="manualSettle"
|
||
>
|
||
手工结算
|
||
</el-button>
|
||
<span class="form-help">到达“结束时间 + 结算等待”后才可执行;UNKNOWN 必须按奖励项 ID 核账,禁止直接重发。</span>
|
||
</div>
|
||
<el-table v-loading="settlementsLoading" :data="settlementRecords" border max-height="570">
|
||
<el-table-column type="expand" width="50">
|
||
<template slot-scope="scope">
|
||
<el-table :data="scope.row.deliveryItems || []" border size="mini" class="delivery-item-table">
|
||
<el-table-column label="奖励项 ID" min-width="145" prop="id" />
|
||
<el-table-column label="类型" min-width="115">
|
||
<template slot-scope="itemScope">
|
||
{{ itemScope.row.rewardType }} / {{ itemScope.row.detailType || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="内容" min-width="160" show-overflow-tooltip prop="content" />
|
||
<el-table-column label="数量" width="80" align="right" prop="quantity" />
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template slot-scope="itemScope">
|
||
<el-tag :type="deliveryStatusTag(itemScope.row.deliveryStatus)">{{ itemScope.row.deliveryStatus }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="重试" width="70" align="center" prop="retryCount" />
|
||
<el-table-column label="失败原因" min-width="180" show-overflow-tooltip prop="failureReason" />
|
||
<el-table-column label="发放时间" min-width="165" align="center">
|
||
<template slot-scope="itemScope">{{ formatTime(itemScope.row.deliverTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="UNKNOWN 核账" min-width="185" align="center" fixed="right">
|
||
<template slot-scope="itemScope">
|
||
<template v-if="itemScope.row.deliveryStatus === 'UNKNOWN'">
|
||
<el-button
|
||
type="text"
|
||
:loading="resolvingDeliveryItemId === itemScope.row.id"
|
||
@click="resolveUnknownDeliveryItem(itemScope.row, true)"
|
||
>
|
||
已到账
|
||
</el-button>
|
||
<el-button
|
||
type="text"
|
||
class="danger-link"
|
||
:loading="resolvingDeliveryItemId === itemScope.row.id"
|
||
@click="resolveUnknownDeliveryItem(itemScope.row, false)"
|
||
>
|
||
未到账并补发
|
||
</el-button>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="名次" width="70" align="center" prop="rank" />
|
||
<el-table-column label="用户 ID" min-width="160" prop="userId" />
|
||
<el-table-column label="消费金币" min-width="120" align="right">
|
||
<template slot-scope="scope">{{ formatNumber(scope.row.totalConsumed) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="资源组" min-width="145" prop="resourceGroupId" />
|
||
<el-table-column label="发放状态" width="110" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="deliveryStatusTag(scope.row.deliveryStatus)">{{ scope.row.deliveryStatus }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="重试次数" width="90" align="center" prop="retryCount" />
|
||
<el-table-column label="失败原因" min-width="210" show-overflow-tooltip prop="failureReason" />
|
||
<el-table-column label="发放时间" min-width="165" align="center">
|
||
<template slot-scope="scope">{{ formatTime(scope.row.deliverTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button
|
||
v-if="scope.row.deliveryStatus === 'FAILED'"
|
||
type="text"
|
||
:loading="retryingRecordId === scope.row.id"
|
||
@click="retrySettlement(scope.row)"
|
||
>
|
||
重试
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="`${selectedActivity.activityName || ''} - 抽奖发放记录`"
|
||
:visible.sync="drawRecordsVisible"
|
||
width="1120px"
|
||
top="6vh"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="dialog-actions">
|
||
<el-select v-model="drawDeliveryStatus" clearable placeholder="全部发放状态" @change="loadDrawRecords">
|
||
<el-option label="待发放" value="PENDING" />
|
||
<el-option label="发放中" value="PROCESSING" />
|
||
<el-option label="成功" value="SUCCESS" />
|
||
<el-option label="失败" value="FAILED" />
|
||
<el-option label="结果未知(需核账)" value="UNKNOWN" />
|
||
</el-select>
|
||
<el-button icon="el-icon-refresh" :loading="drawRecordsLoading" @click="loadDrawRecords">刷新</el-button>
|
||
<span class="form-help">仅 FAILED 可重试;UNKNOWN 表示调用结果不确定,必须按奖励项 ID 核账,页面不会自动重发。</span>
|
||
</div>
|
||
<el-table v-loading="drawRecordsLoading" :data="drawRecords" border max-height="570">
|
||
<el-table-column type="expand" width="50">
|
||
<template slot-scope="scope">
|
||
<el-table :data="scope.row.deliveryItems || []" border size="mini" class="delivery-item-table">
|
||
<el-table-column label="奖励项 ID" min-width="145" prop="id" />
|
||
<el-table-column label="类型" min-width="115">
|
||
<template slot-scope="itemScope">
|
||
{{ itemScope.row.rewardType }} / {{ itemScope.row.detailType || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="内容" min-width="160" show-overflow-tooltip prop="content" />
|
||
<el-table-column label="数量" width="80" align="right" prop="quantity" />
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template slot-scope="itemScope">
|
||
<el-tag :type="deliveryStatusTag(itemScope.row.deliveryStatus)">{{ itemScope.row.deliveryStatus }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="重试" width="70" align="center" prop="retryCount" />
|
||
<el-table-column label="失败原因" min-width="180" show-overflow-tooltip prop="failureReason" />
|
||
<el-table-column label="发放时间" min-width="165" align="center">
|
||
<template slot-scope="itemScope">{{ formatTime(itemScope.row.deliverTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="UNKNOWN 核账" min-width="185" align="center" fixed="right">
|
||
<template slot-scope="itemScope">
|
||
<template v-if="itemScope.row.deliveryStatus === 'UNKNOWN'">
|
||
<el-button
|
||
type="text"
|
||
:loading="resolvingDeliveryItemId === itemScope.row.id"
|
||
@click="resolveUnknownDeliveryItem(itemScope.row, true)"
|
||
>
|
||
已到账
|
||
</el-button>
|
||
<el-button
|
||
type="text"
|
||
class="danger-link"
|
||
:loading="resolvingDeliveryItemId === itemScope.row.id"
|
||
@click="resolveUnknownDeliveryItem(itemScope.row, false)"
|
||
>
|
||
未到账并补发
|
||
</el-button>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="记录 ID" min-width="150" prop="id" />
|
||
<el-table-column label="用户 ID" min-width="150" prop="userId" />
|
||
<el-table-column label="奖项" min-width="150" prop="prizeName" />
|
||
<el-table-column label="资源组" min-width="145" prop="resourceGroupId" />
|
||
<el-table-column label="发放状态" width="110" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="deliveryStatusTag(scope.row.deliveryStatus)">{{ scope.row.deliveryStatus }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="失败原因" min-width="210" show-overflow-tooltip prop="failureReason" />
|
||
<el-table-column label="抽奖时间" min-width="165" align="center">
|
||
<template slot-scope="scope">{{ formatTime(scope.row.drawTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="发放时间" min-width="165" align="center">
|
||
<template slot-scope="scope">{{ formatTime(scope.row.deliverTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button
|
||
v-if="scope.row.deliveryStatus === 'FAILED'"
|
||
type="text"
|
||
:loading="retryingDrawRecordId === scope.row.id"
|
||
@click="retryDrawRecord(scope.row)"
|
||
>
|
||
重试
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="`${selectedActivity.activityName || ''} - 历史游戏流水补数`"
|
||
:visible.sync="backfillVisible"
|
||
width="620px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="补数按收支类型、单个精确 eventType 和金币流水主键游标执行。每组条件必须分别补到 finished=true,防止共用游标漏数据。"
|
||
/>
|
||
<el-form label-width="120px" class="backfill-form">
|
||
<el-form-item label="流水类型">
|
||
<el-select v-model="backfillForm.receiptType" style="width: 100%" @change="resetBackfillCursor">
|
||
<el-option label="游戏消费(Tycoon / 抽奖次数)" :value="1" />
|
||
<el-option label="游戏中奖收入(Victorious)" :value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="精确 eventType">
|
||
<el-select
|
||
v-model="backfillForm.eventType"
|
||
style="width: 100%"
|
||
filterable
|
||
@change="resetBackfillCursor"
|
||
>
|
||
<el-option
|
||
v-for="item in selectedActivity.eventTypes || []"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="lastId 游标">
|
||
<el-input v-model.trim="backfillForm.lastId" placeholder="首次留空;后续自动回填 nextLastId" />
|
||
</el-form-item>
|
||
<el-form-item label="单批数量">
|
||
<el-input-number v-model="backfillForm.limit" :min="1" :max="500" :precision="0" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-descriptions v-if="backfillResult" :column="2" border>
|
||
<el-descriptions-item label="扫描">{{ backfillResult.scanned }}</el-descriptions-item>
|
||
<el-descriptions-item label="计入">{{ backfillResult.accepted }}</el-descriptions-item>
|
||
<el-descriptions-item label="下一游标">{{ backfillResult.nextLastId || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="完成">{{ backfillResult.finished ? '是' : '否' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
<span slot="footer">
|
||
<el-button @click="backfillVisible = false">关闭</el-button>
|
||
<el-button type="primary" :loading="backfillLoading" :disabled="backfillResult && backfillResult.finished" @click="runBackfill">
|
||
执行一批
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<associated-resources
|
||
v-if="resourceSelectorVisible"
|
||
:sys-origin="form.sysOrigin"
|
||
@select="applyResourceGroup"
|
||
@close="resourceSelectorVisible = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex'
|
||
import AssociatedResources from '@/components/data/AssociatedResources'
|
||
import PropsRow from '@/components/data/PropsRow'
|
||
import UploadImage from '@/components/upload-image'
|
||
import {
|
||
backfillAslanGameKingConsumption,
|
||
getAslanGameKingDetail,
|
||
listAslanGameKingActivities,
|
||
listAslanGameKingDrawRecords,
|
||
listAslanGameKingSettlementRecords,
|
||
resolveAslanGameKingUnknownDeliveryItem,
|
||
retryAslanGameKingDraw,
|
||
retryAslanGameKingSettlement,
|
||
saveAslanGameKingActivity,
|
||
setAslanGameKingEnabled,
|
||
settleAslanGameKingActivity
|
||
} from '@/api/aslan-game-king'
|
||
|
||
const REQUIRED_RANK_RANGES = [
|
||
[1, 1],
|
||
[2, 2],
|
||
[3, 3],
|
||
[4, 7],
|
||
[8, 10],
|
||
[11, 30]
|
||
]
|
||
|
||
export default {
|
||
name: 'ActivityGameKing',
|
||
components: { AssociatedResources, PropsRow, UploadImage },
|
||
data() {
|
||
return {
|
||
activities: [],
|
||
listLoading: false,
|
||
editorVisible: false,
|
||
editorLoading: false,
|
||
saveLoading: false,
|
||
serverTime: 0,
|
||
originalStartTime: 0,
|
||
backendActivityStatus: '',
|
||
form: this.emptyForm(),
|
||
formRules: {
|
||
activityCode: [{ required: true, message: '请输入活动编码', trigger: 'blur' }],
|
||
activityName: [{ required: true, message: '请输入活动名称', trigger: 'blur' }],
|
||
sysOrigin: [{ required: true, message: '请选择所属平台', trigger: 'change' }],
|
||
timeZone: [{ required: true, message: '请选择活动时区', trigger: 'change' }],
|
||
timeRange: [{ type: 'array', required: true, len: 2, message: '请选择完整活动时间', trigger: 'change' }],
|
||
coinPerDraw: [{ required: true, message: '请输入每次抽奖金币数', trigger: 'blur' }],
|
||
settlementDelayMinutes: [{ required: true, message: '请输入结算等待分钟数', trigger: 'blur' }],
|
||
eventTypes: [{ type: 'array', required: true, min: 1, message: '至少配置一个精确 eventType', trigger: 'change' }]
|
||
},
|
||
timeZoneOptions: ['Asia/Riyadh', 'Asia/Shanghai', 'UTC'],
|
||
resourceSelectorVisible: false,
|
||
resourceSelection: null,
|
||
selectedActivity: {},
|
||
settlementsVisible: false,
|
||
settlementsLoading: false,
|
||
settleLoading: false,
|
||
settlementRecords: [],
|
||
retryingRecordId: '',
|
||
drawRecordsVisible: false,
|
||
drawRecordsLoading: false,
|
||
drawRecords: [],
|
||
drawDeliveryStatus: '',
|
||
retryingDrawRecordId: '',
|
||
resolvingDeliveryItemId: '',
|
||
backfillVisible: false,
|
||
backfillLoading: false,
|
||
backfillResult: null,
|
||
backfillForm: {
|
||
receiptType: 1,
|
||
eventType: '',
|
||
lastId: '',
|
||
limit: 500
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
...mapGetters(['permissionsSysOriginPlatforms']),
|
||
startedLocked() {
|
||
if (!this.form.id) {
|
||
return false
|
||
}
|
||
// 以详情接口返回的 serverTime/startTime 为最终判定,避免运营电脑时间漂移绕过后端锁定。
|
||
return ['ACTIVE', 'ENDED'].indexOf(this.backendActivityStatus) >= 0 ||
|
||
(this.serverTime > 0 && this.originalStartTime > 0 && this.serverTime >= this.originalStartTime)
|
||
},
|
||
totalPrizeWeight() {
|
||
return this.form.prizes.reduce((total, item) => total + Number(item.weight || 0), 0)
|
||
}
|
||
},
|
||
created() {
|
||
this.loadActivities()
|
||
},
|
||
methods: {
|
||
emptyForm() {
|
||
return {
|
||
id: '',
|
||
activityCode: '',
|
||
activityName: '',
|
||
activityDesc: '',
|
||
sysOrigin: 'ATYOU',
|
||
timeZone: 'Asia/Riyadh',
|
||
timeRange: [],
|
||
coinPerDraw: 1,
|
||
settlementDelayMinutes: 30,
|
||
eventTypes: [],
|
||
rankingType: 'TYCOON',
|
||
rankingPeriod: 'OVERALL',
|
||
enabled: false,
|
||
prizes: this.emptyPrizes(),
|
||
rankRewards: this.emptyRankRewards()
|
||
}
|
||
},
|
||
emptyPrizes() {
|
||
return Array.from({ length: 7 }, (item, index) => ({
|
||
id: '',
|
||
activityId: '',
|
||
prizeName: '',
|
||
prizeImage: '',
|
||
weight: 1,
|
||
stock: -1,
|
||
resourceGroupId: '',
|
||
enabled: true,
|
||
sortOrder: index + 1,
|
||
rewards: []
|
||
}))
|
||
},
|
||
emptyRankRewards() {
|
||
return REQUIRED_RANK_RANGES.map((range, index) => ({
|
||
id: '',
|
||
activityId: '',
|
||
startRank: range[0],
|
||
endRank: range[1],
|
||
resourceGroupId: '',
|
||
rewardName: index < 3 ? `Top ${index + 1} 奖励` : `Top ${range[0]}-${range[1]} 奖励`,
|
||
rewards: []
|
||
}))
|
||
},
|
||
loadActivities() {
|
||
this.listLoading = true
|
||
listAslanGameKingActivities().then(res => {
|
||
this.activities = Array.isArray(res.body) ? res.body : []
|
||
}).finally(() => {
|
||
this.listLoading = false
|
||
})
|
||
},
|
||
openCreate() {
|
||
this.resetEditor()
|
||
const availablePlatforms = this.permissionsSysOriginPlatforms || []
|
||
if (availablePlatforms.length > 0) {
|
||
this.form.sysOrigin = availablePlatforms[0].value
|
||
}
|
||
this.editorVisible = true
|
||
},
|
||
openEdit(row) {
|
||
this.resetEditor()
|
||
this.editorVisible = true
|
||
this.editorLoading = true
|
||
getAslanGameKingDetail(row.id).then(res => {
|
||
const detail = res.body || {}
|
||
const activity = detail.activity || {}
|
||
const prizes = this.mergePrizes(detail.prizes || [])
|
||
const rankRewards = this.mergeRankRewards(detail.rankRewards || [])
|
||
this.form = Object.assign(this.emptyForm(), activity, {
|
||
timeZone: activity.timeZone || 'Asia/Riyadh',
|
||
settlementDelayMinutes: activity.settlementDelayMinutes === null || activity.settlementDelayMinutes === undefined
|
||
? 30 : Number(activity.settlementDelayMinutes),
|
||
timeRange: [activity.startTime, activity.endTime],
|
||
prizes,
|
||
rankRewards
|
||
})
|
||
this.serverTime = Number(detail.serverTime || 0)
|
||
this.originalStartTime = Number(activity.startTime || 0)
|
||
this.backendActivityStatus = detail.activityStatus || ''
|
||
}).finally(() => {
|
||
this.editorLoading = false
|
||
})
|
||
},
|
||
mergePrizes(prizes) {
|
||
const result = this.emptyPrizes()
|
||
prizes.forEach((item, index) => {
|
||
const targetIndex = Number(item.sortOrder || index + 1) - 1
|
||
if (targetIndex >= 0 && targetIndex < result.length) {
|
||
result.splice(targetIndex, 1, Object.assign({}, result[targetIndex], item, {
|
||
stock: item.stock === null || item.stock === undefined ? -1 : item.stock,
|
||
rewards: item.rewardItems || item.rewards || []
|
||
}))
|
||
}
|
||
})
|
||
return result
|
||
},
|
||
mergeRankRewards(rankRewards) {
|
||
const result = this.emptyRankRewards()
|
||
rankRewards.forEach(item => {
|
||
const index = REQUIRED_RANK_RANGES.findIndex(range => range[0] === Number(item.startRank) && range[1] === Number(item.endRank))
|
||
if (index >= 0) {
|
||
result.splice(index, 1, Object.assign({}, result[index], item, {
|
||
rewards: item.rewardItems || item.rewards || []
|
||
}))
|
||
}
|
||
})
|
||
return result
|
||
},
|
||
resetEditor() {
|
||
this.form = this.emptyForm()
|
||
this.serverTime = 0
|
||
this.originalStartTime = 0
|
||
this.backendActivityStatus = ''
|
||
this.resourceSelectorVisible = false
|
||
this.resourceSelection = null
|
||
if (this.$refs.activityForm) {
|
||
this.$refs.activityForm.clearValidate()
|
||
}
|
||
},
|
||
submitActivity() {
|
||
this.$refs.activityForm.validate(valid => {
|
||
if (!valid || !this.validateConfiguration()) {
|
||
return
|
||
}
|
||
const payload = this.buildSavePayload()
|
||
this.saveLoading = true
|
||
// 基础配置、七个奖项与六档排行奖励一次提交,由后端事务保证不会保存出半套配置。
|
||
saveAslanGameKingActivity(payload).then(() => {
|
||
this.$message.success('活动配置已保存')
|
||
this.editorVisible = false
|
||
this.loadActivities()
|
||
}).finally(() => {
|
||
this.saveLoading = false
|
||
})
|
||
})
|
||
},
|
||
validateConfiguration() {
|
||
const range = this.form.timeRange || []
|
||
if (range.length !== 2 || Number(range[0]) >= Number(range[1])) {
|
||
this.$message.error('活动结束时间必须晚于开始时间')
|
||
return false
|
||
}
|
||
if (!Number.isSafeInteger(Number(this.form.coinPerDraw)) || Number(this.form.coinPerDraw) <= 0) {
|
||
this.$message.error('每次抽奖金币数必须是大于 0 的安全整数')
|
||
return false
|
||
}
|
||
if (!Number.isInteger(Number(this.form.settlementDelayMinutes)) ||
|
||
Number(this.form.settlementDelayMinutes) < 0 || Number(this.form.settlementDelayMinutes) > 1440) {
|
||
this.$message.error('结算等待必须是 0 到 1440 的整数分钟')
|
||
return false
|
||
}
|
||
const eventTypes = (this.form.eventTypes || []).map(item => String(item).trim())
|
||
const invalidEventType = eventTypes.find(item => !item || item.length > 64 || item.indexOf(',') >= 0)
|
||
if (invalidEventType !== undefined || new Set(eventTypes).size !== eventTypes.length) {
|
||
this.$message.error('eventType 必须非空、最长 64 字符、不含逗号且不可重复')
|
||
return false
|
||
}
|
||
// 活动开始后后端只允许更新非统计字段;奖项与排行奖励既不重新校验,也不随保存请求发送。
|
||
if (this.startedLocked) {
|
||
return true
|
||
}
|
||
if (this.form.prizes.length !== 7 || this.form.prizes.some(item => !item.enabled)) {
|
||
this.$message.error('必须且只能配置 7 个启用奖项')
|
||
return false
|
||
}
|
||
const invalidPrize = this.form.prizes.find(item =>
|
||
!item.prizeName || !item.prizeImage || !item.resourceGroupId ||
|
||
!Number.isSafeInteger(Number(item.weight)) || Number(item.weight) <= 0 ||
|
||
!Number.isSafeInteger(Number(item.stock)) || Number(item.stock) < -1
|
||
)
|
||
if (invalidPrize || this.totalPrizeWeight <= 0) {
|
||
this.$message.error('请补全 7 个奖项的名称、图片、正整数权重、合法库存和奖励资源组')
|
||
return false
|
||
}
|
||
const rangesValid = this.form.rankRewards.length === REQUIRED_RANK_RANGES.length &&
|
||
this.form.rankRewards.every((item, index) =>
|
||
Number(item.startRank) === REQUIRED_RANK_RANGES[index][0] &&
|
||
Number(item.endRank) === REQUIRED_RANK_RANGES[index][1] &&
|
||
Boolean(item.resourceGroupId)
|
||
)
|
||
if (!rangesValid) {
|
||
this.$message.error('六档名次范围必须保持固定,并为每档选择奖励资源组')
|
||
return false
|
||
}
|
||
return true
|
||
},
|
||
buildSavePayload() {
|
||
const payload = {
|
||
id: this.form.id || null,
|
||
activityCode: this.form.activityCode.trim(),
|
||
activityName: this.form.activityName.trim(),
|
||
activityDesc: this.form.activityDesc,
|
||
sysOrigin: this.form.sysOrigin,
|
||
timeZone: this.form.timeZone,
|
||
startTime: Number(this.form.timeRange[0]),
|
||
endTime: Number(this.form.timeRange[1]),
|
||
settlementDelayMinutes: Number(this.form.settlementDelayMinutes),
|
||
coinPerDraw: Number(this.form.coinPerDraw),
|
||
eventTypes: this.form.eventTypes.map(item => String(item).trim()),
|
||
rankingType: 'TYCOON',
|
||
rankingPeriod: 'OVERALL',
|
||
enabled: Boolean(this.form.enabled)
|
||
}
|
||
if (!this.startedLocked) {
|
||
payload.prizes = this.form.prizes.map(item => ({
|
||
id: item.id || null,
|
||
activityId: this.form.id || null,
|
||
prizeName: item.prizeName.trim(),
|
||
prizeImage: item.prizeImage,
|
||
weight: Number(item.weight),
|
||
stock: Number(item.stock),
|
||
resourceGroupId: String(item.resourceGroupId),
|
||
enabled: true,
|
||
sortOrder: Number(item.sortOrder)
|
||
}))
|
||
payload.rankRewards = this.form.rankRewards.map(item => ({
|
||
id: item.id || null,
|
||
activityId: this.form.id || null,
|
||
startRank: Number(item.startRank),
|
||
endRank: Number(item.endRank),
|
||
resourceGroupId: String(item.resourceGroupId),
|
||
rewardName: item.rewardName
|
||
}))
|
||
}
|
||
return payload
|
||
},
|
||
selectResourceGroup(type, index) {
|
||
if (this.startedLocked) {
|
||
return
|
||
}
|
||
if (!this.form.sysOrigin) {
|
||
this.$message.warning('请先选择所属平台')
|
||
return
|
||
}
|
||
this.resourceSelection = { type, index }
|
||
this.resourceSelectorVisible = true
|
||
},
|
||
applyResourceGroup(source) {
|
||
if (!this.resourceSelection) {
|
||
return
|
||
}
|
||
const list = this.resourceSelection.type === 'prize' ? this.form.prizes : this.form.rankRewards
|
||
const row = list[this.resourceSelection.index]
|
||
// Long ID 保留为字符串传回 Java,避免 Snowflake ID 在浏览器里发生精度截断。
|
||
row.resourceGroupId = String(source.id)
|
||
row.rewards = source.rewardConfigList || []
|
||
if (this.resourceSelection.type === 'rank' && !row.rewardName) {
|
||
row.rewardName = source.name || ''
|
||
}
|
||
this.resourceSelectorVisible = false
|
||
this.resourceSelection = null
|
||
},
|
||
changeEnabled(row) {
|
||
const enabled = Boolean(row.enabled)
|
||
const action = enabled ? '启用' : '停用'
|
||
this.$confirm(`确认${action}活动「${row.activityName}」?`, '状态确认', { type: 'warning' }).then(() => {
|
||
return setAslanGameKingEnabled(row.id, enabled)
|
||
}).then(() => {
|
||
this.$message.success(`活动已${action}`)
|
||
this.loadActivities()
|
||
}).catch(() => {
|
||
// 确认取消和接口失败都恢复开关,列表状态只能以后端成功响应为准。
|
||
row.enabled = !enabled
|
||
})
|
||
},
|
||
openSettlements(row) {
|
||
this.selectedActivity = row
|
||
this.settlementsVisible = true
|
||
this.loadSettlements()
|
||
},
|
||
loadSettlements() {
|
||
if (!this.selectedActivity.id) {
|
||
return
|
||
}
|
||
this.settlementsLoading = true
|
||
listAslanGameKingSettlementRecords(this.selectedActivity.id).then(res => {
|
||
this.settlementRecords = Array.isArray(res.body) ? res.body : []
|
||
}).finally(() => {
|
||
this.settlementsLoading = false
|
||
})
|
||
},
|
||
manualSettle() {
|
||
this.$confirm('将按活动结束时最终消费榜生成 Top 30 奖励并立即尝试发放,确认继续?', '手工结算', {
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.settleLoading = true
|
||
return settleAslanGameKingActivity(this.selectedActivity.id)
|
||
}).then(() => {
|
||
this.$message.success('结算任务已执行')
|
||
this.loadSettlements()
|
||
this.loadActivities()
|
||
}).finally(() => {
|
||
this.settleLoading = false
|
||
})
|
||
},
|
||
retrySettlement(row) {
|
||
this.retryingRecordId = row.id
|
||
retryAslanGameKingSettlement(row.id).then(() => {
|
||
this.$message.success('该条奖励已重新发放')
|
||
this.loadSettlements()
|
||
}).finally(() => {
|
||
this.retryingRecordId = ''
|
||
})
|
||
},
|
||
openDrawRecords(row) {
|
||
this.selectedActivity = row
|
||
this.drawDeliveryStatus = ''
|
||
this.drawRecordsVisible = true
|
||
this.loadDrawRecords()
|
||
},
|
||
loadDrawRecords() {
|
||
if (!this.selectedActivity.id) {
|
||
return
|
||
}
|
||
const params = { limit: 200 }
|
||
if (this.drawDeliveryStatus) {
|
||
params.deliveryStatus = this.drawDeliveryStatus
|
||
}
|
||
this.drawRecordsLoading = true
|
||
listAslanGameKingDrawRecords(this.selectedActivity.id, params).then(res => {
|
||
this.drawRecords = Array.isArray(res.body) ? res.body : []
|
||
}).finally(() => {
|
||
this.drawRecordsLoading = false
|
||
})
|
||
},
|
||
retryDrawRecord(row) {
|
||
this.retryingDrawRecordId = row.id
|
||
retryAslanGameKingDraw(row.id).then(() => {
|
||
this.$message.success('该条抽奖奖励已重新发放')
|
||
this.loadDrawRecords()
|
||
}).finally(() => {
|
||
this.retryingDrawRecordId = ''
|
||
})
|
||
},
|
||
resolveUnknownDeliveryItem(item, delivered) {
|
||
const action = delivered ? '确认该奖励项已经到账并标记成功' : '确认该奖励项未到账并立即补发'
|
||
const warning = delivered
|
||
? '此操作不会再次发奖。请先按奖励项 ID 核对钱包流水或背包记录。'
|
||
: '此操作会真实补发奖励。只有下游核账明确未到账时才能继续,否则可能重复发放。'
|
||
this.$confirm(`${action}?${warning}`, 'UNKNOWN 奖励核账', {
|
||
type: delivered ? 'warning' : 'error',
|
||
confirmButtonText: delivered ? '已核账,标记到账' : '已核账,确认补发'
|
||
}).then(() => {
|
||
this.resolvingDeliveryItemId = item.id
|
||
return resolveAslanGameKingUnknownDeliveryItem(item.id, delivered)
|
||
}).then(() => {
|
||
this.$message.success(delivered ? '已标记该奖励项到账' : '已解锁并执行补发')
|
||
if (this.drawRecordsVisible) {
|
||
this.loadDrawRecords()
|
||
}
|
||
if (this.settlementsVisible) {
|
||
this.loadSettlements()
|
||
}
|
||
}).finally(() => {
|
||
this.resolvingDeliveryItemId = ''
|
||
})
|
||
},
|
||
openBackfill(row) {
|
||
this.selectedActivity = row
|
||
this.backfillForm = {
|
||
receiptType: 1,
|
||
eventType: row.eventTypes && row.eventTypes.length ? row.eventTypes[0] : '',
|
||
lastId: '',
|
||
limit: 500
|
||
}
|
||
this.backfillResult = null
|
||
this.backfillVisible = true
|
||
},
|
||
runBackfill() {
|
||
if (this.backfillForm.receiptType !== 0 && this.backfillForm.receiptType !== 1) {
|
||
this.$message.error('请选择金币流水类型')
|
||
return
|
||
}
|
||
if (!this.backfillForm.eventType) {
|
||
this.$message.error('请选择一个精确 eventType')
|
||
return
|
||
}
|
||
const params = {
|
||
eventType: this.backfillForm.eventType,
|
||
receiptType: this.backfillForm.receiptType,
|
||
limit: Number(this.backfillForm.limit)
|
||
}
|
||
if (this.backfillForm.lastId) {
|
||
params.lastId = this.backfillForm.lastId
|
||
}
|
||
this.backfillLoading = true
|
||
backfillAslanGameKingConsumption(this.selectedActivity.id, params).then(res => {
|
||
this.backfillResult = res.body || {}
|
||
// 游标由服务端返回并按 eventType 独立推进;浏览器不自行计算,避免大整数精度和跳行问题。
|
||
this.backfillForm.lastId = this.backfillResult.nextLastId || this.backfillForm.lastId
|
||
this.$message.success(this.backfillResult.finished ? '该 eventType 已补数完成' : '本批补数完成,请继续下一批')
|
||
}).finally(() => {
|
||
this.backfillLoading = false
|
||
})
|
||
},
|
||
resetBackfillCursor() {
|
||
// lastId 仅在同一 receiptType + eventType 查询条件内有效,切换条件必须从首批重新开始。
|
||
this.backfillForm.lastId = ''
|
||
this.backfillResult = null
|
||
},
|
||
activityStatus(row) {
|
||
if (!row || !row.id) {
|
||
return 'DISABLED'
|
||
}
|
||
if (!row.enabled) {
|
||
return 'DISABLED'
|
||
}
|
||
const now = Date.now()
|
||
if (now < Number(row.startTime)) {
|
||
return 'UPCOMING'
|
||
}
|
||
if (now >= Number(row.endTime)) {
|
||
return 'ENDED'
|
||
}
|
||
return 'ACTIVE'
|
||
},
|
||
hasStarted(row) {
|
||
return Boolean(row && row.startTime && Date.now() >= Number(row.startTime))
|
||
},
|
||
settlementReady(row) {
|
||
// 手工结算只负责首次冻结榜单;PROCESSING/PARTIAL_FAILED 的恢复与 UNKNOWN 核账
|
||
// 走各自专用链路,避免运营把“继续发放”误当成再次生成快照。
|
||
if (!row || !row.id || row.settlementStatus !== 'NOT_STARTED') {
|
||
return false
|
||
}
|
||
const settlementTime = Number(row.settlementTime ||
|
||
(Number(row.endTime) + Number(row.settlementDelayMinutes || 0) * 60 * 1000))
|
||
return Number.isFinite(settlementTime) && Date.now() >= settlementTime
|
||
},
|
||
activityStatusText(status) {
|
||
return {
|
||
DISABLED: '未启用',
|
||
UPCOMING: '待开始',
|
||
ACTIVE: '进行中',
|
||
ENDED: '已结束'
|
||
}[status] || status
|
||
},
|
||
activityStatusTag(status) {
|
||
return {
|
||
DISABLED: 'info',
|
||
UPCOMING: 'warning',
|
||
ACTIVE: 'success',
|
||
ENDED: 'danger'
|
||
}[status] || 'info'
|
||
},
|
||
settlementStatusText(status) {
|
||
return {
|
||
NOT_STARTED: '未结算',
|
||
PROCESSING: '结算中',
|
||
COMPLETED: '已完成',
|
||
PARTIAL_FAILED: '部分失败'
|
||
}[status] || status || '未结算'
|
||
},
|
||
deliveryStatusTag(status) {
|
||
return {
|
||
SUCCESS: 'success',
|
||
FAILED: 'danger',
|
||
UNKNOWN: 'warning',
|
||
PROCESSING: 'warning',
|
||
PENDING: 'info'
|
||
}[status] || 'info'
|
||
},
|
||
rankRangeText(row) {
|
||
return row.startRank === row.endRank ? `Top ${row.startRank}` : `Top ${row.startRank}-${row.endRank}`
|
||
},
|
||
formatTime(value) {
|
||
if (!value) {
|
||
return '-'
|
||
}
|
||
const date = new Date(Number(value))
|
||
if (Number.isNaN(date.getTime())) {
|
||
return '-'
|
||
}
|
||
const pad = number => String(number).padStart(2, '0')
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||
},
|
||
formatNumber(value) {
|
||
if (value === null || value === undefined || value === '') {
|
||
return '-'
|
||
}
|
||
return Number(value).toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.aslan-game-king {
|
||
.filter-container {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.page-tip {
|
||
flex: 1;
|
||
margin-left: 15px;
|
||
}
|
||
|
||
.primary-text {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.secondary-text,
|
||
.form-help {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.event-tag {
|
||
margin: 2px 4px 2px 0;
|
||
}
|
||
|
||
.settlement-status {
|
||
margin-top: 6px;
|
||
color: #606266;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.editor-body {
|
||
max-height: 76vh;
|
||
overflow-y: auto;
|
||
padding: 0 8px 20px;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 22px 0 14px;
|
||
padding-left: 10px;
|
||
border-left: 4px solid #409eff;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.config-table {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.weight-summary {
|
||
padding: 10px 5px;
|
||
color: #606266;
|
||
text-align: right;
|
||
}
|
||
|
||
.readonly-prize-image {
|
||
width: 72px;
|
||
height: 72px;
|
||
}
|
||
|
||
.dialog-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.backfill-form {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.delivery-item-table {
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.danger-link {
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
</style>
|