fix: 统一商务表格展示并修正筛选和全量排序
This commit is contained in:
@@ -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)",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 + "))")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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' ? '升序' : '降序'}`
|
||||
|
||||
Reference in New Issue
Block a user