Files
medical-mall/components/analytics/AnalyticsRegionMap.uvue
2026-02-24 10:35:34 +08:00

263 lines
7.4 KiB
Plaintext

<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 == null" class="chart-loading">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
</view>
<EChartsView v-else class="chart-box" :option="chartOption" :key="'map-' + mapType" />
</view>
</template>
<script setup lang="uts">
import { ref, watch, onMounted } from 'vue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
/**
* 销售地域分布组件 (UTS / uni-app-x)
* 1:1 复刻数据分析中心地图分布
*/
type RegionDataItem = { name: string; value: number }
const props = defineProps({
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
topMerchants: { type: Array, default: () => [] as Array<any> },
loading: { type: Boolean, default: false }
})
const mapType = ref('china')
const regionData = ref<Array<RegionDataItem>>([])
const chartOption = ref<any>(null)
// --- 数据处理逻辑 ---
function toPlainObject(obj: any): any {
if (obj == null) return null
if (typeof obj !== 'object') return obj
if (Array.isArray(obj)) {
return (obj as Array<any>).map((item : any) : any => toPlainObject(item))
}
const plain: Record<string, any> = {}
const keys = Object.keys(obj as object)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = (obj as Record<string, any>)[key]
if (typeof value === 'function' || key.startsWith('_') || key === 'toJSON') {
continue
}
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
plain[key] = toPlainObject(value)
} else {
plain[key] = value
}
}
return plain
}
function buildChartOption() {
if (regionData.value.length === 0) {
chartOption.value = null
return
}
const currentData = regionData.value
const maxValue = Math.max(...currentData.map((d: RegionDataItem): number => d.value), 1)
let option: any = {}
if (mapType.value === 'china') {
option = {
tooltip: {
trigger: 'item'
},
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: currentData,
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 {
option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: { left: 60, right: 20, top: 36, bottom: 60 },
xAxis: {
type: 'category',
data: currentData
.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)'
},
splitLine: {
lineStyle: { color: 'rgba(0,0,0,0.06)' }
}
},
series: [
{
name: '销售额',
type: 'bar',
data: currentData
.sort((a, b) => b.value - a.value)
.slice(0, 15)
.map((d) => d.value),
barWidth: 18,
itemStyle: {
borderRadius: [6, 6, 0, 0],
color: '#3b82f6'
}
}
]
}
}
chartOption.value = toPlainObject(option)
}
function loadData() {
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 }
]
const merchants = props.topMerchants as Array<any>
if (merchants.length > 0) {
for (let i = 0; i < Math.min(merchants.length, mockProvinces.length); i++) {
const salesStr = String(merchants[i].sales || '0')
const sales = parseFloat(salesStr)
mockProvinces[i].value = Math.round(sales * (Math.random() * 0.3 + 0.7))
}
} else {
for (let i = 0; i < mockProvinces.length; i++) {
mockProvinces[i].value = Math.round(Math.random() * 100000)
}
}
regionData.value = mockProvinces
buildChartOption()
}
function switchMapType(type: string) {
mapType.value = type
buildChartOption()
}
watch([() => props.startDate, () => props.endDate, () => props.topMerchants], () => {
loadData()
})
onMounted(() => {
loadData()
})
</script>
<style scoped>
.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>