Initial commit
This commit is contained in:
@@ -41,6 +41,11 @@
|
|||||||
<span>{{ row.age }}</span>
|
<span>{{ row.age }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="城市" width="100px" align="center">
|
||||||
|
<template slot-scope="{row}">
|
||||||
|
<span>{{ row.city || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="已报项目" align="center" min-width="200">
|
<el-table-column label="已报项目" align="center" min-width="200">
|
||||||
<template slot-scope="{row}">
|
<template slot-scope="{row}">
|
||||||
<div v-if="row.enrolled_projects && row.enrolled_projects.length > 0" style="text-align: left;">
|
<div v-if="row.enrolled_projects && row.enrolled_projects.length > 0" style="text-align: left;">
|
||||||
@@ -115,6 +120,9 @@
|
|||||||
<el-form-item label="年龄" prop="age">
|
<el-form-item label="年龄" prop="age">
|
||||||
<el-input v-model.number="temp.age" />
|
<el-input v-model.number="temp.age" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="城市" prop="city">
|
||||||
|
<el-input v-model="temp.city" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="地址" prop="address">
|
<el-form-item label="地址" prop="address">
|
||||||
<el-input v-model="temp.address" />
|
<el-input v-model="temp.address" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -219,6 +227,7 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
age: undefined,
|
age: undefined,
|
||||||
|
city: '',
|
||||||
address: '',
|
address: '',
|
||||||
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde',
|
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde',
|
||||||
openid: '',
|
openid: '',
|
||||||
@@ -276,6 +285,7 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
age: undefined,
|
age: undefined,
|
||||||
|
city: '',
|
||||||
address: '',
|
address: '',
|
||||||
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde',
|
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde',
|
||||||
openid: '',
|
openid: '',
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export default {
|
|||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '300px'
|
default: '350px'
|
||||||
},
|
},
|
||||||
chartData: {
|
chartData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ names: [], counts: [], title: '热门机构' })
|
default: () => ({ names: [], counts: [], title: '机构学员' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -50,9 +50,16 @@ export default {
|
|||||||
|
|
||||||
const names = this.chartData.names || []
|
const names = this.chartData.names || []
|
||||||
const counts = this.chartData.counts || []
|
const counts = this.chartData.counts || []
|
||||||
const title = this.chartData.title || '热门机构'
|
const title = this.chartData.title || '机构学员'
|
||||||
|
|
||||||
this.chart.setOption({
|
this.chart.setOption({
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { // 坐标轴指示器,坐标轴触发有效
|
axisPointer: { // 坐标轴指示器,坐标轴触发有效
|
||||||
@@ -60,7 +67,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
top: 10,
|
top: 40,
|
||||||
left: '2%',
|
left: '2%',
|
||||||
right: '2%',
|
right: '2%',
|
||||||
bottom: '3%',
|
bottom: '3%',
|
||||||
@@ -71,12 +78,34 @@ export default {
|
|||||||
data: names,
|
data: names,
|
||||||
axisTick: {
|
axisTick: {
|
||||||
alignWithLabel: true
|
alignWithLabel: true
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
yAxis: [{
|
yAxis: [{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
minInterval: 1, // Ensure integer ticks
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.1)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
series: [{
|
series: [{
|
||||||
@@ -85,7 +114,34 @@ export default {
|
|||||||
stack: 'vistors',
|
stack: 'vistors',
|
||||||
barWidth: '60%',
|
barWidth: '60%',
|
||||||
data: counts,
|
data: counts,
|
||||||
animationDuration: 6000
|
animationDuration: 6000,
|
||||||
|
itemStyle: {
|
||||||
|
color: function(params) {
|
||||||
|
const colorList = [
|
||||||
|
['#4facfe', '#00f2fe'],
|
||||||
|
['#43e97b', '#38f9d7'],
|
||||||
|
['#fa709a', '#fee140'],
|
||||||
|
['#a18cd1', '#fbc2eb'],
|
||||||
|
['#ff9a9e', '#fecfef'],
|
||||||
|
['#667eea', '#764ba2'],
|
||||||
|
['#f093fb', '#f5576c'],
|
||||||
|
['#8ec5fc', '#e0c3fc']
|
||||||
|
];
|
||||||
|
const index = params.dataIndex % colorList.length;
|
||||||
|
const color = colorList[index];
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: color[0] },
|
||||||
|
{ offset: 1, color: color[1] }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
borderRadius: [4, 4, 0, 0] // Rounded top
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowColor: 'rgba(0,0,0,0.3)'
|
||||||
|
}
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ export default {
|
|||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -87,10 +95,26 @@ export default {
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.1)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: ['上周', '本周']
|
data: ['上周', '本周'],
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '上周', itemStyle: {
|
name: '上周', itemStyle: {
|
||||||
@@ -98,7 +122,9 @@ export default {
|
|||||||
color: '#FF005A',
|
color: '#FF005A',
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#FF005A',
|
color: '#FF005A',
|
||||||
width: 2
|
width: 3,
|
||||||
|
shadowColor: 'rgba(255, 0, 90, 0.3)',
|
||||||
|
shadowBlur: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,13 +140,27 @@ export default {
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
normal: {
|
normal: {
|
||||||
color: '#3888fa',
|
color: '#00f2fe',
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#3888fa',
|
color: '#00f2fe',
|
||||||
width: 2
|
width: 3,
|
||||||
|
shadowColor: 'rgba(0, 242, 254, 0.3)',
|
||||||
|
shadowBlur: 10
|
||||||
},
|
},
|
||||||
areaStyle: {
|
areaStyle: {
|
||||||
color: '#f3f8ff'
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [{
|
||||||
|
offset: 0, color: 'rgba(0, 242, 254, 0.3)' // 0% 处的颜色
|
||||||
|
}, {
|
||||||
|
offset: 1, color: 'rgba(0, 242, 254, 0)' // 100% 处的颜色
|
||||||
|
}],
|
||||||
|
global: false // 缺省为 false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
155
admin/client/src/views/dashboard/components/MapChart.vue
Normal file
155
admin/client/src/views/dashboard/components/MapChart.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="className" :style="{height:height,width:width}" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import resize from '../mixins/resize'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [resize],
|
||||||
|
props: {
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: 'chart'
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '100%'
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '700px'
|
||||||
|
},
|
||||||
|
chartData: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
chartData: {
|
||||||
|
deep: true,
|
||||||
|
handler(val) {
|
||||||
|
this.setOptions(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initChart()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (!this.chart) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.chart.dispose()
|
||||||
|
this.chart = null
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initChart() {
|
||||||
|
this.chart = echarts.init(this.$el, 'macarons')
|
||||||
|
|
||||||
|
// Check if map is already registered
|
||||||
|
if (echarts.getMap('china')) {
|
||||||
|
this.setOptions(this.chartData)
|
||||||
|
} else {
|
||||||
|
// Fetch China Map GeoJSON
|
||||||
|
// Using a reliable public source
|
||||||
|
axios.get('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
|
||||||
|
.then(response => {
|
||||||
|
echarts.registerMap('china', response.data)
|
||||||
|
this.setOptions(this.chartData)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load China map data:', error)
|
||||||
|
// Fallback or show error? For now just log.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setOptions(data) {
|
||||||
|
if (!this.chart) return
|
||||||
|
|
||||||
|
this.chart.setOption({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
title: {
|
||||||
|
text: '学员城市分布',
|
||||||
|
left: 'center',
|
||||||
|
top: 20,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: function(params) {
|
||||||
|
if (params.value) {
|
||||||
|
return params.name + ': ' + params.value + '人';
|
||||||
|
}
|
||||||
|
return params.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualMap: {
|
||||||
|
min: 0,
|
||||||
|
max: data.length > 0 ? Math.max(...data.map(d => d.value)) : 100,
|
||||||
|
left: '50',
|
||||||
|
bottom: '50',
|
||||||
|
text: ['高', '低'],
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
calculable: true,
|
||||||
|
inRange: {
|
||||||
|
color: ['#e0ffff', '#006edd']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '学员分布',
|
||||||
|
type: 'map',
|
||||||
|
mapType: 'china',
|
||||||
|
roam: true, // Allow zooming and panning
|
||||||
|
zoom: 1.2,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: function(params) {
|
||||||
|
if (params.value > 0) {
|
||||||
|
return params.name + '\n' + params.value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
areaColor: '#fbc2eb',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
areaColor: 'rgba(20, 41, 87, 0.6)',
|
||||||
|
borderColor: '#4facfe',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
data: data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-row :gutter="40" class="panel-group">
|
<el-row :gutter="40" class="panel-group">
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('students')">
|
<div class="card-panel card-panel-blue" @click="handleSetLineChartData('students')">
|
||||||
<div class="card-panel-icon-wrapper icon-people">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
|
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('organizations')">
|
<div class="card-panel card-panel-purple" @click="handleSetLineChartData('organizations')">
|
||||||
<div class="card-panel-icon-wrapper icon-message">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="education" class-name="card-panel-icon" />
|
<svg-icon icon-class="education" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('projects')">
|
<div class="card-panel card-panel-orange" @click="handleSetLineChartData('projects')">
|
||||||
<div class="card-panel-icon-wrapper icon-money">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="component" class-name="card-panel-icon" />
|
<svg-icon icon-class="component" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('coupons')">
|
<div class="card-panel card-panel-green" @click="handleSetLineChartData('coupons')">
|
||||||
<div class="card-panel-icon-wrapper icon-shopping">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="money" class-name="card-panel-icon" />
|
<svg-icon icon-class="money" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="40" class="panel-group">
|
<el-row :gutter="40" class="panel-group">
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('projects')">
|
<div class="card-panel card-panel-red" @click="handleSetLineChartData('projects')">
|
||||||
<div class="card-panel-icon-wrapper icon-money">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="list" class-name="card-panel-icon" />
|
<svg-icon icon-class="list" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('coupons')">
|
<div class="card-panel card-panel-yellow" @click="handleSetLineChartData('coupons')">
|
||||||
<div class="card-panel-icon-wrapper icon-banner">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="tab" class-name="card-panel-icon" />
|
<svg-icon icon-class="tab" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
|
||||||
<div class="card-panel" @click="handleSetLineChartData('coupons')">
|
<div class="card-panel card-panel-pink" @click="handleSetLineChartData('coupons')">
|
||||||
<div class="card-panel-icon-wrapper icon-video">
|
<div class="card-panel-icon-wrapper">
|
||||||
<svg-icon icon-class="star" class-name="card-panel-icon" />
|
<svg-icon icon-class="star" class-name="card-panel-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-panel-description">
|
<div class="card-panel-description">
|
||||||
@@ -148,96 +148,46 @@ export default {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #666;
|
color: #fff;
|
||||||
background: #fff;
|
background: rgba(255, 255, 255, 0.05);
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
backdrop-filter: blur(10px);
|
||||||
border-color: rgba(0, 0, 0, .05);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px) scale(1.02);
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
.card-panel-icon-wrapper {
|
.card-panel-icon-wrapper {
|
||||||
color: #fff;
|
transform: scale(1.1);
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-people {
|
|
||||||
background: #40c9c6;
|
|
||||||
box-shadow: 0 4px 12px rgba(64, 201, 198, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-message {
|
|
||||||
background: #36a3f7;
|
|
||||||
box-shadow: 0 4px 12px rgba(54, 163, 247, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-money {
|
|
||||||
background: #f4516c;
|
|
||||||
box-shadow: 0 4px 12px rgba(244, 81, 108, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shopping {
|
|
||||||
background: #34bfa3;
|
|
||||||
box-shadow: 0 4px 12px rgba(52, 191, 163, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-banner {
|
|
||||||
background: #ff9800;
|
|
||||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-video {
|
|
||||||
background: #9c27b0;
|
|
||||||
box-shadow: 0 4px 12px rgba(156, 39, 176, 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-people {
|
|
||||||
color: #40c9c6;
|
|
||||||
background: rgba(64, 201, 198, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-message {
|
|
||||||
color: #36a3f7;
|
|
||||||
background: rgba(54, 163, 247, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-money {
|
|
||||||
color: #f4516c;
|
|
||||||
background: rgba(244, 81, 108, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-shopping {
|
|
||||||
color: #34bfa3;
|
|
||||||
background: rgba(52, 191, 163, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-banner {
|
|
||||||
color: #ff9800;
|
|
||||||
background: rgba(255, 152, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-video {
|
|
||||||
color: #9c27b0;
|
|
||||||
background: rgba(156, 39, 176, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-icon-wrapper {
|
.card-panel-icon-wrapper {
|
||||||
float: none;
|
float: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
transition: all 0.38s ease-out;
|
transition: all 0.38s ease-out;
|
||||||
border-radius: 16px;
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-icon {
|
.card-panel-icon {
|
||||||
float: left;
|
float: left;
|
||||||
font-size: 48px;
|
font-size: 32px;
|
||||||
|
fill: #fff !important; /* Force white icon */
|
||||||
|
color: #fff; /* Some svgs use color */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-description {
|
.card-panel-description {
|
||||||
@@ -250,16 +200,44 @@ export default {
|
|||||||
|
|
||||||
.card-panel-text {
|
.card-panel-text {
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-panel-num {
|
.card-panel-num {
|
||||||
font-size: 20px;
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Specific Gradient Backgrounds */
|
||||||
|
.card-panel-blue {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
.card-panel-purple {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-panel-orange {
|
||||||
|
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); /* Lighter peach */
|
||||||
|
/* Let's try stronger orange */
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
.card-panel-green {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
.card-panel-red {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
.card-panel-yellow {
|
||||||
|
background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%);
|
||||||
|
}
|
||||||
|
.card-panel-pink {
|
||||||
|
background: linear-gradient(135deg, #ff0844 0%, #ffb199 100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width:550px) {
|
@media (max-width:550px) {
|
||||||
@@ -272,6 +250,8 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="className" :style="{width:width}">
|
<div :class="className" :style="{height:height,width:width}" />
|
||||||
<div ref="chart" :style="{height:height,width:'100%'}" />
|
|
||||||
<div v-if="legendData && legendData.length > 0" class="chart-legend">
|
|
||||||
<div v-for="(item, index) in legendData" :key="index" class="legend-item">
|
|
||||||
<span class="legend-icon" :style="{background: item.color}"></span>
|
|
||||||
<span class="legend-text">{{ item.name }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -28,7 +20,7 @@ export default {
|
|||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '300px'
|
default: '350px'
|
||||||
},
|
},
|
||||||
chartData: {
|
chartData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -58,15 +50,27 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initChart() {
|
initChart() {
|
||||||
this.chart = echarts.init(this.$refs.chart, 'macarons')
|
this.chart = echarts.init(this.$el, 'macarons')
|
||||||
|
|
||||||
this.chart.setOption({
|
this.chart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '项目人数',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: false
|
left: 'center',
|
||||||
|
bottom: '10',
|
||||||
|
data: this.legendData && this.legendData.map(item => item.name),
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -74,16 +78,27 @@ export default {
|
|||||||
type: 'pie',
|
type: 'pie',
|
||||||
roseType: 'radius',
|
roseType: 'radius',
|
||||||
radius: [15, 95],
|
radius: [15, 95],
|
||||||
center: ['50%', '38%'],
|
center: ['50%', '45%'],
|
||||||
data: this.chartData || [],
|
data: this.chartData || [],
|
||||||
animationEasing: 'cubicInOut',
|
animationEasing: 'cubicInOut',
|
||||||
animationDuration: 2600,
|
animationDuration: 2600,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}'
|
formatter: '{b}',
|
||||||
|
color: '#fff'
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
show: true
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: function(params) {
|
||||||
|
// Custom bright colors
|
||||||
|
const colorList = ['#37a2da', '#32c5e9', '#67e0e3', '#9fe6b8', '#ffdb5c', '#ff9f7f', '#fb7293', '#e062ae', '#e690d1', '#e7bcf3', '#9d96f5', '#8378ea', '#96bfff'];
|
||||||
|
return colorList[params.dataIndex % colorList.length];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,22 +7,22 @@
|
|||||||
@handleSetLineChartData="handleSetLineChartData"
|
@handleSetLineChartData="handleSetLineChartData"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;border-radius:12px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);">
|
|
||||||
<line-chart :chart-data="lineChartData" />
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="32">
|
<el-row :gutter="32">
|
||||||
<el-col :xs="24" :sm="24" :lg="8">
|
<el-col :xs="24" :sm="24" :lg="12">
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" />
|
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" />
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="24" :lg="8">
|
<el-col :xs="24" :sm="24" :lg="12">
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<bar-chart :chart-data="barChartData" />
|
<bar-chart :chart-data="barChartData" />
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<el-row class="map-chart-wrapper">
|
||||||
|
<map-chart :chart-data="mapChartData" />
|
||||||
|
</el-row>
|
||||||
<div v-if="error" class="chart-wrapper">{{ error }}</div>
|
<div v-if="error" class="chart-wrapper">{{ error }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import PanelGroup from './components/PanelGroup'
|
import PanelGroup from './components/PanelGroup'
|
||||||
import LineChart from './components/LineChart'
|
import LineChart from './components/LineChart'
|
||||||
|
import MapChart from './components/MapChart'
|
||||||
import PieChart from './components/PieChart'
|
import PieChart from './components/PieChart'
|
||||||
import BarChart from './components/BarChart'
|
import BarChart from './components/BarChart'
|
||||||
import { getDashboardStats } from '@/api/dashboard'
|
import { getDashboardStats } from '@/api/dashboard'
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
PanelGroup,
|
PanelGroup,
|
||||||
LineChart,
|
LineChart,
|
||||||
|
MapChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
BarChart
|
BarChart
|
||||||
},
|
},
|
||||||
@@ -47,6 +49,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
error: '',
|
error: '',
|
||||||
|
mapChartData: [],
|
||||||
lineChartData: {
|
lineChartData: {
|
||||||
expectedData: [],
|
expectedData: [],
|
||||||
actualData: [],
|
actualData: [],
|
||||||
@@ -65,7 +68,7 @@ export default {
|
|||||||
pieChartData: [],
|
pieChartData: [],
|
||||||
pieChartLegend: [],
|
pieChartLegend: [],
|
||||||
barChartData: {
|
barChartData: {
|
||||||
title: '热门机构',
|
title: '机构学员',
|
||||||
names: [],
|
names: [],
|
||||||
counts: []
|
counts: []
|
||||||
}
|
}
|
||||||
@@ -84,6 +87,7 @@ export default {
|
|||||||
this.allLineChartData = data.line_chart_data
|
this.allLineChartData = data.line_chart_data
|
||||||
// Default to organizations line chart
|
// Default to organizations line chart
|
||||||
this.lineChartData = this.allLineChartData.organizations || this.allLineChartData.students
|
this.lineChartData = this.allLineChartData.organizations || this.allLineChartData.students
|
||||||
|
this.mapChartData = data.map_chart_data
|
||||||
this.pieChartData = data.pie_chart_data
|
this.pieChartData = data.pie_chart_data
|
||||||
this.pieChartLegend = data.pie_chart_legend
|
this.pieChartLegend = data.pie_chart_legend
|
||||||
this.barChartData = data.bar_chart_data
|
this.barChartData = data.bar_chart_data
|
||||||
@@ -106,21 +110,40 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
background-color: rgb(240, 242, 245);
|
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
|
||||||
|
min-height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* Subtle animated background elements could be added here if needed, but gradient is good for now */
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
background: #fff;
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
padding: 16px 16px 0;
|
padding: 16px 16px 0;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.45);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-chart-wrapper {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 16px 16px 0;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width:1024px) {
|
@media (max-width:1024px) {
|
||||||
|
|||||||
18
admin/server/apps/crm/migrations/0031_student_city.py
Normal file
18
admin/server/apps/crm/migrations/0031_student_city.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.23 on 2025-12-09 07:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('crm', '0030_auto_20251209_1207'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='student',
|
||||||
|
name='city',
|
||||||
|
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='城市'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -132,6 +132,7 @@ class Student(models.Model):
|
|||||||
# parent = models.CharField(max_length=50, verbose_name="家长", null=True, blank=True)
|
# parent = models.CharField(max_length=50, verbose_name="家长", null=True, blank=True)
|
||||||
responsible_teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="负责老师", related_name="students")
|
responsible_teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="负责老师", related_name="students")
|
||||||
address = models.CharField(max_length=200, verbose_name="地址", null=True, blank=True)
|
address = models.CharField(max_length=200, verbose_name="地址", null=True, blank=True)
|
||||||
|
city = models.CharField(max_length=50, verbose_name="城市", null=True, blank=True)
|
||||||
# 已经有avatar字段,对应微信头像
|
# 已经有avatar字段,对应微信头像
|
||||||
avatar = models.URLField(verbose_name="微信头像", default="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde")
|
avatar = models.URLField(verbose_name="微信头像", default="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde")
|
||||||
is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class StudentSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Student
|
model = Student
|
||||||
fields = ['id', 'name', 'phone', 'age', 'address', 'avatar', 'wechat_nickname', 'openid', 'teacher', 'teacher_name', 'teaching_center', 'teaching_center_name', 'company_name', 'position', 'status', 'status_display', 'stats', 'enrolled_projects', 'coupons', 'created_at']
|
fields = ['id', 'name', 'phone', 'age', 'city', 'address', 'avatar', 'wechat_nickname', 'openid', 'teacher', 'teacher_name', 'teaching_center', 'teaching_center_name', 'company_name', 'position', 'status', 'status_display', 'stats', 'enrolled_projects', 'coupons', 'created_at']
|
||||||
read_only_fields = ['stats', 'enrolled_projects', 'coupons', 'teaching_center_name', 'teacher_name', 'status_display']
|
read_only_fields = ['stats', 'enrolled_projects', 'coupons', 'teaching_center_name', 'teacher_name', 'status_display']
|
||||||
|
|
||||||
def get_stats(self, obj):
|
def get_stats(self, obj):
|
||||||
|
|||||||
@@ -285,6 +285,35 @@ class DashboardStatsView(APIView):
|
|||||||
for item in coupon_status_counts
|
for item in coupon_status_counts
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 6. Student City Distribution
|
||||||
|
city_counts = Student.objects.values('city').annotate(count=Count('id')).order_by('-count')
|
||||||
|
|
||||||
|
def clean_city_name(name):
|
||||||
|
if not name:
|
||||||
|
return name
|
||||||
|
|
||||||
|
# Mapping from keyword to DataV standard name (Full Names required for DataV GeoJSON)
|
||||||
|
mapping = {
|
||||||
|
'北京': '北京市', '天津': '天津市', '上海': '上海市', '重庆': '重庆市',
|
||||||
|
'河北': '河北省', '山西': '山西省', '辽宁': '辽宁省', '吉林': '吉林省', '黑龙江': '黑龙江省',
|
||||||
|
'江苏': '江苏省', '浙江': '浙江省', '安徽': '安徽省', '福建': '福建省', '江西': '江西省', '山东': '山东省',
|
||||||
|
'河南': '河南省', '湖北': '湖北省', '湖南': '湖南省', '广东': '广东省', '海南': '海南省',
|
||||||
|
'四川': '四川省', '贵州': '贵州省', '云南': '云南省', '陕西': '陕西省', '甘肃': '甘肃省', '青海': '青海省', '台湾': '台湾省',
|
||||||
|
'内蒙古': '内蒙古自治区', '广西': '广西壮族自治区', '西藏': '西藏自治区', '宁夏': '宁夏回族自治区', '新疆': '新疆维吾尔自治区',
|
||||||
|
'香港': '香港特别行政区', '澳门': '澳门特别行政区'
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, full_name in mapping.items():
|
||||||
|
if key in name:
|
||||||
|
return full_name
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
map_chart_data = [
|
||||||
|
{'name': clean_city_name(item['city']), 'value': item['count']}
|
||||||
|
for item in city_counts if item['city']
|
||||||
|
]
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'panel_data': {
|
'panel_data': {
|
||||||
'students': student_count,
|
'students': student_count,
|
||||||
@@ -298,6 +327,7 @@ class DashboardStatsView(APIView):
|
|||||||
'pie_chart_data': pie_chart_data,
|
'pie_chart_data': pie_chart_data,
|
||||||
'pie_chart_legend': pie_chart_legend,
|
'pie_chart_legend': pie_chart_legend,
|
||||||
'coupon_pie_chart_data': coupon_pie_chart_data,
|
'coupon_pie_chart_data': coupon_pie_chart_data,
|
||||||
|
'map_chart_data': map_chart_data,
|
||||||
'bar_chart_data': {
|
'bar_chart_data': {
|
||||||
'title': bar_title,
|
'title': bar_title,
|
||||||
'names': bar_names,
|
'names': bar_names,
|
||||||
@@ -560,7 +590,7 @@ class UserProfileView(APIView):
|
|||||||
return Response({'error': 'Not authenticated'}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({'error': 'Not authenticated'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
payload = request.data or {}
|
payload = request.data or {}
|
||||||
fields = ['name', 'phone', 'age', 'company_name', 'position', 'address', 'wechat_nickname', 'avatar']
|
fields = ['name', 'phone', 'age', 'company_name', 'position', 'address', 'city', 'wechat_nickname', 'avatar']
|
||||||
for f in fields:
|
for f in fields:
|
||||||
if f in payload:
|
if f in payload:
|
||||||
setattr(student, f, payload.get(f))
|
setattr(student, f, payload.get(f))
|
||||||
|
|||||||
BIN
admin/server/media/2025/12/09/61cc289e253b3.jpg
Normal file
BIN
admin/server/media/2025/12/09/61cc289e253b3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 390 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -5,10 +5,12 @@ Page({
|
|||||||
user: {},
|
user: {},
|
||||||
isFormOpen: false,
|
isFormOpen: false,
|
||||||
isDevtools: false,
|
isDevtools: false,
|
||||||
|
region: [],
|
||||||
formData: {
|
formData: {
|
||||||
name: '',
|
name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
age: '',
|
age: '',
|
||||||
|
city: '',
|
||||||
company_name: '',
|
company_name: '',
|
||||||
position: '',
|
position: '',
|
||||||
wechat_nickname: '',
|
wechat_nickname: '',
|
||||||
@@ -17,8 +19,15 @@ Page({
|
|||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
try {
|
try {
|
||||||
const sys = wx.getSystemInfoSync()
|
let isDevtools = false
|
||||||
this.setData({ isDevtools: sys.platform === 'devtools' })
|
if (wx.getDeviceInfo) {
|
||||||
|
const info = wx.getDeviceInfo()
|
||||||
|
isDevtools = info.platform === 'devtools'
|
||||||
|
} else {
|
||||||
|
const sys = wx.getSystemInfoSync()
|
||||||
|
isDevtools = sys.platform === 'devtools'
|
||||||
|
}
|
||||||
|
this.setData({ isDevtools })
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
// 检查是否已登录
|
// 检查是否已登录
|
||||||
const app = getApp();
|
const app = getApp();
|
||||||
@@ -64,10 +73,12 @@ Page({
|
|||||||
},
|
},
|
||||||
initFormData(user) {
|
initFormData(user) {
|
||||||
this.setData({
|
this.setData({
|
||||||
|
region: user.city ? user.city.split(' ') : [],
|
||||||
formData: {
|
formData: {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
phone: user.phone,
|
phone: user.phone,
|
||||||
age: user.age,
|
age: user.age,
|
||||||
|
city: user.city,
|
||||||
company_name: user.company_name,
|
company_name: user.company_name,
|
||||||
position: user.position,
|
position: user.position,
|
||||||
wechat_nickname: user.wechat_nickname,
|
wechat_nickname: user.wechat_nickname,
|
||||||
@@ -86,10 +97,12 @@ Page({
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.setData({
|
this.setData({
|
||||||
user: data,
|
user: data,
|
||||||
|
region: data.city ? data.city.split(' ') : [],
|
||||||
formData: {
|
formData: {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
age: data.age,
|
age: data.age,
|
||||||
|
city: data.city,
|
||||||
company_name: data.company_name,
|
company_name: data.company_name,
|
||||||
position: data.position,
|
position: data.position,
|
||||||
wechat_nickname: data.wechat_nickname,
|
wechat_nickname: data.wechat_nickname,
|
||||||
@@ -114,6 +127,13 @@ Page({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onCityChange(e) {
|
||||||
|
const val = e.detail.value;
|
||||||
|
this.setData({
|
||||||
|
region: val,
|
||||||
|
'formData.city': val.join(' ')
|
||||||
|
});
|
||||||
|
},
|
||||||
handleInput(e) {
|
handleInput(e) {
|
||||||
const field = e.currentTarget.dataset.field;
|
const field = e.currentTarget.dataset.field;
|
||||||
const value = e.detail.value;
|
const value = e.detail.value;
|
||||||
|
|||||||
@@ -87,6 +87,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="label">城市</text>
|
||||||
|
<view class="input-wrap">
|
||||||
|
<picker mode="region" bindchange="onCityChange" value="{{region}}">
|
||||||
|
<view class="picker" style="{{!region.length ? 'color:#808080' : ''}}">
|
||||||
|
{{region.length ? region[0] + ' ' + region[1] + ' ' + region[2] : '请选择城市'}}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="label">公司名称</text>
|
<text class="label">公司名称</text>
|
||||||
<view class="input-wrap">
|
<view class="input-wrap">
|
||||||
|
|||||||
Reference in New Issue
Block a user