Files
medical-mall/pages/mall/admin/product/product-statistics/index.uvue
2026-02-06 12:06:33 +08:00

563 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="product-statistic-page">
<!-- 鍟嗗搧姒傚喌澶撮儴 -->
<view class="page-header-row">
<view class="title-wrap">
<text class="page-title">鍟嗗搧姒傚喌</text>
<view class="info-icon">?</view>
</view>
<view class="header-right">
<view class="date-picker-wrap">
<text class="calendar-emoji">馃搮</text>
<text class="date-range">2026/01/04 - 2026/02/02</text>
</view>
<button class="btn-query">鏌ヨ</button>
<button class="btn-export">瀵煎嚭</button>
</view>
</view>
<!-- 缁熻鎸囨爣缃戞牸 (浣跨敤缁熶竴鍝嶅簲寮忕綉鏍? -->
<view class="kpi-grid">
<view v-for="(item, index) in statItems" :key="index" class="stat-card">
<view class="stat-main">
<view class="icon-box" :style="{ backgroundColor: item.bgColor }">
<text class="stat-emoji">{{ item.emoji }}</text>
</view>
<view class="stat-content">
<text class="stat-label">{{ item.label }}</text>
<text class="stat-value">{{ item.value }}</text>
<view class="stat-compare">
<text class="compare-label">鍧忔瘮澧為暱锛?/text>
<text class="compare-val" :class="item.trendClass">
{{ item.compare }}
<text v-if="item.trend === 'up'" class="arrow">鈻?/text>
<text v-else-if="item.trend === 'down'" class="arrow">鈻?/text>
<text v-else>-</text>
</text>
</view>
</view>
</view>
</view>
</view>
<!-- 鍥捐〃鍗$墖 -->
<view class="chart-card">
<view class="chart-header">
<view class="legend-wrap">
<view class="legend-item"><view class="dot purple"></view><text>鍟嗗搧娴忚閲?/text></view>
<view class="legend-item"><view class="dot orange"></view><text>鍟嗗搧璁垮閲?/text></view>
<view class="legend-item"><view class="dot blue"></view><text>鏀粯閲戦</text></view>
<view class="legend-item"><view class="dot green"></view><text>閫€娆鹃噾棰?/text></view>
</view>
<view class="download-icon">
<text class="download-emoji">馃摜</text>
</view>
</view>
<view class="chart-main">
<EChartsView v-if="chartOption != null" :option="chartOption" class="main-chart" />
</view>
</view>
<!-- 鍟嗗搧鎺掕 -->
<view class="ranking-card">
<view class="ranking-header">
<text class="ranking-title">鍟嗗搧鎺掕</text>
<view class="ranking-filters">
<view class="mock-select-wrap">
<text class="select-val">娴忚閲?/text>
<text class="select-arrow">鈻?/text>
</view>
<view class="date-picker-wrap">
<text class="calendar-emoji">馃搮</text>
<text class="date-range">2026/01/04 - 2026/02/02</text>
</view>
<button class="btn-query small">鏌ヨ</button>
</view>
</view>
<view class="ranking-table">
<view class="table-header">
<text class="th col-id">ID</text>
<text class="th col-img">鍟嗗搧鍥剧墖</text>
<text class="th col-name">鍟嗗搧鍚嶇О</text>
<text class="th col-num">娴忚閲?/text>
<text class="th col-num">璁垮鏁?/text>
<text class="th col-num">鍔犺喘浠舵暟</text>
<text class="th col-num">涓嬪崟浠舵暟</text>
<text class="th col-num">鏀粯浠舵暟</text>
<text class="th col-num">鏀粯閲戦</text>
<text class="th col-num">鏀惰棌鏁?/text>
<text class="th col-num wide">璁垮-鏀粯杞寲鐜?%)</text>
</view>
<view v-for="(item, index) in rankingList" :key="index" class="table-row">
<text class="td col-id">{{ item.id }}</text>
<view class="td col-img">
<image class="product-img" :src="item.image" mode="aspectFill" />
</view>
<view class="td col-name">
<text class="product-name-txt">{{ item.name }}</text>
</view>
<text class="td col-num">{{ item.views }}</text>
<text class="td col-num">{{ item.visitors }}</text>
<text class="td col-num">{{ item.cartCount }}</text>
<text class="td col-num">{{ item.orderCount }}</text>
<text class="td col-num">{{ item.payCount }}</text>
<text class="td col-num">{{ item.payAmount }}</text>
<text class="td col-num">{{ item.favCount }}</text>
<text class="td col-num wide">{{ item.conversion }}%</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
const statItems = ref([
{ label: '鍟嗗搧娴忚閲?, value: '7576', compare: '0.93%', trend: 'up', trendClass: 'up-red', bgColor: '#e6f7ff', emoji: '馃憗锔? },
{ label: '鍟嗗搧璁垮閲?, value: '765', compare: '0.79%', trend: 'up', trendClass: 'up-red', bgColor: '#f6ffed', emoji: '馃懁' },
{ label: '鏀粯浠舵暟', value: '322', compare: '-49.52%', trend: 'down', trendClass: 'down-green', bgColor: '#fff7e6', emoji: '馃泹锔? },
{ label: '鏀粯閲戦', value: '443254.62', compare: '-63.62%', trend: 'down', trendClass: 'down-green', bgColor: '#f9f0ff', emoji: '馃挵' },
{ label: '閫€娆句欢鏁?, value: '0', compare: '0.00%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '馃攧' },
{ label: '閫€娆鹃噾棰?, value: '0', compare: '0.00%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '馃挻' }
])
const rankingList = ref([
{
id: 963,
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
name: 'UR2024澶忓鏂版濂宠澶嶅彜绾姘涘洿鎰熶竴瀛楄偐鐭T鎭よUWG440060',
views: 1200,
visitors: 246,
cartCount: 74,
orderCount: 214,
payCount: 180,
payAmount: '11877.49',
favCount: 13,
conversion: 18
},
{
id: 116,
image: 'https://img2.baidu.com/it/u=3775079632,546700868&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
name: '鐖卞鑹烘櫤鑳?濂囬亣LT01 鎶曞奖浠?瀹剁敤鍗у瓒呴珮娓呮墜鏈轰究鎼烘姇褰辨満 (4K瓒呮竻 鏀寔渚ф姇 鎵嬫満鍚屽睆 鍗庝负涓€纰板嵆鎶?',
views: 959,
visitors: 376,
cartCount: 1,
orderCount: 60,
payCount: 29,
payAmount: '26.00',
favCount: 6,
conversion: 7
},
{
id: 48,
image: 'https://img0.baidu.com/it/u=1762118431,3101886131&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
name: '闃胯开杈炬柉瀹樼綉 adidas BBALL CAP COT 鐢峰コ璁粌杩愬姩甯藉瓙FQ5270 浼犲澧ㄦ按钃?浼犲澧ㄦ按钃?鐧?XL',
views: 758,
visitors: 207,
cartCount: 63,
orderCount: 67,
payCount: 17,
payAmount: '1409.30',
favCount: 4,
conversion: 7
},
{
id: 108,
image: 'https://img2.baidu.com/it/u=3033501986,2204481084&fm=253&fmt=auto&app=138&f=JPEG?w=569&h=500',
name: 'FOMIX 铔嬪3妞?杩涘彛澶村眰鐗涚毊姗欒壊鍗曚汉娌欏彂妞匛gg chair璁捐甯堝笀鍗曟鍗曟矙澶村眰鐗涚毊/鍗曟',
views: 730,
visitors: 216,
cartCount: 26999,
orderCount: 327,
payCount: 14,
payAmount: '66197.00',
favCount: 4,
conversion: 6
}
])
const chartOption = ref<any>({})
onMounted(() => {
setTimeout(() => {
initChart()
}, 300)
})
function toPlainObject(obj: any): any {
if (obj == null) return null
if (typeof obj !== 'object') return obj
if (Array.isArray(obj)) {
return obj.map((item: any) : any => toPlainObject(item))
}
const plain: any = {}
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = obj[key]
if (typeof value === 'function' || key.startsWith('_') || key === 'toJSON') {
continue
}
if (value != null && typeof value === 'object') {
plain[key] = toPlainObject(value)
} else {
plain[key] = value
}
}
return plain
}
function initChart() {
const dates = [
'01-04', '01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13',
'01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23',
'01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02'
]
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50, 50, 50, 0.7)',
padding: [10, 15],
textStyle: { color: '#fff' },
formatter: (params: any[]) : string => {
let res = `<div style="font-size:12px; color:#ccc; margin-bottom:5px;">${params[0].name}</div>`
params.forEach(p => {
res += `<div style="display:flex; align-items:center;">
<div style="width:8px; height:8px; border-radius:50%; background:${p.color}; margin-right:8px;"></div>
<span>${p.seriesName}: ${p.value}</span>
</div>`
})
return res
}
},
grid: {
left: '3%',
right: '4%',
bottom: '5%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: dates,
axisLine: { lineStyle: { color: '#f0f0f0' } },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
axisTick: { show: false }
},
yAxis: [
{
type: 'value',
name: '閲戦',
nameTextStyle: { color: '#8c8c8c', padding: [0, 30, 0, 0] },
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
axisLabel: { color: '#8c8c8c' }
},
{
type: 'value',
name: '鏁伴噺',
nameTextStyle: { color: '#8c8c8c', padding: [0, 0, 0, 30] },
splitLine: { show: false },
axisLabel: { color: '#8c8c8c' }
}
],
series: [
{
name: '鍟嗗搧娴忚閲?,
type: 'line',
yAxisIndex: 1,
smooth: true,
showSymbol: false,
itemStyle: { color: '#b37feb' },
lineStyle: { width: 2 },
data: [90, 110, 115, 100, 95, 80, 60, 40, 70, 85, 75, 65, 70, 80, 100, 120, 110, 90, 60, 95, 115, 110, 85, 50, 45, 55, 75]
},
{
name: '鍟嗗搧璁垮閲?,
type: 'line',
yAxisIndex: 1,
smooth: true,
showSymbol: false,
itemStyle: { color: '#ffbb96' },
lineStyle: { width: 2 },
data: [15, 12, 10, 8, 11, 14, 13, 8, 9, 11, 10, 15, 12, 11, 9, 12, 14, 15, 11, 10, 13, 15, 11, 8, 12, 10, 14]
},
{
name: '鏀粯閲戦',
type: 'bar',
barWidth: '25%',
itemStyle: { color: '#1890ff' },
data: [10, 5, 8, 0, 145, 15, 5, 0, 0, 0, 0, 5, 30, 0, 15, 20, 100, 20, 25, 5, 1, 3, 70, 5, 10, 5, 15, 10]
},
{
name: '閫€娆鹃噾棰?,
type: 'bar',
barWidth: '25%',
itemStyle: { color: '#52c41a' },
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
],
markLine: {
silent: true,
symbol: ['none', 'none'],
label: { show: false },
lineStyle: { color: '#bfbfbf', type: 'dashed' },
data: [{ yAxis: 145853.16 }]
}
}
chartOption.value = toPlainObject(option)
}
</script>
<style scoped lang="scss">
.product-statistic-page {
/* padding removed */
}
.page-header-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title-wrap {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.page-title { font-size: 16px; font-weight: bold; color: #333; }
.info-icon {
width: 14px; height: 14px;
border-radius: 50%; border: 1px solid #999;
color: #999; font-size: 10px;
display: flex; align-items: center; justify-content: center;
}
.header-right {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.date-picker-wrap {
background: #fff;
border: 1px solid #d9d9d9;
padding: 6px 12px;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.calendar-emoji { font-size: 14px; }
.date-range { font-size: 14px; color: #595959; }
.btn-query { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; }
.btn-export { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; }
/* stat-grid 宸插簾寮冿紝鐢卞叏灞€ kpi-grid 鎺ョ */
.stat-card {
background: #fff;
border-radius: 8px;
padding: 20px;
min-width: 0;
}
.stat-main {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
}
.icon-box {
width: 48px; height: 48px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
.stat-emoji { font-size: 24px; }
.stat-content {
flex: 1;
display: flex;
flex-direction: column;
}
.stat-label { font-size: 13px; color: #8c8c8c; margin-bottom: 4px; }
.stat-value { font-size: 24px; font-weight: bold; color: #262626; margin-bottom: 4px; }
.stat-compare {
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
}
.compare-label { color: #8c8c8c; }
.up-red { color: #ff4d4f; }
.down-green { color: #52c41a; }
.none-gray { color: #8c8c8c; }
.arrow { font-size: 10px; margin-left: 2px; }
.chart-card {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.chart-header {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
margin-bottom: 30px;
}
.legend-wrap {
display: flex;
flex-direction: row;
gap: 24px;
}
.legend-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
text { font-size: 12px; color: #8c8c8c; }
}
.dot { width: 10px; height: 10px; border-radius: 2px; }
.purple { background-color: #b37feb; }
.orange { background-color: #ffbb96; }
.blue { background-color: #1890ff; }
.green { background-color: #52c41a; }
.download-icon {
position: absolute;
right: 0;
}
.download-emoji { font-size: 18px; opacity: 0.6; }
.chart-main {
height: 400px;
width: 100%;
}
.main-chart { width: 100%; height: 100%; }
/* 鍟嗗搧鎺掕 */
.ranking-card {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-top: 16px;
margin-bottom: 24px;
}
.ranking-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.ranking-title { font-size: 16px; font-weight: bold; color: #333; }
.ranking-filters {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.mock-select-wrap {
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 4px 12px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
min-width: 120px;
}
.select-val { font-size: 14px; color: #595959; flex: 1; }
.select-arrow { font-size: 10px; color: #bfbfbf; }
.btn-query.small { height: 32px; font-size: 13px; margin: 0; }
.ranking-table {
border: 1px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #e6f7ff;
border-bottom: 1px solid #f0f0f0;
}
.th {
padding: 12px 8px;
font-size: 13px;
font-weight: 500;
color: #595959;
text-align: center;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
align-items: stretch;
}
.table-row:last-child { border-bottom: none; }
.td {
padding: 16px 8px;
font-size: 13px;
color: #262626;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
/* 鍒楀搴﹂厤缃?*/
.col-id { width: 60px; }
.col-img { width: 100px; }
.col-name { flex: 1; text-align: left; justify-content: flex-start; }
.col-num { width: 80px; }
.col-num.wide { width: 120px; }
.product-img {
width: 48px;
height: 48px;
background: #f5f5f5;
border-radius: 4px;
}
.product-name-txt {
font-size: 13px;
line-height: 1.5;
color: #262626;
}
</style>