feat: ✨ 首页控制台新增登录区域分布
parent
c656d7ac97
commit
80bc18c5fa
|
@ -198,3 +198,63 @@ class DataVViewSet(GenericViewSet):
|
|||
'monthly_active': monthly_active
|
||||
}
|
||||
return DetailResponse(data=data, msg="获取成功")
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
|
||||
def login_region(self, request):
|
||||
"""
|
||||
登录用户区域分布
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
CHINA_PROVINCES = [
|
||||
{'name': '北京', 'code': '110000'},
|
||||
{'name': '天津', 'code': '120000'},
|
||||
{'name': '河北', 'code': '130000'},
|
||||
{'name': '山西', 'code': '140000'},
|
||||
{'name': '内蒙古', 'code': '150000'},
|
||||
{'name': '辽宁', 'code': '210000'},
|
||||
{'name': '吉林', 'code': '220000'},
|
||||
{'name': '黑龙江', 'code': '230000'},
|
||||
{'name': '上海', 'code': '310000'},
|
||||
{'name': '江苏', 'code': '320000'},
|
||||
{'name': '浙江', 'code': '330000'},
|
||||
{'name': '安徽', 'code': '340000'},
|
||||
{'name': '福建', 'code': '350000'},
|
||||
{'name': '江西', 'code': '360000'},
|
||||
{'name': '山东', 'code': '370000'},
|
||||
{'name': '河南', 'code': '410000'},
|
||||
{'name': '湖北', 'code': '420000'},
|
||||
{'name': '湖南', 'code': '430000'},
|
||||
{'name': '广东', 'code': '440000'},
|
||||
{'name': '广西', 'code': '450000'},
|
||||
{'name': '海南', 'code': '460000'},
|
||||
{'name': '重庆', 'code': '500000'},
|
||||
{'name': '四川', 'code': '510000'},
|
||||
{'name': '贵州', 'code': '520000'},
|
||||
{'name': '云南', 'code': '530000'},
|
||||
{'name': '西藏', 'code': '540000'},
|
||||
{'name': '陕西', 'code': '610000'},
|
||||
{'name': '甘肃', 'code': '620000'},
|
||||
{'name': '青海', 'code': '630000'},
|
||||
{'name': '宁夏', 'code': '640000'},
|
||||
{'name': '新疆', 'code': '650000'},
|
||||
{'name': '台湾', 'code': '710000'},
|
||||
{'name': '香港', 'code': '810000'},
|
||||
{'name': '澳门', 'code': '820000'},
|
||||
{'name': '未知区域', 'code': '000000'},
|
||||
]
|
||||
provinces = [x['name'] for x in CHINA_PROVINCES]
|
||||
day = 30
|
||||
today = datetime.datetime.today()
|
||||
seven_days_ago = today - datetime.timedelta(days=day)
|
||||
province_data = LoginLog.objects.filter(create_datetime__gte=seven_days_ago).values('province').annotate(
|
||||
count=Count('id')).order_by('-count')
|
||||
province_dict = {p: 0 for p in provinces}
|
||||
for ele in province_data:
|
||||
if ele.get('province') in province_dict:
|
||||
province_dict[ele.get('province')] += 1
|
||||
else:
|
||||
province_dict['未知区域'] += ele.get('count')
|
||||
data = [{'region': key, 'count': val} for key, val in province_dict.items()]
|
||||
data = sorted(data, key=lambda x: x['count'], reverse=True)
|
||||
return DetailResponse(data=data, msg="获取成功")
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
<template>
|
||||
<el-card
|
||||
class="card-view"
|
||||
:style="{
|
||||
backgroundColor: randomColor(),
|
||||
}"
|
||||
>
|
||||
<div id="region" :style="{width: pxData.wpx+'px',height: pxData.hpx+'px'}"></div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { request } from '@/api/service'
|
||||
|
||||
export default {
|
||||
sort: 7,
|
||||
title: '登录区域分布',
|
||||
name: 'loginRegion',
|
||||
icon: 'el-icon-s-data',
|
||||
description: '登录区域分布详情',
|
||||
height: 28,
|
||||
width: 20,
|
||||
isResizable: true,
|
||||
props: {
|
||||
pxData: {
|
||||
type: Object,
|
||||
require: false,
|
||||
default: () => ({
|
||||
wpx: 0,
|
||||
hpx: 0
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pxData: {
|
||||
handler () {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
this.myChart?.resize({ width: this.pxData.wpx, height: this.pxData.hpx })
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
this.myChart = null
|
||||
return {
|
||||
data: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initGet () {
|
||||
request({
|
||||
url: '/api/system/datav/login_region/'
|
||||
}).then((res) => {
|
||||
this.data = res.data
|
||||
this.drawLine(this.data)
|
||||
})
|
||||
},
|
||||
// 生成一个随机整数
|
||||
randomColor () {
|
||||
const color = ['#fffff']
|
||||
const ran = Math.floor(Math.random() * 4)
|
||||
return color[ran]
|
||||
},
|
||||
drawLine () {
|
||||
// 基于准备好的dom,初始化echarts实例
|
||||
// 绘制图表
|
||||
const xAxisData = this.data.map(item => item.region)
|
||||
const seriesData = this.data.map(item => item.count)
|
||||
const option = {
|
||||
title: {
|
||||
text: '登录区域分布',
|
||||
textStyle: {
|
||||
color: '#666666',
|
||||
fontSize: 14,
|
||||
fontWeight: '600'
|
||||
},
|
||||
left: 'left'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
textStyle: {
|
||||
color: '#666'
|
||||
},
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#999',
|
||||
type: 'dotted',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
formatter: params => {
|
||||
const param = params[0]
|
||||
return `<div style="padding: 8px;"><div style="color: #333;">${param.name}</div><div style="color: #FFA500;">${param.value} 次</div></div>`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['登录区域分布'],
|
||||
textStyle: {
|
||||
color: '#666',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 40,
|
||||
left: 40,
|
||||
right: 65,
|
||||
bottom: 75
|
||||
},
|
||||
xAxis: {
|
||||
data: xAxisData,
|
||||
boundaryGap: true,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#aaa',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
interval: '0',
|
||||
maxInterval: 1,
|
||||
rotate: 0,
|
||||
formatter: function (value) {
|
||||
return value.split('').join('\n')
|
||||
},
|
||||
textStyle: {
|
||||
color: '#333',
|
||||
fontSize: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#aaa',
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#333',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#ddd',
|
||||
type: 'dotted',
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用户注册数',
|
||||
type: 'bar',
|
||||
data: seriesData,
|
||||
barWidth: 16,
|
||||
barGap: 0,
|
||||
barCategoryGap: '20%',
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(0, 128, 255, 1)'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(0, 128, 255, 0.2)'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.myChart.setOption(option)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.myChart = this.$echarts.init(document.getElementById('region'))
|
||||
this.initGet()
|
||||
this.drawLine()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-view {
|
||||
//border-radius: 10px;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -262,7 +262,6 @@ export default {
|
|||
},
|
||||
// 设置实际的宽度和高度
|
||||
containerResizedEvent: function (i, newH, newW, newHPx, newWPx) {
|
||||
console.log(this.layout, 'CONTAINER RESIZED i=' + i + ', H=' + newH + ', W=' + newW + ', H(px)=' + newHPx + ', W(px)=' + newWPx)
|
||||
this.layout.map(val => {
|
||||
if (val.i === i) {
|
||||
this.$set(this.pxData, val.i, {
|
||||
|
|
|
@ -302,6 +302,17 @@ const log = [
|
|||
isResizable: true,
|
||||
element: 'ver',
|
||||
moved: false
|
||||
},
|
||||
{
|
||||
i: 'loginRegion12',
|
||||
x: 0,
|
||||
y: 75,
|
||||
w: 48,
|
||||
h: 30,
|
||||
config: {},
|
||||
isResizable: true,
|
||||
element: 'loginRegion',
|
||||
moved: false
|
||||
}
|
||||
]
|
||||
export default log
|
||||
|
|
Loading…
Reference in New Issue