feat(DictSelect): 支持多选模式并添加标签折叠功能

feat(meal): 修改用餐人数为成员选择并显示详细名单

feat: 新增用餐报表页面,包含数据统计和可视化图表
This commit is contained in:
砂糖
2026-01-21 16:24:57 +08:00
parent a2ffe5cc73
commit e34e5a7a1f
3 changed files with 600 additions and 11 deletions

View File

@@ -1,7 +1,12 @@
<template>
<div style="display: flex; align-items: center;" v-loading="loading">
<!-- 下拉选择器绑定计算属性做双向绑定保留原有样式+清空功能 -->
<el-select v-model="innerValue" :placeholder="placeholder" clearable filterable style="width: 200px;">
<el-select
v-model="innerValue"
:placeholder="placeholder"
clearable filterable
style="width: 200px;"
:multiple="multiple" collapse-tags>
<el-option
v-for="item in dictOptions"
:key="item.dictValue"
@@ -35,7 +40,6 @@
width="600px"
append-to-body
>
<!-- 快捷新增表单区域仅做新增无编辑功能 -->
<el-form
ref="dictFormRef"
:model="form"
@@ -140,7 +144,7 @@
</template>
<script>
import { getDicts, addData, updateData, delData, listData } from '@/api/system/dict/data'
import { addData, updateData, delData, listData } from '@/api/system/dict/data'
import { listType } from '@/api/system/dict/type'
export default {
@@ -152,6 +156,7 @@ export default {
value: { type: String, default: '' },
placeholder: { type: String, default: '请选择' },
refresh: { type: Boolean, default: true },
multiple: { type: Boolean, default: false },
},
data() {
return {
@@ -178,8 +183,22 @@ export default {
},
computed: {
innerValue: {
get() { return this.value },
set(val) { this.$emit('input', val) }
get() {
if (this.multiple) {
if (!this.value) return []
return this.value?.split(',') || []
}
return this.value
},
set(val) {
if (this.multiple) {
this.$emit('input', val?.join(',') || '')
this.$emit('change', val)
} else {
this.$emit('input', val)
this.$emit('change', val)
}
}
}
},
watch: {

View File

@@ -96,8 +96,20 @@
</template>
</el-table-column>
<el-table-column label="用餐总人数" align="center" prop="totalPeople" />
<el-table-column label="堂食人数" align="center" prop="dineInPeople" />
<el-table-column label="打包人数" align="center" prop="takeoutPeople" />
<el-table-column label="堂食人数" align="center" prop="dineInPeople">
<template slot-scope="scope">
<el-tooltip :content="scope.row.dineInPeopleList || 0" placement="top">
<span style="cursor: pointer;">{{ scope.row.dineInPeople || 0 }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="打包人数" align="center" prop="takeoutPeople">
<template slot-scope="scope">
<el-tooltip :content="scope.row.takeoutPeopleList || 0" placement="top">
<span style="cursor: pointer;">{{ scope.row.takeoutPeople || 0 }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="报餐人姓名" align="center" prop="reportUserName">
<template slot-scope="scope">
<dict-tag :options="dict.type.hrm_leave_employee" :value="scope.row.reportUserName"/>
@@ -150,11 +162,13 @@
<el-form-item label="部门名称" prop="deptName">
<DictSelect dictType="hrm_department" v-model="form.deptName" placeholder="请选择部门名称"></DictSelect>
</el-form-item>
<el-form-item label="堂食人数" prop="dineInPeople">
<el-input v-model="form.dineInPeople" placeholder="请输入堂食人数" type="number" min="0" />
<el-form-item label="堂食成员" prop="dineInPeopleList">
<dict-select dictType="hrm_leave_employee" v-model="form.dineInPeopleList" placeholder="请选择堂食成员" multiple @change="handleDineInPeopleChange"/>
<div>{{ form.dineInPeople || 0 }}</div>
</el-form-item>
<el-form-item label="打包人数" prop="takeoutPeople">
<el-input v-model="form.takeoutPeople" placeholder="请输入打包人数" type="number" min="0" />
<el-form-item label="打包成员" prop="takeoutPeopleList">
<dict-select dictType="hrm_leave_employee" v-model="form.takeoutPeopleList" placeholder="请选择打包成员" multiple @change="handleTakeoutPeopleChange"/>
<div>{{ form.takeoutPeople || 0 }}</div>
</el-form-item>
<el-form-item label="用餐总人数" prop="totalPeople">
<el-input v-model="form.totalPeople" placeholder="请输入用餐总人数" disabled />
@@ -261,6 +275,16 @@ export default {
this.calcTableSum(); // 重新计算统计数据
});
},
/** 新增:处理堂食成员选择变化 */
handleDineInPeopleChange(val) {
this.form.dineInPeople = val?.length || 0;
this.calcTotalPeople();
},
/** 新增:处理打包成员选择变化 */
handleTakeoutPeopleChange(val) {
this.form.takeoutPeople = val?.length || 0;
this.calcTotalPeople();
},
/** 核心修改:区分有效/无效统计 */
calcTableSum(){
// 初始化统计变量

View File

@@ -0,0 +1,546 @@
<template>
<div class="app-container" v-loading="loading">
<!-- 1. 筛选区域日期选择器 -->
<div class="filter-wrapper" style="padding: 16px; background: #fff; margin-bottom: 16px;">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" format="yyyy-MM-dd" value-format="yyyy-MM-dd" @change="handleDateChange" />
<el-button type="primary" style="margin-left: 8px;" @click="getMealReportList">查询</el-button>
</div>
<!-- 2. 核心指标卡片 -->
<div class="card-wrapper" style="display: flex; gap: 16px; padding: 0 16px; margin-bottom: 16px;">
<el-card class="stat-card" style="flex: 1;">
<div class="stat-item">
<span class="stat-label">报餐总次数</span>
<span class="stat-value">{{ totalReportTimes }}</span>
</div>
</el-card>
<el-card class="stat-card" style="flex: 1;">
<div class="stat-item">
<span class="stat-label">用餐总人数</span>
<span class="stat-value">{{ totalDinePeople }}</span>
</div>
</el-card>
<el-card class="stat-card" style="flex: 1;">
<div class="stat-item">
<span class="stat-label">打包总人数</span>
<span class="stat-value">{{ totalTakeoutPeople }}</span>
</div>
</el-card>
<el-card class="stat-card" style="flex: 1;">
<div class="stat-item">
<span class="stat-label">堂食总人数</span>
<span class="stat-value">{{ totalDineInPeople }}</span>
</div>
</el-card>
</div>
<!-- 3. 图表区域 -->
<div class="chart-wrapper" style="padding: 0 16px; margin-bottom: 16px;">
<div style="display: flex; gap: 16px; height: 400px;">
<!-- 3.1 按天汇总折线图 -->
<div class="chart-item" style="flex: 1;" ref="lineChart"></div>
<!-- 3.2 按餐别汇总饼图 -->
<div class="chart-item" style="flex: 1;" ref="pieChart"></div>
<!-- 3.3 按部门汇总柱状图 -->
<div class="chart-item" style="flex: 1;" ref="barChart"></div>
</div>
</div>
<!-- 4. 人员明细表格 -->
<div class="table-wrapper" style="padding: 0 16px;">
<el-table :data="userMealTableData" border style="width: 100%;" stripe>
<el-table-column label="用餐人" prop="userName" align="center" />
<el-table-column label="早餐" align="center">
<el-table-column label="堂食次数" prop="breakfastDineIn" align="center" />
<el-table-column label="打包次数" prop="breakfastTakeout" align="center" />
</el-table-column>
<el-table-column label="午餐" align="center">
<el-table-column label="堂食次数" prop="lunchDineIn" align="center" />
<el-table-column label="打包次数" prop="lunchTakeout" align="center" />
</el-table-column>
<el-table-column label="晚餐" align="center">
<el-table-column label="堂食次数" prop="dinnerDineIn" align="center" />
<el-table-column label="打包次数" prop="dinnerTakeout" align="center" />
</el-table-column>
<el-table-column label="夜宵" align="center">
<el-table-column label="堂食次数" prop="supperDineIn" align="center" />
<el-table-column label="打包次数" prop="supperTakeout" align="center" />
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import { listMealReport } from "@/api/wms/mealReport";
import * as echarts from "echarts";
export default {
name: "MealReport",
data() {
return {
// 日期范围(绑定选择器)
dateRange: [],
// 原始报表数据
mealReportList: [],
// 查询参数
queryParams: {
startTime: "",
endTime: "",
pageNum: 1,
pageSize: 1000,
},
// 指标卡数据
totalReportTimes: 0, // 报餐总次数
totalDinePeople: 0, // 用餐总人数(去重)
totalTakeoutPeople: 0, // 打包总人数(去重)
totalDineInPeople: 0, // 堂食总人数(去重)
// 图表实例
lineChart: null,
pieChart: null,
barChart: null,
// 人员明细表格数据
userMealTableData: [],
// 餐别映射1-早餐 2-午餐 3-晚餐 4-夜宵)
mealTypeMap: {
1: "早餐",
2: "午餐",
3: "晚餐",
4: "夜宵",
},
// 加载状态
loading: false,
};
},
mounted() {
// 初始化默认日期(本月第一天至今日)
this.initDefaultDate();
// 初始化图表
this.initCharts();
// 获取报表数据
this.getMealReportList();
},
beforeDestroy() {
// 销毁图表实例,防止内存泄漏
if (this.lineChart) this.lineChart.dispose();
if (this.pieChart) this.pieChart.dispose();
if (this.barChart) this.barChart.dispose();
},
methods: {
formatDateToYMD(date) {
const year = date.getFullYear();
// 月份补0原生月份是0-11所以要+1
const month = String(date.getMonth() + 1).padStart(2, "0");
// 日期补0
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
},
// ========== 修改初始化默认日期纯原生Date实现 ==========
initDefaultDate() {
const now = new Date(); // 当前日期
// 1. 获取本月第一天
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// 2. 获取今日
const today = new Date(now);
// 格式化为 YYYY-MM-DD
const startStr = this.formatDateToYMD(startOfMonth);
const todayStr = this.formatDateToYMD(today);
// 赋值给日期选择器和查询参数
this.dateRange = [startStr, todayStr];
this.queryParams.startTime = startStr;
this.queryParams.endTime = todayStr;
},
// 初始化图表实例
initCharts() {
// 折线图(按天汇总)
this.lineChart = echarts.init(this.$refs.lineChart);
// 饼图(按餐别汇总)
this.pieChart = echarts.init(this.$refs.pieChart);
// 柱状图(按部门汇总)
this.barChart = echarts.init(this.$refs.barChart);
},
// 日期选择器变化事件
handleDateChange(val) {
if (val && val.length === 2) {
this.queryParams.startTime = val[0];
this.queryParams.endTime = val[1];
} else {
// 清空日期时重置为默认
this.initDefaultDate();
}
},
// 获取报表数据
getMealReportList() {
this.loading = true; // 显示加载中状态
listMealReport(this.queryParams)
.then((res) => {
this.mealReportList = res.rows || [];
// 处理数据:计算指标、图表、表格数据
this.handleReportData();
})
.catch((err) => {
console.error("获取报表数据失败:", err);
})
.finally(() => {
this.loading = false; // 加载完成后隐藏状态
});
},
// 核心:处理报表数据(指标/图表/表格)
handleReportData() {
const list = this.mealReportList;
if (!list.length) {
// 清空数据
this.resetReportData();
return;
}
// ========== 1. 计算指标卡数据 ==========
// 所有人员去重(用餐总人数)
const allPeopleSet = new Set();
// 打包人员去重
const takeoutPeopleSet = new Set();
// 堂食人员去重
const dineInPeopleSet = new Set();
list.forEach((item) => {
// 拆分人员列表并去重加入集合
if (item.dineInPeopleList) {
item.dineInPeopleList.split(",").forEach((name) => {
if (name) {
dineInPeopleSet.add(name.trim());
allPeopleSet.add(name.trim());
}
});
}
if (item.takeoutPeopleList) {
item.takeoutPeopleList.split(",").forEach((name) => {
if (name) {
takeoutPeopleSet.add(name.trim());
allPeopleSet.add(name.trim());
}
});
}
this.totalDinePeople += item.totalPeople;
this.totalTakeoutPeople += item.takeoutPeople;
this.totalDineInPeople += item.dineInPeople;
});
this.totalReportTimes = list.length; // 报餐总次数=记录数
// this.totalDinePeople = dineInPeopleSet.size; // 堂食总人数(去重)
// this.totalTakeoutPeople = takeoutPeopleSet.size; // 打包总人数(去重)
// this.totalDineInPeople = dineInPeopleSet.size; // 堂食总人数(去重)
// ========== 2. 处理折线图数据(按天汇总) ==========
// 按日期分组key=日期(YYYY-MM-DD)value=统计数据
const dayGroup = {};
list.forEach((item) => {
// 格式化日期(去掉时分秒)
const reportDate = this.parseTime(item.reportDate, "{y}-{m}-{d}");
if (!dayGroup[reportDate]) {
dayGroup[reportDate] = {
reportCount: 0, // 报餐次数
totalPeople: 0, // 报餐人数
takeoutPeople: 0, // 打包人数
dineInPeople: 0, // 堂食人数
};
}
dayGroup[reportDate].reportCount += 1;
dayGroup[reportDate].totalPeople += item.totalPeople;
dayGroup[reportDate].takeoutPeople += item.takeoutPeople;
dayGroup[reportDate].dineInPeople += item.dineInPeople;
});
// 提取日期和对应数值
const dayLabels = Object.keys(dayGroup).sort(); // 按日期排序
const reportCountData = dayLabels.map((day) => dayGroup[day].reportCount);
const totalPeopleData = dayLabels.map((day) => dayGroup[day].totalPeople);
const takeoutPeopleData = dayLabels.map((day) => dayGroup[day].takeoutPeople);
const dineInPeopleData = dayLabels.map((day) => dayGroup[day].dineInPeople);
// 渲染折线图
this.renderLineChart(dayLabels, reportCountData, totalPeopleData, takeoutPeopleData, dineInPeopleData);
// ========== 3. 处理饼图数据(按餐别汇总) ==========
const mealTypeGroup = {};
list.forEach((item) => {
const mealType = item.mealType;
if (!mealTypeGroup[mealType]) {
mealTypeGroup[mealType] = {
count: 0, // 报餐次数
people: 0, // 报餐人数
};
}
mealTypeGroup[mealType].count += 1;
mealTypeGroup[mealType].people += item.totalPeople;
});
// 饼图-报餐次数
const pieCountData = Object.keys(mealTypeGroup).map((type) => ({
name: this.mealTypeMap[type] || `未知(${type})`,
value: mealTypeGroup[type].count,
}));
// 饼图-报餐人数
const piePeopleData = Object.keys(mealTypeGroup).map((type) => ({
name: this.mealTypeMap[type] || `未知(${type})`,
value: mealTypeGroup[type].people,
}));
// 渲染饼图
this.renderPieChart(pieCountData, piePeopleData);
// ========== 4. 处理柱状图数据(按部门汇总) ==========
const deptGroup = {};
list.forEach((item) => {
const deptName = item.deptName || "未知部门";
if (!deptGroup[deptName]) {
deptGroup[deptName] = {
reportCount: 0, // 报餐次数
totalPeople: 0, // 报餐人数
takeoutPeople: 0, // 打包人数
dineInPeople: 0, // 堂食人数
};
}
deptGroup[deptName].reportCount += 1;
deptGroup[deptName].totalPeople += item.totalPeople;
deptGroup[deptName].takeoutPeople += item.takeoutPeople;
deptGroup[deptName].dineInPeople += item.dineInPeople;
});
const deptLabels = Object.keys(deptGroup);
const deptReportCount = deptLabels.map((dept) => deptGroup[dept].reportCount);
const deptTotalPeople = deptLabels.map((dept) => deptGroup[dept].totalPeople);
const deptTakeoutPeople = deptLabels.map((dept) => deptGroup[dept].takeoutPeople);
const deptDineInPeople = deptLabels.map((dept) => deptGroup[dept].dineInPeople);
// 渲染柱状图
this.renderBarChart(deptLabels, deptReportCount, deptTotalPeople, deptTakeoutPeople, deptDineInPeople);
// ========== 5. 处理人员明细表格数据 ==========
const userGroup = {};
list.forEach((item) => {
const mealType = item.mealType; // 1-早餐 2-午餐 3-晚餐 4-夜宵
// 处理堂食人员
if (item.dineInPeopleList) {
item.dineInPeopleList.split(",").forEach((name) => {
name = name.trim();
if (!name) return;
if (!userGroup[name]) {
// 初始化用户数据
userGroup[name] = {
userName: name,
breakfastDineIn: 0,
breakfastTakeout: 0,
lunchDineIn: 0,
lunchTakeout: 0,
dinnerDineIn: 0,
dinnerTakeout: 0,
supperDineIn: 0,
supperTakeout: 0,
};
}
// 根据餐别累加堂食次数
switch (mealType) {
case 1:
userGroup[name].breakfastDineIn += 1;
break;
case 2:
userGroup[name].lunchDineIn += 1;
break;
case 3:
userGroup[name].dinnerDineIn += 1;
break;
case 4:
userGroup[name].supperDineIn += 1;
break;
}
});
}
// 处理打包人员
if (item.takeoutPeopleList) {
item.takeoutPeopleList.split(",").forEach((name) => {
name = name.trim();
if (!name) return;
if (!userGroup[name]) {
userGroup[name] = {
userName: name,
breakfastDineIn: 0,
breakfastTakeout: 0,
lunchDineIn: 0,
lunchTakeout: 0,
dinnerDineIn: 0,
dinnerTakeout: 0,
supperDineIn: 0,
supperTakeout: 0,
};
}
// 根据餐别累加打包次数
switch (mealType) {
case 1:
userGroup[name].breakfastTakeout += 1;
break;
case 2:
userGroup[name].lunchTakeout += 1;
break;
case 3:
userGroup[name].dinnerTakeout += 1;
break;
case 4:
userGroup[name].supperTakeout += 1;
break;
}
});
}
});
// 转换为表格数组
this.userMealTableData = Object.values(userGroup);
},
// 重置报表数据(无数据时)
resetReportData() {
this.totalReportTimes = 0;
this.totalDinePeople = 0;
this.totalTakeoutPeople = 0;
this.totalDineInPeople = 0;
this.userMealTableData = [];
// 清空图表
this.lineChart.setOption({ series: [] });
this.pieChart.setOption({ series: [] });
this.barChart.setOption({ series: [] });
},
// 渲染按天汇总折线图
renderLineChart(dayLabels, reportCount, totalPeople, takeoutPeople, dineInPeople) {
const option = {
title: { text: "按天汇总数据", left: "center" },
tooltip: { trigger: "axis" },
legend: { top: "bottom", data: ["报餐次数", "报餐人数", "打包人数", "堂食人数"] },
grid: { left: "3%", right: "4%", bottom: "15%", containLabel: true },
xAxis: { type: "category", boundaryGap: false, data: dayLabels },
yAxis: { type: "value", min: 0 },
series: [
{
name: "报餐次数",
type: "line",
data: reportCount,
smooth: true,
},
{
name: "报餐人数",
type: "line",
data: totalPeople,
smooth: true,
},
{
name: "打包人数",
type: "line",
data: takeoutPeople,
smooth: true,
},
{
name: "堂食人数",
type: "line",
data: dineInPeople,
smooth: true,
},
],
};
this.lineChart.setOption(option);
// 自适应窗口大小
window.addEventListener("resize", () => this.lineChart.resize());
},
// 渲染按餐别汇总饼图
renderPieChart(pieCountData, piePeopleData) {
const option = {
title: {
text: "按餐别汇总",
left: "center",
subtext: "次数/人数",
},
tooltip: { trigger: "item" },
legend: { orient: "vertical", left: "left", data: pieCountData.map((item) => item.name) },
series: [
{
name: "报餐次数",
type: "pie",
radius: ["30%", "40%"],
center: ["30%", "50%"],
data: pieCountData,
},
{
name: "报餐人数",
type: "pie",
radius: ["30%", "40%"],
center: ["70%", "50%"],
data: piePeopleData,
},
],
};
this.pieChart.setOption(option);
window.addEventListener("resize", () => this.pieChart.resize());
},
// 渲染按部门汇总柱状图
renderBarChart(deptLabels, reportCount, totalPeople, takeoutPeople, dineInPeople) {
const option = {
title: { text: "按部门汇总数据", left: "center" },
tooltip: { trigger: "axis" },
legend: { top: "bottom", data: ["报餐次数", "报餐人数", "打包人数", "堂食人数"] },
grid: { left: "3%", right: "4%", bottom: "15%", containLabel: true },
xAxis: { type: "category", data: deptLabels },
yAxis: { type: "value", min: 0 },
series: [
{
name: "报餐次数",
type: "bar",
data: reportCount,
},
{
name: "报餐人数",
type: "bar",
data: totalPeople,
},
{
name: "打包人数",
type: "bar",
data: takeoutPeople,
},
{
name: "堂食人数",
type: "bar",
data: dineInPeople,
},
],
};
this.barChart.setOption(option);
window.addEventListener("resize", () => this.barChart.resize());
},
},
};
</script>
<style scoped>
.stat-card {
text-align: center;
}
.stat-item .stat-label {
font-size: 14px;
color: #666;
display: block;
margin-bottom: 8px;
}
.stat-item .stat-value {
font-size: 24px;
font-weight: bold;
color: #1989fa;
}
.chart-item {
border: 1px solid #ebeef5;
border-radius: 4px;
}
</style>