feat: integrate OA activation workflows and business migration
This commit is contained in:
@@ -2,3 +2,8 @@ import service from '@/utils/businessRequest'
|
||||
|
||||
export const listChannelActivations = (params) => service({ url: '/sw/promoteActivePool/bindList', method: 'get', params })
|
||||
export const getActivationSpend = (id, options = {}) => service({ ...options, url: '/sw/promoteActivePool/bindSpendDetail', method: 'get', params: { id } })
|
||||
|
||||
export const listActivationPool = (params, options = {}) => service({ ...options, url: '/sw/promoteActivePool/pool', method: 'get', params })
|
||||
export const listActivationOptions = () => service({ url: '/sw/promoteActivePool/options', method: 'get' })
|
||||
export const applyActivation = (data) => service({ url: '/sw/promoteActivePool/apply', method: 'post', data })
|
||||
export const listActivationChurn = (params, options = {}) => service({ ...options, url: '/sw/promoteActivePool/churn', method: 'get', params })
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div class="sw-page">
|
||||
<el-tabs v-model="tab" @tab-change="changeTab"><el-tab-pane label="激活列表" name="activate" /><el-tab-pane label="申请绑定" name="apply" /><el-tab-pane label="流失记录" name="churn" /></el-tabs>
|
||||
<activation-resources v-if="tab !== 'activate'" :key="tab" :mode="tab" @applied="showActivations" />
|
||||
<template v-else>
|
||||
<div class="kra-search-box">
|
||||
<el-form :model="query" :inline="true" @submit.prevent="search">
|
||||
<el-form-item label="所属平台"><el-select v-model="query.platformNames" 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.promoteAccount" clearable placeholder="渠道名称 / 账号" /></el-form-item>
|
||||
<el-form-item label="绑定商务"><el-input v-model="query.busierAccount" clearable placeholder="绑定商务" /></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="query.statuses" clearable placeholder="全部状态" style="width:140px"><el-option v-for="(label,value) in statuses" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item><el-button icon="refresh" @click="reset">重置</el-button><el-button type="primary" icon="search" native-type="submit">查询</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -15,14 +19,16 @@
|
||||
<el-table-column prop="platformTitle" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb || row.platformCode" :label="row.platformTitle" /></template></el-table-column>
|
||||
<el-table-column prop="promoteAccount" label="渠道名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="busierAccount" label="绑定商务" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="绑定日期" min-width="120" class-name="sw-col-meta"><template #default="{ row }">{{ date(row.bindAt) }}</template></el-table-column>
|
||||
<el-table-column prop="bindAt" sortable :sort-method="(a,b) => (a.bindAt || a.createdAt || '').localeCompare(b.bindAt || b.createdAt || '')" label="绑定日期" min-width="120" class-name="sw-col-meta"><template #default="{ row }">{{ date(row.bindAt || row.createdAt) }}</template></el-table-column>
|
||||
<el-table-column label="解绑时间" min-width="120" class-name="sw-col-meta"><template #default="{ row }">{{ date(row.unbindAt) }}</template></el-table-column>
|
||||
<el-table-column label="累计流水金额" min-width="160" align="right"><template #default="{ row }"><metric-value kind="money" :value="row.accumAmount" /></template></el-table-column>
|
||||
<el-table-column prop="accumAmount" sortable :sort-method="compareAmount" label="累计流水金额" min-width="160" align="right"><template #default="{ row }"><metric-value kind="money" :value="row.bindAt && row.metricsAvailable ? row.accumAmount : null" /></template></el-table-column>
|
||||
<el-table-column label="累计注册玩家" min-width="145" align="right"><template #default="{ row }"><metric-value kind="count" :value="row.registeredPlayers" /></template></el-table-column>
|
||||
<el-table-column label="激活状态" width="110"><template #default="{ row }"><el-tag effect="plain" :type="statusTone(row.status)">{{ row.statusText || row.status || '—' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><el-button v-if="row.status !== 'rejected' && row.statusText !== '已驳回'" link type="primary" @click="openFlow(row)">流水明细</el-button><span v-else>—</span></template></el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><el-button v-if="row.bindAt && !['pending','rejected'].includes(row.status)" link type="primary" @click="openFlow(row)">流水明细</el-button><span v-else>—</span></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>
|
||||
</template>
|
||||
<el-dialog class="sw-dialog" v-model="dialog" title="流水明细" width="min(640px, 95vw)" append-to-body destroy-on-close>
|
||||
<div v-loading="flowLoading">
|
||||
<el-descriptions :column="1">
|
||||
@@ -47,6 +53,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ActivationResources from './components/ActivationResources.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import PlatformTag from './components/PlatformTag.vue'
|
||||
import MetricValue from './components/MetricValue.vue'
|
||||
import { computed, onBeforeUnmount, onDeactivated, onMounted, reactive, ref, watch } from 'vue'
|
||||
@@ -54,8 +62,19 @@ import { createLatestRequest } from '@/utils/latestRequest'
|
||||
import { listChannelActivations, getActivationSpend } from '@/api/activation'
|
||||
|
||||
defineOptions({ name: 'SwChannelActivationList' })
|
||||
const route = useRoute(), router = useRouter()
|
||||
const tabFromHash = () => ['apply','churn'].includes(route.hash.slice(1)) ? route.hash.slice(1) : 'activate'
|
||||
const tab = ref(tabFromHash())
|
||||
const statuses = { pending: '审核中', in_progress: '进行中', reached: '已达标', unbound: '已解绑' }
|
||||
const changeTab = value => { router.replace({ hash: `#${value}` }); if (value === 'activate') load() }
|
||||
const showActivations = () => { tab.value = 'activate'; changeTab('activate') }
|
||||
watch(() => route.hash, () => { const next = tabFromHash(); if (next !== tab.value) { tab.value = next; if (next === 'activate') load() } })
|
||||
const compareAmount = (a,b) => {
|
||||
const cents = value => { const [whole, fraction = ''] = String(value || '0').split('.'); return BigInt(whole) * 100n + BigInt(fraction.padEnd(2,'0').slice(0,2)) }
|
||||
const x = cents(a.accumAmount), y = cents(b.accumAmount); return x === y ? 0 : x > y ? 1 : -1
|
||||
}
|
||||
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
|
||||
const query = reactive({ pageNum: 1, pageSize: 10, platformNames: Object.keys(platforms), promoteAccount: '', busierAccount: '' })
|
||||
const query = reactive({ pageNum: 1, pageSize: 10, platformNames: Object.keys(platforms), promoteAccount: '', busierAccount: '', statuses: '' })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const available = ref(true)
|
||||
@@ -98,7 +117,7 @@ const load = async () => {
|
||||
} catch { if (version === listVersion) error.value = '渠道激活查询失败,请重试' } finally { if (version === listVersion) loading.value = false }
|
||||
}
|
||||
const search = () => { query.pageNum = 1; return load() }
|
||||
const reset = () => { Object.assign(query, { platformNames: Object.keys(platforms), promoteAccount: '', busierAccount: '' }); return search() }
|
||||
const reset = () => { Object.assign(query, { platformNames: Object.keys(platforms), promoteAccount: '', busierAccount: '', statuses: '' }); return search() }
|
||||
const clearDetail = () => { detailRequests.cancel(); detail.value = null; selected.value = null; flowError.value = ''; flowLoading.value = false }
|
||||
const openFlow = async row => {
|
||||
clearDetail()
|
||||
@@ -116,5 +135,5 @@ const openFlow = async row => {
|
||||
watch(dialog, open => { if (!open) clearDetail() }, { flush: 'sync' })
|
||||
onBeforeUnmount(clearDetail)
|
||||
onDeactivated(() => { dialog.value = false; clearDetail() })
|
||||
onMounted(load)
|
||||
onMounted(() => { if (tab.value === 'activate') load() })
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="kra-search-box">
|
||||
<el-form :inline="true" :model="query" @submit.prevent="search">
|
||||
<el-form-item label="所属平台"><el-select v-model="query.platformNames" multiple collapse-tags clearable placeholder="全部平台" style="width:190px"><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.promoteAccount" clearable /></el-form-item>
|
||||
<template v-if="isPool">
|
||||
<el-form-item label="渠道类型"><el-select v-model="query.channelType" clearable placeholder="全部类型" style="width:130px"><el-option v-for="value in ['公会','个人']" :key="value" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item label="锁定状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width:140px"><el-option v-for="value in ['正常','锁定','解除合作']" :key="value" :value="value" /></el-select></el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label="换绑到的商务"><el-input v-model="query.target" clearable /></el-form-item>
|
||||
<el-form-item label="流失时间"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>
|
||||
</template>
|
||||
<el-form-item><el-button @click="reset">重置</el-button><el-button type="primary" native-type="submit">搜索</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" class="mb-4" />
|
||||
<el-alert v-else-if="!available" title="数据暂不可用,请稍后重试或联系管理员。" type="warning" :closable="false" class="mb-4" />
|
||||
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.id}`" @sort-change="sortChanged">
|
||||
<el-table-column label="所属平台" width="110"><template #default="{row}"><platform-tag :code="row.sourceDb" :label="row.platformTitle" /></template></el-table-column>
|
||||
<el-table-column label="渠道名称" min-width="180"><template #default="{row}">{{ row.name }} <el-tag v-if="row.rejected" type="danger" size="small">已驳回</el-tag></template></el-table-column>
|
||||
<template v-if="isPool">
|
||||
<el-table-column prop="channelType" label="渠道类型" width="120" />
|
||||
<el-table-column label="锁定状态" width="120"><template #default="{row}"><el-tag :type="row.status === '正常' ? 'success' : row.status === '锁定' ? 'warning' : 'danger'">{{ row.status }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="noLoginDays" label="未登录天数" sortable width="150"><template #default="{row}">{{ row.noLoginDays == null ? '—' : `${row.noLoginDays} 天` }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="120"><template #default="{row}"><el-button link type="primary" @click="openApplication(row)">申请绑定</el-button></template></el-table-column>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-table-column prop="at" label="流失时间" width="180" />
|
||||
<el-table-column prop="from" label="原归属商务" min-width="150" />
|
||||
<el-table-column label="换绑到的商务" min-width="150"><template #default="{row}"><el-tag v-if="row.toPool" type="warning">渠道激活池</el-tag><span v-else>{{ row.to || '—' }}</span></template></el-table-column>
|
||||
<el-table-column prop="reason" label="流失原因" min-width="140" />
|
||||
<el-table-column prop="operator" label="操作人" min-width="130" />
|
||||
</template>
|
||||
</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" @current-change="load" @size-change="search" /></div>
|
||||
</div>
|
||||
<el-dialog v-model="dialog" title="申请绑定" width="min(520px,95vw)" append-to-body :close-on-click-modal="!submitting" :show-close="!submitting">
|
||||
<el-descriptions :column="1"><el-descriptions-item label="渠道名称">{{ selected?.name }}</el-descriptions-item><el-descriptions-item label="所属平台">{{ selected?.platformTitle }}</el-descriptions-item><el-descriptions-item label="锁定状态">{{ selected?.status }}</el-descriptions-item></el-descriptions>
|
||||
<el-alert v-if="selected?.rejected" :title="`上次申请已驳回 ${selected.rejectedAt || ''}`" :description="selected.rejectReason || '请联系审核人了解原因'" type="warning" :closable="false" class="mb-4" />
|
||||
<el-form label-width="85px"><el-form-item label="绑定商务" required><el-select v-model="busierId" placeholder="选择本人已绑定的游戏商务" style="width:100%" :loading="optionsLoading" :disabled="submitting"><el-option v-for="option in applicableOptions" :key="option.id" :value="option.id" :label="`${option.account}(ID ${option.id} · ${option.level}级)`" /></el-select></el-form-item></el-form>
|
||||
<el-alert v-if="applyError" :title="applyError" type="error" :closable="false" />
|
||||
<el-alert v-else-if="!optionsLoading && !applicableOptions.length" title="当前平台暂无已审核通过的游戏商务,请先完成账号绑定。" type="info" :closable="false" />
|
||||
<template #footer><el-button :disabled="submitting" @click="dialog=false">取消</el-button><el-button type="primary" :disabled="!busierId || optionsLoading" :loading="submitting" @click="submit">提交申请</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listActivationPool, listActivationChurn, listActivationOptions, applyActivation } from '@/api/activation'
|
||||
import { createLatestRequest } from '@/utils/latestRequest'
|
||||
import PlatformTag from './PlatformTag.vue'
|
||||
|
||||
const props = defineProps({ mode: { type: String, required: true } }), emit = defineEmits(['applied'])
|
||||
const isPool = computed(() => props.mode === 'apply'), platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
|
||||
const defaults = () => ({ pageNum: 1, pageSize: 10, platformNames: [], promoteAccount: '', channelType: '', status: '', target: '' })
|
||||
const query = reactive(defaults()), dates = ref([]), rows = ref([]), total = ref(0), loading = ref(false), error = ref(''), available = ref(true)
|
||||
const dialog = ref(false), selected = ref(null), busierId = ref(''), options = ref([]), optionsLoading = ref(false), submitting = ref(false), applyError = ref('')
|
||||
const applicableOptions = computed(() => options.value.filter(option => option.sourceDb === selected.value?.sourceDb))
|
||||
const requests = createLatestRequest(); let optionVersion = 0
|
||||
const load = async () => {
|
||||
const request = requests.start(); loading.value = true; error.value = ''
|
||||
try {
|
||||
const res = await (isPool.value ? listActivationPool : listActivationChurn)({ ...query, platformNames: query.platformNames.join(','), begin: dates.value?.[0] || '', end: dates.value?.[1] || '' }, { signal: request.signal })
|
||||
if (!request.isCurrent()) return
|
||||
if (res.code !== 0) throw new Error(res.msg || '查询失败')
|
||||
rows.value = res.data.rows; total.value = res.data.total; available.value = res.data.available
|
||||
} catch (e) { if (request.isCurrent()) { rows.value = []; total.value = 0; error.value = e.message || '查询失败,请重试' } }
|
||||
finally { if (request.isCurrent()) loading.value = false }
|
||||
}
|
||||
const search = () => { query.pageNum = 1; return load() }
|
||||
const reset = () => { Object.assign(query, defaults()); dates.value = []; return load() }
|
||||
const sortChanged = () => { if (query.pageNum !== 1) { query.pageNum = 1; load() } }
|
||||
const openApplication = async row => {
|
||||
selected.value = row; busierId.value = ''; applyError.value = ''; options.value = []; dialog.value = true; optionsLoading.value = true
|
||||
const version = ++optionVersion
|
||||
try { const res = await listActivationOptions(); if (version !== optionVersion) return; if (res.code !== 0) throw new Error(res.msg); options.value = res.data }
|
||||
catch (e) { if (version === optionVersion) applyError.value = e.message || '商务选项读取失败' }
|
||||
finally { if (version === optionVersion) optionsLoading.value = false }
|
||||
}
|
||||
const submit = async () => {
|
||||
if (submitting.value || !busierId.value || !selected.value) return
|
||||
const channel = selected.value, option = applicableOptions.value.find(item => item.id === busierId.value)
|
||||
if (!option) return
|
||||
try { await ElMessageBox.confirm(`确认申请绑定「${channel.name}」到「${option.account}」?提交后等待 OA 审核。`, '提交绑定申请', { type: 'warning' }) } catch { return }
|
||||
submitting.value = true; applyError.value = ''
|
||||
try { const res = await applyActivation({ poolId: channel.id, busierId: option.id, sourceDb: channel.sourceDb }); if (res.code !== 0) throw new Error(res.msg); dialog.value = false; ElMessage.success('申请已提交,请在激活列表查看进度'); emit('applied') }
|
||||
catch (e) { applyError.value = e.message || '提交失败,请重试' }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
onMounted(load)
|
||||
onBeforeUnmount(() => { requests.cancel(); ++optionVersion })
|
||||
</script>
|
||||
Reference in New Issue
Block a user