Initial commit

This commit is contained in:
admin
2025-12-09 16:27:48 +08:00
parent 1384bb1d4a
commit 602ce92418
15 changed files with 488 additions and 129 deletions

View File

@@ -41,6 +41,11 @@
<span>{{ row.age }}</span>
</template>
</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">
<template slot-scope="{row}">
<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-input v-model.number="temp.age" />
</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-input v-model="temp.address" />
</el-form-item>
@@ -219,6 +227,7 @@ export default {
name: '',
phone: '',
age: undefined,
city: '',
address: '',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde',
openid: '',
@@ -276,6 +285,7 @@ export default {
name: '',
phone: '',
age: undefined,
city: '',
address: '',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde',
openid: '',

View File

@@ -20,11 +20,11 @@ export default {
},
height: {
type: String,
default: '300px'
default: '350px'
},
chartData: {
type: Object,
default: () => ({ names: [], counts: [], title: '热门机构' })
default: () => ({ names: [], counts: [], title: '机构学员' })
}
},
data() {
@@ -50,9 +50,16 @@ export default {
const names = this.chartData.names || []
const counts = this.chartData.counts || []
const title = this.chartData.title || '热门机构'
const title = this.chartData.title || '机构学员'
this.chart.setOption({
title: {
text: title,
textStyle: {
color: '#fff'
},
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器坐标轴触发有效
@@ -60,7 +67,7 @@ export default {
}
},
grid: {
top: 10,
top: 40,
left: '2%',
right: '2%',
bottom: '3%',
@@ -71,12 +78,34 @@ export default {
data: names,
axisTick: {
alignWithLabel: true
},
axisLabel: {
color: '#fff'
},
axisLine: {
lineStyle: {
color: '#fff'
}
}
}],
yAxis: [{
type: 'value',
minInterval: 1, // Ensure integer ticks
axisTick: {
show: false
},
axisLabel: {
color: '#fff'
},
axisLine: {
lineStyle: {
color: '#fff'
}
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
}],
series: [{
@@ -85,7 +114,34 @@ export default {
stack: 'vistors',
barWidth: '60%',
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)'
}
}
}]
})
}

View File

@@ -68,6 +68,14 @@ export default {
boundaryGap: false,
axisTick: {
show: false
},
axisLabel: {
color: '#fff'
},
axisLine: {
lineStyle: {
color: '#fff'
}
}
},
grid: {
@@ -87,10 +95,26 @@ export default {
yAxis: {
axisTick: {
show: false
},
axisLabel: {
color: '#fff'
},
axisLine: {
lineStyle: {
color: '#fff'
}
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
},
legend: {
data: ['上周', '本周']
data: ['上周', '本周'],
textStyle: {
color: '#fff'
}
},
series: [{
name: '上周', itemStyle: {
@@ -98,7 +122,9 @@ export default {
color: '#FF005A',
lineStyle: {
color: '#FF005A',
width: 2
width: 3,
shadowColor: 'rgba(255, 0, 90, 0.3)',
shadowBlur: 10
}
}
},
@@ -114,13 +140,27 @@ export default {
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
color: '#00f2fe',
lineStyle: {
color: '#3888fa',
width: 2
color: '#00f2fe',
width: 3,
shadowColor: 'rgba(0, 242, 254, 0.3)',
shadowBlur: 10
},
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
}
}
}
},

View 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>

View File

@@ -2,8 +2,8 @@
<div>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('students')">
<div class="card-panel-icon-wrapper icon-people">
<div class="card-panel card-panel-blue" @click="handleSetLineChartData('students')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -17,8 +17,8 @@
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('organizations')">
<div class="card-panel-icon-wrapper icon-message">
<div class="card-panel card-panel-purple" @click="handleSetLineChartData('organizations')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="education" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -30,8 +30,8 @@
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('projects')">
<div class="card-panel-icon-wrapper icon-money">
<div class="card-panel card-panel-orange" @click="handleSetLineChartData('projects')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="component" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -43,8 +43,8 @@
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('coupons')">
<div class="card-panel-icon-wrapper icon-shopping">
<div class="card-panel card-panel-green" @click="handleSetLineChartData('coupons')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="money" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -60,8 +60,8 @@
</el-row>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('projects')">
<div class="card-panel-icon-wrapper icon-money">
<div class="card-panel card-panel-red" @click="handleSetLineChartData('projects')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="list" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -73,8 +73,8 @@
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('coupons')">
<div class="card-panel-icon-wrapper icon-banner">
<div class="card-panel card-panel-yellow" @click="handleSetLineChartData('coupons')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="tab" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -86,8 +86,8 @@
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('coupons')">
<div class="card-panel-icon-wrapper icon-video">
<div class="card-panel card-panel-pink" @click="handleSetLineChartData('coupons')">
<div class="card-panel-icon-wrapper">
<svg-icon icon-class="star" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
@@ -148,96 +148,46 @@ export default {
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border-color: rgba(0, 0, 0, .05);
color: #fff;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-radius: 12px;
transition: all 0.3s ease;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
display: flex;
align-items: center;
padding: 0 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
&:hover {
transform: translateY(-5px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
transform: translateY(-5px) scale(1.02);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
.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 {
float: none;
margin: 0;
padding: 16px;
padding: 12px;
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 {
float: left;
font-size: 48px;
font-size: 32px;
fill: #fff !important; /* Force white icon */
color: #fff; /* Some svgs use color */
}
.card-panel-description {
@@ -250,16 +200,44 @@ export default {
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, 0.45);
color: rgba(255, 255, 255, 0.85);
font-size: 16px;
margin-bottom: 12px;
margin-bottom: 8px;
}
.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) {
@@ -272,6 +250,8 @@ export default {
width: 100%;
height: 100%;
margin: 0 !important;
border-radius: 0 !important;
background: transparent !important;
.svg-icon {
display: block;

View File

@@ -1,13 +1,5 @@
<template>
<div :class="className" :style="{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>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
@@ -28,7 +20,7 @@ export default {
},
height: {
type: String,
default: '300px'
default: '350px'
},
chartData: {
type: Array,
@@ -58,15 +50,27 @@ export default {
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart, 'macarons')
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
title: {
text: '项目人数',
left: 'center',
textStyle: {
color: '#fff'
}
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
show: false
left: 'center',
bottom: '10',
data: this.legendData && this.legendData.map(item => item.name),
textStyle: {
color: '#fff'
}
},
series: [
{
@@ -74,16 +78,27 @@ export default {
type: 'pie',
roseType: 'radius',
radius: [15, 95],
center: ['50%', '38%'],
center: ['50%', '45%'],
data: this.chartData || [],
animationEasing: 'cubicInOut',
animationDuration: 2600,
label: {
show: true,
formatter: '{b}'
formatter: '{b}',
color: '#fff'
},
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];
}
}
}
]

View File

@@ -7,22 +7,22 @@
@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-col :xs="24" :sm="24" :lg="8">
<el-col :xs="24" :sm="24" :lg="12">
<div class="chart-wrapper">
<pie-chart :chart-data="pieChartData" :legend-data="pieChartLegend" />
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<el-col :xs="24" :sm="24" :lg="12">
<div class="chart-wrapper">
<bar-chart :chart-data="barChartData" />
</div>
</el-col>
</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>
</div>
@@ -31,6 +31,7 @@
<script>
import PanelGroup from './components/PanelGroup'
import LineChart from './components/LineChart'
import MapChart from './components/MapChart'
import PieChart from './components/PieChart'
import BarChart from './components/BarChart'
import { getDashboardStats } from '@/api/dashboard'
@@ -40,6 +41,7 @@ export default {
components: {
PanelGroup,
LineChart,
MapChart,
PieChart,
BarChart
},
@@ -47,6 +49,7 @@ export default {
return {
loading: true,
error: '',
mapChartData: [],
lineChartData: {
expectedData: [],
actualData: [],
@@ -65,7 +68,7 @@ export default {
pieChartData: [],
pieChartLegend: [],
barChartData: {
title: '热门机构',
title: '机构学员',
names: [],
counts: []
}
@@ -84,6 +87,7 @@ export default {
this.allLineChartData = data.line_chart_data
// Default to organizations line chart
this.lineChartData = this.allLineChartData.organizations || this.allLineChartData.students
this.mapChartData = data.map_chart_data
this.pieChartData = data.pie_chart_data
this.pieChartLegend = data.pie_chart_legend
this.barChartData = data.bar_chart_data
@@ -106,21 +110,40 @@ export default {
<style lang="scss" scoped>
.dashboard-container {
padding: 32px;
background-color: rgb(240, 242, 245);
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
min-height: 100vh;
position: relative;
overflow: hidden;
/* Subtle animated background elements could be added here if needed, but gradient is good for now */
.chart-wrapper {
background: #fff;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
padding: 16px 16px 0;
margin-bottom: 32px;
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;
&: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) {

View 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='城市'),
),
]

View File

@@ -132,6 +132,7 @@ class Student(models.Model):
# 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")
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 = models.URLField(verbose_name="微信头像", default="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde")
is_active = models.BooleanField(default=True, verbose_name="是否活跃")

View File

@@ -77,7 +77,7 @@ class StudentSerializer(serializers.ModelSerializer):
class Meta:
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']
def get_stats(self, obj):

View File

@@ -285,6 +285,35 @@ class DashboardStatsView(APIView):
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({
'panel_data': {
'students': student_count,
@@ -298,6 +327,7 @@ class DashboardStatsView(APIView):
'pie_chart_data': pie_chart_data,
'pie_chart_legend': pie_chart_legend,
'coupon_pie_chart_data': coupon_pie_chart_data,
'map_chart_data': map_chart_data,
'bar_chart_data': {
'title': bar_title,
'names': bar_names,
@@ -560,7 +590,7 @@ class UserProfileView(APIView):
return Response({'error': 'Not authenticated'}, status=status.HTTP_401_UNAUTHORIZED)
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:
if f in payload:
setattr(student, f, payload.get(f))

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -5,10 +5,12 @@ Page({
user: {},
isFormOpen: false,
isDevtools: false,
region: [],
formData: {
name: '',
phone: '',
age: '',
city: '',
company_name: '',
position: '',
wechat_nickname: '',
@@ -17,8 +19,15 @@ Page({
},
onLoad(options) {
try {
const sys = wx.getSystemInfoSync()
this.setData({ isDevtools: sys.platform === 'devtools' })
let isDevtools = false
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) {}
// 检查是否已登录
const app = getApp();
@@ -64,10 +73,12 @@ Page({
},
initFormData(user) {
this.setData({
region: user.city ? user.city.split(' ') : [],
formData: {
name: user.name,
phone: user.phone,
age: user.age,
city: user.city,
company_name: user.company_name,
position: user.position,
wechat_nickname: user.wechat_nickname,
@@ -86,10 +97,12 @@ Page({
.then((data) => {
this.setData({
user: data,
region: data.city ? data.city.split(' ') : [],
formData: {
name: data.name,
phone: data.phone,
age: data.age,
city: data.city,
company_name: data.company_name,
position: data.position,
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) {
const field = e.currentTarget.dataset.field;
const value = e.detail.value;

View File

@@ -87,6 +87,17 @@
</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">
<text class="label">公司名称</text>
<view class="input-wrap">