完善页面8
This commit is contained in:
@@ -21,359 +21,242 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading || !chartOption || !chartOption.series || chartOption.series.length === 0" class="chart-loading">
|
||||
<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 lang="uts">
|
||||
<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 }
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
},
|
||||
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
|
||||
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>
|
||||
.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 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>
|
||||
|
||||
Reference in New Issue
Block a user