数据分析ui补充完善,接入数据库

This commit is contained in:
comlibmb
2026-02-01 20:17:37 +08:00
parent 6716398175
commit 19970db288
19 changed files with 393 additions and 435 deletions

View File

@@ -109,7 +109,8 @@
</template>
<script setup lang="uts">
import { computed, onLoad, ref } from 'vue'
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 EChartsView from '@/uni_modules/charts/EChartsView.vue'

View File

@@ -163,12 +163,13 @@
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import { goToLogin } from '@/utils/utils.uts'
import { goToLogin as goToLoginPage } from '@/utils/utils.uts'
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
import { listCustomReports, createCustomReport, updateCustomReport, deleteCustomReport } from '@/services/analytics/customReportService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
import { onLoad, onShow, reactive, ref } from 'vue'
import { reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import type { CustomReport, ReportForm, ReportFormErrors } from '@/types/analytics/custom-report.uts'
import type { Metric, TimePeriod, ChartType } from '@/types/analytics/common.uts'
@@ -516,360 +517,10 @@ function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
}
function goToLogin() {
goToLogin('/pages/mall/analytics/custom-report')
function handleGoToLogin() {
goToLoginPage('/pages/mall/analytics/custom-report')
}
return {
showMoreMenu: false,
showSidebarMenu: false,
currentPath: '/pages/mall/analytics/custom-report',
showCreateModal: false,
editingReport: null as Report | null,
reports: [] as Array<Report>,
isLoggedIn: false,
reportForm: {
name: '',
description: '',
metrics: [] as Array<string>,
period: '7d',
chartType: 'line'
} as ReportForm,
formErrors: {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
} as ReportFormErrors,
availableMetrics: [
{ key: 'gmv', label: 'GMV' },
{ key: 'orders', label: '订单数' },
{ key: 'users', label: '用户数' },
{ key: 'conversion', label: '转化率' },
{ key: 'avg_order', label: '客单价' },
{ key: 'repurchase', label: '复购率' }
] as Array<Metric>,
timePeriods: [
{ value: '7d', label: '7天' },
{ value: '30d', label: '30天' },
{ value: '90d', label: '90天' },
{ value: '1y', label: '1年' }
] as Array<TimePeriod>,
chartTypes: [
{ value: 'line', label: '折线图' },
{ value: 'bar', label: '柱状图' },
{ value: 'pie', label: '饼图' },
{ value: 'area', label: '面积图' },
{ value: 'combo', label: '组合图' }
] as Array<ChartType>
}
},
onLoad() {
this.currentPath = '/pages/mall/analytics/custom-report'
this.loadReports()
},
onShow() {
this.currentPath = '/pages/mall/analytics/custom-report'
},
methods: {
async loadReports() {
try {
await ensureSupabaseReady()
// 获取当前登录用户,用于按 owner_user_id 过滤自定义报表
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
// 未登录时显示空列表
this.isLoggedIn = false
this.reports = []
return
}
this.isLoggedIn = true
const items = await listCustomReports(uid)
const list: Array<Report> = []
for (let i = 0; i < items.length; i++) {
const r = items[i]
list.push({
id: `${r.id}`,
name: `${r.title}`,
description: `${r.description || ''}`,
metrics: [] as Array<string>,
charts: [] as Array<string>,
updated_at: `${r.updated_at || ''}`
} as Report)
}
this.reports = list
} catch (e) {
console.error('loadReports failed', e)
uni.showToast({ title: '报表加载失败', icon: 'none' })
}
},
createReport() {
this.editingReport = null
this.reportForm = {
name: '',
description: '',
metrics: [],
period: '7d',
chartType: 'line'
}
this.formErrors = {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
}
this.showCreateModal = true
},
editReport(report: Report) {
this.editingReport = report
this.reportForm = {
name: report.name,
description: report.description,
metrics: report.metrics,
period: '7d',
chartType: 'line'
}
this.formErrors = {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
}
this.showCreateModal = true
},
deleteReport(report: Report) {
uni.showModal({
title: '确认删除',
content: `确定要删除报表"${report.name}"吗?`,
success: (res) => {
if (res.confirm) {
this.doDeleteReport(report)
}
}
})
},
async doDeleteReport(report: Report) {
try {
await ensureSupabaseReady()
await deleteCustomReport(report.id)
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadReports()
} catch (e: any) {
console.error('doDeleteReport failed', e)
const errorMsg = e?.message || '删除失败'
uni.showToast({ title: errorMsg, icon: 'none' })
}
},
toggleMetric(key: string) {
const index = this.reportForm.metrics.indexOf(key)
if (index >= 0) {
this.reportForm.metrics.splice(index, 1)
} else {
this.reportForm.metrics.push(key)
}
if (this.reportForm.metrics.length > 0) {
this.formErrors.metrics = ''
}
},
onNameInput() {
const name = this.reportForm.name.trim()
if (name.length === 0) {
this.formErrors.name = '报表名称不能为空'
} else if (name.length > 50) {
this.formErrors.name = '报表名称不能超过50个字符'
} else {
this.formErrors.name = ''
}
},
onDescriptionInput() {
const desc = this.reportForm.description
if (desc.length > 200) {
this.formErrors.description = '报表描述不能超过200个字符'
} else {
this.formErrors.description = ''
}
},
selectPeriod(value: string) {
this.reportForm.period = value
this.formErrors.period = ''
},
selectChartType(value: string) {
this.reportForm.chartType = value
this.formErrors.chartType = ''
},
validateReportForm(): boolean {
this.onNameInput()
this.onDescriptionInput()
if (this.reportForm.metrics.length === 0) {
this.formErrors.metrics = '请至少选择一个指标'
} else {
this.formErrors.metrics = ''
}
if (!this.reportForm.period) {
this.formErrors.period = '请选择时间维度'
}
if (!this.reportForm.chartType) {
this.formErrors.chartType = '请选择图表类型'
}
if (this.formErrors.name || this.formErrors.description || this.formErrors.metrics || this.formErrors.period || this.formErrors.chartType) {
uni.showToast({ title: '请先修正表单中的错误提示', icon: 'none' })
return false
}
return true
},
async saveReport() {
if (!this.validateReportForm()) {
return
}
try {
uni.showLoading({ title: '保存中...' })
await ensureSupabaseReady()
// 获取当前登录用户,作为 owner_user_id
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
uni.hideLoading()
uni.showModal({
title: '需要登录',
content: '创建自定义报表需要先登录,是否前往登录页面?',
success: (res) => {
if (res.confirm) {
goToLogin('/pages/mall/analytics/custom-report')
}
}
})
return
}
let newReportId = ''
// 1) 创建或更新自定义报表
if (this.editingReport == null) {
newReportId = await createCustomReport({
title: this.reportForm.name,
description: this.reportForm.description || '',
period: this.reportForm.period,
metrics: this.reportForm.metrics,
chartType: this.reportForm.chartType || 'line'
})
} else {
await updateCustomReport({
reportId: this.editingReport.id,
title: this.reportForm.name,
description: this.reportForm.description || null,
period: this.reportForm.period || null
})
newReportId = this.editingReport.id
}
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
this.closeModal()
this.loadReports()
// 新建或编辑成功后,直接进入报表详情页,给用户明确反馈
if (newReportId.length > 0) {
setTimeout(() => {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${newReportId}`
})
}, 400)
}
} catch (e: any) {
uni.hideLoading()
console.error('saveReport exception:', e)
uni.showToast({
title: mapAnalyticsError(e, { fallbackMessage: '保存失败' }),
icon: 'none',
duration: 3000
})
}
},
openReport(report: Report) {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${report.id}`
})
},
closeModal() {
this.showCreateModal = false
this.editingReport = null
},
goToLogin() {
goToLogin('/pages/mall/analytics/custom-report')
},
refreshData() {
this.loadReports()
uni.showToast({ title: '已刷新', icon: 'success' })
},
handleMenu() {
this.showSidebarMenu = true
},
handleSidebarUpdate(visible: boolean) {
this.showSidebarMenu = visible
},
toggleMoreMenu() {
this.showMoreMenu = !this.showMoreMenu
},
closeMoreMenu() {
this.showMoreMenu = false
},
handleSearch() {
uni.showToast({ title: '搜索', icon: 'none' })
},
handleNotification() {
uni.showToast({ title: '通知', icon: 'none' })
},
handleFullscreen() {
uni.showToast({ title: '全屏', icon: 'none' })
},
handleMobile() {
uni.showToast({ title: '移动端', icon: 'none' })
},
handleDropdown() {
uni.showToast({ title: '下拉菜单', icon: 'none' })
},
handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
}
}
}
</script>
<style>

View File

@@ -90,7 +90,8 @@
</template>
<script setup lang="uts">
import { onLoad, onShow, reactive, ref } from 'vue'
import { reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'

View File

@@ -115,7 +115,8 @@
</template>
<script setup lang="uts">
import { computed, onLoad, reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'

View File

@@ -76,19 +76,34 @@
</view>
</view>
<!-- 时间维度:横排 -->
<!-- 时间维度筛选(快捷 + 自定义) -->
<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">
@@ -252,10 +267,10 @@
<!-- 留白 -->
<view style="height: 24px;"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
@@ -264,6 +279,7 @@ import { computed, reactive, ref, watch } from 'vue'
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
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 { fetchDashboardRealtime, fetchDashboardTrend, fetchDashboardUserSegments, fetchDashboardTrafficSources, fetchDashboardTopProducts, fetchDashboardTopMerchants } from '@/services/analytics/dashboardService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
@@ -271,6 +287,10 @@ import type { TrendData, SegmentItem, TrafficItem, TopProductItem, TopMerchantIt
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/index')
@@ -333,7 +353,11 @@ function stopAutoRefresh() {
async function loadTrend() {
try {
const data = await fetchDashboardTrend(selectedPeriod.value)
const range = selectedStartDate.value && selectedEndDate.value
? { start: selectedStartDate.value, end: selectedEndDate.value }
: null
const data = await fetchDashboardTrend(selectedPeriod.value, range)
trend.x = data.x
trend.gmv = data.gmv
trend.orders = data.orders
@@ -362,7 +386,10 @@ async function loadRealTime() {
async function loadTopProducts() {
try {
const list = await fetchDashboardTopProducts(selectedPeriod.value, 50)
const range = selectedStartDate.value && selectedEndDate.value
? { start: selectedStartDate.value, end: selectedEndDate.value }
: null
const list = await fetchDashboardTopProducts(selectedPeriod.value, 50, range)
topProducts.splice(0, topProducts.length, ...list)
} catch (e) {
console.error('❌ loadTopProducts failed', e)
@@ -372,7 +399,10 @@ async function loadTopProducts() {
async function loadTopMerchants() {
try {
const list = await fetchDashboardTopMerchants(selectedPeriod.value, 50)
const range = selectedStartDate.value && selectedEndDate.value
? { start: selectedStartDate.value, end: selectedEndDate.value }
: null
const list = await fetchDashboardTopMerchants(selectedPeriod.value, 50, range)
topMerchants.splice(0, topMerchants.length, ...list)
} catch (e) {
console.error('❌ loadTopMerchants failed', e)
@@ -382,7 +412,10 @@ async function loadTopMerchants() {
async function loadUserSegments() {
try {
const list = await fetchDashboardUserSegments(selectedPeriod.value)
const range = selectedStartDate.value && selectedEndDate.value
? { start: selectedStartDate.value, end: selectedEndDate.value }
: null
const list = await fetchDashboardUserSegments(selectedPeriod.value, range)
userSegments.splice(0, userSegments.length, ...list)
} catch (e) {
console.error('❌ loadUserSegments failed', e)
@@ -392,7 +425,10 @@ async function loadUserSegments() {
async function loadTrafficSources() {
try {
const list = await fetchDashboardTrafficSources(selectedPeriod.value)
const range = selectedStartDate.value && selectedEndDate.value
? { start: selectedStartDate.value, end: selectedEndDate.value }
: null
const list = await fetchDashboardTrafficSources(selectedPeriod.value, range)
trafficSources.splice(0, trafficSources.length, ...list)
} catch (e) {
console.error('❌ loadTrafficSources failed', e)
@@ -713,6 +749,24 @@ function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
}
function toggleCustomRange() {
customRangeEnabled.value = !customRangeEnabled.value
}
function onDateRangeApply(range: { start: string; end: string }) {
selectedStartDate.value = range.start
selectedEndDate.value = range.end
customRangeEnabled.value = true
refreshAll()
}
function onDateRangeClear() {
selectedStartDate.value = ''
selectedEndDate.value = ''
customRangeEnabled.value = false
refreshAll()
}
function formatInt(n: number): string {
const v = isFinite(n) ? Math.round(n) : 0
if (v >= 10000) return (v / 10000).toFixed(1) + '万'

View File

@@ -67,7 +67,8 @@
</template>
<script setup lang="uts">
import { onLoad, onShow, reactive, ref } from 'vue'
import { reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'

View File

@@ -94,7 +94,8 @@
</template>
<script setup lang="uts">
import { computed, onLoad, onShow, reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'

View File

@@ -168,7 +168,8 @@
</template>
<script setup lang="uts">
import { computed, onLoad, reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'

View File

@@ -265,6 +265,7 @@ import { ref, onMounted, computed } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import type { UserType } from '@/types/mall-types'
import { formatTime } from '@/utils/utils.uts'
import type { RecentReport, OverviewData, ReportCounts, TodayInsights, TrendDatum } from '@/types/analytics/profile.uts'
@@ -607,20 +608,7 @@ function getReportStatusText(status: any): string {
return statusMap[s] || '未知'
}
function formatTime(dateStr: string): string {
const date = new Date(dateStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const hours = Math.floor(diff / (1000 * 60 * 60))
if (hours < 1) {
return '刚刚'
} else if (hours < 24) {
return `${hours}小时前`
} else {
return `${Math.floor(hours / 24)}天前`
}
}
function changeTrendPeriod(period: string) {
trendPeriod.value = period

View File

@@ -201,12 +201,14 @@
</template>
<script setup lang="uts">
import { onLoad, onShow, reactive, ref } from 'vue'
import { reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import { fetchReport, fetchReportMetrics, fetchReportRows, fetchReportInsights, fetchRelatedReports } from '@/services/analytics/reportDetailService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
import { formatTime } from '@/utils/utils.uts'
import type { ReportType, MetricType, ChartTabType, ChartLegendType, TableColumnType, InsightType } from '@/types/analytics/report-detail.uts'
@@ -233,14 +235,19 @@ const dataInsights = reactive<Array<InsightType>>([])
const relatedReports = reactive<Array<ReportType>>([])
const sortIndex = ref(0)
const sortOptions = ref<Array<string>>([])
const sortOptions: Array<string> = []
const limitIndex = ref(1)
const limitOptions = ref<Array<string>>(['10条', '20条', '50条', '100条'])
const limitOptions: Array<string> = ['10条', '20条', '50条', '100条']
const currentPage = ref(1)
const totalPages = ref(1)
const autoRefresh = ref(false)
const intervalIndex = ref(1)
const intervalOptions = ref<Array<string>>(['1分钟', '5分钟', '10分钟', '30分钟', '1小时'])
const intervalOptions: Array<string> = ['1分钟', '5分钟', '10分钟', '30分钟', '1小时']
const emailNotify = ref(false)
onLoad((options: any) => {
@@ -302,7 +309,7 @@ async function loadReportDetail(reportId: string) {
{ key: 'avg_value', title: '客单价', width: '120rpx', type: 'currency' }
)
sortOptions.value = ['按日期降序', '按销售额降序', '按订单数降序', '按转化率降序']
sortOptions.splice(0, sortOptions.length, '按日期降序', '按销售额降序', '按订单数降序', '按转化率降序')
const rows = await fetchReportRows(reportId)
allRows.splice(0, allRows.length, ...rows)
@@ -326,7 +333,7 @@ async function loadReportDetail(reportId: string) {
function updateTotalPages() {
const total = allRows.length
const limit = parseInt(limitOptions.value[limitIndex.value])
const limit = parseInt(limitOptions[limitIndex.value])
totalPages.value = total > 0 ? Math.ceil(total / limit) : 1
}
@@ -336,7 +343,7 @@ function generateTableData() {
if (total === 0) {
return
}
const limit = parseInt(limitOptions.value[limitIndex.value])
const limit = parseInt(limitOptions[limitIndex.value])
const start = (currentPage.value - 1) * limit
const end = Math.min(start + limit, total)
@@ -377,9 +384,7 @@ function formatMetricValue(value: number, format: string): string {
}
}
function formatTime(timeStr: string): string {
return timeStr.replace('T', ' ').split('.')[0]
}
function getInsightIcon(type: string): string {
const icons: Record<string, string> = {

View File

@@ -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">
@@ -153,7 +168,9 @@ import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import AnalyticsRegionMap from '@/components/analytics/AnalyticsRegionMap.uvue'
import { computed, onLoad, reactive, ref } from 'vue'
import AnalyticsDateRangePicker from '@/components/analytics/AnalyticsDateRangePicker.uvue'
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { fetchSalesKpis, fetchSalesTrend, fetchSalesTopProducts, fetchSalesTopMerchants } from '@/services/analytics/salesReportService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
@@ -162,6 +179,10 @@ import type { SalesTrendData, SalesData, ProductRank, MerchantRank } from '@/typ
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/sales-report')
@@ -212,8 +233,12 @@ async function loadSalesData() {
try {
updateTime()
const range = selectedStartDate.value && selectedEndDate.value
? { start: selectedStartDate.value, end: selectedEndDate.value }
: null
// KPI
const kpi = await fetchSalesKpis(selectedPeriod.value)
const kpi = await fetchSalesKpis(selectedPeriod.value, range)
salesData.gmv = kpi.gmv
salesData.gmv_growth = kpi.gmv_growth
salesData.orders = kpi.orders
@@ -224,19 +249,19 @@ async function loadSalesData() {
salesData.avg_order_growth = kpi.avg_order_growth
// 趋势
const t = await fetchSalesTrend(selectedPeriod.value)
const t = await fetchSalesTrend(selectedPeriod.value, range)
trend.x = t.x
trend.gmv = t.gmv
trend.orders = t.orders
// TOP 商品/商家
const pList = await fetchSalesTopProducts(selectedPeriod.value, 50)
const pList = await fetchSalesTopProducts(selectedPeriod.value, 50, range)
for (let i = 0; i < pList.length; i++) {
pList[i].rank = i + 1
}
topProducts.splice(0, topProducts.length, ...pList)
const mList = await fetchSalesTopMerchants(selectedPeriod.value, 50)
const mList = await fetchSalesTopMerchants(selectedPeriod.value, 50, range)
for (let i = 0; i < mList.length; i++) {
mList[i].rank = i + 1
}
@@ -331,6 +356,24 @@ function handleDropdown() {
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
}
function toggleCustomRange() {
customRangeEnabled.value = !customRangeEnabled.value
}
function onDateRangeApply(range: { start: string; end: string }) {
selectedStartDate.value = range.start
selectedEndDate.value = range.end
customRangeEnabled.value = true
loadSalesData()
}
function onDateRangeClear() {
selectedStartDate.value = ''
selectedEndDate.value = ''
customRangeEnabled.value = false
loadSalesData()
}
</script>
<style>

View File

@@ -214,7 +214,8 @@ import EChartsView from '@/uni_modules/charts/EChartsView.vue'
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
import { computed, onLoad, reactive, ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import type { TimePeriod } from '@/types/analytics/common.uts'
import type { UserData, FunnelStep } from '@/types/analytics/user.uts'
@@ -514,14 +515,6 @@ function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
}
components: {
AnalyticsSidebarMenu,
AnalyticsTopBar,
EChartsView
calcDateRange() {
const now = new Date()
const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
</script>
<style>