This commit is contained in:
zpc 2026-02-04 02:03:25 +08:00
parent f0bbcef48b
commit 190302b318
9 changed files with 1020 additions and 3 deletions

View File

@ -11,9 +11,9 @@
// 测试环境配置 - .NET 10 后端
const testing = {
// baseUrl: 'https://app.zpc-xy.com/honey/api',
baseUrl: 'https://app.zpc-xy.com/honey/api',
// baseUrl: 'http://192.168.1.24:5238',
baseUrl: 'http://192.168.195.15:2822',
// baseUrl: 'http://192.168.195.15:2822',
imageUrl: 'https://youdas-1308826010.cos.ap-shanghai.myqcloud.com',
loginPage: '',
wxAppId: ''

View File

@ -0,0 +1,43 @@
-- 盒子利润统计菜单种子数据
-- 执行前请确保统计报表目录菜单已存在
USE honey_box_admin;
GO
-- 查找统计报表目录的ID
DECLARE @statisticsMenuId BIGINT;
SELECT @statisticsMenuId = Id FROM menus WHERE Path = '/business/statistics' AND MenuType = 1;
-- 如果统计报表目录不存在,先创建它
IF @statisticsMenuId IS NULL
BEGIN
DECLARE @businessMenuId BIGINT;
SELECT @businessMenuId = Id FROM menus WHERE Path = '/business' AND MenuType = 1;
INSERT INTO menus (Name, Path, Component, Icon, SortOrder, MenuType, Status, ParentId, Permission, CreatedAt, UpdatedAt, IsExternal, IsCache)
VALUES (N'统计报表', '/business/statistics', NULL, 'DataAnalysis', 80, 1, 1, @businessMenuId, NULL, GETDATE(), GETDATE(), 0, 0);
SET @statisticsMenuId = SCOPE_IDENTITY();
PRINT N'创建统计报表目录菜单ID: ' + CAST(@statisticsMenuId AS NVARCHAR(10));
END
-- 检查盒子利润统计菜单是否已存在
IF NOT EXISTS (SELECT 1 FROM menus WHERE Path = '/business/statistics/box-profit')
BEGIN
INSERT INTO menus (Name, Path, Component, Icon, SortOrder, MenuType, Status, ParentId, Permission, CreatedAt, UpdatedAt, IsExternal, IsCache)
VALUES (N'盒子利润统计', '/business/statistics/box-profit', 'business/statistics/box-profit', 'TrendCharts', 2, 2, 1,
@statisticsMenuId, 'statistics:box-profit', GETDATE(), GETDATE(), 0, 1);
PRINT N'创建盒子利润统计菜单成功';
END
ELSE
BEGIN
PRINT N'盒子利润统计菜单已存在,跳过创建';
END
-- 查看结果
SELECT Id, Name, Path, Component, Permission, ParentId, SortOrder, MenuType, Status
FROM menus
WHERE Path LIKE '/business/statistics%'
ORDER BY ParentId, SortOrder;
GO

View File

@ -1,4 +1,5 @@
using HoneyBox.Admin.Business.Attributes;
using HoneyBox.Admin.Business.Models.Statistics;
using HoneyBox.Admin.Business.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
@ -64,4 +65,56 @@ public class StatisticsController : BusinessControllerBase
var result = await _statisticsService.GetUserStatsAsync();
return Ok(result);
}
/// <summary>
/// 获取单个盒子的统计数据
/// </summary>
/// <param name="goodsId">盒子ID</param>
/// <param name="startTime">开始时间</param>
/// <param name="endTime">结束时间</param>
/// <returns>盒子统计数据</returns>
[HttpGet("box-statistics")]
[BusinessPermission("statistics:view")]
public async Task<IActionResult> GetBoxStatistics(
[FromQuery] int goodsId,
[FromQuery] DateTime? startTime,
[FromQuery] DateTime? endTime)
{
var request = new BoxStatisticsRequest
{
GoodsId = goodsId,
StartTime = startTime,
EndTime = endTime
};
var result = await _statisticsService.GetBoxStatisticsAsync(request);
return Ok(new { code = 0, msg = "获取成功", data = result });
}
/// <summary>
/// 获取盒子利润统计列表
/// </summary>
/// <param name="request">请求参数</param>
/// <returns>盒子利润统计列表</returns>
[HttpGet("box-profit-list")]
[BusinessPermission("statistics:view")]
public async Task<IActionResult> GetBoxProfitList([FromQuery] BoxProfitListRequest request)
{
var result = await _statisticsService.GetBoxProfitListAsync(request);
return Ok(new
{
code = 0,
msg = "获取数据成功",
count = result.Total,
data = result.List,
summary = new BoxProfitSummary
{
TotalIncome = 0,
TotalCost = 0,
TotalProfit = 0,
TotalReMoney = 0,
TotalFhMoney = 0
}
});
}
}

View File

@ -0,0 +1,236 @@
namespace HoneyBox.Admin.Business.Models.Statistics;
/// <summary>
/// 盒子统计请求
/// </summary>
public class BoxStatisticsRequest
{
/// <summary>
/// 盒子ID
/// </summary>
public int GoodsId { get; set; }
/// <summary>
/// 开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public DateTime? EndTime { get; set; }
}
/// <summary>
/// 盒子统计响应
/// </summary>
public class BoxStatisticsResponse
{
/// <summary>
/// 盒子ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 消费金额(充值金额 + 余额消费)
/// </summary>
public decimal UseMoney { get; set; }
/// <summary>
/// 出货成本(奖品价值)
/// </summary>
public decimal ScMoney { get; set; }
/// <summary>
/// 兑换成本(已回收的奖品价值)
/// </summary>
public decimal ReMoney { get; set; }
/// <summary>
/// 发货成本(已发货的奖品价值)
/// </summary>
public decimal FhMoney { get; set; }
/// <summary>
/// 抽奖次数
/// </summary>
public int CjCount { get; set; }
/// <summary>
/// 利润 = UseMoney - (ScMoney - ReMoney)
/// </summary>
public decimal Profit { get; set; }
/// <summary>
/// 利润率
/// </summary>
public decimal ProfitRate { get; set; }
/// <summary>
/// 是否亏损
/// </summary>
public bool IsNegative { get; set; }
}
/// <summary>
/// 盒子利润统计列表请求
/// </summary>
public class BoxProfitListRequest : PagedRequest
{
/// <summary>
/// 盒子ID
/// </summary>
public int? GoodsId { get; set; }
/// <summary>
/// 盒子名称
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 状态0-下架1-上架
/// </summary>
public int? Status { get; set; }
/// <summary>
/// 盒子类型
/// </summary>
public int? Type { get; set; }
/// <summary>
/// 开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public DateTime? EndTime { get; set; }
}
/// <summary>
/// 盒子利润统计列表项
/// </summary>
public class BoxProfitItem
{
/// <summary>
/// 盒子ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 盒子名称
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 盒子图片
/// </summary>
public string? ImgUrl { get; set; }
/// <summary>
/// 单价
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 库存
/// </summary>
public int Stock { get; set; }
/// <summary>
/// 状态0-下架1-上架
/// </summary>
public int Status { get; set; }
/// <summary>
/// 状态名称
/// </summary>
public string StatusName => Status == 1 ? "上架" : "下架";
/// <summary>
/// 盒子类型
/// </summary>
public int Type { get; set; }
/// <summary>
/// 类型名称
/// </summary>
public string TypeName { get; set; } = string.Empty;
/// <summary>
/// 消费金额
/// </summary>
public decimal UseMoney { get; set; }
/// <summary>
/// 出货成本
/// </summary>
public decimal ScMoney { get; set; }
/// <summary>
/// 兑换成本
/// </summary>
public decimal ReMoney { get; set; }
/// <summary>
/// 发货成本
/// </summary>
public decimal FhMoney { get; set; }
/// <summary>
/// 抽奖次数
/// </summary>
public int CjCount { get; set; }
/// <summary>
/// 利润
/// </summary>
public decimal Profit { get; set; }
/// <summary>
/// 利润率
/// </summary>
public decimal ProfitRate { get; set; }
/// <summary>
/// 是否亏损
/// </summary>
public bool IsNegative { get; set; }
/// <summary>
/// 是否已加载统计数据
/// </summary>
public bool Loaded { get; set; }
}
/// <summary>
/// 盒子利润汇总
/// </summary>
public class BoxProfitSummary
{
/// <summary>
/// 总收入
/// </summary>
public decimal TotalIncome { get; set; }
/// <summary>
/// 总成本
/// </summary>
public decimal TotalCost { get; set; }
/// <summary>
/// 总利润
/// </summary>
public decimal TotalProfit { get; set; }
/// <summary>
/// 总兑换金额
/// </summary>
public decimal TotalReMoney { get; set; }
/// <summary>
/// 总发货金额
/// </summary>
public decimal TotalFhMoney { get; set; }
}

View File

@ -1,3 +1,4 @@
using HoneyBox.Admin.Business.Models;
using HoneyBox.Admin.Business.Models.Statistics;
namespace HoneyBox.Admin.Business.Services.Interfaces;
@ -30,4 +31,18 @@ public interface IStatisticsService
/// </summary>
/// <returns>用户统计</returns>
Task<UserStatsResponse> GetUserStatsAsync();
/// <summary>
/// 获取单个盒子的统计数据
/// </summary>
/// <param name="request">请求参数</param>
/// <returns>盒子统计数据</returns>
Task<BoxStatisticsResponse> GetBoxStatisticsAsync(BoxStatisticsRequest request);
/// <summary>
/// 获取盒子利润统计列表
/// </summary>
/// <param name="request">请求参数</param>
/// <returns>盒子利润统计列表</returns>
Task<PagedResult<BoxProfitItem>> GetBoxProfitListAsync(BoxProfitListRequest request);
}

View File

@ -1,3 +1,4 @@
using HoneyBox.Admin.Business.Models;
using HoneyBox.Admin.Business.Models.Statistics;
using HoneyBox.Admin.Business.Services.Interfaces;
using HoneyBox.Model.Data;
@ -14,6 +15,20 @@ public class StatisticsService : IStatisticsService
private readonly HoneyBoxDbContext _dbContext;
private readonly ILogger<StatisticsService> _logger;
// 盒子类型名称映射
private static readonly Dictionary<int, string> BoxTypeNames = new()
{
{ 1, "一番赏" },
{ 2, "无限赏" },
{ 3, "擂台赏" },
{ 4, "抽卡机" },
{ 5, "福袋" },
{ 6, "幸运赏" },
{ 8, "盲盒" },
{ 9, "扭蛋" },
{ 15, "福利屋" }
};
public StatisticsService(HoneyBoxDbContext dbContext, ILogger<StatisticsService> logger)
{
_dbContext = dbContext;
@ -386,4 +401,186 @@ public class StatisticsService : IStatisticsService
ShippedAmount = shippedAmount
};
}
/// <summary>
/// 获取单个盒子的统计数据
/// </summary>
public async Task<BoxStatisticsResponse> GetBoxStatisticsAsync(BoxStatisticsRequest request)
{
if (request.GoodsId <= 0)
{
throw new ArgumentException("盒子ID无效");
}
// 验证盒子存在
var goodsExists = await _dbContext.Goods.AnyAsync(g => g.Id == request.GoodsId && g.DeletedAt == null);
if (!goodsExists)
{
throw new ArgumentException("盒子不存在");
}
// 获取测试用户ID列表
var testUserIds = await _dbContext.Users
.Where(u => u.IsTest > 0 || u.Status == 2)
.Select(u => u.Id)
.ToListAsync();
if (!testUserIds.Any())
{
testUserIds = new List<int> { 0 };
}
// 构建时间范围条件转换为Unix时间戳
var hasTimeRange = request.StartTime.HasValue && request.EndTime.HasValue;
var startTimestamp = hasTimeRange ? (int)((DateTimeOffset)request.StartTime!.Value).ToUnixTimeSeconds() : 0;
var endTimestamp = hasTimeRange ? (int)((DateTimeOffset)request.EndTime!.Value).ToUnixTimeSeconds() : 0;
// 查询1获取消费金额充值金额 + 余额消费)
var orderQuery = _dbContext.Orders
.Where(o => o.Status == 1)
.Where(o => o.Price > 0 || o.UseMoney > 0)
.Where(o => o.GoodsId == request.GoodsId)
.Where(o => !testUserIds.Contains(o.UserId));
if (hasTimeRange)
{
orderQuery = orderQuery.Where(o => o.PayTime > startTimestamp && o.PayTime < endTimestamp);
}
var priceSum = await orderQuery.SumAsync(o => (decimal?)o.Price) ?? 0;
var useMoneySum = await orderQuery.SumAsync(o => (decimal?)o.UseMoney) ?? 0;
var useMoney = priceSum + useMoneySum;
// 查询2获取出货成本奖品价值
var scMoneyQuery = _dbContext.OrderItems
.Where(oi => oi.GoodsId == request.GoodsId)
.Where(oi => !testUserIds.Contains(oi.UserId));
if (hasTimeRange)
{
scMoneyQuery = scMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
}
var scMoney = await scMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
// 查询3获取兑换成本已回收的奖品价值status=1
var reMoneyQuery = _dbContext.OrderItems
.Where(oi => oi.GoodsId == request.GoodsId)
.Where(oi => oi.Status == 1)
.Where(oi => !testUserIds.Contains(oi.UserId));
if (hasTimeRange)
{
reMoneyQuery = reMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
}
var reMoney = await reMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
// 查询4获取发货成本已发货的奖品价值status=2
var fhMoneyQuery = _dbContext.OrderItems
.Where(oi => oi.GoodsId == request.GoodsId)
.Where(oi => oi.Status == 2)
.Where(oi => !testUserIds.Contains(oi.UserId));
if (hasTimeRange)
{
fhMoneyQuery = fhMoneyQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
}
var fhMoney = await fhMoneyQuery.SumAsync(oi => (decimal?)oi.GoodslistMoney) ?? 0;
// 查询5获取抽奖次数parent_goods_list_id = 0 表示主抽奖记录)
var cjCountQuery = _dbContext.OrderItems
.Where(oi => oi.GoodsId == request.GoodsId)
.Where(oi => !testUserIds.Contains(oi.UserId))
.Where(oi => oi.ParentGoodsListId == 0);
if (hasTimeRange)
{
cjCountQuery = cjCountQuery.Where(oi => oi.CreatedAt > request.StartTime && oi.CreatedAt < request.EndTime);
}
var cjCount = await cjCountQuery.CountAsync();
// 计算利润和利润率
var profit = useMoney - (scMoney - reMoney);
var profitRate = useMoney > 0 ? Math.Round((profit / useMoney) * 100, 2) : 0;
return new BoxStatisticsResponse
{
Id = request.GoodsId,
UseMoney = useMoney,
ScMoney = scMoney,
ReMoney = reMoney,
FhMoney = fhMoney,
CjCount = cjCount,
Profit = profit,
ProfitRate = profitRate,
IsNegative = profit < 0
};
}
/// <summary>
/// 获取盒子利润统计列表
/// </summary>
public async Task<PagedResult<BoxProfitItem>> GetBoxProfitListAsync(BoxProfitListRequest request)
{
// 构建查询条件
var query = _dbContext.Goods
.AsNoTracking()
.Where(g => g.DeletedAt == null);
if (request.GoodsId.HasValue && request.GoodsId > 0)
{
query = query.Where(g => g.Id == request.GoodsId);
}
if (!string.IsNullOrWhiteSpace(request.Title))
{
query = query.Where(g => g.Title.Contains(request.Title));
}
if (request.Status.HasValue)
{
query = query.Where(g => g.Status == request.Status);
}
if (request.Type.HasValue && request.Type > 0)
{
query = query.Where(g => g.Type == request.Type);
}
// 获取总数
var total = await query.CountAsync();
// 分页获取盒子列表
var goods = await query
.OrderByDescending(g => g.Id)
.Skip(request.Skip)
.Take(request.PageSize)
.Select(g => new BoxProfitItem
{
Id = g.Id,
Title = g.Title,
ImgUrl = g.ImgUrl,
Price = g.Price,
Stock = g.Stock,
Status = g.Status,
Type = g.Type,
TypeName = BoxTypeNames.ContainsKey(g.Type) ? BoxTypeNames[g.Type] : "未知类型",
// 统计数据初始化为0后续异步加载
UseMoney = 0,
ScMoney = 0,
ReMoney = 0,
FhMoney = 0,
CjCount = 0,
Profit = 0,
ProfitRate = 0,
IsNegative = false,
Loaded = false
})
.ToListAsync();
return PagedResult<BoxProfitItem>.Create(goods, total, request.Page, request.PageSize);
}
}

View File

@ -163,3 +163,160 @@ export function getUserStats(): Promise<ApiResponse<UserStats>> {
method: 'get'
})
}
// ==================== 盒子统计类型定义 ====================
/**
*
*/
export interface BoxStatisticsParams {
/** 盒子ID */
goodsId: number
/** 开始时间 */
startTime?: string
/** 结束时间 */
endTime?: string
}
/**
*
*/
export interface BoxStatistics {
/** 盒子ID */
id: number
/** 消费金额(充值金额 + 余额消费) */
useMoney: number
/** 出货成本(奖品价值) */
scMoney: number
/** 兑换成本(已回收的奖品价值) */
reMoney: number
/** 发货成本(已发货的奖品价值) */
fhMoney: number
/** 抽奖次数 */
cjCount: number
/** 利润 */
profit: number
/** 利润率 */
profitRate: number
/** 是否亏损 */
isNegative: boolean
}
/**
*
*/
export interface BoxProfitListParams {
/** 页码 */
page?: number
/** 每页数量 */
pageSize?: number
/** 盒子ID */
goodsId?: number
/** 盒子名称 */
title?: string
/** 状态 */
status?: number
/** 盒子类型 */
type?: number
/** 开始时间 */
startTime?: string
/** 结束时间 */
endTime?: string
}
/**
*
*/
export interface BoxProfitItem {
/** 盒子ID */
id: number
/** 盒子名称 */
title: string
/** 盒子图片 */
imgUrl: string
/** 单价 */
price: number
/** 库存 */
stock: number
/** 状态 */
status: number
/** 状态名称 */
statusName: string
/** 盒子类型 */
type: number
/** 类型名称 */
typeName: string
/** 消费金额 */
useMoney: number
/** 出货成本 */
scMoney: number
/** 兑换成本 */
reMoney: number
/** 发货成本 */
fhMoney: number
/** 抽奖次数 */
cjCount: number
/** 利润 */
profit: number
/** 利润率 */
profitRate: number
/** 是否亏损 */
isNegative: boolean
/** 是否已加载统计数据 */
loaded: boolean
}
/**
*
*/
export interface BoxProfitSummary {
/** 总收入 */
totalIncome: number
/** 总成本 */
totalCost: number
/** 总利润 */
totalProfit: number
/** 总兑换金额 */
totalReMoney: number
/** 总发货金额 */
totalFhMoney: number
}
/**
*
*/
export interface BoxProfitListResponse {
code: number
msg: string
count: number
data: BoxProfitItem[]
summary: BoxProfitSummary
}
// ==================== 盒子统计 API ====================
/**
*
* @param params
* @returns
*/
export function getBoxStatistics(params: BoxStatisticsParams): Promise<ApiResponse<{ code: number; msg: string; data: BoxStatistics }>> {
return request({
url: `${STATISTICS_BASE_URL}/box-statistics`,
method: 'get',
params
})
}
/**
*
* @param params
* @returns
*/
export function getBoxProfitList(params: BoxProfitListParams): Promise<ApiResponse<BoxProfitListResponse>> {
return request({
url: `${STATISTICS_BASE_URL}/box-profit-list`,
method: 'get',
params
})
}

View File

@ -589,6 +589,16 @@ export const businessRoutes: RouteRecordRaw[] = [
permission: 'statistics:data-stand',
keepAlive: true
}
},
{
path: 'box-profit',
name: 'BoxProfit',
component: () => import('@/views/business/statistics/box-profit.vue'),
meta: {
title: '盒子利润统计',
permission: 'statistics:box-profit',
keepAlive: true
}
}
]
}
@ -1199,7 +1209,9 @@ export const welfareTaskPermissions = {
*/
export const statisticsPermissions = {
// 数据看板
dataStand: 'statistics:data-stand'
dataStand: 'statistics:data-stand',
// 盒子利润统计
boxProfit: 'statistics:box-profit'
}
export default businessRoutes

View File

@ -0,0 +1,304 @@
<template>
<div class="box-profit-container">
<!-- 搜索表单 -->
<el-card class="search-card" shadow="never">
<el-form :model="searchForm" inline>
<el-form-item label="盒子ID">
<el-input v-model.number="searchForm.goodsId" placeholder="请输入盒子ID" clearable style="width: 120px" />
</el-form-item>
<el-form-item label="盒子名称">
<el-input v-model="searchForm.title" placeholder="请输入盒子名称" clearable style="width: 160px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100px">
<el-option label="上架" :value="1" />
<el-option label="下架" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="盒子类型">
<el-select v-model="searchForm.type" placeholder="全部" clearable style="width: 120px">
<el-option v-for="item in boxTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card class="table-card" shadow="never">
<el-table :data="tableData" v-loading="loading" stripe border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="title" label="盒子名称" min-width="180">
<template #default="{ row }">
<div class="goods-info">
<el-image v-if="row.imgUrl" :src="row.imgUrl" fit="cover" class="goods-img" />
<span class="goods-title">{{ row.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="typeName" label="类型" width="100" align="center" />
<el-table-column prop="price" label="单价" width="80" align="right">
<template #default="{ row }">¥{{ row.price.toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="statusName" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.statusName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="cjCount" label="抽奖次数" width="100" align="right">
<template #default="{ row }">
<span v-if="row.loaded">{{ row.cjCount }}</span>
<el-button v-else type="primary" link size="small" @click="loadBoxStats(row)">加载</el-button>
</template>
</el-table-column>
<el-table-column prop="useMoney" label="消费金额" width="120" align="right">
<template #default="{ row }">
<span v-if="row.loaded">¥{{ row.useMoney.toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="scMoney" label="出货成本" width="120" align="right">
<template #default="{ row }">
<span v-if="row.loaded">¥{{ row.scMoney.toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="reMoney" label="兑换成本" width="120" align="right">
<template #default="{ row }">
<span v-if="row.loaded">¥{{ row.reMoney.toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="fhMoney" label="发货成本" width="120" align="right">
<template #default="{ row }">
<span v-if="row.loaded">¥{{ row.fhMoney.toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="profit" label="利润" width="120" align="right">
<template #default="{ row }">
<span v-if="row.loaded" :class="{ 'text-danger': row.isNegative, 'text-success': !row.isNegative }">
¥{{ row.profit.toFixed(2) }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="profitRate" label="利润率" width="100" align="right">
<template #default="{ row }">
<span v-if="row.loaded" :class="{ 'text-danger': row.isNegative, 'text-success': !row.isNegative }">
{{ row.profitRate.toFixed(2) }}%
</span>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Search, Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getBoxProfitList, getBoxStatistics, type BoxProfitItem } from '@/api/business/statistics'
//
const boxTypes = [
{ value: 1, label: '一番赏' },
{ value: 2, label: '无限赏' },
{ value: 3, label: '擂台赏' },
{ value: 4, label: '抽卡机' },
{ value: 5, label: '福袋' },
{ value: 6, label: '幸运赏' },
{ value: 8, label: '盲盒' },
{ value: 9, label: '扭蛋' },
{ value: 15, label: '福利屋' }
]
//
const searchForm = reactive({
goodsId: undefined as number | undefined,
title: '',
status: undefined as number | undefined,
type: undefined as number | undefined
})
const dateRange = ref<[string, string] | null>(null)
//
const tableData = ref<BoxProfitItem[]>([])
const loading = ref(false)
//
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
//
async function loadData() {
loading.value = true
try {
const params: any = {
page: pagination.page,
pageSize: pagination.pageSize
}
if (searchForm.goodsId) params.goodsId = searchForm.goodsId
if (searchForm.title) params.title = searchForm.title
if (searchForm.status !== undefined) params.status = searchForm.status
if (searchForm.type) params.type = searchForm.type
if (dateRange.value) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const res = await getBoxProfitList(params) as any
if (res.code === 0) {
tableData.value = res.data
pagination.total = res.count
}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
//
async function loadBoxStats(row: BoxProfitItem) {
try {
const params: any = { goodsId: row.id }
if (dateRange.value) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const res = await getBoxStatistics(params) as any
if (res.code === 0 && res.data) {
const stats = res.data
row.useMoney = stats.useMoney
row.scMoney = stats.scMoney
row.reMoney = stats.reMoney
row.fhMoney = stats.fhMoney
row.cjCount = stats.cjCount
row.profit = stats.profit
row.profitRate = stats.profitRate
row.isNegative = stats.isNegative
row.loaded = true
}
} catch (error) {
console.error('加载盒子统计失败:', error)
ElMessage.error('加载盒子统计失败')
}
}
//
function handleSearch() {
pagination.page = 1
loadData()
}
//
function handleReset() {
searchForm.goodsId = undefined
searchForm.title = ''
searchForm.status = undefined
searchForm.type = undefined
dateRange.value = null
pagination.page = 1
loadData()
}
//
function handleSizeChange(size: number) {
pagination.pageSize = size
pagination.page = 1
loadData()
}
//
function handleCurrentChange(page: number) {
pagination.page = page
loadData()
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.box-profit-container {
padding: 20px;
}
.search-card {
margin-bottom: 16px;
}
.table-card {
margin-bottom: 16px;
}
.goods-info {
display: flex;
align-items: center;
gap: 8px;
}
.goods-img {
width: 40px;
height: 40px;
border-radius: 4px;
flex-shrink: 0;
}
.goods-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.text-danger {
color: #f56c6c;
}
.text-success {
color: #67c23a;
}
</style>