Files
medical-mall/pages/mall/analytics/custom-report.uvue
2026-02-06 12:06:33 +08:00

1045 lines
24 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="閫夊~锛屾渶澶?00涓瓧绗?
@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 as goToLoginPage } from '@/utils/utils.uts'
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
import { ensureAnalyticsLogin } from '@/services/analytics/authGuard.uts'
import { listCustomReports, createCustomReport, updateCustomReport, deleteCustomReport } from '@/services/analytics/customReportService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
import { reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
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'
if (!ensureAnalyticsLogin({ toastTitle: '璇峰厛鐧诲綍鍚庝娇鐢ㄨ嚜瀹氫箟鎶ヨ〃' })) return
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()
// 妫€鏌?newReportId 鏄惁鏈夋晥锛屾棤鏁堝垯璁や负鍒涘缓澶辫触
if (newReportId == null || newReportId.length === 0) {
uni.showToast({
title: '鍒涘缓澶辫触锛氭湭杩斿洖鎶ヨ〃ID',
icon: 'none',
duration: 3000
})
return
}
uni.showToast({ title: '淇濆瓨鎴愬姛', icon: 'success' })
closeModal()
loadReports()
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 goToLogin() {
ensureAnalyticsLogin({ toastTitle: '璇峰厛鐧诲綍鍚庝娇鐢ㄨ嚜瀹氫箟鎶ヨ〃' })
}
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 handleGoToLogin() {
goToLoginPage('/pages/mall/analytics/custom-report')
}
</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 removed */ 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>