Files
estate/src/views/system/statistics/Personnel/index.vue
2026-09-10 16:12:22 +08:00

700 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>