700 lines
17 KiB
Vue
700 lines
17 KiB
Vue
<template>
|
||
<div class="personnel p-2 h-full flex flex-col">
|
||
<div class="flex flex-items-center searchBox">
|
||
<div class="searchLabel">选择日期</div>
|
||
<el-date-picker
|
||
v-model="date"
|
||
placeholder="选择日期"
|
||
style="width: 400px; margin-right: 10px"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
></el-date-picker>
|
||
<el-button type="primary" @click="querySelect">查询</el-button>
|
||
<el-button @click="reset">重置</el-button>
|
||
</div>
|
||
|
||
<el-row :gutter="20" style="position: relative; z-index: 2">
|
||
<el-col :lg="8" :xl="8">
|
||
<div class="cardbox">
|
||
<div>入住情况</div>
|
||
<div class="cardBody" style="height: 200px">
|
||
<div class="card_LeftBox">
|
||
<div class="valueBox">{{ MoveIn.totalResident }}</div>
|
||
<div class="labelBox">入住人数</div>
|
||
</div>
|
||
<div class="echartsBox h-full">
|
||
<Echarts :options="options as EChartsOption"></Echarts>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :lg="10" :xl="8">
|
||
<div class="cardbox">
|
||
<div>住户类型情况</div>
|
||
<div class="cardBody w-full" style="height: 180px">
|
||
<Echarts :options="options1 as EChartsOption"></Echarts>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :lg="6" :xl="8">
|
||
<div class="cardbox">
|
||
<div>男女比例</div>
|
||
<div class="cardBody w-full" style="height: 180px">
|
||
<Echarts :options="options2 as EChartsOption"></Echarts>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20" style="margin-top: 20px; position: relative; z-index: 2">
|
||
<el-col :lg="24" :xl="24">
|
||
<div class="cardbox w-full" style="height: 400px">
|
||
<Echarts :options="options3"></Echarts>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row style="margin-top: 20px; position: relative; z-index: 2">
|
||
<el-col :span="24">
|
||
<div class="cardbox w-full" style="height: 450px">
|
||
<Echarts :options="flowLineOption as EChartsOption"></Echarts>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref } from 'vue';
|
||
import Echarts from '@/components/Echarts/index.vue';
|
||
import { EChartsOption } from 'echarts';
|
||
import { getLastNDays } from '@/utils/time';
|
||
import { get_movein_moveout_list, get_movein_user_sex, get_movein_user_type, get_person_age, user_movein_list } from '@/api/system/Personnel';
|
||
import { useBgColor } from '@/store/modules/bgColor';
|
||
|
||
const BgStore = useBgColor();
|
||
const date = ref('');
|
||
|
||
onMounted(() => {
|
||
BgStore.toggleBgColor(true);
|
||
});
|
||
onUnmounted(() => {
|
||
BgStore.toggleBgColor(false);
|
||
});
|
||
|
||
function reset() {
|
||
date.value = getLastNDays(7)[0];
|
||
querySelect();
|
||
}
|
||
|
||
/** 入住情况 */
|
||
const options = ref<EChartsOption>({
|
||
tooltip: { show: true, trigger: 'item' },
|
||
series: [
|
||
// 1. 底层细圆环(灰色底色)
|
||
{
|
||
name: '总房屋',
|
||
type: 'pie',
|
||
radius: ['68%', '90%'], // 细一点
|
||
clockwise: true,
|
||
data: [100],
|
||
itemStyle: { color: '#ebeef5' },
|
||
emphasis: { disabled: true },
|
||
tooltip: { show: false }, // 关键:不显示提示
|
||
label: { show: false }
|
||
},
|
||
// 2. 上层粗圆环(进度条)
|
||
{
|
||
name: '入住房屋',
|
||
type: 'pie',
|
||
radius: ['65%', '98%'], // 粗一点
|
||
clockwise: true,
|
||
itemStyle: {
|
||
color: '#409eff',
|
||
borderRadius: 2
|
||
},
|
||
emphasis: { disabled: true },
|
||
label: {
|
||
show: true,
|
||
position: 'center',
|
||
fontSize: 18,
|
||
fontWeight: 'bold',
|
||
color: '#333',
|
||
// 【新增】富文本样式配置,可以分别控制两行的样式
|
||
rich: {
|
||
rate: {
|
||
color: 'rgba(63, 223, 149, 1)',
|
||
fontSize: 20, // 百分比用主题色
|
||
padding: [0, 0, 10, 0] // 下边距,拉开与文字的距离
|
||
},
|
||
title: {
|
||
fontSize: 14,
|
||
color: 'rgba(0, 0, 0, 1)' // 文字用灰色
|
||
}
|
||
}
|
||
},
|
||
data: [
|
||
{ value: 85 },
|
||
{
|
||
value: 15,
|
||
itemStyle: { color: 'transparent' }
|
||
}
|
||
]
|
||
}
|
||
]
|
||
});
|
||
|
||
/** 住户类型 */
|
||
const options1 = ref<EChartsOption>({
|
||
legend: {
|
||
top: 'center',
|
||
left: '0',
|
||
orient: 'vertical',
|
||
itemGap: 80
|
||
},
|
||
tooltip: {
|
||
trigger: 'item'
|
||
},
|
||
series: [
|
||
{
|
||
name: '住户类型',
|
||
type: 'pie',
|
||
radius: ['45%', '95%'],
|
||
avoidLabelOverlap: false,
|
||
left: '50%',
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: false,
|
||
fontSize: 20,
|
||
fontWeight: 'bold'
|
||
}
|
||
},
|
||
labelLine: {
|
||
show: false
|
||
},
|
||
data: [
|
||
{ value: 25, name: '业主', itemStyle: { color: '#0a74fe' } },
|
||
{ value: 25, name: '租户', itemStyle: { color: '#fe3d4e' } },
|
||
{ value: 25, name: '亲属', itemStyle: { color: '#00cf7f' } },
|
||
{ value: 25, name: '其他', itemStyle: { color: '#f48e3b' } }
|
||
]
|
||
}
|
||
]
|
||
});
|
||
|
||
/** 男女比例 */
|
||
const options2 = ref<EChartsOption>({
|
||
legend: {
|
||
top: 'center',
|
||
left: '0',
|
||
orient: 'vertical',
|
||
itemGap: 80
|
||
},
|
||
tooltip: {
|
||
trigger: 'item'
|
||
},
|
||
clockwise: true,
|
||
series: [
|
||
{
|
||
name: '男女比例',
|
||
type: 'pie',
|
||
startAngle: 280,
|
||
radius: ['50%', '100%'],
|
||
avoidLabelOverlap: false,
|
||
left: '50%',
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: false,
|
||
fontSize: 20,
|
||
fontWeight: 'bold'
|
||
}
|
||
},
|
||
labelLine: {
|
||
show: false
|
||
},
|
||
data: [
|
||
{ value: 60, name: '男', itemStyle: { color: '#0a74fe' } },
|
||
{ value: 40, name: '女', itemStyle: { color: '#f74043' } }
|
||
]
|
||
}
|
||
]
|
||
});
|
||
|
||
// ! 年龄分布 -----------------------------------------
|
||
const agesX = ['6岁以下', '6-11岁', '11-14岁', '15-24岁', '25-30岁', '31-40岁', '41-50岁', '51-61岁', '61-65岁', '65-70岁', '70岁以上'];
|
||
const agesData = ref(new Array(11).fill(0));
|
||
const maxValue = ref(10); // 设定总高度
|
||
const middleHeight = 0.05; // 中间白条高度
|
||
const barWidth = '10%'; // 柱状图粗度
|
||
const bgData = computed(() => {
|
||
return agesData.value.map((item) => {
|
||
const num = maxValue.value - item - middleHeight;
|
||
if (num > 0) {
|
||
return num;
|
||
} else {
|
||
return maxValue.value;
|
||
}
|
||
});
|
||
});
|
||
const bgData2 = agesX.map(() => middleHeight);
|
||
/** 年龄分布 */
|
||
const options3 = shallowRef<EChartsOption>({
|
||
title: {
|
||
text: '住户年龄分布',
|
||
left: '0',
|
||
top: '0',
|
||
textStyle: { fontSize: 16, fontWeight: 'bold' }
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
top: '12%',
|
||
bottom: '8%',
|
||
containLabel: true
|
||
},
|
||
tooltip: {
|
||
trigger: 'item',
|
||
confine: true,
|
||
axisPointer: { type: 'shadow' }
|
||
},
|
||
xAxis: { type: 'category', data: agesX, axisLabel: { interval: 0 } },
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: { show: true, lineStyle: { color: '#eee' } }
|
||
},
|
||
series: [
|
||
{
|
||
type: 'bar',
|
||
barWidth: barWidth,
|
||
data: agesData.value,
|
||
itemStyle: {
|
||
color: '#409eff',
|
||
borderRadius: [4, 4, 0, 0]
|
||
},
|
||
stack: 'total'
|
||
},
|
||
{
|
||
type: 'bar',
|
||
barWidth: barWidth,
|
||
data: bgData2,
|
||
tooltip: { show: false }, // 关键:不显示提示
|
||
itemStyle: {
|
||
color: '#fff'
|
||
},
|
||
stack: 'total'
|
||
},
|
||
{
|
||
type: 'bar',
|
||
barWidth: barWidth,
|
||
data: bgData.value,
|
||
tooltip: { show: false }, // 关键:不显示提示
|
||
itemStyle: {
|
||
color: 'rgba(180, 180, 180, 0.2)'
|
||
},
|
||
stack: 'total'
|
||
}
|
||
]
|
||
});
|
||
function updateOptions3(data: number[]): EChartsOption {
|
||
return {
|
||
title: {
|
||
text: '住户年龄分布',
|
||
left: '0',
|
||
top: '0',
|
||
textStyle: { fontSize: 16, fontWeight: 'bold' }
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
top: '12%',
|
||
bottom: '8%',
|
||
containLabel: true
|
||
},
|
||
tooltip: {
|
||
trigger: 'item',
|
||
confine: true,
|
||
axisPointer: { type: 'shadow' }
|
||
},
|
||
xAxis: { type: 'category', data: agesX, axisLabel: { interval: 0 } },
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: { show: true, lineStyle: { color: '#eee' } }
|
||
},
|
||
series: [
|
||
{
|
||
type: 'bar',
|
||
barWidth: barWidth,
|
||
data: data,
|
||
itemStyle: {
|
||
color: '#409eff',
|
||
borderRadius: [4, 4, 0, 0]
|
||
},
|
||
stack: 'total'
|
||
},
|
||
{
|
||
type: 'bar',
|
||
barWidth: barWidth,
|
||
data: bgData2,
|
||
tooltip: { show: false }, // 关键:不显示提示
|
||
itemStyle: {
|
||
color: '#fff'
|
||
},
|
||
stack: 'total'
|
||
},
|
||
{
|
||
type: 'bar',
|
||
barWidth: barWidth,
|
||
data: bgData.value,
|
||
tooltip: { show: false }, // 关键:不显示提示
|
||
itemStyle: {
|
||
color: 'rgba(180, 180, 180, 0.2)'
|
||
},
|
||
stack: 'total'
|
||
}
|
||
]
|
||
};
|
||
}
|
||
// ! 年龄分布 -------------------------------------------------------------------------------------
|
||
|
||
// ! 人员流动情况 -------------------------------------------------------------------------------------
|
||
// 近10天日期
|
||
const dayX = ref([]);
|
||
|
||
// 小区真实规模:人员流入 / 流出 数据
|
||
// 【全新打乱随机数据】小区人员流入 / 流出
|
||
const inData = ref([]);
|
||
const outData = ref([]);
|
||
|
||
/** 近十天人员流入流出 - 平滑折线图 */
|
||
const flowLineOption = ref<EChartsOption>({
|
||
title: {
|
||
text: '近七日人员流动趋势',
|
||
left: 0,
|
||
top: 0,
|
||
textStyle: { fontSize: 14, fontWeight: 'normal' }
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
confine: true
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
top: '12%',
|
||
bottom: '8%',
|
||
containLabel: true
|
||
},
|
||
legend: {
|
||
data: ['人员流入', '人员流出'],
|
||
right: 10,
|
||
top: 0
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dayX.value,
|
||
boundaryGap: false,
|
||
axisLine: { lineStyle: { color: '#eee' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#eee' }
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '人员流入',
|
||
type: 'line',
|
||
smooth: true, // 平滑折线 核心
|
||
data: inData.value,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
itemStyle: { color: '#f94144' },
|
||
lineStyle: { width: 4 },
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: '#fbd7d9' },
|
||
{ offset: 1, color: '#fbd7d9' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '人员流出',
|
||
type: 'line',
|
||
smooth: true, // 平滑折线 核心
|
||
data: outData.value,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
itemStyle: { color: '#f9c74f' },
|
||
lineStyle: { width: 4 },
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: '#fcf2dc' },
|
||
{ offset: 1, color: '#fcf2dc' }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
});
|
||
function flowLineOptionFun(days: string[], InData: number[], OutData: number[]): EChartsOption {
|
||
return {
|
||
title: {
|
||
text: '近七日人员流动趋势',
|
||
left: 0,
|
||
top: 0,
|
||
textStyle: { fontSize: 14, fontWeight: 'normal' }
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
confine: true
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
top: '12%',
|
||
bottom: '8%',
|
||
containLabel: true
|
||
},
|
||
legend: {
|
||
data: ['人员流入', '人员流出'],
|
||
right: 10,
|
||
top: 0
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: days,
|
||
boundaryGap: false,
|
||
axisLine: { lineStyle: { color: '#eee' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#eee' }
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '人员流入',
|
||
type: 'line',
|
||
smooth: true, // 平滑折线 核心
|
||
data: InData,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
itemStyle: { color: '#f94144' },
|
||
lineStyle: { width: 4 },
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: '#fbd7d9' },
|
||
{ offset: 1, color: '#fbd7d9' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '人员流出',
|
||
type: 'line',
|
||
smooth: true, // 平滑折线 核心
|
||
data: OutData,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
itemStyle: { color: '#f9c74f' },
|
||
lineStyle: { width: 4 },
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: '#fcf2dc' },
|
||
{ offset: 1, color: '#fcf2dc' }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
};
|
||
}
|
||
// ! 人员流动情况 -------------------------------------------------------------------------------------
|
||
|
||
const MoveIn = ref({
|
||
totalResident: 0,
|
||
totalHouse: 0
|
||
});
|
||
function init() {
|
||
// 入住情况
|
||
MoveInFun();
|
||
// 入住类型
|
||
MoveInUserType();
|
||
// 男女类型
|
||
MoveInUserSex();
|
||
// 迁入迁出
|
||
MoveInUserMoveInOut();
|
||
// 住户年龄分布
|
||
MoveInPersonAge();
|
||
}
|
||
init();
|
||
function querySelect() {
|
||
init();
|
||
}
|
||
// 入住情况
|
||
function MoveInFun() {
|
||
user_movein_list({
|
||
dateTime: date.value
|
||
}).then((res) => {
|
||
// 获取入住情况
|
||
MoveIn.value.totalResident = res.data.occupancyCount;
|
||
MoveIn.value.totalHouse = res.data.totalHouse;
|
||
options.value.series[1].data = [
|
||
{ value: res.data.occupiedHouse },
|
||
{
|
||
value: res.data.totalHouse - res.data.occupiedHouse,
|
||
itemStyle: { color: 'transparent' },
|
||
tooltip: {
|
||
show: false
|
||
}
|
||
}
|
||
];
|
||
options.value.series[1].label.formatter = () => {
|
||
const total = res.data.totalHouse;
|
||
const occupied = res.data.occupiedHouse || 0; // 确保有值
|
||
// 计算入住率,防止除以0
|
||
const rate = total > 0 ? ((occupied / total) * 100).toFixed(2) : '0.0';
|
||
// 返回带换行符的字符串:第一行是百分比,第二行是文字
|
||
return `{rate|${rate}%}\n{title|入住率}`;
|
||
};
|
||
options.value = { ...options.value };
|
||
});
|
||
}
|
||
// 获取住户类型
|
||
function MoveInUserType() {
|
||
get_movein_user_type({
|
||
dateTime: date.value
|
||
}).then((res) => {
|
||
options1.value.series[0].data = [
|
||
{ value: res.owner, name: '业主', itemStyle: { color: '#0a74fe' } },
|
||
{ value: res.tenant, name: '租户', itemStyle: { color: '#fe3d4e' } },
|
||
{ value: res.relatives, name: '亲属', itemStyle: { color: '#00cf7f' } },
|
||
{ value: res.otherType, name: '其他', itemStyle: { color: '#f48e3b' } }
|
||
];
|
||
});
|
||
}
|
||
// 住户性别
|
||
function MoveInUserSex() {
|
||
get_movein_user_sex({
|
||
dateTime: date.value
|
||
}).then((res) => {
|
||
options2.value.series[0].data = [
|
||
{ value: res.man, name: '男', itemStyle: { color: '#0a74fe' } },
|
||
{ value: res.woman, name: '女', itemStyle: { color: '#f74043' } }
|
||
];
|
||
});
|
||
}
|
||
// 迁入迁出
|
||
function MoveInUserMoveInOut() {
|
||
get_movein_moveout_list({
|
||
date: date.value
|
||
}).then((res) => {
|
||
dayX.value = res.data.map((item) => item.date);
|
||
inData.value = res.data.map((item) => item.moveInCount);
|
||
outData.value = res.data.map((item) => item.moveOutCount);
|
||
flowLineOption.value = flowLineOptionFun(dayX.value, inData.value, outData.value);
|
||
});
|
||
}
|
||
// 住户年龄分布
|
||
function MoveInPersonAge() {
|
||
get_person_age({
|
||
dateTime: date.value
|
||
}).then((res) => {
|
||
const values = Object.values(res).slice(0, -1);
|
||
maxValue.value = res.total;
|
||
agesData.value = values;
|
||
options3.value = updateOptions3(values);
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.personnel {
|
||
position: relative;
|
||
background: #deedff;
|
||
.el-button {
|
||
width: 80px;
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
}
|
||
.searchBox {
|
||
margin-bottom: 20px;
|
||
}
|
||
.searchLabel {
|
||
width: 100px;
|
||
font-size: 0.95rem;
|
||
}
|
||
// 入住清空
|
||
.cardbox {
|
||
box-sizing: border-box;
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, #ffffff 70%);
|
||
border-radius: 16px 16px 16px 16px;
|
||
border: 2px solid #ffffff;
|
||
font-size: 20px;
|
||
padding: 20px;
|
||
min-height: 275px;
|
||
@media (max-width: 1200px) {
|
||
margin-bottom: 20px;
|
||
}
|
||
.cardBody {
|
||
padding: 0 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
.card_LeftBox {
|
||
text-align: center;
|
||
margin-right: 20px;
|
||
.labelBox {
|
||
font-size: 16px;
|
||
}
|
||
.valueBox {
|
||
font-weight: 600;
|
||
font-size: 36px;
|
||
color: #222222;
|
||
width: 100px;
|
||
}
|
||
}
|
||
.echartsBox {
|
||
width: 400px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|