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)
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// @Router /super/v1/tenants/:tenantID<int>/contents/:contentID<int>/status [patch]
|
||||
|
||||
@@ -131,6 +131,18 @@ type SuperContentListFilter struct {
|
||||
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 {
|
||||
requests.Pagination
|
||||
// ID 订单ID,精确匹配。
|
||||
@@ -635,3 +647,19 @@ type AdminContentOwnerLite struct {
|
||||
// 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,
|
||||
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")
|
||||
router.Get("/super/v1/tenants/:tenantID<int>/contents"[len(r.Path()):], DataFunc2(
|
||||
r.contents.ListTenantContents,
|
||||
|
||||
@@ -2059,6 +2059,74 @@ func (s *super) BatchReviewContents(ctx context.Context, operatorID int64, form
|
||||
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) {
|
||||
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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1048,6 +1048,17 @@ definitions:
|
||||
required:
|
||||
- action
|
||||
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:
|
||||
properties:
|
||||
code:
|
||||
@@ -1060,6 +1071,15 @@ definitions:
|
||||
description: Name 租户名称。
|
||||
type: string
|
||||
type: object
|
||||
dto.SuperContentTrendItem:
|
||||
properties:
|
||||
created_count:
|
||||
description: CreatedCount 当日新增内容数量。
|
||||
type: integer
|
||||
date:
|
||||
description: Date 日期(YYYY-MM-DD)。
|
||||
type: string
|
||||
type: object
|
||||
dto.SuperCouponGrantItem:
|
||||
properties:
|
||||
coupon_id:
|
||||
@@ -2500,6 +2520,21 @@ paths:
|
||||
summary: Batch review contents
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
## 1) 总体结论
|
||||
|
||||
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录。
|
||||
- **部分落地**:平台概览(缺内容统计/趋势、退款率/漏斗)、租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)。
|
||||
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录。
|
||||
- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)、报表(缺提现/内容深度指标与钻取)。
|
||||
- **未落地**:资产治理、通知中心、审计与系统配置类能力。
|
||||
|
||||
## 2) 按页面完成度(对照 2.x)
|
||||
@@ -16,9 +16,9 @@
|
||||
- 缺口:无显著功能缺口。
|
||||
|
||||
### 2.2 平台概览 `/`
|
||||
- 状态:**部分完成**
|
||||
- 已有:用户统计、订单统计、租户总数(来自租户列表分页)。
|
||||
- 缺口:平台级内容总量、趋势图、退款率/订单漏斗等运营指标未落地。
|
||||
- 状态:**已完成**
|
||||
- 已有:用户统计、订单统计、租户总数、内容总量与趋势、退款率与订单漏斗。
|
||||
- 缺口:无显著功能缺口。
|
||||
|
||||
### 2.3 租户管理 `/superadmin/tenants`
|
||||
- 状态:**已完成**
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
## 4) 建议的下一步(按优先级)
|
||||
|
||||
1. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。
|
||||
2. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。
|
||||
3. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。
|
||||
4. **优惠券异常核查**:完善发放/核销异常监测与风控处理流程。
|
||||
1. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。
|
||||
2. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。
|
||||
3. **优惠券异常核查**:完善发放/核销异常监测与风控处理流程。
|
||||
4. **报表深化**:补齐提现/内容维度指标与多维钻取能力。
|
||||
|
||||
@@ -130,5 +130,21 @@ export const ContentService = {
|
||||
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>
|
||||
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { ContentService } from '@/service/ContentService';
|
||||
import { OrderService } from '@/service/OrderService';
|
||||
import { ReportService } from '@/service/ReportService';
|
||||
import { TenantService } from '@/service/TenantService';
|
||||
import { UserService } from '@/service/UserService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const toast = useToast();
|
||||
const { getPrimary, getSurface, isDarkTheme } = useLayout();
|
||||
|
||||
const tenantTotal = ref(null);
|
||||
const tenantLoading = ref(false);
|
||||
@@ -17,12 +21,25 @@ const statisticsLoading = ref(false);
|
||||
const orderStats = ref(null);
|
||||
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) {
|
||||
const amount = Number(amountInCents) / 100;
|
||||
if (!Number.isFinite(amount)) return '-';
|
||||
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 total = (statistics.value || []).reduce((sum, row) => sum + (Number(row?.count) || 0), 0);
|
||||
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() {
|
||||
tenantLoading.value = true;
|
||||
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(() => {
|
||||
loadTenantTotal();
|
||||
loadStatistics();
|
||||
loadOrderStatistics();
|
||||
loadContentStatistics();
|
||||
loadReportOverview();
|
||||
});
|
||||
|
||||
watch([getPrimary, getSurface, isDarkTheme], () => {
|
||||
updateContentChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -136,6 +279,32 @@ onMounted(() => {
|
||||
<StatisticsStrip :items="orderItems" 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="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">快捷入口</h4>
|
||||
|
||||
Reference in New Issue
Block a user