Files
akmon/pages/mall/analytics/report-detail.uvue
2026-01-20 08:04:15 +08:00

1035 lines
24 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 数据分析端 - 报表详情页 -->
<template>
<view class="report-detail-page">
<!-- 报表头部 -->
<view class="report-header">
<view class="header-info">
<text class="report-title">{{ report.title }}</text>
<view class="report-meta">
<text class="meta-item">{{ getReportTypeText() }}</text>
<text class="meta-item">{{ report.period }}</text>
<text class="meta-item">{{ formatTime(report.generated_at) }}</text>
</view>
</view>
<view class="header-actions">
<button class="action-btn export" @click="exportReport">📊 导出</button>
<button class="action-btn refresh" @click="refreshReport">🔄 刷新</button>
</view>
</view>
<!-- 核心指标概览 -->
<view class="metrics-overview">
<view class="section-title">核心指标</view>
<view class="metrics-grid">
<view v-for="metric in coreMetrics" :key="metric.key" class="metric-card">
<view class="metric-icon" :style="{ backgroundColor: metric.color }">{{ metric.icon }}</view>
<view class="metric-content">
<text class="metric-value">{{ formatMetricValue(metric.value, metric.format) }}</text>
<text class="metric-label">{{ metric.label }}</text>
<view class="metric-change" :class="{ positive: metric.change > 0, negative: metric.change < 0 }">
<text class="change-icon">{{ metric.change > 0 ? '↗' : metric.change < 0 ? '↘' : '→' }}</text>
<text class="change-value">{{ Math.abs(metric.change) }}%</text>
</view>
</view>
</view>
</view>
</view>
<!-- 趋势图表 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">趋势分析</text>
<view class="chart-tabs">
<text v-for="tab in chartTabs" :key="tab.key"
class="chart-tab"
:class="{ active: activeChartTab === tab.key }"
@click="switchChartTab(tab.key)">{{ tab.label }}</text>
</view>
</view>
<view class="chart-container">
<canvas class="chart-canvas" canvas-id="trendChart" @touchstart="onChartTouch" @touchmove="onChartTouch" @touchend="onChartTouch"></canvas>
</view>
<view class="chart-legend">
<view v-for="legend in chartLegends" :key="legend.key" class="legend-item">
<view class="legend-color" :style="{ backgroundColor: legend.color }"></view>
<text class="legend-label">{{ legend.label }}</text>
</view>
</view>
</view>
<!-- 数据表格 -->
<view class="data-table">
<view class="section-title">详细数据</view>
<view class="table-filters">
<view class="filter-item">
<text class="filter-label">排序方式:</text>
<picker :value="sortIndex" :range="sortOptions" @change="onSortChange">
<text class="filter-value">{{ sortOptions[sortIndex] }}</text>
</picker>
</view>
<view class="filter-item">
<text class="filter-label">显示条数:</text>
<picker :value="limitIndex" :range="limitOptions" @change="onLimitChange">
<text class="filter-value">{{ limitOptions[limitIndex] }}</text>
</picker>
</view>
</view>
<view class="table-container">
<scroll-view scroll-x="true" class="table-scroll">
<view class="table">
<view class="table-header">
<text v-for="column in tableColumns" :key="column.key"
class="table-cell header-cell"
:style="{ width: column.width }">{{ column.title }}</text>
</view>
<view v-for="(row, index) in tableData" :key="index" class="table-row">
<text v-for="column in tableColumns" :key="column.key"
class="table-cell data-cell"
:style="{ width: column.width }"
:class="{ number: column.type === 'number', currency: column.type === 'currency' }">
{{ formatCellValue(row[column.key], column) }}
</text>
</view>
</view>
</scroll-view>
</view>
<view class="table-pagination">
<button class="page-btn" :disabled="currentPage <= 1" @click="previousPage">上一页</button>
<text class="page-info">{{ currentPage }} / {{ totalPages }}</text>
<button class="page-btn" :disabled="currentPage >= totalPages" @click="nextPage">下一页</button>
</view>
</view>
<!-- 数据洞察 -->
<view class="data-insights">
<view class="section-title">数据洞察</view>
<view v-for="insight in dataInsights" :key="insight.id" class="insight-card">
<view class="insight-header">
<view class="insight-icon" :class="insight.type">{{ getInsightIcon(insight.type) }}</view>
<text class="insight-title">{{ insight.title }}</text>
</view>
<text class="insight-content">{{ insight.content }}</text>
<view class="insight-actions">
<text class="insight-impact" :class="insight.impact">{{ getImpactText(insight.impact) }}</text>
<text class="insight-action" @click="viewInsightDetail(insight)">查看详情 ></text>
</view>
</view>
</view>
<!-- 报表配置 -->
<view class="report-config">
<view class="section-title">报表配置</view>
<view class="config-item">
<text class="config-label">自动刷新</text>
<switch :checked="autoRefresh" @change="toggleAutoRefresh" />
</view>
<view class="config-item">
<text class="config-label">刷新间隔</text>
<picker :value="intervalIndex" :range="intervalOptions" @change="onIntervalChange">
<text class="config-value">{{ intervalOptions[intervalIndex] }}</text>
</picker>
</view>
<view class="config-item">
<text class="config-label">邮件通知</text>
<switch :checked="emailNotify" @change="toggleEmailNotify" />
</view>
<view class="config-actions">
<button class="config-btn save" @click="saveConfig">保存配置</button>
<button class="config-btn reset" @click="resetConfig">重置配置</button>
</view>
</view>
<!-- 相关报表 -->
<view class="related-reports">
<view class="section-title">相关报表</view>
<view class="report-list">
<view v-for="relatedReport in relatedReports" :key="relatedReport.id"
class="report-item" @click="viewRelatedReport(relatedReport)">
<view class="report-icon">📊</view>
<view class="report-info">
<text class="report-name">{{ relatedReport.title }}</text>
<text class="report-desc">{{ relatedReport.description }}</text>
<text class="report-time">{{ formatTime(relatedReport.generated_at) }}</text>
</view>
<text class="report-arrow">></text>
</view>
</view>
</view>
</view>
</template>
<script>
type ReportType = {
id: string
title: string
type: string
period: string
generated_at: string
description: string
}
type MetricType = {
key: string
label: string
value: number
format: string
icon: string
color: string
change: number
}
type ChartTabType = {
key: string
label: string
}
type ChartLegendType = {
key: string
label: string
color: string
}
type TableColumnType = {
key: string
title: string
width: string
type: string
}
type InsightType = {
id: string
type: string
title: string
content: string
impact: string
}
export default {
data() {
return {
report: {
id: '',
title: '',
type: '',
period: '',
generated_at: '',
description: ''
} as ReportType,
coreMetrics: [] as Array<MetricType>,
chartTabs: [] as Array<ChartTabType>,
activeChartTab: '',
chartLegends: [] as Array<ChartLegendType>,
tableColumns: [] as Array<TableColumnType>,
tableData: [] as Array<any>,
dataInsights: [] as Array<InsightType>,
relatedReports: [] as Array<ReportType>,
sortIndex: 0,
sortOptions: [] as Array<string>,
limitIndex: 1,
limitOptions: ['10条', '20条', '50条', '100条'],
currentPage: 1,
totalPages: 1,
autoRefresh: false,
intervalIndex: 1,
intervalOptions: ['1分钟', '5分钟', '10分钟', '30分钟', '1小时'],
emailNotify: false
}
},
onLoad(options: any) {
const reportId = options.reportId as string
if (reportId) {
this.loadReportDetail(reportId)
}
},
methods: {
loadReportDetail(reportId: string) {
// 模拟加载报表详情数据
this.report = {
id: reportId,
title: '销售业绩分析报表',
type: 'sales',
period: '2024年1月',
generated_at: '2024-01-15T14:30:00',
description: '详细分析1月份的销售业绩情况'
}
this.coreMetrics = [
{
key: 'total_sales',
label: '总销售额',
value: 1250000,
format: 'currency',
icon: '💰',
color: '#4caf50',
change: 15.6
},
{
key: 'order_count',
label: '订单数量',
value: 8650,
format: 'number',
icon: '📦',
color: '#2196f3',
change: 8.3
},
{
key: 'avg_order_value',
label: '客单价',
value: 144.5,
format: 'currency',
icon: '🛍️',
color: '#ff9800',
change: 6.8
},
{
key: 'conversion_rate',
label: '转化率',
value: 3.2,
format: 'percent',
icon: '📈',
color: '#9c27b0',
change: -2.1
}
]
this.chartTabs = [
{ key: 'sales', label: '销售额' },
{ key: 'orders', label: '订单量' },
{ key: 'users', label: '用户数' }
]
this.activeChartTab = 'sales'
this.chartLegends = [
{ key: 'current', label: '当期', color: '#2196f3' },
{ key: 'previous', label: '上期', color: '#ff9800' },
{ key: 'target', label: '目标', color: '#4caf50' }
]
this.tableColumns = [
{ key: 'date', title: '日期', width: '120rpx', type: 'text' },
{ key: 'sales', title: '销售额', width: '120rpx', type: 'currency' },
{ key: 'orders', title: '订单数', width: '100rpx', type: 'number' },
{ key: 'users', title: '用户数', width: '100rpx', type: 'number' },
{ key: 'conversion', title: '转化率', width: '100rpx', type: 'percent' },
{ key: 'avg_value', title: '客单价', width: '120rpx', type: 'currency' }
]
this.sortOptions = ['按日期降序', '按销售额降序', '按订单数降序', '按转化率降序']
// 模拟表格数据
this.generateTableData()
this.dataInsights = [
{
id: 'insight_001',
type: 'positive',
title: '销售额显著增长',
content: '相比上月本月销售额增长15.6%,主要得益于新产品上线和营销活动效果显著。',
impact: 'high'
},
{
id: 'insight_002',
type: 'warning',
title: '转化率轻微下降',
content: '转化率较上月下降2.1%,建议优化商品页面和购买流程,提升用户体验。',
impact: 'medium'
},
{
id: 'insight_003',
title: '周末销售高峰',
content: '数据显示周末周六、周日的销售额占总销售额的35%,建议加强周末营销投入。',
impact: 'medium',
type: 'info'
}
]
this.relatedReports = [
{
id: 'report_002',
title: '用户行为分析报表',
type: 'user',
period: '2024年1月',
generated_at: '2024-01-15T10:00:00',
description: '分析用户浏览、搜索、购买行为'
},
{
id: 'report_003',
title: '商品销售排行报表',
type: 'product',
period: '2024年1月',
generated_at: '2024-01-15T09:30:00',
description: '商品销售排行和库存分析'
}
]
this.totalPages = Math.ceil(31 / parseInt(this.limitOptions[this.limitIndex]))
},
generateTableData() {
this.tableData = []
const days = 31
const limit = parseInt(this.limitOptions[this.limitIndex])
const start = (this.currentPage - 1) * limit
const end = Math.min(start + limit, days)
for (let i = start; i < end; i++) {
const day = i + 1
this.tableData.push({
date: `2024-01-${day.toString().padStart(2, '0')}`,
sales: Math.floor(Math.random() * 50000) + 20000,
orders: Math.floor(Math.random() * 300) + 200,
users: Math.floor(Math.random() * 1000) + 500,
conversion: (Math.random() * 5 + 1).toFixed(1),
avg_value: (Math.random() * 100 + 50).toFixed(2)
})
}
},
getReportTypeText(): string {
const types: Record<string, string> = {
sales: '销售报表',
user: '用户报表',
product: '商品报表',
financial: '财务报表',
marketing: '营销报表'
}
return types[this.report.type] || '其他报表'
},
formatMetricValue(value: number, format: string): string {
switch (format) {
case 'currency':
return `¥${(value / 10000).toFixed(1)}万`
case 'percent':
return `${value}%`
case 'number':
return value.toLocaleString()
default:
return value.toString()
}
},
formatTime(timeStr: string): string {
return timeStr.replace('T', ' ').split('.')[0]
},
getInsightIcon(type: string): string {
const icons: Record<string, string> = {
positive: '✅',
warning: '⚠️',
negative: '❌',
info: ''
}
return icons[type] || ''
},
getImpactText(impact: string): string {
const impacts: Record<string, string> = {
high: '高影响',
medium: '中影响',
low: '低影响'
}
return impacts[impact] || '未知影响'
},
formatCellValue(value: any, column: TableColumnType): string {
switch (column.type) {
case 'currency':
return `¥${parseFloat(value).toLocaleString()}`
case 'percent':
return `${value}%`
case 'number':
return parseInt(value).toLocaleString()
default:
return value.toString()
}
},
switchChartTab(tabKey: string) {
this.activeChartTab = tabKey
// 这里可以重新绘制图表
},
onChartTouch(e: any) {
// 处理图表触摸事件
},
onSortChange(e: any) {
this.sortIndex = e.detail.value
this.generateTableData()
},
onLimitChange(e: any) {
this.limitIndex = e.detail.value
this.currentPage = 1
this.totalPages = Math.ceil(31 / parseInt(this.limitOptions[this.limitIndex]))
this.generateTableData()
},
previousPage() {
if (this.currentPage > 1) {
this.currentPage--
this.generateTableData()
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.generateTableData()
}
},
exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: (res) => {
const formats = ['Excel', 'PDF', '图片']
uni.showToast({
title: `正在导出${formats[res.tapIndex]}`,
icon: 'loading'
})
setTimeout(() => {
uni.showToast({
title: '导出成功',
icon: 'success'
})
}, 2000)
}
})
},
refreshReport() {
uni.showLoading({ title: '刷新中...' })
setTimeout(() => {
uni.hideLoading()
this.loadReportDetail(this.report.id)
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}, 1500)
},
viewInsightDetail(insight: InsightType) {
uni.navigateTo({
url: `/pages/mall/analytics/insight-detail?insightId=${insight.id}`
})
},
viewRelatedReport(report: ReportType) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${report.id}`
})
},
toggleAutoRefresh(e: any) {
this.autoRefresh = e.detail.value
},
onIntervalChange(e: any) {
this.intervalIndex = e.detail.value
},
toggleEmailNotify(e: any) {
this.emailNotify = e.detail.value
},
saveConfig() {
uni.showToast({
title: '配置已保存',
icon: 'success'
})
},
resetConfig() {
this.autoRefresh = false
this.intervalIndex = 1
this.emailNotify = false
uni.showToast({
title: '配置已重置',
icon: 'success'
})
}
}
}
</script>
<style>
.report-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
}
.report-header, .metrics-overview, .chart-section, .data-table, .data-insights, .report-config, .related-reports {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.report-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.header-info {
flex: 1;
}
.report-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
}
.report-meta {
display: flex;
gap: 20rpx;
}
.meta-item {
font-size: 24rpx;
color: #666;
background-color: #f0f0f0;
padding: 6rpx 12rpx;
border-radius: 8rpx;
}
.header-actions {
display: flex;
gap: 15rpx;
}
.action-btn.export, .action-btn.refresh {
padding: 15rpx 25rpx;
border-radius: 8rpx;
font-size: 24rpx;
border: none;
}
.action-btn.export {
background-color: #4caf50;
color: #fff;
}
.action-btn.refresh {
background-color: #2196f3;
color: #fff;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.metrics-grid {
display: flex;
gap: 20rpx;
flex-wrap: wrap;
}
.metric-card {
flex: 1;
min-width: 300rpx;
display: flex;
align-items: center;
padding: 25rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
}
.metric-icon {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 20rpx;
}
.metric-content {
flex: 1;
}
.metric-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.metric-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.metric-change {
display: flex;
align-items: center;
}
.metric-change.positive {
color: #4caf50;
}
.metric-change.negative {
color: #ff4444;
}
.change-icon {
font-size: 20rpx;
margin-right: 5rpx;
}
.change-value {
font-size: 22rpx;
font-weight: bold;
}
.chart-tabs {
display: flex;
gap: 15rpx;
}
.chart-tab {
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #666;
background-color: #f0f0f0;
}
.chart-tab.active {
background-color: #2196f3;
color: #fff;
}
.chart-container {
height: 500rpx;
margin: 30rpx 0;
border: 1rpx solid #eee;
border-radius: 8rpx;
}
.chart-canvas {
width: 100%;
height: 100%;
}
.chart-legend {
display: flex;
justify-content: center;
gap: 40rpx;
}
.legend-item {
display: flex;
align-items: center;
}
.legend-color {
width: 20rpx;
height: 20rpx;
border-radius: 10rpx;
margin-right: 10rpx;
}
.legend-label {
font-size: 24rpx;
color: #666;
}
.table-filters {
display: flex;
gap: 40rpx;
margin-bottom: 25rpx;
}
.filter-item {
display: flex;
align-items: center;
}
.filter-label {
font-size: 24rpx;
color: #666;
margin-right: 10rpx;
}
.filter-value, .config-value {
font-size: 24rpx;
color: #333;
padding: 10rpx 20rpx;
background-color: #f0f0f0;
border-radius: 6rpx;
}
.table-container {
border: 1rpx solid #eee;
border-radius: 8rpx;
margin-bottom: 25rpx;
}
.table-scroll {
white-space: nowrap;
}
.table {
min-width: 100%;
}
.table-header, .table-row {
display: flex;
border-bottom: 1rpx solid #eee;
}
.table-row:last-child {
border-bottom: none;
}
.table-cell {
padding: 20rpx 15rpx;
font-size: 24rpx;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
}
.header-cell {
background-color: #f8f9fa;
font-weight: bold;
color: #333;
}
.data-cell {
color: #666;
}
.data-cell.number, .data-cell.currency {
text-align: right;
}
.table-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 30rpx;
}
.page-btn {
padding: 15rpx 30rpx;
background-color: #2196f3;
color: #fff;
border: none;
border-radius: 6rpx;
font-size: 24rpx;
}
.page-btn:disabled {
background-color: #ccc;
}
.page-info {
font-size: 24rpx;
color: #666;
}
.insight-card {
padding: 25rpx;
background-color: #f8f9fa;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.insight-header {
display: flex;
align-items: center;
margin-bottom: 15rpx;
}
.insight-icon {
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
margin-right: 15rpx;
}
.insight-icon.positive {
background-color: #e8f5e8;
}
.insight-icon.warning {
background-color: #fff8e1;
}
.insight-icon.negative {
background-color: #ffebee;
}
.insight-icon.info {
background-color: #e3f2fd;
}
.insight-title {
font-size: 26rpx;
font-weight: bold;
color: #333;
}
.insight-content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
margin-bottom: 15rpx;
}
.insight-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.insight-impact {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 8rpx;
color: #fff;
}
.insight-impact.high {
background-color: #ff4444;
}
.insight-impact.medium {
background-color: #ffa726;
}
.insight-impact.low {
background-color: #4caf50;
}
.insight-action {
font-size: 22rpx;
color: #2196f3;
}
.config-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.config-item:last-of-type {
border-bottom: none;
}
.config-label {
font-size: 26rpx;
color: #333;
}
.config-actions {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
}
.config-btn {
flex: 1;
height: 70rpx;
border-radius: 8rpx;
font-size: 26rpx;
border: none;
}
.config-btn.save {
background-color: #4caf50;
color: #fff;
}
.config-btn.reset {
background-color: #f0f0f0;
color: #666;
}
.report-list {
margin-top: 25rpx;
}
.report-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.report-item:last-child {
border-bottom: none;
}
.report-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.report-info {
flex: 1;
}
.report-name {
font-size: 26rpx;
color: #333;
font-weight: bold;
margin-bottom: 5rpx;
}
.report-desc {
font-size: 22rpx;
color: #666;
margin-bottom: 5rpx;
}
.report-time {
font-size: 20rpx;
color: #999;
}
.report-arrow {
font-size: 24rpx;
color: #999;
}
</style>