consumer模块完成90%,前端完成supabase对接

This commit is contained in:
2026-02-04 17:21:15 +08:00
parent 8a535e3f38
commit 39aa1b6bec
1335 changed files with 191376 additions and 4 deletions

View File

@@ -0,0 +1,21 @@
import supa from '@/components/supadb/aksupainstance.uts'
export function getUserIdOrNull(): string | null {
try {
const sessionInfo = supa.getSession()
if (sessionInfo == null || sessionInfo.user == null) return null
const userObj = sessionInfo.user as any
if (userObj.getString != null) {
return (userObj.getString('id') as string | null)
}
if (userObj.id != null) {
return String(userObj.id)
}
if (typeof userObj === 'object' && 'id' in userObj) {
return String((userObj as any).id)
}
return null
} catch (e) {
return null
}
}

View File

@@ -0,0 +1,20 @@
import { getUserIdOrNull } from './auth.uts'
import { toLoginWithRedirect, getCurrentPageUrlWithQuery } from '@/utils/authRedirect.uts'
export type EnsureLoginOptions = {
redirectUrl?: string
toastTitle?: string
}
export function ensureAnalyticsLogin(opts?: EnsureLoginOptions): boolean {
const uid = getUserIdOrNull()
if (uid != null && uid !== '') return true
const target = (opts?.redirectUrl != null && opts?.redirectUrl?.length > 0)
? opts?.redirectUrl as string
: getCurrentPageUrlWithQuery()
uni.showToast({ title: opts?.toastTitle ?? '请先登录', icon: 'none' })
toLoginWithRedirect(target)
return false
}

View File

@@ -0,0 +1,42 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
export type CouponAnalysisData = {
overviewRow: UTSJSONObject | null
typeList: Array<UTSJSONObject>
channelList: Array<UTSJSONObject>
trendList: Array<UTSJSONObject>
conversionList: Array<UTSJSONObject>
}
export async function fetchCouponAnalysis(period: string, range?: { start: string; end: string } | null): Promise<CouponAnalysisData> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const p_start_date = toDateOnly(startIso)
const p_end_date = toDateOnly(endIso)
const params = {
p_start_date,
p_end_date
} as any
const [overviewRow, typeList, channelList, trendList, conversionList] = await Promise.all([
rpcOrNull('rpc_analytics_coupon_overview', params),
rpcOrEmptyArray('rpc_analytics_coupon_by_type', params),
rpcOrEmptyArray('rpc_analytics_coupon_by_channel', params),
rpcOrEmptyArray('rpc_analytics_coupon_trend', params),
rpcOrEmptyArray('rpc_analytics_coupon_conversion', params)
])
return { overviewRow, typeList, channelList, trendList, conversionList }
}

View File

@@ -0,0 +1,91 @@
import { rpcOrEmptyArray, rpcOrValue } from './rpc.uts'
export type CustomReportListItem = {
id: string
title: string
description: string
period: string
updated_at: string
}
export type CreateCustomReportParams = {
title: string
description: string
period: string
metrics: Array<string>
chartType: string
}
export type UpdateCustomReportParams = {
reportId: string
title: string
description: string | null
period: string | null
}
function safeString(v: any): string {
return v != null ? `${v}` : ''
}
// 改造:不再直查 analytics_reports 表,统一通过 RPC 获取当前用户的报表列表
export async function listCustomReports(ownerUserId: string): Promise<Array<CustomReportListItem>> {
// rpc_get_custom_reports 基于 auth.uid() 过滤,无需额外参数,这里保留签名用于兼容调用方
const rows = await rpcOrEmptyArray('rpc_get_custom_reports', {} as any)
const list: Array<CustomReportListItem> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
list.push({
id: safeString(r.getAny?.('id') ?? r.getString?.('id')),
title: safeString(r.getAny?.('title') ?? r.getString?.('title')),
description: safeString(r.getAny?.('description') ?? r.getString?.('description')),
period: safeString(r.getAny?.('period') ?? r.getString?.('period')),
updated_at: safeString(r.getAny?.('updated_at') ?? r.getString?.('updated_at'))
})
}
return list
}
// 改造RPC 参数改为 p_definitionJSONB承载 period/metrics/chartType
export async function createCustomReport(params: CreateCustomReportParams): Promise<string> {
const definition = {
period: params.period,
metrics: params.metrics,
chartType: params.chartType || 'line'
}
const data = await rpcOrValue('rpc_create_custom_report', {
p_title: params.title,
p_description: params.description || '',
p_definition: definition
} as any)
if (data == null) {
throw new Error('保存失败未返回报表ID')
}
return `${data}`
}
export async function updateCustomReport(params: UpdateCustomReportParams): Promise<boolean> {
// 注意:旧 UI 只传 title/description/period这里把 period 合并进 definition
const definition = {
period: params.period
}
await rpcOrValue('rpc_update_custom_report', {
p_report_id: params.reportId,
p_title: params.title,
p_description: params.description,
p_definition: definition
} as any)
return true
}
export async function deleteCustomReport(reportId: string): Promise<boolean> {
await rpcOrValue('rpc_delete_custom_report', {
p_report_id: reportId
} as any)
return true
}

View File

@@ -0,0 +1,145 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray, rpcOrNull, rpcOrValue } from './rpc.uts'
export type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
export type SegmentItem = { name: string; value: number }
export type TrafficItem = { name: string; value: number }
export type TopProductItem = { id: string; rank: number; name: string; sales: number }
export type TopMerchantItem = { id: string; rank: number; name: string; sales: number; growth: number }
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
export async function fetchDashboardTrend(period: string, range?: { start: string; end: string } | null): Promise<TrendData> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const p_start_date = toDateOnly(startIso)
const p_end_date = toDateOnly(endIso)
const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', {
p_start_date,
p_end_date
} as any)
const x: Array<string> = []
const gmv: Array<number> = []
const orders: Array<number> = []
for (let i = 0; i < rows.length; i++) {
const row: any = rows[i]
const d = `${row.getAny?.('date') ?? ''}`
x.push(d.length >= 10 ? d.slice(5) : d)
gmv.push(safeNumber(row.getAny?.('gmv') ?? 0))
orders.push(safeNumber(row.getAny?.('orders') ?? 0))
}
return { x, gmv, orders }
}
export async function fetchDashboardRealtime(): Promise<any> {
const [kpiRow, onlineUsersVal] = await Promise.all([
rpcOrNull('rpc_analytics_realtime_kpis', {} as any),
rpcOrValue('rpc_analytics_online_users', {} as any)
])
const obj: any = kpiRow != null ? kpiRow : ({} as any)
return {
gmv: Math.round(safeNumber(obj.getAny?.('gmv') ?? 0)),
gmv_growth: safeNumber(obj.getAny?.('gmv_growth') ?? 0),
orders: Math.round(safeNumber(obj.getAny?.('orders') ?? 0)),
order_growth: safeNumber(obj.getAny?.('order_growth') ?? 0),
online_users: Math.round(safeNumber(onlineUsersVal ?? 0)),
conversion_rate: safeNumber(obj.getAny?.('conversion_rate') ?? 0),
conversion_growth: safeNumber(obj.getAny?.('conversion_growth') ?? 0)
}
}
export async function fetchDashboardTopProducts(period: string, limit: number = 50): Promise<Array<TopProductItem>> {
const { startIso, endIso } = computeDateRange(period)
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
p_limit: limit
} as any)
const list: Array<TopProductItem> = []
for (let i = 0; i < rows.length; i++) {
const row: any = rows[i]
list.push({
id: `${row.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${row.getAny?.('name') ?? '未知商品'}`,
sales: safeNumber(row.getAny?.('sales') ?? 0)
})
}
return list
}
export async function fetchDashboardTopMerchants(period: string, limit: number = 50): Promise<Array<TopMerchantItem>> {
const { startIso, endIso } = computeDateRange(period)
const rows = await rpcOrEmptyArray('rpc_analytics_top_merchants', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
p_limit: limit
} as any)
const list: Array<TopMerchantItem> = []
for (let i = 0; i < rows.length; i++) {
const row: any = rows[i]
list.push({
id: `${row.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${row.getAny?.('name') ?? row.getAny?.('shop_name') ?? '未知商家'}`,
sales: safeNumber(row.getAny?.('sales') ?? 0),
growth: safeNumber(row.getAny?.('growth') ?? 0)
})
}
return list
}
export async function fetchDashboardUserSegments(period: string): Promise<Array<SegmentItem>> {
const { startIso, endIso } = computeDateRange(period)
const rows = await rpcOrEmptyArray('rpc_analytics_user_segments', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
const list: Array<SegmentItem> = []
for (let i = 0; i < rows.length; i++) {
const row: any = rows[i]
list.push({
name: `${row.getAny?.('name') ?? row.getAny?.('segment_name') ?? row.getAny?.('label') ?? '未知'}`,
value: safeNumber(row.getAny?.('value') ?? row.getAny?.('count') ?? row.getAny?.('amount') ?? 0)
})
}
return list
}
export async function fetchDashboardTrafficSources(period: string): Promise<Array<TrafficItem>> {
const { startIso, endIso } = computeDateRange(period)
const rows = await rpcOrEmptyArray('rpc_analytics_traffic_sources', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
const list: Array<TrafficItem> = []
for (let i = 0; i < rows.length; i++) {
const row: any = rows[i]
list.push({
name: `${row.getAny?.('name') ?? row.getAny?.('source_name') ?? row.getAny?.('label') ?? '未知'}`,
value: safeNumber(row.getAny?.('value') ?? row.getAny?.('count') ?? row.getAny?.('amount') ?? 0)
})
}
return list
}

View File

@@ -0,0 +1,58 @@
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
export type ReportInfo = {
id: string
title: string
description: string
definition: any
updated_at: string
}
function safeString(v: any): string {
return v != null ? `${v}` : ''
}
// 改造:调用 rpc_data_detail_report_info
export async function fetchReportInfo(reportId: string): Promise<ReportInfo | null> {
const row = await rpcOrNull('rpc_data_detail_report_info', {
p_report_id: reportId
} as any)
if (row == null) return null
return {
id: safeString(row.getAny?.('id')),
title: safeString(row.getAny?.('title')),
description: safeString(row.getAny?.('description')),
definition: row.getAny?.('definition'),
updated_at: safeString(row.getAny?.('updated_at'))
}
}
// 改造:调用 rpc_data_detail_rows
export async function fetchReportRows(reportId: string, params: any): Promise<Array<UTSJSONObject>> {
const result = await rpcOrNull('rpc_data_detail_rows', {
p_report_id: reportId,
p_params: params
} as any)
if (result == null) return []
const anyData = result as any
return Array.isArray(anyData) ? (anyData as Array<UTSJSONObject>) : ([] as Array<UTSJSONObject>)
}
// 保留调用,但 RPC 是模拟数据
export async function fetchDrilldown(reportId: string, itemId: string): Promise<Array<UTSJSONObject>> {
return await rpcOrEmptyArray('rpc_data_detail_drill_items', {
p_report_id: reportId,
p_item_id: itemId
} as any)
}
// 保留调用,但 RPC 是模拟数据
export async function fetchComparison(itemId: string, period: string): Promise<Array<UTSJSONObject>> {
return await rpcOrEmptyArray('rpc_data_detail_compare_gmv', {
p_item_id: itemId,
p_period: period
} as any)
}

View File

@@ -0,0 +1,15 @@
export type DateRange = { startIso: string; endIso: string }
export function computeDateRange(period: string): DateRange {
const now = new Date()
const start = new Date(now.getTime())
if (period === '7d') start.setDate(start.getDate() - 7)
else if (period === '30d') start.setDate(start.getDate() - 30)
else if (period === '90d') start.setDate(start.getDate() - 90)
else if (period === '1y') start.setFullYear(start.getFullYear() - 1)
return { startIso: start.toISOString(), endIso: now.toISOString() }
}
export function toDateOnly(iso: string): string {
return iso.length >= 10 ? iso.substring(0, 10) : iso
}

View File

@@ -0,0 +1,36 @@
import { computeDateRange } from './dateRange.uts'
import { rpcOrEmptyArray } from './rpc.uts'
export type DeliveryAnalysisData = {
trendList: Array<UTSJSONObject>
topList: Array<UTSJSONObject>
startIso: string
endIso: string
}
export async function fetchDeliveryAnalysis(period: string, range?: { start: string; end: string } | null): Promise<DeliveryAnalysisData> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const trendList = await rpcOrEmptyArray('rpc_delivery_efficiency_daily', {
p_start: startIso,
p_end: endIso
} as any)
const topList = await rpcOrEmptyArray('rpc_delivery_efficiency_top_drivers', {
p_start: startIso,
p_end: endIso,
p_limit: 10
} as any)
return { trendList, topList, startIso, endIso }
}

View File

@@ -0,0 +1,53 @@
export type AnalyticsErrorContext = {
action?: string
fallbackMessage?: string
}
export function mapAnalyticsError(err: any, ctx?: AnalyticsErrorContext): string {
const fallback = ctx?.fallbackMessage ?? '操作失败'
try {
if (err == null) return fallback
// string
if (typeof err === 'string') {
const s = err.trim()
return s.length > 0 ? s : fallback
}
// Error
const eAny = err as any
const msg: string = (eAny?.message != null ? String(eAny.message) : '')
const code: string = (eAny?.code != null ? String(eAny.code) : '')
const status: number | null = (typeof eAny?.status === 'number' ? (eAny.status as number) : null)
// RPC not found / route not found
if (status === 404) {
return '功能尚未部署RPC 未创建)'
}
// auth
if (code === 'P0001' || msg.includes('用户未登录') || msg.toLowerCase().includes('not logged') || msg.toLowerCase().includes('jwt')) {
return '请先登录'
}
// permission
if (msg.includes('无权限') || msg.toLowerCase().includes('permission') || msg.toLowerCase().includes('forbidden')) {
return '无权限操作'
}
// not found
if (msg.includes('不存在') || msg.toLowerCase().includes('not found')) {
return '数据不存在或已删除'
}
// fallback to message
if (msg.trim().length > 0) {
return msg
}
return fallback
} catch (e) {
return fallback
}
}

View File

@@ -0,0 +1,67 @@
import supa from '@/components/supadb/aksupainstance.uts'
export type InsightDetail = {
id: string
report_id: string
type: string
impact: string
title: string
content: string
created_at: string
}
export type RelatedReport = {
id: string
title: string
type: string
period: string
generated_at: string
}
export async function fetchInsightDetail(insightId: string): Promise<InsightDetail | null> {
const res: any = await supa
.from('analytics_insights')
.select('id, report_id, type, impact, title, content, created_at')
.eq('id', insightId)
.single()
if (res?.error != null) {
throw res.error
}
const it: any = res.data
if (it == null) return null
return {
id: `${it.id}`,
report_id: `${it.report_id || ''}`,
type: `${it.type || 'info'}`,
impact: `${it.impact || 'medium'}`,
title: `${it.title || ''}`,
content: `${it.content || ''}`,
created_at: `${it.created_at || ''}`
}
}
export async function fetchRelatedReport(reportId: string): Promise<RelatedReport | null> {
const rRes: any = await supa
.from('analytics_reports')
.select('id, title, type, period, generated_at')
.eq('id', reportId)
.single()
if (rRes?.error != null) {
throw rRes.error
}
const r: any = rRes.data
if (r == null) return null
return {
id: `${r.id}`,
title: `${r.title}`,
type: `${r.type}`,
period: `${r.period}`,
generated_at: `${r.generated_at || ''}`
}
}

View File

@@ -0,0 +1,64 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray } from './rpc.uts'
export type MarketTrendsData = {
trendRows: Array<UTSJSONObject>
categoryRows: Array<UTSJSONObject>
seasonalRows: Array<UTSJSONObject>
priceRows: Array<UTSJSONObject>
competitionRows: Array<UTSJSONObject>
startIso: string
endIso: string
}
export async function fetchMarketTrends(period: string, range?: { start: string; end: string } | null): Promise<MarketTrendsData> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const startDate = toDateOnly(startIso)
const endDate = toDateOnly(endIso)
const trendRows = await rpcOrEmptyArray('rpc_analytics_market_trend_daily', {
p_start: startIso,
p_end: endIso
} as UTSJSONObject)
const categoryRows = await rpcOrEmptyArray('rpc_analytics_category_sales', {
p_start_date: startDate,
p_end_date: endDate
} as UTSJSONObject)
const seasonalRows = await rpcOrEmptyArray('rpc_analytics_seasonal_trend', {
p_start_date: startDate,
p_end_date: endDate
} as UTSJSONObject)
const priceRows = await rpcOrEmptyArray('rpc_analytics_price_trend', {
p_start: startIso,
p_end: endIso
} as UTSJSONObject)
const competitionRows = await rpcOrEmptyArray('rpc_analytics_competition_share', {
p_start_date: startDate,
p_end_date: endDate
} as UTSJSONObject)
return {
trendRows,
categoryRows,
seasonalRows,
priceRows,
competitionRows,
startIso,
endIso
}
}

View File

@@ -0,0 +1,163 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
export type ProductOverview = {
total_products: number
product_growth: number
hot_products: number
turnover_rate: number
turnover_growth: number
avg_stock: number
stock_growth: number
}
export type ProductRank = { id: string; rank: number; name: string; sales: number; growth: number }
export type ProductTrendRow = { date: string; gmv: number; qty: number; orders: number }
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
export async function fetchProductOverview(period: string, range?: { start: string; end: string } | null): Promise<ProductOverview> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const row = await rpcOrNull('rpc_product_insights_overview', {
p_start: toDateOnly(startIso),
p_end: toDateOnly(endIso)
} as any)
const obj: any = row != null ? row : ({} as any)
return {
total_products: safeNumber(obj.getAny?.('total_products') ?? 0),
product_growth: safeNumber(obj.getAny?.('product_growth') ?? 0),
hot_products: safeNumber(obj.getAny?.('hot_products') ?? 0),
turnover_rate: safeNumber(obj.getAny?.('turnover_rate') ?? 0),
turnover_growth: safeNumber(obj.getAny?.('turnover_growth') ?? 0),
avg_stock: safeNumber(obj.getAny?.('avg_stock') ?? 0),
stock_growth: safeNumber(obj.getAny?.('stock_growth') ?? 0)
}
}
export async function fetchTopProducts(period: string, limit: number = 10, range?: { start: string; end: string } | null): Promise<Array<ProductRank>> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
p_limit: limit
} as any)
const list: Array<ProductRank> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
list.push({
id: `${r.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${r.getAny?.('name') ?? '未知商品'}`,
sales: safeNumber(r.getAny?.('sales') ?? 0),
growth: safeNumber(r.getAny?.('growth') ?? 0)
})
}
return list
}
export async function fetchProductTrend(period: string, productId: string, range?: { start: string; end: string } | null): Promise<Array<ProductTrendRow>> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
const rows = await rpcOrEmptyArray('rpc_analytics_product_trend', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
p_product_id: productId
} as any)
const out: Array<ProductTrendRow> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
const date = `${r.getAny?.('date') ?? ''}`
out.push({
date,
gmv: safeNumber(r.getAny?.('gmv') ?? 0),
qty: safeNumber(r.getAny?.('qty') ?? 0),
orders: safeNumber(r.getAny?.('orders') ?? 0)
})
}
return out
}
export async function fetchCategorySales(period: string, range?: { start: string; end: string } | null): Promise<Array<UTSJSONObject>> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
return await rpcOrEmptyArray('rpc_analytics_category_sales', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
}
export async function fetchStockInsights(period: string): Promise<Array<UTSJSONObject>> {
return await rpcOrEmptyArray('rpc_product_insights_stock', {} as any)
}
export async function fetchPriceTrend(period: string, range?: { start: string; end: string } | null): Promise<Array<UTSJSONObject>> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period)
startIso = computedRange.startIso
endIso = computedRange.endIso
}
return await rpcOrEmptyArray('rpc_analytics_price_trend', {
p_start: startIso,
p_end: endIso
} as any)
}
export async function fetchReviewInsights(): Promise<Array<UTSJSONObject>> {
return await rpcOrEmptyArray('rpc_product_insights_reviews', {} as any)
}

View File

@@ -0,0 +1,146 @@
import supa from '@/components/supadb/aksupainstance.uts'
export type AnalyticsReport = {
id: string
title: string
type: string
period: string
generated_at: string
description: string
}
export type AnalyticsReportMetric = {
key: string
label: string
value: number
format: string
icon: string
color: string
change: number
}
export type AnalyticsReportRow = {
row_date: string
gmv: number
orders: number
users: number
conversion: number
avg_order_amount: number
}
export type AnalyticsInsight = {
id: string
type: string
title: string
content: string
impact: string
}
export type AnalyticsRelatedReport = AnalyticsReport
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
export async function fetchReport(reportId: string): Promise<AnalyticsReport | null> {
const reportRes: any = await supa
.from('analytics_reports')
.select('id, title, type, period, generated_at, description')
.eq('id', reportId)
if (reportRes?.error != null) throw reportRes.error
const rows: Array<any> = Array.isArray(reportRes.data) ? (reportRes.data as Array<any>) : []
if (rows.length === 0) return null
const r = rows[0]
return {
id: `${r.id}`,
title: `${r.title}`,
type: `${r.type}`,
period: `${r.period}`,
generated_at: `${r.generated_at}`,
description: `${r.description || ''}`
}
}
export async function fetchReportMetrics(reportId: string): Promise<Array<AnalyticsReportMetric>> {
const metricRes: any = await supa
.from('analytics_report_metrics')
.select('metric_key, metric_label, metric_value_num, format, icon, color, change_pct')
.eq('report_id', reportId)
if (metricRes?.error != null) throw metricRes.error
const metricRows: Array<any> = Array.isArray(metricRes.data) ? (metricRes.data as Array<any>) : []
return metricRows.map((m: any) => ({
key: `${m.metric_key}`,
label: `${m.metric_label}`,
value: safeNumber(m.metric_value_num),
format: `${m.format || 'number'}`,
icon: `${m.icon || '📊'}`,
color: `${m.color || '#4caf50'}`,
change: safeNumber(m.change_pct)
}))
}
export async function fetchReportRows(reportId: string): Promise<Array<AnalyticsReportRow>> {
const rowsRes: any = await supa
.from('analytics_report_rows')
.select('row_date, gmv, orders, users, conversion, avg_order_amount')
.eq('report_id', reportId)
.order('row_date', { ascending: true } as any)
if (rowsRes?.error != null) throw rowsRes.error
const rows: Array<any> = Array.isArray(rowsRes.data) ? (rowsRes.data as Array<any>) : []
return rows.map((row: any) => ({
row_date: `${row.row_date}`,
gmv: safeNumber(row.gmv),
orders: safeNumber(row.orders),
users: safeNumber(row.users),
conversion: safeNumber(row.conversion),
avg_order_amount: safeNumber(row.avg_order_amount)
}))
}
export async function fetchReportInsights(reportId: string): Promise<Array<AnalyticsInsight>> {
const insightRes: any = await supa
.from('analytics_insights')
.select('id, type, title, content, impact')
.eq('report_id', reportId)
.order('created_at', { ascending: false } as any)
if (insightRes?.error != null) throw insightRes.error
const insRows: Array<any> = Array.isArray(insightRes.data) ? (insightRes.data as Array<any>) : []
return insRows.map((it: any) => ({
id: `${it.id}`,
type: `${it.type || 'info'}`,
title: `${it.title}`,
content: `${it.content}`,
impact: `${it.impact || 'medium'}`
}))
}
export async function fetchRelatedReports(reportType: string, excludeReportId: string): Promise<Array<AnalyticsRelatedReport>> {
const relatedRes: any = await supa
.from('analytics_reports')
.select('id, title, type, period, generated_at, description')
.eq('type', reportType)
.neq('id', excludeReportId)
.order('generated_at', { ascending: false } as any)
.limit(3 as any)
if (relatedRes?.error != null) throw relatedRes.error
const relRows: Array<any> = Array.isArray(relatedRes.data) ? (relatedRes.data as Array<any>) : []
return relRows.map((it: any) => ({
id: `${it.id}`,
title: `${it.title}`,
type: `${it.type}`,
period: `${it.period}`,
generated_at: `${it.generated_at}`,
description: `${it.description || ''}`
}))
}

View File

@@ -0,0 +1,42 @@
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
export async function rpcOrEmptyArray(functionName: string, params: UTSJSONObject): Promise<Array<UTSJSONObject>> {
await ensureSupabaseReady()
const res: any = await supa.rpc(functionName, params)
if (res?.status === 404) {
return [] as Array<UTSJSONObject>
}
if (res?.error != null) {
throw res.error
}
const anyData = res.data as any
return Array.isArray(anyData) ? (anyData as Array<UTSJSONObject>) : ([] as Array<UTSJSONObject>)
}
export async function rpcOrNull(functionName: string, params: UTSJSONObject): Promise<UTSJSONObject | null> {
await ensureSupabaseReady()
const res: any = await supa.rpc(functionName, params)
if (res?.status === 404) {
return null
}
if (res?.error != null) {
throw res.error
}
const anyData = res.data as any
if (Array.isArray(anyData)) {
return anyData.length > 0 ? (anyData[0] as UTSJSONObject) : null
}
return anyData != null ? (anyData as UTSJSONObject) : null
}
export async function rpcOrValue(functionName: string, params: UTSJSONObject): Promise<any> {
await ensureSupabaseReady()
const res: any = await supa.rpc(functionName, params)
if (res?.status === 404) {
return null
}
if (res?.error != null) {
throw res.error
}
return res.data
}

View File

@@ -0,0 +1,152 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
export type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
export type SalesKpis = {
gmv: number
gmv_growth: number
orders: number
order_growth: number
conversion_rate: number
conversion_growth: number
avg_order_amount: number
avg_order_growth: number
}
export type ProductRank = { id: string; rank: number; name: string; sales: number }
export type MerchantRank = { id: string; rank: number; name: string; sales: number; growth: number }
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
export async function fetchSalesKpis(period: string, range?: { start: string; end: string } | null): Promise<SalesKpis> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period);
startIso = computedRange.startIso;
endIso = computedRange.endIso;
}
const row = await rpcOrNull('rpc_analytics_sales_kpis', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
const obj: any = row != null ? row : ({} as any)
const gmv = safeNumber(obj.getAny?.('gmv') ?? 0)
const orders = safeNumber(obj.getAny?.('orders') ?? 0)
const avgOrder = orders > 0 ? gmv / orders : 0
return {
gmv: Math.round(gmv),
gmv_growth: safeNumber(obj.getAny?.('gmv_growth') ?? 0),
orders: Math.round(orders),
order_growth: safeNumber(obj.getAny?.('order_growth') ?? 0),
conversion_rate: safeNumber(obj.getAny?.('conversion_rate') ?? 0),
conversion_growth: safeNumber(obj.getAny?.('conversion_growth') ?? 0),
avg_order_amount: avgOrder,
avg_order_growth: safeNumber(obj.getAny?.('avg_order_growth') ?? obj.getAny?.('gmv_growth') ?? 0)
}
}
export async function fetchSalesTrend(period: string, range?: { start: string; end: string } | null): Promise<TrendData> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period);
startIso = computedRange.startIso;
endIso = computedRange.endIso;
}
const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
const x: Array<string> = []
const gmvArr: Array<number> = []
const orderArr: Array<number> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
const d = `${r.getAny?.('date') ?? ''}`
x.push(d.length >= 10 ? d.slice(5) : d)
gmvArr.push(safeNumber(r.getAny?.('gmv') ?? 0))
orderArr.push(safeNumber(r.getAny?.('orders') ?? 0))
}
return { x, gmv: gmvArr, orders: orderArr }
}
export async function fetchSalesTopProducts(period: string, limit: number = 50, range?: { start: string; end: string } | null): Promise<Array<ProductRank>> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period);
startIso = computedRange.startIso;
endIso = computedRange.endIso;
}
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
p_limit: limit
} as any)
const list: Array<ProductRank> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
list.push({
id: `${r.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${r.getAny?.('name') ?? ''}`,
sales: safeNumber(r.getAny?.('sales') ?? 0)
})
}
return list
}
export async function fetchSalesTopMerchants(period: string, limit: number = 50, range?: { start: string; end: string } | null): Promise<Array<MerchantRank>> {
let startIso: string;
let endIso: string;
if (range != null && range.start && range.end) {
startIso = range.start;
endIso = range.end;
} else {
const computedRange = computeDateRange(period);
startIso = computedRange.startIso;
endIso = computedRange.endIso;
}
const rows = await rpcOrEmptyArray('rpc_analytics_top_merchants', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso),
p_limit: limit
} as any)
const list: Array<MerchantRank> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
list.push({
id: `${r.getAny?.('id') ?? i}`,
rank: i + 1,
name: `${r.getAny?.('name') ?? ''}`,
sales: safeNumber(r.getAny?.('sales') ?? 0),
growth: safeNumber(r.getAny?.('growth') ?? 0)
})
}
return list
}

View File

@@ -0,0 +1,121 @@
import { computeDateRange, toDateOnly } from './dateRange.uts'
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
// --- Type Definitions ---
export type UserKpis = {
total_users: number
user_growth: number
new_users: number
new_user_growth: number
active_users: number // DAU
active_growth: number
paid_users: number
paid_growth: number
new_user_conversion_rate: number
repurchase_rate: number
}
export type UserGrowthTrend = {
dates: Array<string>
newUsers: Array<number>
activeUsers: Array<number>
}
export type UserActivity = {
dau: number
wau: number
mau: number
}
export type UserRetention = {
days: Array<string> // e.g., '次日', '3日', '7日', '14日', '30日'
rates: Array<number>
}
export type NewVsOldComparison = {
categories: Array<string> // e.g., 'GMV', '订单数', '客单价'
newUserData: Array<number>
oldUserData: Array<number>
}
// --- Helper ---
function safeNumber(v: any): number {
const n = Number(v)
return isFinite(n) ? n : 0
}
// --- Service Functions ---
export async function fetchUserKpis(period: string): Promise<UserKpis> {
const { startIso, endIso } = computeDateRange(period)
const row = await rpcOrNull('rpc_analytics_user_kpis', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
const obj: any = row != null ? row : ({} as any)
return {
total_users: safeNumber(obj.getAny?.('total_users') ?? 0),
user_growth: safeNumber(obj.getAny?.('user_growth') ?? 0),
new_users: safeNumber(obj.getAny?.('new_users') ?? 0),
new_user_growth: safeNumber(obj.getAny?.('new_user_growth') ?? 0),
active_users: safeNumber(obj.getAny?.('active_users') ?? 0),
active_growth: safeNumber(obj.getAny?.('active_growth') ?? 0),
paid_users: safeNumber(obj.getAny?.('paid_users') ?? 0), // Placeholder
paid_growth: safeNumber(obj.getAny?.('paid_growth') ?? 0), // Placeholder
new_user_conversion_rate: safeNumber(obj.getAny?.('new_user_conversion_rate') ?? 0), // Placeholder
repurchase_rate: safeNumber(obj.getAny?.('repurchase_rate') ?? 0)
}
}
export async function fetchUserGrowthTrend(period: string): Promise<UserGrowthTrend> {
const { startIso, endIso } = computeDateRange(period)
const rows = await rpcOrEmptyArray('rpc_analytics_user_growth_trend', {
p_start_date: toDateOnly(startIso),
p_end_date: toDateOnly(endIso)
} as any)
const dates: Array<string> = []
const newUsers: Array<number> = []
const activeUsers: Array<number> = []
for (let i = 0; i < rows.length; i++) {
const r: any = rows[i]
const d = `${r.getAny?.('date') ?? ''}`
dates.push(d.length >= 10 ? d.slice(5) : d)
newUsers.push(safeNumber(r.getAny?.('new_users') ?? 0))
activeUsers.push(safeNumber(r.getAny?.('active_users') ?? 0))
}
return { dates, newUsers, activeUsers }
}
// Placeholder for functions that need new RPCs
export async function fetchUserActivity(period: string): Promise<UserActivity> {
console.warn('fetchUserActivity needs rpc_analytics_user_activity RPC')
return { dau: 0, wau: 0, mau: 0 }
}
export async function fetchUserRetention(period: string): Promise<UserRetention> {
console.warn('fetchUserRetention needs rpc_analytics_user_retention RPC')
return { days: ['次日', '3日', '7日', '14日', '30日'], rates: [0, 0, 0, 0, 0] }
}
export async function fetchNewVsOldComparison(period: string): Promise<NewVsOldComparison> {
console.warn('fetchNewVsOldComparison needs rpc_analytics_new_vs_old_users RPC')
return { categories: ['GMV', '订单数', '客单价'], newUserData: [0, 0, 0], oldUserData: [0, 0, 0] }
}
export async function fetchConversionFunnel(period: string): Promise<Array<{ step: string; value: number }>> {
console.warn('fetchConversionFunnel needs rpc_analytics_conversion_funnel RPC')
return [
{ step: '访问', value: 0 },
{ step: '详情页', value: 0 },
{ step: '加购', value: 0 },
{ step: '下单', value: 0 },
{ step: '支付', value: 0 }
]
}