数据分析ui补充完善,接入数据库
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
<view class="kpi-card">
|
||||
<text class="kpi-label">热销商品</text>
|
||||
<text class="kpi-value">{{ formatInt(productData.hot_products) }}</text>
|
||||
<text class="kpi-meta">销量 > 100</text>
|
||||
<text class="kpi-meta">销量 > 100</text>
|
||||
</view>
|
||||
<view class="kpi-card">
|
||||
<text class="kpi-label">库存周转率</text>
|
||||
@@ -70,7 +70,7 @@
|
||||
<text class="card-title">商品销售分析</text>
|
||||
<view class="card-head-right">
|
||||
<select class="select" v-model="selectedProductId" @change="handleProductChange">
|
||||
<option v-for="p in realTopProducts" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
<option v-for="p in topProducts" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||
</select>
|
||||
</view>
|
||||
</view>
|
||||
@@ -167,274 +167,307 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
<script setup lang="uts">
|
||||
import { computed, onLoad, reactive, ref } from 'vue'
|
||||
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchProductOverview, fetchTopProducts, fetchProductTrend, fetchCategorySales, fetchStockInsights, fetchPriceTrend, fetchReviewInsights } from '@/services/analytics/productInsightsService.uts'
|
||||
import { computeDateRange, toDateOnly } from '@/services/analytics/dateRange.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
|
||||
type TimePeriod = { value: string; label: string }
|
||||
type ProductData = {
|
||||
total_products: number
|
||||
product_growth: number
|
||||
hot_products: number
|
||||
turnover_rate: number
|
||||
turnover_growth: number
|
||||
avg_stock: number
|
||||
stock_growth: number
|
||||
import type { TimePeriod } from '@/types/analytics/common.uts'
|
||||
import type { ProductData, ProductRank } from '@/types/analytics/product.uts'
|
||||
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/product-insights')
|
||||
|
||||
const timePeriods = ref<Array<TimePeriod>>([
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
{ value: '90d', label: '90天' },
|
||||
{ value: '1y', label: '1年' }
|
||||
])
|
||||
|
||||
const productData = reactive<ProductData>({
|
||||
total_products: 0,
|
||||
product_growth: 0,
|
||||
hot_products: 0,
|
||||
turnover_rate: 0,
|
||||
turnover_growth: 0,
|
||||
avg_stock: 0,
|
||||
stock_growth: 0
|
||||
})
|
||||
|
||||
const topProducts = reactive<Array<ProductRank>>([])
|
||||
|
||||
const salesChartOption = ref<any>({})
|
||||
const categoryChartOption = ref<any>({})
|
||||
const stockChartOption = ref<any>({})
|
||||
const priceChartOption = ref<any>({})
|
||||
const reviewChartOption = ref<any>({})
|
||||
|
||||
const selectedProductId = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const selectedPeriodText = computed((): string => {
|
||||
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
|
||||
return p ? p.label : '7天'
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
updateTime()
|
||||
loadProductData()
|
||||
})
|
||||
|
||||
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}`
|
||||
}
|
||||
type ProductRank = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||
type ProductTrendRow = { date: string; gmv: number; qty: number; orders: number }
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AnalyticsSidebarMenu,
|
||||
AnalyticsTopBar,
|
||||
EChartsView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastUpdateTime: '',
|
||||
selectedPeriod: '7d',
|
||||
showMoreMenu: false,
|
||||
showSidebarMenu: false,
|
||||
currentPath: '/pages/mall/analytics/product-insights',
|
||||
timePeriods: [
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
{ value: '90d', label: '90天' },
|
||||
{ value: '1y', label: '1年' }
|
||||
] as Array<TimePeriod>,
|
||||
function formatInt(n: number): string {
|
||||
const v = isFinite(n) ? Math.round(n) : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toString()
|
||||
}
|
||||
|
||||
productData: {
|
||||
total_products: 0,
|
||||
product_growth: 0,
|
||||
hot_products: 0,
|
||||
turnover_rate: 0,
|
||||
turnover_growth: 0,
|
||||
avg_stock: 0,
|
||||
stock_growth: 0
|
||||
} as ProductData,
|
||||
function formatPct(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
const sign = v > 0 ? '+' : ''
|
||||
return `${sign}${v.toFixed(1)}%`
|
||||
}
|
||||
|
||||
topProducts: [] as Array<ProductRank>,
|
||||
|
||||
salesChartOption: {} as any,
|
||||
categoryChartOption: {} as any,
|
||||
stockChartOption: {} as any,
|
||||
priceChartOption: {} as any,
|
||||
reviewChartOption: {} as any,
|
||||
selectedProductId: '' as string,
|
||||
loading: false
|
||||
async function loadSelectedProductTrend() {
|
||||
try {
|
||||
if (selectedProductId.value == null || selectedProductId.value === '') {
|
||||
salesChartOption.value = {}
|
||||
priceChartOption.value = {}
|
||||
return
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
selectedPeriodText(): string {
|
||||
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||
return p ? p.label : '7天'
|
||||
},
|
||||
realTopProducts(): Array<ProductRank> {
|
||||
return this.topProducts.filter((p) => !String(p.id).startsWith('fake-product-'))
|
||||
const trend = await fetchProductTrend(selectedPeriod.value, selectedProductId.value)
|
||||
const rows: Array<any> = trend as any
|
||||
|
||||
const x: Array<string> = []
|
||||
const gmv: Array<number> = []
|
||||
const qty: Array<number> = []
|
||||
const orders: Array<number> = []
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const d = `${rows[i].date}`
|
||||
x.push(d.slice(5))
|
||||
gmv.push(Number(rows[i].gmv) || 0)
|
||||
qty.push(Number(rows[i].qty) || 0)
|
||||
orders.push(Number(rows[i].orders) || 0)
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.updateTime()
|
||||
this.loadProductData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadSelectedProductTrend() {
|
||||
try {
|
||||
if (this.selectedProductId == null || this.selectedProductId === '') {
|
||||
this.salesChartOption = {}
|
||||
return
|
||||
}
|
||||
|
||||
const trend = await fetchProductTrend(this.selectedPeriod, this.selectedProductId)
|
||||
const rows: Array<any> = trend as any
|
||||
|
||||
const x: Array<string> = []
|
||||
const gmv: Array<number> = []
|
||||
const qty: Array<number> = []
|
||||
const orders: Array<number> = []
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const d = `${rows[i].date}`
|
||||
x.push(d.slice(5))
|
||||
gmv.push(Number(rows[i].gmv) || 0)
|
||||
qty.push(Number(rows[i].qty) || 0)
|
||||
orders.push(Number(rows[i].orders) || 0)
|
||||
}
|
||||
|
||||
// 组合图:GMV(柱,左轴) + 件数/订单(线,右轴)
|
||||
this.salesChartOption = {
|
||||
grid: { left: 50, right: 50, top: 20, bottom: 46 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['GMV', '件数', '订单数'], bottom: 0 },
|
||||
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: [
|
||||
{ type: 'value', name: 'GMV', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
|
||||
{ type: 'value', name: '件/单', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { show: false } }
|
||||
],
|
||||
series: [
|
||||
{ name: 'GMV', type: 'bar', data: gmv, barWidth: 14, itemStyle: { borderRadius: 6 } },
|
||||
{ name: '件数', type: 'line', yAxisIndex: 1, data: qty, smooth: true, symbolSize: 6 },
|
||||
{ name: '订单数', type: 'line', yAxisIndex: 1, data: orders, smooth: true, symbolSize: 6 }
|
||||
]
|
||||
}
|
||||
|
||||
// 价格趋势:计算均价
|
||||
const avgPrice: Array<number> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const g = Number(rows[i].gmv) || 0
|
||||
const q = Number(rows[i].qty) || 0
|
||||
avgPrice.push(q > 0 ? g / q : 0)
|
||||
}
|
||||
this.priceChartOption = {
|
||||
grid: { left: 40, right: 18, top: 20, bottom: 40 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: { type: 'value', name: '均价', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
|
||||
series: [{ name: '均价', type: 'line', data: avgPrice, smooth: true, symbolSize: 6, color: '#f97316' }]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('loadSelectedProductTrend failed', e)
|
||||
this.salesChartOption = {}
|
||||
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '加载商品趋势失败' }), icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
handleProductChange() {
|
||||
this.loadSelectedProductTrend()
|
||||
},
|
||||
|
||||
|
||||
async loadProductData() {
|
||||
this.loading = true
|
||||
try {
|
||||
this.updateTime()
|
||||
|
||||
const [overview, topList, catRows, stockRows, priceRows, reviewRows] = await Promise.all([
|
||||
fetchProductOverview(this.selectedPeriod),
|
||||
fetchTopProducts(this.selectedPeriod, 10),
|
||||
fetchCategorySales(this.selectedPeriod),
|
||||
fetchStockInsights(this.selectedPeriod),
|
||||
fetchPriceTrend(this.selectedPeriod),
|
||||
fetchReviewInsights()
|
||||
])
|
||||
|
||||
this.productData = overview
|
||||
|
||||
// 不足 10 条时补齐虚拟数据(真实数据优先,虚拟数据追加)
|
||||
const top = topList.slice()
|
||||
if (top.length < 10) {
|
||||
const need = 10 - top.length
|
||||
for (let i = 0; i < need; i++) {
|
||||
const n = top.length + 1
|
||||
top.push({
|
||||
id: `fake-product-${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 {
|
||||
top.splice(10)
|
||||
}
|
||||
for (let i = 0; i < top.length; i++) top[i].rank = i + 1
|
||||
this.topProducts = top
|
||||
|
||||
if ((this.selectedProductId == null || this.selectedProductId === '') && top.length > 0) {
|
||||
const real = top.find((it) => !String(it.id).startsWith('fake-product-'))
|
||||
this.selectedProductId = real ? real.id : ''
|
||||
}
|
||||
|
||||
if (this.selectedProductId == null || this.selectedProductId === '') {
|
||||
this.salesChartOption = {}
|
||||
} else {
|
||||
await this.loadSelectedProductTrend()
|
||||
}
|
||||
|
||||
this.buildCategoryChart(catRows)
|
||||
this.buildStockChart(stockRows)
|
||||
// priceChartOption 在 loadSelectedProductTrend 里会生成均价趋势;这里仍保留整体价格趋势图(如果你有对应图表函数可以接入)
|
||||
this.buildReviewChart(reviewRows)
|
||||
|
||||
this.updateTime()
|
||||
} catch (e) {
|
||||
console.error('loadProductData 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.loadProductData()
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.loadProductData()
|
||||
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()
|
||||
},
|
||||
|
||||
formatPct(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
const sign = v > 0 ? '+' : ''
|
||||
return `${sign}${v.toFixed(1)}%`
|
||||
},
|
||||
|
||||
buildChartOptions() {
|
||||
// TODO: 构建图表配置
|
||||
this.salesChartOption = {}
|
||||
this.categoryChartOption = {}
|
||||
this.stockChartOption = {}
|
||||
this.priceChartOption = {}
|
||||
this.reviewChartOption = {}
|
||||
},
|
||||
|
||||
handleMenu() {
|
||||
this.showSidebarMenu = true
|
||||
},
|
||||
handleSidebarUpdate(visible: boolean) {
|
||||
this.showSidebarMenu = visible
|
||||
},
|
||||
|
||||
toggleMoreMenu() {
|
||||
this.showMoreMenu = !this.showMoreMenu
|
||||
},
|
||||
|
||||
closeMoreMenu() {
|
||||
this.showMoreMenu = false
|
||||
salesChartOption.value = {
|
||||
grid: { left: 50, right: 50, top: 20, bottom: 46 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['GMV', '件数', '订单数'], bottom: 0 },
|
||||
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: [
|
||||
{ type: 'value', name: 'GMV', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
|
||||
{ type: 'value', name: '件/单', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { show: false } }
|
||||
],
|
||||
series: [
|
||||
{ name: 'GMV', type: 'bar', data: gmv, barWidth: 14, itemStyle: { borderRadius: 6 } },
|
||||
{ name: '件数', type: 'line', yAxisIndex: 1, data: qty, smooth: true, symbolSize: 6 },
|
||||
{ name: '订单数', type: 'line', yAxisIndex: 1, data: orders, smooth: true, symbolSize: 6 }
|
||||
]
|
||||
}
|
||||
|
||||
const avgPrice: Array<number> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const g = Number(rows[i].gmv) || 0
|
||||
const q = Number(rows[i].qty) || 0
|
||||
avgPrice.push(q > 0 ? g / q : 0)
|
||||
}
|
||||
|
||||
priceChartOption.value = {
|
||||
grid: { left: 40, right: 18, top: 20, bottom: 40 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: { type: 'value', name: '均价', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
|
||||
series: [{ name: '均价', type: 'line', data: avgPrice, smooth: true, symbolSize: 6, color: '#f97316' }]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('loadSelectedProductTrend failed', e)
|
||||
salesChartOption.value = {}
|
||||
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '加载商品趋势失败' }), icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function handleProductChange() {
|
||||
loadSelectedProductTrend()
|
||||
}
|
||||
|
||||
function buildCategoryChart(catRows: any) {
|
||||
const rows: Array<any> = Array.isArray(catRows) ? (catRows as Array<any>) : []
|
||||
const names: Array<string> = []
|
||||
const values: Array<number> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
names.push(`${rows[i].category_name ?? '未分类'}`)
|
||||
values.push(Number(rows[i].total_sales) || 0)
|
||||
}
|
||||
|
||||
categoryChartOption.value = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 60, right: 18, top: 20, bottom: 40 },
|
||||
xAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: { type: 'category', data: names, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
series: [{ type: 'bar', data: values, barWidth: 14, itemStyle: { borderRadius: 6 } }]
|
||||
}
|
||||
}
|
||||
|
||||
function buildStockChart(stockRows: any) {
|
||||
const rows: Array<any> = Array.isArray(stockRows) ? (stockRows as Array<any>) : []
|
||||
const names: Array<string> = []
|
||||
const values: Array<number> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
names.push(`${rows[i].bucket ?? ''}`)
|
||||
values.push(Number(rows[i].value) || 0)
|
||||
}
|
||||
|
||||
stockChartOption.value = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 60, right: 18, top: 20, bottom: 40 },
|
||||
xAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: { type: 'category', data: names, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
series: [{ type: 'bar', data: values, barWidth: 14, itemStyle: { borderRadius: 6 } }]
|
||||
}
|
||||
}
|
||||
|
||||
function buildReviewChart(reviewRows: any) {
|
||||
const rows: Array<any> = Array.isArray(reviewRows) ? (reviewRows as Array<any>) : []
|
||||
const names: Array<string> = []
|
||||
const values: Array<number> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
names.push(`${rows[i].rating ?? ''}`)
|
||||
values.push(Number(rows[i].count) || 0)
|
||||
}
|
||||
|
||||
reviewChartOption.value = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 18, top: 20, bottom: 40 },
|
||||
xAxis: { type: 'category', data: names, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
yAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' } },
|
||||
series: [{ type: 'bar', data: values, barWidth: 14, itemStyle: { borderRadius: 6 } }]
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProductData() {
|
||||
loading.value = true
|
||||
try {
|
||||
updateTime()
|
||||
|
||||
const [overview, topList, catRows, stockRows, _priceRows, reviewRows] = await Promise.all([
|
||||
fetchProductOverview(selectedPeriod.value),
|
||||
fetchTopProducts(selectedPeriod.value, 10),
|
||||
fetchCategorySales(selectedPeriod.value),
|
||||
fetchStockInsights(selectedPeriod.value),
|
||||
fetchPriceTrend(selectedPeriod.value),
|
||||
fetchReviewInsights()
|
||||
])
|
||||
|
||||
productData.total_products = overview.total_products
|
||||
productData.product_growth = overview.product_growth
|
||||
productData.hot_products = overview.hot_products
|
||||
productData.turnover_rate = overview.turnover_rate
|
||||
productData.turnover_growth = overview.turnover_growth
|
||||
productData.avg_stock = overview.avg_stock
|
||||
productData.stock_growth = overview.stock_growth
|
||||
|
||||
const top = topList.slice()
|
||||
for (let i = 0; i < top.length; i++) top[i].rank = i + 1
|
||||
topProducts.splice(0, topProducts.length, ...top)
|
||||
|
||||
if ((selectedProductId.value == null || selectedProductId.value === '') && top.length > 0) {
|
||||
const real = top.find((it) => !String(it.id).startsWith('fake-product-'))
|
||||
selectedProductId.value = real ? real.id : ''
|
||||
}
|
||||
|
||||
if (selectedProductId.value == null || selectedProductId.value === '') {
|
||||
salesChartOption.value = {}
|
||||
} else {
|
||||
await loadSelectedProductTrend()
|
||||
}
|
||||
|
||||
buildCategoryChart(catRows)
|
||||
buildStockChart(stockRows)
|
||||
buildReviewChart(reviewRows)
|
||||
|
||||
updateTime()
|
||||
} catch (e) {
|
||||
console.error('loadProductData failed', e)
|
||||
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '商品洞察数据加载失败' }), icon: 'none', duration: 2000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
updateTime()
|
||||
}
|
||||
}
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
loadProductData()
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
loadProductData()
|
||||
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -561,6 +594,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -700,12 +734,62 @@ export default {
|
||||
color: rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
.card-head-right {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
/* 排行列表 */
|
||||
.chart-box-sm {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.chart-loading {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(0,0,0,0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chart-loading-sm {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.grid-col-item {
|
||||
flex: 1 1 calc(50% - 6px);
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.rank-list-scroll {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -742,11 +826,6 @@ export default {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.rank-val {
|
||||
font-size: 13px;
|
||||
color: rgba(0,0,0,0.65);
|
||||
}
|
||||
|
||||
.rank-right {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
@@ -754,6 +833,11 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rank-val {
|
||||
font-size: 13px;
|
||||
color: rgba(0,0,0,0.65);
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
@@ -770,24 +854,25 @@ export default {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media screen and (min-width: 960px) {
|
||||
.kpi-card {
|
||||
flex: 1 1 calc(25% - 9px);
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
.grid-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grid-col-item {
|
||||
flex: 1 1 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
|
||||
.topbar-right .btn-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
.more-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
@@ -798,7 +883,7 @@ export default {
|
||||
.page-layout {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user