750 lines
16 KiB
Plaintext
750 lines
16 KiB
Plaintext
<template>
|
|
<view class="page" @click="closeMoreMenu">
|
|
<!-- 固定顶部导航栏 -->
|
|
<AnalyticsTopBar
|
|
:title="'自定义报表'"
|
|
:lastUpdateTime="'创建和管理您的专属报表'"
|
|
:sidebarVisible="showSidebarMenu"
|
|
@menu-click="handleMenu"
|
|
@refresh="refreshData"
|
|
@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="container">
|
|
|
|
<!-- 报表列表 -->
|
|
<view class="report-list">
|
|
<view v-for="report in reports" :key="report.id" class="report-card" @click="openReport(report)">
|
|
<view class="report-header">
|
|
<text class="report-title">{{ report.name }}</text>
|
|
<view class="report-actions">
|
|
<view class="action-btn" @click.stop="editReport(report)">
|
|
<text class="icon">✏️</text>
|
|
</view>
|
|
<view class="action-btn" @click.stop="deleteReport(report)">
|
|
<text class="icon">🗑️</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<text class="report-desc">{{ report.description }}</text>
|
|
<view class="report-meta">
|
|
<text class="meta-item">指标:{{ report.metrics.length }}个</text>
|
|
<text class="meta-item">图表:{{ report.charts.length }}个</text>
|
|
<text class="meta-item">更新:{{ report.updated_at }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 新建报表对话框 -->
|
|
<view class="modal" v-if="showCreateModal" @click.stop>
|
|
<view class="modal-content" @click.stop>
|
|
<view class="modal-header">
|
|
<text class="modal-title">{{ editingReport ? '编辑报表' : '新建报表' }}</text>
|
|
<view class="modal-close" @click="closeModal">
|
|
<text class="icon">✕</text>
|
|
</view>
|
|
</view>
|
|
<view class="modal-body">
|
|
<view class="form-item">
|
|
<text class="form-label">报表名称</text>
|
|
<input class="form-input" v-model="reportForm.name" placeholder="请输入报表名称" />
|
|
</view>
|
|
<view class="form-item">
|
|
<text class="form-label">报表描述</text>
|
|
<textarea class="form-textarea" v-model="reportForm.description" placeholder="请输入报表描述"></textarea>
|
|
</view>
|
|
<view class="form-item">
|
|
<text class="form-label">选择指标</text>
|
|
<view class="metric-list">
|
|
<view
|
|
v-for="m in availableMetrics"
|
|
:key="m.key"
|
|
class="metric-item"
|
|
:class="{ selected: reportForm.metrics.includes(m.key) }"
|
|
@click="toggleMetric(m.key)"
|
|
>
|
|
<text>{{ m.label }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="form-item">
|
|
<text class="form-label">时间维度</text>
|
|
<view class="period-list">
|
|
<view
|
|
v-for="p in timePeriods"
|
|
:key="p.value"
|
|
class="period-item"
|
|
:class="{ selected: reportForm.period === p.value }"
|
|
@click="reportForm.period = p.value"
|
|
>
|
|
<text>{{ p.label }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="form-item">
|
|
<text class="form-label">图表类型</text>
|
|
<view class="chart-type-list">
|
|
<view
|
|
v-for="t in chartTypes"
|
|
:key="t.value"
|
|
class="chart-type-item"
|
|
:class="{ selected: reportForm.chartType === t.value }"
|
|
@click="reportForm.chartType = t.value"
|
|
>
|
|
<text>{{ t.label }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="modal-footer">
|
|
<view class="btn btn-cancel" @click="closeModal">取消</view>
|
|
<view class="btn btn-primary" @click="saveReport">保存</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 留白 -->
|
|
<view style="height: 24px;"></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="uts">
|
|
import supa from '@/components/supadb/aksupainstance.uts'
|
|
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
|
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
|
|
|
type Report = {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
metrics: Array<string>
|
|
charts: Array<string>
|
|
updated_at: string
|
|
}
|
|
type Metric = { key: string; label: string }
|
|
type TimePeriod = { value: string; label: string }
|
|
type ChartType = { value: string; label: string }
|
|
type ReportForm = {
|
|
name: string
|
|
description: string
|
|
metrics: Array<string>
|
|
period: string
|
|
chartType: string
|
|
}
|
|
|
|
export default {
|
|
components: {
|
|
AnalyticsSidebarMenu,
|
|
AnalyticsTopBar
|
|
},
|
|
data() {
|
|
return {
|
|
showMoreMenu: false,
|
|
showSidebarMenu: false,
|
|
currentPath: '/pages/mall/analytics/custom-report',
|
|
showCreateModal: false,
|
|
editingReport: null as Report | null,
|
|
|
|
reports: [] as Array<Report>,
|
|
|
|
reportForm: {
|
|
name: '',
|
|
description: '',
|
|
metrics: [] as Array<string>,
|
|
period: '7d',
|
|
chartType: 'line'
|
|
} as ReportForm,
|
|
|
|
availableMetrics: [
|
|
{ key: 'gmv', label: 'GMV' },
|
|
{ key: 'orders', label: '订单数' },
|
|
{ key: 'users', label: '用户数' },
|
|
{ key: 'conversion', label: '转化率' },
|
|
{ key: 'avg_order', label: '客单价' },
|
|
{ key: 'repurchase', label: '复购率' }
|
|
] as Array<Metric>,
|
|
|
|
timePeriods: [
|
|
{ value: '7d', label: '7天' },
|
|
{ value: '30d', label: '30天' },
|
|
{ value: '90d', label: '90天' },
|
|
{ value: '1y', label: '1年' }
|
|
] as Array<TimePeriod>,
|
|
|
|
chartTypes: [
|
|
{ value: 'line', label: '折线图' },
|
|
{ value: 'bar', label: '柱状图' },
|
|
{ value: 'pie', label: '饼图' },
|
|
{ value: 'area', label: '面积图' },
|
|
{ value: 'combo', label: '组合图' }
|
|
] as Array<ChartType>
|
|
}
|
|
},
|
|
|
|
onLoad() {
|
|
this.currentPath = '/pages/mall/analytics/custom-report'
|
|
this.loadReports()
|
|
},
|
|
|
|
onShow() {
|
|
this.currentPath = '/pages/mall/analytics/custom-report'
|
|
},
|
|
|
|
methods: {
|
|
async loadReports() {
|
|
// TODO: 实现报表列表加载
|
|
},
|
|
|
|
createReport() {
|
|
this.editingReport = null
|
|
this.reportForm = {
|
|
name: '',
|
|
description: '',
|
|
metrics: [],
|
|
period: '7d',
|
|
chartType: 'line'
|
|
}
|
|
this.showCreateModal = true
|
|
},
|
|
|
|
editReport(report: Report) {
|
|
this.editingReport = report
|
|
this.reportForm = {
|
|
name: report.name,
|
|
description: report.description,
|
|
metrics: report.metrics,
|
|
period: '7d',
|
|
chartType: 'line'
|
|
}
|
|
this.showCreateModal = true
|
|
},
|
|
|
|
deleteReport(report: Report) {
|
|
uni.showModal({
|
|
title: '确认删除',
|
|
content: `确定要删除报表"${report.name}"吗?`,
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
// TODO: 实现删除逻辑
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
this.loadReports()
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
toggleMetric(key: string) {
|
|
const index = this.reportForm.metrics.indexOf(key)
|
|
if (index >= 0) {
|
|
this.reportForm.metrics.splice(index, 1)
|
|
} else {
|
|
this.reportForm.metrics.push(key)
|
|
}
|
|
},
|
|
|
|
saveReport() {
|
|
if (!this.reportForm.name.trim()) {
|
|
uni.showToast({ title: '请输入报表名称', icon: 'none' })
|
|
return
|
|
}
|
|
if (this.reportForm.metrics.length === 0) {
|
|
uni.showToast({ title: '请至少选择一个指标', icon: 'none' })
|
|
return
|
|
}
|
|
// TODO: 实现保存逻辑
|
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
|
this.closeModal()
|
|
this.loadReports()
|
|
},
|
|
|
|
openReport(report: Report) {
|
|
uni.navigateTo({
|
|
url: `/pages/mall/analytics/report-detail?id=${report.id}`
|
|
})
|
|
},
|
|
|
|
closeModal() {
|
|
this.showCreateModal = false
|
|
this.editingReport = null
|
|
},
|
|
|
|
refreshData() {
|
|
this.loadReports()
|
|
uni.showToast({ title: '已刷新', icon: 'success' })
|
|
},
|
|
|
|
handleMenu() {
|
|
this.showSidebarMenu = true
|
|
},
|
|
handleSidebarUpdate(visible: boolean) {
|
|
this.showSidebarMenu = visible
|
|
},
|
|
|
|
toggleMoreMenu() {
|
|
this.showMoreMenu = !this.showMoreMenu
|
|
},
|
|
|
|
closeMoreMenu() {
|
|
this.showMoreMenu = false
|
|
},
|
|
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' })
|
|
}
|
|
}
|
|
}
|
|
</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; /* 为固定顶部导航栏留出空间 */
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 16px 16px 28px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* 顶部栏 */
|
|
.topbar {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
background: #fff;
|
|
border-radius: 14px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.topbar-left {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.menu-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
background: #f3f4f6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.menu-icon:active {
|
|
background: #e5e7eb;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.menu-icon .icon {
|
|
font-size: 18px;
|
|
color: #111;
|
|
line-height: 1;
|
|
}
|
|
|
|
.title-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.title {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #111;
|
|
max-width: 420px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 12px;
|
|
color: rgba(0,0,0,0.55);
|
|
max-width: 420px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.topbar-right {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
gap: 8px;
|
|
align-items: center;
|
|
flex-wrap: nowrap;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.icon-btn-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
background: #f3f4f6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.icon-btn-icon:active {
|
|
background: #e5e7eb;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.icon-btn-icon .icon {
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.more-btn {
|
|
display: none;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
background: #f3f4f6;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.2s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.more-btn.active {
|
|
background: #e5e7eb;
|
|
}
|
|
|
|
.more-btn .icon {
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
color: #111;
|
|
}
|
|
|
|
/* 报表列表 */
|
|
.report-list {
|
|
margin-top: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.report-card {
|
|
background: #fff;
|
|
border-radius: 14px;
|
|
border: 1px solid rgba(0,0,0,0.06);
|
|
padding: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.report-card:active {
|
|
background: #f9fafb;
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.report-header {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.report-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #111;
|
|
}
|
|
|
|
.report-actions {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
background: #f3f4f6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.action-btn:active {
|
|
background: #e5e7eb;
|
|
}
|
|
|
|
.action-btn .icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.report-desc {
|
|
font-size: 13px;
|
|
color: rgba(0,0,0,0.65);
|
|
margin-bottom: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.report-meta {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.meta-item {
|
|
font-size: 12px;
|
|
color: rgba(0,0,0,0.45);
|
|
}
|
|
|
|
/* 模态框 */
|
|
.modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal-content {
|
|
width: 90%;
|
|
max-width: 600px;
|
|
max-height: 80vh;
|
|
background: #fff;
|
|
border-radius: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #111;
|
|
}
|
|
|
|
.modal-close {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
background: #f3f4f6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.modal-close .icon {
|
|
font-size: 18px;
|
|
color: #111;
|
|
}
|
|
|
|
.modal-body {
|
|
flex: 1;
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.form-item {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #111;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.form-input,
|
|
.form-textarea {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-textarea {
|
|
min-height: 80px;
|
|
resize: none;
|
|
}
|
|
|
|
.metric-list,
|
|
.period-list,
|
|
.chart-type-list {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.metric-item,
|
|
.period-item,
|
|
.chart-type-item {
|
|
padding: 8px 12px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.metric-item.selected,
|
|
.period-item.selected,
|
|
.chart-type-item.selected {
|
|
background: #111;
|
|
color: #fff;
|
|
border-color: #111;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
flex-direction: row !important;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
border-top: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
|
|
.btn {
|
|
flex: 1;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-cancel {
|
|
background: #f3f4f6;
|
|
color: #111;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #111;
|
|
color: #fff;
|
|
}
|
|
|
|
/* 响应式 */
|
|
@media screen and (max-width: 960px) {
|
|
.title,
|
|
.subtitle {
|
|
max-width: 200px;
|
|
}
|
|
|
|
.topbar-right .btn-hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.more-btn {
|
|
display: flex !important;
|
|
}
|
|
|
|
.modal-content {
|
|
width: 95%;
|
|
max-height: 90vh;
|
|
}
|
|
}
|
|
|
|
/* 响应式:窄屏时全屏显示 */
|
|
@media screen and (max-width: 959px) {
|
|
.page-layout {
|
|
flex-direction: column !important;
|
|
}
|
|
|
|
.main-content {
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style>
|