fix: 统一商务表格展示并修正筛选和全量排序

This commit is contained in:
Yvan
2026-11-05 01:48:09 +08:00
parent dc97690eb1
commit 10600d1bc9
38 changed files with 356 additions and 145 deletions
+1
View File
@@ -36,6 +36,7 @@ func TestChannelAndGameAnalyticsHTTP(t *testing.T) {
"INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,platform_busier_account,status) VALUES(9,'lx',14,'SDK账号','0'),(99,'hw',20,'其他账号','0')",
"CREATE TABLE tab_promote(id INTEGER,source_db TEXT,account TEXT,promote_level INTEGER,top_promote_id INTEGER,busier_id INTEGER,register_type INTEGER)",
"INSERT INTO tab_promote VALUES(100,'lx','root-channel',1,0,14,0),(101,'lx','child-channel',2,100,14,0),(200,'lx','guild-channel',1,0,14,1),(100,'hw','other-platform',1,0,20,0)",
"ALTER TABLE tab_promote ADD COLUMN nickname TEXT",
"CREATE TABLE tab_promote_apply(source_db TEXT,promote_id INTEGER,game_id INTEGER,status INTEGER)",
"INSERT INTO tab_promote_apply VALUES('lx',100,1,1),('hw',100,1,1),('lx',200,2,1)",
"CREATE TABLE tab_game(id INTEGER,source_db TEXT,game_name TEXT)",
+2 -1
View File
@@ -254,6 +254,7 @@ func sortAnalytics(rows []*AnalyticsRow, kind AnalyticsKind, field string, ascen
}
allowed := map[string]bool{"newRegisterCount": true, "activeCount": true, "payUserCount": true, "newPayUserCount": true, "totalPay": true, "payRate": true}
if kind == GameAnalytics {
allowed["gameId"] = true
allowed["arpu"] = true
allowed["arppu"] = true
} else {
@@ -277,7 +278,7 @@ func sortAnalytics(rows []*AnalyticsRow, kind AnalyticsKind, field string, ascen
n = big.NewRat(row.Counts.Pay, 1)
case "newPayUserCount":
n = big.NewRat(row.Counts.NewPay, 1)
case "promoteId":
case "promoteId", "gameId":
n = big.NewRat(row.ID, 1)
case "promoteType":
value := int64(0)
+10
View File
@@ -6,6 +6,16 @@ import (
"time"
)
func TestGameIDSortDoesNotFallBackToRechargeAmount(t *testing.T) {
rows := []*AnalyticsRow{{AnalyticsKey: AnalyticsKey{ID: 20, SourceDB: "lx"}, Pay: "1.00"}, {AnalyticsKey: AnalyticsKey{ID: 3, SourceDB: "lx"}, Pay: "900.00"}}
if err := sortAnalytics(rows, GameAnalytics, "gameId", true); err != nil || rows[0].ID != 3 {
t.Fatal("game ID ascending sort failed")
}
if err := sortAnalytics(rows, GameAnalytics, "gameId", false); err != nil || rows[0].ID != 20 {
t.Fatal("game ID descending sort fell back to recharge amount")
}
}
type analyticsRepoFake struct {
data *AnalyticsData
scope AnalyticsScope
+2
View File
@@ -142,6 +142,8 @@ func registrationSort(field string) string {
}
func spendSort(field string) string {
switch field {
case "id":
return "id"
case "payAmount", "pay_amount", "amount":
return "payAmount"
}
+3 -1
View File
@@ -3,6 +3,7 @@ package business
import (
"context"
"sort"
"strings"
"time"
"golang.org/x/sync/errgroup"
@@ -25,7 +26,8 @@ func analyticsPromotes(db *gorm.DB, scope business.AnalyticsScope, f business.An
if f.PromoteName == "" {
return q
}
hits := analyticsBasePromotes(db, scope, f, "hit").Select("1").Where("hit.account LIKE ?", "%"+f.PromoteName+"%").Where("hit.source_db=" + alias + ".source_db")
keyword := strings.ToLower(strings.TrimSpace(f.PromoteName))
hits := analyticsBasePromotes(db, scope, f, "hit").Select("1").Where("INSTR(LOWER(hit.account),?)>0 OR INSTR(LOWER(hit.nickname),?)>0", keyword, keyword).Where("hit.source_db=" + alias + ".source_db")
if scope.IncludeSub {
root := "CASE WHEN hit.promote_level=1 THEN hit.id WHEN hit.top_promote_id>0 THEN hit.top_promote_id ELSE hit.id END"
hits = hits.Where("(" + alias + ".id=(" + root + ") OR " + alias + ".top_promote_id=(" + root + "))")
+18
View File
@@ -102,6 +102,24 @@ func TestAnalyticsFiltersKeepHierarchyAndSnapshotNames(t *testing.T) {
t.Fatal("不能用当前游戏表名替代报表快照过滤")
}
}
func TestAnalyticsChannelSearchMatchesNicknameAndKeepsScope(t *testing.T) {
db, repo, scope, filter := prepareAnalyticsData(t)
if err := db.Exec("UPDATE tab_promote SET nickname='同名推广' WHERE id=100").Error; err != nil {
t.Fatal(err)
}
scope.Viewer = business.UserScope{SelfID: 9, OnlySelf: true}
filter.PromoteName = "推广"
data, err := repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
if err != nil || len(data.Rows) != 1 || data.Rows[0].SourceDB != "lx" || data.Rows[0].ID != 100 {
t.Fatalf("nickname search must remain scoped: %+v %v", data, err)
}
filter.PromoteName = "%"
data, err = repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
if err != nil || len(data.Rows) != 0 {
t.Fatal("wildcard keyword must not broaden the query")
}
}
func TestAnalyticsMissingScopeAndTablesDoNotBroaden(t *testing.T) {
db, repo, scope, filter := prepareAnalyticsData(t)
scope.IncludeSub = false
+2 -1
View File
@@ -86,7 +86,8 @@ func (r *channelRepo) List(ctx context.Context, scope business.UserScope, filter
q = q.Where("p.source_db IN ?", filter.Sources)
}
if filter.Keyword != "" {
q = q.Where("p.account LIKE ?", "%"+filter.Keyword+"%")
keyword := strings.ToLower(strings.TrimSpace(filter.Keyword))
q = q.Where("INSTR(LOWER(p.account),?)>0 OR INSTR(LOWER(p.nickname),?)>0 OR INSTR(LOWER(p.real_name),?)>0", keyword, keyword, keyword)
}
for _, field := range []struct {
column string
+17
View File
@@ -43,6 +43,23 @@ func prepareChannels(t *testing.T) (*gorm.DB, business.ChannelRepo) {
func channelTestScope() business.UserScope {
return business.UserScope{SelfID: 8, IncludeSelf: true, DescendantPath: "0,7,8"}
}
func TestChannelKeywordMatchesNameAndContactWithinScope(t *testing.T) {
db, repo := prepareChannels(t)
if err := db.Exec("UPDATE tab_promote SET nickname='同名公会', real_name='负责人甲' WHERE id=100").Error; err != nil {
t.Fatal(err)
}
for _, keyword := range []string{"公会", "负责人甲"} {
page, err := repo.List(context.Background(), business.UserScope{SelfID: 9, OnlySelf: true}, business.ChannelFilter{Keyword: keyword})
if err != nil || page.Total != 1 || len(page.Rows) != 1 || page.Rows[0].SourceDB != "lx" {
t.Fatalf("keyword must not escape the business scope: %q %+v %v", keyword, page, err)
}
}
page, err := repo.List(context.Background(), channelTestScope(), business.ChannelFilter{Keyword: "%"})
if err != nil || page.Total != 0 {
t.Fatal("keyword wildcards must be treated literally")
}
}
func TestChannelListMatchesBindingsAndLiteralBusinessKeyword(t *testing.T) {
db, repo := prepareChannels(t)
ctx := context.Background()
+2
View File
@@ -287,6 +287,8 @@ func (r *ledgerRepo) Spends(ctx context.Context, scope business.LedgerScope, fil
order := "r.pay_time"
if filter.SortField == "payAmount" {
order = "r.pay_amount"
} else if filter.SortField == "id" {
order = "r.id"
}
if filter.Ascending {
order += " ASC"
+21
View File
@@ -147,3 +147,24 @@ func TestLedgerChannelNicknameAndLiteralFuzzyFilter(t *testing.T) {
t.Fatal("模糊搜索不得扩大商务范围")
}
}
func TestSpendIDSortIsGlobalAndStableAcrossPages(t *testing.T) {
db, repo, scope, filter := prepareLedgerData(t)
if err := db.Exec("UPDATE tab_spend SET pay_time=1999 WHERE id=901").Error; err != nil {
t.Fatal(err)
}
filter.SortField, filter.Ascending = "id", true
first, err := repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Limit: 2})
if err != nil {
t.Fatal(err)
}
second, err := repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Offset: 2, Limit: 2})
if err != nil || len(first.Rows) != 2 || len(second.Rows) != 2 || first.Rows[1].ID >= second.Rows[0].ID {
t.Fatal("ascending sort must be applied before pagination")
}
filter.Ascending = false
last, err := repo.Spends(context.Background(), scope, filter, business.LedgerListOptions{Limit: 2})
if err != nil || last.Rows[0].ID != 909 || last.Rows[1].ID != 904 {
t.Fatalf("ID ordering must not fall back to timestamps: %+v %v", last, err)
}
}
+1
View File
@@ -7,6 +7,7 @@ import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 主题扩展样式(菜单风格/卡片盒子模式/圆角),置于 EP 基础样式之后以便覆盖
import '@/style/theme.scss'
import '@/style/business.scss'
// 引入kra前端初始化相关内容
import './core/kra'
// 引入封装的router
+47
View File
@@ -0,0 +1,47 @@
// Business views share a compact table rhythm and semantic emphasis.
// Scope these rules so framework administration tables retain their own layout.
.sw-page, .sw-dialog {
.el-table {
--el-table-header-bg-color: var(--el-fill-color-light);
--el-table-row-hover-bg-color: var(--el-color-primary-light-9);
--el-table-border-color: var(--el-border-color-lighter);
font-size: 13px;
th.el-table__cell { background: var(--el-table-header-bg-color); padding: 9px 0; }
th.el-table__cell .cell { line-height: 24px; font-weight: 600; color: var(--el-text-color-regular); }
td.el-table__cell { padding: 10px 0; }
td.el-table__cell .cell { line-height: 24px; }
.el-button.is-link, .el-tag { white-space: nowrap; }
.el-button.is-link { font-weight: 500; }
.el-tag { font-size: 12px; }
.el-scrollbar__bar.is-horizontal { height: 9px; opacity: 1; }
.el-scrollbar__thumb { background: var(--el-color-primary-light-3); }
.el-table__footer-wrapper td { background: var(--el-fill-color-light); font-weight: 600; }
.sw-col-meta .cell { color: var(--el-text-color-secondary); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.sw-col-identity .cell { font-weight: 500; }
.sw-col-number .cell { font-variant-numeric: tabular-nums; white-space: nowrap; }
}
.kra-search-box {
.el-form-item { margin-right: 20px; margin-bottom: 14px; }
.el-form-item__label { font-size: 13px; color: var(--el-text-color-secondary); }
}
.kra-pagination .el-pagination { margin-top: 18px; }
.sw-table-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.sw-sort-label { color: var(--el-text-color-secondary); font-size: 12px; }
.sw-table-summary { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding: 12px 14px; border: 1px solid var(--el-border-color-lighter); border-radius: 6px; background: var(--el-fill-color-light); color: var(--el-text-color-secondary); font-size: 13px; }
.sw-table-summary strong { color: var(--el-color-primary); font-variant-numeric: tabular-nums; font-size: 16px; }
.sw-empty-value { color: var(--el-text-color-placeholder); }
}
@media (max-width: 640px) {
.sw-page .kra-search-box {
.el-form-item { display: flex; flex-direction: column; width: 100%; margin-right: 0; }
.el-form-item__label { justify-content: flex-start; height: auto; line-height: 22px; padding-bottom: 4px; }
.el-form-item__content { width: 100%; min-width: 0; }
.el-form-item__content > .el-input,
.el-form-item__content > .el-select,
.el-form-item__content > .el-date-editor { width: 100% !important; }
}
}
@@ -1,5 +1,5 @@
<template>
<div class="business-dashboard">
<div class="business-dashboard sw-page">
<el-card shadow="never">
<div class="business-toolbar">
<slot name="heading"><strong>商务工作台</strong></slot>
+9 -7
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="sw-page">
<div class="alert-summary-grid">
<button v-for="(label, value) in statuses" :key="value" class="alert-summary-card" :class="{ active: query.status === Number(value) }" :aria-pressed="query.status === Number(value)" @click="selectStatus(Number(value))">
<span>{{ label }}</span><strong>{{ available ? (counts[value] || 0) : '未接入' }}</strong>
@@ -9,8 +9,9 @@
<el-form :model="query" :inline="true" @submit.prevent="search">
<el-form-item label="所属平台"><el-select v-model="platforms" multiple collapse-tags clearable placeholder="选择平台" style="width: 210px"><el-option v-for="(label, value) in platformOptions" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="规则名称"><el-input v-model="query.ruleName" placeholder="规则名称" clearable /></el-form-item>
<el-form-item label="预警对象"><el-input v-model="query.targetPromoteName" placeholder="渠道名称" clearable /></el-form-item>
<el-form-item label="规则类型"><el-select v-model="query.ruleType" clearable placeholder="全部类型" style="width: 160px"><el-option v-for="type in types" :key="type" :label="type" :value="type" /></el-select></el-form-item>
<el-form-item label="状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width: 150px"><el-option v-for="(label, value) in statuses" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item label="处理状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width: 150px"><el-option v-for="(label, value) in statuses" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item label="归属商务"><el-input v-model="query.businessName" clearable placeholder="归属商务" /></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">搜索</el-button></el-form-item>
</el-form>
@@ -18,15 +19,15 @@
<div class="kra-table-box">
<el-alert v-if="!available" class="mb-4" type="warning" :closable="false" title="渠道数据尚未接入,暂时无法核对预警可见范围。" />
<el-alert v-else-if="!isAdmin" class="mb-4" type="info" :closable="false" title="仅展示当前账号自身及全部下级商务渠道产生的预警;上级、平级其他商务渠道预警不可见。" />
<el-table v-loading="loading" :data="rows" row-key="id">
<el-table-column prop="platformName" label="所属平台" min-width="100" />
<el-table scrollbar-always-on v-loading="loading" :data="rows" row-key="id">
<el-table-column prop="platformName" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb || row.platformCode" :label="row.platformName" /></template></el-table-column>
<el-table-column label="触发时间" min-width="120"><template #default="{ row }">{{ shortTime(row.createdAt) }}</template></el-table-column>
<el-table-column prop="ruleName" label="规则名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="ruleType" label="规则类型" min-width="130" show-overflow-tooltip />
<el-table-column prop="targetPromoteName" label="预警对象" min-width="160" show-overflow-tooltip />
<el-table-column prop="businessName" label="归属商务" min-width="130" show-overflow-tooltip />
<el-table-column prop="alertContent" label="预警内容" min-width="280" show-overflow-tooltip />
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="statusTone(row.status)" effect="plain">{{ row.statusLabel || statuses[row.status] }}</el-tag></template></el-table-column>
<el-table-column label="处理状态" width="110"><template #default="{ row }"><el-tag :type="statusTone(row.status)" effect="plain">{{ row.statusLabel || statuses[row.status] }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="115" fixed="right"><template #default="{ row }"><el-button v-if="canQuery" link type="primary" @click="open(row)">{{ actionLabel(row.status) }}</el-button></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>
@@ -36,6 +37,7 @@
</template>
<script setup>
import PlatformTag from './components/PlatformTag.vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
@@ -54,7 +56,7 @@ const canAudit = computed(() => Number(user.userInfo.level) === 1 && (isAdmin.va
const platformOptions = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
const platforms = ref(Object.keys(platformOptions))
const types = ref([])
const query = reactive({ pageNum: 1, pageSize: 10, ruleName: '', ruleType: '', status: undefined, businessName: '' })
const query = reactive({ pageNum: 1, pageSize: 10, ruleName: '', targetPromoteName: '', ruleType: '', status: undefined, businessName: '' })
const rows = ref([])
const total = ref(0)
const counts = ref({})
@@ -69,7 +71,7 @@ const load = async () => {
if (!platforms.value.length) { clear(); available.value = true; loading.value = false; return }
loading.value = true
try {
const res = await listAlertRecords({ ...query, ruleName: query.ruleName.trim(), businessName: query.businessName.trim(), platformCodes: platforms.value.join(',') })
const res = await listAlertRecords({ ...query, ruleName: query.ruleName.trim(), targetPromoteName: query.targetPromoteName.trim(), businessName: query.businessName.trim(), platformCodes: platforms.value.join(',') })
if (version !== requestVersion) return
if (res.code !== 0) { clear(); return }
rows.value = res.data.pageData.rows
+3 -3
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="sw-page">
<div class="kra-search-box">
<el-form :inline="true" :model="query" @submit.prevent="search">
<el-form-item label="监控指标"><el-select v-model="query.monitorMetric" clearable placeholder="全部监控指标" style="width: 160px"><el-option v-for="(label, value) in metrics" :key="value" :value="Number(value)" :label="label" /></el-select></el-form-item>
@@ -10,7 +10,7 @@
<div class="kra-table-box">
<div class="kra-btn-list"><el-button v-if="canCreate" type="primary" icon="plus" @click="open()">新建规则</el-button></div>
<el-alert class="mb-4" type="info" :closable="false" title="同一平台、同一渠道、同类预警未关闭时不重复新建。未处理预警再次命中会刷新;处理中、待审核和审核驳回的记录保持不变。" />
<el-table v-loading="loading" :data="rows" row-key="id">
<el-table scrollbar-always-on v-loading="loading" :data="rows" row-key="id">
<el-table-column label="启用状态" width="110"><template #default="{ row }"><el-switch :model-value="row.enabled" :active-value="1" :inactive-value="0" :disabled="!canEdit || busy === row.id" @change="toggle(row, $event)" /></template></el-table-column>
<el-table-column prop="ruleName" label="规则名称" min-width="150" />
<el-table-column label="规则类型" min-width="130"><template #default="{ row }"><el-tag effect="plain" :style="{ color: row.warningColor, borderColor: row.warningColor }">{{ row.ruleTypeLabel }}</el-tag></template></el-table-column>
@@ -21,7 +21,7 @@
</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>
<el-drawer v-model="drawer" append-to-body :title="form.id ? '编辑规则' : '新建规则'" size="480px" destroy-on-close>
<el-drawer class="sw-dialog" v-model="drawer" append-to-body :title="form.id ? '编辑规则' : '新建规则'" size="480px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item label="规则名称" prop="ruleName"><el-input v-model="form.ruleName" maxlength="50" placeholder="请输入规则名称" /></el-form-item>
<el-form-item label="规则类型" prop="ruleType"><el-input v-model="form.ruleType" maxlength="20" placeholder="如:注册异常 / 充值异常 / 登录异常" /></el-form-item>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="performance-page">
<div class="performance-page sw-page">
<div class="kra-search-box"><el-form inline @submit.prevent="search">
<el-form-item label="年份"><el-select v-model="query.year" style="width:110px"><el-option v-for="year in years" :key="year" :label="year" :value="year" /></el-select></el-form-item>
<el-form-item label="月份区间"><el-select v-model="query.beginMonth" style="width:95px"><el-option v-for="month in 12" :key="month" :label="`${month} 月`" :value="month" /></el-select><span class="mx-2">至</span><el-select v-model="query.endMonth" style="width:95px"><el-option v-for="month in 12" :key="month" :label="`${month} 月`" :value="month" /></el-select></el-form-item>
@@ -42,7 +42,7 @@
</table>
</performance-scroll>
</template>
<div v-if="tab === 'trend' && data" class="trend-layout"><performance-trend :periods="data?.periods || []" :trend="data?.trend || {}" /><div><el-text type="info">共 {{ descendingPeriods.length }} {{ data?.granularity === 'day' ? '天,表内可滚动' : '个月' }}</el-text><el-table :data="trendRows" height="421" class="mt-2"><el-table-column prop="label" :label="data?.granularity === 'day' ? '日期' : '月份'" width="90" /><el-table-column v-for="metric in metrics" :key="metric.key" :label="metric.label" min-width="100"><template #default="{ row }">{{ displayMetric(metric.key, row.values[metric.key]) }}</template></el-table-column><el-table-column label="有效率" width="95"><template #default="{ row }">{{ rateText(effectiveRate(row.values)) }}</template></el-table-column><el-table-column v-for="metric in metrics" :key="`${metric.key}-change`" :label="`${metric.label}环比`" min-width="115"><template #default="{ row }">{{ row.changes[metric.key] }}</template></el-table-column></el-table></div></div>
<div v-if="tab === 'trend' && data" class="trend-layout"><performance-trend :periods="data?.periods || []" :trend="data?.trend || {}" /><div><el-text type="info">共 {{ descendingPeriods.length }} {{ data?.granularity === 'day' ? '天,表内可滚动' : '个月' }}</el-text><el-table scrollbar-always-on :data="trendRows" height="421" class="mt-2"><el-table-column prop="label" :label="data?.granularity === 'day' ? '日期' : '月份'" width="90" /><el-table-column v-for="metric in metrics" :key="metric.key" :label="metric.label" min-width="100"><template #default="{ row }">{{ displayMetric(metric.key, row.values[metric.key]) }}</template></el-table-column><el-table-column label="有效率" width="95"><template #default="{ row }">{{ rateText(effectiveRate(row.values)) }}</template></el-table-column><el-table-column v-for="metric in metrics" :key="`${metric.key}-change`" :label="`${metric.label}环比`" min-width="115"><template #default="{ row }">{{ row.changes[metric.key] }}</template></el-table-column></el-table></div></div>
</div>
<performance-detail-drawer v-model="detailOpen" :query="detailQuery" :title="detailTitle" />
</div>
+15 -13
View File
@@ -1,9 +1,9 @@
<template>
<div>
<div class="sw-page">
<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.promoteAccount" clearable placeholder="渠道名称 / 账号" /></el-form-item>
<el-form-item label="绑定商务"><el-input v-model="query.busierAccount" clearable placeholder="绑定商务" /></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>
@@ -11,19 +11,19 @@
<div class="kra-table-box">
<el-alert v-if="error" class="mb-4" type="error" :closable="false" :title="error" />
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="OA 渠道激活数据源尚未接入或暂不可用。" description="渠道激活记录来自 OA;商务库中的渠道数据不会自动生成激活记录。请联系管理员检查 OA 数据源连接。" />
<el-table v-loading="loading" :data="rows" row-key="id" :empty-text="emptyText">
<el-table-column prop="platformTitle" label="所属平台" min-width="100" />
<el-table scrollbar-always-on v-loading="loading" :data="rows" row-key="id" :empty-text="emptyText">
<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"><template #default="{ row }">{{ date(row.bindAt) }}</template></el-table-column>
<el-table-column label="解绑时间" min-width="120"><template #default="{ row }">{{ date(row.unbindAt) }}</template></el-table-column>
<el-table-column label="累计流水金额" min-width="160"><template #default="{ row }">{{ currency(row.accumAmount) }}</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="绑定日期" min-width="120" class-name="sw-col-meta"><template #default="{ row }">{{ date(row.bindAt) }}</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 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>
<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>
<el-dialog v-model="dialog" title="流水明细" width="min(640px, 95vw)" append-to-body destroy-on-close @closed="clearDetail">
<el-dialog class="sw-dialog" v-model="dialog" title="流水明细" width="min(640px, 95vw)" append-to-body destroy-on-close @closed="clearDetail">
<div v-loading="flowLoading">
<el-descriptions :column="1">
<el-descriptions-item label="所属平台">{{ selected?.platformTitle || '—' }}</el-descriptions-item>
@@ -33,12 +33,12 @@
<el-alert v-if="flowError" type="warning" :title="flowError" :closable="false" />
<template v-else-if="detail">
<el-alert class="mb-4" type="info" title="仅统计绑定期间产生的流水。" :closable="false" />
<el-table :data="detail.list" max-height="320">
<el-table-column prop="platformTitle" label="所属平台" min-width="100" />
<el-table scrollbar-always-on :data="detail.list" max-height="320">
<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="gameName" label="游戏名称" min-width="160" show-overflow-tooltip />
<el-table-column label="累计金额" min-width="150" align="right"><template #default="{ row }">{{ currency(row.totalAmount) }}</template></el-table-column>
<el-table-column label="累计金额" min-width="150" align="right"><template #default="{ row }"><metric-value kind="money" :value="row.totalAmount" /></template></el-table-column>
</el-table>
<p class="mt-4 text-right">绑定期间累计流水:<strong>{{ currency(detail.totalAmount) }}</strong></p>
<p class="sw-table-summary">绑定期间累计流水:<strong>{{ currency(detail.totalAmount) }}</strong></p>
</template>
</div>
<template #footer><el-button @click="dialog = false">关闭</el-button></template>
@@ -47,6 +47,8 @@
</template>
<script setup>
import PlatformTag from './components/PlatformTag.vue'
import MetricValue from './components/MetricValue.vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { listChannelActivations, getActivationSpend } from '@/api/activation'
+20 -17
View File
@@ -1,13 +1,13 @@
<template>
<div>
<div class="sw-page">
<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-input v-model="query.busierAccount" clearable placeholder="新商务 / SDK 商务账号" /></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 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 label="有效端"><el-select v-model="query.effective" clearable placeholder="全部" style="width:120px"><el-option label="是" :value="true" /><el-option label="否" :value="false" /></el-select></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">搜索</el-button></el-form-item>
</el-form>
@@ -15,22 +15,22 @@
<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 scrollbar-always-on v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.id}`">
<el-table-column prop="platformName" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb" :label="row.platformName" /></template></el-table-column>
<el-table-column prop="id" label="ID" min-width="90" class-name="sw-col-meta" />
<el-table-column prop="account" label="渠道名称" class-name="sw-col-identity" min-width="150" show-overflow-tooltip />
<el-table-column label="渠道层级" min-width="110"><template #default="{ row }"><level-tag :level="row.promoteLevel" channel /></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="100"><template #default="{ row }"><el-tag v-if="row.registerType === 0 || row.registerType === 1" size="small" :type="row.registerType === 1 ? 'warning' : 'info'" effect="plain">{{ row.registerType === 1 ? '公会' : '个人' }}</el-tag><span v-else class="sw-empty-value">—</span></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="145"><template #default="{ row }">{{ dateText(row.createdAt) }}</template></el-table-column>
<el-table-column label="有效端" width="100"><template #default="{ row }"><el-tag :type="row.effective ? 'success' : 'info'">{{ row.effective ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="达标时间" min-width="145"><template #default="{ row }">{{ dateText(row.qualifiedAt) }}</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-column label="玩家数量" min-width="110" align="right" class-name="sw-col-number"><template #default="{ row }"><metric-value :value="row.playerCount" /></template></el-table-column>
<el-table-column label="累计充值" min-width="150" align="right" class-name="sw-col-number"><template #default="{ row }"><metric-value kind="money" :value="row.totalPayAmount" /></template></el-table-column>
<el-table-column label="注册时间" min-width="120" class-name="sw-col-meta"><template #default="{ row }">{{ dateText(row.createdAt) }}</template></el-table-column>
<el-table-column label="有效端" width="100"><template #default="{ row }"><el-tag size="small" :type="row.effective ? 'success' : 'info'">{{ row.effective ? '是' : '否' }}</el-tag></template></el-table-column>
<el-table-column label="达标时间" min-width="120" class-name="sw-col-meta"><template #default="{ row }">{{ dateText(row.qualifiedAt) }}</template></el-table-column>
<el-table-column label="锁定状态" min-width="110"><template #default="{ row }"><el-tag size="small" :type="{ 0: 'success', '-1': 'danger', '-2': 'info' }[row.lockStatus] || 'info'" effect="plain">{{ statuses[row.lockStatus] ?? row.lockStatus ?? '—' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openProfile(row)">画像</el-button></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>
@@ -47,7 +47,10 @@ 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 { channelCount } from './channel-display'
import PlatformTag from './components/PlatformTag.vue'
import LevelTag from './components/LevelTag.vue'
import MetricValue from './components/MetricValue.vue'
import { dateText } from './performance-display'
import AddChannelDrawer from './components/AddChannelDrawer.vue'
import ChannelGameDrawer from './components/ChannelGameDrawer.vue'
+2 -2
View File
@@ -1,11 +1,11 @@
<template>
<div>
<div class="sw-page">
<div class="kra-search-box">
<el-form :inline="true" @submit.prevent="load">
<el-form-item label="所属平台"><el-select v-model="sources" multiple collapse-tags clearable placeholder="全部平台" style="width: 240px" @change="load"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item><el-radio-group :model-value="dimension" @change="changeDimension"><el-radio-button v-for="(label, value) in dimensions" :key="value" :value="value">{{ label }}排行</el-radio-button></el-radio-group></el-form-item>
<el-form-item><el-radio-group v-model="memory[dimension].range" @change="changeRange"><el-radio-button v-for="item in ranges" :key="item.value" :value="item.value">{{ item.label }}</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="memory[dimension].dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" style="width: 300px" @change="customDate" /></el-form-item>
<el-form-item label="统计时间"><el-date-picker v-model="memory[dimension].dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" style="width: 300px" @change="customDate" /></el-form-item>
</el-form>
<el-text type="info">统计周期:{{ dates.join(' 至 ') }}</el-text>
</div>
+14 -10
View File
@@ -1,11 +1,11 @@
<template>
<div>
<div class="sw-page">
<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" @change="sourceChanged"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="游戏"><el-select v-model="selectedGame" filterable :reserve-keyword="false" :loading="gameLoading" :disabled="!sources.length" placeholder="请选择游戏" style="width: 200px" @change="gameChanged"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item label="游戏名称"><el-select v-model="selectedGame" filterable :reserve-keyword="false" :loading="gameLoading" :disabled="!sources.length" placeholder="请选择游戏" style="width: 200px" @change="gameChanged"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item label="区服"><el-select v-model="query.serverNum" clearable filterable :loading="serverLoading" :disabled="!sources.length || !games.length" placeholder="全部区服" style="width: 200px"><el-option label="全部区服" value="" /><el-option v-for="row in serverOptions" :key="row.serverNum" :label="row.serverName ? `${row.serverName} (${row.serverNum})` : row.serverNum" :value="row.serverNum" /></el-select></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item label="统计时间"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">搜索</el-button></el-form-item>
</el-form>
</div>
@@ -13,12 +13,13 @@
<div class="server-heading"><strong>区服数据</strong><el-text type="info">统计周期:{{ period }}</el-text></div>
<el-alert v-if="error || optionError" class="mb-4" type="error" :closable="false" :title="error || optionError" />
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="区服统计所需业务汇总尚未接入。" />
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.gameId}:${row.serverNum}`" show-summary :summary-method="summaries" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<div class="sw-table-toolbar"><span class="sw-sort-label" role="status">{{ currentSort }}</span></div>
<el-table scrollbar-always-on v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.gameId}:${row.serverNum}`" show-summary :summary-method="summaries" :default-sort="{ prop: 'totalPay', order: 'descending' }" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb" :label="row.sourceDbLabel" /></template></el-table-column>
<el-table-column prop="serverNum" label="区服编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="serverName" label="区服名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="openTime" label="开服日期" min-width="120" sortable="custom"><template #default="{ row }">{{ openDate(row.openTime) }}</template></el-table-column>
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.money ? 140 : 110" sortable="custom"><template #header><el-tooltip :content="metricTips[column.key]" placement="top"><span>{{ column.label }}</span></el-tooltip></template><template #default="{ row }">{{ format(column, row[column.key]) }}</template></el-table-column>
<el-table-column prop="openTime" label="开服日期" min-width="120" sortable="custom" class-name="sw-col-meta"><template #default="{ row }">{{ openDate(row.openTime) }}</template></el-table-column>
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.money ? 140 : 110" sortable="custom" align="right" class-name="sw-col-number"><template #header><el-tooltip :content="metricTips[column.key]" placement="top"><span>{{ column.label }}</span></el-tooltip></template><template #default="{ row }"><metric-value :kind="column.money ? 'money' : 'count'" :value="row[column.key]" /></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, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
@@ -31,7 +32,9 @@ import { ElMessage } from 'element-plus'
import { listLedgerGames } from '@/api/ledger'
import { listServerAnalysis, listServerOptions } from '@/api/serverAnalysis'
import { channelCount, channelMoney } from './channel-display'
import { metricTips } from './table-display'
import PlatformTag from './components/PlatformTag.vue'
import MetricValue from './components/MetricValue.vue'
import { metricTips, remoteSort, sortDescription } from './table-display'
import { analysisGameSelection } from './analysis-game-selection'
defineOptions({ name: 'ServerAnalysis' })
@@ -48,7 +51,7 @@ const shortcuts = [
]
const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), serverOptions = ref([])
const selectedGame = computed({ get: () => games.value[0] || '', set: value => { games.value = value ? [value] : [] } })
const query = reactive({ pageNum: 1, pageSize: 10, serverNum: '', sortField: '', sortOrder: 'desc' })
const query = reactive({ pageNum: 1, pageSize: 10, serverNum: '', sortField: 'totalPay', sortOrder: 'desc' })
const rows = ref([]), total = ref(0), summary = ref(null), result = ref(null), loading = ref(false), available = ref(true), error = ref(''), optionError = ref('')
const gameLoading = ref(false), serverLoading = ref(false)
let version = 0, gameVersion = 0, serverVersion = 0
@@ -83,7 +86,8 @@ const search = () => {
else if (!games.value.length) ElMessage.warning('请选择游戏')
return load()
}
const sortChanged = ({ prop, order }) => { query.pageNum = 1; query.sortField = order ? prop : ''; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return load() }
const sortChanged = event => { query.pageNum = 1; Object.assign(query, remoteSort(event, 'totalPay')); return load() }
const currentSort = computed(() => sortDescription(query, { ...Object.fromEntries(columns.map(c => [c.key, c.label])), openTime: '开服日期' }))
const loadGames = async () => {
const current = ++gameVersion; gameLoading.value = true; optionError.value = ''
try {
@@ -1,5 +1,5 @@
<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-drawer class="sw-dialog" 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>
@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="visible" append-to-body destroy-on-close title="预警处理 / 详情" size="min(560px, 100vw)" :close-on-click-modal="!busy" :close-on-press-escape="!busy" :show-close="!busy" @closed="reset">
<el-drawer class="sw-dialog" v-model="visible" append-to-body destroy-on-close title="预警处理 / 详情" size="min(560px, 100vw)" :close-on-click-modal="!busy" :close-on-press-escape="!busy" :show-close="!busy" @closed="reset">
<div v-loading="loading" tabindex="0" @paste="pasteImages">
<template v-if="current">
<h3 class="mb-3">渠道:{{ current.targetPromoteName.replace(/^渠道[::]\s*/, '') || '—' }} — {{ current.ruleName || '预警' }}</h3>
+12 -11
View File
@@ -1,23 +1,23 @@
<template>
<div>
<div class="sw-page">
<div v-if="!userId" class="kra-search-box">
<el-form :inline="true" :model="query" @submit.prevent="search">
<el-form-item label="所属平台"><el-select v-model="query.sourceDb" multiple clearable placeholder="全部平台" style="width: 220px"><el-option v-for="(label, value) in platforms" :key="value" :value="value" :label="label" /></el-select></el-form-item>
<el-form-item label="游戏商务账号"><el-input v-model="query.platformBusierAccount" clearable /></el-form-item>
<el-form-item label="SDK商务账号"><el-input v-model="query.platformBusierAccount" clearable /></el-form-item>
<el-form-item label="审核状态"><el-select v-model="query.status" clearable style="width: 130px"><el-option v-for="(label, value) in statuses" :key="value" :value="value" :label="label" /></el-select></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">查询</el-button><el-button icon="refresh" @click="reset">重置</el-button></el-form-item>
</el-form>
</div>
<div class="kra-table-box">
<div v-if="!userId && canBind" class="kra-btn-list"><el-button type="primary" icon="plus" @click="openBind">绑定游戏商务</el-button></div>
<el-table v-loading="loading" :data="rows" :row-key="rowKey">
<el-table-column label="所属平台" min-width="80"><template #default="{ row }"><el-button v-if="userId && canPortrait && row.status === '0'" type="primary" link @click="showPortrait(row)">{{ row.platformName }}</el-button><span v-else>{{ row.platformName }}</span></template></el-table-column>
<el-table scrollbar-always-on v-loading="loading" :data="rows" :row-key="rowKey">
<el-table-column label="所属平台" min-width="80"><template #default="{ row }"><el-button v-if="userId && canPortrait && row.status === '0'" type="primary" link @click="showPortrait(row)">{{ row.platformName }}</el-button><platform-tag v-else :code="row.sourceDb" :label="row.platformName" /></template></el-table-column>
<el-table-column v-if="userId" prop="platformBusierId" label="ID" min-width="90" />
<el-table-column prop="platformBusierAccount" label="游戏商务账号" min-width="140" />
<el-table-column prop="platformBusierAccount" label="SDK商务账号" class-name="sw-col-identity" min-width="140" />
<el-table-column v-if="!userId" prop="bindUserName" label="绑定商务" min-width="140" />
<el-table-column v-if="!userId" label="旗下渠道数" min-width="110"><template #default="{ row }"><span v-if="row.promoteCount === null" class="text-gray-400">未接入</span><el-button v-else type="primary" link aria-label="查看旗下渠道" :disabled="row.status !== '0'" @click="showChannels(row)">{{ row.promoteCount }}</el-button></template></el-table-column>
<el-table-column label="审核状态" width="110"><template #default="{ row }"><el-tag :type="row.status === '0' ? 'success' : row.status === '2' ? 'danger' : 'warning'">{{ statuses[row.status] || row.status }}</el-tag></template></el-table-column>
<el-table-column prop="createTime" label="绑定时间" min-width="130"><template #default="{ row }">{{ row.createTime?.slice(0, 10) }}</template></el-table-column>
<el-table-column label="审核状态" width="110"><template #default="{ row }"><el-tag size="small" :type="row.status === '0' ? 'success' : row.status === '2' ? 'danger' : 'warning'">{{ statuses[row.status] || row.status }}</el-tag></template></el-table-column>
<el-table-column prop="createTime" label="绑定时间" class-name="sw-col-meta" min-width="130"><template #default="{ row }">{{ row.createTime?.slice(0, 10) }}</template></el-table-column>
<el-table-column v-if="!userId" label="操作" width="145" fixed="right">
<template #default="{ row }">
<template v-if="row.status === '0'"><el-button v-if="canPortrait" link type="primary" @click="showPortrait(row)">画像</el-button><el-button v-if="canBind && row.userId === user.userInfo.ID" link type="danger" @click="remove(row)">解绑</el-button></template>
@@ -28,14 +28,14 @@
</el-table>
<div v-if="!userId" class="kra-pagination"><el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
<el-drawer append-to-body v-model="channelsDrawer" title="旗下渠道" size="520px" destroy-on-close><business-channels v-if="selectedChannel" :user-id="selectedChannel.userId" :source-db="selectedChannel.sourceDb" :platform-busier-id="selectedChannel.platformBusierId" /></el-drawer>
<el-drawer class="sw-dialog" append-to-body v-model="channelsDrawer" title="旗下渠道" size="520px" destroy-on-close><business-channels v-if="selectedChannel" :user-id="selectedChannel.userId" :source-db="selectedChannel.sourceDb" :platform-busier-id="selectedChannel.platformBusierId" /></el-drawer>
<business-profile-drawer v-model="portraitOpen" :user-id="portraitRow?.userId || userId" :source-db="portraitRow?.sourceDb || ''" :platform-busier-id="portraitRow?.platformBusierId || 0" :name="portraitRow?.platformBusierAccount || ''" />
<el-drawer append-to-body v-model="dialog" title="绑定游戏商务" size="460px" destroy-on-close>
<el-alert title="验证成功后提交 OA 审核,审核通过后可查看对应渠道数据。" type="info" :closable="false" class="mb-4" />
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item label="所属平台" prop="sourceDb"><el-select v-model="form.sourceDb"><el-option v-for="(label, value) in platforms" :key="value" :value="value" :label="label" /></el-select></el-form-item>
<el-form-item label="游戏商务账号" prop="account"><el-input v-model="form.account" maxlength="30" autocomplete="off" /></el-form-item>
<el-form-item label="游戏商务密码" prop="password"><el-input v-model="form.password" type="password" show-password autocomplete="new-password" /></el-form-item>
<el-form-item label="SDK商务账号" prop="account"><el-input v-model="form.account" maxlength="30" autocomplete="off" /></el-form-item>
<el-form-item label="SDK商务密码" prop="password"><el-input v-model="form.password" type="password" show-password autocomplete="new-password" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="submit">验证并提交</el-button></template>
</el-drawer>
@@ -43,7 +43,7 @@
<el-descriptions v-if="selectedRow" :column="1" border>
<el-descriptions-item label="申请商务">{{ selectedRow.bindUserName || '—' }}</el-descriptions-item>
<el-descriptions-item label="所属平台">{{ selectedRow.platformName }}</el-descriptions-item>
<el-descriptions-item label="游戏商务账号">{{ selectedRow.platformBusierAccount }}</el-descriptions-item>
<el-descriptions-item label="SDK商务账号">{{ selectedRow.platformBusierAccount }}</el-descriptions-item>
<el-descriptions-item label="当前状态">{{ statuses[selectedRow.status] }}</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ selectedRow.createTime || '—' }}</el-descriptions-item>
<el-descriptions-item label="审核时间">{{ selectedRow.reviewTime || '—' }}</el-descriptions-item>
@@ -61,6 +61,7 @@
</template>
<script setup>
import PlatformTag from './PlatformTag.vue'
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
@@ -1,11 +1,11 @@
<template>
<div v-loading="loading">
<div v-loading="loading" class="sw-page">
<el-empty v-if="!available" description="当前数据库尚未接入渠道数据" />
<template v-else>
<el-table :data="rows" :row-key="(row) => `${row.sourceDb}:${row.promoteId}`">
<el-table scrollbar-always-on :data="rows" :row-key="(row) => `${row.sourceDb}:${row.promoteId}`">
<el-table-column type="index" label="序号" width="70" />
<el-table-column prop="promoteName" label="渠道名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="platformName" label="所属平台" min-width="110" />
<el-table-column prop="platformName" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb" :label="row.platformName" /></template></el-table-column>
</el-table>
<div v-if="total" class="kra-pagination"><el-pagination v-model:current-page="page" :page-size="10" :total="total" layout="total, prev, pager, next" @current-change="load" /></div>
</template>
@@ -13,6 +13,7 @@
</template>
<script setup>
import PlatformTag from './PlatformTag.vue'
import { ref, watch } from 'vue'
import { listBusinessChannels } from '@/api/business'
@@ -1,14 +1,14 @@
<template>
<div>
<div class="sw-page">
<el-tabs v-model="tab"><el-tab-pane label="商务列表" name="accounts" /><el-tab-pane label="小组列表" name="groups" /><el-tab-pane label="绑定列表" name="bindings" /></el-tabs>
<business-bindings v-if="tab === 'bindings'" @changed="refresh" />
<template v-else>
<div class="kra-search-box">
<el-form inline @submit.prevent="search">
<template v-if="tab === 'accounts'">
<el-form-item label="商务名称"><el-input v-model="filters.name" clearable placeholder="商务名称或负责人" /></el-form-item>
<el-form-item label="商务名称"><el-input v-model="filters.name" clearable placeholder="商务名称 / 负责人" /></el-form-item>
<el-form-item label="商务账号"><el-input v-model="filters.account" clearable /></el-form-item>
<el-form-item label="等级"><el-select v-model="filters.level" clearable style="width:130px"><el-option v-for="(label, value) in businessLevels" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item label="商务等级"><el-select v-model="filters.level" clearable style="width:130px"><el-option v-for="(label, value) in businessLevels" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item label="小组"><el-select v-model="filters.group" clearable style="width:180px"><el-option label="未分组" value="0" /><el-option v-for="group in catalog.groups" :key="group.id" :label="`${group.name}(${group.memberIds.length} 人)`" :value="String(group.id)" /></el-select></el-form-item>
<el-form-item label="在职状态"><el-select v-model="filters.employment" clearable style="width:130px"><el-option v-for="(label, value) in employmentLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item><el-button type="primary" native-type="submit">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
@@ -23,28 +23,28 @@
<div class="kra-table-box">
<div class="kra-btn-list"><el-button v-if="tab === 'accounts' && canAdd" type="primary" @click="openCreate">添加子商务</el-button><el-button v-if="catalog.canManage" plain type="primary" @click="teamSettings = true">小组设置</el-button></div>
<el-alert v-if="error || metricError" :title="error || metricError" type="error" :closable="false" class="mb-3" />
<el-table v-if="tab === 'accounts'" v-loading="loading" :data="pagedMembers" row-key="userId" @sort-change="sortMembers">
<el-table-column prop="userId" label="ID" width="75" />
<el-table-column prop="userName" label="商务账号" min-width="130" />
<el-table-column label="等级" width="105"><template #default="{ row }">{{ businessLevels[row.level] }}</template></el-table-column>
<el-table-column prop="parentUserName" label="上级商务" min-width="120" />
<el-table-column label="商务 / 负责人" min-width="150"><template #default="{ row }"><strong>{{ displayMember(row) }}</strong><div class="muted">{{ row.realName || '—' }}</div></template></el-table-column>
<el-table scrollbar-always-on v-if="tab === 'accounts'" v-loading="loading" :data="pagedMembers" row-key="userId" @sort-change="sortMembers">
<el-table-column prop="userId" label="ID" width="80" class-name="sw-col-meta" />
<el-table-column prop="userName" label="商务账号" class-name="sw-col-identity" min-width="130" show-overflow-tooltip />
<el-table-column label="商务等级" width="105"><template #default="{ row }"><level-tag :level="row.level" /></template></el-table-column>
<el-table-column prop="parentUserName" label="上级商务" min-width="120" show-overflow-tooltip />
<el-table-column label="商务名称 / 负责人" min-width="150"><template #default="{ row }"><strong class="sw-business-name" :title="displayMember(row)">{{ displayMember(row) }}</strong><div class="muted">{{ row.realName || '—' }}</div></template></el-table-column>
<el-table-column label="SDK商务" width="100"><template #default="{ row }"><el-button v-if="row.bindCount" link type="primary" @click="showBindings(row)">{{ row.bindCount }}</el-button><span v-else class="muted">0</span></template></el-table-column>
<el-table-column label="旗下渠道数" width="115"><template #default="{ row }"><el-button v-if="row.promoteCount != null" link type="primary" @click="showChannels(row)">{{ row.promoteCount }}</el-button><span v-else>未接入</span></template></el-table-column>
<el-table-column v-for="metric in ['open', 'effective']" :key="metric" :label="metric === 'open' ? '开端数' : '有效端数'" width="105"><template #header><el-tooltip :content="periodTip"><span>{{ metric === 'open' ? '开端数' : '有效端数' }}</span></el-tooltip></template><template #default="{ row }"><el-button v-if="row.metrics?.[metric]" link :class="metric" @click="details(row, metric)">{{ row.metrics[metric] }}</el-button><span v-else :class="metric">{{ metricLoading ? '…' : row.metrics ? 0 : '—' }}</span></template></el-table-column>
<el-table-column label="有效率" width="90"><template #default="{ row }"><span :class="row.rate != null && Number(row.rate) < 50 ? 'low-rate' : 'effective'">{{ row.rate == null ? '—' : `${row.rate}%` }}</span></template></el-table-column>
<el-table-column prop="hireDate" label="入职日期" width="115"><template #default="{ row }">{{ row.hireDate || '—' }}</template></el-table-column>
<el-table-column prop="hireDate" label="入职日期" width="115" class-name="sw-col-meta"><template #default="{ row }">{{ row.hireDate || '—' }}</template></el-table-column>
<el-table-column prop="groupName" label="小组" width="125" sortable="custom"><template #default="{ row }">{{ row.groupName || '—' }}</template></el-table-column>
<el-table-column label="在职状态" width="100"><template #default="{ row }"><el-tag :type="employmentTypes[row.employment]">{{ employmentLabels[row.employment] }}</el-tag></template></el-table-column>
<el-table-column label="在职状态" width="100"><template #default="{ row }"><el-tag size="small" :type="employmentTypes[row.employment]">{{ employmentLabels[row.employment] }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="195" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="showPortrait(row)">画像</el-button><el-button v-if="row.canEdit" link type="primary" @click="openEdit(row)">编辑</el-button><el-button v-if="row.canSetGoal" link type="primary" @click="openGoal('person', row.userId, displayMember(row))">设目标</el-button></template></el-table-column>
</el-table>
<el-table v-else v-loading="loading" :data="groupRows" row-key="id" @sort-change="sortGroups">
<el-table scrollbar-always-on v-else v-loading="loading" :data="groupRows" row-key="id" @sort-change="sortGroups">
<el-table-column prop="name" label="小组" min-width="145" sortable="custom"><template #default="{ row }">{{ row.name }} <el-tag v-if="row.mine" size="small">本组</el-tag></template></el-table-column>
<el-table-column prop="relation" label="与我的关系" min-width="145" />
<el-table-column prop="memberCount" label="成员数" width="110" sortable="custom" />
<el-table-column label="成员" min-width="230"><template #default="{ row }"><span :title="row.memberNames.join('、')">{{ row.memberNames.slice(0, 4).join('、') }}{{ row.memberCount > 4 ? ` 等 ${row.memberCount} 人` : '' }}</span></template></el-table-column>
<el-table-column label="在职状态" min-width="170"><template #default="{ row }">{{ Object.entries(row.employments).map(([key, value]) => `${employmentLabels[key]} ${value}`).join(' · ') }}</template></el-table-column>
<el-table-column prop="channelCount" label="旗下渠道数" width="135" sortable="custom" />
<el-table-column prop="channelCount" label="旗下渠道数" width="135" sortable="custom" align="right"><template #default="{ row }"><metric-value :value="row.channelCount" /></template></el-table-column>
<el-table-column label="操作" width="180"><template #default="{ row }"><el-button v-if="row.canSetGoal" link type="primary" @click="openGoal('group', row.id, row.name)">设目标</el-button><el-button link type="primary" @click="viewMembers(row)">查看成员</el-button></template></el-table-column>
</el-table>
<div v-if="tab === 'accounts'" class="kra-pagination"><el-pagination v-model:current-page="page" v-model:page-size="size" :total="filteredMembers.length" :page-sizes="[10,20,50,100]" layout="total, sizes, prev, pager, next" /></div>
@@ -53,7 +53,7 @@
<team-settings-dialog v-model="teamSettings" :catalog="catalog" @saved="teamsSaved" />
<goal-drawer v-model="goalOpen" :target="goalTarget" @saved="loadMetrics" />
<performance-detail-drawer v-model="detailOpen" :query="detailQuery" :title="detailTitle" />
<el-drawer v-model="channelsOpen" append-to-body title="旗下渠道" size="520px" destroy-on-close><business-channels :user-id="selectedID" /></el-drawer>
<el-drawer class="sw-dialog" v-model="channelsOpen" append-to-body title="旗下渠道" size="520px" destroy-on-close><business-channels :user-id="selectedID" /></el-drawer>
<el-drawer v-model="bindingsOpen" append-to-body title="SDK商务绑定详情" size="85%" destroy-on-close><business-bindings :user-id="selectedID" /></el-drawer>
<business-profile-drawer v-model="portraitOpen" :user-id="selectedID" :name="selectedName" />
<el-drawer v-model="memberDrawer" append-to-body :title="editing ? '编辑商务' : '添加子商务'" size="min(480px,96vw)" destroy-on-close>
@@ -77,6 +77,8 @@ import { useUserStore } from '@/pinia/modules/user'
import { useBtnAuth } from '@/utils/btnAuth'
import { createBusiness } from '@/api/business'
import { getTeams, getManagement, updateMember } from '@/api/performance'
import LevelTag from './LevelTag.vue'
import MetricValue from './MetricValue.vue'
import { businessLevels, employmentLabels, employmentTypes, displayMember } from '../performance-display'
import TeamSettingsDialog from './TeamSettings.vue'
import GoalDrawer from './GoalDrawer.vue'
@@ -130,6 +132,7 @@ const saveMember = async () => { if (saving.value || !await memberForm.value.val
onMounted(refresh)
</script>
<style scoped>
.sw-business-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.muted { color: var(--el-text-color-secondary); font-size: 12px; }
.open { color: var(--el-color-primary); }
.effective { color: var(--el-color-success); }
@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="visible" append-to-body destroy-on-close :title="title" size="min(960px, 100vw)" @closed="reset">
<el-drawer class="sw-dialog" v-model="visible" append-to-body destroy-on-close :title="title" size="min(960px, 100vw)" @closed="reset">
<div v-loading="loading">
<el-alert v-if="error" type="warning" :closable="false" :title="error" />
<template v-if="profile">
@@ -30,14 +30,14 @@
</div>
<h3 class="profile-heading">旗下渠道排行</h3>
<div class="profile-two-columns">
<el-card shadow="never"><template #header>注册量 TOP 5</template><el-table :data="profile.registerTop5" size="small"><el-table-column label="渠道" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.promoteName || '—' }} <el-text type="info" size="small">{{ row.sourceDbLabel }}</el-text></template></el-table-column><el-table-column label="注册量" min-width="100"><template #default="{ row }">{{ number(row.value) }}</template></el-table-column></el-table></el-card>
<el-card shadow="never"><template #header>充值金额 TOP 5</template><el-table :data="profile.payTop5" size="small"><el-table-column label="渠道" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.promoteName || '—' }} <el-text type="info" size="small">{{ row.sourceDbLabel }}</el-text></template></el-table-column><el-table-column label="充值金额" min-width="120"><template #default="{ row }">{{ channelMoney(row.value) }}</template></el-table-column></el-table></el-card>
<el-card shadow="never"><template #header>注册量 TOP 5</template><el-table scrollbar-always-on :data="profile.registerTop5" size="small"><el-table-column label="渠道" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.promoteName || '—' }} <el-text type="info" size="small">{{ row.sourceDbLabel }}</el-text></template></el-table-column><el-table-column label="注册量" min-width="100"><template #default="{ row }">{{ number(row.value) }}</template></el-table-column></el-table></el-card>
<el-card shadow="never"><template #header>充值金额 TOP 5</template><el-table scrollbar-always-on :data="profile.payTop5" size="small"><el-table-column label="渠道" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.promoteName || '—' }} <el-text type="info" size="small">{{ row.sourceDbLabel }}</el-text></template></el-table-column><el-table-column label="充值金额" min-width="120"><template #default="{ row }">{{ channelMoney(row.value) }}</template></el-table-column></el-table></el-card>
</div>
</template>
<h3 class="profile-heading">关联预警</h3>
<el-alert v-if="alertError" type="warning" :closable="false" :title="alertError" />
<template v-else>
<el-table v-loading="alertsLoading" :data="alerts" size="small">
<el-table scrollbar-always-on v-loading="alertsLoading" :data="alerts" size="small">
<el-table-column label="时间" width="120"><template #default="{ row }">{{ row.createdAt?.slice(5, 16) || '—' }}</template></el-table-column>
<el-table-column prop="targetPromoteName" label="渠道" min-width="140" show-overflow-tooltip />
<el-table-column label="预警类型" min-width="130"><template #default="{ row }">{{ alertType(row) }}</template></el-table-column>
@@ -1,11 +1,11 @@
<template>
<el-drawer v-model="visible" append-to-body destroy-on-close :title="`推广游戏数据 — ${channel?.nickname || channel?.account || ''}`" size="min(560px, 100vw)" @closed="reset">
<el-drawer class="sw-dialog" 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 scrollbar-always-on 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" align="right"><template #default="{ row }"><metric-value :value="row.registerCount" /></template></el-table-column>
<el-table-column label="充值金额" min-width="150" align="right"><template #default="{ row }"><metric-value kind="money" :value="row.payAmount" /></template></el-table-column>
</el-table>
</el-drawer>
</template>
@@ -13,7 +13,7 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { channelGames } from '@/api/channel'
import { channelCount, channelMoney } from '../channel-display'
import MetricValue from './MetricValue.vue'
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) })
@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="visible" append-to-body destroy-on-close :title="title" size="min(900px, 100vw)" @closed="reset">
<el-drawer class="sw-dialog" v-model="visible" append-to-body destroy-on-close :title="title" size="min(900px, 100vw)" @closed="reset">
<div v-loading="loading">
<el-alert v-if="error" :title="error" type="warning" :closable="false" />
<template v-if="profile">
@@ -37,7 +37,7 @@
<h3 class="profile-heading">关联预警</h3>
<el-alert v-if="alertError" :title="alertError" type="warning" :closable="false" />
<template v-else>
<el-table v-loading="alertsLoading" :data="alerts" size="small">
<el-table scrollbar-always-on v-loading="alertsLoading" :data="alerts" size="small">
<el-table-column label="时间" width="120"><template #default="{ row }">{{ row.createdAt?.slice(5, 16) || '—' }}</template></el-table-column>
<el-table-column label="预警类型" min-width="130"><template #default="{ row }">{{ alertType(row) }}</template></el-table-column>
<el-table-column prop="alertContent" label="详情" min-width="240" show-overflow-tooltip />
@@ -15,7 +15,7 @@
</tr></thead>
<tbody>
<tr v-for="row in rows" :key="row.rowKey" :class="{ 'ranking-top': row.rank <= 3 && !row.placeholder }">
<td class="ranking-number">{{ row.placeholder ? '—' : row.rank }}</td>
<td class="ranking-number" :class="!row.placeholder && row.rank <= 3 ? `ranking-medal-${row.rank}` : ''">{{ row.placeholder ? '—' : row.rank }}</td>
<td :title="row.objectName">{{ row.objectName || '—' }}</td>
<td class="ranking-platform">{{ row.sourceDbLabel || '—' }}</td>
<td class="ranking-numeric" :title="valueFormatter(row.metricValue)">{{ valueFormatter(row.metricValue) }}</td>
@@ -58,7 +58,11 @@ const change = row => row.momValue == null ? '—' : `${row.momStatus > 0 ? '+'
.ranking-table .ranking-top { background: var(--el-color-primary-light-9); }
.ranking-table tbody tr:hover { background: var(--el-fill-color-light); }
.ranking-table .ranking-empty { height: 100px; text-align: center; color: var(--el-text-color-secondary); }
.ranking-platform, .ranking-number { color: var(--el-text-color-secondary); font-size: 12px; }
.ranking-platform { color: var(--el-color-primary); font-size: 12px; font-weight: 600; }
.ranking-number { color: var(--el-text-color-secondary); font-size: 12px; }
.ranking-number.ranking-medal-1 { color: #b77908; font-weight: 700; }
.ranking-number.ranking-medal-2 { color: #64748b; font-weight: 700; }
.ranking-number.ranking-medal-3 { color: #b66b3d; font-weight: 700; }
.ranking-up { color: var(--el-color-success); }
.ranking-down { color: var(--el-color-danger); }
@container (max-width: 300px) {
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="visible" append-to-body :title="`设置目标 · ${target?.name || data?.name || ''}`" size="min(650px, 96vw)" destroy-on-close>
<el-drawer class="sw-dialog" v-model="visible" append-to-body :title="`设置目标 · ${target?.name || data?.name || ''}`" size="min(650px, 96vw)" destroy-on-close>
<div v-loading="loading">
<el-alert v-if="error" type="error" :title="error" :closable="false" class="mb-3" />
<el-tabs v-model="tab"><el-tab-pane label="设置目标" name="settings" /><el-tab-pane label="目标记录" name="history" /></el-tabs>
@@ -16,7 +16,7 @@
<el-text tag="p" type="info" class="mt-3">实际绩效自动统计;个人包含本人及全部下级,小组为组内成员绩效之和。留空表示该项不考核,三项全空表示未设目标。</el-text>
<el-text v-if="selected?.goal?.updatedBy" tag="p" type="info" size="small" class="mt-3">最后修改人:#{{ selected.goal.updatedBy }} · {{ selected.goal.updatedAt }}</el-text>
</template>
<el-table v-else :data="history" size="small">
<el-table scrollbar-always-on v-else :data="history" size="small">
<el-table-column prop="month" label="月份" width="85" />
<el-table-column v-for="metric in metrics" :key="metric.key" :label="metric.label" min-width="120"><template #default="{ row }"><span v-if="row.goal?.[metric.goal] != null">{{ displayTarget(metric.goal, row.goal[metric.goal]) }} / {{ row.available === false ? '未接入' : displayMetric(metric.key, row.actual[metric.key]) }}</span><span v-else>—</span></template></el-table-column>
<el-table-column label="达成" width="95"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ row.status === 'missing' ? `差 ${row.missing} 项` : statuses[row.status] }}</el-tag></template></el-table-column>
+29 -24
View File
@@ -1,43 +1,43 @@
<template>
<div>
<div class="sw-page">
<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" @change="sourceChanged"><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="玩家ID"><el-input v-model="playerID" clearable placeholder="玩家ID" /></el-form-item>
<el-form-item v-if="spend" label="角色名"><el-input v-model="query.gamePlayerName" clearable placeholder="角色名" /></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="新商务 / SDK 商务账号" /></el-form-item>
<el-form-item label="玩家ID"><el-input v-model="playerID" clearable placeholder="精确匹配玩家ID" /></el-form-item>
<el-form-item v-if="spend" label="角色名称"><el-input v-model="query.gamePlayerName" clearable placeholder="角色名称" /></el-form-item>
<el-form-item label="渠道名称"><el-input v-model="query.promoteAccount" clearable placeholder="渠道名称 / 账号" /></el-form-item>
<el-form-item v-if="spend" label="订单号"><el-input v-model="query.payOrderNumber" clearable placeholder="订单号" /></el-form-item>
<el-form-item label="游戏"><el-select v-model="gameNames" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gamesLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 200px"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item v-if="spend" label="方式"><el-select v-model="query.payWay" clearable placeholder="全部方式" style="width: 140px"><el-option v-for="(label, value) in payWays" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item v-if="spend" label="状态"><el-select v-model="query.payStatus" 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 label="日期"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item label="游戏名称"><el-select v-model="gameNames" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gamesLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 200px"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item v-if="spend" label="支付方式"><el-select v-model="query.payWay" clearable placeholder="全部方式" style="width: 140px"><el-option v-for="(label, value) in payWays" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
<el-form-item v-if="spend" label="支付状态"><el-select v-model="query.payStatus" 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 :label="spend ? '充值时间' : '注册时间'"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" range-separator="至" :start-placeholder="spend ? '充值开始日期' : '注册开始日期'" :end-placeholder="spend ? '充值结束日期' : '注册结束日期'" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></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="canExport" type="warning" plain icon="download" :loading="exporting" @click="download">导出</el-button></div>
<div class="sw-table-toolbar"><el-button v-if="canExport" type="warning" plain icon="download" :loading="exporting" @click="download">导出</el-button><span class="sw-sort-label" role="status">{{ currentSort }}</span></div>
<el-alert v-if="error" class="mb-4" type="error" :closable="false" :title="error" />
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="明细所需业务数据尚未接入。" />
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.id}`" :row-class-name="rowClass">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<el-table-column v-if="spend" prop="id" label="ID" min-width="90" />
<el-table-column prop="userId" label="玩家ID" min-width="110" />
<el-table-column v-if="!spend" prop="registerTime" label="注册时间" min-width="165" sortable :sort-method="(a, b) => Number(a.registerTime) - Number(b.registerTime)"><template #default="{ row }">{{ unixTime(row.registerTime) }}</template></el-table-column>
<el-table scrollbar-always-on v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.id}`" :row-class-name="rowClass" :default-sort="{ prop: defaultSortField, order: 'descending' }" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb" :label="row.sourceDbLabel" /></template></el-table-column>
<el-table-column v-if="spend" prop="id" label="ID" min-width="90" class-name="sw-col-meta" sortable="custom" />
<el-table-column prop="userId" label="玩家ID" min-width="110" class-name="sw-col-meta" />
<el-table-column v-if="!spend" prop="registerTime" label="注册时间" min-width="165" sortable="custom" class-name="sw-col-meta"><template #default="{ row }">{{ unixTime(row.registerTime) }}</template></el-table-column>
<el-table-column v-if="spend" prop="payOrderNumber" label="订单号" min-width="190" show-overflow-tooltip />
<el-table-column :prop="spend ? 'gameName' : 'fgameName'" label="游戏" min-width="150" show-overflow-tooltip />
<el-table-column :prop="spend ? 'gameName' : 'fgameName'" label="游戏名称" min-width="150" show-overflow-tooltip />
<el-table-column v-if="spend" prop="serverName" label="区服" min-width="140" show-overflow-tooltip />
<el-table-column v-if="spend" prop="gamePlayerName" label="角色" min-width="130" show-overflow-tooltip />
<el-table-column :prop="spend ? 'payAmount' : 'totalPayAmount'" :label="spend ? '实付金额' : '累计充值'" min-width="150" sortable :sort-method="(a, b) => compareAmounts(a[spend ? 'payAmount' : 'totalPayAmount'], b[spend ? 'payAmount' : 'totalPayAmount'])"><template #default="{ row }">{{ channelMoney(spend ? row.payAmount ?? '0.00' : row.totalPayAmount) }}</template></el-table-column>
<el-table-column v-if="spend" prop="payTime" label="充值时间" min-width="165" sortable :sort-method="(a, b) => Number(a.payTime) - Number(b.payTime)"><template #default="{ row }">{{ unixTime(row.payTime) }}</template></el-table-column>
<el-table-column v-if="spend" label="方式" min-width="100"><template #default="{ row }">{{ payWays[row.payWay] ?? row.payWay ?? '—' }}</template></el-table-column>
<el-table-column v-if="spend" label="状态" min-width="110"><template #default="{ row }"><el-tag :type="{ 1: 'success', 2: 'warning', 0: 'danger' }[row.payStatus] || 'info'">{{ statuses[row.payStatus] ?? row.payStatus ?? '—' }}</el-tag></template></el-table-column>
<el-table-column :prop="spend ? 'payAmount' : 'totalPayAmount'" :label="spend ? '实付金额' : '累计充值'" min-width="150" sortable="custom" align="right" class-name="sw-col-number"><template #default="{ row }"><metric-value kind="money" :muted="spend && row.payStatus !== 1" :value="spend ? row.payAmount : row.totalPayAmount" /></template></el-table-column>
<el-table-column v-if="spend" prop="payTime" label="充值时间" min-width="165" sortable="custom" class-name="sw-col-meta"><template #default="{ row }">{{ unixTime(row.payTime) }}</template></el-table-column>
<el-table-column v-if="spend" label="支付方式" min-width="100"><template #default="{ row }">{{ payWays[row.payWay] ?? row.payWay ?? '—' }}</template></el-table-column>
<el-table-column v-if="spend" label="支付状态" min-width="110"><template #default="{ row }"><el-tag size="small" :type="{ 1: 'success', 2: 'warning', 0: 'danger' }[row.payStatus] || 'info'">{{ statuses[row.payStatus] ?? row.payStatus ?? '—' }}</el-tag></template></el-table-column>
<el-table-column :label="spend ? 'IP' : '注册IP'" min-width="130"><template #default="{ row }">{{ (spend ? row.spendIp : row.registerIp) || '—' }}</template></el-table-column>
<el-table-column v-if="!spend" prop="deviceName" label="机型" min-width="140" show-overflow-tooltip />
<el-table-column prop="promoteAccount" label="渠道" min-width="170" show-overflow-tooltip />
<el-table-column prop="promoteAccount" label="渠道名称" class-name="sw-col-identity" min-width="170" show-overflow-tooltip />
<el-table-column prop="busierName" label="归属商务" min-width="190" show-overflow-tooltip />
</el-table>
<p v-if="spend" class="mt-4 text-right">合计:成功支付总额 <strong>{{ error ? '未获取' : available ? channelMoney(totalPayAmount) : '未接入' }}</strong></p>
<p v-if="spend" class="sw-table-summary">合计:成功支付总额 <strong>{{ error ? '未获取' : available ? channelMoney(totalPayAmount) : '未接入' }}</strong></p>
<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, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
</div>
@@ -51,10 +51,13 @@ import { useBtnAuth } from '@/utils/btnAuth'
import { listLedger, exportLedger, listLedgerGames } from '@/api/ledger'
import { channelMoney } from '../channel-display'
import { createGameOptionsLoader } from '../game-options-loader'
import { compareAmounts } from '../table-display'
import { remoteSort, sortDescription } from '../table-display'
import PlatformTag from './PlatformTag.vue'
import MetricValue from './MetricValue.vue'
const fetchGameOptions = createGameOptionsLoader(listLedgerGames)
const props = defineProps({ kind: { type: String, required: true } })
const spend = computed(() => props.kind === 'spend')
const defaultSortField = spend.value ? 'payTime' : 'registerTime'
const user = useUserStore(), buttons = useBtnAuth()
const canExport = computed(() => Number(user.userInfo.authorityId) === 888 || !!buttons[`sw:${props.kind}:export`])
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
@@ -74,7 +77,9 @@ const sources = ref(Object.keys(platforms)), gameNames = ref([]), gameOptions =
const gamesLoading = ref(false), exporting = ref(false), loading = ref(false), available = ref(true)
const error = ref('')
const rows = ref([]), total = ref(0), totalPayAmount = ref('0.00')
const query = reactive({ pageNum: 1, pageSize: 10, busierAccount: '', promoteAccount: '', gamePlayerName: '', payOrderNumber: '', payStatus: undefined, payWay: undefined, sortField: spend.value ? 'payTime' : 'registerTime', sortOrder: 'desc' })
const query = reactive({ pageNum: 1, pageSize: 10, busierAccount: '', promoteAccount: '', gamePlayerName: '', payOrderNumber: '', payStatus: undefined, payWay: undefined, sortField: defaultSortField, sortOrder: 'desc' })
const currentSort = computed(() => sortDescription(query, { id: 'ID', payTime: '充值时间', registerTime: '注册时间', payAmount: '实付金额', totalPayAmount: '累计充值' }))
const sortChanged = event => { Object.assign(query, remoteSort(event, defaultSortField)); return search() }
let listVersion = 0, gameVersion = 0
const buildQuery = () => {
const player = playerID.value.trim()
+16
View File
@@ -0,0 +1,16 @@
<template>
<el-tag v-if="level >= 1 && level <= 3" size="small" effect="plain" class="sw-level-tag" :class="`sw-level-${level}`">{{ ['一级', '二级', '三级'][level - 1] }}{{ channel ? '渠道' : '商务' }}</el-tag>
<span v-else class="sw-empty-value">—</span>
</template>
<script setup>
defineProps({ level: [Number, String], channel: Boolean })
</script>
<style scoped>
.sw-level-tag { --level-color: var(--el-color-primary); --el-tag-text-color: var(--level-color); --el-tag-border-color: color-mix(in srgb, var(--level-color) 35%, transparent); --el-tag-bg-color: color-mix(in srgb, var(--level-color) 8%, transparent); font-weight: 600; }
.sw-level-1 { --level-color: #7c3aed; }.sw-level-2 { --level-color: var(--el-color-primary); }.sw-level-3 { --level-color: #0f766e; }
:global(html.dark .sw-level-1) { --level-color: #c4b5fd; }
:global(html.dark .sw-level-3) { --level-color: #5eead4; }
.sw-empty-value { color: var(--el-text-color-placeholder); }
</style>
@@ -0,0 +1,19 @@
<template>
<span class="sw-metric" :class="[`sw-metric--${kind}`, { 'is-empty': value == null, 'is-zero': zero, 'is-muted': muted }]">{{ text }}</span>
</template>
<script setup>
import { computed } from 'vue'
import { channelCount, channelMoney } from '../channel-display'
const props = defineProps({ muted: Boolean, value: [Number, String], kind: { type: String, default: 'count' } })
const zero = computed(() => props.value != null && /^-?0+(\.0+)?$/.test(String(props.value)))
const text = computed(() => props.value == null ? '—' : props.kind === 'money' ? channelMoney(props.value) : props.kind === 'percent' ? `${props.value}%` : channelCount(props.value))
</script>
<style scoped>
.sw-metric { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; color: var(--el-color-primary); }
.sw-metric--money { color: #15803d; }.sw-metric--percent { color: #7c3aed; }
.sw-metric.is-zero, .sw-metric.is-muted { color: var(--el-text-color-secondary); font-weight: 400; }
.sw-metric.is-empty { color: var(--el-text-color-placeholder); font-weight: 400; }
:global(html.dark .sw-metric--money) { color: #6ee7a0; }:global(html.dark .sw-metric--percent) { color: #c4b5fd; }
</style>
@@ -1,8 +1,8 @@
<template>
<el-drawer v-model="visible" append-to-body :title="title" size="min(1100px,96vw)" destroy-on-close>
<el-drawer class="sw-dialog" v-model="visible" append-to-body :title="title" size="min(1100px,96vw)" destroy-on-close>
<el-text type="info">{{ query?.begin }} 至 {{ query?.end }} · {{ query?.kind === 'team' ? '按成员贡献统计,同一渠道可计入多位成员' : '所选成员及全部下级商务,按渠道去重' }}</el-text>
<el-alert v-if="error" :title="error" type="error" :closable="false" class="mt-3" />
<el-table v-loading="loading" :data="rows" class="mt-3" :row-key="row => `${row.sourceDb}:${row.id}:${row.contributorId || 0}`">
<el-table scrollbar-always-on v-loading="loading" :data="rows" class="mt-3" :row-key="row => `${row.sourceDb}:${row.id}:${row.contributorId || 0}`">
<el-table-column v-if="query?.kind === 'team'" prop="contributorName" label="计入成员" min-width="140" />
<el-table-column prop="account" label="渠道" min-width="150" />
<el-table-column prop="ownerName" label="归属商务" min-width="140" />
@@ -0,0 +1,16 @@
<template>
<span v-if="text !== '—'" class="sw-platform-tag" :title="text"><i aria-hidden="true" />{{ text }}</span>
<span v-else class="sw-empty-value">—</span>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({ code: String, label: String })
const text = computed(() => props.label || ({ lx: '龙翔', hw: '汇玩', yq: '游趣', oa: 'OA' }[props.code]) || props.code || '—')
</script>
<style scoped>
.sw-platform-tag { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 2px 8px; border: 1px solid var(--el-color-primary-light-7); border-radius: 5px; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-size: 12px; font-weight: 600; line-height: 20px; white-space: nowrap; vertical-align: middle; }
.sw-platform-tag i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.sw-empty-value { color: var(--el-text-color-placeholder); }
</style>
+18 -14
View File
@@ -1,18 +1,18 @@
<template>
<div>
<div class="sw-page">
<div v-if="isChannel" v-loading="loading" class="report-cards">
<el-card v-for="card in cards" :key="card.label" shadow="never"><div class="report-card-label">{{ card.label }}</div><strong>{{ card.value }}</strong></el-card>
</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" @change="sourceChanged"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="游戏">
<el-form-item label="游戏名称">
<el-select v-if="isChannel" v-model="games" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select>
<el-select v-else v-model="selectedGame" clearable filterable :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px" @change="gameChanged"><el-option label="全部游戏" value="" /><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select>
</el-form-item>
<el-form-item v-if="isChannel" label="渠道类型"><el-select v-model="query.promoteType" clearable placeholder="全部渠道类型" style="width: 150px"><el-option label="公会" :value="1" /><el-option label="个人" :value="0" /></el-select></el-form-item>
<el-form-item v-if="isChannel" label="渠道账号"><el-input v-model="query.promoteName" clearable placeholder="渠道账号(模糊匹配)" /></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item v-if="isChannel" label="渠道名称"><el-input v-model="query.promoteName" clearable placeholder="渠道名称或账号" /></el-form-item>
<el-form-item label="统计时间"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">{{ isChannel ? '搜索' : '查询' }}</el-button></el-form-item>
</el-form>
</div>
@@ -20,15 +20,16 @@
<div v-if="!isChannel" class="report-heading"><strong>核心指标</strong><el-text type="info">统计周期:{{ period }}</el-text></div>
<el-alert v-if="error" class="mb-4" type="error" :closable="false" :title="error" />
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="统计所需业务汇总尚未接入。" />
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${isChannel ? row.promoteId : row.gameId}`" :default-sort="isChannel ? { prop: 'totalPay', order: 'descending' } : {}" :show-summary="!isChannel" :summary-method="summaries" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<el-table-column :prop="isChannel ? 'promoteName' : 'gameName'" :label="isChannel ? '渠道名称' : '游戏名称'" min-width="170" show-overflow-tooltip />
<div class="sw-table-toolbar"><span class="sw-sort-label" role="status">{{ currentSort }}</span></div>
<el-table scrollbar-always-on v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${isChannel ? row.promoteId : row.gameId}`" :default-sort="{ prop: 'totalPay', order: 'descending' }" :show-summary="!isChannel" :summary-method="summaries" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" width="104"><template #default="{ row }"><platform-tag :code="row.sourceDb" :label="row.sourceDbLabel" /></template></el-table-column>
<el-table-column :prop="isChannel ? 'promoteName' : 'gameName'" :label="isChannel ? '渠道名称' : '游戏名称'" min-width="170" show-overflow-tooltip class-name="sw-col-identity" />
<el-table-column v-if="isChannel" prop="busierName" label="归属商务" min-width="140" show-overflow-tooltip />
<el-table-column v-if="isChannel" label="渠道类型" min-width="100"><template #default="{ row }">{{ { 0: '个人', 1: '公会' }[row.promoteType] ?? row.promoteType ?? '—' }}</template></el-table-column>
<el-table-column v-else prop="gameId" label="游戏ID" min-width="100" />
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.width || 110" sortable="custom"><template #header><el-tooltip :content="metricTips[column.key]" placement="top"><span>{{ column.label }}</span></el-tooltip></template><template #default="{ row }">{{ format(column.type, row[column.key]) }}</template></el-table-column>
<el-table-column v-if="isChannel" label="渠道类型" min-width="100"><template #default="{ row }"><el-tag v-if="row.promoteType === 0 || row.promoteType === 1" size="small" :type="row.promoteType === 1 ? 'warning' : 'info'" effect="plain">{{ row.promoteType === 1 ? '公会' : '个人' }}</el-tag><span v-else class="sw-empty-value">—</span></template></el-table-column>
<el-table-column v-else prop="gameId" label="游戏ID" min-width="100" sortable="custom" class-name="sw-col-meta" />
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.width || 110" sortable="custom" align="right" class-name="sw-col-number"><template #header><el-tooltip :content="metricTips[column.key]" placement="top"><span>{{ column.label }}</span></el-tooltip></template><template #default="{ row }"><metric-value :kind="column.type" :value="row[column.key]" /></template></el-table-column>
</el-table>
<p v-if="isChannel" class="mt-4">合计:有效端 <el-text type="success">{{ result?.effectiveCount == null ? '未接入' : result.effectiveCount }}</el-text> 个</p>
<p v-if="isChannel" class="sw-table-summary">合计:有效端 <el-text type="success">{{ result?.effectiveCount == null ? '未接入' : result.effectiveCount }}</el-text> 个</p>
<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, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
</div>
@@ -41,7 +42,9 @@ import { listAnalytics } from '@/api/analytics'
import { listLedgerGames } from '@/api/ledger'
import { channelCount, channelMoney } from '../channel-display'
import { createGameOptionsLoader } from '../game-options-loader'
import { metricTips } from '../table-display'
import PlatformTag from './PlatformTag.vue'
import MetricValue from './MetricValue.vue'
import { metricTips, remoteSort, sortDescription } from '../table-display'
import { analysisGameSelection } from '../analysis-game-selection'
const fetchGameOptions = createGameOptionsLoader(listLedgerGames)
const props = defineProps({ kind: { type: String, required: true } })
@@ -58,7 +61,7 @@ const shortcuts = [
]
const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), gameLoading = ref(false)
const selectedGame = computed({ get: () => games.value[0] || '', set: value => { games.value = value ? [value] : [] } })
const query = reactive({ pageNum: 1, pageSize: 10, promoteType: undefined, promoteName: '', sortField: isChannel.value ? 'totalPay' : '', sortOrder: 'desc' })
const query = reactive({ pageNum: 1, pageSize: 10, promoteType: undefined, promoteName: '', sortField: 'totalPay', sortOrder: 'desc' })
const rows = ref([]), total = ref(0), summary = ref(null), result = ref(null), loading = ref(false), available = ref(true), error = ref('')
let version = 0, gameVersion = 0
const period = computed(() => result.value?.beginTime && result.value?.endTime ? `${result.value.beginTime} 至 ${result.value.endTime}` : dates.value.join(' 至 '))
@@ -80,7 +83,8 @@ const load = async () => {
} catch { if (current === version) { clear(); error.value = '统计查询失败,请重试' } } finally { if (current === version) loading.value = false }
}
const search = () => { query.pageNum = 1; if (!sources.value.length) ElMessage.warning('请选择平台'); return load() }
const sortChanged = ({ prop, order }) => { query.sortField = order ? prop : isChannel.value ? 'totalPay' : ''; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return search() }
const sortChanged = event => { Object.assign(query, remoteSort(event, 'totalPay')); return search() }
const currentSort = computed(() => sortDescription(query, { ...Object.fromEntries(Object.values(fields).map(f => [f.key, f.label])), gameId: '游戏ID' }))
const gameChanged = () => { analysisGameSelection.value = selectedGame.value; return search() }
const sourceChanged = async () => {
++version; ++gameVersion; games.value = []; gameOptions.value = []; gameLoading.value = false; loading.value = false; error.value = ''; clear()
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="visible" append-to-body title="小组设置" width="min(820px, 95vw)" :close-on-click-modal="false" destroy-on-close>
<el-dialog class="sw-dialog" v-model="visible" append-to-body title="小组设置" width="min(820px, 95vw)" :close-on-click-modal="false" destroy-on-close>
<el-alert v-if="error" :title="error" type="error" :closable="false" class="mb-3" />
<div class="team-editor" v-loading="saving">
<aside>
+8
View File
@@ -22,3 +22,11 @@ export const metricTips = {
arppu: '充值金额 ÷ 付费玩家。',
newPayUserCount: '所选周期内首次产生成功充值的玩家数,此前从未付费。'
}
// Sorting must describe the full query, never just the current page.
export const remoteSort = ({ prop, order }, defaultField) => ({
sortField: order ? prop : defaultField,
sortOrder: order === 'ascending' ? 'asc' : 'desc'
})
export const sortDescription = (query, labels) => `排序:${labels[query.sortField] || query.sortField} · ${query.sortOrder === 'asc' ? '升序' : '降序'}`