Files
medical-mall/pages/mall/analytics/report-detail.uvue
2026-01-23 16:33:11 +08:00

1160 lines
28 KiB
Plaintext
Raw 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="page">
<!-- 固定顶部导航栏 -->
<AnalyticsTopBar
:title="report.title || '报表详情'"
:lastUpdateTime="formatTime(report.generated_at)"
:sidebarVisible="showSidebarMenu"
@menu-click="handleMenu"
@refresh="refreshReport"
@search="handleSearch"
@notification="handleNotification"
@fullscreen="handleFullscreen"
@mobile="handleMobile"
@dropdown="handleDropdown"
@settings="handleSettings"
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<view class="main-content">
<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>
</view>
</view>
</view>
</template>
<script lang="uts">
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import supa from '@/components/supadb/aksupainstance.uts'
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 {
components: {
AnalyticsSidebarMenu,
AnalyticsTopBar
},
data() {
return {
showSidebarMenu: false,
currentPath: '/pages/mall/analytics/report-detail',
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>,
allRows: [] as Array<any>,
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) {
// 兼容两种参数名reportId 和 id
const reportId = (options.reportId || options.id) as string
if (reportId) {
this.loadReportDetail(reportId)
} else {
uni.showToast({
title: '缺少报表ID',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
this.currentPath = '/pages/mall/analytics/report-detail'
},
onShow() {
this.currentPath = '/pages/mall/analytics/report-detail'
},
methods: {
handleMenu() {
this.showSidebarMenu = true
},
handleSidebarUpdate(visible: boolean) {
this.showSidebarMenu = visible
},
safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
},
async loadReportDetail(reportId: string) {
try {
uni.showLoading({ title: '加载中...' })
// 1. 加载报表主体
const reportRes: any = await supa
.from('analytics_reports')
.select('id, title, type, period, generated_at, description')
.eq('id', reportId)
const reportRows: Array<any> = Array.isArray(reportRes.data) ? (reportRes.data as Array<any>) : []
if (reportRows.length === 0) {
uni.showToast({ title: '报表不存在', icon: 'none' })
return
}
const r = reportRows[0]
this.report = {
id: `${r.id}`,
title: `${r.title}`,
type: `${r.type}`,
period: `${r.period}`,
generated_at: `${r.generated_at}`,
description: `${r.description || ''}`
}
// 2. 加载核心指标
const metricRes: any = await supa
.from('analytics_report_metrics')
.select('metric_key, metric_label, metric_value_num, format, icon, color, change_pct')
.eq('report_id', reportId)
const metricRows: Array<any> = Array.isArray(metricRes.data) ? (metricRes.data as Array<any>) : []
this.coreMetrics = metricRows.map((m: any) => ({
key: `${m.metric_key}`,
label: `${m.metric_label}`,
value: this.safeNumber(m.metric_value_num),
format: `${m.format || 'number'}`,
icon: `${m.icon || '📊'}`,
color: `${m.color || '#4caf50'}`,
change: this.safeNumber(m.change_pct)
}))
// 3. 配置表头与排序选项(固定结构)
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 = ['按日期降序', '按销售额降序', '按订单数降序', '按转化率降序']
// 4. 加载明细行(趋势/表格)
const rowsRes: any = await supa
.from('analytics_report_rows')
.select('row_date, gmv, orders, users, conversion, avg_order_amount')
.eq('report_id', reportId)
.order('row_date', { ascending: true } as any)
const rows: Array<any> = Array.isArray(rowsRes.data) ? (rowsRes.data as Array<any>) : []
this.allRows = rows
this.currentPage = 1
this.updateTotalPages()
this.generateTableData()
// 5. 加载洞察
const insightRes: any = await supa
.from('analytics_insights')
.select('id, type, title, content, impact')
.eq('report_id', reportId)
.order('created_at', { ascending: false } as any)
const insRows: Array<any> = Array.isArray(insightRes.data) ? (insightRes.data as Array<any>) : []
this.dataInsights = insRows.map((it: any) => ({
id: `${it.id}`,
type: `${it.type || 'info'}`,
title: `${it.title}`,
content: `${it.content}`,
impact: `${it.impact || 'medium'}`
}))
// 6. 相关报表(同类型最近报表)
const relatedRes: any = await supa
.from('analytics_reports')
.select('id, title, type, period, generated_at, description')
.eq('type', this.report.type)
.neq('id', reportId)
.order('generated_at', { ascending: false } as any)
.limit(3 as any)
const relRows: Array<any> = Array.isArray(relatedRes.data) ? (relatedRes.data as Array<any>) : []
this.relatedReports = relRows.map((it: any) => ({
id: `${it.id}`,
title: `${it.title}`,
type: `${it.type}`,
period: `${it.period}`,
generated_at: `${it.generated_at}`,
description: `${it.description || ''}`
}))
} catch (e) {
console.error('loadReportDetail failed', e)
uni.showToast({ title: '报表加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
},
updateTotalPages() {
const total = this.allRows.length
const limit = parseInt(this.limitOptions[this.limitIndex])
this.totalPages = total > 0 ? Math.ceil(total / limit) : 1
},
generateTableData() {
this.tableData = []
const total = this.allRows.length
if (total === 0) {
return
}
const limit = parseInt(this.limitOptions[this.limitIndex])
const start = (this.currentPage - 1) * limit
const end = Math.min(start + limit, total)
for (let i = start; i < end; i++) {
const row = this.allRows[i]
this.tableData.push({
date: `${row.row_date}`,
sales: this.safeNumber(row.gmv),
orders: this.safeNumber(row.orders),
users: this.safeNumber(row.users),
conversion: this.safeNumber(row.conversion).toFixed(1),
avg_value: this.safeNumber(row.avg_order_amount).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.updateTotalPages()
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'
})
},
handleSearch() {
uni.showToast({ title: '搜索', icon: 'none' })
},
handleNotification() {
uni.showToast({ title: '通知', icon: 'none' })
},
handleFullscreen() {
uni.showToast({ title: '全屏', icon: 'none' })
},
handleMobile() {
uni.showToast({ title: '移动端', icon: 'none' })
},
handleDropdown() {
uni.showToast({ title: '下拉菜单', icon: 'none' })
},
handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
},
resetConfig() {
this.autoRefresh = false
this.intervalIndex = 1
this.emailNotify = false
uni.showToast({
title: '配置已重置',
icon: 'success'
})
}
}
}
</script>
<style>
.page {
min-height: 100vh;
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
.page-layout {
display: flex;
flex-direction: row !important;
min-height: 100vh;
}
.main-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
}
.menu-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 8px;
background: #f3f4f6;
}
.menu-icon .icon {
font-size: 18px;
color: #333;
}
.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;
}
/* 响应式:窄屏时全屏显示 */
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
}
.main-content {
width: 100%;
}
}
</style>