feat: 迁移渠道列表新增和游戏累计分布

This commit is contained in:
Yvan
2026-11-03 21:32:51 +08:00
parent 5e58c6bcf3
commit 075d94a51e
39 changed files with 2216 additions and 12 deletions
+6
View File
@@ -0,0 +1,6 @@
import service from '@/utils/request'
export const listChannels = params => service({ url: '/sw/promote/list', method: 'get', params })
export const channelBindOptions = sourceDb => service({ url: '/sw/promote/bindOptions', method: 'get', params: { sourceDb } })
export const createChannel = data => service({ url: '/sw/promote', method: 'post', data })
export const channelGames = params => service({ url: '/sw/promote/game-distribution', method: 'get', params })
+1
View File
@@ -56,6 +56,7 @@
"/src/view/sw/AlertRules.vue": "SwAlertRules",
"/src/view/sw/BusinessList.vue": "SwBusinessList",
"/src/view/sw/ChannelActivationList.vue": "SwChannelActivationList",
"/src/view/sw/ChannelList.vue": "SwChannelList",
"/src/view/sw/components/BusinessBindings.vue": "SwBusinessBindings",
"/src/view/sw/components/BusinessChannels.vue": "SwBusinessChannels",
"/src/view/system/security/forceChangePassword.vue": "ForceChangePassword",
+79
View File
@@ -0,0 +1,79 @@
<template>
<div>
<div class="kra-search-box">
<el-form :model="query" :inline="true" @submit.prevent="search">
<el-form-item label="所属平台"><el-select v-model="sources" multiple collapse-tags clearable placeholder="选择平台" style="width: 210px"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="商务账号"><el-input v-model="query.busierAccount" clearable placeholder="商务账号" /></el-form-item>
<el-form-item label="渠道账号"><el-input v-model="query.keyword" clearable placeholder="渠道账号" /></el-form-item>
<el-form-item label="渠道层级"><el-select v-model="query.promoteLevel" clearable placeholder="全部层级" style="width: 140px"><el-option v-for="(label, value) in levels" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item label="渠道类型"><el-select v-model="query.registerType" clearable placeholder="全部类型" style="width: 140px"><el-option label="个人" :value="0" /><el-option label="公会" :value="1" /></el-select></el-form-item>
<el-form-item label="状态"><el-select v-model="query.lockStatus" clearable placeholder="全部状态" style="width: 140px"><el-option v-for="(label, value) in statuses" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">搜索</el-button></el-form-item>
</el-form>
</div>
<div class="kra-table-box">
<div class="kra-btn-list"><el-button v-if="canAdd" type="primary" icon="plus" @click="adding = true">添加渠道</el-button></div>
<el-alert v-if="!available" class="mb-4" type="warning" :closable="false" title="渠道数据尚未接入。" />
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.id}`">
<el-table-column prop="platformName" label="所属平台" min-width="100" />
<el-table-column prop="id" label="ID" min-width="90" />
<el-table-column prop="account" label="渠道账号" min-width="150" show-overflow-tooltip />
<el-table-column label="渠道层级" min-width="110"><template #default="{ row }"><el-tag effect="plain">{{ levels[row.promoteLevel] || row.promoteLevel || '—' }}</el-tag></template></el-table-column>
<el-table-column prop="busierDisplayName" label="归属商务" min-width="200" show-overflow-tooltip />
<el-table-column label="上级渠道" min-width="130" show-overflow-tooltip><template #default="{ row }">{{ row.parentName || '—' }}</template></el-table-column>
<el-table-column label="负责人" min-width="100"><template #default="{ row }">{{ row.realName || '—' }}</template></el-table-column>
<el-table-column label="渠道类型" min-width="100"><template #default="{ row }">{{ { 0: '个人', 1: '公会' }[row.registerType] ?? row.registerType ?? '—' }}</template></el-table-column>
<el-table-column label="推广游戏" min-width="115"><template #default="{ row }"><el-button v-if="row.gameCount != null" link type="primary" @click="openGames(row)">{{ channelCount(row.gameCount) }} 款</el-button><span v-else>未接入</span></template></el-table-column>
<el-table-column label="玩家数量" min-width="130"><template #default="{ row }">{{ channelCount(row.playerCount) }}</template></el-table-column>
<el-table-column label="累计充值" min-width="160"><template #default="{ row }">{{ channelMoney(row.totalPayAmount) }}</template></el-table-column>
<el-table-column label="状态" min-width="110"><template #default="{ row }"><el-tag :type="{ 0: 'info', '-1': 'danger', '-2': 'warning' }[row.lockStatus] || 'info'" effect="plain">{{ statuses[row.lockStatus] ?? row.lockStatus ?? '—' }}</el-tag></template></el-table-column>
</el-table>
<div class="kra-pagination"><el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
<add-channel-drawer v-model="adding" @success="search" />
<channel-game-drawer v-model="gamesOpen" :channel="selected" />
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
import { useBtnAuth } from '@/utils/btnAuth'
import { listChannels } from '@/api/channel'
import { channelCount, channelMoney } from './channel-display'
import AddChannelDrawer from './components/AddChannelDrawer.vue'
import ChannelGameDrawer from './components/ChannelGameDrawer.vue'
defineOptions({ name: 'SwChannelList' })
const user = useUserStore()
const buttons = useBtnAuth()
const canAdd = computed(() => Number(user.userInfo.authorityId) === 888 || !!buttons['sw:promote:add'])
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
const levels = { 1: '一级渠道', 2: '二级渠道', 3: '三级渠道' }
const statuses = { 0: '正常', '-1': '已锁定', '-2': '解除合作' }
const sources = ref(Object.keys(platforms))
const query = reactive({ pageNum: 1, pageSize: 10, busierAccount: '', keyword: '', promoteLevel: undefined, registerType: undefined, lockStatus: undefined })
const rows = ref([])
const total = ref(0)
const available = ref(true)
const loading = ref(false)
const adding = ref(false)
const gamesOpen = ref(false)
const selected = ref(null)
let requestVersion = 0
const load = async () => {
const version = ++requestVersion
if (!sources.value.length) { rows.value = []; total.value = 0; available.value = true; loading.value = false; return }
loading.value = true
try {
const res = await listChannels({ ...query, sourceDb: sources.value.join(','), busierAccount: query.busierAccount.trim(), keyword: query.keyword.trim() })
if (version !== requestVersion) return
if (res.code === 0) { rows.value = res.data.rows; total.value = res.data.total; available.value = res.data.available !== false }
else { rows.value = []; total.value = 0 }
} catch { if (version === requestVersion) { rows.value = []; total.value = 0 } } finally { if (version === requestVersion) loading.value = false }
}
const search = () => { query.pageNum = 1; if (!sources.value.length) ElMessage.warning('请选择平台'); return load() }
const openGames = row => { selected.value = row; gamesOpen.value = true }
onMounted(load)
</script>
+8
View File
@@ -0,0 +1,8 @@
export const channelCount = value => value == null ? '未接入' : String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
export const channelMoney = value => {
if (value == null) return '未接入'
const text = String(value)
if (!/^-?\d+(\.\d+)?$/.test(text)) return '—'
const [whole, fraction = ''] = text.split('.')
return `¥${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}.${fraction.padEnd(2, '0').slice(0, 2)}`
}
@@ -0,0 +1,58 @@
<template>
<el-drawer v-model="visible" append-to-body destroy-on-close title="添加渠道" size="min(520px, 100vw)" :close-on-click-modal="!saving" :close-on-press-escape="!saving" :show-close="!saving" @closed="reset">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="saving">
<el-form-item label="所属平台" prop="sourceDb"><el-select v-model="form.sourceDb" placeholder="请选择所属平台" clearable @change="loadOptions"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="游戏商务" prop="platformBusierId"><el-select v-model="form.platformBusierId" placeholder="请选择游戏商务" filterable clearable :disabled="!form.sourceDb" :loading="optionsLoading"><el-option v-for="item in options" :key="item.id" :label="item.account" :value="item.id" /></el-select></el-form-item>
<el-form-item label="推广类型"><el-select v-model="form.registerType"><el-option label="个人" :value="0" /><el-option label="企业" :value="1" /></el-select></el-form-item>
<el-form-item label="渠道账号" prop="account"><el-input v-model="form.account" placeholder="中文/英文/数字,4-30位" maxlength="30" clearable /></el-form-item>
<el-form-item label="渠道密码" prop="password"><el-input v-model="form.password" type="password" show-password placeholder="6-15位" maxlength="15" autocomplete="new-password" /></el-form-item>
</el-form>
<el-alert type="info" :closable="false" title="创建后该渠道归属到所选游戏商务名下,由其拓展并管理玩家。" />
<template #footer><el-button :disabled="saving" @click="visible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">确认添加</el-button></template>
</el-drawer>
</template>
<script setup>
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { channelBindOptions, createChannel } from '@/api/channel'
const props = defineProps({ modelValue: Boolean })
const emit = defineEmits(['update:modelValue', 'success'])
const visible = computed({ get: () => props.modelValue, set: value => emit('update:modelValue', value) })
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
const initial = () => ({ sourceDb: '', platformBusierId: undefined, registerType: 0, account: '', password: '' })
const form = ref(initial())
const formRef = ref()
const options = ref([])
const optionsLoading = ref(false)
const saving = ref(false)
let optionVersion = 0
const rules = {
sourceDb: [{ required: true, message: '请选择所属平台', trigger: 'change' }],
platformBusierId: [{ required: true, message: '请选择游戏商务', trigger: 'change' }],
account: [{ required: true, message: '请输入渠道账号', trigger: 'blur' }, { validator: (_rule, value, callback) => /^[\u4e00-\u9fa5a-zA-Z0-9]{4,30}$/.test(value?.trim() || '') ? callback() : callback(new Error('支持中文、英文、数字;长度 4-30 位')), trigger: 'blur' }],
password: [{ required: true, message: '请输入渠道密码', trigger: 'blur' }, { min: 6, max: 15, message: '密码长度需在 6 到 15 位', trigger: 'blur' }]
}
const reset = () => { ++optionVersion; form.value = initial(); options.value = []; optionsLoading.value = false }
const loadOptions = async () => {
const version = ++optionVersion
form.value.platformBusierId = undefined
options.value = []
if (!form.value.sourceDb) { optionsLoading.value = false; return }
optionsLoading.value = true
try {
const res = await channelBindOptions(form.value.sourceDb)
if (version !== optionVersion || res.code !== 0) return
const unique = new Map(res.data.map(item => [item.platformBusierId, { id: item.platformBusierId, account: item.platformBusierAccount?.trim() || String(item.platformBusierId) }]))
options.value = [...unique.values()]
} finally { if (version === optionVersion) optionsLoading.value = false }
}
const save = async () => {
if (saving.value || !await formRef.value.validate().catch(() => false)) return
saving.value = true
try {
const res = await createChannel({ ...form.value, account: form.value.account.trim() })
if (res.code === 0) { ElMessage.success(res.msg || '添加成功,数据同步到列表可能有延迟'); visible.value = false; emit('success') }
} finally { saving.value = false }
}
</script>
@@ -0,0 +1,38 @@
<template>
<el-drawer v-model="visible" append-to-body destroy-on-close :title="`推广游戏数据 — ${channel?.nickname || channel?.account || ''}`" size="min(560px, 100vw)" @closed="reset">
<p class="mb-4">该渠道在各推广游戏的注册与充值分布(累计口径)。</p>
<el-alert v-if="error" type="warning" :closable="false" :title="error" />
<el-table v-else v-loading="loading" :data="games">
<el-table-column label="游戏" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ row.gameName || '—' }}</template></el-table-column>
<el-table-column label="玩家数量" min-width="110"><template #default="{ row }">{{ channelCount(row.registerCount) }}</template></el-table-column>
<el-table-column label="充值金额" min-width="150"><template #default="{ row }">{{ channelMoney(row.payAmount) }}</template></el-table-column>
</el-table>
</el-drawer>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { channelGames } from '@/api/channel'
import { channelCount, channelMoney } from '../channel-display'
const props = defineProps({ modelValue: Boolean, channel: { type: Object, default: null } })
const emit = defineEmits(['update:modelValue'])
const visible = computed({ get: () => props.modelValue, set: value => emit('update:modelValue', value) })
const games = ref([])
const loading = ref(false)
const error = ref('')
let requestVersion = 0
const reset = () => { ++requestVersion; games.value = []; error.value = ''; loading.value = false }
const load = async () => {
reset()
const version = ++requestVersion
if (!props.channel) return
loading.value = true
try {
const res = await channelGames({ sourceDb: props.channel.sourceDb, promoteId: props.channel.id, period: 'total' })
if (version !== requestVersion) return
if (res.code === 0) games.value = res.data
else error.value = res.msg || '推广游戏数据读取失败'
} catch { if (version === requestVersion) error.value = '推广游戏数据读取失败,请重试' } finally { if (version === requestVersion) loading.value = false }
}
watch(() => [props.modelValue, props.channel], ([open]) => { if (open) void load() })
</script>