Files
klp-mono/apps/hand-factory/pages/meal/meal.vue
砂糖 0902be0135 feat(报餐): 新增报餐功能模块并更新版本号至1.3.27
- 添加报餐页面及相关API接口
- 实现报餐统计功能,包括有效/无效报餐人数计算
- 新增自定义时间选择器组件
- 更新应用版本号至1.3.27
- 调整角色权限控制,新增食堂角色权限配置
2026-03-07 17:31:46 +08:00

508 lines
12 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>
<view class="meal-report-container">
<!-- 日期和时间选择区域 -->
<view class="date-time-wrap">
<view class="picker-item">
<text class="picker-label">餐别</text>
<uni-data-select
v-model="queryParams.mealType"
:localdata="range"
@change="getList"
></uni-data-select>
</view>
<!-- 报餐日期选择 -->
<view class="picker-item">
<text class="picker-label">报餐日期</text>
<uni-datetime-picker
v-model="queryParams.reportDate"
type="date"
placeholder="选择日期"
@change="onDateConfirm"
class="picker-input"
></uni-datetime-picker>
</view>
<!-- 自定义截止时间选择 -->
<view class="picker-item">
<text class="picker-label">截止时间</text>
<view
class="custom-time-picker"
@click="openTimePopup"
>
<text class="time-text">{{ formattedDeadlineTime }}</text>
<uni-icons type="arrowdown" size="14" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 报餐统计卡片 -->
<view class="statistics-card">
<view class="card-title">报餐人数统计</view>
<view class="stats-grid">
<!-- 第一行 -->
<view class="stats-item">
<text class="item-label">堂食人数</text>
<text class="item-value">{{ validDineIn + invalidDineIn }}</text>
</view>
<view class="stats-item">
<text class="item-label">打包人数</text>
<text class="item-value">{{ validTakeout + invalidTakeout }}</text>
</view>
<view class="stats-item">
<text class="item-label">总人数</text>
<text class="item-value">{{ validTotal + invalidTotal }}</text>
</view>
<!-- 第二行 -->
<view class="stats-item">
<text class="item-label">有效堂食人数</text>
<text class="item-value">{{ validDineIn }}</text>
</view>
<view class="stats-item">
<text class="item-label">有效打包人数</text>
<text class="item-value">{{ validTakeout }}</text>
</view>
<view class="stats-item">
<text class="item-label">有效总人数</text>
<text class="item-value">{{ validTotal }}</text>
</view>
<!-- 第三行 -->
<view class="stats-item">
<text class="item-label">无效堂食人数</text>
<text class="item-value">{{ invalidDineIn }}</text>
</view>
<view class="stats-item">
<text class="item-label">无效打包人数</text>
<text class="item-value">{{ invalidTakeout }}</text>
</view>
<view class="stats-item">
<text class="item-label">无效总人数</text>
<text class="item-value">{{ invalidTotal }}</text>
</view>
</view>
</view>
<!-- 自定义时分秒选择弹窗 -->
<uni-popup ref="timePopup" type="bottom" border-radius="10px 10px 0 0">
<view class="time-popup-content">
<!-- 弹窗标题 -->
<view class="popup-header">
<text class="popup-title">选择截止时间</text>
<view class="popup-btns">
<button class="cancel-btn" @click="closeTimePopup">取消</button>
<button class="confirm-btn" @click="confirmTime">确认</button>
</view>
</view>
<!-- 时分秒选择区域 -->
<view class="time-select-wrap">
<!-- 小时选择 -->
<view class="time-unit">
<text class="unit-label"></text>
<uni-number-box
v-model="timeSelect.hour"
:min="0"
:max="23"
:step="1"
@change="handleTimeChange"
></uni-number-box>
</view>
<!-- 分钟选择 -->
<view class="time-unit">
<text class="unit-label"></text>
<uni-number-box
v-model="timeSelect.minute"
:min="0"
:max="59"
:step="1"
@change="handleTimeChange"
></uni-number-box>
</view>
<!-- 秒选择 -->
<view class="time-unit">
<text class="unit-label"></text>
<uni-number-box
v-model="timeSelect.second"
:min="0"
:max="59"
:step="1"
@change="handleTimeChange"
></uni-number-box>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { listMealReport } from "@/api/wms/mealReport";
import { getDicts } from '@/api/system/dict/data.js'
export default {
name: 'MealReportStatistics',
data() {
return {
queryParams: {
mealType: '',
reportDate: '',
pageSize: 9999,
pageNum: 1
},
deadlineDate: '', // 截止日期
deadlineTime: '12:00:00', // 截止时间(原始值)
// 时分秒选择器临时变量
timeSelect: {
hour: 12,
minute: 0,
second: 0
},
list: [],
loading: false,
// 统计数据
validDineIn: 0,
validTakeout: 0,
validTotal: 0,
invalidDineIn: 0,
invalidTakeout: 0,
invalidTotal: 0,
range: []
}
},
computed: {
// 格式化截止时间显示(补零)
formattedDeadlineTime() {
const [hour, minute, second] = this.deadlineTime.split(':');
return `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
}
},
onLoad() {
// 初始化今日日期
this.initTodayDate();
// 初始化截止日期为今日
this.deadlineDate = this.queryParams.reportDate;
// 获取餐别字典数据
this.getRangeData();
// 加载报餐数据
this.getList();
},
methods: {
/** 初始化今日日期为yyyy-MM-dd格式 */
initTodayDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
this.queryParams.reportDate = `${year}-${month}-${day}`;
},
/** 获取餐别字典数据 */
getRangeData() {
getDicts('hrm_meal_type').then(res => {
this.range = res.data.map(item => ({
text: item.dictLabel,
value: item.dictValue
}));
});
},
/** 查询部门报餐列表 */
getList() {
this.loading = true;
listMealReport(this.queryParams).then(response => {
this.list = response.rows || [];
this.loading = false;
this.calcTableSum();
}).catch(error => {
console.error('获取报餐数据失败:', error);
this.loading = false;
});
},
/** 核心逻辑:区分有效/无效报餐统计 */
calcTableSum() {
let validDine = 0, validTake = 0, validAll = 0;
let invalidDine = 0, invalidTake = 0, invalidAll = 0;
this.list.forEach(item => {
// 处理空值,转为数字
const dine = item.dineInPeople ? Number(item.dineInPeople) : 0;
const take = item.takeoutPeople ? Number(item.takeoutPeople) : 0;
const total = item.totalPeople ? Number(item.totalPeople) : 0;
// 判断当前报餐是否有效
if (this.isValidMealReport(item.createTime)) {
validDine += dine;
validTake += take;
validAll += total;
} else {
invalidDine += dine;
invalidTake += take;
invalidAll += total;
}
});
// 赋值到统计变量
this.validDineIn = validDine;
this.validTakeout = validTake;
this.validTotal = validAll;
this.invalidDineIn = invalidDine;
this.invalidTakeout = invalidTake;
this.invalidTotal = invalidAll;
},
/** 判断报餐是否有效:创建时间在截止时间之前则有效 */
isValidMealReport(createTime) {
if (!createTime) return false;
// 拼接完整的截止时间字符串
const deadlineDateTime = `${this.deadlineDate || this.queryParams.reportDate} ${this.deadlineTime}`;
// 比较时间
const createTimeObj = new Date(createTime);
const deadlineTimeObj = new Date(deadlineDateTime);
return createTimeObj <= deadlineTimeObj;
},
/** 报餐日期选择确认 */
onDateConfirm(e) {
this.queryParams.reportDate = e;
// 如果截止日期未选择,同步为报餐日期
if (!this.deadlineDate) {
this.deadlineDate = e;
}
this.getList();
},
/** 截止日期选择确认 */
onDeadlineDateConfirm(e) {
this.deadlineDate = e;
this.calcTableSum();
},
// ===== 自定义时间选择器逻辑 =====
/** 打开时间选择弹窗 */
openTimePopup() {
// 解析当前时间到选择器
const [hour, minute, second] = this.deadlineTime.split(':').map(Number);
this.timeSelect = { hour, minute, second };
// 打开弹窗
this.$refs.timePopup.open('center');
},
/** 关闭时间选择弹窗 */
closeTimePopup() {
this.$refs.timePopup.close();
},
/** 处理时分秒数值变化 */
handleTimeChange() {
// 确保数值在合法范围内
this.timeSelect.hour = Math.max(0, Math.min(23, this.timeSelect.hour));
this.timeSelect.minute = Math.max(0, Math.min(59, this.timeSelect.minute));
this.timeSelect.second = Math.max(0, Math.min(59, this.timeSelect.second));
},
/** 确认选择的时间 */
confirmTime() {
// 格式化时分秒(补零)
const hour = String(this.timeSelect.hour).padStart(2, '0');
const minute = String(this.timeSelect.minute).padStart(2, '0');
const second = String(this.timeSelect.second).padStart(2, '0');
// 更新截止时间
this.deadlineTime = `${hour}:${minute}:${second}`;
// 重新计算统计数据
this.calcTableSum();
// 关闭弹窗
this.closeTimePopup();
}
}
}
</script>
<style scoped>
/* 容器样式 */
.meal-report-container {
padding: 15px;
background-color: #f5f5f5;
min-height: 100vh;
}
/* 日期时间选择区域 */
.date-time-wrap {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
background-color: #fff;
padding: 15px;
border-radius: 8px;
}
.picker-item {
display: flex;
align-items: center;
gap: 8px;
}
.picker-label {
font-size: 14px;
color: #333;
min-width: 80px;
}
.picker-input {
flex: 1;
}
/* 自定义时间选择器样式 */
.custom-time-picker {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #fff;
}
.time-text {
font-size: 14px;
color: #333;
}
/* 统计卡片样式 */
.statistics-card {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
padding: 12px 15px;
border-bottom: 1px solid #eee;
}
/* 网格布局3列 */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
border: 1px solid #eee;
border-top: none;
}
/* 统计项样式 */
.stats-item {
padding: 12px 8px;
text-align: center;
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
box-sizing: border-box;
font-size: 14px;
}
/* 去掉最后一列右边框 */
.stats-grid .stats-item:nth-child(3n) {
border-right: none;
}
/* 去掉最后一行下边框 */
.stats-grid .stats-item:last-child,
.stats-grid .stats-item:nth-last-child(2),
.stats-grid .stats-item:nth-last-child(3) {
border-bottom: none;
}
/* 标签和值样式 */
.item-label {
display: block;
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.item-value {
display: block;
color: #333;
font-size: 15px;
font-weight: 500;
}
/* 时间弹窗样式 */
.time-popup-content {
background-color: #fff;
padding-bottom: 20px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #eee;
}
.popup-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.popup-btns {
display: flex;
gap: 10px;
}
.cancel-btn, .confirm-btn {
padding: 6px 12px;
font-size: 14px;
border-radius: 4px;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
border: 1px solid #eee;
}
.confirm-btn {
background-color: #007aff;
color: #fff;
border: none;
}
/* 时分秒选择区域 */
.time-select-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 20px 15px;
}
.time-unit {
display: flex;
align-items: center;
gap: 8px;
}
.unit-label {
font-size: 12px;
color: #999;
}
.colon {
font-size: 20px;
color: #333;
margin: 0 5px;
}
/* 适配小屏手机 */
@media (max-width: 375px) {
.stats-item {
padding: 10px 5px;
}
.item-label {
font-size: 11px;
}
.item-value {
font-size: 14px;
}
.time-select-wrap {
gap: 5px;
padding: 15px 10px;
}
}
</style>