feat(meal&wms): add employee spicy stats and upgrade version to 1.3.37

1. 升级应用版本号从1.3.34到1.3.37,同步更新配置文件和静态默认版本
2. 新增员工信息API接口,用于获取员工吃辣偏好数据
3. 重构报餐统计页面:
   - 拆分有效/无效/总计三个统计卡片,使用十字交叉表格展示吃辣/不吃辣、堂食/打包的统计数据
   - 新增员工吃辣偏好映射,基于员工信息计算分类统计数据
   - 优化报餐有效性判断逻辑,改为仅比较时分秒匹配PC端逻辑
   - 调整截止时间默认值和同步逻辑
4. 新增APP所需的系统权限模块配置
This commit is contained in:
2026-06-30 11:20:55 +08:00
parent fb5ee8356b
commit 95332ac8be
6 changed files with 328 additions and 113 deletions

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询员工信息列表
export function listEmployeeInfo(query) {
return request({
url: '/wms/employeeInfo/list',
method: 'get',
params: query
})
}
// 查询员工信息详细
export function getEmployeeInfo(infoId) {
return request({
url: '/wms/employeeInfo/' + infoId,
method: 'get'
})
}
// 新增员工信息
export function addEmployeeInfo(data) {
return request({
url: '/wms/employeeInfo',
method: 'post',
data: data
})
}
// 修改员工信息
export function updateEmployeeInfo(data) {
return request({
url: '/wms/employeeInfo',
method: 'put',
data: data
})
}
// 删除员工信息
export function delEmployeeInfo(infoId) {
return request({
url: '/wms/employeeInfo/' + infoId,
method: 'delete'
})
}

View File

@@ -9,7 +9,7 @@ module.exports = {
// 应用名称 // 应用名称
name: "ruoyi-app", name: "ruoyi-app",
// 应用版本 // 应用版本
version: "1.3.34", version: "1.3.37",
// 应用logo // 应用logo
logo: "/static/logo.jpg", logo: "/static/logo.jpg",
// 官方网站 // 官方网站

View File

@@ -15,7 +15,18 @@
"delay" : 0 "delay" : 0
}, },
"modules" : { "modules" : {
"Barcode" : {} "Barcode" : {},
"Contacts" : {},
"Bluetooth" : {},
"FaceID" : {},
"Fingerprint" : {},
"Geolocation" : {},
"Record" : {},
"Camera" : {},
"iBeacon" : {},
"LivePusher" : {},
"Messaging" : {},
"VideoPlayer" : {}
}, },
"distribute" : { "distribute" : {
"android" : { "android" : {
@@ -41,7 +52,13 @@
"ios" : { "ios" : {
"dSYMs" : false "dSYMs" : false
}, },
"sdkConfigs" : {}, "sdkConfigs" : {
"geolocation" : {
"system" : {
"__platform__" : [ "android" ]
}
}
},
"icons" : { "icons" : {
"android" : { "android" : {
"hdpi" : "unpackage/res/icons/72x72.png", "hdpi" : "unpackage/res/icons/72x72.png",

View File

@@ -23,51 +23,93 @@
</view> </view>
</view> </view>
<!-- 报餐统计卡片 --> <!-- 卡片1有效报餐 -->
<view class="statistics-card" style="margin-bottom: 15px;">
<view class="card-title"> 有效报餐</view>
<view class="cross-grid">
<!-- 第1行表头 -->
<view class="stats-item cross-header"></view>
<view class="stats-item cross-header">堂食</view>
<view class="stats-item cross-header">打包</view>
<view class="stats-item cross-header">小计</view>
<!-- 第2行吃辣 -->
<view class="stats-item cross-label">🌶 吃辣</view>
<view class="stats-item">{{ ms.validDine }}</view>
<view class="stats-item">{{ ms.validTake }}</view>
<view class="stats-item">{{ ms.validTotal }}</view>
<!-- 第3行不吃辣 -->
<view class="stats-item cross-label">🌿 不吃辣</view>
<view class="stats-item">{{ mn.validDine }}</view>
<view class="stats-item">{{ mn.validTake }}</view>
<view class="stats-item">{{ mn.validTotal }}</view>
<!-- 第4行小计 -->
<view class="stats-item cross-label cross-total">小计</view>
<view class="stats-item cross-total">{{ ma.validDine }}</view>
<view class="stats-item cross-total">{{ ma.validTake }}</view>
<view class="stats-item cross-total">{{ ma.validTotal }}</view>
</view>
</view>
<!-- 卡片2无效报餐 -->
<view class="statistics-card" style="margin-bottom: 15px;">
<view class="card-title"> 无效报餐</view>
<view class="cross-grid">
<!-- 第1行表头 -->
<view class="stats-item cross-header"></view>
<view class="stats-item cross-header">堂食</view>
<view class="stats-item cross-header">打包</view>
<view class="stats-item cross-header">小计</view>
<!-- 第2行吃辣 -->
<view class="stats-item cross-label">🌶 吃辣</view>
<view class="stats-item">{{ ms.invalidDine }}</view>
<view class="stats-item">{{ ms.invalidTake }}</view>
<view class="stats-item">{{ ms.invalidTotal }}</view>
<!-- 第3行不吃辣 -->
<view class="stats-item cross-label">🌿 不吃辣</view>
<view class="stats-item">{{ mn.invalidDine }}</view>
<view class="stats-item">{{ mn.invalidTake }}</view>
<view class="stats-item">{{ mn.invalidTotal }}</view>
<!-- 第4行小计 -->
<view class="stats-item cross-label cross-total">小计</view>
<view class="stats-item cross-total">{{ ma.invalidDine }}</view>
<view class="stats-item cross-total">{{ ma.invalidTake }}</view>
<view class="stats-item cross-total">{{ ma.invalidTotal }}</view>
</view>
</view>
<!-- 卡片3总计 -->
<view class="statistics-card"> <view class="statistics-card">
<view class="card-title">报餐人数统</view> <view class="card-title">📊 </view>
<view class="stats-grid"> <view class="cross-grid">
<!-- 一行 --> <!-- 1行表头 -->
<view class="stats-item"> <view class="stats-item cross-header"></view>
<text class="item-label">堂食人数</text> <view class="stats-item cross-header">堂食</view>
<text class="item-value">{{ validDineIn + invalidDineIn }}</text> <view class="stats-item cross-header">打包</view>
</view> <view class="stats-item cross-header">小计</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>
<!-- 二行 --> <!-- 2行吃辣 -->
<view class="stats-item"> <view class="stats-item cross-label">🌶 吃辣</view>
<text class="item-label">有效堂食人数</text> <view class="stats-item">{{ ms.dine }}</view>
<text class="item-value">{{ validDineIn }}</text> <view class="stats-item">{{ ms.take }}</view>
</view> <view class="stats-item">{{ ms.total }}</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>
<!-- 三行 --> <!-- 3行不吃辣 -->
<view class="stats-item"> <view class="stats-item cross-label">🌿 不吃辣</view>
<text class="item-label">无效堂食人数</text> <view class="stats-item">{{ mn.dine }}</view>
<text class="item-value">{{ invalidDineIn }}</text> <view class="stats-item">{{ mn.take }}</view>
</view> <view class="stats-item">{{ mn.total }}</view>
<view class="stats-item">
<text class="item-label">无效打包人数</text> <!-- 第4行小计 -->
<text class="item-value">{{ invalidTakeout }}</text> <view class="stats-item cross-label cross-total">小计</view>
</view> <view class="stats-item cross-total">{{ ma.dine }}</view>
<view class="stats-item"> <view class="stats-item cross-total">{{ ma.take }}</view>
<text class="item-label">无效总人数</text> <view class="stats-item cross-total">{{ ma.total }}</view>
<text class="item-value">{{ invalidTotal }}</text>
</view>
</view> </view>
</view> </view>
@@ -115,6 +157,9 @@
import { import {
listMealReport listMealReport
} from "@/api/wms/mealReport"; } from "@/api/wms/mealReport";
import {
listEmployeeInfo
} from "@/api/wms/employeeInfo";
import { import {
getDicts getDicts
} from '@/api/system/dict/data.js' } from '@/api/system/dict/data.js'
@@ -127,13 +172,17 @@
data() { data() {
return { return {
queryParams: { queryParams: {
mealType: '', mealType: undefined,
reportDate: '', reportDate: '',
deptName: undefined,
reportUserName: undefined,
status: undefined,
deadlineTime: '16:00:00',
pageSize: 9999, pageSize: 9999,
pageNum: 1 pageNum: 1
}, },
deadlineDate: '', // 截止日期 deadlineDate: '',
deadlineTime: '12:00:00', // 截止时间(原始值) deadlineTime: '16:00:00', // 截止时间(原始值)
// 时分秒选择器临时变量 // 时分秒选择器临时变量
timeSelect: { timeSelect: {
hour: 12, hour: 12,
@@ -141,14 +190,14 @@
second: 0 second: 0
}, },
list: [], list: [],
employeeSpicyMap: {}, // name -> isSpicyEater (1吃辣/0不吃辣)
loading: false, loading: false,
// 统计数据 // 三维交叉统计矩阵(吃辣×打包×有效)
validDineIn: 0, matrix: {
validTakeout: 0, spicy: { validDine: 0, validTake: 0, validTotal: 0, invalidDine: 0, invalidTake: 0, invalidTotal: 0, dine: 0, take: 0, total: 0 },
validTotal: 0, nonSpicy: { validDine: 0, validTake: 0, validTotal: 0, invalidDine: 0, invalidTake: 0, invalidTotal: 0, dine: 0, take: 0, total: 0 },
invalidDineIn: 0, all: { validDine: 0, validTake: 0, validTotal: 0, invalidDine: 0, invalidTake: 0, invalidTotal: 0, dine: 0, take: 0, total: 0 }
invalidTakeout: 0, },
invalidTotal: 0,
range: [] range: []
} }
}, },
@@ -157,7 +206,11 @@
formattedDeadlineTime() { formattedDeadlineTime() {
const [hour, minute, second] = this.deadlineTime.split(':'); const [hour, minute, second] = this.deadlineTime.split(':');
return `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`; return `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
} },
// 从 matrix 对象中提取快捷访问属性(保持模板简洁)
ms() { return this.matrix.spicy; },
mn() { return this.matrix.nonSpicy; },
ma() { return this.matrix.all; }
}, },
onLoad() { onLoad() {
// 初始化今日日期 // 初始化今日日期
@@ -166,6 +219,8 @@
this.deadlineDate = this.queryParams.reportDate; this.deadlineDate = this.queryParams.reportDate;
// 获取餐别字典数据 // 获取餐别字典数据
this.getRangeData(); this.getRangeData();
// 加载员工信息(含吃辣偏好)
this.getEmployeeList();
// 加载报餐数据 // 加载报餐数据
this.getList(); this.getList();
this.getDeadlineConfig(); this.getDeadlineConfig();
@@ -182,7 +237,9 @@
getDeadlineConfig() { getDeadlineConfig() {
getConfigKey('hrm.meal.deadline').then(response => { getConfigKey('hrm.meal.deadline').then(response => {
this.queryParams.deadlineTime = response.msg || '16:00:00' const time = response.msg || '16:00:00';
this.queryParams.deadlineTime = time;
this.deadlineTime = time;
}) })
}, },
@@ -196,6 +253,94 @@
}); });
}, },
/** 构建员工吃辣偏好映射name -> isSpicyEater */
getEmployeeList() {
listEmployeeInfo({ pageSize: 9999, pageNum: 1 }).then(response => {
const map = {};
(response.rows || []).forEach(emp => {
// 仅在职且明确吃辣偏好的员工
if (emp.name && emp.isLeave !== 1 && emp.isSpicyEater !== undefined && emp.isSpicyEater !== null) {
map[emp.name] = emp.isSpicyEater == 1 ? 1 : 0;
}
});
this.employeeSpicyMap = map;
if (this.list.length > 0) {
this.calcTableSum();
}
}).catch(err => {
console.error('获取员工信息失败:', err);
});
},
/** 从逗号分隔的姓名列表中统计不吃辣人数 */
countNonSpicy(nameList) {
if (!nameList) return 0;
const names = nameList.split(',').map(n => n.trim()).filter(n => n);
let count = 0;
const counted = {};
names.forEach(name => {
if (!counted[name] && this.employeeSpicyMap[name] === 0) {
count++;
counted[name] = true;
}
});
return count;
},
/** 三维交叉统计:吃辣/不吃辣 × 堂食/打包 × 有效/无效 */
calcTableSum() {
const m = {
spicy: { validDine: 0, validTake: 0, validTotal: 0, invalidDine: 0, invalidTake: 0, invalidTotal: 0, dine: 0, take: 0, total: 0 },
nonSpicy: { validDine: 0, validTake: 0, validTotal: 0, invalidDine: 0, invalidTake: 0, invalidTotal: 0, dine: 0, take: 0, total: 0 },
all: { validDine: 0, validTake: 0, validTotal: 0, invalidDine: 0, invalidTake: 0, invalidTotal: 0, dine: 0, take: 0, total: 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;
// 从姓名列表统计不吃辣人数
const nonSpicyDine = this.countNonSpicy(item.dineInPeopleList);
const nonSpicyTake = this.countNonSpicy(item.takeoutPeopleList);
const spicyDine = dine - nonSpicyDine;
const spicyTake = take - nonSpicyTake;
const nonSpicyTotal = nonSpicyDine + nonSpicyTake;
const spicyTotal = spicyDine + spicyTake;
// 有效/无效
const v = this.isValidMealReport(item.createTime) ? 'valid' : 'invalid';
// 吃辣
m.spicy[v + 'Dine'] += spicyDine;
m.spicy[v + 'Take'] += spicyTake;
m.spicy[v + 'Total'] += spicyTotal;
m.spicy.dine += spicyDine;
m.spicy.take += spicyTake;
m.spicy.total += spicyTotal;
// 不吃辣
m.nonSpicy[v + 'Dine'] += nonSpicyDine;
m.nonSpicy[v + 'Take'] += nonSpicyTake;
m.nonSpicy[v + 'Total'] += nonSpicyTotal;
m.nonSpicy.dine += nonSpicyDine;
m.nonSpicy.take += nonSpicyTake;
m.nonSpicy.total += nonSpicyTotal;
// 合计
m.all[v + 'Dine'] += dine;
m.all[v + 'Take'] += take;
m.all[v + 'Total'] += total;
m.all.dine += dine;
m.all.take += take;
m.all.total += total;
});
this.matrix = m;
console.log('【移动端矩阵结果】吃辣总计:', m.spicy.total, '不吃辣总计:', m.nonSpicy.total, '合计:', m.all.total);
console.log('【移动端矩阵明细】吃辣:', JSON.stringify(m.spicy), '不吃辣:', JSON.stringify(m.nonSpicy));
},
/** 查询部门报餐列表 */ /** 查询部门报餐列表 */
getList() { getList() {
this.loading = true; this.loading = true;
@@ -209,53 +354,23 @@
}); });
}, },
/** 核心逻辑:区分有效/无效报餐统计 */ /** 判断报餐是否有效仅比较时分秒参考PC端逻辑 */
calcTableSum() { isValidMealReport(createTime) {
let validDine = 0, if (!createTime || !this.queryParams.deadlineTime) return true; // 无时间默认有效
validTake = 0,
validAll = 0; // 提取报餐时间的时分秒
let invalidDine = 0, const reportTime = new Date(createTime);
invalidTake = 0, const reportHms = this.addZero(reportTime.getHours()) + ':' +
invalidAll = 0; this.addZero(reportTime.getMinutes()) + ':' +
this.addZero(reportTime.getSeconds());
this.list.forEach(item => {
// 处理空值,转为数字 // 比较时分秒字符串格式HH:mm:ss可直接字符串比较
const dine = item.dineInPeople ? Number(item.dineInPeople) : 0; return reportHms <= this.queryParams.deadlineTime;
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) { addZero(num) {
if (!createTime) return false; return num < 10 ? '0' + num : num;
// 拼接完整的截止时间字符串
const deadlineDateTime = `${this.deadlineDate || this.queryParams.reportDate} ${this.deadlineTime}`;
// 比较时间
const createTimeObj = new Date(createTime);
const deadlineTimeObj = new Date(deadlineDateTime);
return createTimeObj <= deadlineTimeObj;
}, },
/** 报餐日期选择确认 */ /** 报餐日期选择确认 */
@@ -307,8 +422,9 @@
const hour = String(this.timeSelect.hour).padStart(2, '0'); const hour = String(this.timeSelect.hour).padStart(2, '0');
const minute = String(this.timeSelect.minute).padStart(2, '0'); const minute = String(this.timeSelect.minute).padStart(2, '0');
const second = String(this.timeSelect.second).padStart(2, '0'); const second = String(this.timeSelect.second).padStart(2, '0');
// 更新截止时间 // 更新截止时间(同步到 queryParams 供有效性判断使用)
this.deadlineTime = `${hour}:${minute}:${second}`; this.deadlineTime = `${hour}:${minute}:${second}`;
this.queryParams.deadlineTime = `${hour}:${minute}:${second}`;
// 重新计算统计数据 // 重新计算统计数据
this.calcTableSum(); this.calcTableSum();
// 关闭弹窗 // 关闭弹窗
@@ -394,6 +510,14 @@
border-top: none; border-top: none;
} }
/* 交叉表网格4列标签 + 堂食 + 打包 + 小计) */
.cross-grid {
display: grid;
grid-template-columns: 70px repeat(3, 1fr);
border: 1px solid #eee;
border-top: none;
}
/* 统计项样式 */ /* 统计项样式 */
.stats-item { .stats-item {
padding: 12px 8px; padding: 12px 8px;
@@ -404,18 +528,54 @@
font-size: 14px; font-size: 14px;
} }
/* 去掉最后一列右边框 */ /* 3列网格去掉最后一列右边框 */
.stats-grid .stats-item:nth-child(3n) { .stats-grid .stats-item:nth-child(3n) {
border-right: none; border-right: none;
} }
/* 去掉最后一行下边框 */ /* 3列网格去掉最后一行下边框 */
.stats-grid .stats-item:last-child, .stats-grid .stats-item:last-child,
.stats-grid .stats-item:nth-last-child(2), .stats-grid .stats-item:nth-last-child(2),
.stats-grid .stats-item:nth-last-child(3) { .stats-grid .stats-item:nth-last-child(3) {
border-bottom: none; border-bottom: none;
} }
/* 4列交叉网格去掉最后一列右边框 */
.cross-grid .stats-item:nth-child(4n) {
border-right: none;
}
/* 4列交叉网格去掉最后一行下边框每行4个最后4个 */
.cross-grid .stats-item:nth-last-child(-n+4) {
border-bottom: none;
}
/* 交叉表行标签(吃辣/不吃辣/小计) */
.cross-label {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-weight: 500;
color: #333;
font-size: 13px;
background-color: #fafafa;
}
/* 交叉表头(堂食/打包/小计) */
.cross-header {
font-weight: 600;
color: #555;
font-size: 12px;
background-color: #f5f7fa;
}
/* 交叉表汇总行背景 */
.cross-total {
background-color: #f9f9f9;
font-weight: 600;
}
/* 标签和值样式 */ /* 标签和值样式 */
.item-label { .item-label {
display: block; display: block;
@@ -496,12 +656,6 @@
color: #999; color: #999;
} }
.colon {
font-size: 20px;
color: #333;
margin: 0 5px;
}
/* 适配小屏手机 */ /* 适配小屏手机 */
@media (max-width: 375px) { @media (max-width: 375px) {
.stats-item { .stats-item {

View File

@@ -73,7 +73,7 @@ function checkStorageSpace() {
function checkUpdate(forceCheck = false) { function checkUpdate(forceCheck = false) {
// 1. 准备本地版本信息 // 1. 准备本地版本信息
const localVersion = plus.runtime.version; // 基座版本 const localVersion = plus.runtime.version; // 基座版本
const staticVersion = '1.3.34'; // 静态默认版本 const staticVersion = '1.3.37'; // 静态默认版本
// const localWgtVersion = staticVersion; // const localWgtVersion = staticVersion;
const localWgtVersion = uni.getStorageSync('wgtVersion') || staticVersion; // 本地wgt版本从存储获取或用默认 const localWgtVersion = uni.getStorageSync('wgtVersion') || staticVersion; // 本地wgt版本从存储获取或用默认
const currentVersion = compareVersion(localWgtVersion, localVersion) > 0 const currentVersion = compareVersion(localWgtVersion, localVersion) > 0

View File

@@ -1,5 +1,5 @@
{ {
"version": "klp 1.3.34", "version": "klp 1.3.37",
"wgtUrl": "http://49.232.154.205:10900/fadapp-update/klp/klp.wgt", "wgtUrl": "http://49.232.154.205:10900/fadapp-update/klp/klp.wgt",
"apkUrl": "http://49.232.154.205:10900/fadapp-update/klp/klp.apk" "apkUrl": "http://49.232.154.205:10900/fadapp-update/klp/klp.apk"
} }