数据库文档编写,开发规范文档,数据库接入
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page" @click="closeMoreMenu">
|
||||
<view class="page" @click.self="closeMoreMenu">
|
||||
<!-- 固定顶部导航栏 -->
|
||||
<AnalyticsTopBar
|
||||
:title="'优惠券效果分析'"
|
||||
@@ -27,19 +27,34 @@
|
||||
<view class="main-content">
|
||||
<view class="container">
|
||||
|
||||
<!-- 时间维度筛选 -->
|
||||
<!-- 时间维度筛选(快捷 + 自定义) -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="p in timePeriods"
|
||||
:key="p.value"
|
||||
class="tab"
|
||||
:class="{ active: selectedPeriod === p.value }"
|
||||
:class="{ active: selectedPeriod === p.value && !customRangeEnabled }"
|
||||
@click="selectPeriod(p.value)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</view>
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: customRangeEnabled }"
|
||||
@click="toggleCustomRange"
|
||||
>
|
||||
自定义
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AnalyticsDateRangePicker
|
||||
v-if="customRangeEnabled"
|
||||
:initialStartDate="selectedStartDate"
|
||||
:initialEndDate="selectedEndDate"
|
||||
@apply="onDateRangeApply"
|
||||
@clear="onDateRangeClear"
|
||||
/>
|
||||
|
||||
<!-- KPI 指标卡片 -->
|
||||
<view class="kpi-grid">
|
||||
<view class="kpi-card">
|
||||
@@ -113,6 +128,7 @@ import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchCouponAnalysis } from '@/services/analytics/couponAnalysisService.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
@@ -121,6 +137,11 @@ import type { TimePeriod } from '@/types/analytics/common.uts'
|
||||
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
|
||||
const customRangeEnabled = ref(false)
|
||||
const selectedStartDate = ref('')
|
||||
const selectedEndDate = ref('')
|
||||
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/coupon-analysis')
|
||||
@@ -165,7 +186,11 @@ onLoad(() => {
|
||||
|
||||
async function loadCouponData() {
|
||||
try {
|
||||
const data = await fetchCouponAnalysis(selectedPeriod.value)
|
||||
const range = selectedStartDate.value && selectedEndDate.value
|
||||
? { start: selectedStartDate.value, end: selectedEndDate.value }
|
||||
: null
|
||||
|
||||
const data = await fetchCouponAnalysis(selectedPeriod.value, range)
|
||||
|
||||
const overviewRow = data.overviewRow
|
||||
const typeList = data.typeList
|
||||
@@ -227,6 +252,27 @@ async function loadCouponData() {
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
loadCouponData()
|
||||
}
|
||||
|
||||
function toggleCustomRange() {
|
||||
customRangeEnabled.value = !customRangeEnabled.value
|
||||
}
|
||||
|
||||
function onDateRangeApply(range: { start: string; end: string }) {
|
||||
selectedStartDate.value = range.start
|
||||
selectedEndDate.value = range.end
|
||||
customRangeEnabled.value = true
|
||||
loadCouponData()
|
||||
}
|
||||
|
||||
function onDateRangeClear() {
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
customRangeEnabled.value = false
|
||||
loadCouponData()
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uv
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import { goToLogin as goToLoginPage } from '@/utils/utils.uts'
|
||||
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
|
||||
import { ensureAnalyticsLogin } from '@/services/analytics/authGuard.uts'
|
||||
import { listCustomReports, createCustomReport, updateCustomReport, deleteCustomReport } from '@/services/analytics/customReportService.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
|
||||
@@ -225,6 +226,7 @@ const chartTypes = ref<Array<ChartType>>([
|
||||
|
||||
onLoad(() => {
|
||||
currentPath.value = '/pages/mall/analytics/custom-report'
|
||||
if (!ensureAnalyticsLogin({ toastTitle: '请先登录后使用自定义报表' })) return
|
||||
loadReports()
|
||||
})
|
||||
|
||||
@@ -439,17 +441,27 @@ async function saveReport() {
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
// 检查 newReportId 是否有效,无效则认为创建失败
|
||||
if (newReportId == null || newReportId.length === 0) {
|
||||
uni.showToast({
|
||||
title: '创建失败:未返回报表ID',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
closeModal()
|
||||
loadReports()
|
||||
|
||||
if (newReportId.length > 0) {
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/analytics/report-detail?reportId=${newReportId}`
|
||||
})
|
||||
}, 400)
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/analytics/report-detail?reportId=${newReportId}`
|
||||
})
|
||||
}, 400)
|
||||
|
||||
} catch (e: any) {
|
||||
uni.hideLoading()
|
||||
console.error('saveReport exception:', e)
|
||||
@@ -493,6 +505,10 @@ function closeMoreMenu() {
|
||||
showMoreMenu.value = false
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
ensureAnalyticsLogin({ toastTitle: '请先登录后使用自定义报表' })
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
uni.showToast({ title: '搜索', icon: 'none' })
|
||||
}
|
||||
|
||||
@@ -27,19 +27,34 @@
|
||||
<view class="main-content">
|
||||
<view class="container">
|
||||
|
||||
<!-- 时间维度筛选 -->
|
||||
<!-- 时间维度筛选(快捷 + 自定义) -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="p in timePeriods"
|
||||
:key="p.value"
|
||||
class="tab"
|
||||
:class="{ active: selectedPeriod === p.value }"
|
||||
:class="{ active: selectedPeriod === p.value && !customRangeEnabled }"
|
||||
@click="selectPeriod(p.value)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</view>
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: customRangeEnabled }"
|
||||
@click="toggleCustomRange"
|
||||
>
|
||||
自定义
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AnalyticsDateRangePicker
|
||||
v-if="customRangeEnabled"
|
||||
:initialStartDate="selectedStartDate"
|
||||
:initialEndDate="selectedEndDate"
|
||||
@apply="onDateRangeApply"
|
||||
@clear="onDateRangeClear"
|
||||
/>
|
||||
|
||||
<!-- KPI 指标卡片 -->
|
||||
<view class="kpi-grid">
|
||||
<view class="kpi-card">
|
||||
@@ -120,6 +135,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
|
||||
import { fetchDeliveryAnalysis } from '@/services/analytics/deliveryAnalysisService.uts'
|
||||
@@ -130,6 +146,11 @@ import type { DeliveryData, DriverRank } from '@/types/analytics/delivery.uts'
|
||||
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
|
||||
const customRangeEnabled = ref(false)
|
||||
const selectedStartDate = ref('')
|
||||
const selectedEndDate = ref('')
|
||||
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/delivery-analysis')
|
||||
@@ -171,7 +192,11 @@ onLoad(() => {
|
||||
|
||||
async function loadDeliveryData() {
|
||||
try {
|
||||
const data: any = await fetchDeliveryAnalysis(selectedPeriod.value)
|
||||
const range = selectedStartDate.value && selectedEndDate.value
|
||||
? { start: selectedStartDate.value, end: selectedEndDate.value }
|
||||
: null
|
||||
|
||||
const data: any = await fetchDeliveryAnalysis(selectedPeriod.value, range)
|
||||
const trendList = data.trendList
|
||||
const topList = data.topList
|
||||
|
||||
@@ -263,6 +288,27 @@ async function loadDeliveryData() {
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
loadDeliveryData()
|
||||
}
|
||||
|
||||
function toggleCustomRange() {
|
||||
customRangeEnabled.value = !customRangeEnabled.value
|
||||
}
|
||||
|
||||
function onDateRangeApply(range: { start: string; end: string }) {
|
||||
selectedStartDate.value = range.start
|
||||
selectedEndDate.value = range.end
|
||||
customRangeEnabled.value = true
|
||||
loadDeliveryData()
|
||||
}
|
||||
|
||||
function onDateRangeClear() {
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
customRangeEnabled.value = false
|
||||
loadDeliveryData()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page" @click="closeMoreMenu">
|
||||
<view class="page" @click.self="closeMoreMenu">
|
||||
<!-- 固定顶部导航栏 -->
|
||||
<AnalyticsTopBar
|
||||
:title="'数据分析中心'"
|
||||
@@ -275,6 +275,8 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ensureAnalyticsLogin } from '@/services/analytics/authGuard.uts'
|
||||
|
||||
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
@@ -684,6 +686,12 @@ async function initDashboard() {
|
||||
|
||||
async function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
|
||||
// 切换到快捷时间段时,退出自定义范围
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([loadTrend(), loadUserSegments(), loadTrafficSources(), loadTopProducts(), loadTopMerchants()])
|
||||
@@ -852,7 +860,8 @@ function exportReport() {
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
initDashboard()
|
||||
if (!ensureAnalyticsLogin({ toastTitle: '请先登录后查看数据分析' })) return
|
||||
initDashboard()
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page" @click="closeMoreMenu">
|
||||
<view class="page" @click.self="closeMoreMenu">
|
||||
<!-- 固定顶部导航栏 -->
|
||||
<AnalyticsTopBar
|
||||
:title="'市场趋势'"
|
||||
@@ -27,19 +27,34 @@
|
||||
<view class="main-content">
|
||||
<view class="container">
|
||||
|
||||
<!-- 时间维度筛选 -->
|
||||
<!-- 时间维度筛选(快捷 + 自定义) -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="p in timePeriods"
|
||||
:key="p.value"
|
||||
class="tab"
|
||||
:class="{ active: selectedPeriod === p.value }"
|
||||
:class="{ active: selectedPeriod === p.value && !customRangeEnabled }"
|
||||
@click="selectPeriod(p.value)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</view>
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: customRangeEnabled }"
|
||||
@click="toggleCustomRange"
|
||||
>
|
||||
自定义
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AnalyticsDateRangePicker
|
||||
v-if="customRangeEnabled"
|
||||
:initialStartDate="selectedStartDate"
|
||||
:initialEndDate="selectedEndDate"
|
||||
@apply="onDateRangeApply"
|
||||
@clear="onDateRangeClear"
|
||||
/>
|
||||
|
||||
<!-- 市场整体趋势 -->
|
||||
<view class="card card-full">
|
||||
<view class="card-head">
|
||||
@@ -101,6 +116,7 @@ import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uv
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import { fetchMarketTrends } from '@/services/analytics/marketTrendsService.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
|
||||
@@ -109,6 +125,11 @@ import type { MarketTrendsResponse } from '@/types/analytics/market.uts'
|
||||
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
|
||||
const customRangeEnabled = ref(false)
|
||||
const selectedStartDate = ref('')
|
||||
const selectedEndDate = ref('')
|
||||
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/market-trends')
|
||||
@@ -149,7 +170,11 @@ onShow(() => {
|
||||
|
||||
async function loadMarketData() {
|
||||
try {
|
||||
const data = (await fetchMarketTrends(selectedPeriod.value)) as MarketTrendsResponse
|
||||
const range = selectedStartDate.value && selectedEndDate.value
|
||||
? { start: selectedStartDate.value, end: selectedEndDate.value }
|
||||
: null
|
||||
|
||||
const data = (await fetchMarketTrends(selectedPeriod.value, range)) as MarketTrendsResponse
|
||||
|
||||
_marketTrendRows.value = data.trendRows
|
||||
_industryRows.value = data.categoryRows
|
||||
@@ -169,6 +194,9 @@ async function loadMarketData() {
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
loadMarketData()
|
||||
}
|
||||
|
||||
@@ -177,6 +205,24 @@ function refreshData() {
|
||||
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||
}
|
||||
|
||||
function toggleCustomRange() {
|
||||
customRangeEnabled.value = !customRangeEnabled.value
|
||||
}
|
||||
|
||||
function onDateRangeApply(range: { start: string; end: string }) {
|
||||
selectedStartDate.value = range.start
|
||||
selectedEndDate.value = range.end
|
||||
customRangeEnabled.value = true
|
||||
loadMarketData()
|
||||
}
|
||||
|
||||
function onDateRangeClear() {
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
customRangeEnabled.value = false
|
||||
loadMarketData()
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page" @click="closeMoreMenu">
|
||||
<view class="page" @click.self="closeMoreMenu">
|
||||
<!-- 固定顶部导航栏 -->
|
||||
<AnalyticsTopBar
|
||||
:title="'商品洞察'"
|
||||
@@ -27,19 +27,34 @@
|
||||
<view class="main-content">
|
||||
<view class="container">
|
||||
|
||||
<!-- 时间维度筛选 -->
|
||||
<!-- 时间维度筛选(快捷 + 自定义) -->
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="p in timePeriods"
|
||||
:key="p.value"
|
||||
class="tab"
|
||||
:class="{ active: selectedPeriod === p.value }"
|
||||
:class="{ active: selectedPeriod === p.value && !customRangeEnabled }"
|
||||
@click="selectPeriod(p.value)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</view>
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: customRangeEnabled }"
|
||||
@click="toggleCustomRange"
|
||||
>
|
||||
自定义
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AnalyticsDateRangePicker
|
||||
v-if="customRangeEnabled"
|
||||
:initialStartDate="selectedStartDate"
|
||||
:initialEndDate="selectedEndDate"
|
||||
@apply="onDateRangeApply"
|
||||
@clear="onDateRangeClear"
|
||||
/>
|
||||
|
||||
<!-- KPI 指标卡片 -->
|
||||
<view class="kpi-grid">
|
||||
<view class="kpi-card">
|
||||
@@ -173,6 +188,7 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchProductOverview, fetchTopProducts, fetchProductTrend, fetchCategorySales, fetchStockInsights, fetchPriceTrend, fetchReviewInsights } from '@/services/analytics/productInsightsService.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
@@ -182,6 +198,11 @@ import type { ProductData, ProductRank } from '@/types/analytics/product.uts'
|
||||
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
|
||||
const customRangeEnabled = ref(false)
|
||||
const selectedStartDate = ref('')
|
||||
const selectedEndDate = ref('')
|
||||
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/product-insights')
|
||||
@@ -251,7 +272,11 @@ async function loadSelectedProductTrend() {
|
||||
return
|
||||
}
|
||||
|
||||
const trend = await fetchProductTrend(selectedPeriod.value, selectedProductId.value)
|
||||
const range = selectedStartDate.value && selectedEndDate.value
|
||||
? { start: selectedStartDate.value, end: selectedEndDate.value }
|
||||
: null
|
||||
|
||||
const trend = await fetchProductTrend(selectedPeriod.value, selectedProductId.value, range)
|
||||
const rows: Array<any> = trend as any
|
||||
|
||||
const x: Array<string> = []
|
||||
@@ -367,12 +392,16 @@ async function loadProductData() {
|
||||
try {
|
||||
updateTime()
|
||||
|
||||
const range = selectedStartDate.value && selectedEndDate.value
|
||||
? { start: selectedStartDate.value, end: selectedEndDate.value }
|
||||
: null
|
||||
|
||||
const [overview, topList, catRows, stockRows, _priceRows, reviewRows] = await Promise.all([
|
||||
fetchProductOverview(selectedPeriod.value),
|
||||
fetchTopProducts(selectedPeriod.value, 10),
|
||||
fetchCategorySales(selectedPeriod.value),
|
||||
fetchProductOverview(selectedPeriod.value, range),
|
||||
fetchTopProducts(selectedPeriod.value, 10, range),
|
||||
fetchCategorySales(selectedPeriod.value, range),
|
||||
fetchStockInsights(selectedPeriod.value),
|
||||
fetchPriceTrend(selectedPeriod.value),
|
||||
fetchPriceTrend(selectedPeriod.value, range),
|
||||
fetchReviewInsights()
|
||||
])
|
||||
|
||||
@@ -415,6 +444,27 @@ async function loadProductData() {
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
loadProductData()
|
||||
}
|
||||
|
||||
function toggleCustomRange() {
|
||||
customRangeEnabled.value = !customRangeEnabled.value
|
||||
}
|
||||
|
||||
function onDateRangeApply(range: { start: string; end: string }) {
|
||||
selectedStartDate.value = range.start
|
||||
selectedEndDate.value = range.end
|
||||
customRangeEnabled.value = true
|
||||
loadProductData()
|
||||
}
|
||||
|
||||
function onDateRangeClear() {
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
customRangeEnabled.value = false
|
||||
loadProductData()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page" @click="closeMoreMenu">
|
||||
<view class="page" @click.self="closeMoreMenu">
|
||||
<!-- 固定顶部导航栏 -->
|
||||
<AnalyticsTopBar
|
||||
:title="'销售报表'"
|
||||
@@ -277,6 +277,9 @@ async function loadSalesData() {
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
loadSalesData()
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,8 @@ $$;
|
||||
|
||||
|
||||
-- 4) 渠道来源(按注册来源,统计周期内新增用户来源)
|
||||
-- 兼容性说明:部分环境的 ak_users 可能不存在 registration_source 字段。
|
||||
-- 为避免 RPC 报错导致首页整体加载失败,这里做“字段存在则分组统计,不存在则全部归为未知”的兼容。
|
||||
CREATE OR REPLACE FUNCTION public.rpc_analytics_traffic_sources(
|
||||
p_start_date DATE,
|
||||
p_end_date DATE
|
||||
@@ -194,13 +196,37 @@ RETURNS TABLE (
|
||||
name TEXT,
|
||||
value BIGINT
|
||||
)
|
||||
LANGUAGE sql
|
||||
AS $$
|
||||
SELECT
|
||||
COALESCE(registration_source, '未知') AS name,
|
||||
COUNT(id)::BIGINT AS value
|
||||
FROM public.ak_users
|
||||
WHERE created_at::DATE BETWEEN p_start_date AND p_end_date
|
||||
GROUP BY name
|
||||
ORDER BY value DESC;
|
||||
$$;
|
||||
LANGUAGE plpgsql
|
||||
AS $
|
||||
DECLARE
|
||||
has_registration_source BOOLEAN := FALSE;
|
||||
BEGIN
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'ak_users'
|
||||
AND column_name = 'registration_source'
|
||||
) INTO has_registration_source;
|
||||
|
||||
IF has_registration_source THEN
|
||||
RETURN QUERY
|
||||
EXECUTE '
|
||||
SELECT
|
||||
COALESCE(registration_source, ''未知'') AS name,
|
||||
COUNT(id)::BIGINT AS value
|
||||
FROM public.ak_users
|
||||
WHERE created_at::DATE BETWEEN $1 AND $2
|
||||
GROUP BY name
|
||||
ORDER BY value DESC
|
||||
'
|
||||
USING p_start_date, p_end_date;
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT '未知'::TEXT AS name,
|
||||
COUNT(id)::BIGINT AS value
|
||||
FROM public.ak_users
|
||||
WHERE created_at::DATE BETWEEN p_start_date AND p_end_date;
|
||||
END IF;
|
||||
END;
|
||||
$;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page" @click="closeMoreMenu">
|
||||
<view class="page" @click.self="closeMoreMenu">
|
||||
<!-- 固定顶部导航栏 -->
|
||||
<AnalyticsTopBar
|
||||
:title="'用户分析'"
|
||||
@@ -36,14 +36,29 @@
|
||||
v-for="p in timePeriods"
|
||||
:key="p.value"
|
||||
class="tab"
|
||||
:class="{ active: selectedPeriod === p.value }"
|
||||
:class="{ active: selectedPeriod === p.value && !customRangeEnabled }"
|
||||
@click="selectPeriod(p.value)"
|
||||
>
|
||||
{{ p.label }}
|
||||
</view>
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: customRangeEnabled }"
|
||||
@click="toggleCustomRange"
|
||||
>
|
||||
自定义
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AnalyticsDateRangePicker
|
||||
v-if="customRangeEnabled"
|
||||
:initialStartDate="selectedStartDate"
|
||||
:initialEndDate="selectedEndDate"
|
||||
@apply="onDateRangeApply"
|
||||
@clear="onDateRangeClear"
|
||||
/>
|
||||
|
||||
<view class="filter-hint">
|
||||
<text class="filter-hint-text">渠道/终端/会员/新老:待接入数据后开放</text>
|
||||
</view>
|
||||
@@ -210,6 +225,7 @@
|
||||
<script setup lang="uts">
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||||
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
|
||||
@@ -222,6 +238,11 @@ import type { UserData, FunnelStep } from '@/types/analytics/user.uts'
|
||||
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
|
||||
const customRangeEnabled = ref(false)
|
||||
const selectedStartDate = ref('')
|
||||
const selectedEndDate = ref('')
|
||||
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/user-analysis')
|
||||
@@ -297,6 +318,10 @@ onLoad(() => {
|
||||
})
|
||||
|
||||
function calcDateRange() {
|
||||
if (selectedStartDate.value && selectedEndDate.value) {
|
||||
return { startDate: new Date(selectedStartDate.value), endDate: new Date(selectedEndDate.value) }
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const days = selectedPeriod.value === '7d' ? 7 : selectedPeriod.value === '30d' ? 30 : selectedPeriod.value === '90d' ? 90 : 365
|
||||
@@ -440,6 +465,27 @@ async function loadUserData() {
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
customRangeEnabled.value = false
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
loadUserData()
|
||||
}
|
||||
|
||||
function toggleCustomRange() {
|
||||
customRangeEnabled.value = !customRangeEnabled.value
|
||||
}
|
||||
|
||||
function onDateRangeApply(range: { start: string; end: string }) {
|
||||
selectedStartDate.value = range.start
|
||||
selectedEndDate.value = range.end
|
||||
customRangeEnabled.value = true
|
||||
loadUserData()
|
||||
}
|
||||
|
||||
function onDateRangeClear() {
|
||||
selectedStartDate.value = ''
|
||||
selectedEndDate.value = ''
|
||||
customRangeEnabled.value = false
|
||||
loadUserData()
|
||||
}
|
||||
|
||||
|
||||
@@ -335,7 +335,20 @@ const handleLogin = async () => {
|
||||
}
|
||||
|
||||
const navigateToRegister = () => {
|
||||
uni.navigateTo({ url: '/pages/user/register' })
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
const opts = currentPage?.options as any
|
||||
const redirect = opts?.redirect as string | null
|
||||
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/register?redirect=${redirect}`
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/register'
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleTutorial = () => uni.showToast({ title: '扫码教程开发中', icon: 'none' })
|
||||
const handleForgotPassword = () => uni.showToast({ title: '忘记密码开发中', icon: 'none' })
|
||||
|
||||
@@ -330,9 +330,20 @@
|
||||
|
||||
// 跳转到登录页
|
||||
const navigateToLogin = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
const pages = getCurrentPages() as any[]
|
||||
const currentPage = pages.length > 0 ? pages[pages.length - 1] : null
|
||||
const opts = currentPage?.options as any
|
||||
const redirect = opts?.redirect as string | null
|
||||
|
||||
if (redirect != null && redirect.length > 0) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/user/login?redirect=${redirect}`
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到协议页面
|
||||
|
||||
Reference in New Issue
Block a user