1288 lines
51 KiB
Vue
1288 lines
51 KiB
Vue
<template>
|
||
<div class="app-container aslan-gift-challenge">
|
||
<div class="filter-container">
|
||
<el-button
|
||
v-if="canEdit"
|
||
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="积分从活动开始时间起按 1 金币 = 1 积分累计;普通幸运礼物与超级幸运礼物由服务端强制排除,不参与榜单或送礼任务。"
|
||
/>
|
||
</div>
|
||
|
||
<el-table v-loading="listLoading" :data="activities" border fit highlight-current-row>
|
||
<el-table-column label="活动" min-width="230">
|
||
<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="140" 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="210" 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="155" align="center">
|
||
<template slot-scope="scope">
|
||
<div>展示 Top {{ scope.row.displayTopN || 100 }}</div>
|
||
<div class="secondary-text">日榜等待 {{ scope.row.dailySettlementDelayMinutes || 0 }} 分钟</div>
|
||
<div class="secondary-text">总榜等待 {{ scope.row.overallSettlementDelayMinutes || 0 }} 分钟</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="150" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="activityStatusTag(activityStatus(scope.row))">
|
||
{{ activityStatusText(activityStatus(scope.row)) }}
|
||
</el-tag>
|
||
<div class="secondary-text">总榜结算:{{ settlementStatusText(scope.row.overallSettlementStatus) }}</div>
|
||
<div class="secondary-text">可结算:{{ formatTime(scope.row.overallSettlementTime) }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="启用" width="90" align="center">
|
||
<template slot-scope="scope">
|
||
<el-switch
|
||
v-model="scope.row.enabled"
|
||
:disabled="!canEnable || hasStarted(scope.row)"
|
||
@change="changeEnabled(scope.row)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="245" align="center" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" @click="openEdit(scope.row)">
|
||
{{ canEdit ? '配置' : '查看配置' }}
|
||
</el-button>
|
||
<el-button type="text" @click="openRanking(scope.row)">查看榜单</el-button>
|
||
<el-button type="text" @click="openSettlements(scope.row)">结算记录</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-dialog
|
||
:title="form.id ? '编辑 Aslan 礼物挑战' : '新建 Aslan 礼物挑战'"
|
||
:visible.sync="editorVisible"
|
||
width="1240px"
|
||
top="3vh"
|
||
: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="145px">
|
||
<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="editorReadOnly"
|
||
placeholder="例如 ASLAN_GIFT_CHALLENGE_S1"
|
||
/>
|
||
</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
|
||
:disabled="!canEdit"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="所属平台" prop="sysOrigin">
|
||
<el-input v-model="form.sysOrigin" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="活动时区" prop="timeZone">
|
||
<el-select
|
||
v-model="form.timeZone"
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
style="width: 100%"
|
||
:disabled="editorReadOnly"
|
||
placeholder="IANA 时区,例如 Asia/Riyadh"
|
||
>
|
||
<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="editorReadOnly"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="榜单展示 TopN" prop="displayTopN">
|
||
<el-input-number
|
||
v-model="form.displayTopN"
|
||
:min="1"
|
||
:max="500"
|
||
:precision="0"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
:disabled="editorReadOnly"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="日榜晚到窗口" prop="dailySettlementDelayMinutes">
|
||
<el-input-number
|
||
v-model="form.dailySettlementDelayMinutes"
|
||
:min="0"
|
||
:max="1440"
|
||
:precision="0"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
:disabled="editorReadOnly"
|
||
/>
|
||
<div class="form-help">每日结束后继续接收异步礼物流水的分钟数。</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="总榜晚到窗口" prop="overallSettlementDelayMinutes">
|
||
<el-input-number
|
||
v-model="form.overallSettlementDelayMinutes"
|
||
:min="0"
|
||
:max="1440"
|
||
:precision="0"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
:disabled="editorReadOnly"
|
||
/>
|
||
<div class="form-help">活动结束后等待异步流水,再冻结活动总榜。</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="积分比例">
|
||
<el-input value="1 金币 = 1 积分(服务端固定)" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="排除礼物">
|
||
<el-input value="普通幸运礼物、超级幸运礼物(服务端强制)" 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="1000"
|
||
show-word-limit
|
||
:disabled="!canEdit"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
|
||
<div class="section-title">每日任务(固定 3 个槽位)</div>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="任务固定为 1 个每日进入页面和 2 个累计送礼金币档位;第二档门槛必须高于第一档,运营只配置展示文案、门槛和奖励。"
|
||
/>
|
||
<el-table :data="form.tasks" border class="config-table">
|
||
<el-table-column label="槽位" width="58" align="center">
|
||
<template slot-scope="scope">{{ scope.$index + 1 }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="启用" width="70" align="center">
|
||
<template slot-scope="scope">
|
||
<el-switch v-model="scope.row.enabled" disabled />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="任务编码" min-width="165">
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.taskCode }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="任务类型" min-width="170">
|
||
<template slot-scope="scope">
|
||
<el-tag size="small">{{ scope.row.taskType === 'ENTER_PAGE' ? '每日进入活动页面' : '每日累计送礼金币' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="任务标题" min-width="180">
|
||
<template slot-scope="scope">
|
||
<el-input
|
||
v-model.trim="scope.row.taskTitle"
|
||
maxlength="128"
|
||
:disabled="editorReadOnly"
|
||
placeholder="H5 展示标题"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="目标值" min-width="120">
|
||
<template slot-scope="scope">
|
||
<el-input-number
|
||
v-model="scope.row.targetValue"
|
||
:min="1"
|
||
:max="999999999999"
|
||
:precision="0"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
:disabled="editorReadOnly || scope.row.taskType === 'ENTER_PAGE'"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="排序" width="95">
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.sortOrder }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励资源组" min-width="210">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" :disabled="editorReadOnly" @click="selectResourceGroup('task', 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="task-desc-grid">
|
||
<div v-for="(task, index) in form.tasks" :key="`desc-${index}`" class="task-desc-item">
|
||
<div class="task-desc-label">槽位 {{ index + 1 }} 描述</div>
|
||
<el-input
|
||
v-model.trim="task.taskDesc"
|
||
maxlength="500"
|
||
show-word-limit
|
||
:disabled="editorReadOnly"
|
||
placeholder="选填,H5 展示说明"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-title rank-title-row">
|
||
<span>日榜奖励区间</span>
|
||
<el-button type="primary" size="mini" icon="el-icon-plus" :disabled="editorReadOnly" @click="addRankReward('DAILY')">
|
||
新增日榜区间
|
||
</el-button>
|
||
</div>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="同一周期内名次区间不能重叠,结束名次不能超过 TopN;允许留空档,但至少配置一档。"
|
||
/>
|
||
<el-table :data="form.dailyRankRewards" border class="config-table">
|
||
<el-table-column label="开始名次" width="125">
|
||
<template slot-scope="scope">
|
||
<el-input-number v-model="scope.row.startRank" :min="1" :max="form.displayTopN" :precision="0" controls-position="right" style="width: 100%" :disabled="editorReadOnly" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结束名次" width="125">
|
||
<template slot-scope="scope">
|
||
<el-input-number v-model="scope.row.endRank" :min="1" :max="form.displayTopN" :precision="0" controls-position="right" style="width: 100%" :disabled="editorReadOnly" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励名称" min-width="190">
|
||
<template slot-scope="scope">
|
||
<el-input v-model.trim="scope.row.rewardName" maxlength="128" :disabled="editorReadOnly" placeholder="例如 日榜 Top 1" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励资源组" min-width="310">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" :disabled="editorReadOnly" @click="selectResourceGroup('daily', 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-column label="操作" width="80" align="center">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" class="danger-link" :disabled="editorReadOnly || form.dailyRankRewards.length <= 1" @click="removeRankReward('DAILY', scope.$index)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="section-title rank-title-row">
|
||
<span>活动总榜奖励区间</span>
|
||
<el-button type="primary" size="mini" icon="el-icon-plus" :disabled="editorReadOnly" @click="addRankReward('OVERALL')">
|
||
新增总榜区间
|
||
</el-button>
|
||
</div>
|
||
<el-table :data="form.overallRankRewards" border class="config-table">
|
||
<el-table-column label="开始名次" width="125">
|
||
<template slot-scope="scope">
|
||
<el-input-number v-model="scope.row.startRank" :min="1" :max="form.displayTopN" :precision="0" controls-position="right" style="width: 100%" :disabled="editorReadOnly" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结束名次" width="125">
|
||
<template slot-scope="scope">
|
||
<el-input-number v-model="scope.row.endRank" :min="1" :max="form.displayTopN" :precision="0" controls-position="right" style="width: 100%" :disabled="editorReadOnly" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励名称" min-width="190">
|
||
<template slot-scope="scope">
|
||
<el-input v-model.trim="scope.row.rewardName" maxlength="128" :disabled="editorReadOnly" placeholder="例如 总榜 Top 1" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖励资源组" min-width="310">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" :disabled="editorReadOnly" @click="selectResourceGroup('overall', 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-column label="操作" width="80" align="center">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" class="danger-link" :disabled="editorReadOnly || form.overallRankRewards.length <= 1" @click="removeRankReward('OVERALL', scope.$index)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<span slot="footer">
|
||
<el-button @click="editorVisible = false">关闭</el-button>
|
||
<el-button v-if="canEdit" type="primary" :loading="saveLoading" @click="submitActivity">
|
||
保存全部配置
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="`${selectedActivity.activityName || ''} - 排行榜`"
|
||
:visible.sync="rankingVisible"
|
||
width="930px"
|
||
top="6vh"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="dialog-actions">
|
||
<el-select v-model="rankingQuery.periodType" @change="rankingPeriodChanged">
|
||
<el-option label="活动总榜" value="OVERALL" />
|
||
<el-option label="日榜" value="DAILY" />
|
||
</el-select>
|
||
<el-date-picker
|
||
v-if="rankingQuery.periodType === 'DAILY'"
|
||
v-model="rankingQuery.statDate"
|
||
type="date"
|
||
value-format="yyyy-MM-dd"
|
||
placeholder="选择活动日"
|
||
/>
|
||
<el-input-number v-model="rankingQuery.limit" :min="1" :max="500" :precision="0" controls-position="right" />
|
||
<el-button type="primary" icon="el-icon-search" :loading="rankingLoading" @click="loadRanking">查询</el-button>
|
||
<el-tag :type="rankingResult.settled ? 'success' : 'info'">
|
||
{{ rankingResult.settled ? '已生成结算快照' : '未结算' }}
|
||
</el-tag>
|
||
</div>
|
||
<el-table v-loading="rankingLoading" :data="rankingResult.entries || []" border max-height="560">
|
||
<el-table-column label="名次" width="80" align="center" prop="rank" />
|
||
<el-table-column label="用户 ID" min-width="165" prop="userId" />
|
||
<el-table-column label="账号" min-width="125" prop="account" />
|
||
<el-table-column label="昵称" min-width="160" show-overflow-tooltip prop="nickname" />
|
||
<el-table-column label="积分" min-width="140" align="right">
|
||
<template slot-scope="scope">{{ formatNumber(scope.row.score) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:title="`${selectedActivity.activityName || ''} - 结算记录`"
|
||
:visible.sync="settlementsVisible"
|
||
width="1240px"
|
||
top="4vh"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="dialog-actions settlement-actions">
|
||
<el-select v-model="settlementQuery.periodType" clearable placeholder="全部周期" @change="settlementPeriodChanged">
|
||
<el-option label="活动总榜" value="OVERALL" />
|
||
<el-option label="日榜" value="DAILY" />
|
||
</el-select>
|
||
<el-date-picker
|
||
v-if="settlementQuery.periodType === 'DAILY'"
|
||
v-model="settlementQuery.statDate"
|
||
type="date"
|
||
value-format="yyyy-MM-dd"
|
||
placeholder="选择活动日"
|
||
/>
|
||
<el-select v-model="settlementQuery.deliveryStatus" clearable placeholder="全部发放状态">
|
||
<el-option label="待发放" value="PENDING" />
|
||
<el-option label="发放中" value="PROCESSING" />
|
||
<el-option label="成功" value="SUCCESS" />
|
||
<el-option label="结果未知(需核账)" value="UNKNOWN" />
|
||
</el-select>
|
||
<el-input-number v-model="settlementQuery.limit" :min="1" :max="500" :precision="0" controls-position="right" />
|
||
<el-button icon="el-icon-search" :loading="settlementsLoading" @click="loadSettlements">查询</el-button>
|
||
<el-button
|
||
v-if="canSettle"
|
||
type="danger"
|
||
icon="el-icon-s-check"
|
||
:loading="settleLoading"
|
||
:disabled="!settlementQuery.periodType"
|
||
@click="manualSettle"
|
||
>
|
||
手动结算当前周期
|
||
</el-button>
|
||
</div>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="冻结榜单前先确认 ASLAN_GIFT_CHALLENGE_GIFT 消费组无不可接受积压;UNKNOWN 表示奖励可能已到账,只有核账确认未到账才会开放该单项补发。"
|
||
/>
|
||
<el-table v-loading="settlementsLoading" :data="settlementRecords" border max-height="580" class="settlement-table">
|
||
<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="150" prop="id" />
|
||
<el-table-column label="类型" min-width="135">
|
||
<template slot-scope="itemScope">
|
||
{{ itemScope.row.rewardType }} / {{ itemScope.row.detailType || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="内容" min-width="150" show-overflow-tooltip prop="content" />
|
||
<el-table-column label="数量" width="75" align="right" prop="quantity" />
|
||
<el-table-column label="状态" width="105" 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="状态说明" min-width="190" show-overflow-tooltip prop="failureReason" />
|
||
<el-table-column label="UNKNOWN 核账" min-width="190" align="center" fixed="right">
|
||
<template slot-scope="itemScope">
|
||
<template v-if="itemScope.row.deliveryStatus === 'UNKNOWN' && canReconcile">
|
||
<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="90" align="center">
|
||
<template slot-scope="scope">{{ periodText(scope.row.periodType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="统计日" width="115" align="center">
|
||
<template slot-scope="scope">{{ scope.row.statDate || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="名次" width="70" align="center" prop="rank" />
|
||
<el-table-column label="用户 ID" min-width="155" prop="userId" />
|
||
<el-table-column label="积分" min-width="110" align="right">
|
||
<template slot-scope="scope">{{ formatNumber(scope.row.score) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="资源组" min-width="145" prop="resourceGroupId" />
|
||
<el-table-column label="状态" width="105" 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="180" show-overflow-tooltip prop="failureReason" />
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<associated-resources
|
||
v-if="resourceSelectorVisible"
|
||
:sys-origin="form.sysOrigin"
|
||
@select="applyResourceGroup"
|
||
@close="closeResourceSelector"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex'
|
||
import AssociatedResources from '@/components/data/AssociatedResources'
|
||
import PropsRow from '@/components/data/PropsRow'
|
||
import {
|
||
getAslanGiftChallengeDetail,
|
||
getAslanGiftChallengeRanking,
|
||
listAslanGiftChallengeActivities,
|
||
listAslanGiftChallengeRankRewards,
|
||
listAslanGiftChallengeSettlementRecords,
|
||
listAslanGiftChallengeTasks,
|
||
resolveAslanGiftChallengeUnknownDeliveryItem,
|
||
saveAslanGiftChallengeActivity,
|
||
setAslanGiftChallengeEnabled,
|
||
settleAslanGiftChallengeActivity
|
||
} from '@/api/aslan-gift-challenge'
|
||
|
||
const PERIOD_DAILY = 'DAILY'
|
||
const PERIOD_OVERALL = 'OVERALL'
|
||
const TASK_ENTER_PAGE = 'ENTER_PAGE'
|
||
const TASK_SEND_GIFT_GOLD = 'SEND_GIFT_GOLD'
|
||
|
||
export default {
|
||
name: 'ActivityGiftChallenge',
|
||
components: { AssociatedResources, PropsRow },
|
||
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' }],
|
||
displayTopN: [{ required: true, message: '请输入榜单展示数量', trigger: 'blur' }],
|
||
dailySettlementDelayMinutes: [{ required: true, message: '请输入日榜晚到窗口', trigger: 'blur' }],
|
||
overallSettlementDelayMinutes: [{ required: true, message: '请输入总榜晚到窗口', trigger: 'blur' }]
|
||
},
|
||
timeZoneOptions: ['Asia/Riyadh', 'Asia/Shanghai', 'Europe/Istanbul', 'UTC'],
|
||
resourceSelectorVisible: false,
|
||
resourceSelection: null,
|
||
selectedActivity: {},
|
||
rankingVisible: false,
|
||
rankingLoading: false,
|
||
rankingQuery: {
|
||
periodType: PERIOD_OVERALL,
|
||
statDate: '',
|
||
limit: 100
|
||
},
|
||
rankingResult: {
|
||
settled: false,
|
||
entries: []
|
||
},
|
||
settlementsVisible: false,
|
||
settlementsLoading: false,
|
||
settleLoading: false,
|
||
settlementQuery: {
|
||
periodType: PERIOD_OVERALL,
|
||
statDate: '',
|
||
deliveryStatus: '',
|
||
limit: 100
|
||
},
|
||
settlementRecords: [],
|
||
resolvingDeliveryItemId: ''
|
||
}
|
||
},
|
||
computed: {
|
||
...mapGetters(['buttonPermissions']),
|
||
canEdit() {
|
||
return this.hasButtonPermission('activity:gift-challenge:edit')
|
||
},
|
||
canEnable() {
|
||
return this.hasButtonPermission('activity:gift-challenge:enable')
|
||
},
|
||
canSettle() {
|
||
return this.hasButtonPermission('activity:gift-challenge:settle')
|
||
},
|
||
canReconcile() {
|
||
return this.hasButtonPermission('activity:gift-challenge:reconcile')
|
||
},
|
||
startedLocked() {
|
||
if (!this.form.id) {
|
||
return false
|
||
}
|
||
// 后端返回的活动状态和 serverTime 是锁定判断基准,不能依赖运营电脑的本地时间绕开规则冻结。
|
||
return ['ACTIVE', 'ENDED'].indexOf(this.backendActivityStatus) >= 0 ||
|
||
(this.serverTime > 0 && this.originalStartTime > 0 && this.serverTime >= this.originalStartTime)
|
||
},
|
||
editorReadOnly() {
|
||
return !this.canEdit || this.startedLocked
|
||
}
|
||
},
|
||
created() {
|
||
this.loadActivities()
|
||
},
|
||
methods: {
|
||
emptyForm() {
|
||
return {
|
||
id: '',
|
||
activityCode: '',
|
||
activityName: '',
|
||
activityDesc: '',
|
||
sysOrigin: 'ATYOU',
|
||
timeZone: 'Asia/Riyadh',
|
||
timeRange: [],
|
||
dailySettlementDelayMinutes: 30,
|
||
overallSettlementDelayMinutes: 30,
|
||
displayTopN: 100,
|
||
enabled: false,
|
||
version: null,
|
||
tasks: this.emptyTasks(),
|
||
dailyRankRewards: [this.emptyRankReward(PERIOD_DAILY, 1)],
|
||
overallRankRewards: [this.emptyRankReward(PERIOD_OVERALL, 1)]
|
||
}
|
||
},
|
||
emptyTasks() {
|
||
// 产品固定一个进入页任务和两档送礼任务;编码、类型和排序不是运营配置项。
|
||
return [
|
||
this.emptyTask('daily_enter', TASK_ENTER_PAGE, '每日进入活动页面', 1, 1),
|
||
this.emptyTask('daily_gift_1', TASK_SEND_GIFT_GOLD, '每日累计送礼任务一', 1, 2),
|
||
this.emptyTask('daily_gift_2', TASK_SEND_GIFT_GOLD, '每日累计送礼任务二', 1, 3)
|
||
]
|
||
},
|
||
emptyTask(taskCode, taskType, taskTitle, targetValue, sortOrder) {
|
||
return {
|
||
id: '',
|
||
activityId: '',
|
||
taskCode,
|
||
taskType,
|
||
taskTitle,
|
||
taskDesc: '',
|
||
targetValue,
|
||
resourceGroupId: '',
|
||
enabled: true,
|
||
sortOrder,
|
||
rewards: []
|
||
}
|
||
},
|
||
emptyRankReward(periodType, startRank) {
|
||
return {
|
||
id: '',
|
||
activityId: '',
|
||
periodType,
|
||
startRank,
|
||
endRank: startRank,
|
||
resourceGroupId: '',
|
||
rewardName: '',
|
||
rewards: []
|
||
}
|
||
},
|
||
hasButtonPermission(alias) {
|
||
return Array.isArray(this.buttonPermissions) && this.buttonPermissions.indexOf(alias) >= 0
|
||
},
|
||
loadActivities() {
|
||
this.listLoading = true
|
||
listAslanGiftChallengeActivities().then(res => {
|
||
this.activities = Array.isArray(res.body) ? res.body : []
|
||
}).finally(() => {
|
||
this.listLoading = false
|
||
})
|
||
},
|
||
openCreate() {
|
||
this.resetEditor()
|
||
this.editorVisible = true
|
||
},
|
||
openEdit(row) {
|
||
this.resetEditor()
|
||
this.editorVisible = true
|
||
this.editorLoading = true
|
||
// 详情、任务和排行奖励分别走已发布的管理端点;即使以后详情做裁剪,配置页仍不会丢失子配置。
|
||
Promise.all([
|
||
getAslanGiftChallengeDetail(row.id),
|
||
listAslanGiftChallengeTasks(row.id),
|
||
listAslanGiftChallengeRankRewards(row.id)
|
||
]).then(results => {
|
||
const detail = results[0].body || {}
|
||
const activity = detail.activity || {}
|
||
const tasks = Array.isArray(results[1].body) ? results[1].body : (detail.tasks || [])
|
||
const rewards = Array.isArray(results[2].body) ? results[2].body : (detail.rankRewards || [])
|
||
this.form = Object.assign(this.emptyForm(), activity, {
|
||
sysOrigin: 'ATYOU',
|
||
timeZone: activity.timeZone || 'Asia/Riyadh',
|
||
dailySettlementDelayMinutes: this.numberOrDefault(activity.dailySettlementDelayMinutes, 30),
|
||
overallSettlementDelayMinutes: this.numberOrDefault(activity.overallSettlementDelayMinutes, 30),
|
||
displayTopN: this.numberOrDefault(activity.displayTopN, 100),
|
||
timeRange: [activity.startTime, activity.endTime],
|
||
tasks: this.normalizeTasks(tasks),
|
||
dailyRankRewards: this.normalizeRankRewards(rewards, PERIOD_DAILY),
|
||
overallRankRewards: this.normalizeRankRewards(rewards, PERIOD_OVERALL)
|
||
})
|
||
this.serverTime = Number(detail.serverTime || 0)
|
||
this.originalStartTime = Number(activity.startTime || 0)
|
||
this.backendActivityStatus = detail.activityStatus || ''
|
||
}).finally(() => {
|
||
this.editorLoading = false
|
||
})
|
||
},
|
||
normalizeTasks(tasks) {
|
||
const defaults = this.emptyTasks()
|
||
if (!Array.isArray(tasks) || tasks.length === 0) {
|
||
return defaults
|
||
}
|
||
return tasks.slice().sort((left, right) => Number(left.sortOrder) - Number(right.sortOrder))
|
||
.slice(0, 3).map((item, index) => Object.assign({}, defaults[index], item, {
|
||
taskCode: defaults[index].taskCode,
|
||
taskType: defaults[index].taskType,
|
||
sortOrder: defaults[index].sortOrder,
|
||
targetValue: index === 0 ? 1 : Number(item.targetValue || defaults[index].targetValue),
|
||
enabled: true,
|
||
rewards: item.rewardItems || item.rewards || []
|
||
})).concat(defaults.slice(Math.min(tasks.length, 3)))
|
||
},
|
||
normalizeRankRewards(rewards, periodType) {
|
||
const normalized = (Array.isArray(rewards) ? rewards : []).filter(item => item.periodType === periodType)
|
||
.sort((left, right) => Number(left.startRank) - Number(right.startRank))
|
||
.map(item => Object.assign({}, item, {
|
||
rewards: item.rewardItems || item.rewards || []
|
||
}))
|
||
return normalized.length > 0 ? normalized : [this.emptyRankReward(periodType, 1)]
|
||
},
|
||
numberOrDefault(value, defaultValue) {
|
||
return value === null || value === undefined ? defaultValue : Number(value)
|
||
},
|
||
resetEditor() {
|
||
this.form = this.emptyForm()
|
||
this.serverTime = 0
|
||
this.originalStartTime = 0
|
||
this.backendActivityStatus = ''
|
||
this.resourceSelectorVisible = false
|
||
this.resourceSelection = null
|
||
this.$nextTick(() => {
|
||
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
|
||
// 新建或开始前编辑采用聚合保存,基础配置、三个任务和两类排行奖励由后端事务一次提交,避免半套配置被启用。
|
||
saveAslanGiftChallengeActivity(payload).then(() => {
|
||
this.$message.success('礼物挑战配置已保存')
|
||
this.editorVisible = false
|
||
this.loadActivities()
|
||
}).finally(() => {
|
||
this.saveLoading = false
|
||
})
|
||
})
|
||
},
|
||
validateConfiguration() {
|
||
const timeRange = this.form.timeRange || []
|
||
if (timeRange.length !== 2 || Number(timeRange[0]) >= Number(timeRange[1])) {
|
||
this.$message.error('活动结束时间必须晚于开始时间')
|
||
return false
|
||
}
|
||
if (this.form.sysOrigin !== 'ATYOU') {
|
||
this.$message.error('Aslan 礼物挑战只允许配置 ATYOU 平台')
|
||
return false
|
||
}
|
||
if (!this.validInteger(this.form.displayTopN, 1, 500)) {
|
||
this.$message.error('榜单 TopN 必须是 1 到 500 的整数')
|
||
return false
|
||
}
|
||
if (!this.validInteger(this.form.dailySettlementDelayMinutes, 0, 1440) ||
|
||
!this.validInteger(this.form.overallSettlementDelayMinutes, 0, 1440)) {
|
||
this.$message.error('日榜和总榜晚到窗口必须是 0 到 1440 的整数分钟')
|
||
return false
|
||
}
|
||
// 统计开始后后端要求 tasks/rankRewards 完全不出现在请求中;这里只校验仍允许修改的基础字段。
|
||
if (this.startedLocked) {
|
||
return true
|
||
}
|
||
if (!this.validateTasks()) {
|
||
return false
|
||
}
|
||
return this.validateRankRewards(PERIOD_DAILY, this.form.dailyRankRewards) &&
|
||
this.validateRankRewards(PERIOD_OVERALL, this.form.overallRankRewards)
|
||
},
|
||
validateTasks() {
|
||
const tasks = this.form.tasks || []
|
||
if (tasks.length !== 3) {
|
||
this.$message.error('任务必须且只能保留 3 个槽位')
|
||
return false
|
||
}
|
||
const codes = new Set()
|
||
const sortOrders = new Set()
|
||
let enterCount = 0
|
||
let giftCount = 0
|
||
const fixedTasks = this.emptyTasks()
|
||
for (let index = 0; index < tasks.length; index++) {
|
||
const task = tasks[index]
|
||
const code = String(task.taskCode || '').trim()
|
||
const title = String(task.taskTitle || '').trim()
|
||
if (!/^[A-Za-z0-9_-]{1,64}$/.test(code) || codes.has(code) ||
|
||
code !== fixedTasks[index].taskCode || task.taskType !== fixedTasks[index].taskType) {
|
||
this.$message.error(`任务槽位 ${index + 1} 的固定编码或类型无效`)
|
||
return false
|
||
}
|
||
codes.add(code)
|
||
if (!title || title.length > 128 || String(task.taskDesc || '').length > 500) {
|
||
this.$message.error(`请检查任务槽位 ${index + 1} 的标题和描述长度`)
|
||
return false
|
||
}
|
||
if (!this.validInteger(task.targetValue, 1, 999999999999)) {
|
||
this.$message.error(`任务槽位 ${index + 1} 的目标值必须是正整数`)
|
||
return false
|
||
}
|
||
if (Number(task.sortOrder) !== fixedTasks[index].sortOrder ||
|
||
sortOrders.has(Number(task.sortOrder))) {
|
||
this.$message.error(`任务槽位 ${index + 1} 的固定排序无效`)
|
||
return false
|
||
}
|
||
sortOrders.add(Number(task.sortOrder))
|
||
if (!task.enabled || !task.resourceGroupId) {
|
||
this.$message.error(`任务槽位 ${index + 1} 必须启用并选择奖励资源组`)
|
||
return false
|
||
}
|
||
if (task.taskType === TASK_ENTER_PAGE) {
|
||
enterCount++
|
||
if (Number(task.targetValue) !== 1) {
|
||
this.$message.error('进入页面任务的目标值必须为 1')
|
||
return false
|
||
}
|
||
} else {
|
||
giftCount++
|
||
}
|
||
}
|
||
if (enterCount !== 1 || giftCount !== 2) {
|
||
this.$message.error('任务必须固定为 1 个每日进入页面和 2 个累计送礼金币档位')
|
||
return false
|
||
}
|
||
const giftTasks = tasks
|
||
.filter(item => item.taskType === TASK_SEND_GIFT_GOLD)
|
||
.sort((left, right) => Number(left.sortOrder) - Number(right.sortOrder))
|
||
// 两档按固定槽位递增,提前在 console 阻止“同一次送礼同时完成两个重复档位”的配置。
|
||
if (Number(giftTasks[0].targetValue) >= Number(giftTasks[1].targetValue)) {
|
||
this.$message.error('第二档累计送礼金币门槛必须高于第一档')
|
||
return false
|
||
}
|
||
return true
|
||
},
|
||
validateRankRewards(periodType, rewards) {
|
||
const periodName = periodType === PERIOD_DAILY ? '日榜' : '总榜'
|
||
if (!Array.isArray(rewards) || rewards.length === 0) {
|
||
this.$message.error(`${periodName}至少配置一个奖励区间`)
|
||
return false
|
||
}
|
||
const ordered = rewards.slice().sort((left, right) => Number(left.startRank) - Number(right.startRank))
|
||
let previousEnd = 0
|
||
for (let index = 0; index < ordered.length; index++) {
|
||
const reward = ordered[index]
|
||
const start = Number(reward.startRank)
|
||
const end = Number(reward.endRank)
|
||
if (!this.validInteger(start, 1, Number(this.form.displayTopN)) ||
|
||
!this.validInteger(end, 1, Number(this.form.displayTopN)) || start > end) {
|
||
this.$message.error(`${periodName}第 ${index + 1} 档名次范围无效或超过 TopN`)
|
||
return false
|
||
}
|
||
if (start <= previousEnd) {
|
||
this.$message.error(`${periodName}奖励区间存在重叠`)
|
||
return false
|
||
}
|
||
if (!reward.resourceGroupId || String(reward.rewardName || '').length > 128) {
|
||
this.$message.error(`${periodName}第 ${index + 1} 档必须选择奖励资源组,并检查奖励名称长度`)
|
||
return false
|
||
}
|
||
previousEnd = end
|
||
}
|
||
return true
|
||
},
|
||
validInteger(value, min, max) {
|
||
const number = Number(value)
|
||
return Number.isSafeInteger(number) && number >= min && number <= max
|
||
},
|
||
buildSavePayload() {
|
||
const payload = {
|
||
id: this.form.id || null,
|
||
activityCode: this.form.activityCode.trim(),
|
||
activityName: this.form.activityName.trim(),
|
||
activityDesc: this.form.activityDesc,
|
||
sysOrigin: 'ATYOU',
|
||
timeZone: this.form.timeZone,
|
||
startTime: Number(this.form.timeRange[0]),
|
||
endTime: Number(this.form.timeRange[1]),
|
||
dailySettlementDelayMinutes: Number(this.form.dailySettlementDelayMinutes),
|
||
overallSettlementDelayMinutes: Number(this.form.overallSettlementDelayMinutes),
|
||
displayTopN: Number(this.form.displayTopN),
|
||
enabled: Boolean(this.form.enabled),
|
||
version: this.form.id ? Number(this.form.version) : null
|
||
}
|
||
if (!this.startedLocked) {
|
||
payload.tasks = this.form.tasks.map(item => ({
|
||
id: item.id || null,
|
||
activityId: this.form.id || null,
|
||
taskCode: item.taskCode.trim(),
|
||
taskType: item.taskType,
|
||
taskTitle: item.taskTitle.trim(),
|
||
taskDesc: item.taskDesc,
|
||
targetValue: Number(item.targetValue),
|
||
resourceGroupId: String(item.resourceGroupId),
|
||
enabled: true,
|
||
sortOrder: Number(item.sortOrder)
|
||
}))
|
||
payload.rankRewards = this.form.dailyRankRewards.concat(this.form.overallRankRewards).map(item => ({
|
||
id: item.id || null,
|
||
activityId: this.form.id || null,
|
||
periodType: item.periodType,
|
||
startRank: Number(item.startRank),
|
||
endRank: Number(item.endRank),
|
||
resourceGroupId: String(item.resourceGroupId),
|
||
rewardName: item.rewardName
|
||
}))
|
||
}
|
||
return payload
|
||
},
|
||
addRankReward(periodType) {
|
||
const list = this.rankRewardList(periodType)
|
||
const lastEnd = list.reduce((max, item) => Math.max(max, Number(item.endRank || 0)), 0)
|
||
const nextRank = lastEnd + 1
|
||
if (nextRank > Number(this.form.displayTopN)) {
|
||
this.$message.warning('当前区间已到达 TopN,请先调整已有区间或提高 TopN')
|
||
return
|
||
}
|
||
list.push(this.emptyRankReward(periodType, nextRank))
|
||
},
|
||
removeRankReward(periodType, index) {
|
||
const list = this.rankRewardList(periodType)
|
||
if (list.length <= 1) {
|
||
this.$message.warning('每个排行周期至少保留一个奖励区间')
|
||
return
|
||
}
|
||
list.splice(index, 1)
|
||
},
|
||
rankRewardList(periodType) {
|
||
return periodType === PERIOD_DAILY ? this.form.dailyRankRewards : this.form.overallRankRewards
|
||
},
|
||
selectResourceGroup(scope, index) {
|
||
if (this.editorReadOnly) {
|
||
return
|
||
}
|
||
this.resourceSelection = { scope, index }
|
||
this.resourceSelectorVisible = true
|
||
},
|
||
applyResourceGroup(source) {
|
||
if (!this.resourceSelection || !source) {
|
||
return
|
||
}
|
||
let row
|
||
if (this.resourceSelection.scope === 'task') {
|
||
row = this.form.tasks[this.resourceSelection.index]
|
||
} else if (this.resourceSelection.scope === 'daily') {
|
||
row = this.form.dailyRankRewards[this.resourceSelection.index]
|
||
} else {
|
||
row = this.form.overallRankRewards[this.resourceSelection.index]
|
||
}
|
||
if (!row) {
|
||
this.closeResourceSelector()
|
||
return
|
||
}
|
||
// 奖励组 Long ID 始终按字符串保存,防止浏览器 Number 截断 Snowflake ID 后配置到错误资源组。
|
||
row.resourceGroupId = String(source.id)
|
||
row.rewards = source.rewardConfigList || []
|
||
if (this.resourceSelection.scope !== 'task' && !row.rewardName) {
|
||
row.rewardName = source.name || ''
|
||
}
|
||
this.closeResourceSelector()
|
||
},
|
||
closeResourceSelector() {
|
||
this.resourceSelectorVisible = false
|
||
this.resourceSelection = null
|
||
},
|
||
changeEnabled(row) {
|
||
const enabled = Boolean(row.enabled)
|
||
const previous = !enabled
|
||
const action = enabled ? '启用' : '停用'
|
||
this.$confirm(`确认${action}活动「${row.activityName}」?活动开始后不可切换状态。`, '状态确认', {
|
||
type: 'warning'
|
||
}).then(() => setAslanGiftChallengeEnabled(row.id, enabled)).then(() => {
|
||
this.$message.success(`${action}成功`)
|
||
this.loadActivities()
|
||
}).catch(() => {
|
||
// 用户取消和接口失败都必须回滚 switch;服务端仍是唯一状态真相。
|
||
row.enabled = previous
|
||
})
|
||
},
|
||
openRanking(row) {
|
||
this.selectedActivity = row
|
||
this.rankingQuery = {
|
||
periodType: PERIOD_OVERALL,
|
||
statDate: '',
|
||
limit: Number(row.displayTopN || 100)
|
||
}
|
||
this.rankingResult = { settled: false, entries: [] }
|
||
this.rankingVisible = true
|
||
this.loadRanking()
|
||
},
|
||
rankingPeriodChanged() {
|
||
// 活动日由配置时区决定,不能用运营电脑本地日期猜测;切到日榜后要求人工明确选择。
|
||
this.rankingQuery.statDate = ''
|
||
this.loadRanking()
|
||
},
|
||
loadRanking() {
|
||
if (!this.selectedActivity.id) {
|
||
return
|
||
}
|
||
if (this.rankingQuery.periodType === PERIOD_DAILY && !this.rankingQuery.statDate) {
|
||
this.$message.warning('查询日榜时请选择统计日')
|
||
return
|
||
}
|
||
this.rankingLoading = true
|
||
getAslanGiftChallengeRanking(this.selectedActivity.id, {
|
||
periodType: this.rankingQuery.periodType,
|
||
statDate: this.rankingQuery.periodType === PERIOD_DAILY ? this.rankingQuery.statDate : undefined,
|
||
limit: Number(this.rankingQuery.limit)
|
||
}).then(res => {
|
||
this.rankingResult = res.body || { settled: false, entries: [] }
|
||
}).finally(() => {
|
||
this.rankingLoading = false
|
||
})
|
||
},
|
||
openSettlements(row) {
|
||
this.selectedActivity = row
|
||
this.settlementQuery = {
|
||
periodType: PERIOD_OVERALL,
|
||
statDate: '',
|
||
deliveryStatus: '',
|
||
limit: 100
|
||
}
|
||
this.settlementRecords = []
|
||
this.settlementsVisible = true
|
||
this.loadSettlements()
|
||
},
|
||
settlementPeriodChanged() {
|
||
// 日榜结算会真实发奖,切换周期时清空旧日期,防止沿用另一时区下的“今天”。
|
||
this.settlementQuery.statDate = ''
|
||
this.loadSettlements()
|
||
},
|
||
loadSettlements() {
|
||
if (!this.selectedActivity.id) {
|
||
return
|
||
}
|
||
this.settlementsLoading = true
|
||
listAslanGiftChallengeSettlementRecords(this.selectedActivity.id, {
|
||
periodType: this.settlementQuery.periodType || undefined,
|
||
statDate: this.settlementQuery.periodType === PERIOD_DAILY ? this.settlementQuery.statDate || undefined : undefined,
|
||
deliveryStatus: this.settlementQuery.deliveryStatus || undefined,
|
||
limit: Number(this.settlementQuery.limit)
|
||
}).then(res => {
|
||
this.settlementRecords = Array.isArray(res.body) ? res.body : []
|
||
}).finally(() => {
|
||
this.settlementsLoading = false
|
||
})
|
||
},
|
||
manualSettle() {
|
||
const period = this.settlementQuery.periodType
|
||
const statDate = period === PERIOD_DAILY ? this.settlementQuery.statDate : ''
|
||
if (!period || (period === PERIOD_DAILY && !statDate)) {
|
||
this.$message.warning('手动结算日榜时必须选择统计日')
|
||
return
|
||
}
|
||
const periodName = period === PERIOD_DAILY ? `日榜 ${statDate}` : '活动总榜'
|
||
this.$confirm(`确认手动结算「${periodName}」?未到晚到窗口或已结算时服务端会拒绝。`, '结算确认', {
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.settleLoading = true
|
||
return settleAslanGiftChallengeActivity(this.selectedActivity.id, period, statDate)
|
||
}).then(() => {
|
||
this.$message.success('结算请求执行完成')
|
||
this.loadSettlements()
|
||
this.loadActivities()
|
||
}).finally(() => {
|
||
this.settleLoading = false
|
||
})
|
||
},
|
||
resolveUnknownDeliveryItem(item, delivered) {
|
||
const action = delivered ? '确认该奖励项实际已到账' : '确认未到账并开放补发'
|
||
this.$confirm(`${action}?此操作依赖人工核账结果,不可用作普通重试。`, 'UNKNOWN 核账确认', {
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.resolvingDeliveryItemId = item.id
|
||
return resolveAslanGiftChallengeUnknownDeliveryItem(item.id, delivered)
|
||
}).then(() => {
|
||
this.$message.success('核账结果已提交')
|
||
this.loadSettlements()
|
||
}).finally(() => {
|
||
this.resolvingDeliveryItemId = ''
|
||
})
|
||
},
|
||
activityStatus(row) {
|
||
const now = Date.now()
|
||
if (!row.enabled) {
|
||
return 'DISABLED'
|
||
}
|
||
if (now < Number(row.startTime || 0)) {
|
||
return 'PLANNED'
|
||
}
|
||
if (now <= Number(row.endTime || 0)) {
|
||
return 'ACTIVE'
|
||
}
|
||
return 'ENDED'
|
||
},
|
||
activityStatusText(status) {
|
||
return {
|
||
DISABLED: '未启用',
|
||
PLANNED: '待开始',
|
||
ACTIVE: '进行中',
|
||
ENDED: '已结束'
|
||
}[status] || status
|
||
},
|
||
activityStatusTag(status) {
|
||
return {
|
||
DISABLED: 'info',
|
||
PLANNED: 'warning',
|
||
ACTIVE: 'success',
|
||
ENDED: ''
|
||
}[status] || 'info'
|
||
},
|
||
settlementStatusText(status) {
|
||
return {
|
||
NOT_STARTED: '未开始',
|
||
PROCESSING: '处理中',
|
||
COMPLETED: '已完成',
|
||
RECONCILIATION_REQUIRED: '待核账'
|
||
}[status] || status || '未开始'
|
||
},
|
||
deliveryStatusTag(status) {
|
||
return {
|
||
SUCCESS: 'success',
|
||
UNKNOWN: 'warning',
|
||
PROCESSING: '',
|
||
PENDING: 'info'
|
||
}[status] || 'info'
|
||
},
|
||
periodText(period) {
|
||
return period === PERIOD_DAILY ? '日榜' : (period === PERIOD_OVERALL ? '总榜' : period)
|
||
},
|
||
hasStarted(row) {
|
||
return Number(row.startTime || 0) <= Date.now()
|
||
},
|
||
formatTime(value) {
|
||
if (!value) {
|
||
return '-'
|
||
}
|
||
const date = new Date(Number(value))
|
||
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString()
|
||
},
|
||
formatNumber(value) {
|
||
if (value === null || value === undefined || value === '') {
|
||
return '0'
|
||
}
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? number.toLocaleString() : String(value)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.aslan-gift-challenge {
|
||
.page-tip {
|
||
margin: 8px 0 18px;
|
||
}
|
||
|
||
.primary-text {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.secondary-text,
|
||
.form-help {
|
||
margin-top: 4px;
|
||
color: #909399;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
.editor-body {
|
||
max-height: 76vh;
|
||
padding-right: 8px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 24px 0 14px;
|
||
padding-left: 10px;
|
||
border-left: 4px solid #409eff;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.rank-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.config-table,
|
||
.settlement-table {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.task-desc-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.task-desc-item {
|
||
padding: 10px;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.task-desc-label {
|
||
margin-bottom: 8px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dialog-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.delivery-item-table {
|
||
width: calc(100% - 30px);
|
||
margin: 4px 15px 12px;
|
||
}
|
||
|
||
.danger-link {
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.aslan-gift-challenge .task-desc-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|