Files
medical-mall/pages/mall/analytics/profile.uvue
2026-02-04 09:14:37 +08:00

1110 lines
29 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">
<!-- 固定顶部导航栏 -->
<AnalyticsTopBar
:title="'个人中心'"
:lastUpdateTime="''"
@menu-click="handleMenu"
@refresh="handleRefresh"
@search="handleSearch"
@notification="handleNotification"
@fullscreen="handleFullscreen"
@mobile="handleMobile"
@dropdown="handleDropdown"
@settings="goToSettings"
></AnalyticsTopBar>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
></AnalyticsSidebarMenu>
<!-- 主内容区域 -->
<view class="main-content">
<view class="analytics-profile">
<!-- 蓝色系顶部看板 -->
<view class="header-card">
<view class="header-top-section">
<view class="profile-info-row">
<view class="avatar-wrapper" @click="editProfile">
<view class="avatar-square">1</view>
</view>
<view class="info-content">
<view class="name-row">
<text class="analyst-name">113</text>
<view class="status-badge">未认证</view>
</view>
<view class="id-row">
<text class="enterprise-id">企业编号FGA9DLX41K6 | 认证立享群成员/云文档储存空间/参会人/邮箱扩容</text>
<text class="verify-link" @click="goToSettings">立即认证 &gt;</text>
</view>
</view>
</view>
</view>
<view class="header-divider"></view>
<!-- 横向统计指标 -->
<view class="stats-row">
<view class="stat-box">
<text class="stat-label">组织总人数</text>
<text class="stat-num">1</text>
</view>
<view class="stat-box">
<text class="stat-label">部门数</text>
<text class="stat-num">0</text>
</view>
<view class="stat-box">
<text class="stat-label">超级管理员</text>
<text class="stat-num">1</text>
</view>
<view class="stat-box">
<text class="stat-label">子管理员</text>
<text class="stat-num">1</text>
</view>
</view>
<!-- 底部横条 -->
<view class="banner-bar">
<view class="banner-left">
<view class="banner-icon-wrapper">
<text class="banner-icon-v">V</text>
</view>
<text class="banner-text">飞书基础免费版</text>
<text class="banner-subtext">升级版本,享更多专属权益及服务</text>
</view>
<view class="banner-right">
<view class="consult-link">
<text class="consult-icon">🎧</text>
<text class="consult-text">升级咨询</text>
</view>
<button class="upgrade-btn">立即升级</button>
</view>
</view>
</view>
<!-- 快捷入口 -->
<view class="section-container">
<view class="section-title">快捷入口</view>
<view class="quick-grid">
<view class="quick-item" @click="handleQuickJump('user-analysis')">
<view class="quick-icon-box bg-lark-blue">
<text class="quick-icon">👥</text>
</view>
<text class="quick-label">用户分析</text>
</view>
<view class="quick-item" @click="handleQuickJump('sales-report')">
<view class="quick-icon-box bg-lark-cyan">
<text class="quick-icon">📊</text>
</view>
<text class="quick-label">销售报表</text>
</view>
<view class="quick-item" @click="handleQuickJump('product-insights')">
<view class="quick-icon-box bg-lark-indigo">
<text class="quick-icon">🛍️</text>
</view>
<text class="quick-label">商品洞察</text>
</view>
<view class="quick-item" @click="handleQuickJump('market-trends')">
<view class="quick-icon-box bg-lark-teal">
<text class="quick-icon">📈</text>
</view>
<text class="quick-label">市场趋势</text>
</view>
<view class="quick-item" @click="handleQuickJump('coupon-analysis')">
<view class="quick-icon-box bg-lark-purple">
<text class="quick-icon">🎫</text>
</view>
<text class="quick-label">优惠券分析</text>
</view>
<view class="quick-item" @click="handleQuickJump('delivery-analysis')">
<view class="quick-icon-box bg-lark-soft-blue">
<text class="quick-icon">🚚</text>
</view>
<text class="quick-label">配送分析</text>
</view>
</view>
</view>
<!-- 功能使用情况 -->
<view class="section-container">
<view class="section-header-row">
<text class="section-title">功能使用情况 ></text>
</view>
<view class="usage-stats">
<view class="usage-column">
<text class="usage-label">昨日活跃人数</text>
<text class="usage-value">0</text>
</view>
<view class="usage-column">
<text class="usage-label">昨日活跃率</text>
<text class="usage-value">0.00%</text>
</view>
</view>
<!-- 趋势图 -->
<view class="trend-container">
<view class="trend-header">
<view class="trend-title-box">
<text class="trend-main-title">功能使用趋势</text>
<text class="trend-subtitle">截止日期2026/02/02</text>
</view>
<view class="trend-legend">
<view class="legend-item"><view class="legend-dot blue"></view><text class="legend-text">总量</text></view>
<view class="legend-item"><view class="legend-dot light-blue"></view><text class="legend-text">消息</text></view>
<view class="legend-item"><view class="legend-dot purple"></view><text class="legend-text">云文档</text></view>
<view class="legend-item"><view class="legend-dot orange"></view><text class="legend-text">日历</text></view>
<view class="legend-item"><view class="legend-dot teal"></view><text class="legend-text">视频会议</text></view>
</view>
</view>
<view class="line-chart-area">
<EChartsView class="trend-chart" :option="trendChartOption" />
</view>
</view>
</view>
<!-- 功能菜单 (保留原有的帮助与反馈) -->
<view class="section-container no-padding">
<view class="menu-group">
<view class="menu-item" @click="goToHelp">
<text class="menu-icon">❓</text>
<text class="menu-label">帮助中心</text>
<text class="menu-arrow">&gt;</text>
</view>
<view class="menu-item" @click="goToFeedback">
<text class="menu-icon">💬</text>
<text class="menu-label">意见反馈</text>
<text class="menu-arrow">&gt;</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
import type { UserType } from '@/types/mall-types'
import { formatTime } from '@/utils/utils.uts'
import type { RecentReport, OverviewData, ReportCounts, TodayInsights, TrendDatum } from '@/types/analytics/profile.uts'
// 响应式数据
const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/profile')
// TODO: 与 Supabase Auth / users 表打通后,这里应该来自 auth.uid()
// 现在先使用 analytics 测试 seed 中的固定用户 ID保证可联调出“真实数据效果”
const currentUserId = ref('00000000-0000-0000-0000-000000000001')
const analystInfo = ref({
id: '',
phone: '',
nickname: '数据分析师',
avatar_url: ''
} as UserType)
const workExperience = ref(5)
const expertise = ref('电商数据')
const overviewData = ref<OverviewData>({
totalSales: '0',
salesGrowth: 0,
totalUsers: '0',
userGrowth: 0,
totalOrders: '0',
orderGrowth: 0,
conversionRate: 0,
conversionGrowth: 0
})
const reportCounts = ref<ReportCounts>({
total: 0,
pending: 0,
scheduled: 0,
shared: 0
})
const todayInsights = ref<TodayInsights>({
hotProduct: '-',
peakTraffic: '0',
conversionAnomaly: '-',
mobileRatio: 0
})
const recentReports = ref<Array<RecentReport>>([])
const trendPeriod = ref('week')
const trendChartOption = ref<any>({})
const trendData = ref<Array<TrendDatum>>([
{ label: '周一', sales: 0, orders: 0 },
{ label: '周二', sales: 0, orders: 0 },
{ label: '周三', sales: 0, orders: 0 },
{ label: '周四', sales: 0, orders: 0 },
{ label: '周五', sales: 0, orders: 0 },
{ label: '周六', sales: 0, orders: 0 },
{ label: '周日', sales: 0, orders: 0 }
])
// 计算属性
const maxSales = computed(() => {
return Math.max(...trendData.value.map(item => item.sales))
})
const maxOrders = computed(() => {
return Math.max(...trendData.value.map(item => item.orders))
})
// 生命周期
onMounted(() => {
currentPath.value = '/pages/mall/analytics/profile'
void loadAll()
})
// 方法
function handleMenu() {
showSidebarMenu.value = true
}
function handleSidebarUpdate(visible: boolean) {
showSidebarMenu.value = visible
}
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
function fmtInt(n: number): string {
const v = isFinite(n) ? Math.round(n) : 0
return v.toLocaleString()
}
function fmtMoney(n: number): string {
const v = isFinite(n) ? n : 0
return v.toLocaleString()
}
function pctGrowth(cur: number, prev: number): number {
if (prev > 0) return ((cur - prev) / prev) * 100
return cur > 0 ? 100 : 0
}
function dateISO(d: Date): string {
return d.toISOString().slice(0, 10)
}
async function loadAll() {
await loadAnalystInfo()
await loadReportCounts()
await loadRecentReports()
await loadOverview()
await loadTrend()
await loadTodayInsights()
}
async function loadAnalystInfo() {
try {
const res: any = await supa
.from('users')
.select('id, phone, email, nickname, avatar_url')
.eq('id', currentUserId.value)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
if (rows.length > 0) {
analystInfo.value = {
...(analystInfo.value as any),
id: `${rows[0].id}`,
phone: `${rows[0].phone || ''}`,
email: `${rows[0].email || ''}`,
nickname: `${rows[0].nickname || '数据分析师'}`,
avatar_url: `${rows[0].avatar_url || ''}`
} as any
}
} catch (e) {
console.error('loadAnalystInfo failed', e)
}
}
async function loadReportCounts() {
try {
const res: any = await supa
.from('analytics_reports')
.select('status')
.eq('owner_user_id', currentUserId.value)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
let total = rows.length
let pending = 0
let scheduled = 0
let shared = 0
for (let i = 0; i < rows.length; i++) {
const s = `${rows[i].status || ''}`
if (s === 'pending') pending++
if (s === 'scheduled') scheduled++
if (s === 'shared') shared++
}
reportCounts.value = { total, pending, scheduled, shared }
} catch (e) {
console.error('loadReportCounts failed', e)
}
}
async function loadRecentReports() {
try {
const res: any = await supa
.from('analytics_reports')
.select('id, title, description, status, created_at')
.eq('owner_user_id', currentUserId.value)
.order('created_at', { ascending: false } as any)
.limit(5 as any)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
recentReports.value = rows.map((r: any) => ({
id: `${r.id}`,
title: `${r.title}`,
description: `${r.description || ''}`,
status: `${r.status || 'ready'}`,
created_at: `${r.created_at || ''}`
}))
} catch (e) {
console.error('loadRecentReports failed', e)
}
}
async function loadOverview() {
try {
const now = new Date()
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const start = new Date(end.getTime() - 29 * 24 * 60 * 60 * 1000) // Last 30 days
const p = new UTSJSONObject()
p.set('p_start_date', dateISO(start))
p.set('p_end_date', dateISO(end))
const [salesKpisRes, userKpisRes] = await Promise.all([
supa.rpc('rpc_analytics_sales_kpis', p),
supa.rpc('rpc_analytics_user_kpis', p)
])
const salesKpis = Array.isArray(salesKpisRes.data) && salesKpisRes.data.length > 0 ? salesKpisRes.data[0] : {}
const userKpis = Array.isArray(userKpisRes.data) && userKpisRes.data.length > 0 ? userKpisRes.data[0] : {}
overviewData.value = {
totalSales: fmtMoney(safeNumber(salesKpis.gmv)),
salesGrowth: safeNumber(salesKpis.gmv_growth),
totalUsers: fmtInt(safeNumber(userKpis.new_users)), // Use new_users for the period
userGrowth: safeNumber(userKpis.new_user_growth),
totalOrders: fmtInt(safeNumber(salesKpis.orders)),
orderGrowth: safeNumber(salesKpis.order_growth),
conversionRate: safeNumber(salesKpis.conversion_rate),
conversionGrowth: safeNumber(salesKpis.conversion_growth)
}
} catch (e) {
console.error('loadOverview failed', e)
}
}
async function loadTrend() {
try {
const now = new Date()
if (trendPeriod.value === 'week') {
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const start = new Date(end.getTime() - 6 * 24 * 60 * 60 * 1000)
const p = new UTSJSONObject()
p.set('p_start_date', dateISO(start))
p.set('p_end_date', dateISO(end))
const res: any = await supa.rpc('rpc_analytics_sales_trend', p)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
const weekLabels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
trendData.value = rows.map((r: any) => {
const d = new Date(`${r.date}T00:00:00`)
return {
label: weekLabels[d.getDay()],
sales: safeNumber(r.gmv),
orders: safeNumber(r.orders)
}
})
} else if (trendPeriod.value === 'month') {
// 最近6个月按月聚合
const end = new Date(now.getFullYear(), now.getMonth(), 1)
const start = new Date(end.getFullYear(), end.getMonth() - 5, 1)
const p = new UTSJSONObject()
p.set('p_start_date', dateISO(start))
p.set('p_end_date', dateISO(new Date(now.getFullYear(), now.getMonth(), now.getDate())))
const res: any = await supa.rpc('rpc_analytics_sales_trend', p)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
const buckets: Record<string, { sales: number; orders: number }> = {}
for (let i = 0; i < rows.length; i++) {
const key = `${rows[i].date}`.slice(0, 7) // yyyy-mm
if (!buckets[key]) buckets[key] = { sales: 0, orders: 0 }
buckets[key].sales += safeNumber(rows[i].gmv)
buckets[key].orders += safeNumber(rows[i].orders)
}
const keys = Object.keys(buckets).sort()
trendData.value = keys.map((k) => ({
label: `${k.slice(5)}月`,
sales: buckets[k].sales,
orders: buckets[k].orders
}))
} else {
// quarter最近4个季度按季度聚合
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const start = new Date(end.getFullYear(), end.getMonth() - 11, 1)
const p = new UTSJSONObject()
p.set('p_start_date', dateISO(start))
p.set('p_end_date', dateISO(end))
const res: any = await supa.rpc('rpc_analytics_sales_trend', p)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
const buckets: Record<string, { sales: number; orders: number }> = {}
for (let i = 0; i < rows.length; i++) {
const d = new Date(`${rows[i].date}T00:00:00`)
const q = Math.floor(d.getMonth() / 3) + 1
const key = `${d.getFullYear()}-Q${q}`
if (!buckets[key]) buckets[key] = { sales: 0, orders: 0 }
buckets[key].sales += safeNumber(rows[i].gmv)
buckets[key].orders += safeNumber(rows[i].orders)
}
const keys = Object.keys(buckets).sort()
trendData.value = keys.map((k) => ({
label: k.split('-')[1],
sales: buckets[k].sales,
orders: buckets[k].orders
}))
}
// 更新 ECharts 配置以匹配图 2
updateTrendChart()
} catch (e) {
console.error('loadTrend failed', e)
}
}
function updateTrendChart() {
const xData = ['01/13', '01/17', '01/21', '01/25', '01/29', '02/02']
// 模拟数据以对齐图 2 的峰值点
const yData = [0.4, 0.4, 0.35, 1, 0.25, 1, 0.5]
trendChartOption.value = {
grid: { left: 40, right: 10, top: 20, bottom: 30 },
xAxis: {
type: 'category',
data: xData,
axisLine: { lineStyle: { color: '#f0f0f0' } },
axisLabel: { color: '#8c929b', fontSize: 10 },
boundaryGap: false
},
yAxis: {
type: 'value',
min: 0,
max: 1,
interval: 0.2,
axisLabel: { color: '#8c929b', fontSize: 10 },
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } }
},
series: [
{
name: '功能使用趋势',
type: 'line',
data: yData,
smooth: false, // 匹配图 2 的直线折线
symbol: 'none',
lineStyle: { color: '#5a7fff', width: 2 },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(90, 127, 255, 0.1)' },
{ offset: 1, color: 'rgba(90, 127, 255, 0.01)' }
]
}
}
}
]
}
}
async function loadTodayInsights() {
try {
const now = new Date()
const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const p = new UTSJSONObject()
p.set('p_start_date', dateISO(today0))
p.set('p_end_date', dateISO(today0))
p.set('p_limit', 1)
const prodRes: any = await supa.rpc('rpc_analytics_top_products', p)
const prodRows: Array<any> = Array.isArray(prodRes.data) ? (prodRes.data as Array<any>) : []
if (prodRows.length > 0) {
todayInsights.value.hotProduct = `${prodRows[0].name}`
}
// 访问量峰值用今日浏览行为数近似主库口径ml_browse_history
const pvRes: any = await supa
.from('ml_browse_history')
.select('id, created_at')
.gte('created_at', today0.toISOString())
.lt('created_at', new Date(today0.getTime() + 24 * 60 * 60 * 1000).toISOString())
const pvRows: Array<any> = Array.isArray(pvRes.data) ? (pvRes.data as Array<any>) : []
todayInsights.value.peakTraffic = fmtInt(pvRows.length)
// 转化异常:使用无参版 rpc_analytics_realtime_kpis
const kpiRes: any = await supa.rpc('rpc_analytics_realtime_kpis', {} as any)
const row = Array.isArray(kpiRes.data) && kpiRes.data.length > 0 ? kpiRes.data[0] : (kpiRes.data || {})
const cg = safeNumber(row.conversion_growth)
todayInsights.value.conversionAnomaly = cg < 0 ? `下降${Math.abs(cg).toFixed(1)}%` : `上升${cg.toFixed(1)}%`
// mobileRatio暂无来源维度先置 0后续可接入埋点/设备信息)
todayInsights.value.mobileRatio = 0
} catch (e) {
console.error('loadTodayInsights failed', e)
}
}
function getAnalystRole(): string {
return '高级数据分析师'
}
function getReportStatusText(status: any): string {
const s = `${status || ''}`
const statusMap: Record<string, string> = {
pending: '待生成',
ready: '已完成',
failed: '失败',
scheduled: '定时',
shared: '共享'
}
return statusMap[s] || '未知'
}
function changeTrendPeriod(period: string) {
trendPeriod.value = period
void loadTrend()
}
function viewReportDetail(reportId: string) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${reportId}`
})
}
// 导航方法
function handleQuickJump(pageName: string) {
uni.navigateTo({
url: `/pages/mall/analytics/${pageName}`
})
}
function editProfile() {
uni.navigateTo({
url: '/pages/mall/analytics/profile-edit'
})
}
function goToSettings() {
uni.navigateTo({
url: '/pages/mall/analytics/settings'
})
}
function goToReports(type: string) {
uni.navigateTo({
url: `/pages/mall/analytics/reports?type=${type}`
})
}
function goToDataSource() {
uni.navigateTo({
url: '/pages/mall/analytics/data-source'
})
}
function goToAlerts() {
uni.navigateTo({
url: '/pages/mall/analytics/alerts'
})
}
function goToExport() {
uni.navigateTo({
url: '/pages/mall/analytics/export'
})
}
function goToHelp() {
uni.navigateTo({
url: '/pages/mall/common/help'
})
}
function goToFeedback() {
uni.navigateTo({
url: '/pages/mall/common/feedback'
})
}
</script>
<style scoped>
.page {
min-height: 100vh;
background: #f6f8fb;
}
/* 页面布局 */
.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;
}
.analytics-profile {
padding: 20rpx 30rpx 120rpx 30rpx;
}
/* 蓝色看板头部 */
.header-card {
background: #ffffff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
display: flex;
flex-direction: column;
}
.header-top-section {
padding-bottom: 20rpx;
}
.profile-info-row {
display: flex;
flex-direction: row !important;
align-items: center;
}
.avatar-wrapper {
margin-right: 30rpx;
}
.avatar-square {
width: 90rpx;
height: 90rpx;
background-color: #3370ff;
color: #ffffff;
font-size: 40rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
}
.info-content {
flex: 1;
}
.name-row {
display: flex;
flex-direction: row !important;
align-items: center;
margin-bottom: 8rpx;
}
.analyst-name {
font-size: 36rpx;
font-weight: 500;
color: #1f2329;
margin-right: 20rpx;
}
.status-badge {
background: #f2f3f5;
color: #8f959e;
font-size: 22rpx;
padding: 4rpx 16rpx;
border-radius: 4rpx;
}
.id-row {
display: flex;
flex-direction: row !important;
align-items: center;
flex-wrap: wrap;
}
.enterprise-id {
font-size: 24rpx;
color: #8f959e;
line-height: 1.5;
}
.verify-link {
font-size: 24rpx;
color: #3370ff;
margin-left: 10rpx;
}
.header-divider {
height: 1rpx;
background-color: #eff0f1;
margin: 10rpx 0 30rpx 0;
}
/* 统计指标行 */
.stats-row {
display: flex;
flex-direction: row !important;
justify-content: space-between;
margin-bottom: 40rpx;
padding: 0 10rpx;
}
.stat-box {
flex: 1;
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 26rpx;
color: #8f959e;
margin-bottom: 15rpx;
}
.stat-num {
font-size: 48rpx;
font-weight: 600;
color: #1f2329;
}
/* Banner条 */
.banner-bar {
background: #f0f4ff;
border-radius: 8rpx;
padding: 16rpx 24rpx;
display: flex;
flex-direction: row !important;
justify-content: space-between;
align-items: center;
}
.banner-left {
display: flex;
flex-direction: row !important;
align-items: center;
}
.banner-icon-wrapper {
background: #3370ff;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.banner-icon-v {
color: white;
font-size: 18rpx;
font-weight: bold;
}
.banner-text {
font-size: 26rpx;
color: #1f2329;
font-weight: 500;
margin-right: 20rpx;
}
.banner-subtext {
font-size: 26rpx;
color: #646a73;
}
.banner-right {
display: flex;
flex-direction: row !important;
align-items: center;
}
.consult-link {
display: flex;
flex-direction: row !important;
align-items: center;
margin-right: 30rpx;
}
.consult-icon {
font-size: 28rpx;
color: #3370ff;
margin-right: 8rpx;
}
.consult-text {
font-size: 26rpx;
color: #3370ff;
}
.upgrade-btn {
background: #3370ff;
color: white;
font-size: 26rpx;
border-radius: 8rpx;
padding: 12rpx 24rpx;
border: none;
line-height: normal;
}
/* 通用容器 */
.section-container {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
}
.section-container.no-padding {
padding: 10rpx 30rpx;
}
.section-header-row {
display: flex;
flex-direction: row !important;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #1f2329;
}
.section-more {
color: #999;
font-size: 32rpx;
}
/* 快捷入口网格 */
.quick-grid {
display: flex;
flex-direction: row !important;
justify-content: space-around;
padding: 10rpx 0;
}
.quick-item {
width: 16.66%;
display: flex;
flex-direction: column;
align-items: center;
}
.quick-icon-box {
width: 90rpx;
height: 90rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
}
.quick-icon {
font-size: 44rpx;
}
/* Lark Icon Colors */
.bg-lark-blue { background-color: #3370ff; color: #ffffff; }
.bg-lark-cyan { background-color: #2ebcfc; color: #ffffff; }
.bg-lark-indigo { background-color: #6366f1; color: #ffffff; }
.bg-lark-teal { background-color: #00b8a9; color: #ffffff; }
.bg-lark-purple { background-color: #8b5cf6; color: #ffffff; }
.bg-lark-soft-blue { background-color: #3b82f6; color: #ffffff; }
.quick-label {
font-size: 24rpx;
color: #1f2329;
text-align: center;
white-space: nowrap;
}
/* 使用统计 */
.usage-stats {
display: flex;
flex-direction: row !important;
margin-bottom: 40rpx;
}
.usage-column {
margin-right: 80rpx;
display: flex;
flex-direction: column;
}
.usage-label {
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
display: block;
}
.usage-value {
font-size: 56rpx;
font-weight: bold;
color: #1f2329;
margin-top: 4rpx;
}
/* 趋势图 */
.trend-container {
border-top: 1rpx solid #eff0f1;
padding-top: 30rpx;
margin-top: 20rpx;
}
.trend-header {
display: flex;
flex-direction: row !important;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30rpx;
}
.trend-title-box {
display: flex;
flex-direction: column;
}
.trend-main-title {
font-size: 28rpx;
font-weight: bold;
color: #1f2329;
display: block;
margin-bottom: 8rpx;
}
.trend-subtitle {
font-size: 22rpx;
color: #8c929b;
}
.trend-legend {
display: flex;
flex-direction: row !important;
align-items: center;
}
.legend-item {
display: flex;
flex-direction: row !important;
align-items: center;
margin-left: 20rpx;
}
.legend-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.legend-dot.blue { background-color: #3370ff; }
.legend-dot.light-blue { background-color: #2ebcfc; }
.legend-dot.purple { background-color: #8b5cf6; }
.legend-dot.orange { background-color: #ff9c38; }
.legend-dot.teal { background-color: #2db7b5; }
.legend-text {
font-size: 22rpx;
color: #646a73;
}
.line-chart-area {
height: 320rpx;
position: relative;
margin-top: 10rpx;
}
.trend-chart {
width: 100%;
height: 320rpx;
}
/* 导航组 */
/* 菜单组 */
.menu-group {
margin-top: 10rpx;
display: flex;
flex-direction: column;
}
.menu-item {
display: flex;
flex-direction: row !important;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f8f8f8;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.menu-label {
flex: 1;
font-size: 28rpx;
color: #333;
}
.menu-arrow {
font-size: 24rpx;
color: #ccc;
}
/* 响应式 */
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
}
.main-content {
width: 100%;
}
.quick-item {
width: 16.66%;
}
}
</style>