feat: 排行榜奖励后台配置页

- 小程序配置下新增排行榜奖励菜单与 RankReward 页面

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-01 17:40:56 +08:00
parent 1c64f84d68
commit 6ed29939cb
3 changed files with 265 additions and 0 deletions

View File

@@ -275,6 +275,11 @@ const routes = [
component: () => import('@/views/miniapp/DashouExam.vue'),
meta: { title: '打手接单考试' }
},
{
path: 'miniapp/rank-reward',
component: () => import('@/views/miniapp/RankReward.vue'),
meta: { title: '排行榜奖励' }
},
// ========== 店铺管理(原有) ==========
{
path: 'shop/list',

View File

@@ -144,6 +144,7 @@
<el-menu-item v-if="canShow('miniapp.leixing')" index="/miniapp/leixing">抢单商品类型</el-menu-item>
<el-menu-item v-if="canShow('miniapp.kaohe-tags')" index="/miniapp/kaohe-tags">抢单考核标签</el-menu-item>
<el-menu-item v-if="canShow('miniapp.dashou-exam')" index="/miniapp/dashou-exam">打手接单考试</el-menu-item>
<el-menu-item v-if="canShow('miniapp.rank-reward')" index="/miniapp/rank-reward">排行榜奖励</el-menu-item>
<el-menu-item v-if="canShow('miniapp.withdraw-settings')" index="/withdraw/settings">提现与收款设置</el-menu-item>
</el-sub-menu>

View File

@@ -0,0 +1,259 @@
<template>
<div class="rank-reward-page">
<el-alert
v-if="isAllClubScope"
type="warning"
:closable="false"
show-icon
title="排行榜奖励需在具体俱乐部视图下配置,请切换顶栏俱乐部。"
class="scope-alert"
/>
<ClubScopeHint v-else />
<div v-if="!hasPermission" class="no-permission">
<div class="no-permission-icon">🔒</div>
<div>无排行榜奖励配置权限phbj666 / 8080a</div>
</div>
<div v-else>
<div class="header-bar">
<h2>排行榜奖励配置</h2>
<p class="desc">按俱乐部分奖小程序可展示集团总榜奖金仅发给本俱乐部用户结算周期昨日 / 上周 / 上月</p>
</div>
<el-tabs v-model="activeShenfen" @tab-change="loadSchemes">
<el-tab-pane label="接单员" name="dashou" />
<el-tab-pane label="管事" name="guanshi" />
<el-tab-pane label="组长" name="zuzhang" />
<el-tab-pane label="商家" name="shangjia" />
</el-tabs>
<el-tabs v-model="activePeriod" type="card" @tab-change="selectScheme">
<el-tab-pane label="日榜(昨日)" name="day" />
<el-tab-pane label="周榜(上周)" name="week" />
<el-tab-pane label="月榜(上月)" name="month" />
</el-tabs>
<el-card v-loading="loading">
<el-form :model="form" label-width="120px" :disabled="isAllClubScope">
<el-form-item label="启用">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item label="标题">
<el-input v-model="form.title" placeholder="如:星阙接单员周榜奖励" />
</el-form-item>
<el-form-item label="排序指标">
<el-select v-model="form.sort_field" style="width: 280px">
<el-option v-for="o in sortOptions" :key="o.key" :label="o.label" :value="o.key" />
</el-select>
</el-form-item>
<el-form-item label="最低指标">
<el-input-number v-model="form.min_metric" :min="0" :precision="2" />
</el-form-item>
<el-form-item label="领取有效天">
<el-input-number v-model="form.claim_days" :min="1" :max="365" />
</el-form-item>
<el-form-item label="规则说明">
<el-input v-model="form.description" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<div class="tier-toolbar">
<span>奖励档位</span>
<el-button size="small" :disabled="isAllClubScope" @click="addTier">+ 添加档位</el-button>
</div>
<el-table :data="form.tiers" border size="small">
<el-table-column label="名次起" width="100">
<template #default="{ row }"><el-input-number v-model="row.rank_from" :min="1" size="small" /></template>
</el-table-column>
<el-table-column label="名次止" width="100">
<template #default="{ row }"><el-input-number v-model="row.rank_to" :min="1" size="small" /></template>
</el-table-column>
<el-table-column label="奖金(元)" width="120">
<template #default="{ row }"><el-input-number v-model="row.reward_amount" :min="0" :precision="2" size="small" /></template>
</el-table-column>
<el-table-column label="标签">
<template #default="{ row }"><el-input v-model="row.label" size="small" placeholder="冠军" /></template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" link :disabled="isAllClubScope" @click="form.tiers.splice($index, 1)"></el-button>
</template>
</el-table-column>
</el-table>
<div class="actions">
<el-button type="primary" :loading="saving" :disabled="isAllClubScope" @click="save">保存配置</el-button>
</div>
</el-card>
<el-card class="claims-card" header="领取明细最近100条">
<el-table :data="claims" border size="small" v-loading="claimsLoading">
<el-table-column prop="yonghuid" label="用户ID" width="100" />
<el-table-column prop="shenfen" label="身份" width="90" />
<el-table-column prop="mingci" label="名次" width="70" />
<el-table-column prop="amount" label="金额" width="90" />
<el-table-column prop="status_text" label="状态" width="90" />
<el-table-column prop="claimed_at" label="领取时间" />
</el-table>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import ClubScopeHint from '@/components/ClubScopeHint.vue'
import { getAdminClubScope } from '@/utils/club-context'
const username = ref(localStorage.getItem('username') || '')
const hasPermission = ref(true)
const loading = ref(false)
const saving = ref(false)
const claimsLoading = ref(false)
const schemes = ref([])
const claims = ref([])
const activeShenfen = ref('dashou')
const activePeriod = ref('week')
const isAllClubScope = computed(() => getAdminClubScope() === 'all')
const SORT_OPTIONS = {
dashou: [
{ key: 'chengjiao_zongliang', label: '成交量' },
{ key: 'chengjiao_zonge', label: '成交总额' },
{ key: 'jiedan_zongliang', label: '接单量' },
{ key: 'jiedan_zonge', label: '接单总额' },
],
guanshi: [
{ key: 'chongzhi_dashou_shu', label: '有效打手数' },
{ key: 'shouru_zonge', label: '收益总额' },
{ key: 'yaoqing_dashou_shu', label: '邀请打手数' },
],
zuzhang: [
{ key: 'yaoqing_guanshi_shu', label: '邀请管事数' },
{ key: 'shouru_zonge', label: '收入总额' },
{ key: 'fenyong_jine', label: '分佣金额' },
],
shangjia: [
{ key: 'jiesuan_jine', label: '结算金额' },
{ key: 'jiesuan_dingdan_shu', label: '结算单量' },
{ key: 'paifa_jine', label: '派单流水' },
],
}
const sortOptions = computed(() => SORT_OPTIONS[activeShenfen.value] || [])
const form = reactive({
enabled: false,
title: '',
sort_field: 'chengjiao_zongliang',
min_metric: 0,
claim_days: 30,
description: '',
tiers: [{ rank_from: 1, rank_to: 1, reward_amount: 0, label: '冠军' }],
})
const loadSchemes = async () => {
loading.value = true
try {
const res = await request.post('/houtai/phbjhq', { username: username.value })
if (res.code === 403) {
hasPermission.value = false
return
}
if (res.code === 0) {
schemes.value = res.data.schemes || []
selectScheme()
}
} finally {
loading.value = false
}
}
const selectScheme = () => {
const hit = schemes.value.find(
(s) => s.shenfen === activeShenfen.value && s.period_type === activePeriod.value,
)
const defaults = SORT_OPTIONS[activeShenfen.value]?.[0]?.key || 'chengjiao_zongliang'
if (hit) {
Object.assign(form, {
enabled: hit.enabled,
title: hit.title || '',
sort_field: hit.sort_field || defaults,
min_metric: hit.min_metric || 0,
claim_days: hit.claim_days || 30,
description: hit.description || '',
tiers: hit.tiers?.length
? hit.tiers.map((t) => ({ ...t, reward_amount: Number(t.reward_amount) }))
: [{ rank_from: 1, rank_to: 1, reward_amount: 0, label: '冠军' }],
})
} else {
Object.assign(form, {
enabled: false,
title: '',
sort_field: defaults,
min_metric: 0,
claim_days: 30,
description: '',
tiers: [{ rank_from: 1, rank_to: 1, reward_amount: 0, label: '冠军' }],
})
}
}
const addTier = () => {
form.tiers.push({ rank_from: 1, rank_to: 3, reward_amount: 0, label: '' })
}
const save = async () => {
if (!form.tiers.length) {
ElMessage.warning('请配置奖励档位')
return
}
saving.value = true
try {
const res = await request.post('/houtai/phbjbc', {
username: username.value,
shenfen: activeShenfen.value,
period_type: activePeriod.value,
...form,
})
if (res.code === 0) {
ElMessage.success('保存成功')
await loadSchemes()
} else {
ElMessage.error(res.msg || '保存失败')
}
} finally {
saving.value = false
}
}
const loadClaims = async () => {
claimsLoading.value = true
try {
const res = await request.post('/houtai/phbjjl', { username: username.value })
if (res.code === 0) claims.value = res.data.list || []
} finally {
claimsLoading.value = false
}
}
onMounted(() => {
loadSchemes()
loadClaims()
})
</script>
<style scoped>
.rank-reward-page { padding: 16px; }
.scope-alert { margin-bottom: 12px; }
.header-bar { margin-bottom: 16px; }
.header-bar h2 { margin: 0 0 8px; }
.desc { margin: 0; color: #888; font-size: 13px; }
.tier-toolbar { display: flex; justify-content: space-between; margin: 16px 0 8px; }
.actions { margin-top: 16px; }
.claims-card { margin-top: 20px; }
.no-permission { text-align: center; padding: 80px 20px; }
</style>