Files
medical-mall/pages/mall/analytics/custom-report.uvue
2026-01-31 21:47:42 +08:00

1377 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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" @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="toolbar">
<view class="toolbar-left">
<text class="toolbar-title">我的自定义报表</text>
<text class="toolbar-subtitle">按需组合指标和时间范围,生成专属报表</text>
</view>
<view class="toolbar-right">
<button class="btn-primary" @click.stop="createReport"> 新建报表</button>
</view>
</view>
<!-- 报表列表 / 空状态 -->
<view v-if="reports.length > 0" 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.period || '自定义' }}</text>
<text class="meta-item">最近更新:{{ report.updated_at || '-' }}</text>
</view>
</view>
</view>
<view v-else class="empty-state">
<text v-if="isLoggedIn" class="empty-title">暂无自定义报表</text>
<text v-else class="empty-title">请先登录</text>
<text v-if="isLoggedIn" class="empty-desc">点击下方按钮创建第一份报表,用于复用常看的指标组合。</text>
<text v-else class="empty-desc">创建自定义报表需要登录账号,请先登录后再使用此功能。</text>
<button v-if="isLoggedIn" class="btn-primary" @click.stop="createReport"> 新建报表</button>
<button v-else class="btn-primary" @click.stop="goToLogin">前往登录</button>
</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="请输入报表名称1-50个字符"
@input="onNameInput"
/>
<text v-if="formErrors.name" class="form-error">{{ formErrors.name }}</text>
</view>
<view class="form-item">
<text class="form-label">报表描述</text>
<textarea
class="form-textarea"
v-model="reportForm.description"
placeholder="选填最多200个字符"
@input="onDescriptionInput"
></textarea>
<text v-if="formErrors.description" class="form-error">{{ formErrors.description }}</text>
</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>
<text v-if="formErrors.metrics" class="form-error">{{ formErrors.metrics }}</text>
</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="selectPeriod(p.value)"
>
<text>{{ p.label }}</text>
</view>
</view>
<text v-if="formErrors.period" class="form-error">{{ formErrors.period }}</text>
</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="selectChartType(t.value)"
>
<text>{{ t.label }}</text>
</view>
</view>
<text v-if="formErrors.chartType" class="form-error">{{ formErrors.chartType }}</text>
</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 setup lang="uts">
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import { goToLogin } from '@/utils/utils.uts'
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
import { listCustomReports, createCustomReport, updateCustomReport, deleteCustomReport } from '@/services/analytics/customReportService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
import { onLoad, onShow, reactive, ref } from 'vue'
import type { CustomReport, ReportForm, ReportFormErrors } from '@/types/analytics/custom-report.uts'
import type { Metric, TimePeriod, ChartType } from '@/types/analytics/common.uts'
const showMoreMenu = ref(false)
const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/custom-report')
const showCreateModal = ref(false)
const editingReport = ref<CustomReport | null>(null)
const reports = reactive<Array<CustomReport>>([])
const isLoggedIn = ref(false)
const reportForm = reactive<ReportForm>({
name: '',
description: '',
metrics: [] as Array<string>,
period: '7d',
chartType: 'line'
})
const formErrors = reactive<ReportFormErrors>({
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
})
const availableMetrics = ref<Array<Metric>>([
{ key: 'gmv', label: 'GMV' },
{ key: 'orders', label: '订单数' },
{ key: 'users', label: '用户数' },
{ key: 'conversion', label: '转化率' },
{ key: 'avg_order', label: '客单价' },
{ key: 'repurchase', label: '复购率' }
])
const timePeriods = ref<Array<TimePeriod>>([
{ value: '7d', label: '7天' },
{ value: '30d', label: '30天' },
{ value: '90d', label: '90天' },
{ value: '1y', label: '1年' }
])
const chartTypes = ref<Array<ChartType>>([
{ value: 'line', label: '折线图' },
{ value: 'bar', label: '柱状图' },
{ value: 'pie', label: '饼图' },
{ value: 'area', label: '面积图' },
{ value: 'combo', label: '组合图' }
])
onLoad(() => {
currentPath.value = '/pages/mall/analytics/custom-report'
loadReports()
})
onShow(() => {
currentPath.value = '/pages/mall/analytics/custom-report'
})
async function loadReports() {
try {
await ensureSupabaseReady()
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
isLoggedIn.value = false
reports.splice(0, reports.length)
return
}
isLoggedIn.value = true
const items = await listCustomReports(uid)
const list: Array<CustomReport> = []
for (let i = 0; i < items.length; i++) {
const r = items[i]
list.push({
id: `${r.id}`,
name: `${r.title}`,
description: `${r.description || ''}`,
metrics: [] as Array<string>,
charts: [] as Array<string>,
updated_at: `${r.updated_at || ''}`
} as CustomReport)
}
reports.splice(0, reports.length, ...list)
} catch (e) {
console.error('loadReports failed', e)
uni.showToast({ title: '报表加载失败', icon: 'none' })
}
}
function createReport() {
editingReport.value = null
reportForm.name = ''
reportForm.description = ''
reportForm.metrics = [] as Array<string>
reportForm.period = '7d'
reportForm.chartType = 'line'
formErrors.name = ''
formErrors.description = ''
formErrors.metrics = ''
formErrors.period = ''
formErrors.chartType = ''
showCreateModal.value = true
}
function editReport(report: CustomReport) {
editingReport.value = report
reportForm.name = report.name
reportForm.description = report.description
reportForm.metrics = report.metrics
reportForm.period = '7d'
reportForm.chartType = 'line'
formErrors.name = ''
formErrors.description = ''
formErrors.metrics = ''
formErrors.period = ''
formErrors.chartType = ''
showCreateModal.value = true
}
function deleteReport(report: CustomReport) {
uni.showModal({
title: '确认删除',
content: `确定要删除报表"${report.name}"吗?`,
success: (res) => {
if (res.confirm) {
doDeleteReport(report)
}
}
})
}
async function doDeleteReport(report: CustomReport) {
try {
await ensureSupabaseReady()
await deleteCustomReport(report.id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadReports()
} catch (e: any) {
console.error('doDeleteReport failed', e)
const errorMsg = e?.message || '删除失败'
uni.showToast({ title: errorMsg, icon: 'none' })
}
}
function toggleMetric(key: string) {
const index = reportForm.metrics.indexOf(key)
if (index >= 0) {
reportForm.metrics.splice(index, 1)
} else {
reportForm.metrics.push(key)
}
if (reportForm.metrics.length > 0) {
formErrors.metrics = ''
}
}
function onNameInput() {
const name = reportForm.name.trim()
if (name.length === 0) {
formErrors.name = '报表名称不能为空'
} else if (name.length > 50) {
formErrors.name = '报表名称不能超过50个字符'
} else {
formErrors.name = ''
}
}
function onDescriptionInput() {
const desc = reportForm.description
if (desc.length > 200) {
formErrors.description = '报表描述不能超过200个字符'
} else {
formErrors.description = ''
}
}
function selectPeriod(value: string) {
reportForm.period = value
formErrors.period = ''
}
function selectChartType(value: string) {
reportForm.chartType = value
formErrors.chartType = ''
}
function validateReportForm(): boolean {
onNameInput()
onDescriptionInput()
if (reportForm.metrics.length === 0) {
formErrors.metrics = '请至少选择一个指标'
} else {
formErrors.metrics = ''
}
if (!reportForm.period) {
formErrors.period = '请选择时间维度'
}
if (!reportForm.chartType) {
formErrors.chartType = '请选择图表类型'
}
if (formErrors.name || formErrors.description || formErrors.metrics || formErrors.period || formErrors.chartType) {
uni.showToast({ title: '请先修正表单中的错误提示', icon: 'none' })
return false
}
return true
}
async function saveReport() {
if (!validateReportForm()) {
return
}
try {
uni.showLoading({ title: '保存中...' })
await ensureSupabaseReady()
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
uni.hideLoading()
uni.showModal({
title: '需要登录',
content: '创建自定义报表需要先登录,是否前往登录页面?',
success: (res) => {
if (res.confirm) {
goToLogin('/pages/mall/analytics/custom-report')
}
}
})
return
}
let newReportId = ''
if (editingReport.value == null) {
newReportId = await createCustomReport({
title: reportForm.name,
description: reportForm.description || '',
period: reportForm.period,
metrics: reportForm.metrics,
chartType: reportForm.chartType || 'line'
})
} else {
await updateCustomReport({
reportId: editingReport.value.id,
title: reportForm.name,
description: reportForm.description || null,
period: reportForm.period || null
})
newReportId = editingReport.value.id
}
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
closeModal()
loadReports()
if (newReportId.length > 0) {
setTimeout(() => {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${newReportId}`
})
}, 400)
}
} catch (e: any) {
uni.hideLoading()
console.error('saveReport exception:', e)
uni.showToast({
title: mapAnalyticsError(e, { fallbackMessage: '保存失败' }),
icon: 'none',
duration: 3000
})
}
}
function openReport(report: CustomReport) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${report.id}`
})
}
function closeModal() {
showCreateModal.value = false
editingReport.value = null
}
function refreshData() {
loadReports()
uni.showToast({ title: '已刷新', icon: 'success' })
}
function handleMenu() {
showSidebarMenu.value = true
}
function handleSidebarUpdate(visible: boolean) {
showSidebarMenu.value = visible
}
function toggleMoreMenu() {
showMoreMenu.value = !showMoreMenu.value
}
function closeMoreMenu() {
showMoreMenu.value = false
}
function handleSearch() {
uni.showToast({ title: '搜索', icon: 'none' })
}
function handleNotification() {
uni.showToast({ title: '通知', icon: 'none' })
}
function handleFullscreen() {
uni.showToast({ title: '全屏', icon: 'none' })
}
function handleMobile() {
uni.showToast({ title: '移动端', icon: 'none' })
}
function handleDropdown() {
uni.showToast({ title: '下拉菜单', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
}
function goToLogin() {
goToLogin('/pages/mall/analytics/custom-report')
}
return {
showMoreMenu: false,
showSidebarMenu: false,
currentPath: '/pages/mall/analytics/custom-report',
showCreateModal: false,
editingReport: null as Report | null,
reports: [] as Array<Report>,
isLoggedIn: false,
reportForm: {
name: '',
description: '',
metrics: [] as Array<string>,
period: '7d',
chartType: 'line'
} as ReportForm,
formErrors: {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
} as ReportFormErrors,
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() {
try {
await ensureSupabaseReady()
// 获取当前登录用户,用于按 owner_user_id 过滤自定义报表
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
// 未登录时显示空列表
this.isLoggedIn = false
this.reports = []
return
}
this.isLoggedIn = true
const items = await listCustomReports(uid)
const list: Array<Report> = []
for (let i = 0; i < items.length; i++) {
const r = items[i]
list.push({
id: `${r.id}`,
name: `${r.title}`,
description: `${r.description || ''}`,
metrics: [] as Array<string>,
charts: [] as Array<string>,
updated_at: `${r.updated_at || ''}`
} as Report)
}
this.reports = list
} catch (e) {
console.error('loadReports failed', e)
uni.showToast({ title: '报表加载失败', icon: 'none' })
}
},
createReport() {
this.editingReport = null
this.reportForm = {
name: '',
description: '',
metrics: [],
period: '7d',
chartType: 'line'
}
this.formErrors = {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
}
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.formErrors = {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
}
this.showCreateModal = true
},
deleteReport(report: Report) {
uni.showModal({
title: '确认删除',
content: `确定要删除报表"${report.name}"吗?`,
success: (res) => {
if (res.confirm) {
this.doDeleteReport(report)
}
}
})
},
async doDeleteReport(report: Report) {
try {
await ensureSupabaseReady()
await deleteCustomReport(report.id)
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadReports()
} catch (e: any) {
console.error('doDeleteReport failed', e)
const errorMsg = e?.message || '删除失败'
uni.showToast({ title: errorMsg, icon: 'none' })
}
},
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)
}
if (this.reportForm.metrics.length > 0) {
this.formErrors.metrics = ''
}
},
onNameInput() {
const name = this.reportForm.name.trim()
if (name.length === 0) {
this.formErrors.name = '报表名称不能为空'
} else if (name.length > 50) {
this.formErrors.name = '报表名称不能超过50个字符'
} else {
this.formErrors.name = ''
}
},
onDescriptionInput() {
const desc = this.reportForm.description
if (desc.length > 200) {
this.formErrors.description = '报表描述不能超过200个字符'
} else {
this.formErrors.description = ''
}
},
selectPeriod(value: string) {
this.reportForm.period = value
this.formErrors.period = ''
},
selectChartType(value: string) {
this.reportForm.chartType = value
this.formErrors.chartType = ''
},
validateReportForm(): boolean {
this.onNameInput()
this.onDescriptionInput()
if (this.reportForm.metrics.length === 0) {
this.formErrors.metrics = '请至少选择一个指标'
} else {
this.formErrors.metrics = ''
}
if (!this.reportForm.period) {
this.formErrors.period = '请选择时间维度'
}
if (!this.reportForm.chartType) {
this.formErrors.chartType = '请选择图表类型'
}
if (this.formErrors.name || this.formErrors.description || this.formErrors.metrics || this.formErrors.period || this.formErrors.chartType) {
uni.showToast({ title: '请先修正表单中的错误提示', icon: 'none' })
return false
}
return true
},
async saveReport() {
if (!this.validateReportForm()) {
return
}
try {
uni.showLoading({ title: '保存中...' })
await ensureSupabaseReady()
// 获取当前登录用户,作为 owner_user_id
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
uni.hideLoading()
uni.showModal({
title: '需要登录',
content: '创建自定义报表需要先登录,是否前往登录页面?',
success: (res) => {
if (res.confirm) {
goToLogin('/pages/mall/analytics/custom-report')
}
}
})
return
}
let newReportId = ''
// 1) 创建或更新自定义报表
if (this.editingReport == null) {
newReportId = await createCustomReport({
title: this.reportForm.name,
description: this.reportForm.description || '',
period: this.reportForm.period,
metrics: this.reportForm.metrics,
chartType: this.reportForm.chartType || 'line'
})
} else {
await updateCustomReport({
reportId: this.editingReport.id,
title: this.reportForm.name,
description: this.reportForm.description || null,
period: this.reportForm.period || null
})
newReportId = this.editingReport.id
}
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
this.closeModal()
this.loadReports()
// 新建或编辑成功后,直接进入报表详情页,给用户明确反馈
if (newReportId.length > 0) {
setTimeout(() => {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${newReportId}`
})
}, 400)
}
} catch (e: any) {
uni.hideLoading()
console.error('saveReport exception:', e)
uni.showToast({
title: mapAnalyticsError(e, { fallbackMessage: '保存失败' }),
icon: 'none',
duration: 3000
})
}
},
openReport(report: Report) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${report.id}`
})
},
closeModal() {
this.showCreateModal = false
this.editingReport = null
},
goToLogin() {
goToLogin('/pages/mall/analytics/custom-report')
},
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;
}
/* 工具栏 */
.toolbar {
margin-top: 12px;
padding: 12px 16px;
background: #fff;
border-radius: 14px;
border: 1px solid rgba(0,0,0,0.06);
display: flex;
flex-direction: row !important;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.toolbar-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.toolbar-title {
font-size: 14px;
font-weight: 600;
color: #111;
}
.toolbar-subtitle {
font-size: 12px;
color: rgba(0,0,0,0.55);
}
.toolbar-right {
display: flex;
flex-direction: row !important;
align-items: center;
gap: 8px;
}
.btn-primary {
padding: 8px 16px;
border-radius: 999px;
border: none;
background: #111827;
color: #fff;
font-size: 14px;
}
.btn-primary:active {
opacity: 0.9;
}
/* 报表列表 */
.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);
}
.empty-state {
margin-top: 24px;
padding: 32px 16px;
background: #fff;
border-radius: 14px;
border: 1px solid rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: #111;
}
.empty-desc {
font-size: 13px;
color: rgba(0,0,0,0.55);
text-align: center;
}
/* 模态框 */
.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-error {
margin-top: 4px;
font-size: 12px;
color: #dc2626;
}
.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>