feat: 省市区存储文字、工厂关联用户账号、大屏图表主题优化
- 省市区字段改为存储文字名称而非编码 - 工厂序列化器新增usernames字段,列表和详情页展示关联用户账号 - 地区分布统计改为仅按省份聚合 - 新增ECharts screen-dark主题,统一配色和字号 - 大屏卡片背景由纯黑改为深蓝渐变 Made-with: Cursor
This commit is contained in:
parent
2ce0d75c41
commit
c6ba742f9d
|
|
@ -7,25 +7,32 @@ class FactorySerializer(serializers.ModelSerializer):
|
|||
工厂序列化器
|
||||
"""
|
||||
material_count = serializers.SerializerMethodField()
|
||||
usernames = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'dealer_name', 'product_category', 'factory_name',
|
||||
'brand', 'province', 'city', 'district',
|
||||
'address', 'website', 'created_at', 'updated_at', 'material_count']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count']
|
||||
'address', 'website', 'created_at', 'updated_at',
|
||||
'material_count', 'usernames']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'material_count', 'usernames']
|
||||
|
||||
def get_material_count(self, obj):
|
||||
"""
|
||||
获取工厂的材料数量
|
||||
"""
|
||||
return obj.materials.count()
|
||||
|
||||
def get_usernames(self, obj):
|
||||
return list(obj.users.values_list('username', flat=True))
|
||||
|
||||
|
||||
class FactoryListSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
工厂列表序列化器(简化版)
|
||||
"""
|
||||
usernames = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Factory
|
||||
fields = ['id', 'factory_name', 'brand', 'province', 'city', 'dealer_name']
|
||||
fields = ['id', 'factory_name', 'brand', 'province', 'city', 'dealer_name', 'usernames']
|
||||
|
||||
def get_usernames(self, obj):
|
||||
return list(obj.users.values_list('username', flat=True))
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ def overview_statistics(request):
|
|||
count=Count('id')
|
||||
).order_by('-count')
|
||||
|
||||
# 按地区的工厂数量分布
|
||||
region_stats = Factory.objects.values('province', 'city').annotate(
|
||||
# 按省份的工厂数量分布
|
||||
region_stats = Factory.objects.values('province').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ def factory_statistics(request):
|
|||
if request.user.role != 'admin':
|
||||
return Response({"detail": "无权访问"}, status=403)
|
||||
|
||||
region_stats = list(Factory.objects.values('province', 'city').annotate(
|
||||
region_stats = list(Factory.objects.values('province').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count'))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.screen-body {
|
||||
.screen-body {
|
||||
margin: 0;
|
||||
background: radial-gradient(circle at top left, #203450 0%, #0c1422 45%, #070b12 100%);
|
||||
color: #e6edf5;
|
||||
|
|
@ -101,11 +101,11 @@
|
|||
}
|
||||
|
||||
.screen-card {
|
||||
background: rgba(7, 14, 24, 0.92);
|
||||
border: 1px solid rgba(159, 226, 255, 0.18);
|
||||
background: linear-gradient(145deg, rgba(20, 38, 62, 0.88), rgba(12, 24, 44, 0.92));
|
||||
border: 1px solid rgba(159, 226, 255, 0.12);
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45);
|
||||
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.35);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -147,9 +147,9 @@
|
|||
.stat-card {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(140deg, rgba(78, 134, 184, 0.35), rgba(8, 18, 28, 0.9));
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 20px rgba(159, 226, 255, 0.08);
|
||||
background: linear-gradient(140deg, rgba(78, 134, 184, 0.3), rgba(16, 30, 52, 0.85));
|
||||
border: 1px solid rgba(159, 226, 255, 0.1);
|
||||
box-shadow: inset 0 0 20px rgba(159, 226, 255, 0.06);
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import * as echarts from 'echarts'
|
||||
|
||||
const COLORS = [
|
||||
'#36d6fc', '#7b6cf6', '#f5a623', '#5dd9a7',
|
||||
'#f06292', '#4fc3f7', '#ffd54f', '#9575cd',
|
||||
'#4db6ac', '#ff8a65'
|
||||
]
|
||||
|
||||
const TEXT = '#c9d7e6'
|
||||
const AXIS = 'rgba(180, 200, 220, 0.55)'
|
||||
const SPLIT = 'rgba(159, 226, 255, 0.06)'
|
||||
|
||||
echarts.registerTheme('screen-dark', {
|
||||
color: COLORS,
|
||||
backgroundColor: 'transparent',
|
||||
textStyle: { color: TEXT, fontSize: 14 },
|
||||
title: { textStyle: { color: TEXT, fontSize: 16 } },
|
||||
legend: {
|
||||
textStyle: { color: TEXT, fontSize: 13 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(12, 22, 38, 0.92)',
|
||||
borderColor: 'rgba(54, 214, 252, 0.25)',
|
||||
textStyle: { color: '#e6edf5', fontSize: 13 }
|
||||
},
|
||||
categoryAxis: {
|
||||
axisLine: { lineStyle: { color: AXIS } },
|
||||
axisTick: { lineStyle: { color: AXIS } },
|
||||
axisLabel: { color: TEXT, fontSize: 13 },
|
||||
splitLine: { lineStyle: { color: SPLIT } }
|
||||
},
|
||||
valueAxis: {
|
||||
axisLine: { lineStyle: { color: AXIS } },
|
||||
axisTick: { lineStyle: { color: AXIS } },
|
||||
axisLabel: { color: TEXT, fontSize: 13 },
|
||||
splitLine: { lineStyle: { color: SPLIT } }
|
||||
},
|
||||
line: {
|
||||
smooth: true,
|
||||
symbolSize: 6
|
||||
},
|
||||
pie: {
|
||||
label: { color: TEXT, fontSize: 13 }
|
||||
}
|
||||
})
|
||||
|
||||
export const THEME = 'screen-dark'
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
<el-descriptions-item label="地区">{{ formatRegion(factory.province, factory.city, factory.district) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址">{{ factory.address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="官网">{{ factory.website }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户账号">{{ (factory.usernames || []).join('、') || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="材料数量">{{ factory.material_count }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
<el-table-column prop="factory_name" label="工厂全称" />
|
||||
<el-table-column prop="brand" label="品牌" />
|
||||
<el-table-column prop="dealer_name" label="经销商" />
|
||||
<el-table-column label="用户账号">
|
||||
<template #default="scope">
|
||||
{{ (scope.row.usernames || []).join('、') || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地区">
|
||||
<template #default="scope">
|
||||
{{ formatRegion(scope.row.province, scope.row.city, scope.row.district) }}
|
||||
|
|
@ -54,6 +59,7 @@
|
|||
<el-cascader
|
||||
v-model="regionValue"
|
||||
:options="regionOptions"
|
||||
:props="{ value: 'label' }"
|
||||
clearable
|
||||
@change="onRegionChange"
|
||||
/>
|
||||
|
|
@ -79,7 +85,7 @@ import { useRouter } from 'vue-router'
|
|||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { regionData } from 'element-china-area-data'
|
||||
import { useAuth } from '@/store/auth'
|
||||
import { formatRegion } from '@/utils/region'
|
||||
import { formatRegion, regionLabel } from '@/utils/region'
|
||||
import { fetchFactories, fetchFactoryDetail, createFactory, updateFactory, deleteFactory } from '@/api/factory'
|
||||
|
||||
const router = useRouter()
|
||||
|
|
@ -154,7 +160,7 @@ const openEdit = async (row) => {
|
|||
currentId.value = row.id
|
||||
const detail = await fetchFactoryDetail(row.id)
|
||||
Object.assign(form, detail)
|
||||
regionValue.value = [detail.province, detail.city, detail.district].filter(Boolean)
|
||||
regionValue.value = [detail.province, detail.city, detail.district].filter(Boolean).map(regionLabel)
|
||||
dialogTitle.value = '编辑工厂'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<template>
|
||||
<template>
|
||||
<div>
|
||||
<div class="screen-grid">
|
||||
<div class="screen-card" style="grid-column: span 6;">
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts'
|
||||
import { THEME } from '@/utils/chartTheme'
|
||||
import { regionLabel, formatRegion } from '@/utils/region'
|
||||
import { fetchFactoryStats } from '@/api/statistics'
|
||||
|
||||
|
|
@ -45,19 +46,23 @@ const onResize = () => charts.forEach((chart) => chart.resize())
|
|||
|
||||
const initCharts = () => {
|
||||
charts = [
|
||||
echarts.init(regionChart.value),
|
||||
echarts.init(categoryChart.value)
|
||||
echarts.init(regionChart.value, THEME),
|
||||
echarts.init(categoryChart.value, THEME)
|
||||
]
|
||||
}
|
||||
|
||||
const updateCharts = (data) => {
|
||||
const regionData = (data.region_stats || []).map((item) => ({
|
||||
name: `${regionLabel(item.province)}-${regionLabel(item.city)}`,
|
||||
name: regionLabel(item.province),
|
||||
value: item.count
|
||||
}))
|
||||
charts[0].setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [{ type: 'pie', radius: ['30%', '70%'], data: regionData }]
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['30%', '70%'],
|
||||
data: regionData
|
||||
}]
|
||||
})
|
||||
|
||||
const factoryStats = data.factory_category_stats || []
|
||||
|
|
@ -76,10 +81,10 @@ const updateCharts = (data) => {
|
|||
|
||||
charts[1].setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { textStyle: { color: '#c9d7e6' } },
|
||||
legend: {},
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||
xAxis: { type: 'category', data: factoriesAxis, axisLabel: { color: '#9fb3c8', rotate: 20 } },
|
||||
yAxis: { type: 'value', axisLabel: { color: '#9fb3c8' } },
|
||||
xAxis: { type: 'category', data: factoriesAxis, axisLabel: { rotate: 20 } },
|
||||
yAxis: { type: 'value' },
|
||||
series
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<template>
|
||||
<template>
|
||||
<div>
|
||||
<div class="toolbar" style="margin-bottom: 16px;">
|
||||
<el-select v-model="subcategory" placeholder="按材料子类筛选" clearable @change="loadData" style="width: 220px">
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts'
|
||||
import { THEME } from '@/utils/chartTheme'
|
||||
import { fetchMaterialStats } from '@/api/statistics'
|
||||
|
||||
const router = useRouter()
|
||||
|
|
@ -62,9 +63,9 @@ const onResize = () => charts.forEach((chart) => chart.resize())
|
|||
|
||||
const initCharts = () => {
|
||||
charts = [
|
||||
echarts.init(starChart.value),
|
||||
echarts.init(advChart.value),
|
||||
echarts.init(sceneChart.value)
|
||||
echarts.init(starChart.value, THEME),
|
||||
echarts.init(advChart.value, THEME),
|
||||
echarts.init(sceneChart.value, THEME)
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -78,15 +79,16 @@ const updateCharts = (data) => {
|
|||
const levels = data.levels || [1, 2, 3]
|
||||
charts[0].setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { textStyle: { color: '#c9d7e6' } },
|
||||
xAxis: { type: 'category', data: levels.map((l) => `${l}星`), axisLabel: { color: '#9fb3c8' } },
|
||||
yAxis: { type: 'value', axisLabel: { color: '#9fb3c8' } },
|
||||
legend: {},
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 30 },
|
||||
xAxis: { type: 'category', data: levels.map((l) => `${l}星`) },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: '质量', type: 'line', data: data.star_stats.quality_level, smooth: true },
|
||||
{ name: '耐久', type: 'line', data: data.star_stats.durability_level, smooth: true },
|
||||
{ name: '环保', type: 'line', data: data.star_stats.eco_level, smooth: true },
|
||||
{ name: '低碳', type: 'line', data: data.star_stats.carbon_level, smooth: true },
|
||||
{ name: '总评', type: 'line', data: data.star_stats.score_level, smooth: true }
|
||||
{ name: '质量', type: 'line', data: data.star_stats.quality_level },
|
||||
{ name: '耐久', type: 'line', data: data.star_stats.durability_level },
|
||||
{ name: '环保', type: 'line', data: data.star_stats.eco_level },
|
||||
{ name: '低碳', type: 'line', data: data.star_stats.carbon_level },
|
||||
{ name: '总评', type: 'line', data: data.star_stats.score_level }
|
||||
]
|
||||
})
|
||||
|
||||
|
|
@ -103,17 +105,20 @@ const updateCharts = (data) => {
|
|||
}))
|
||||
charts[1].setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { textStyle: { color: '#c9d7e6' } },
|
||||
xAxis: { type: 'category', data: advTypes.map((adv) => mapLabel(choices.advantage, adv)), axisLabel: { color: '#9fb3c8' } },
|
||||
yAxis: { type: 'value', axisLabel: { color: '#9fb3c8' } },
|
||||
legend: {},
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 30 },
|
||||
xAxis: { type: 'category', data: advTypes.map((adv) => mapLabel(choices.advantage, adv)) },
|
||||
yAxis: { type: 'value' },
|
||||
series
|
||||
})
|
||||
|
||||
const sceneStats = data.application_scene_stats || []
|
||||
charts[2].setOption({
|
||||
xAxis: { type: 'category', data: sceneStats.map((item) => mapLabel(choices.application_scene, item.application_scene)), axisLabel: { color: '#9fb3c8' } },
|
||||
yAxis: { type: 'value', axisLabel: { color: '#9fb3c8' } },
|
||||
series: [{ type: 'bar', data: sceneStats.map((item) => item.count), itemStyle: { color: '#f2b24c' } }]
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, top: 20, bottom: 30 },
|
||||
xAxis: { type: 'category', data: sceneStats.map((item) => mapLabel(choices.application_scene, item.application_scene)) },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{ type: 'bar', data: sceneStats.map((item) => item.count), barWidth: '50%' }]
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<template>
|
||||
<template>
|
||||
<div>
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts'
|
||||
import { THEME } from '@/utils/chartTheme'
|
||||
import { regionLabel } from '@/utils/region'
|
||||
import { fetchOverviewStats } from '@/api/statistics'
|
||||
|
||||
|
|
@ -71,10 +72,10 @@ const onResize = () => charts.forEach((chart) => chart.resize())
|
|||
|
||||
const initCharts = () => {
|
||||
charts = [
|
||||
echarts.init(majorChart.value),
|
||||
echarts.init(subChart.value),
|
||||
echarts.init(brandChart.value),
|
||||
echarts.init(regionChart.value)
|
||||
echarts.init(majorChart.value, THEME),
|
||||
echarts.init(subChart.value, THEME),
|
||||
echarts.init(brandChart.value, THEME),
|
||||
echarts.init(regionChart.value, THEME)
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -91,26 +92,23 @@ const updateCharts = () => {
|
|||
}))
|
||||
charts[0].setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: majorData
|
||||
}
|
||||
]
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: majorData
|
||||
}]
|
||||
})
|
||||
|
||||
const subData = stats.value.material_subcategory_stats || []
|
||||
charts[1].setOption({
|
||||
grid: { left: 80, right: 20, top: 20, bottom: 20 },
|
||||
xAxis: { type: 'value', axisLabel: { color: '#9fb3c8' } },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: subData.map((item) => item.material_subcategory),
|
||||
axisLabel: { color: '#9fb3c8' }
|
||||
data: subData.map((item) => item.material_subcategory)
|
||||
},
|
||||
series: [
|
||||
{ type: 'bar', data: subData.map((item) => item.count), itemStyle: { color: '#7cb4e3' } }
|
||||
{ type: 'bar', data: subData.map((item) => item.count), barWidth: '60%' }
|
||||
]
|
||||
})
|
||||
|
||||
|
|
@ -119,31 +117,29 @@ const updateCharts = () => {
|
|||
value: item.count
|
||||
}))
|
||||
charts[2].setOption({
|
||||
series: [
|
||||
{
|
||||
type: 'treemap',
|
||||
data: brandData,
|
||||
label: { show: true, color: '#0b121c' },
|
||||
itemStyle: {
|
||||
borderColor: '#0b121c'
|
||||
}
|
||||
}
|
||||
]
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [{
|
||||
type: 'treemap',
|
||||
data: brandData,
|
||||
label: { show: true, color: '#e8f0f8', fontSize: 12 },
|
||||
itemStyle: { borderColor: 'rgba(12, 24, 44, 0.6)', borderWidth: 2 },
|
||||
levels: [{
|
||||
itemStyle: { borderColor: 'rgba(12, 24, 44, 0.8)', borderWidth: 3, gapWidth: 3 }
|
||||
}]
|
||||
}]
|
||||
})
|
||||
|
||||
const regionData = (stats.value.region_stats || []).map((item) => ({
|
||||
name: `${regionLabel(item.province)}-${regionLabel(item.city)}`,
|
||||
name: regionLabel(item.province),
|
||||
value: item.count
|
||||
}))
|
||||
charts[3].setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['30%', '70%'],
|
||||
data: regionData
|
||||
}
|
||||
]
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['30%', '70%'],
|
||||
data: regionData
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue