Files
medical-mall/pages/mall/analytics/profile.uvue
2026-01-26 21:34:17 +08:00

1267 lines
33 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"
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<view class="main-content">
<view class="analytics-profile">
<!-- 分析师信息头部 -->
<view class="profile-header">
<image :src="analystInfo.avatar_url || '/static/default-avatar.png'" class="analyst-avatar" @click="editProfile" />
<view class="analyst-info">
<text class="analyst-name">{{ analystInfo.nickname || analystInfo.phone }}</text>
<text class="analyst-role">{{ getAnalystRole() }}</text>
<view class="analyst-stats">
<text class="stat-item">工作经验: {{ workExperience }}年</text>
<text class="stat-item">专业领域: {{ expertise }}</text>
</view>
</view>
<view class="settings-icon" @click="goToSettings">⚙️</view>
</view>
<!-- 数据概览 -->
<view class="data-overview">
<view class="section-title">数据概览</view>
<view class="overview-cards">
<view class="overview-card" @click="goToReports('sales')">
<text class="card-icon">💰</text>
<text class="card-title">销售数据</text>
<text class="card-value">¥{{ overviewData.totalSales }}</text>
<text class="card-change positive">+{{ overviewData.salesGrowth }}%</text>
</view>
<view class="overview-card" @click="goToReports('users')">
<text class="card-icon">👥</text>
<text class="card-title">用户增长</text>
<text class="card-value">{{ overviewData.totalUsers }}</text>
<text class="card-change positive">+{{ overviewData.userGrowth }}%</text>
</view>
<view class="overview-card" @click="goToReports('orders')">
<text class="card-icon">📋</text>
<text class="card-title">订单量</text>
<text class="card-value">{{ overviewData.totalOrders }}</text>
<text class="card-change" :class="{ positive: overviewData.orderGrowth > 0 }">
{{ overviewData.orderGrowth > 0 ? '+' : '' }}{{ overviewData.orderGrowth }}%
</text>
</view>
<view class="overview-card" @click="goToReports('conversion')">
<text class="card-icon">📈</text>
<text class="card-title">转化率</text>
<text class="card-value">{{ overviewData.conversionRate }}%</text>
<text class="card-change positive">+{{ overviewData.conversionGrowth }}%</text>
</view>
</view>
</view>
<!-- 报表管理 -->
<view class="report-management">
<view class="section-title">报表管理</view>
<view class="report-tabs">
<view class="report-tab" @click="goToReports('all')">
<text class="tab-icon">📊</text>
<text class="tab-text">全部报表</text>
<text v-if="reportCounts.total > 0" class="tab-badge">{{ reportCounts.total }}</text>
</view>
<view class="report-tab" @click="goToReports('pending')">
<text class="tab-icon">⏳</text>
<text class="tab-text">待生成</text>
<text v-if="reportCounts.pending > 0" class="tab-badge alert">{{ reportCounts.pending }}</text>
</view>
<view class="report-tab" @click="goToReports('scheduled')">
<text class="tab-icon">📅</text>
<text class="tab-text">定时报表</text>
<text v-if="reportCounts.scheduled > 0" class="tab-badge">{{ reportCounts.scheduled }}</text>
</view>
<view class="report-tab" @click="goToReports('shared')">
<text class="tab-icon">🔗</text>
<text class="tab-text">共享报表</text>
<text v-if="reportCounts.shared > 0" class="tab-badge">{{ reportCounts.shared }}</text>
</view>
</view>
</view>
<!-- 今日数据洞察 -->
<view class="today-insights">
<view class="section-title">今日洞察</view>
<view class="insight-grid">
<view class="insight-card">
<text class="insight-icon">🔥</text>
<text class="insight-title">热销商品</text>
<text class="insight-value">{{ todayInsights.hotProduct }}</text>
<text class="insight-desc">销量同比增长156%</text>
</view>
<view class="insight-card">
<text class="insight-icon">⚡</text>
<text class="insight-title">流量峰值</text>
<text class="insight-value">{{ todayInsights.peakTraffic }}</text>
<text class="insight-desc">14:30达到峰值</text>
</view>
<view class="insight-card">
<text class="insight-icon">🎯</text>
<text class="insight-title">转化异常</text>
<text class="insight-value">{{ todayInsights.conversionAnomaly }}</text>
<text class="insight-desc">需要关注</text>
</view>
<view class="insight-card">
<text class="insight-icon">📱</text>
<text class="insight-title">移动端占比</text>
<text class="insight-value">{{ todayInsights.mobileRatio }}%</text>
<text class="insight-desc">持续增长</text>
</view>
</view>
</view>
<!-- 最近生成的报表 -->
<view class="recent-reports">
<view class="section-header">
<text class="section-title">最近报表</text>
<text class="view-all" @click="goToReports('all')">查看全部 ></text>
</view>
<view v-if="recentReports.length > 0" class="report-list">
<view v-for="report in recentReports" :key="report.id" class="report-item" @click="viewReportDetail(report.id)">
<view class="report-icon">
<text class="icon-text">📊</text>
</view>
<view class="report-info">
<text class="report-title">{{ report.title }}</text>
<text class="report-desc">{{ report.description }}</text>
<text class="report-time">{{ formatTime(report.created_at) }}</text>
</view>
<view class="report-status">
<text class="status-text" :class="'status-' + report.status">{{ getReportStatusText(report.status) }}</text>
</view>
</view>
</view>
<view v-else class="no-data">
<text class="no-data-text">暂无最近报表</text>
</view>
</view>
<!-- 数据趋势图表 -->
<view class="trend-chart">
<view class="section-header">
<text class="section-title">数据趋势</text>
<view class="chart-controls">
<text class="control-btn" :class="{ active: trendPeriod === 'week' }" @click="changeTrendPeriod('week')">周</text>
<text class="control-btn" :class="{ active: trendPeriod === 'month' }" @click="changeTrendPeriod('month')">月</text>
<text class="control-btn" :class="{ active: trendPeriod === 'quarter' }" @click="changeTrendPeriod('quarter')">季</text>
</view>
</view>
<view class="chart-container">
<view class="chart-legend">
<view class="legend-item">
<view class="legend-color sales"></view>
<text class="legend-text">销售额</text>
</view>
<view class="legend-item">
<view class="legend-color orders"></view>
<text class="legend-text">订单量</text>
</view>
</view>
<view class="chart-area">
<view class="chart-bars">
<view v-for="(data, index) in trendData" :key="index" class="bar-group">
<view class="bar sales" :style="{ height: (data.sales / maxSales * 100) + '%' }"></view>
<view class="bar orders" :style="{ height: (data.orders / maxOrders * 100) + '%' }"></view>
<text class="bar-label">{{ data.label }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 分析工具 -->
<view class="analysis-tools">
<view class="section-title">分析工具</view>
<view class="tool-grid">
<view class="tool-item" @click="goToTool('dashboard')">
<text class="tool-icon">📊</text>
<text class="tool-label">数据看板</text>
</view>
<view class="tool-item" @click="goToTool('funnel')">
<text class="tool-icon">🔽</text>
<text class="tool-label">转化漏斗</text>
</view>
<view class="tool-item" @click="goToTool('cohort')">
<text class="tool-icon">👥</text>
<text class="tool-label">用户留存</text>
</view>
<view class="tool-item" @click="goToTool('attribution')">
<text class="tool-icon">🎯</text>
<text class="tool-label">归因分析</text>
</view>
<view class="tool-item" @click="goToTool('segmentation')">
<text class="tool-icon">🔍</text>
<text class="tool-label">用户分群</text>
</view>
<view class="tool-item" @click="goToTool('prediction')">
<text class="tool-icon">🔮</text>
<text class="tool-label">预测分析</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="function-menu">
<view class="menu-group">
<view class="menu-item" @click="goToDataSource">
<text class="menu-icon">🗄️</text>
<text class="menu-label">数据源管理</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToAlerts">
<text class="menu-icon">🚨</text>
<text class="menu-label">数据预警</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToExport">
<text class="menu-icon">📤</text>
<text class="menu-label">数据导出</text>
<text class="menu-arrow">></text>
</view>
</view>
<view class="menu-group">
<view class="menu-item" @click="goToHelp">
<text class="menu-icon">❓</text>
<text class="menu-label">帮助中心</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="goToFeedback">
<text class="menu-icon">💬</text>
<text class="menu-label">意见反馈</text>
<text class="menu-arrow">></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 type { UserType } from '@/types/mall-types'
// 报表类型定义
type ReportType = {
id: string
title: string
description: string
status: string
created_at: string
}
// 响应式数据
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({
totalSales: '0',
salesGrowth: 0,
totalUsers: '0',
userGrowth: 0,
totalOrders: '0',
orderGrowth: 0,
conversionRate: 0,
conversionGrowth: 0
})
const reportCounts = ref({
total: 0,
pending: 0,
scheduled: 0,
shared: 0
})
const todayInsights = ref({
hotProduct: '-',
peakTraffic: '0',
conversionAnomaly: '-',
mobileRatio: 0
})
const recentReports = ref([] as Array<ReportType>)
const trendPeriod = ref('week')
const trendData = ref([
{ 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() + 1) // < end
const start = new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000)
const prevEnd = start
const prevStart = new Date(prevEnd.getTime() - 30 * 24 * 60 * 60 * 1000)
const curRes: any = await supa
.from('orders')
.select('total_amount, user_id, created_at')
.gte('created_at', start.toISOString())
.lt('created_at', end.toISOString())
.eq('status', 2)
const prevRes: any = await supa
.from('orders')
.select('total_amount, user_id, created_at')
.gte('created_at', prevStart.toISOString())
.lt('created_at', prevEnd.toISOString())
.eq('status', 2)
const curOrders: Array<any> = Array.isArray(curRes.data) ? (curRes.data as Array<any>) : []
const prevOrders: Array<any> = Array.isArray(prevRes.data) ? (prevRes.data as Array<any>) : []
let curSales = 0
let prevSales = 0
const curUsers: Record<string, boolean> = {}
const prevUsers: Record<string, boolean> = {}
for (let i = 0; i < curOrders.length; i++) {
curSales += safeNumber(curOrders[i].total_amount)
const uid = `${curOrders[i].user_id || ''}`
if (uid) curUsers[uid] = true
}
for (let i = 0; i < prevOrders.length; i++) {
prevSales += safeNumber(prevOrders[i].total_amount)
const uid = `${prevOrders[i].user_id || ''}`
if (uid) prevUsers[uid] = true
}
const curOrderCnt = curOrders.length
const prevOrderCnt = prevOrders.length
const curUserCnt = Object.keys(curUsers).length
const prevUserCnt = Object.keys(prevUsers).length
// 转化率:下单用户 / 访问用户(用 user_sessions 近30天会话去重近似
const curSessRes: any = await supa
.from('user_sessions')
.select('user_id, created_at')
.gte('created_at', start.toISOString())
.lt('created_at', end.toISOString())
const prevSessRes: any = await supa
.from('user_sessions')
.select('user_id, created_at')
.gte('created_at', prevStart.toISOString())
.lt('created_at', prevEnd.toISOString())
const curSess: Array<any> = Array.isArray(curSessRes.data) ? (curSessRes.data as Array<any>) : []
const prevSess: Array<any> = Array.isArray(prevSessRes.data) ? (prevSessRes.data as Array<any>) : []
const curVisitUsers: Record<string, boolean> = {}
const prevVisitUsers: Record<string, boolean> = {}
for (let i = 0; i < curSess.length; i++) {
const uid = `${curSess[i].user_id || ''}`
if (uid) curVisitUsers[uid] = true
}
for (let i = 0; i < prevSess.length; i++) {
const uid = `${prevSess[i].user_id || ''}`
if (uid) prevVisitUsers[uid] = true
}
const curVisitCnt = Object.keys(curVisitUsers).length
const prevVisitCnt = Object.keys(prevVisitUsers).length
const curConv = curVisitCnt > 0 ? (curUserCnt / curVisitCnt) * 100 : 0
const prevConv = prevVisitCnt > 0 ? (prevUserCnt / prevVisitCnt) * 100 : 0
overviewData.value = {
totalSales: fmtMoney(curSales),
salesGrowth: safeNumber(pctGrowth(curSales, prevSales)),
totalUsers: fmtInt(curUserCnt),
userGrowth: safeNumber(pctGrowth(curUserCnt, prevUserCnt)),
totalOrders: fmtInt(curOrderCnt),
orderGrowth: safeNumber(pctGrowth(curOrderCnt, prevOrderCnt)),
conversionRate: safeNumber(curConv),
conversionGrowth: safeNumber(pctGrowth(curConv, prevConv))
}
} 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))
p.set('p_merchant_id', null)
const res: any = await supa.rpc('rpc_analytics_trend_data', 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())))
p.set('p_merchant_id', null)
const res: any = await supa.rpc('rpc_analytics_trend_data', 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))
p.set('p_merchant_id', null)
const res: any = await supa.rpc('rpc_analytics_trend_data', 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
}))
}
} catch (e) {
console.error('loadTrend failed', e)
}
}
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)
p.set('p_merchant_id', null)
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}`
}
// 访问量峰值(简化:今日总访问量)
const pvRes: any = await supa
.from('page_views')
.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)
// 转化异常:取今日 KPI 增长简化负数提示“下降xx%”)
const kpiP = new UTSJSONObject()
kpiP.set('p_start', today0.toISOString())
kpiP.set('p_end', now.toISOString())
const ySame = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const y0 = new Date(ySame.getFullYear(), ySame.getMonth(), ySame.getDate())
kpiP.set('p_compare_start', y0.toISOString())
kpiP.set('p_compare_end', ySame.toISOString())
kpiP.set('p_merchant_id', null)
const kpiRes: any = await supa.rpc('rpc_analytics_realtime_kpis', kpiP)
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 formatTime(dateStr: string): string {
const date = new Date(dateStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const hours = Math.floor(diff / (1000 * 60 * 60))
if (hours < 1) {
return '刚刚'
} else if (hours < 24) {
return `${hours}小时前`
} else {
return `${Math.floor(hours / 24)}天前`
}
}
function changeTrendPeriod(period: string) {
trendPeriod.value = period
void loadTrend()
}
function viewReportDetail(reportId: string) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${reportId}`
})
}
// 导航方法
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 goToTool(tool: string) {
uni.navigateTo({
url: `/pages/mall/analytics/tools/${tool}`
})
}
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: #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;
}
.analytics-profile {
padding: 0 0 120rpx 0;
background-color: #f5f5f5;
min-height: 100vh;
}
.profile-header {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%);
position: relative;
}
.analyst-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
margin-right: 30rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.analyst-info {
flex: 1;
}
.analyst-name {
font-size: 36rpx;
font-weight: bold;
color: white;
margin-bottom: 10rpx;
}
.analyst-role {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 15rpx;
}
.analyst-stats {
display: flex;
gap: 30rpx;
}
.stat-item {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
}
.settings-icon {
font-size: 36rpx;
color: white;
padding: 10rpx;
}
.data-overview, .report-management, .today-insights, .recent-reports, .trend-chart, .analysis-tools, .function-menu {
margin: 20rpx 30rpx;
background: white;
border-radius: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.view-all {
font-size: 24rpx;
color: #fd79a8;
}
.overview-cards {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20rpx;
}
.overview-card {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 150rpx;
padding: 25rpx 15rpx;
background: linear-gradient(135deg, #ffe8f0 0%, #fdd8e8 100%);
border-radius: 20rpx;
}
.card-icon {
font-size: 48rpx;
margin-bottom: 10rpx;
}
.card-title {
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
}
.card-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.card-change {
font-size: 20rpx;
color: #ff6b6b;
}
.card-change.positive {
color: #00b894;
}
.report-tabs {
display: flex;
justify-content: space-between;
}
.report-tab {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.tab-icon {
font-size: 48rpx;
margin-bottom: 10rpx;
}
.tab-text {
font-size: 24rpx;
color: #666;
}
.tab-badge {
position: absolute;
top: -10rpx;
right: 20rpx;
background: #ff6b6b;
color: white;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 10rpx;
min-width: 30rpx;
text-align: center;
}
.tab-badge.alert {
background: #ff4757;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.insight-grid {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20rpx;
}
.insight-card {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 150rpx;
padding: 25rpx 15rpx;
background: #ffe8f0;
border-radius: 15rpx;
}
.insight-icon {
font-size: 48rpx;
margin-bottom: 10rpx;
}
.insight-title {
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
}
.insight-value {
font-size: 28rpx;
font-weight: bold;
color: #fd79a8;
margin-bottom: 5rpx;
}
.insight-desc {
font-size: 20rpx;
color: #999;
text-align: center;
}
.report-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.report-item {
display: flex;
align-items: center;
padding: 25rpx;
background: #f8f9ff;
border-radius: 15rpx;
border-left: 6rpx solid #fd79a8;
}
.report-icon {
margin-right: 20rpx;
}
.icon-text {
font-size: 36rpx;
}
.report-info {
flex: 1;
}
.report-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 10rpx;
}
.report-desc {
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
line-height: 1.4;
}
.report-time {
font-size: 20rpx;
color: #999;
}
.report-status {
margin-left: 20rpx;
}
.status-text {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 20rpx;
background: #e3f2fd;
color: #1976d2;
}
.status-1 {
background: #fff3e0;
color: #f57c00;
}
.status-2 {
background: #e8f5e8;
color: #388e3c;
}
.chart-controls {
display: flex;
gap: 10rpx;
}
.control-btn {
padding: 10rpx 20rpx;
font-size: 24rpx;
color: #666;
background: #f0f0f0;
border-radius: 15rpx;
}
.control-btn.active {
background: #fd79a8;
color: white;
}
.chart-container {
padding: 20rpx 0;
}
.chart-legend {
display: flex;
justify-content: center;
gap: 30rpx;
margin-bottom: 30rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 10rpx;
}
.legend-color {
width: 20rpx;
height: 20rpx;
border-radius: 10rpx;
}
.legend-color.sales {
background: #fd79a8;
}
.legend-color.orders {
background: #74b9ff;
}
.legend-text {
font-size: 22rpx;
color: #666;
}
.chart-area {
height: 300rpx;
position: relative;
}
.chart-bars {
display: flex;
justify-content: space-between;
align-items: end;
height: 250rpx;
gap: 10rpx;
}
.bar-group {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.bar {
width: 20rpx;
margin-bottom: 10rpx;
border-radius: 10rpx 10rpx 0 0;
min-height: 10rpx;
}
.bar.sales {
background: #fd79a8;
margin-right: 5rpx;
}
.bar.orders {
background: #74b9ff;
}
.bar-label {
font-size: 20rpx;
color: #666;
margin-top: 10rpx;
}
.tool-grid {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20rpx;
}
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 140rpx;
padding: 25rpx 15rpx;
background: #ffe8f0;
border-radius: 15rpx;
}
.tool-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.tool-label {
font-size: 24rpx;
color: #333;
}
.menu-group {
margin-bottom: 30rpx;
}
.menu-group:last-child {
margin-bottom: 0;
}
.menu-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
font-size: 36rpx;
width: 60rpx;
margin-right: 25rpx;
}
.menu-label {
flex: 1;
font-size: 28rpx;
color: #333;
}
.menu-arrow {
font-size: 24rpx;
color: #ccc;
}
.no-data {
text-align: center;
padding: 60rpx 0;
}
.no-data-text {
font-size: 24rpx;
color: #999;
}
/* 响应式:窄屏时全屏显示 */
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
}
.main-content {
width: 100%;
}
}
</style>