接入数据库

This commit is contained in:
comlibmb
2026-01-26 21:34:17 +08:00
parent c14f67cfc8
commit 3fbd9a2b3d
26 changed files with 3559 additions and 427 deletions

View File

@@ -26,29 +26,71 @@
<!-- 主内容区域 -->
<view class="main-content">
<view class="container">
<!-- KPI宽屏 4列窄屏 2列 -->
<!-- KPI宽屏 4列窄屏 2列(增强版:渐变背景 + sparkline -->
<view class="kpi-grid">
<view class="kpi-card">
<text class="kpi-label">实时 GMV</text>
<view class="kpi-card kpi-card-gmv" @click="goToSalesReport">
<view class="kpi-header">
<text class="kpi-label">实时 GMV</text>
<view class="kpi-sparkline">
<EChartsView v-if="!loading" class="sparkline-chart" :option="kpiSparklineOptions.gmv" />
<view v-else class="sparkline-skeleton"></view>
</view>
</view>
<text class="kpi-value">¥{{ formatMoney(realTime.gmv) }}</text>
<text class="kpi-meta">较昨日同刻:{{ formatPct(realTime.gmv_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">实时订单</text>
<view class="kpi-footer">
<text class="kpi-meta">较昨日同刻</text>
<text class="kpi-chip" :class="realTime.gmv_growth >= 0 ? 'pos' : 'neg'">
{{ formatPct(realTime.gmv_growth) }}
</text>
</view>
</view>
<view class="kpi-card kpi-card-orders" @click="goToSalesReport">
<view class="kpi-header">
<text class="kpi-label">实时订单</text>
<view class="kpi-sparkline">
<EChartsView v-if="!loading" class="sparkline-chart" :option="kpiSparklineOptions.orders" />
<view v-else class="sparkline-skeleton"></view>
</view>
</view>
<text class="kpi-value">{{ formatInt(realTime.orders) }}</text>
<text class="kpi-meta">较昨日同刻:{{ formatPct(realTime.order_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">在线用户</text>
<view class="kpi-footer">
<text class="kpi-meta">较昨日同刻</text>
<text class="kpi-chip" :class="realTime.order_growth >= 0 ? 'pos' : 'neg'">
{{ formatPct(realTime.order_growth) }}
</text>
</view>
</view>
<view class="kpi-card kpi-card-users" @click="goToUserAnalysis">
<view class="kpi-header">
<text class="kpi-label">在线用户</text>
<view class="kpi-sparkline">
<EChartsView v-if="!loading" class="sparkline-chart" :option="kpiSparklineOptions.users" />
<view v-else class="sparkline-skeleton"></view>
</view>
</view>
<text class="kpi-value">{{ formatInt(realTime.online_users) }}</text>
<text class="kpi-meta">最近 5 分钟</text>
</view>
<view class="kpi-card">
<text class="kpi-label">转化率</text>
<view class="kpi-footer">
<text class="kpi-meta">最近 5 分钟</text>
<text class="kpi-chip neutral">实时</text>
</view>
</view>
<view class="kpi-card kpi-card-conversion" @click="goToSalesReport">
<view class="kpi-header">
<text class="kpi-label">转化率</text>
<view class="kpi-sparkline">
<EChartsView v-if="!loading" class="sparkline-chart" :option="kpiSparklineOptions.conversion" />
<view v-else class="sparkline-skeleton"></view>
</view>
</view>
<text class="kpi-value">{{ formatPct(realTime.conversion_rate) }}</text>
<text class="kpi-meta">较昨日同刻:{{ formatPct(realTime.conversion_growth) }}</text>
</view>
</view>
<view class="kpi-footer">
<text class="kpi-meta">较昨日同刻</text>
<text class="kpi-chip" :class="realTime.conversion_growth >= 0 ? 'pos' : 'neg'">
{{ formatPct(realTime.conversion_growth) }}
</text>
</view>
</view>
</view>
<!-- 时间维度:横排 -->
<view class="tabs">
@@ -135,6 +177,46 @@
</view>
</view>
<!-- 快速工具卡片区6个工具入口 -->
<view class="tools-section">
<view class="section-header">
<text class="section-title">快速分析工具</text>
<text class="section-desc">点击进入详细分析</text>
</view>
<view class="tools-grid">
<view class="tool-card" @click="goToSalesReport">
<view class="tool-icon sales">📊</view>
<text class="tool-title">销售报表</text>
<text class="tool-desc">GMV、订单、转化率</text>
</view>
<view class="tool-card" @click="goToUserAnalysis">
<view class="tool-icon users">👥</view>
<text class="tool-title">用户分析</text>
<text class="tool-desc">增长、活跃、留存</text>
</view>
<view class="tool-card" @click="goToProductInsights">
<view class="tool-icon products">📦</view>
<text class="tool-title">商品洞察</text>
<text class="tool-desc">销量、库存、价格</text>
</view>
<view class="tool-card" @click="goToMarketTrends">
<view class="tool-icon market">📈</view>
<text class="tool-title">市场趋势</text>
<text class="tool-desc">整体趋势、行业对比</text>
</view>
<view class="tool-card" @click="goToCouponAnalysis">
<view class="tool-icon coupon">🎫</view>
<text class="tool-title">优惠券分析</text>
<text class="tool-desc">发放、使用、ROI</text>
</view>
<view class="tool-card" @click="goToCustomReport">
<view class="tool-icon custom">⚙️</view>
<text class="tool-title">自定义报表</text>
<text class="tool-desc">创建专属报表</text>
</view>
</view>
</view>
<!-- 留白 -->
<view style="height: 24px;"></view>
</view>
@@ -170,6 +252,10 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
showMoreMenu: false,
showSidebarMenu: false,
currentPath: '/pages/mall/analytics/index',
loading: true,
autoRefreshEnabled: true,
autoRefreshInterval: 60000, // 60秒自动刷新
autoRefreshTimer: null as any,
timePeriods: [
{ value: '7d', label: '7天' },
{ value: '30d', label: '30天' },
@@ -193,35 +279,23 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
orders: [] as Array<number>
} as TrendData,
userSegments: [
{ name: '未消费用户', value: 72 },
{ name: '消费一次用户', value: 14 },
{ name: '留存客户', value: 9 },
{ name: '回流客户', value: 5 }
] as Array<SegmentItem>,
userSegments: [] as Array<SegmentItem>,
trafficSources: [
{ name: '直接访问', value: 45 },
{ name: '搜索引擎', value: 28 },
{ name: '社交媒体', value: 18 },
{ name: '广告推广', value: 9 }
] as Array<TrafficItem>,
trafficSources: [] as Array<TrafficItem>,
topProducts: [
{ id: '1', rank: 1, name: '苹果 iPhone 15', sales: 580 },
{ id: '2', rank: 2, name: '华为 Mate 60', sales: 456 },
{ id: '3', rank: 3, name: '小米 14 Pro', sales: 389 }
],
topProducts: [] as Array<TopProductItem>,
topMerchants: [
{ id: '1', rank: 1, name: '华强北电子城', sales: 580000, growth: 15.6 },
{ id: '2', rank: 2, name: '时尚服装馆', sales: 456000, growth: 12.3 },
{ id: '3', rank: 3, name: '美食天地', sales: 389000, growth: -2.1 }
],
topMerchants: [] as Array<TopMerchantItem>,
// 图表 options
trafficBarOption: {} as any,
userSegmentOption: {} as any
userSegmentOption: {} as any,
kpiSparklineOptions: {
gmv: {} as any,
orders: {} as any,
users: {} as any,
conversion: {} as any
}
}
},
@@ -242,30 +316,92 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
},
onLoad() {
this.refreshAll()
this.buildChartOptions()
this.initDashboard()
},
onUnload() {
this.showMoreMenu = false
this.stopAutoRefresh()
},
onShow() {
// 页面显示时恢复自动刷新
if (this.autoRefreshEnabled) {
this.startAutoRefresh()
}
},
onHide() {
// 页面隐藏时暂停自动刷新
this.stopAutoRefresh()
},
methods: {
async initDashboard() {
this.loading = true
await this.refreshAll()
this.loading = false
this.buildChartOptions()
this.buildSparklineOptions()
if (this.autoRefreshEnabled) {
this.startAutoRefresh()
}
},
async refreshAll() {
this.updateTime()
await this.loadRealTime()
await this.loadTrend()
await this.loadUserSegments()
await this.loadTrafficSources()
await this.loadTopProducts()
await this.loadTopMerchants()
this.updateTime()
uni.showToast({ title: '已刷新', icon: 'success' })
try {
await Promise.all([
this.loadRealTime(),
this.loadTrend(),
this.loadUserSegments(),
this.loadTrafficSources(),
this.loadTopProducts(),
this.loadTopMerchants()
])
this.updateTime()
// 更新 sparkline
this.buildSparklineOptions()
// 更新图表
this.buildChartOptions()
} catch (e) {
console.error('refreshAll failed', e)
uni.showToast({ title: '数据加载失败', icon: 'none' })
}
},
startAutoRefresh() {
this.stopAutoRefresh()
this.autoRefreshTimer = setInterval(() => {
this.refreshAll()
}, this.autoRefreshInterval)
},
stopAutoRefresh() {
if (this.autoRefreshTimer != null) {
clearInterval(this.autoRefreshTimer)
this.autoRefreshTimer = null
}
},
toggleAutoRefresh() {
this.autoRefreshEnabled = !this.autoRefreshEnabled
if (this.autoRefreshEnabled) {
this.startAutoRefresh()
uni.showToast({ title: '已开启自动刷新', icon: 'success' })
} else {
this.stopAutoRefresh()
uni.showToast({ title: '已关闭自动刷新', icon: 'none' })
}
},
selectPeriod(p: string) {
this.selectedPeriod = p
this.loadTrend()
this.loadUserSegments()
this.loadTrafficSources()
this.loadTopProducts()
this.loadTopMerchants()
},
updateTime() {
@@ -291,21 +427,49 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
p.set('p_end_date', endDate.toISOString().slice(0, 10))
p.set('p_merchant_id', null)
console.log('📊 loadTrend: 请求参数', {
start_date: startDate.toISOString().slice(0, 10),
end_date: endDate.toISOString().slice(0, 10)
})
const res: any = await supa.rpc('rpc_analytics_trend_data', p)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
console.log('📊 loadTrend: RPC 返回结果', res)
// 检查返回结构:可能是 res.data 或 res 本身
let rows: Array<any> = []
if (Array.isArray(res.data)) {
rows = res.data as Array<any>
} else if (Array.isArray(res)) {
rows = res as Array<any>
} else if (res && typeof res === 'object') {
// 可能是 { data: [...] } 或其他结构
const data = res.data || res.rows || res.result || []
rows = Array.isArray(data) ? data : []
}
console.log('📊 loadTrend: 解析后的 rows', rows, '数量:', rows.length)
const x: Array<string> = []
const gmv: Array<number> = []
const orders: Array<number> = []
for (let i = 0; i < rows.length; i++) {
const d = `${rows[i].date}` // yyyy-mm-dd
x.push(d.slice(5))
gmv.push(Number(rows[i].gmv) || 0)
orders.push(Number(rows[i].orders) || 0)
const row = rows[i]
const d = `${row.date || row.day || row.date_key}` // 兼容不同字段名
if (d && d.length >= 10) {
x.push(d.slice(5)) // MM-DD
} else {
x.push(`${i + 1}`)
}
gmv.push(Number(row.gmv || row.total_amount || 0) || 0)
orders.push(Number(row.orders || row.order_count || 0) || 0)
}
console.log('📊 loadTrend: 最终数据', { x: x.length, gmv: gmv.length, orders: orders.length })
this.trend = { x, gmv, orders }
} catch (e) {
console.error('loadTrend failed', e)
console.error('loadTrend failed', e)
// 即使失败也设置空数据,避免图表报错
this.trend = { x: [], gmv: [], orders: [] }
}
},
@@ -326,24 +490,46 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
p.set('p_compare_end', ySame.toISOString())
p.set('p_merchant_id', null)
console.log('⚡ loadRealTime: 请求参数', {
p_start: todayISO,
p_end: now.toISOString(),
p_compare_start: y0.toISOString(),
p_compare_end: ySame.toISOString()
})
const res: any = await supa.rpc('rpc_analytics_realtime_kpis', p)
const row = Array.isArray(res.data) && res.data.length > 0 ? res.data[0] : (res.data || {})
console.log('⚡ loadRealTime: RPC 返回结果', res)
// 检查返回结构
let row: any = {}
if (Array.isArray(res.data) && res.data.length > 0) {
row = res.data[0]
} else if (Array.isArray(res) && res.length > 0) {
row = res[0]
} else if (res && typeof res === 'object' && !Array.isArray(res)) {
// 可能是直接返回对象,或者 { data: {...} }
row = res.data || res.result || res
}
console.log('⚡ loadRealTime: 解析后的 row', row)
const safe = (v: any): number => {
const n = Number(v)
return isFinite(n) ? n : 0
}
this.realTime = {
gmv: Math.round(safe(row.gmv)),
gmv_growth: safe(row.gmv_growth),
orders: Math.round(safe(row.orders)),
order_growth: safe(row.order_growth),
online_users: Math.round(safe(row.online_users)),
conversion_rate: safe(row.conversion_rate),
conversion_growth: safe(row.conversion_growth)
gmv: Math.round(safe(row.gmv || row.total_gmv || row.revenue)),
gmv_growth: safe(row.gmv_growth || row.gmv_growth_rate || row.revenue_growth),
orders: Math.round(safe(row.orders || row.order_count || row.total_orders)),
order_growth: safe(row.order_growth || row.order_growth_rate),
online_users: Math.round(safe(row.online_users || row.active_users || row.current_users)),
conversion_rate: safe(row.conversion_rate || row.conversion),
conversion_growth: safe(row.conversion_growth || row.conversion_growth_rate)
}
console.log('⚡ loadRealTime: 最终数据', this.realTime)
} catch (e) {
console.error(e)
console.error('❌ loadRealTime failed', e)
}
},
@@ -403,15 +589,42 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
const p = new UTSJSONObject()
p.set('p_start_date', startDate.toISOString().slice(0, 10))
p.set('p_end_date', endDate.toISOString().slice(0, 10))
console.log('👥 loadUserSegments: 请求参数', {
start_date: startDate.toISOString().slice(0, 10),
end_date: endDate.toISOString().slice(0, 10)
})
const res: any = await supa.rpc('rpc_analytics_user_segments', p)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
console.log('👥 loadUserSegments: RPC 返回结果', res)
// 检查返回结构
let rows: Array<any> = []
if (Array.isArray(res.data)) {
rows = res.data as Array<any>
} else if (Array.isArray(res)) {
rows = res as Array<any>
} else if (res && typeof res === 'object') {
const data = res.data || res.rows || res.result || []
rows = Array.isArray(data) ? data : []
}
console.log('👥 loadUserSegments: 解析后的 rows', rows, '数量:', rows.length)
const list: Array<SegmentItem> = []
for (let i = 0; i < rows.length; i++) {
list.push({ name: `${rows[i].name}`, value: Number(rows[i].value) || 0 })
const row = rows[i]
const name = `${row.name || row.segment_name || row.label || '未知'}`
const value = Number(row.value || row.count || row.amount || 0) || 0
list.push({ name, value })
}
if (list.length > 0) this.userSegments = list
console.log('👥 loadUserSegments: 最终数据', list)
// 即使为空也更新,确保图表能正确显示空状态
this.userSegments = list
} catch (e) {
console.error('loadUserSegments failed', e)
console.error('loadUserSegments failed', e)
this.userSegments = []
}
},
@@ -421,15 +634,42 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
const p = new UTSJSONObject()
p.set('p_start_date', startDate.toISOString().slice(0, 10))
p.set('p_end_date', endDate.toISOString().slice(0, 10))
console.log('🌐 loadTrafficSources: 请求参数', {
start_date: startDate.toISOString().slice(0, 10),
end_date: endDate.toISOString().slice(0, 10)
})
const res: any = await supa.rpc('rpc_analytics_traffic_sources', p)
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
console.log('🌐 loadTrafficSources: RPC 返回结果', res)
// 检查返回结构
let rows: Array<any> = []
if (Array.isArray(res.data)) {
rows = res.data as Array<any>
} else if (Array.isArray(res)) {
rows = res as Array<any>
} else if (res && typeof res === 'object') {
const data = res.data || res.rows || res.result || []
rows = Array.isArray(data) ? data : []
}
console.log('🌐 loadTrafficSources: 解析后的 rows', rows, '数量:', rows.length)
const list: Array<TrafficItem> = []
for (let i = 0; i < rows.length; i++) {
list.push({ name: `${rows[i].name}`, value: Number(rows[i].value) || 0 })
const row = rows[i]
const name = `${row.name || row.source_name || row.label || '未知'}`
const value = Number(row.value || row.count || row.amount || 0) || 0
list.push({ name, value })
}
if (list.length > 0) this.trafficSources = list
console.log('🌐 loadTrafficSources: 最终数据', list)
// 即使为空也更新
this.trafficSources = list
} catch (e) {
console.error('loadTrafficSources failed', e)
console.error('loadTrafficSources failed', e)
this.trafficSources = []
}
},
@@ -526,21 +766,59 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
// 构建图表 options
buildChartOptions() {
// 流量来源条形图
// 流量来源条形图(增强:渐变 + 动画)
const trafficX = this.trafficSources.map((it) => it.name)
const trafficY = this.trafficSources.map((it) => {
const n = Number(it.value)
return isFinite(n) ? n : 0
})
const total = trafficY.reduce((sum, v) => sum + v, 0)
this.trafficBarOption = {
grid: { left: 80, right: 24, top: 18, bottom: 18 },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
xAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
yAxis: { type: 'category', data: trafficX, axisTick: { show: false }, axisLabel: { color: 'rgba(0,0,0,0.65)' } },
series: [{ type: 'bar', data: trafficY, barWidth: 14, itemStyle: { borderRadius: 6 } }]
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const p = params[0]
const percent = total > 0 ? ((p.value / total) * 100).toFixed(1) : '0'
return `${p.name}<br/>${p.marker} ${p.value} 次 (${percent}%)`
}
},
xAxis: {
type: 'value',
axisLabel: { color: 'rgba(0,0,0,0.55)' },
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } }
},
yAxis: {
type: 'category',
data: trafficX,
axisTick: { show: false },
axisLabel: { color: 'rgba(0,0,0,0.65)' }
},
series: [{
type: 'bar',
data: trafficY,
barWidth: 14,
itemStyle: {
borderRadius: 6,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{ offset: 0, color: '#FF4D4F' },
{ offset: 1, color: '#FF7A45' }
]
}
},
animationDuration: 800,
animationEasing: 'cubicOut'
}]
}
// 用户结构环形图
// 用户结构环形图(增强:颜色 + 动画)
const segmentData = this.userSegments.map((it) => ({
name: it.name,
value: (() => {
@@ -548,9 +826,20 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
return isFinite(n) ? n : 0
})()
}))
const colors = ['#FF6B6B', '#4ECDC4', '#A8E6CF', '#FFD93D', '#95A5A6']
this.userSegmentOption = {
tooltip: { trigger: 'item' },
legend: { left: 0, bottom: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 12 } },
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
left: 0,
bottom: 0,
itemWidth: 10,
itemHeight: 10,
textStyle: { fontSize: 12 }
},
color: colors,
series: [
{
type: 'pie',
@@ -559,10 +848,112 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
avoidLabelOverlap: true,
label: { show: true, formatter: '{b}\n{d}%' },
labelLine: { length: 10, length2: 10 },
data: segmentData
data: segmentData,
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: (idx: number) => idx * 100
}
]
}
},
// 构建 KPI sparklinemini 趋势图)
buildSparklineOptions() {
// 从 trend 数据提取最近 7 天的数据用于 sparkline
const recentDays = Math.min(7, this.trend.gmv.length)
const sparkX = this.trend.x.slice(-recentDays)
const sparkGmv = this.trend.gmv.slice(-recentDays)
const sparkOrders = this.trend.orders.slice(-recentDays)
// GMV sparkline
this.kpiSparklineOptions.gmv = {
grid: { left: 0, right: 0, top: 0, bottom: 0 },
xAxis: { type: 'category', data: sparkX, show: false },
yAxis: { type: 'value', show: false },
series: [{
type: 'line',
data: sparkGmv,
smooth: true,
symbol: 'none',
lineStyle: { width: 2, color: '#fff' },
areaStyle: { color: 'rgba(255,255,255,0.2)' }
}]
}
// Orders sparkline
this.kpiSparklineOptions.orders = {
grid: { left: 0, right: 0, top: 0, bottom: 0 },
xAxis: { type: 'category', data: sparkX, show: false },
yAxis: { type: 'value', show: false },
series: [{
type: 'line',
data: sparkOrders,
smooth: true,
symbol: 'none',
lineStyle: { width: 2, color: '#fff' },
areaStyle: { color: 'rgba(255,255,255,0.2)' }
}]
}
// Users sparkline模拟数据实际应该从 user_sessions 获取)
const sparkUsers = sparkOrders.map((v) => Math.round(v * 0.8))
this.kpiSparklineOptions.users = {
grid: { left: 0, right: 0, top: 0, bottom: 0 },
xAxis: { type: 'category', data: sparkX, show: false },
yAxis: { type: 'value', show: false },
series: [{
type: 'line',
data: sparkUsers,
smooth: true,
symbol: 'none',
lineStyle: { width: 2, color: '#fff' },
areaStyle: { color: 'rgba(255,255,255,0.2)' }
}]
}
// Conversion sparkline基于 GMV/Orders 计算)
const sparkConversion = sparkGmv.map((gmv, i) => {
const orders = sparkOrders[i] || 1
return (gmv / orders / 100).toFixed(2)
})
this.kpiSparklineOptions.conversion = {
grid: { left: 0, right: 0, top: 0, bottom: 0 },
xAxis: { type: 'category', data: sparkX, show: false },
yAxis: { type: 'value', show: false },
series: [{
type: 'line',
data: sparkConversion,
smooth: true,
symbol: 'none',
lineStyle: { width: 2, color: '#fff' },
areaStyle: { color: 'rgba(255,255,255,0.2)' }
}]
}
},
// 快速工具跳转
goToSalesReport() {
uni.navigateTo({ url: '/pages/mall/analytics/sales-report' })
},
goToUserAnalysis() {
uni.navigateTo({ url: '/pages/mall/analytics/user-analysis' })
},
goToProductInsights() {
uni.navigateTo({ url: '/pages/mall/analytics/product-insights' })
},
goToMarketTrends() {
uni.navigateTo({ url: '/pages/mall/analytics/market-trends' })
},
goToCouponAnalysis() {
uni.navigateTo({ url: '/pages/mall/analytics/coupon-analysis' })
},
goToCustomReport() {
uni.navigateTo({ url: '/pages/mall/analytics/custom-report' })
}
}
}
@@ -857,30 +1248,125 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
.kpi-card {
background: #fff;
border-radius: 14px;
border-radius: 16px;
border: 1px solid rgba(0,0,0,0.06);
padding: 14px;
padding: 18px;
box-sizing: border-box;
flex: 1 1 calc(50% - 6px);
min-width: 260px; /* 窄屏自动掉到一列 */
min-width: 260px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.kpi-card:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
/* KPI 卡片渐变背景 */
.kpi-card-gmv {
background: linear-gradient(135deg, #FF6B6B 0%, #FF4D4F 100%);
color: #fff;
}
.kpi-card-orders {
background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);
color: #fff;
}
.kpi-card-users {
background: linear-gradient(135deg, #A8E6CF 0%, #7FCDBB 100%);
color: #fff;
}
.kpi-card-conversion {
background: linear-gradient(135deg, #FFD93D 0%, #FFA07A 100%);
color: #fff;
}
.kpi-header {
display: flex;
flex-direction: row !important;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.kpi-label {
font-size: 12px;
color: rgba(0,0,0,0.55);
font-size: 13px;
color: rgba(255,255,255,0.9);
font-weight: 500;
}
.kpi-sparkline {
width: 80px;
height: 32px;
opacity: 0.8;
}
.sparkline-chart {
width: 100%;
height: 100%;
}
.sparkline-skeleton {
width: 100%;
height: 100%;
background: rgba(255,255,255,0.15);
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 0.3; }
}
.kpi-value {
margin-top: 8px;
font-size: 22px;
font-size: 28px;
font-weight: 800;
color: #111;
color: #fff;
margin-bottom: 12px;
line-height: 1.2;
}
.kpi-footer {
display: flex;
flex-direction: row !important;
justify-content: space-between;
align-items: center;
}
.kpi-meta {
margin-top: 8px;
font-size: 12px;
color: rgba(0,0,0,0.55);
color: rgba(255,255,255,0.75);
}
.kpi-chip {
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: rgba(255,255,255,0.2);
backdrop-filter: blur(4px);
}
.kpi-chip.pos {
background: rgba(34,197,94,0.25);
color: #fff;
}
.kpi-chip.neg {
background: rgba(239,68,68,0.25);
color: #fff;
}
.kpi-chip.neutral {
background: rgba(255,255,255,0.2);
color: #fff;
}
/* 时间维度 tabs 横排 */
@@ -1069,6 +1555,11 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
.more-btn {
display: none !important;
}
/* 宽屏时工具卡片 3列 */
.tools-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* 自适应:窄屏自动变一列(断点用 px */
@@ -1127,6 +1618,115 @@ type TopMerchantItem = { id: string; rank: number; name: string; sales: number;
.dropdown-text {
font-size: 12px;
}
/* 窄屏时 KPI 卡片单列 */
.kpi-card {
flex: 1 1 100%;
}
/* 窄屏时工具卡片 2列 */
.tools-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 快速工具卡片区 */
.tools-section {
margin-top: 24px;
}
.section-header {
display: flex;
flex-direction: row !important;
justify-content: space-between;
align-items: baseline;
margin-bottom: 16px;
}
.section-title {
font-size: 16px;
font-weight: 700;
color: #111;
}
.section-desc {
font-size: 12px;
color: rgba(0,0,0,0.55);
}
.tools-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.tool-card {
background: #fff;
border-radius: 14px;
border: 1px solid rgba(0,0,0,0.06);
padding: 20px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.tool-card:active {
transform: scale(0.96);
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
.tool-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 12px;
background: #f3f4f6;
}
.tool-icon.sales {
background: linear-gradient(135deg, #FF6B6B 0%, #FF4D4F 100%);
}
.tool-icon.users {
background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);
}
.tool-icon.products {
background: linear-gradient(135deg, #A8E6CF 0%, #7FCDBB 100%);
}
.tool-icon.market {
background: linear-gradient(135deg, #FFD93D 0%, #FFA07A 100%);
}
.tool-icon.coupon {
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%);
}
.tool-icon.custom {
background: linear-gradient(135deg, #A8C0FF 0%, #3B82F6 100%);
}
.tool-title {
font-size: 14px;
font-weight: 600;
color: #111;
margin-bottom: 6px;
}
.tool-desc {
font-size: 12px;
color: rgba(0,0,0,0.55);
line-height: 1.4;
}
</style>