sql数据流,amdin业务逻辑接入

This commit is contained in:
comlibmb
2026-02-05 10:11:09 +08:00
parent 859372ca5b
commit ac670cf5d8
81 changed files with 3547 additions and 1472 deletions

View File

@@ -11,11 +11,13 @@
</view>
<view class="filter-item">
<text class="filter-label">选择时间</text>
<view class="date-picker-box">
<text class="date-icon">📅</text>
<text class="date-text">2026/01/04 - 2026/02/02</text>
</view>
<text class="filter-label">选择月份</text>
<AnalyticsDateRangePicker
:initialStartDate="startDate"
:initialEndDate="endDate"
@apply="onDateRangeApply"
@clear="onDateRangeClear"
/>
</view>
<view class="filter-btns">
@@ -69,45 +71,203 @@
<AnalyticsUserMapTable />
</view>
<view class="gender-col">
<AnalyticsUserGenderSection />
<AnalyticsUserGenderSection :startDate="startDate" :endDate="endDate" />
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AnalyticsMultiLineChart from '@/components/analytics/AnalyticsMultiLineChart.uvue'
import AnalyticsUserMapTable from '@/components/analytics/AnalyticsUserMapTable.uvue'
import AnalyticsUserGenderSection from '@/components/analytics/AnalyticsUserGenderSection.uvue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import { ensureAnalyticsLogin } from '@/services/analytics/authGuard.uts'
import { getCurrentUser } from '@/utils/store.uts'
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const kpiData = [
{ title: '累计用户', value: '80834', percent: '0.84%', trend: 'up', icon: '👤', bg: '#f3e8ff' },
{ title: '访客数', value: '1138', percent: '1.04%', trend: 'down', icon: '👤', bg: '#e0f2fe' },
{ title: '浏览量', value: '9519', percent: '2.34%', trend: 'down', icon: '👁️', bg: '#dcfce7' },
{ title: '新增用户数', value: '680', percent: '4.36%', trend: 'down', icon: '👤', bg: '#ffedd5' },
{ title: '成交用户数', value: '132', percent: '11.86%', trend: 'up', icon: '👤', bg: '#f3e8ff' },
{ title: '付费会员数', value: '79', percent: '7.05%', trend: 'down', icon: '💎', bg: '#f3e8ff' }
]
type KpiCard = {
title: string
value: string
percent: string
trend: string
icon: string
bg: string
}
const chartData = {
x: ['01-04', '01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13', '01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23', '01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02'],
type LineSeries = {
name: string
color: string
data: Array<number>
}
const selectedMonth = ref<string>('')
const startDate = ref<string>('')
const endDate = ref<string>('')
const dateRangeText = computed((): string => {
if (!startDate.value || !endDate.value) return '请选择月份'
const fmt = (s: string): string => s.replace(/-/g, '/')
return `${fmt(startDate.value)} - ${fmt(endDate.value)}`
})
const kpiData = ref<Array<KpiCard>>([
{ title: '累计用户', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#f3e8ff' },
{ title: '活跃用户', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#e0f2fe' },
{ title: '新增用户数', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#ffedd5' },
{ title: '下单用户数', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#dcfce7' },
{ title: '支付用户数', value: '-', percent: '-', trend: 'up', icon: '👤', bg: '#f3e8ff' },
{ title: '复购率', value: '-', percent: '-', trend: 'up', icon: '💎', bg: '#f3e8ff' }
])
const chartData = ref<{ x: Array<string>; series: Array<LineSeries> }>({
x: [],
series: [
{ name: '新增用户数', color: '#1890ff', data: [40, 30, 25, 30, 22, 10, 20, 32, 28, 15, 8, 12, 18, 22, 15, 12, 25, 30, 28, 25, 35, 20, 18, 22, 20, 15, 10, 8, 15, 38] },
{ name: '访客数', color: '#52c41a', data: [70, 75, 65, 55, 65, 50, 45, 35, 50, 68, 72, 65, 50, 48, 55, 65, 75, 62, 58, 85, 70, 55, 48, 58, 65, 72, 68, 60, 45, 50] },
{ name: '浏览量', color: '#fa8c16', data: [520, 500, 420, 280, 580, 180, 220, 100, 180, 450, 500, 400, 320, 340, 150, 280, 450, 320, 440, 460, 320, 260, 320, 280, 380, 400, 320, 330, 250, 300] },
{ name: '成交用户数', color: '#722ed1', data: [15, 12, 10, 8, 18, 5, 8, 4, 6, 12, 15, 10, 8, 9, 4, 10, 12, 8, 10, 12, 8, 6, 10, 8, 12, 14, 10, 8, 5, 8] },
{ name: '新增付费用户数', color: '#f5222d', data: [5, 4, 3, 2, 6, 1, 2, 1, 2, 4, 5, 3, 2, 3, 1, 3, 4, 2, 3, 4, 2, 2, 3, 2, 4, 5, 3, 2, 1, 3] }
{ name: '新增用户数', color: '#1890ff', data: [] },
{ name: '活跃用户', color: '#52c41a', data: [] }
]
})
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
function formatInt(v: any): string {
const n = Math.round(safeNumber(v))
return n.toString()
}
function formatPct(v: any): string {
const n = safeNumber(v)
const s = n.toFixed(2) + '%'
return s
}
function trendOf(pct: number): string {
return pct >= 0 ? 'up' : 'down'
}
async function loadData() {
try {
await ensureSupabaseReady()
if (!startDate.value || !endDate.value) {
uni.showToast({ title: '请先选择月份', icon: 'none' })
return
}
const startStr = startDate.value
const endStr = endDate.value
const p = new UTSJSONObject()
p.set('p_start_date', startStr)
p.set('p_end_date', endStr)
const kRes: any = await supa.rpc('rpc_analytics_user_kpis', p)
const row = Array.isArray(kRes.data) && kRes.data.length > 0 ? kRes.data[0] : (kRes.data || {})
const totalUsers = safeNumber(row.total_users)
const totalGrowth = safeNumber(row.user_growth)
const newUsers = safeNumber(row.new_users)
const newGrowth = safeNumber(row.new_user_growth)
const activeUsers = safeNumber(row.active_users)
const activeGrowth = safeNumber(row.active_growth)
const orderingUsers = safeNumber(row.ordering_users)
const orderingGrowth = safeNumber(row.ordering_growth)
const paidUsers = safeNumber(row.paid_users)
const paidGrowth = safeNumber(row.paid_growth)
const repurchaseRate = safeNumber(row.repurchase_rate)
const repurchaseGrowth = safeNumber(row.repurchase_growth)
kpiData.value = [
{ title: '累计用户', value: formatInt(totalUsers), percent: formatPct(totalGrowth), trend: trendOf(totalGrowth), icon: '👤', bg: '#f3e8ff' },
{ title: '活跃用户', value: formatInt(activeUsers), percent: formatPct(activeGrowth), trend: trendOf(activeGrowth), icon: '👤', bg: '#e0f2fe' },
{ title: '新增用户数', value: formatInt(newUsers), percent: formatPct(newGrowth), trend: trendOf(newGrowth), icon: '👤', bg: '#ffedd5' },
{ title: '下单用户数', value: formatInt(orderingUsers), percent: formatPct(orderingGrowth), trend: trendOf(orderingGrowth), icon: '👤', bg: '#dcfce7' },
{ title: '支付用户数', value: formatInt(paidUsers), percent: formatPct(paidGrowth), trend: trendOf(paidGrowth), icon: '👤', bg: '#f3e8ff' },
{ title: '复购率', value: formatPct(repurchaseRate), percent: formatPct(repurchaseGrowth), trend: trendOf(repurchaseGrowth), icon: '💎', bg: '#f3e8ff' }
]
const tRes: any = await supa.rpc('rpc_analytics_user_growth_trend', p)
const rows: Array<any> = Array.isArray(tRes.data) ? (tRes.data as Array<any>) : []
const x: Array<string> = []
const newArr: Array<number> = []
const activeArr: Array<number> = []
for (let i = 0; i < rows.length; i++) {
const d = `${rows[i].date}`
x.push(d.slice(5))
newArr.push(Number(rows[i].new_users) || 0)
activeArr.push(Number(rows[i].active_users) || 0)
}
chartData.value = {
x,
series: [
{ name: '新增用户数', color: '#1890ff', data: newArr },
{ name: '活跃用户', color: '#52c41a', data: activeArr }
]
}
} catch (e) {
console.error('admin user statistic loadData failed', e)
uni.showToast({ title: '数据加载失败', icon: 'none', duration: 2000 })
}
}
function onSearch() {
uni.showToast({ title: '搜索中...' })
loadData()
}
function onExport() {
uni.showToast({ title: '导出中...' })
uni.showToast({ title: '导出功能待接入', icon: 'none' })
}
function onDateRangeApply(range: { start: string; end: string }) {
startDate.value = range.start
endDate.value = range.end
loadData()
}
function onDateRangeClear() {
startDate.value = ''
endDate.value = ''
kpiData.value.forEach(item => {
item.value = '-'
item.percent = '-'
})
chartData.value.x = []
chartData.value.series.forEach(s => s.data = [])
}
// 初始化默认月份为当前月
function initCurrentMonth() {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const firstDay = new Date(year, month - 1, 1)
const lastDay = new Date(year, month, 0)
startDate.value = firstDay.toISOString().slice(0, 10)
endDate.value = lastDay.toISOString().slice(0, 10)
}
onLoad(async () => {
if (!ensureAnalyticsLogin({ toastTitle: '请先登录以访问用户统计' })) return
const profile = await getCurrentUser()
const role = profile?.role
if (!role || !['admin', 'analytics'].includes(role)) {
uni.showToast({ title: '权限不足', icon: 'none' })
setTimeout(() => uni.switchTab({ url: '/pages/mall/consumer/index' }), 800)
return
}
initCurrentMonth()
loadData()
})
</script>
<style scoped>
@@ -222,51 +382,88 @@ function onExport() {
}
.kpi-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 16px;
margin-bottom: 40px;
}
@media (max-width: 1200px) {
.kpi-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.kpi-row {
grid-template-columns: 1fr;
}
}
.kpi-card {
flex: 1;
min-width: 200px;
min-width: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding: 8px;
gap: 10px;
padding: 6px;
}
.kpi-icon-box {
width: 44px;
height: 44px;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.kpi-icon { font-size: 20px; }
.kpi-icon { font-size: 14px; }
.kpi-content {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.kpi-label { font-size: 14px; color: #8c8c8c; }
.kpi-value { font-size: 24px; font-weight: 500; color: #262626; }
.kpi-label {
font-size: 12px;
color: #8c8c8c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.kpi-value {
font-size: 20px;
font-weight: 500;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.kpi-meta {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
font-size: 12px;
min-width: 0;
}
.meta-label { color: #8c8c8c; }
.meta-label {
color: #8c8c8c;
flex: 0 0 auto;
}
.meta-value {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta-value.up { color: #ff4d4f; }
.meta-value.down { color: #52c41a; }