merge(002-U10): admin运营台接线(限曝光/导出/经营看板/赋余额订阅·funds-safe bizNo幂等+二次确认;build绿) → dev/2.0.0
This commit is contained in:
commit
da5ec28c49
@ -4,10 +4,12 @@ import { isMock, mockPage, mockResult } from './mock'
|
||||
/**
|
||||
* 造梦AI —— feed 模块 admin 接口(游戏流精选池运营)
|
||||
* 契约:contracts/api-schemas/feed.yaml
|
||||
* - GET /admin-api/feed/featured/page 精选池列表(可按 zoneId 筛选)
|
||||
* - POST /admin-api/feed/featured 置顶/取消 + 运营加权(写 game_feed_rank.pinned/boost)
|
||||
* - GET /admin-api/feed/featured/page 精选池列表(可按 zoneId 筛选)
|
||||
* - POST /admin-api/feed/featured 置顶/取消 + 运营加权(写 game_feed_rank.pinned/boost)
|
||||
* - POST /admin-api/feed/exposure-limit 限曝光降权(U5;boost 反向操作,写 game_feed_rank.exposure_limit)
|
||||
* - GET /admin-api/feed/featured/export-excel 精选池/限曝光排序覆盖层导出 Excel(U5)
|
||||
* 端与鉴权:admin-api 走 huijing 管理员 Token + RBAC;CommonResult code=0 成功(request 已解包 data)。
|
||||
* 边界:精选只作用于排序覆盖层(pinned/boost),不改 project 本体;仅 status=4 已发布可入精选。
|
||||
* 边界:精选/限曝光只作用于排序覆盖层(pinned/boost/exposure_limit),不改 project 本体;仅 status=4 已发布可入精选/限曝光。
|
||||
*/
|
||||
|
||||
/** 精选池单卡(对齐 feed.yaml FeedCardRespVO) */
|
||||
@ -39,6 +41,16 @@ export interface FeaturedPageReqVO extends PageParam {
|
||||
zoneId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 限曝光降权入参(对齐后端 ExposureLimitReqVO)
|
||||
* exposureLimit 取值:0=取消降权 / >0=降权 x 分 / >=9999=硬限曝光(出流剔除);仅 status=4 已发布且已有排序行可设置。
|
||||
*/
|
||||
export interface ExposureLimitReqVO {
|
||||
gameId: number // 目标游戏ID(须已发布)
|
||||
zoneId?: number // 作用专区;不传=默认混合流(zoneId=0)
|
||||
exposureLimit: number // 限曝光降权因子(>=0)
|
||||
}
|
||||
|
||||
// ---- mock 数据(对齐 FeedCardRespVO,均为已发布且进入精选池的游戏) ----
|
||||
const mockFeaturedList: FeedCardRespVO[] = [
|
||||
{
|
||||
@ -104,3 +116,19 @@ export const saveFeatured = async (data: FeaturedReqVO) => {
|
||||
}
|
||||
return await request.post({ url: '/feed/featured', data })
|
||||
}
|
||||
|
||||
/** 限曝光降权(U5;boost 反向操作,写 exposure_limit;perm feed:featured:update) */
|
||||
export const setExposureLimit = async (data: ExposureLimitReqVO) => {
|
||||
if (isMock()) {
|
||||
return mockResult(true)
|
||||
}
|
||||
return await request.post({ url: '/feed/exposure-limit', data })
|
||||
}
|
||||
|
||||
/** 导出精选池/限曝光排序覆盖层 Excel(U5;blob 下载,perm feed:featured:export;mock 下不下载,返回 null) */
|
||||
export const exportFeaturedExcel = async (params: { zoneId?: number }) => {
|
||||
if (isMock()) {
|
||||
return mockResult(null)
|
||||
}
|
||||
return await request.download({ url: '/feed/featured/export-excel', params })
|
||||
}
|
||||
|
||||
@ -2,13 +2,16 @@ import request from '@/config/axios'
|
||||
import { isMock, mockPage, mockResult } from './mock'
|
||||
|
||||
/**
|
||||
* 造梦AI —— telemetry 模块 admin 接口(数据看板数据源)
|
||||
* 造梦AI —— telemetry 模块 admin 接口(数据看板数据源 + 经营看板聚合)
|
||||
* 契约:contracts/api-schemas/telemetry.yaml
|
||||
* - GET /admin-api/telemetry/game-stat/page 游戏维度聚合分页(看板表格数据源)
|
||||
* - GET /admin-api/telemetry/game-stat/{gameId} 单游戏最新聚合快照
|
||||
* - GET /admin-api/telemetry/game-stat/page 游戏维度聚合分页(看板表格数据源)
|
||||
* - GET /admin-api/telemetry/game-stat/{gameId} 单游戏最新聚合快照
|
||||
* - GET /admin-api/telemetry/dashboard 经营看板聚合(U5;跨模块只读 DAU/GMV/生成量/发布量)
|
||||
* - GET /admin-api/telemetry/dashboard/export-excel 经营看板导出 Excel(U5)
|
||||
* 端与鉴权:admin-api 走 huijing 管理员 Token + RBAC;CommonResult code=0 成功(request 已解包 data)。
|
||||
* 量纲:qualityScore=运营质量分(0-100),≠ aigc 生成质量分(0-1)。
|
||||
* 量纲:qualityScore=运营质量分(0-100),≠ aigc 生成质量分(0-1);GMV 单位「分」(分账前原始收入)。
|
||||
* 范围:广告/收益聚合字段为 Wave2 变现范围外(later 补列,本 VO 不含,非遗漏)。
|
||||
* 经营看板 ≠ game-stat:前者是跨模块(DAU+trade+aigc+project)区间聚合卡片,后者是按游戏维度的明细分页。
|
||||
*/
|
||||
|
||||
/** 游戏维度聚合快照(对齐 telemetry.yaml GameStatRespVO) */
|
||||
@ -34,6 +37,27 @@ export interface GameStatPageReqVO extends PageParam {
|
||||
statDate?: string // 统计日;空则取最新一日
|
||||
}
|
||||
|
||||
/**
|
||||
* 经营看板聚合快照(对齐后端 DashboardRespVO)
|
||||
* 跨模块只读聚合:DAU(本模块) + GMV(trade,分) + 生成量(aigc) + 发布量(project);缺模块数据兜底 0 + degraded 标记,整体不 500。
|
||||
*/
|
||||
export interface DashboardRespVO {
|
||||
statDateStart?: string
|
||||
statDateEnd?: string
|
||||
dau?: number // 统计结束日当天去重活跃身份数
|
||||
gmv?: number // 区间原始收入合计(分账前,单位:分)
|
||||
generatedCount?: number // 区间 aigc 成功生成任务数
|
||||
publishedCount?: number // 区间 project 已发布项目数
|
||||
degraded?: boolean // true=有跨模块数据源不可用、对应指标已兜底 0
|
||||
degradedSources?: string[] // 不可用数据源明细(如 ['trade','aigc'])
|
||||
}
|
||||
|
||||
/** 经营看板查询参数(对齐后端 /admin-api/telemetry/dashboard,区间必填) */
|
||||
export interface DashboardReqVO {
|
||||
statDateStart: string
|
||||
statDateEnd: string
|
||||
}
|
||||
|
||||
// ---- mock 数据(对齐 GameStatRespVO,按日聚合) ----
|
||||
const mockGameStatList: GameStatRespVO[] = [
|
||||
{
|
||||
@ -118,3 +142,28 @@ export const getGameStat = async (gameId: number) => {
|
||||
}
|
||||
return await request.get({ url: '/telemetry/game-stat/' + gameId })
|
||||
}
|
||||
|
||||
/** 经营看板聚合(U5;跨模块只读,perm telemetry:dashboard:query) */
|
||||
export const getDashboard = async (params: DashboardReqVO) => {
|
||||
if (isMock()) {
|
||||
return mockResult<DashboardRespVO>({
|
||||
statDateStart: params.statDateStart,
|
||||
statDateEnd: params.statDateEnd,
|
||||
dau: 1530,
|
||||
gmv: 1000000, // 分(=10000元)
|
||||
generatedCount: 320,
|
||||
publishedCount: 88,
|
||||
degraded: false,
|
||||
degradedSources: []
|
||||
})
|
||||
}
|
||||
return await request.get({ url: '/telemetry/dashboard', params })
|
||||
}
|
||||
|
||||
/** 导出经营看板 Excel(U5;blob 下载,perm telemetry:dashboard:query;mock 下不下载,返回 null) */
|
||||
export const exportDashboardExcel = async (params: DashboardReqVO) => {
|
||||
if (isMock()) {
|
||||
return mockResult(null)
|
||||
}
|
||||
return await request.download({ url: '/telemetry/dashboard/export-excel', params })
|
||||
}
|
||||
|
||||
@ -2,14 +2,18 @@ import request from '@/config/axios'
|
||||
import { isMock, mockPage, mockResult } from './mock'
|
||||
|
||||
/**
|
||||
* 造梦AI —— trade 模块 admin 接口(提现审核 + 收益流水查询 + 平台营收报表)
|
||||
* 造梦AI —— trade 模块 admin 接口(提现审核 + 收益流水查询 + 平台营收报表 + 赋余额 + 赋订阅)
|
||||
* 契约:contracts/api-schemas/trade.yaml
|
||||
* - GET /admin-api/trade/withdraw/page 提现审核队列(默认 status=0 待审核)
|
||||
* - POST /admin-api/trade/withdraw/audit 审核(通过/驳回,驱动状态机;非法流转服务端拒绝)
|
||||
* - GET /admin-api/trade/income/page 收益流水查询(跨创作者对账,只读)
|
||||
* - GET /admin-api/trade/report/revenue 平台营收报表聚合(按日 gross/net/平台分成/提现,只读)
|
||||
* - GET /admin-api/trade/withdraw/page 提现审核队列(默认 status=0 待审核)
|
||||
* - POST /admin-api/trade/withdraw/audit 审核(通过/驳回,驱动状态机;非法流转服务端拒绝)
|
||||
* - GET /admin-api/trade/income/page 收益流水查询(跨创作者对账,只读)
|
||||
* - GET /admin-api/trade/report/revenue 平台营收报表聚合(按日 gross/net/平台分成/提现,只读)
|
||||
* - GET /admin-api/trade/report/revenue/export-excel 营收报表导出 Excel(U5)
|
||||
* - POST /admin-api/trade/account/grant admin 赋余额(U2;幂等键 bizNo,real 入账+流水可对账)
|
||||
* - POST /admin-api/trade/subscription/grant admin 赋订阅(U2;幂等键 bizNo,续期叠加)
|
||||
* 端与鉴权:admin-api 走 huijing 管理员 Token + RBAC;CommonResult code=0 成功(request 已解包 data)。
|
||||
* 资金口径:金额一律「分」(BIGINT),禁浮点;shareRate 为 DECIMAL(如 0.8)。
|
||||
* ⚠️ 资金操作:赋余额/赋订阅须前端二次确认 + 携带 bizNo 幂等键(后端 @NotEmpty 强校验,重试安全);操作人由 token 解析,非前端入参。
|
||||
*/
|
||||
|
||||
/** 提现状态机(对齐 trade.yaml WithdrawRespVO.status / x-state-machines) */
|
||||
@ -94,6 +98,36 @@ export interface RevenueReportReqVO {
|
||||
statDateEnd: string
|
||||
}
|
||||
|
||||
/** 订阅套餐(对齐后端 GrantSubscriptionReqVO.plan) */
|
||||
export enum SubscriptionPlanEnum {
|
||||
MONTHLY = 1, // 月度
|
||||
QUARTERLY = 2, // 季度
|
||||
YEARLY = 3 // 年度
|
||||
}
|
||||
|
||||
/**
|
||||
* admin 赋余额入参(对齐后端 GrantBalanceReqVO)
|
||||
* 资金操作:amount 单位「分」只增不减(>0);bizNo 幂等键(REQUIRED, max64, 调用端生成唯一);操作人由 token 解析非入参。
|
||||
*/
|
||||
export interface GrantBalanceReqVO {
|
||||
userId: number // 被赋余额的创作者用户ID
|
||||
amount: number // 赋余额金额(分,>0)
|
||||
bizNo: string // 业务单号=幂等键(REQUIRED)
|
||||
remark?: string // 赋值原因(可空)
|
||||
}
|
||||
|
||||
/**
|
||||
* admin 赋订阅入参(对齐后端 GrantSubscriptionReqVO)
|
||||
* plan=1月度/2季度/3年度;durationDays>0 续期叠加;bizNo 幂等键(REQUIRED, max64);操作人由 token 解析非入参。
|
||||
*/
|
||||
export interface GrantSubscriptionReqVO {
|
||||
userId: number // 订阅用户ID
|
||||
plan: number // 套餐:1月度/2季度/3年度
|
||||
durationDays: number // 赋订阅时长(天,>0,续期叠加)
|
||||
bizNo: string // 业务单号=幂等键(REQUIRED)
|
||||
remark?: string // 开通原因(可空)
|
||||
}
|
||||
|
||||
// ---- mock 数据 ----
|
||||
const mockWithdrawList: WithdrawRespVO[] = [
|
||||
{
|
||||
@ -200,3 +234,30 @@ export const getRevenueReport = async (params: RevenueReportReqVO) => {
|
||||
}
|
||||
return await request.get({ url: '/trade/report/revenue', params })
|
||||
}
|
||||
|
||||
/** 导出平台营收报表 Excel(U5;blob 下载,perm trade:report:export;mock 下不下载,返回 null) */
|
||||
export const exportRevenueExcel = async (params: RevenueReportReqVO) => {
|
||||
if (isMock()) {
|
||||
return mockResult(null)
|
||||
}
|
||||
return await request.download({ url: '/trade/report/revenue/export-excel', params })
|
||||
}
|
||||
|
||||
/** admin 赋余额(U2;资金操作,返回赋余额流水 ID;perm trade:account:grant) */
|
||||
export const grantBalance = async (data: GrantBalanceReqVO) => {
|
||||
if (isMock()) {
|
||||
// mock:返回一个伪流水 ID(与真实返回 Long 同构)
|
||||
return mockResult<number>(Date.now())
|
||||
}
|
||||
return await request.post({ url: '/trade/account/grant', data })
|
||||
}
|
||||
|
||||
/** admin 赋订阅(U2;资金操作,返回赋值后到期时间字符串;perm trade:subscription:grant) */
|
||||
export const grantSubscription = async (data: GrantSubscriptionReqVO) => {
|
||||
if (isMock()) {
|
||||
// mock:返回一个伪到期时间(与真实返回 LocalDateTime 序列化同构)
|
||||
const d = new Date(Date.now() + (data.durationDays ?? 30) * 24 * 3600 * 1000)
|
||||
return mockResult<string>(d.toISOString().slice(0, 19).replace('T', ' '))
|
||||
}
|
||||
return await request.post({ url: '/trade/subscription/grant', data })
|
||||
}
|
||||
|
||||
@ -238,8 +238,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
// 造梦AI(wanxiang) MVP 运营页静态路由:
|
||||
// - 已裁剪 huijing 无关业务模块(ai/bpm/crm/erp/iot/mall/member/mp/pay/report)的全部静态路由,
|
||||
// 避免对已删视图的死 import 导致构建失败;
|
||||
// - 新增造梦AI 5 个 MVP 运营页(wanxiang/*)静态路由:后端 RBAC 菜单未下发时也可路由可见、
|
||||
// - 新增造梦AI 8 个 MVP 运营页(wanxiang/*)静态路由:后端 RBAC 菜单未下发时也可路由可见、
|
||||
// 可独立构建(mock 兜底见各 wanxiang api 模块),对接我方 admin-api。
|
||||
// (U10 追加 3 页:限曝光控制 / 经营看板 / 赋余额+订阅。)
|
||||
// ===========================================================================
|
||||
{
|
||||
path: '/wanxiang',
|
||||
@ -270,6 +271,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
icon: 'ep:star'
|
||||
}
|
||||
},
|
||||
{
|
||||
// U10:限曝光控制(boost 反向运营,写排序覆盖层 exposure_limit)
|
||||
path: 'exposure-limit',
|
||||
component: () => import('@/views/wanxiang/exposureLimit/index.vue'),
|
||||
name: 'WanxiangExposureLimit',
|
||||
meta: {
|
||||
title: '限曝光控制',
|
||||
icon: 'ep:bottom'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('@/views/wanxiang/dashboard/index.vue'),
|
||||
@ -279,6 +290,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
icon: 'ep:data-line'
|
||||
}
|
||||
},
|
||||
{
|
||||
// U10:经营看板(跨模块只读聚合 DAU/GMV/生成量/发布量;区别于按游戏维度的「数据看板」)
|
||||
path: 'operation-dashboard',
|
||||
component: () => import('@/views/wanxiang/operationDashboard/index.vue'),
|
||||
name: 'WanxiangOperationDashboard',
|
||||
meta: {
|
||||
title: '经营看板',
|
||||
icon: 'ep:pie-chart'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ad-slot',
|
||||
component: () => import('@/views/wanxiang/adSlot/index.vue'),
|
||||
@ -296,6 +317,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
title: '提现审核',
|
||||
icon: 'ep:wallet'
|
||||
}
|
||||
},
|
||||
{
|
||||
// U10:赋余额 + 赋订阅(资金操作 2 Tab;二次确认 + bizNo 幂等键)
|
||||
path: 'grant',
|
||||
component: () => import('@/views/wanxiang/grant/index.vue'),
|
||||
name: 'WanxiangGrant',
|
||||
meta: {
|
||||
title: '赋余额/订阅',
|
||||
icon: 'ep:money'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
246
game-admin/src/views/wanxiang/exposureLimit/index.vue
Normal file
246
game-admin/src/views/wanxiang/exposureLimit/index.vue
Normal file
@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏:复用精选池列表作为候选游戏来源(仅 status=4 已发布且已有排序行可限曝光) -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="专区ID" prop="zoneId">
|
||||
<el-input
|
||||
v-model.number="queryParams.zoneId"
|
||||
placeholder="不填=全部"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
v-hasPermi="['feed:featured:update']"
|
||||
@click="openForm()"
|
||||
>
|
||||
<Icon icon="ep:bottom" class="mr-5px" /> 设置限曝光
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['feed:featured:export']"
|
||||
@click="handleExport"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出排序覆盖层
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表:精选池(候选限曝光游戏;可直接对某游戏发起限曝光降权) -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="游戏ID" align="center" prop="gameId" width="90" />
|
||||
<el-table-column label="封面" align="center" width="80">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.coverUrl"
|
||||
:src="scope.row.coverUrl"
|
||||
fit="cover"
|
||||
style="width: 48px; height: 64px; border-radius: 4px"
|
||||
:preview-src-list="[scope.row.coverUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" align="center" prop="title" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="作者" align="center" prop="authorName" width="120" />
|
||||
<el-table-column label="专区ID" align="center" prop="zoneId" width="90" />
|
||||
<el-table-column label="质量分(0-100)" align="center" prop="qualityScore" width="120">
|
||||
<template #default="scope">{{ scope.row.qualityScore?.toFixed(1) ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="试玩数" align="center" prop="playCount" width="100" />
|
||||
<el-table-column label="操作" align="center" width="160" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
v-hasPermi="['feed:featured:update']"
|
||||
@click="openForm(scope.row)"
|
||||
>
|
||||
限曝光降权
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 限曝光降权 表单弹窗 -->
|
||||
<Dialog v-model="dialogVisible" title="限曝光降权" width="520">
|
||||
<el-alert
|
||||
class="mb-12px"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="限曝光是 boost 的反向运营操作:写入排序覆盖层降权,仅作用于排序、不改游戏本体。仅已发布且已有排序行的游戏可设置。"
|
||||
/>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="游戏ID" prop="gameId">
|
||||
<el-input v-model.number="formData.gameId" placeholder="请输入已发布游戏的ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="作用专区ID" prop="zoneId">
|
||||
<el-input v-model.number="formData.zoneId" placeholder="不填=作用于默认混合流(zoneId=0)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="限曝光降权因子" prop="exposureLimit">
|
||||
<el-input-number
|
||||
v-model="formData.exposureLimit"
|
||||
:precision="1"
|
||||
:step="1"
|
||||
:min="0"
|
||||
class="!w-240px"
|
||||
/>
|
||||
<div class="text-12px text-gray-400 mt-4px">
|
||||
0=取消降权 / >0=降权 x 分 / ≥9999=硬限曝光(出流剔除)
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="submitLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import download from '@/utils/download'
|
||||
import * as FeedApi from '@/api/wanxiang/feed'
|
||||
|
||||
defineOptions({ name: 'WanxiangExposureLimit' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const loading = ref(true) // 列表加载中
|
||||
const total = ref(0) // 总条数
|
||||
const list = ref<FeedApi.FeedCardRespVO[]>([]) // 列表数据(候选限曝光游戏)
|
||||
const queryParams = reactive<FeedApi.FeaturedPageReqVO>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
zoneId: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
const exportLoading = ref(false) // 导出加载中
|
||||
|
||||
/** 查询候选限曝光游戏(复用精选池列表) */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await FeedApi.getFeaturedPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
const resetQuery = () => {
|
||||
queryParams.zoneId = undefined
|
||||
queryFormRef.value?.resetFields?.()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 导出排序覆盖层(精选/限曝光)Excel */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await FeedApi.exportFeaturedExcel({ zoneId: queryParams.zoneId })
|
||||
if (data) download.excel(data as Blob, '精选池限曝光.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 限曝光降权 表单弹窗 ----
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = reactive<FeedApi.ExposureLimitReqVO>({
|
||||
gameId: undefined as unknown as number,
|
||||
zoneId: undefined,
|
||||
exposureLimit: 0
|
||||
})
|
||||
const formRules = reactive({
|
||||
gameId: [{ required: true, message: '游戏ID不能为空', trigger: 'blur' }],
|
||||
exposureLimit: [{ required: true, message: '限曝光降权因子不能为空', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 打开弹窗:传 row=对该游戏限曝光;不传=手填游戏ID限曝光 */
|
||||
const openForm = (row?: FeedApi.FeedCardRespVO) => {
|
||||
dialogVisible.value = true
|
||||
if (row) {
|
||||
formData.gameId = row.gameId
|
||||
formData.zoneId = row.zoneId
|
||||
} else {
|
||||
formData.gameId = undefined as unknown as number
|
||||
formData.zoneId = undefined
|
||||
}
|
||||
formData.exposureLimit = 0
|
||||
formRef.value?.resetFields?.()
|
||||
}
|
||||
|
||||
/** 提交限曝光降权(写排序覆盖层 exposure_limit) */
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 资金外的运营降权操作:二次确认(>=9999 会出流剔除,影响较大)
|
||||
try {
|
||||
const tip =
|
||||
formData.exposureLimit >= 9999
|
||||
? `确认对游戏 #${formData.gameId} 设置「硬限曝光(出流剔除)」?`
|
||||
: formData.exposureLimit > 0
|
||||
? `确认对游戏 #${formData.gameId} 降权 ${formData.exposureLimit} 分?`
|
||||
: `确认取消游戏 #${formData.gameId} 的限曝光降权?`
|
||||
await message.confirm(tip)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await FeedApi.setExposureLimit({
|
||||
gameId: formData.gameId,
|
||||
zoneId: formData.zoneId,
|
||||
exposureLimit: formData.exposureLimit
|
||||
})
|
||||
message.success('限曝光设置成功')
|
||||
dialogVisible.value = false
|
||||
await getList()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
292
game-admin/src/views/wanxiang/grant/index.vue
Normal file
292
game-admin/src/views/wanxiang/grant/index.vue
Normal file
@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" class="wanxiang-grant-tabs">
|
||||
<!-- ============ Tab1:赋余额 ============ -->
|
||||
<el-tab-pane label="赋余额" name="balance">
|
||||
<ContentWrap>
|
||||
<el-alert
|
||||
class="mb-16px"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="资金操作:给创作者账户充值(活动奖励/客诉补偿),金额只增不减。业务单号(bizNo)为幂等键,同一单号重复提交不会重复入账,重试安全。操作人由登录态记录、可审计追溯。"
|
||||
/>
|
||||
<el-form
|
||||
ref="balanceFormRef"
|
||||
:model="balanceForm"
|
||||
:rules="balanceRules"
|
||||
label-width="120px"
|
||||
class="!w-640px"
|
||||
>
|
||||
<el-form-item label="创作者用户ID" prop="userId">
|
||||
<el-input v-model.number="balanceForm.userId" placeholder="被赋余额的创作者用户ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="赋余额金额(元)" prop="amountYuan">
|
||||
<el-input-number
|
||||
v-model="balanceForm.amountYuan"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:min="0.01"
|
||||
class="!w-240px"
|
||||
/>
|
||||
<div class="text-12px text-gray-400 mt-4px">
|
||||
提交按「分」入账({{ yuan2fen(balanceForm.amountYuan) }} 分),只增不减
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务单号" prop="bizNo">
|
||||
<el-input v-model="balanceForm.bizNo" maxlength="64" show-word-limit placeholder="幂等键,调用端生成唯一" class="!w-360px" />
|
||||
<el-button class="ml-8px" @click="balanceForm.bizNo = genBizNo('GR')">重新生成</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="balanceForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="赋值原因(可空),如:活动奖励"
|
||||
class="!w-360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="balanceLoading"
|
||||
v-hasPermi="['trade:account:grant']"
|
||||
@click="submitBalance"
|
||||
>
|
||||
确认赋余额
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ============ Tab2:赋订阅 ============ -->
|
||||
<el-tab-pane label="赋订阅" name="subscription">
|
||||
<ContentWrap>
|
||||
<el-alert
|
||||
class="mb-16px"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="资金操作:给用户开通会员订阅(运营/B 端定制开通,MVP 不接真实支付收单)。业务单号(bizNo)为幂等键,同一单号不重复延长;续期叠加不丢未用时长。操作人由登录态记录、可审计追溯。"
|
||||
/>
|
||||
<el-form
|
||||
ref="subFormRef"
|
||||
:model="subForm"
|
||||
:rules="subRules"
|
||||
label-width="120px"
|
||||
class="!w-640px"
|
||||
>
|
||||
<el-form-item label="订阅用户ID" prop="userId">
|
||||
<el-input v-model.number="subForm.userId" placeholder="开通会员的用户ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订阅套餐" prop="plan">
|
||||
<el-select v-model="subForm.plan" placeholder="请选择套餐" class="!w-240px" @change="onPlanChange">
|
||||
<el-option v-for="p in planOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="赋订阅时长(天)" prop="durationDays">
|
||||
<el-input-number
|
||||
v-model="subForm.durationDays"
|
||||
:precision="0"
|
||||
:step="30"
|
||||
:min="1"
|
||||
class="!w-240px"
|
||||
/>
|
||||
<div class="text-12px text-gray-400 mt-4px">续期叠加;选套餐自动带默认天数,可手改</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务单号" prop="bizNo">
|
||||
<el-input v-model="subForm.bizNo" maxlength="64" show-word-limit placeholder="幂等键,调用端生成唯一" class="!w-360px" />
|
||||
<el-button class="ml-8px" @click="subForm.bizNo = genBizNo('SUB')">重新生成</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="subForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="开通原因(可空),如:B 端定制开通"
|
||||
class="!w-360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="subLoading"
|
||||
v-hasPermi="['trade:subscription:grant']"
|
||||
@click="submitSubscription"
|
||||
>
|
||||
确认赋订阅
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as TradeApi from '@/api/wanxiang/trade'
|
||||
import { SubscriptionPlanEnum } from '@/api/wanxiang/trade'
|
||||
|
||||
defineOptions({ name: 'WanxiangGrant' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const activeTab = ref('balance') // 当前 Tab
|
||||
|
||||
/** 元 → 分(资金一律以分入账;四舍五入避免浮点误差) */
|
||||
const yuan2fen = (yuan?: number): number => Math.round((yuan ?? 0) * 100)
|
||||
|
||||
/**
|
||||
* 生成幂等键 bizNo:前缀 + 时间戳(到毫秒) + 4 位随机。
|
||||
* 唯一性靠时间戳+随机;如运营需自定义可手改/重新生成(后端按 bizNo 幂等去重)。
|
||||
*/
|
||||
const genBizNo = (prefix: string): string => {
|
||||
const now = new Date()
|
||||
const pad = (n: number, w = 2) => String(n).padStart(w, '0')
|
||||
const ts =
|
||||
now.getFullYear().toString() +
|
||||
pad(now.getMonth() + 1) +
|
||||
pad(now.getDate()) +
|
||||
pad(now.getHours()) +
|
||||
pad(now.getMinutes()) +
|
||||
pad(now.getSeconds()) +
|
||||
pad(now.getMilliseconds(), 3)
|
||||
const rand = Math.floor(Math.random() * 9000 + 1000)
|
||||
return `${prefix}${ts}${rand}`
|
||||
}
|
||||
|
||||
// ---- Tab1:赋余额 ----
|
||||
const balanceLoading = ref(false)
|
||||
const balanceFormRef = ref()
|
||||
const balanceForm = reactive<{
|
||||
userId?: number
|
||||
amountYuan?: number
|
||||
bizNo: string
|
||||
remark?: string
|
||||
}>({
|
||||
userId: undefined,
|
||||
amountYuan: undefined,
|
||||
bizNo: genBizNo('GR'),
|
||||
remark: ''
|
||||
})
|
||||
const balanceRules = reactive({
|
||||
userId: [{ required: true, message: '创作者用户ID不能为空', trigger: 'blur' }],
|
||||
amountYuan: [{ required: true, message: '赋余额金额不能为空', trigger: 'blur' }],
|
||||
bizNo: [{ required: true, message: '业务单号(幂等键)不能为空', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 提交赋余额(资金操作:二次确认 + 携带 bizNo 幂等键) */
|
||||
const submitBalance = async () => {
|
||||
if (!balanceFormRef.value) return
|
||||
const valid = await balanceFormRef.value.validate()
|
||||
if (!valid) return
|
||||
const amount = yuan2fen(balanceForm.amountYuan)
|
||||
if (amount <= 0) {
|
||||
message.warning('赋余额金额必须大于 0')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await message.confirm(
|
||||
`确认给创作者 #${balanceForm.userId} 赋余额 ${(balanceForm.amountYuan ?? 0).toFixed(2)} 元(${amount} 分)?业务单号:${balanceForm.bizNo}`
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
balanceLoading.value = true
|
||||
try {
|
||||
const grantId = await TradeApi.grantBalance({
|
||||
userId: balanceForm.userId as number,
|
||||
amount,
|
||||
bizNo: balanceForm.bizNo,
|
||||
remark: balanceForm.remark
|
||||
})
|
||||
message.success(`赋余额成功,流水 ID:${grantId}`)
|
||||
// 成功后重置表单 + 换新幂等键,避免误重复提交同一单号
|
||||
balanceForm.userId = undefined
|
||||
balanceForm.amountYuan = undefined
|
||||
balanceForm.remark = ''
|
||||
balanceForm.bizNo = genBizNo('GR')
|
||||
balanceFormRef.value?.clearValidate?.()
|
||||
} finally {
|
||||
balanceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Tab2:赋订阅 ----
|
||||
const subLoading = ref(false)
|
||||
const subFormRef = ref()
|
||||
// 套餐选项与默认天数(月度30/季度90/年度365;可手改)
|
||||
const planOptions = [
|
||||
{ label: '月度(30天)', value: SubscriptionPlanEnum.MONTHLY, days: 30 },
|
||||
{ label: '季度(90天)', value: SubscriptionPlanEnum.QUARTERLY, days: 90 },
|
||||
{ label: '年度(365天)', value: SubscriptionPlanEnum.YEARLY, days: 365 }
|
||||
]
|
||||
const subForm = reactive<{
|
||||
userId?: number
|
||||
plan?: number
|
||||
durationDays?: number
|
||||
bizNo: string
|
||||
remark?: string
|
||||
}>({
|
||||
userId: undefined,
|
||||
plan: undefined,
|
||||
durationDays: undefined,
|
||||
bizNo: genBizNo('SUB'),
|
||||
remark: ''
|
||||
})
|
||||
const subRules = reactive({
|
||||
userId: [{ required: true, message: '订阅用户ID不能为空', trigger: 'blur' }],
|
||||
plan: [{ required: true, message: '订阅套餐不能为空', trigger: 'change' }],
|
||||
durationDays: [{ required: true, message: '订阅时长不能为空', trigger: 'blur' }],
|
||||
bizNo: [{ required: true, message: '业务单号(幂等键)不能为空', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 选套餐时自动带默认天数(仅当用户未手填时) */
|
||||
const onPlanChange = (plan: number) => {
|
||||
const opt = planOptions.find((p) => p.value === plan)
|
||||
if (opt && subForm.durationDays == null) subForm.durationDays = opt.days
|
||||
}
|
||||
|
||||
/** 提交赋订阅(资金操作:二次确认 + 携带 bizNo 幂等键) */
|
||||
const submitSubscription = async () => {
|
||||
if (!subFormRef.value) return
|
||||
const valid = await subFormRef.value.validate()
|
||||
if (!valid) return
|
||||
const planLabel = planOptions.find((p) => p.value === subForm.plan)?.label ?? subForm.plan
|
||||
try {
|
||||
await message.confirm(
|
||||
`确认给用户 #${subForm.userId} 开通【${planLabel}】订阅 ${subForm.durationDays} 天?业务单号:${subForm.bizNo}`
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
subLoading.value = true
|
||||
try {
|
||||
const expireTime = await TradeApi.grantSubscription({
|
||||
userId: subForm.userId as number,
|
||||
plan: subForm.plan as number,
|
||||
durationDays: subForm.durationDays as number,
|
||||
bizNo: subForm.bizNo,
|
||||
remark: subForm.remark
|
||||
})
|
||||
message.success(`赋订阅成功,到期时间:${expireTime}`)
|
||||
// 成功后重置 + 换新幂等键
|
||||
subForm.userId = undefined
|
||||
subForm.plan = undefined
|
||||
subForm.durationDays = undefined
|
||||
subForm.remark = ''
|
||||
subForm.bizNo = genBizNo('SUB')
|
||||
subFormRef.value?.clearValidate?.()
|
||||
} finally {
|
||||
subLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wanxiang-grant-tabs {
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
169
game-admin/src/views/wanxiang/operationDashboard/index.vue
Normal file
169
game-admin/src/views/wanxiang/operationDashboard/index.vue
Normal file
@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<!-- 经营看板:跨模块只读聚合(DAU + GMV + 生成量 + 发布量),区间维度 -->
|
||||
<ContentWrap>
|
||||
<el-form class="-mb-15px" :inline="true" label-width="68px">
|
||||
<el-form-item label="统计区间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日"
|
||||
end-placeholder="结束日"
|
||||
@change="loadDashboard"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="loadDashboard"><Icon icon="ep:search" class="mr-5px" /> 查询</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['telemetry:dashboard:query']"
|
||||
@click="handleExport"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 降级提示:有跨模块数据源不可用时(对应指标已兜底 0) -->
|
||||
<el-alert
|
||||
v-if="dashboard.degraded"
|
||||
class="mb-12px"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="`部分数据源不可用,对应指标已兜底为 0:${(dashboard.degradedSources || []).join('、') || '未知'}`"
|
||||
/>
|
||||
|
||||
<!-- 关键指标卡 -->
|
||||
<el-row :gutter="16" v-loading="loading">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="metric-label">DAU 日活(结束日当天)</div>
|
||||
<div class="metric-value">{{ formatNumber(dashboard.dau) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="metric-label">GMV 区间营收(元)</div>
|
||||
<div class="metric-value">{{ fen2yuan(dashboard.gmv) }}</div>
|
||||
<div class="metric-sub">分账前原始收入</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="metric-label">区间生成量</div>
|
||||
<div class="metric-value">{{ formatNumber(dashboard.generatedCount) }}</div>
|
||||
<div class="metric-sub">aigc 成功生成任务数</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never">
|
||||
<div class="metric-label">区间发布量</div>
|
||||
<div class="metric-value">{{ formatNumber(dashboard.publishedCount) }}</div>
|
||||
<div class="metric-sub">project 已发布项目数</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import download from '@/utils/download'
|
||||
import * as TelemetryApi from '@/api/wanxiang/telemetry'
|
||||
|
||||
defineOptions({ name: 'WanxiangOperationDashboard' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const loading = ref(false) // 看板加载中
|
||||
const exportLoading = ref(false) // 导出加载中
|
||||
const dateRange = ref<string[]>([]) // 统计区间 [start, end]
|
||||
const dashboard = reactive<TelemetryApi.DashboardRespVO>({
|
||||
dau: 0,
|
||||
gmv: 0,
|
||||
generatedCount: 0,
|
||||
publishedCount: 0,
|
||||
degraded: false,
|
||||
degradedSources: []
|
||||
})
|
||||
|
||||
/** 千分位格式化 */
|
||||
const formatNumber = (n?: number): string => (n ?? 0).toLocaleString('zh-CN')
|
||||
|
||||
/** 金额:分 → 元(GMV 单位为分;展示转元,千分位 2 位小数) */
|
||||
const fen2yuan = (fen?: number): string =>
|
||||
((fen ?? 0) / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
/** 区间默认值:未选时取近 7 日(含今日) */
|
||||
const ensureRange = (): [string, string] => {
|
||||
let start = dateRange.value?.[0]
|
||||
let end = dateRange.value?.[1]
|
||||
if (!start || !end) {
|
||||
const today = new Date()
|
||||
const ago = new Date(today.getTime() - 6 * 24 * 3600 * 1000)
|
||||
const fmt = (d: Date) => d.toISOString().slice(0, 10)
|
||||
start = fmt(ago)
|
||||
end = fmt(today)
|
||||
dateRange.value = [start, end]
|
||||
}
|
||||
return [start, end]
|
||||
}
|
||||
|
||||
/** 加载经营看板聚合 */
|
||||
const loadDashboard = async () => {
|
||||
const [start, end] = ensureRange()
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await TelemetryApi.getDashboard({ statDateStart: start, statDateEnd: end })
|
||||
dashboard.statDateStart = data.statDateStart
|
||||
dashboard.statDateEnd = data.statDateEnd
|
||||
dashboard.dau = data.dau
|
||||
dashboard.gmv = data.gmv
|
||||
dashboard.generatedCount = data.generatedCount
|
||||
dashboard.publishedCount = data.publishedCount
|
||||
dashboard.degraded = data.degraded
|
||||
dashboard.degradedSources = data.degradedSources
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出经营看板 Excel(区间快照) */
|
||||
const handleExport = async () => {
|
||||
const [start, end] = ensureRange()
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
exportLoading.value = true
|
||||
const data = await TelemetryApi.exportDashboardExcel({ statDateStart: start, statDateEnd: end })
|
||||
if (data) download.excel(data as Blob, '经营看板.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化:默认加载近 7 日看板 */
|
||||
onMounted(() => {
|
||||
loadDashboard()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.metric-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.metric-value {
|
||||
margin-top: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.metric-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
@ -128,6 +128,15 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="loadRevenueReport"><Icon icon="ep:search" class="mr-5px" /> 查询</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
:loading="revExportLoading"
|
||||
v-hasPermi="['trade:report:export']"
|
||||
@click="handleRevenueExport"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
@ -187,6 +196,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as TradeApi from '@/api/wanxiang/trade'
|
||||
import { WithdrawStatusEnum, WithdrawAuditDecisionEnum } from '@/api/wanxiang/trade'
|
||||
|
||||
@ -324,6 +334,7 @@ const handleIncomeQuery = () => {
|
||||
|
||||
// ---- Tab3:平台营收报表(只读) ----
|
||||
const revLoading = ref(false)
|
||||
const revExportLoading = ref(false) // 营收报表导出加载中
|
||||
const revenueRange = ref<string[]>([])
|
||||
const revenue = reactive<TradeApi.RevenueReportVO>({
|
||||
totalGross: 0,
|
||||
@ -357,6 +368,30 @@ const loadRevenueReport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出平台营收报表 Excel(区间单行聚合;口径同 /report/revenue) */
|
||||
const handleRevenueExport = async () => {
|
||||
// 复用区间默认逻辑:未选区间先补近 7 日
|
||||
let start = revenueRange.value?.[0]
|
||||
let end = revenueRange.value?.[1]
|
||||
if (!start || !end) {
|
||||
const today = new Date()
|
||||
const ago = new Date(today.getTime() - 6 * 24 * 3600 * 1000)
|
||||
const fmt = (d: Date) => d.toISOString().slice(0, 10)
|
||||
start = fmt(ago)
|
||||
end = fmt(today)
|
||||
revenueRange.value = [start, end]
|
||||
}
|
||||
try {
|
||||
await message.exportConfirm()
|
||||
revExportLoading.value = true
|
||||
const data = await TradeApi.exportRevenueExcel({ statDateStart: start, statDateEnd: end })
|
||||
if (data) download.excel(data as Blob, '平台营收报表.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
revExportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 切换时懒加载对应数据
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'income' && !incLoaded.value) getIncomeList()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user