界面设计

This commit is contained in:
comlibmb
2026-01-27 11:20:10 +08:00
parent 3fbd9a2b3d
commit a052ac1d7b
7 changed files with 1719 additions and 404 deletions

View File

@@ -0,0 +1,379 @@
<template>
<view class="region-map">
<view class="map-head">
<view class="map-head-left">
<text class="map-title">销售地域分布</text>
<view class="map-switch">
<view
class="map-switch-btn"
:class="{ active: mapType === 'china' }"
@click="switchMapType('china')"
>
中国地图
</view>
<view
class="map-switch-btn"
:class="{ active: mapType === 'world' }"
@click="switchMapType('world')"
>
全国地图
</view>
</view>
</view>
</view>
<view v-if="loading || !chartOption || !chartOption.series || chartOption.series.length === 0" class="chart-loading">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
</view>
<EChartsView v-else class="chart-box" :option="chartOption" :key="'map-' + mapType" />
</view>
</template>
<script lang="uts">
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
type RegionDataItem = { name: string; value: number }
export default {
components: {
EChartsView
},
props: {
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
topMerchants: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
},
data() {
return {
mapType: 'china', // 'china' | 'world'
regionData: [] as Array<RegionDataItem>,
chartOption: {} as any
}
},
watch: {
startDate: { handler() { this.loadData() }, deep: true },
endDate: { handler() { this.loadData() }, deep: true },
topMerchants: { handler() { this.loadData() }, deep: true }
},
mounted() {
this.loadData()
},
methods: {
switchMapType(type: string) {
this.mapType = type
this.buildChartOption()
},
async loadData() {
try {
// 暂时使用模拟数据,后续可以创建 RPC 函数获取真实省份数据
// 基于商家数据生成省份分布(模拟)
const mockProvinces: Array<RegionDataItem> = [
{ name: '广东', value: 0 },
{ name: '北京', value: 0 },
{ name: '上海', value: 0 },
{ name: '浙江', value: 0 },
{ name: '江苏', value: 0 },
{ name: '山东', value: 0 },
{ name: '河南', value: 0 },
{ name: '四川', value: 0 },
{ name: '湖北', value: 0 },
{ name: '湖南', value: 0 },
{ name: '福建', value: 0 },
{ name: '安徽', value: 0 },
{ name: '河北', value: 0 },
{ name: '陕西', value: 0 },
{ name: '江西', value: 0 },
{ name: '重庆', value: 0 },
{ name: '辽宁', value: 0 },
{ name: '云南', value: 0 },
{ name: '广西', value: 0 },
{ name: '山西', value: 0 },
{ name: '内蒙古', value: 0 },
{ name: '贵州', value: 0 },
{ name: '新疆', value: 0 },
{ name: '天津', value: 0 },
{ name: '吉林', value: 0 },
{ name: '黑龙江', value: 0 },
{ name: '海南', value: 0 },
{ name: '甘肃', value: 0 },
{ name: '宁夏', value: 0 },
{ name: '青海', value: 0 },
{ name: '西藏', value: 0 }
]
// 如果有商家数据,可以基于商家数量或 GMV 分配省份
const merchants = this.topMerchants as Array<any>
const totalSales = merchants.reduce((sum: number, m: any) => {
return sum + (Number(m.sales) || 0)
}, 0)
if (totalSales > 0 && merchants.length > 0) {
// 基于商家数量分配省份
const merchantCount = merchants.length
for (let i = 0; i < Math.min(merchantCount, mockProvinces.length); i++) {
const sales = Number(merchants[i].sales) || 0
mockProvinces[i].value = Math.round(sales * (Math.random() * 0.5 + 0.5))
}
} else {
// 使用随机数据
for (let i = 0; i < mockProvinces.length; i++) {
mockProvinces[i].value = Math.round(Math.random() * 100000)
}
}
this.regionData = mockProvinces
this.buildChartOption()
} catch (e) {
console.error('❌ AnalyticsRegionMap loadData failed', e)
this.regionData = []
this.buildChartOption()
}
},
buildChartOption() {
if (!this.regionData || this.regionData.length === 0) {
this.chartOption = {}
return
}
const maxValue = Math.max(...this.regionData.map((d) => d.value), 1)
if (this.mapType === 'china') {
// 中国地图配置(使用 geo 组件,兼容性更好)
this.chartOption = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const name = params.name || '未知'
const value = params.value || 0
return `${name}<br/>销售额: ¥${this.formatMoney(value)}`
}
},
visualMap: {
min: 0,
max: maxValue,
left: 'left',
top: 'bottom',
text: ['高', '低'],
inRange: {
color: ['#e0f2fe', '#0ea5e9', '#0284c7', '#0369a1']
},
calculable: true,
textStyle: {
color: 'rgba(0,0,0,0.65)',
fontSize: 12
}
},
geo: {
map: 'china',
roam: false,
zoom: 1.5,
top: '40%',
itemStyle: {
areaColor: '#f0f0f0',
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
itemStyle: {
areaColor: '#0ea5e9'
},
label: {
show: true,
color: '#111'
}
}
},
series: [
{
name: '销售额',
type: 'map',
map: 'china',
geoIndex: 0,
data: this.regionData,
label: {
show: true,
fontSize: 11,
color: 'rgba(0,0,0,0.75)'
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
itemStyle: {
areaColor: '#0ea5e9',
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
color: '#fff',
fontSize: 12,
fontWeight: 'bold'
}
}
}
]
}
} else {
// 全国地图配置(简化版,使用柱状图展示 TOP 省份)
this.chartOption = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: { left: 60, right: 20, top: 36, bottom: 60 },
xAxis: {
type: 'category',
data: this.regionData
.sort((a, b) => b.value - a.value)
.slice(0, 15)
.map((d) => d.name),
axisLabel: {
rotate: 45,
color: 'rgba(0,0,0,0.55)',
fontSize: 12
}
},
yAxis: {
type: 'value',
axisLabel: {
color: 'rgba(0,0,0,0.55)',
formatter: (value: number) => {
if (value >= 10000) return (value / 10000).toFixed(1) + '万'
return String(Math.round(value))
}
},
splitLine: {
lineStyle: { color: 'rgba(0,0,0,0.06)' }
}
},
series: [
{
name: '销售额',
type: 'bar',
data: this.regionData
.sort((a, b) => b.value - a.value)
.slice(0, 15)
.map((d) => d.value),
barWidth: 18,
itemStyle: {
borderRadius: [6, 6, 0, 0],
color: '#3b82f6'
}
}
]
}
}
// 转换为纯 JS 对象
this.chartOption = this.toPlainObject(this.chartOption)
},
formatMoney(n: number): string {
const v = isFinite(n) ? n : 0
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
return v.toFixed(0)
},
// 工具函数:将 UTS 对象转换为纯 JavaScript 对象
toPlainObject(obj: any): any {
if (obj == null) return null
if (typeof obj !== 'object') return obj
if (Array.isArray(obj)) {
return obj.map((item) => this.toPlainObject(item))
}
const plain: any = {}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key]
if (typeof value === 'function' || key.startsWith('_') || key === 'toJSON') {
continue
}
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
let isSimple = true
for (const k in value) {
if (typeof value[k] === 'object' && value[k] !== null) {
isSimple = false
break
}
}
plain[key] = isSimple ? { ...value } : this.toPlainObject(value)
} else {
plain[key] = value
}
}
}
return plain
}
}
}
</script>
<style>
.region-map {
width: 100%;
}
.map-head {
margin-bottom: 8px;
}
.map-head-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
flex: 1;
}
.map-title {
font-size: 14px;
font-weight: 600;
color: #111;
}
.map-switch {
display: flex;
flex-direction: row;
gap: 4px;
background: #f3f4f6;
border-radius: 8px;
padding: 2px;
}
.map-switch-btn {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
color: rgba(0,0,0,0.65);
background: transparent;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.map-switch-btn.active {
background: #fff;
color: #111;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.chart-box {
width: 100%;
height: 360px;
}
.chart-loading {
width: 100%;
height: 360px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0,0,0,0.45);
font-size: 14px;
}
</style>