[update]新增submittedReward页面

This commit is contained in:
2026-06-10 16:43:37 +08:00
parent 45d67f3778
commit 1e1fddf04c
5 changed files with 573 additions and 135 deletions

View File

@@ -0,0 +1,31 @@
import createAxios from '/@/utils/axios'
export interface SubmittedRewardParams {
start: string
end: string
status: '' | '0' | '1' | '2'
username: string
game_id: 268 | 269 | 270
page: number
}
export function getSubmittedRewards(params: SubmittedRewardParams) {
return createAxios({
url: '/admin/user.User/submittedReward',
method: 'get',
params,
})
}
export function editReward(data: { id: number | string; status: 1 | 2 }) {
return createAxios(
{
url: '/admin/user.User/editReward',
method: 'post',
data,
},
{
showSuccessMessage: true,
}
)
}

View File

@@ -10,6 +10,7 @@ export default {
[adminBaseRoutePath + '/moduleStore']: ['./backend/${lang}/module.ts'],
[adminBaseRoutePath + '/user/rule']: ['./backend/${lang}/auth/rule.ts'],
[adminBaseRoutePath + '/user/moneyLog/annualReport']: ['./backend/${lang}/user/moneyLog.ts'],
[adminBaseRoutePath + '/user/submittedReward']: ['./backend/${lang}/user/submittedReward.ts'],
[adminBaseRoutePath + '/user/scoreLog']: ['./backend/${lang}/user/moneyLog.ts'],
[adminBaseRoutePath + '/crud/crud']: ['./backend/${lang}/crud/log.ts', './backend/${lang}/crud/state.ts'],
}

View File

@@ -0,0 +1,27 @@
export default {
'Promotion type': 'Promotion type',
'Submitted Rewards': 'Submitted Rewards',
Running: 'Running',
'Start Date': 'Start Date',
'End Date': 'End Date',
Status: 'Status',
Username: 'Username',
'All status': '- All status -',
'Approve Reward': 'Approve Reward',
'Reject Reward': 'Reject Reward',
'In process': 'In process',
'Search by username': 'Search by username',
Clear: 'Clear',
'Date of data': 'Date of data',
'Submitted Time': 'Submitted Time',
'Reward Claim': 'Reward Claim',
Action: 'Action',
Agree: 'Agree',
Reject: 'Reject',
'Confirm reward action': 'Are you sure you want to {action} the reward request from {username}?',
'No records': 'No records',
'Total records': 'Total {total} records',
to: 'to',
Day: 'Day',
Days: 'Days',
}

View File

@@ -0,0 +1,27 @@
export default {
'Promotion type': '活动类型',
'Submitted Rewards': '已提交奖励',
Running: '运行中',
'Start Date': '开始日期',
'End Date': '结束日期',
Status: '状态',
Username: '用户名',
'All status': '- 全部状态 -',
'Approve Reward': '批准奖励',
'Reject Reward': '拒绝奖励',
'In process': '处理中',
'Search by username': '按用户名搜索',
Clear: '清除',
'Date of data': '数据日期',
'Submitted Time': '提交时间',
'Reward Claim': '领取奖励',
Action: '操作',
Agree: '同意',
Reject: '拒绝',
'Confirm reward action': '确定要{action}用户 {username} 的奖励申请吗?',
'No records': '暂无记录',
'Total records': '共 {total} 条记录',
to: '至',
Day: '天',
Days: '天',
}

View File

@@ -1,156 +1,508 @@
<!--suppress TypeScriptValidateTypes -->
<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<div class="default-main submitted-rewards">
<nav class="reward-tabs" :aria-label="t('user.submittedReward.Promotion type')">
<button
v-for="tab in promotionTabs"
:key="tab.value"
type="button"
:class="{ active: filters.gameId === tab.value }"
@click="changePromotion(tab.value)"
>
{{ tab.label }}
</button>
</nav>
<!-- 表格顶部菜单 -->
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('user.user.User name') + '/' + t('user.user.nickname') })"
<section class="reward-panel">
<header class="panel-heading">
<span>{{ t('user.submittedReward.Submitted Rewards') }} ({{ activePromotionLabel }})</span>
<strong>{{ t('user.submittedReward.Running') }}</strong>
</header>
<div class="filter-panel">
<div class="filter-item">
<label>{{ t('user.submittedReward.Start Date') }}:</label>
<el-date-picker v-model="filters.start" type="date" value-format="YYYY-MM-DD" :clearable="false" />
</div>
<div class="filter-item">
<label>{{ t('user.submittedReward.End Date') }}:</label>
<el-date-picker v-model="filters.end" type="date" value-format="YYYY-MM-DD" :clearable="false" />
</div>
<div class="filter-item">
<label>{{ t('user.submittedReward.Status') }}:</label>
<el-select v-model="filters.status">
<el-option :label="t('user.submittedReward.All status')" value="all" />
<el-option :label="t('user.submittedReward.Approve Reward')" value="1" />
<el-option :label="t('user.submittedReward.Reject Reward')" value="2" />
<el-option :label="t('user.submittedReward.In process')" value="0" />
</el-select>
</div>
<div class="filter-item">
<label>{{ t('user.submittedReward.Username') }}:</label>
<el-input
v-model.trim="filters.username"
:placeholder="t('user.submittedReward.Search by username')"
clearable
@keyup.enter="search"
/>
</div>
<div class="filter-actions">
<el-button @click="search">{{ t('Search') }}</el-button>
<el-button @click="clearFilters">{{ t('user.submittedReward.Clear') }}</el-button>
</div>
</div>
</section>
<!-- 表格 -->
<!-- 要使用`el-table`组件原有的属性直接加在Table标签上即可 -->
<Table />
<div class="date-summary">
<strong>{{ t('user.submittedReward.Date of data') }}:</strong>
{{ dateRangeText }}
</div>
<!-- 表单 -->
<PopupForm />
<MembersPopupForm v-model="customFormVisible" :row="customFormRow" :wallet-data="customFormData" />
<div v-loading="loading" class="reward-table-wrap">
<table class="reward-table">
<thead>
<tr>
<th class="submitted-time">{{ t('user.submittedReward.Submitted Time') }}</th>
<th class="username">{{ t('user.submittedReward.Username') }}</th>
<th class="reward-claim">{{ t('user.submittedReward.Reward Claim') }}</th>
<th>{{ t('user.submittedReward.Status') }}</th>
<th class="action">{{ t('user.submittedReward.Action') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ row.submittedTime }}</td>
<td>{{ row.username }}</td>
<td>{{ row.rewardClaim }}</td>
<td>{{ row.status }}</td>
<td>
<div v-if="row.statusCode === 0" class="action-buttons">
<el-button
type="success"
size="small"
:loading="actionLoadingId === row.id"
@click="confirmReward(row, 1)"
>
{{ t('user.submittedReward.Agree') }}
</el-button>
<el-button
type="danger"
size="small"
:loading="actionLoadingId === row.id"
@click="confirmReward(row, 2)"
>
{{ t('user.submittedReward.Reject') }}
</el-button>
</div>
</td>
</tr>
<tr v-if="!loading && rows.length === 0">
<td class="empty-row" colspan="5">{{ t('user.submittedReward.No records') }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-footer">
<span>{{ t('user.submittedReward.Total records', { total: pagination.total }) }}</span>
<el-pagination
v-if="pagination.total > 0"
v-model:current-page="pagination.currentPage"
background
layout="prev, pager, next"
:page-size="pagination.pageSize"
:total="pagination.total"
@current-change="changePage"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { provide, ref } from 'vue'
import { getWallet } from '/@/api/backend/user/wallet'
import baTableClass from '/@/utils/baTable'
import PopupForm from './popupForm.vue'
import MembersPopupForm from './membersPopupForm.vue'
import Table from '/@/components/table/index.vue'
import TableHeader from '/@/components/table/header/index.vue'
import { defaultOptButtons } from '/@/components/table'
import { baTableApi } from '/@/api/common'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { editReward, getSubmittedRewards, type SubmittedRewardParams } from '/@/api/backend/user/submittedReward'
defineOptions({
name: 'user/user',
name: 'user/submittedReward',
})
const { t } = useI18n()
const customFormVisible = ref(false)
const customFormRow = ref<TableRow | null>(null)
const customFormData = ref<anyObj | null>(null)
const walletLoadingId = ref<number | string | null>(null)
let walletRequestSeq = 0
type GameId = 268 | 269 | 270
const optButtons: OptButton[] = [
{
render: 'tipButton',
name: 'customAction',
title: 'Members Promotion',
text: '',
type: 'primary',
icon: 'el-icon-Wallet',
class: 'table-row-custom',
disabledTip: false,
loading: (row: TableRow) => walletLoadingId.value === row.id,
click: async (row: TableRow) => {
const requestSeq = ++walletRequestSeq
walletLoadingId.value = row.id
try {
const res = await getWallet(row.id)
if (requestSeq !== walletRequestSeq) return
customFormRow.value = { ...row }
customFormData.value = res.data?.row ?? res.data ?? {}
customFormVisible.value = true
} finally {
if (requestSeq === walletRequestSeq) {
walletLoadingId.value = null
}
}
},
},
...defaultOptButtons(['edit', 'delete']),
interface RewardRow {
id: number | string
submittedTime: string
username: string
rewardClaim: string
status: string
statusCode: number
}
const { t } = useI18n()
const formatDate = (date: Date) => {
const pad = (value: number) => String(value).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
const today = () => formatDate(new Date())
const promotionTabs: { label: string; value: GameId }[] = [
{ label: 'Plinko Ball', value: 268 },
{ label: 'Smash Eggs', value: 269 },
{ label: 'Spin Wheel', value: 270 },
]
const baTable = new baTableClass(
new baTableApi('/admin/user.User/'),
{
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('Id'), prop: 'id', align: 'center', operator: '=', operatorPlaceholder: t('Id'), width: 70 },
{ label: t('user.user.User name'), prop: 'jk_username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('user.user.nickname'), prop: 'nickname', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('user.user.jk user id'), prop: 'jk_user_id', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('user.user.referrer code'), prop: 'referrer_code', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), width: 120,},
{
label: t('user.user.group'),
prop: 'userGroup.name',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
render: 'tag',
},
{ label: t('user.user.avatar'), prop: 'avatar', align: 'center', render: 'image', operator: false },
{
label: t('user.user.Gender'),
prop: 'gender',
align: 'center',
render: 'tag',
custom: { '0': 'info', '1': '', '2': 'success' },
replaceValue: { '0': t('Unknown'), '1': t('user.user.male'), '2': t('user.user.female') },
},
{ label: t('user.user.mobile'), prop: 'mobile', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{
label: t('user.user.Last login IP'),
prop: 'last_login_ip',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
render: 'tag',
},
{
label: t('user.user.Last login'),
prop: 'last_login_time',
align: 'center',
render: 'datetime',
sortable: 'custom',
operator: 'RANGE',
width: 160,
},
{ label: t('Create time'), prop: 'create_time', align: 'center', render: 'datetime', sortable: 'custom', operator: 'RANGE', width: 160 },
{
label: t('State'),
prop: 'status',
align: 'center',
render: 'tag',
custom: { disable: 'danger', enable: 'success' },
replaceValue: { disable: t('Disable'), enable: t('Enable') },
},
{
label: t('Operate'),
align: 'center',
width: '160',
render: 'buttons',
buttons: optButtons,
operator: false,
}
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: {
gender: 0,
money: '0',
score: '0',
status: 'enable',
},
}
)
const filters = reactive({
gameId: 268 as GameId,
start: today(),
end: today(),
status: 'all' as 'all' | SubmittedRewardParams['status'],
username: '',
})
baTable.mount()
baTable.getData()
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0,
})
provide('baTable', baTable)
const rows = ref<RewardRow[]>([])
const loading = ref(false)
const actionLoadingId = ref<number | string | null>(null)
const activePromotionLabel = computed(() => {
const labelMap: Record<GameId, string> = {
268: 'Plinko Ball',
269: 'Smash Eggs',
270: 'Spin Wheel',
}
return labelMap[filters.gameId]
})
const dateRangeText = computed(() => {
const start = new Date(`${filters.start}T00:00:00`)
const end = new Date(`${filters.end}T00:00:00`)
const duration = Math.max(1, Math.floor((end.getTime() - start.getTime()) / 86400000) + 1)
const range = filters.start === filters.end ? filters.start : `${filters.start} ${t('user.submittedReward.to')} ${filters.end}`
return `${range} (${duration} ${duration === 1 ? t('user.submittedReward.Day') : t('user.submittedReward.Days')})`
})
const mapRow = (item: Record<string, any>, index: number): RewardRow => {
return {
id: item.id ?? `${pagination.currentPage}-${index}`,
submittedTime: String(item.submitted_time ?? ''),
username: String(item.username ?? ''),
rewardClaim: String(item.reward_claim ?? ''),
status: String(item.status_text ?? ''),
statusCode: Number(item.status),
}
}
const normalizeResponse = (responseData: any) => {
const source = responseData?.data ?? responseData ?? {}
const list = source.list ?? {}
const data = Array.isArray(list.data) ? list.data : []
const total = Number(list.total ?? list.count ?? data.length)
const pageSize = Number(list.per_page ?? list.page_size ?? pagination.pageSize)
const currentPage = Number(list.current_page ?? list.page ?? pagination.currentPage)
return {
rows: data.map((item: Record<string, any>, index: number) => mapRow(item, index)),
total: Number.isFinite(total) ? total : 0,
pageSize: Number.isFinite(pageSize) && pageSize > 0 ? pageSize : 20,
currentPage: Number.isFinite(currentPage) && currentPage > 0 ? currentPage : 1,
}
}
const loadRewards = async (page = pagination.currentPage) => {
loading.value = true
try {
const response = await getSubmittedRewards({
start: filters.start,
end: filters.end,
status: filters.status === 'all' ? '' : filters.status,
username: filters.username,
game_id: filters.gameId,
page,
})
const normalized = normalizeResponse(response.data)
rows.value = normalized.rows
pagination.total = normalized.total
pagination.pageSize = normalized.pageSize
pagination.currentPage = normalized.currentPage
} finally {
loading.value = false
}
}
const search = () => {
pagination.currentPage = 1
loadRewards(1)
}
const clearFilters = () => {
filters.start = today()
filters.end = today()
filters.status = 'all'
filters.username = ''
pagination.currentPage = 1
loadRewards(1)
}
const changePromotion = (gameId: GameId) => {
filters.gameId = gameId
pagination.currentPage = 1
loadRewards(1)
}
const changePage = (page: number) => {
loadRewards(page)
}
const confirmReward = (row: RewardRow, status: 1 | 2) => {
const action = status === 1 ? t('user.submittedReward.Agree') : t('user.submittedReward.Reject')
ElMessageBox.confirm(
t('user.submittedReward.Confirm reward action', {
action,
username: row.username,
}),
t('Confirm'),
{
confirmButtonText: t('Confirm'),
cancelButtonText: t('Cancel'),
type: status === 1 ? 'success' : 'warning',
}
)
.then(() => {
actionLoadingId.value = row.id
return editReward({ id: row.id, status })
})
.then(() => loadRewards(pagination.currentPage))
.finally(() => {
actionLoadingId.value = null
})
.catch(() => {
// Closing the confirmation dialog does not require feedback.
})
}
onMounted(() => {
loadRewards(1)
})
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.submitted-rewards {
--reward-dark: #333;
--reward-border: #ddd;
--reward-approved: #dcfbc9;
padding: 20px;
color: var(--el-text-color-primary);
font-size: 14px;
}
.reward-tabs {
display: flex;
width: fit-content;
margin-bottom: 20px;
overflow: hidden;
border: 1px solid #999;
border-radius: 4px;
button {
min-width: 136px;
height: 31px;
padding: 5px 30px;
border: 0;
border-right: 1px solid #999;
background: var(--el-fill-color-light);
color: var(--el-text-color-primary);
cursor: pointer;
&:last-child {
min-width: 99px;
border-right: 0;
}
&:hover {
background: var(--el-fill-color);
}
&.active {
background: #bbb;
color: #000;
}
}
}
.reward-panel {
border: 1px solid var(--reward-border);
}
.panel-heading {
display: flex;
align-items: center;
gap: 12px;
min-height: 40px;
padding: 10px;
background: var(--reward-dark);
color: #fff;
font-weight: 700;
strong {
color: #57e31a;
}
}
.filter-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 28px;
min-height: 88px;
padding: 14px 40px;
background: var(--el-bg-color);
}
.filter-item {
display: flex;
align-items: center;
gap: 12px;
label {
min-width: 76px;
text-align: right;
font-weight: 700;
white-space: nowrap;
}
:deep(.el-date-editor),
:deep(.el-select),
:deep(.el-input) {
width: 200px;
}
}
.filter-actions {
display: flex;
gap: 8px;
.el-button {
width: 100px;
margin: 0;
}
}
.date-summary {
margin: 20px 0 8px;
}
.reward-table-wrap {
min-height: 120px;
overflow-x: auto;
}
.reward-table {
width: 100%;
min-width: 850px;
border-collapse: collapse;
th,
td {
height: 37px;
padding: 8px;
border: 1px solid var(--reward-border);
text-align: left;
white-space: nowrap;
}
th {
background: var(--reward-dark);
color: #fff;
font-weight: 700;
}
tbody tr {
background: var(--reward-approved);
color: #333;
}
.submitted-time {
width: 150px;
}
.username {
width: 100px;
}
.reward-claim {
width: 180px;
}
.action {
width: 180px;
}
.empty-row {
height: 90px;
background: var(--el-bg-color);
color: var(--el-text-color-secondary);
text-align: center;
}
}
.action-buttons {
display: flex;
gap: 8px;
.el-button {
margin: 0;
}
}
.table-footer {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
margin-top: 18px;
}
@at-root html.dark {
.submitted-rewards {
--reward-dark: #2b2b2b;
--reward-border: var(--el-border-color);
--reward-approved: #29482b;
}
.reward-tabs button.active {
background: #555;
color: #fff;
}
.reward-table tbody tr {
color: var(--el-text-color-primary);
}
}
@media (max-width: 900px) {
.submitted-rewards {
padding: 14px;
}
.reward-tabs {
width: 100%;
overflow-x: auto;
button {
min-width: 125px;
}
}
.filter-panel {
padding: 14px;
}
}
</style>