继续完善页面
This commit is contained in:
123
components/analytics/AnalyticsMultiLineChart.uvue
Normal file
123
components/analytics/AnalyticsMultiLineChart.uvue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<view class="chart-wrap" :style="{ height: heightPx }">
|
||||
<EChartsView :option="chartOption" class="chart" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EChartsView
|
||||
},
|
||||
props: {
|
||||
xLabels: { type: Array, default: () => [] },
|
||||
series: { type: Array, default: () => [] },
|
||||
height: { type: Number, default: 400 }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
heightPx: '400px',
|
||||
chartOption: {} as any
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
xLabels: { handler() { this.updateOption() }, deep: true },
|
||||
series: { handler() { this.updateOption() }, deep: true },
|
||||
height: {
|
||||
handler() {
|
||||
this.heightPx = `${this.height}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.heightPx = `${this.height}px`
|
||||
setTimeout(() => {
|
||||
this.updateOption()
|
||||
}, 200)
|
||||
},
|
||||
methods: {
|
||||
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
|
||||
},
|
||||
updateOption() {
|
||||
if (!this.xLabels || this.xLabels.length === 0) return
|
||||
|
||||
const seriesData = (this.series as Array<any>).map(s => {
|
||||
return {
|
||||
name: s.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: s.color },
|
||||
lineStyle: { width: 2 },
|
||||
data: s.data
|
||||
}
|
||||
})
|
||||
|
||||
const option = {
|
||||
grid: { left: 50, right: 30, top: 20, bottom: 40 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: {
|
||||
top: 0,
|
||||
left: 'center',
|
||||
itemWidth: 12,
|
||||
itemHeight: 2,
|
||||
textStyle: { fontSize: 12, color: '#333' }
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: this.xLabels,
|
||||
axisLine: { lineStyle: { color: 'rgba(0,0,0,0.1)' } },
|
||||
axisLabel: {
|
||||
color: 'rgba(0,0,0,0.45)',
|
||||
fontSize: 10,
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.05)' } },
|
||||
axisLabel: { color: 'rgba(0,0,0,0.45)', fontSize: 10 }
|
||||
},
|
||||
series: seriesData
|
||||
}
|
||||
this.chartOption = this.toPlainObject(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-wrap { width: 100%; position: relative; overflow: hidden; }
|
||||
.chart { width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
|
||||
</style>
|
||||
176
components/analytics/AnalyticsUserGenderSection.uvue
Normal file
176
components/analytics/AnalyticsUserGenderSection.uvue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<view class="gender-card">
|
||||
<view class="card-header">
|
||||
<text class="title">用户性别比例</text>
|
||||
</view>
|
||||
|
||||
<view class="card-content">
|
||||
<view class="chart-container">
|
||||
<EChartsView :option="chartOption" class="donut-chart" />
|
||||
<view class="center-text">
|
||||
<text class="total-label">总用户数</text>
|
||||
<text class="total-value">{{ totalUsers }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EChartsView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
totalUsers: 525,
|
||||
chartOption: {} as any
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.initChart()
|
||||
}, 200)
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
top: '0%',
|
||||
left: 'center',
|
||||
icon: 'rect',
|
||||
itemWidth: 15,
|
||||
itemHeight: 15,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '性别比例',
|
||||
type: 'pie',
|
||||
radius: ['50%', '75%'],
|
||||
center: ['50%', '60%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: 450, name: '未知', itemStyle: { color: '#999999' } },
|
||||
{ value: 50, name: '男', itemStyle: { color: '#3b82f6' } },
|
||||
{ value: 25, name: '女', itemStyle: { color: '#f97316' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
this.chartOption = this.toPlainObject(option)
|
||||
},
|
||||
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 scoped lang="scss">
|
||||
.gender-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
height: 521px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.donut-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.center-text {
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
242
components/analytics/AnalyticsUserMapTable.uvue
Normal file
242
components/analytics/AnalyticsUserMapTable.uvue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<view class="user-map-card">
|
||||
<view class="card-header">
|
||||
<text class="title">用户地域分布</text>
|
||||
</view>
|
||||
|
||||
<view class="card-content">
|
||||
<!-- 左侧地图 -->
|
||||
<view class="map-section">
|
||||
<EChartsView :option="mapOption" class="map-chart" />
|
||||
</view>
|
||||
|
||||
<!-- 右侧表格 -->
|
||||
<view class="table-section">
|
||||
<view class="table-header">
|
||||
<text class="th province">TOP省份</text>
|
||||
<text class="th count">累积用户数</text>
|
||||
<text class="th count">新增用户数</text>
|
||||
<text class="th count">访客数</text>
|
||||
<text class="th amount">支付金额</text>
|
||||
</view>
|
||||
<scroll-view class="table-body" scroll-y="true">
|
||||
<view v-for="(item, index) in tableData" :key="index" class="table-row">
|
||||
<text class="td province">{{ item.name }}</text>
|
||||
<text class="td count">{{ item.total }}</text>
|
||||
<text class="td count">{{ item.newUsers }}</text>
|
||||
<text class="td count">{{ item.visitors }}</text>
|
||||
<text class="td amount">{{ item.amount }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
|
||||
type RegionData = {
|
||||
name: string
|
||||
total: number
|
||||
newUsers: number
|
||||
visitors: number
|
||||
amount: number
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EChartsView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mapOption: {} as any,
|
||||
tableData: [
|
||||
{ name: '未知', total: 55398, newUsers: 463, visitors: 1112, amount: 287460.5 },
|
||||
{ name: '广东', total: 2697, newUsers: 0, visitors: 9, amount: 0 },
|
||||
{ name: '北京', total: 1058, newUsers: 0, visitors: 4, amount: 0 },
|
||||
{ name: '河南', total: 823, newUsers: 0, visitors: 3, amount: 0 },
|
||||
{ name: '福建', total: 799, newUsers: 0, visitors: 4, amount: 0 },
|
||||
{ name: '山东', total: 792, newUsers: 0, visitors: 0, amount: 0 },
|
||||
{ name: '浙江', total: 780, newUsers: 0, visitors: 1, amount: 0 },
|
||||
{ name: '江苏', total: 779, newUsers: 0, visitors: 4, amount: 0 },
|
||||
{ name: '四川', total: 768, newUsers: 0, visitors: 3, amount: 0 },
|
||||
{ name: '湖南', total: 520, newUsers: 0, visitors: 2, amount: 0 },
|
||||
{ name: '湖北', total: 480, newUsers: 0, visitors: 1, amount: 0 }
|
||||
] as Array<RegionData>
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.initMap()
|
||||
}, 200)
|
||||
},
|
||||
methods: {
|
||||
initMap() {
|
||||
// 提取地图数据
|
||||
const data = this.tableData.filter(it => it.name !== '未知').map(it => ({
|
||||
name: it.name,
|
||||
value: it.total
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}<br/>累积用户: {c}'
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: 3000,
|
||||
left: 'left',
|
||||
top: 'bottom',
|
||||
text: ['高', '低'],
|
||||
inRange: {
|
||||
color: ['#fff7ed', '#fbbf24', '#f59e0b', '#d97706']
|
||||
},
|
||||
calculable: true
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
roam: false,
|
||||
zoom: 1.2,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: '#f97316'
|
||||
},
|
||||
label: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
areaColor: '#f8fafc',
|
||||
borderColor: '#e2e8f0'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用户数',
|
||||
type: 'map',
|
||||
geoIndex: 0,
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.mapOption = this.toPlainObject(option)
|
||||
},
|
||||
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 scoped lang="scss">
|
||||
.user-map-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
.map-section {
|
||||
flex: 4;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
flex: 6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #f8faff;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
&:hover {
|
||||
background: #f9fbff;
|
||||
}
|
||||
}
|
||||
|
||||
.th, .td {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.province { flex: 1.5; text-align: left; padding-left: 15px; }
|
||||
.count { flex: 1.5; }
|
||||
.amount { flex: 2; }
|
||||
|
||||
.th {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.td {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user