数据分析ui补充完善,接入数据库
This commit is contained in:
@@ -148,242 +148,189 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
<script setup lang="uts">
|
||||
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 { fetchSalesKpis, fetchSalesTrend, fetchSalesTopProducts, fetchSalesTopMerchants } from '@/services/analytics/salesReportService.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
import type { TimePeriod } from '@/types/analytics/common.uts'
|
||||
import type { SalesTrendData, SalesData, ProductRank, MerchantRank } from '@/types/analytics/sales.uts'
|
||||
|
||||
type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
|
||||
type SalesData = {
|
||||
gmv: number
|
||||
gmv_growth: number
|
||||
orders: number
|
||||
order_growth: number
|
||||
conversion_rate: number
|
||||
conversion_growth: number
|
||||
avg_order_amount: number
|
||||
avg_order_growth: number
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/sales-report')
|
||||
const loading = ref(false)
|
||||
|
||||
const timePeriods = ref<Array<TimePeriod>>([
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
{ value: '90d', label: '90天' },
|
||||
{ value: '1y', label: '1年' }
|
||||
])
|
||||
|
||||
const salesData = reactive<SalesData>({
|
||||
gmv: 0,
|
||||
gmv_growth: 0,
|
||||
orders: 0,
|
||||
order_growth: 0,
|
||||
conversion_rate: 0,
|
||||
conversion_growth: 0,
|
||||
avg_order_amount: 0,
|
||||
avg_order_growth: 0
|
||||
})
|
||||
|
||||
const trend = reactive<SalesTrendData>({ x: [], gmv: [], orders: [] })
|
||||
const topProducts = reactive<Array<ProductRank>>([])
|
||||
const topMerchants = reactive<Array<MerchantRank>>([])
|
||||
|
||||
const selectedPeriodText = computed((): string => {
|
||||
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
|
||||
return p ? p.label : '7天'
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
updateTime()
|
||||
loadSalesData()
|
||||
})
|
||||
|
||||
function calcDateRange() {
|
||||
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
|
||||
const startDate = new Date(endDate.getTime() - (days - 1) * 24 * 60 * 60 * 1000)
|
||||
return { startDate, endDate, days }
|
||||
}
|
||||
type ProductRank = { id: string; rank: number; name: string; sales: number }
|
||||
type MerchantRank = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AnalyticsComboChart,
|
||||
AnalyticsSidebarMenu,
|
||||
AnalyticsTopBar,
|
||||
AnalyticsRegionMap
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastUpdateTime: '',
|
||||
selectedPeriod: '7d',
|
||||
showMoreMenu: false,
|
||||
showSidebarMenu: false,
|
||||
currentPath: '/pages/mall/analytics/sales-report',
|
||||
loading: false,
|
||||
timePeriods: [
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
{ value: '90d', label: '90天' },
|
||||
{ value: '1y', label: '1年' }
|
||||
],
|
||||
async function loadSalesData() {
|
||||
loading.value = true
|
||||
try {
|
||||
updateTime()
|
||||
|
||||
salesData: {
|
||||
gmv: 0,
|
||||
gmv_growth: 0,
|
||||
orders: 0,
|
||||
order_growth: 0,
|
||||
conversion_rate: 0,
|
||||
conversion_growth: 0,
|
||||
avg_order_amount: 0,
|
||||
avg_order_growth: 0
|
||||
} as SalesData,
|
||||
// KPI
|
||||
const kpi = await fetchSalesKpis(selectedPeriod.value)
|
||||
salesData.gmv = kpi.gmv
|
||||
salesData.gmv_growth = kpi.gmv_growth
|
||||
salesData.orders = kpi.orders
|
||||
salesData.order_growth = kpi.order_growth
|
||||
salesData.conversion_rate = kpi.conversion_rate
|
||||
salesData.conversion_growth = kpi.conversion_growth
|
||||
salesData.avg_order_amount = kpi.avg_order_amount
|
||||
salesData.avg_order_growth = kpi.avg_order_growth
|
||||
|
||||
trend: {
|
||||
x: [] as Array<string>,
|
||||
gmv: [] as Array<number>,
|
||||
orders: [] as Array<number>
|
||||
} as TrendData,
|
||||
// 趋势
|
||||
const t = await fetchSalesTrend(selectedPeriod.value)
|
||||
trend.x = t.x
|
||||
trend.gmv = t.gmv
|
||||
trend.orders = t.orders
|
||||
|
||||
topProducts: [] as Array<ProductRank>,
|
||||
topMerchants: [] as Array<MerchantRank>
|
||||
// TOP 商品/商家
|
||||
const pList = await fetchSalesTopProducts(selectedPeriod.value, 50)
|
||||
for (let i = 0; i < pList.length; i++) {
|
||||
pList[i].rank = i + 1
|
||||
}
|
||||
},
|
||||
topProducts.splice(0, topProducts.length, ...pList)
|
||||
|
||||
computed: {
|
||||
selectedPeriodText(): string {
|
||||
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||
return p ? p.label : '7天'
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.updateTime()
|
||||
this.loadSalesData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
calcDateRange() {
|
||||
const now = new Date()
|
||||
const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const days = this.selectedPeriod === '7d' ? 7 : this.selectedPeriod === '30d' ? 30 : this.selectedPeriod === '90d' ? 90 : 365
|
||||
const startDate = new Date(endDate.getTime() - (days - 1) * 24 * 60 * 60 * 1000)
|
||||
return { startDate, endDate, days }
|
||||
},
|
||||
|
||||
async loadSalesData() {
|
||||
this.loading = true
|
||||
try {
|
||||
this.updateTime()
|
||||
const { startDate, endDate } = this.calcDateRange()
|
||||
|
||||
// 1) KPI
|
||||
this.salesData = await fetchSalesKpis(this.selectedPeriod)
|
||||
|
||||
// 2) 趋势
|
||||
this.trend = await fetchSalesTrend(this.selectedPeriod)
|
||||
|
||||
// 3) TOP 商品/商家
|
||||
const pList = await fetchSalesTopProducts(this.selectedPeriod, 50)
|
||||
|
||||
// 不足 50 条时补齐虚拟数据(真实数据优先,虚拟数据追加)
|
||||
if (pList.length < 50) {
|
||||
const need = 50 - pList.length
|
||||
for (let i = 0; i < need; i++) {
|
||||
const n = pList.length + 1
|
||||
pList.push({
|
||||
id: `fake-product-${n}`,
|
||||
rank: n,
|
||||
name: `示例商品${n}`,
|
||||
sales: Math.max(1, Math.floor(Math.random() * 200) + 1)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 超过 50 的话只保留前 50
|
||||
pList.splice(50)
|
||||
}
|
||||
// 重新修正 rank
|
||||
for (let i = 0; i < pList.length; i++) {
|
||||
pList[i].rank = i + 1
|
||||
}
|
||||
|
||||
this.topProducts = pList
|
||||
|
||||
const mList: Array<MerchantRank> = await fetchSalesTopMerchants(this.selectedPeriod, 50)
|
||||
|
||||
// 不足 50 条时补齐虚拟数据(真实数据优先,虚拟数据追加)
|
||||
if (mList.length < 50) {
|
||||
const need = 50 - mList.length
|
||||
for (let i = 0; i < need; i++) {
|
||||
const n = mList.length + 1
|
||||
mList.push({
|
||||
id: `fake-merchant-${n}`,
|
||||
rank: n,
|
||||
name: `示例商家${n}`,
|
||||
sales: Math.max(1, Math.floor(Math.random() * 50000) + 500),
|
||||
growth: Math.round((Math.random() * 20 - 10) * 10) / 10
|
||||
})
|
||||
}
|
||||
} else {
|
||||
mList.splice(50)
|
||||
}
|
||||
// 重新修正 rank
|
||||
for (let i = 0; i < mList.length; i++) {
|
||||
mList[i].rank = i + 1
|
||||
}
|
||||
|
||||
this.topMerchants = mList
|
||||
|
||||
// 4) 地域分布:由 AnalyticsRegionMap 组件自动处理
|
||||
} catch (e) {
|
||||
console.error('❌ loadSalesData failed', e)
|
||||
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '数据加载失败' }), icon: 'none', duration: 2000 })
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.updateTime()
|
||||
}
|
||||
},
|
||||
|
||||
selectPeriod(p: string) {
|
||||
this.selectedPeriod = p
|
||||
this.loadSalesData()
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.loadSalesData()
|
||||
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
exportReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
updateTime() {
|
||||
const now = new Date()
|
||||
const hh = now.getHours().toString().padStart(2, '0')
|
||||
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||
this.lastUpdateTime = `${hh}:${mm}`
|
||||
},
|
||||
|
||||
formatInt(n: number): string {
|
||||
const v = isFinite(n) ? Math.round(n) : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toString()
|
||||
},
|
||||
|
||||
formatMoney(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toFixed(0)
|
||||
},
|
||||
|
||||
formatPct(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
const sign = v > 0 ? '+' : ''
|
||||
return `${sign}${v.toFixed(1)}%`
|
||||
},
|
||||
|
||||
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' })
|
||||
const mList = await fetchSalesTopMerchants(selectedPeriod.value, 50)
|
||||
for (let i = 0; i < mList.length; i++) {
|
||||
mList[i].rank = i + 1
|
||||
}
|
||||
topMerchants.splice(0, topMerchants.length, ...mList)
|
||||
} catch (e) {
|
||||
console.error('❌ loadSalesData failed', e)
|
||||
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '数据加载失败' }), icon: 'none', duration: 2000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
updateTime()
|
||||
}
|
||||
}
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
loadSalesData()
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
loadSalesData()
|
||||
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
})
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
const now = new Date()
|
||||
const hh = now.getHours().toString().padStart(2, '0')
|
||||
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||
lastUpdateTime.value = `${hh}:${mm}`
|
||||
}
|
||||
|
||||
function formatInt(n: number): string {
|
||||
const v = isFinite(n) ? Math.round(n) : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toString()
|
||||
}
|
||||
|
||||
function formatMoney(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toFixed(0)
|
||||
}
|
||||
|
||||
function formatPct(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
const sign = v > 0 ? '+' : ''
|
||||
return `${sign}${v.toFixed(1)}%`
|
||||
}
|
||||
|
||||
function handleMenu() {
|
||||
showSidebarMenu.value = true
|
||||
}
|
||||
|
||||
function handleSidebarUpdate(visible: boolean) {
|
||||
showSidebarMenu.value = visible
|
||||
}
|
||||
|
||||
function toggleMoreMenu() {
|
||||
showMoreMenu.value = !showMoreMenu.value
|
||||
}
|
||||
|
||||
function closeMoreMenu() {
|
||||
showMoreMenu.value = false
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
uni.showToast({ title: '搜索', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleNotification() {
|
||||
uni.showToast({ title: '通知', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleFullscreen() {
|
||||
uni.showToast({ title: '全屏', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleMobile() {
|
||||
uni.showToast({ title: '移动端', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleDropdown() {
|
||||
uni.showToast({ title: '下拉菜单', icon: 'none' })
|
||||
}
|
||||
|
||||
function handleSettings() {
|
||||
uni.showToast({ title: '设置', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user