feat: enhance superadmin dashboard overview
This commit is contained in:
@@ -50,6 +50,20 @@ func (c *contents) ListTenantContents(ctx fiber.Ctx, tenantID int64, filter *dto
|
|||||||
return services.Super.ListContents(ctx, filter)
|
return services.Super.ListContents(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content statistics
|
||||||
|
//
|
||||||
|
// @Router /super/v1/contents/statistics [get]
|
||||||
|
// @Summary Content statistics
|
||||||
|
// @Description Content statistics
|
||||||
|
// @Tags Content
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} dto.SuperContentStatisticsResponse
|
||||||
|
// @Bind filter query
|
||||||
|
func (c *contents) Statistics(ctx fiber.Ctx, filter *dto.SuperContentStatisticsFilter) (*dto.SuperContentStatisticsResponse, error) {
|
||||||
|
return services.Super.ContentStatistics(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
// Update content status
|
// Update content status
|
||||||
//
|
//
|
||||||
// @Router /super/v1/tenants/:tenantID<int>/contents/:contentID<int>/status [patch]
|
// @Router /super/v1/tenants/:tenantID<int>/contents/:contentID<int>/status [patch]
|
||||||
|
|||||||
@@ -131,6 +131,18 @@ type SuperContentListFilter struct {
|
|||||||
Desc *string `query:"desc"`
|
Desc *string `query:"desc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuperContentStatisticsFilter 超管内容统计查询条件。
|
||||||
|
type SuperContentStatisticsFilter struct {
|
||||||
|
// TenantID 租户ID(不传代表全平台)。
|
||||||
|
TenantID *int64 `query:"tenant_id"`
|
||||||
|
// StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。
|
||||||
|
StartAt *string `query:"start_at"`
|
||||||
|
// EndAt 统计结束时间(RFC3339,可选;默认当前时间)。
|
||||||
|
EndAt *string `query:"end_at"`
|
||||||
|
// Granularity 统计粒度(day;目前仅支持 day)。
|
||||||
|
Granularity *string `query:"granularity"`
|
||||||
|
}
|
||||||
|
|
||||||
type SuperOrderListFilter struct {
|
type SuperOrderListFilter struct {
|
||||||
requests.Pagination
|
requests.Pagination
|
||||||
// ID 订单ID,精确匹配。
|
// ID 订单ID,精确匹配。
|
||||||
@@ -635,3 +647,19 @@ type AdminContentOwnerLite struct {
|
|||||||
// Status 用户状态。
|
// Status 用户状态。
|
||||||
Status consts.UserStatus `json:"status"`
|
Status consts.UserStatus `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuperContentStatisticsResponse 超管内容统计响应。
|
||||||
|
type SuperContentStatisticsResponse struct {
|
||||||
|
// TotalCount 内容总量。
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
// Trend 按天新增内容趋势。
|
||||||
|
Trend []SuperContentTrendItem `json:"trend"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuperContentTrendItem 内容新增趋势条目。
|
||||||
|
type SuperContentTrendItem struct {
|
||||||
|
// Date 日期(YYYY-MM-DD)。
|
||||||
|
Date string `json:"date"`
|
||||||
|
// CreatedCount 当日新增内容数量。
|
||||||
|
CreatedCount int64 `json:"created_count"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
r.contents.List,
|
r.contents.List,
|
||||||
Query[dto.SuperContentListFilter]("filter"),
|
Query[dto.SuperContentListFilter]("filter"),
|
||||||
))
|
))
|
||||||
|
r.log.Debugf("Registering route: Get /super/v1/contents/statistics -> contents.Statistics")
|
||||||
|
router.Get("/super/v1/contents/statistics"[len(r.Path()):], DataFunc1(
|
||||||
|
r.contents.Statistics,
|
||||||
|
Query[dto.SuperContentStatisticsFilter]("filter"),
|
||||||
|
))
|
||||||
r.log.Debugf("Registering route: Get /super/v1/tenants/:tenantID<int>/contents -> contents.ListTenantContents")
|
r.log.Debugf("Registering route: Get /super/v1/tenants/:tenantID<int>/contents -> contents.ListTenantContents")
|
||||||
router.Get("/super/v1/tenants/:tenantID<int>/contents"[len(r.Path()):], DataFunc2(
|
router.Get("/super/v1/tenants/:tenantID<int>/contents"[len(r.Path()):], DataFunc2(
|
||||||
r.contents.ListTenantContents,
|
r.contents.ListTenantContents,
|
||||||
|
|||||||
@@ -2059,6 +2059,74 @@ func (s *super) BatchReviewContents(ctx context.Context, operatorID int64, form
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *super) ContentStatistics(ctx context.Context, filter *super_dto.SuperContentStatisticsFilter) (*super_dto.SuperContentStatisticsResponse, error) {
|
||||||
|
// 统一统计时间范围与粒度,默认最近 7 天。
|
||||||
|
reportFilter := &super_dto.SuperReportOverviewFilter{}
|
||||||
|
if filter != nil {
|
||||||
|
reportFilter.TenantID = filter.TenantID
|
||||||
|
reportFilter.StartAt = filter.StartAt
|
||||||
|
reportFilter.EndAt = filter.EndAt
|
||||||
|
reportFilter.Granularity = filter.Granularity
|
||||||
|
}
|
||||||
|
rg, err := s.normalizeReportRange(reportFilter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID := int64(0)
|
||||||
|
if filter != nil && filter.TenantID != nil {
|
||||||
|
tenantID = *filter.TenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计内容总量,支持租户维度过滤。
|
||||||
|
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||||
|
if tenantID > 0 {
|
||||||
|
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||||
|
}
|
||||||
|
total, err := q.Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按天聚合新增内容数量,补齐趋势序列。
|
||||||
|
type contentAggRow struct {
|
||||||
|
Day time.Time `gorm:"column:day"`
|
||||||
|
Count int64 `gorm:"column:count"`
|
||||||
|
}
|
||||||
|
rows := make([]contentAggRow, 0)
|
||||||
|
query := models.ContentQuery.WithContext(ctx).
|
||||||
|
UnderlyingDB().
|
||||||
|
Model(&models.Content{}).
|
||||||
|
Select("date_trunc('day', created_at) as day, count(*) as count").
|
||||||
|
Where("created_at >= ? AND created_at < ?", rg.startDay, rg.endNext)
|
||||||
|
if tenantID > 0 {
|
||||||
|
query = query.Where("tenant_id = ?", tenantID)
|
||||||
|
}
|
||||||
|
if err := query.Group("day").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
trendMap := make(map[string]int64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
key := row.Day.Format("2006-01-02")
|
||||||
|
trendMap[key] = row.Count
|
||||||
|
}
|
||||||
|
|
||||||
|
trend := make([]super_dto.SuperContentTrendItem, 0)
|
||||||
|
for day := rg.startDay; !day.After(rg.endDay); day = day.AddDate(0, 0, 1) {
|
||||||
|
key := day.Format("2006-01-02")
|
||||||
|
trend = append(trend, super_dto.SuperContentTrendItem{
|
||||||
|
Date: key,
|
||||||
|
CreatedCount: trendMap[key],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &super_dto.SuperContentStatisticsResponse{
|
||||||
|
TotalCount: total,
|
||||||
|
Trend: trend,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) {
|
func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) {
|
||||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,29 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/contents/statistics": {
|
||||||
|
"get": {
|
||||||
|
"description": "Content statistics",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Content"
|
||||||
|
],
|
||||||
|
"summary": "Content statistics",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.SuperContentStatisticsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/contents/{id}/review": {
|
"/super/v1/contents/{id}/review": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Review content",
|
"description": "Review content",
|
||||||
@@ -6374,6 +6397,22 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperContentStatisticsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total_count": {
|
||||||
|
"description": "TotalCount 内容总量。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"trend": {
|
||||||
|
"description": "Trend 按天新增内容趋势。",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.SuperContentTrendItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperContentTenantLite": {
|
"dto.SuperContentTenantLite": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6391,6 +6430,19 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperContentTrendItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_count": {
|
||||||
|
"description": "CreatedCount 当日新增内容数量。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"description": "Date 日期(YYYY-MM-DD)。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperCouponGrantItem": {
|
"dto.SuperCouponGrantItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -161,6 +161,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/contents/statistics": {
|
||||||
|
"get": {
|
||||||
|
"description": "Content statistics",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Content"
|
||||||
|
],
|
||||||
|
"summary": "Content statistics",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.SuperContentStatisticsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/contents/{id}/review": {
|
"/super/v1/contents/{id}/review": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Review content",
|
"description": "Review content",
|
||||||
@@ -6368,6 +6391,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperContentStatisticsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total_count": {
|
||||||
|
"description": "TotalCount 内容总量。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"trend": {
|
||||||
|
"description": "Trend 按天新增内容趋势。",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.SuperContentTrendItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperContentTenantLite": {
|
"dto.SuperContentTenantLite": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6385,6 +6424,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperContentTrendItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_count": {
|
||||||
|
"description": "CreatedCount 当日新增内容数量。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"description": "Date 日期(YYYY-MM-DD)。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperCouponGrantItem": {
|
"dto.SuperCouponGrantItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1048,6 +1048,17 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- action
|
- action
|
||||||
type: object
|
type: object
|
||||||
|
dto.SuperContentStatisticsResponse:
|
||||||
|
properties:
|
||||||
|
total_count:
|
||||||
|
description: TotalCount 内容总量。
|
||||||
|
type: integer
|
||||||
|
trend:
|
||||||
|
description: Trend 按天新增内容趋势。
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.SuperContentTrendItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
dto.SuperContentTenantLite:
|
dto.SuperContentTenantLite:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
@@ -1060,6 +1071,15 @@ definitions:
|
|||||||
description: Name 租户名称。
|
description: Name 租户名称。
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.SuperContentTrendItem:
|
||||||
|
properties:
|
||||||
|
created_count:
|
||||||
|
description: CreatedCount 当日新增内容数量。
|
||||||
|
type: integer
|
||||||
|
date:
|
||||||
|
description: Date 日期(YYYY-MM-DD)。
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.SuperCouponGrantItem:
|
dto.SuperCouponGrantItem:
|
||||||
properties:
|
properties:
|
||||||
coupon_id:
|
coupon_id:
|
||||||
@@ -2500,6 +2520,21 @@ paths:
|
|||||||
summary: Batch review contents
|
summary: Batch review contents
|
||||||
tags:
|
tags:
|
||||||
- Content
|
- Content
|
||||||
|
/super/v1/contents/statistics:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Content statistics
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.SuperContentStatisticsResponse'
|
||||||
|
summary: Content statistics
|
||||||
|
tags:
|
||||||
|
- Content
|
||||||
/super/v1/coupon-grants:
|
/super/v1/coupon-grants:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
## 1) 总体结论
|
## 1) 总体结论
|
||||||
|
|
||||||
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录。
|
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录。
|
||||||
- **部分落地**:平台概览(缺内容统计/趋势、退款率/漏斗)、租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)。
|
- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)、报表(缺提现/内容深度指标与钻取)。
|
||||||
- **未落地**:资产治理、通知中心、审计与系统配置类能力。
|
- **未落地**:资产治理、通知中心、审计与系统配置类能力。
|
||||||
|
|
||||||
## 2) 按页面完成度(对照 2.x)
|
## 2) 按页面完成度(对照 2.x)
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
- 缺口:无显著功能缺口。
|
- 缺口:无显著功能缺口。
|
||||||
|
|
||||||
### 2.2 平台概览 `/`
|
### 2.2 平台概览 `/`
|
||||||
- 状态:**部分完成**
|
- 状态:**已完成**
|
||||||
- 已有:用户统计、订单统计、租户总数(来自租户列表分页)。
|
- 已有:用户统计、订单统计、租户总数、内容总量与趋势、退款率与订单漏斗。
|
||||||
- 缺口:平台级内容总量、趋势图、退款率/订单漏斗等运营指标未落地。
|
- 缺口:无显著功能缺口。
|
||||||
|
|
||||||
### 2.3 租户管理 `/superadmin/tenants`
|
### 2.3 租户管理 `/superadmin/tenants`
|
||||||
- 状态:**已完成**
|
- 状态:**已完成**
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
## 4) 建议的下一步(按优先级)
|
## 4) 建议的下一步(按优先级)
|
||||||
|
|
||||||
1. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。
|
1. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。
|
||||||
2. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。
|
2. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。
|
||||||
3. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。
|
3. **优惠券异常核查**:完善发放/核销异常监测与风控处理流程。
|
||||||
4. **优惠券异常核查**:完善发放/核销异常监测与风控处理流程。
|
4. **报表深化**:补齐提现/内容维度指标与多维钻取能力。
|
||||||
|
|||||||
@@ -130,5 +130,21 @@ export const ContentService = {
|
|||||||
reason
|
reason
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
async getContentStatistics({ tenant_id, start_at, end_at, granularity } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
tenant_id,
|
||||||
|
start_at: iso(start_at),
|
||||||
|
end_at: iso(end_at),
|
||||||
|
granularity
|
||||||
|
};
|
||||||
|
return requestJson('/super/v1/contents/statistics', { query });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
||||||
|
import { useLayout } from '@/layout/composables/layout';
|
||||||
|
import { ContentService } from '@/service/ContentService';
|
||||||
import { OrderService } from '@/service/OrderService';
|
import { OrderService } from '@/service/OrderService';
|
||||||
|
import { ReportService } from '@/service/ReportService';
|
||||||
import { TenantService } from '@/service/TenantService';
|
import { TenantService } from '@/service/TenantService';
|
||||||
import { UserService } from '@/service/UserService';
|
import { UserService } from '@/service/UserService';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const { getPrimary, getSurface, isDarkTheme } = useLayout();
|
||||||
|
|
||||||
const tenantTotal = ref(null);
|
const tenantTotal = ref(null);
|
||||||
const tenantLoading = ref(false);
|
const tenantLoading = ref(false);
|
||||||
@@ -17,12 +21,25 @@ const statisticsLoading = ref(false);
|
|||||||
const orderStats = ref(null);
|
const orderStats = ref(null);
|
||||||
const orderStatsLoading = ref(false);
|
const orderStatsLoading = ref(false);
|
||||||
|
|
||||||
|
const contentStats = ref(null);
|
||||||
|
const contentStatsLoading = ref(false);
|
||||||
|
const contentTrendData = ref(null);
|
||||||
|
const contentTrendOptions = ref(null);
|
||||||
|
|
||||||
|
const reportOverview = ref(null);
|
||||||
|
const reportLoading = ref(false);
|
||||||
|
|
||||||
function formatCny(amountInCents) {
|
function formatCny(amountInCents) {
|
||||||
const amount = Number(amountInCents) / 100;
|
const amount = Number(amountInCents) / 100;
|
||||||
if (!Number.isFinite(amount)) return '-';
|
if (!Number.isFinite(amount)) return '-';
|
||||||
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPercent(rate) {
|
||||||
|
if (!Number.isFinite(rate)) return '-';
|
||||||
|
return `${(rate * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
const statisticsItems = computed(() => {
|
const statisticsItems = computed(() => {
|
||||||
const total = (statistics.value || []).reduce((sum, row) => sum + (Number(row?.count) || 0), 0);
|
const total = (statistics.value || []).reduce((sum, row) => sum + (Number(row?.count) || 0), 0);
|
||||||
const statusIcon = (status) => {
|
const statusIcon = (status) => {
|
||||||
@@ -89,6 +106,103 @@ const orderItems = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orderFunnelItems = computed(() => {
|
||||||
|
const byStatus = new Map((orderStats.value?.by_status || []).map((row) => [row?.status, row]));
|
||||||
|
const getCount = (status) => byStatus.get(status)?.count ?? 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ key: 'orders-created', label: '创建:', value: orderStatsLoading.value ? '-' : getCount('created'), icon: 'pi-file' },
|
||||||
|
{ key: 'orders-paid-funnel', label: '已支付:', value: orderStatsLoading.value ? '-' : getCount('paid'), icon: 'pi-check-circle' },
|
||||||
|
{ key: 'orders-refunding-funnel', label: '退款中:', value: orderStatsLoading.value ? '-' : getCount('refunding'), icon: 'pi-spin pi-spinner' },
|
||||||
|
{ key: 'orders-refunded-funnel', label: '已退款:', value: orderStatsLoading.value ? '-' : getCount('refunded'), icon: 'pi-undo' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentItems = computed(() => {
|
||||||
|
const trend = contentStats.value?.trend || [];
|
||||||
|
const recentCreated = trend.reduce((sum, item) => sum + (Number(item?.created_count) || 0), 0);
|
||||||
|
return [
|
||||||
|
{ key: 'contents-total', label: '内容总量:', value: contentStatsLoading.value ? '-' : (contentStats.value?.total_count ?? 0), icon: 'pi-book' },
|
||||||
|
{ key: 'contents-recent', label: '近7日新增:', value: contentStatsLoading.value ? '-' : recentCreated, icon: 'pi-chart-line' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const refundRateText = computed(() => {
|
||||||
|
if (reportLoading.value) return '-';
|
||||||
|
const summary = reportOverview.value?.summary;
|
||||||
|
const paidOrders = Number(summary?.paid_orders ?? 0);
|
||||||
|
const refundOrders = Number(summary?.refund_orders ?? 0);
|
||||||
|
const denominator = paidOrders + refundOrders;
|
||||||
|
if (!denominator) return '0%';
|
||||||
|
return formatPercent(refundOrders / denominator);
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildContentChartData(items) {
|
||||||
|
const documentStyle = getComputedStyle(document.documentElement);
|
||||||
|
const labels = items.map((item) => item?.date ?? '-');
|
||||||
|
const counts = items.map((item) => Number(item?.created_count ?? 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: '新增内容',
|
||||||
|
data: counts,
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
|
||||||
|
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
|
||||||
|
tension: 0.35
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChartOptions() {
|
||||||
|
const documentStyle = getComputedStyle(document.documentElement);
|
||||||
|
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||||
|
const textMutedColor = documentStyle.getPropertyValue('--text-color-secondary');
|
||||||
|
const borderColor = documentStyle.getPropertyValue('--surface-border');
|
||||||
|
|
||||||
|
return {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
aspectRatio: 2.2,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: textMutedColor
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'transparent',
|
||||||
|
borderColor: 'transparent'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: textMutedColor
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: borderColor,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
drawTicks: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContentChart() {
|
||||||
|
const items = contentStats.value?.trend ?? [];
|
||||||
|
contentTrendData.value = buildContentChartData(items);
|
||||||
|
contentTrendOptions.value = buildChartOptions();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTenantTotal() {
|
async function loadTenantTotal() {
|
||||||
tenantLoading.value = true;
|
tenantLoading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -123,10 +237,39 @@ async function loadOrderStatistics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadContentStatistics() {
|
||||||
|
contentStatsLoading.value = true;
|
||||||
|
try {
|
||||||
|
contentStats.value = await ContentService.getContentStatistics({ granularity: 'day' });
|
||||||
|
updateContentChart();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载内容统计信息', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
contentStatsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReportOverview() {
|
||||||
|
reportLoading.value = true;
|
||||||
|
try {
|
||||||
|
reportOverview.value = await ReportService.getOverview({ granularity: 'day' });
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载报表指标', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
reportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTenantTotal();
|
loadTenantTotal();
|
||||||
loadStatistics();
|
loadStatistics();
|
||||||
loadOrderStatistics();
|
loadOrderStatistics();
|
||||||
|
loadContentStatistics();
|
||||||
|
loadReportOverview();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||||
|
updateContentChart();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -136,6 +279,32 @@ onMounted(() => {
|
|||||||
<StatisticsStrip :items="orderItems" containerClass="card mb-4" />
|
<StatisticsStrip :items="orderItems" containerClass="card mb-4" />
|
||||||
<StatisticsStrip :items="statisticsItems" containerClass="card mb-4" />
|
<StatisticsStrip :items="statisticsItems" containerClass="card mb-4" />
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="col-12 xl:col-7">
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="m-0">内容趋势</h4>
|
||||||
|
<div class="text-sm text-muted-color">近7日</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StatisticsStrip :items="contentItems" containerClass="p-0 mb-4" />
|
||||||
|
<Chart v-if="contentTrendData" type="line" :data="contentTrendData" :options="contentTrendOptions" class="h-72" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 xl:col-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 class="m-0">订单漏斗</h4>
|
||||||
|
<div class="text-sm text-muted-color">近7日退款率:{{ refundRateText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StatisticsStrip :items="orderFunnelItems" containerClass="p-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h4 class="m-0">快捷入口</h4>
|
<h4 class="m-0">快捷入口</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user