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",
// 应用版本
version: "1.3.34",
version: "1.3.37",
// 应用logo
logo: "/static/logo.jpg",
// 官方网站

View File

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

View File

@@ -23,51 +23,93 @@
</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="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="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>
<!-- 二行 -->
<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>
<!-- 2行吃辣 -->
<view class="stats-item cross-label">🌶 吃辣</view>
<view class="stats-item">{{ ms.dine }}</view>
<view class="stats-item">{{ ms.take }}</view>
<view class="stats-item">{{ ms.total }}</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>
<!-- 3行不吃辣 -->
<view class="stats-item cross-label">🌿 不吃辣</view>
<view class="stats-item">{{ mn.dine }}</view>
<view class="stats-item">{{ mn.take }}</view>
<view class="stats-item">{{ mn.total }}</view>
<!-- 第4行小计 -->
<view class="stats-item cross-label cross-total">小计</view>
<view class="stats-item cross-total">{{ ma.dine }}</view>
<view class="stats-item cross-total">{{ ma.take }}</view>
<view class="stats-item cross-total">{{ ma.total }}</view>
</view>
</view>
@@ -115,6 +157,9 @@
import {
listMealReport
} from "@/api/wms/mealReport";
import {
listEmployeeInfo
} from "@/api/wms/employeeInfo";
import {
getDicts
} from '@/api/system/dict/data.js'
@@ -127,13 +172,17 @@
data() {
return {
queryParams: {
mealType: '',
mealType: undefined,
reportDate: '',
deptName: undefined,
reportUserName: undefined,
status: undefined,
deadlineTime: '16:00:00',
pageSize: 9999,
pageNum: 1
},
deadlineDate: '', // 截止日期
deadlineTime: '12:00:00', // 截止时间(原始值)
deadlineDate: '',
deadlineTime: '16:00:00', // 截止时间(原始值)
// 时分秒选择器临时变量
timeSelect: {
hour: 12,
@@ -141,14 +190,14 @@
second: 0
},
list: [],
employeeSpicyMap: {}, // name -> isSpicyEater (1吃辣/0不吃辣)
loading: false,
// 统计数据
validDineIn: 0,
validTakeout: 0,
validTotal: 0,
invalidDineIn: 0,
invalidTakeout: 0,
invalidTotal: 0,
// 三维交叉统计矩阵(吃辣×打包×有效)
matrix: {
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 }
},
range: []
}
},
@@ -157,7 +206,11 @@
formattedDeadlineTime() {
const [hour, minute, second] = this.deadlineTime.split(':');
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() {
// 初始化今日日期
@@ -166,6 +219,8 @@
this.deadlineDate = this.queryParams.reportDate;
// 获取餐别字典数据
this.getRangeData();
// 加载员工信息(含吃辣偏好)
this.getEmployeeList();
// 加载报餐数据
this.getList();
this.getDeadlineConfig();
@@ -182,7 +237,9 @@
getDeadlineConfig() {
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() {
this.loading = true;
@@ -209,53 +354,23 @@
});
},
/** 核心逻辑:区分有效/无效报餐统计 */
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;
/** 判断报餐是否有效仅比较时分秒参考PC端逻辑 */
isValidMealReport(createTime) {
if (!createTime || !this.queryParams.deadlineTime) return true; // 无时间默认有效
// 提取报餐时间的时分秒
const reportTime = new Date(createTime);
const reportHms = this.addZero(reportTime.getHours()) + ':' +
this.addZero(reportTime.getMinutes()) + ':' +
this.addZero(reportTime.getSeconds());
// 比较时分秒字符串格式HH:mm:ss可直接字符串比较
return reportHms <= this.queryParams.deadlineTime;
},
/** 判断报餐是否有效:创建时间在截止时间之前则有效 */
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;
/** 数字补零 */
addZero(num) {
return num < 10 ? '0' + num : num;
},
/** 报餐日期选择确认 */
@@ -307,8 +422,9 @@
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');
// 更新截止时间
// 更新截止时间(同步到 queryParams 供有效性判断使用)
this.deadlineTime = `${hour}:${minute}:${second}`;
this.queryParams.deadlineTime = `${hour}:${minute}:${second}`;
// 重新计算统计数据
this.calcTableSum();
// 关闭弹窗
@@ -394,6 +510,14 @@
border-top: none;
}
/* 交叉表网格4列标签 + 堂食 + 打包 + 小计) */
.cross-grid {
display: grid;
grid-template-columns: 70px repeat(3, 1fr);
border: 1px solid #eee;
border-top: none;
}
/* 统计项样式 */
.stats-item {
padding: 12px 8px;
@@ -404,18 +528,54 @@
font-size: 14px;
}
/* 去掉最后一列右边框 */
/* 3列网格去掉最后一列右边框 */
.stats-grid .stats-item:nth-child(3n) {
border-right: none;
}
/* 去掉最后一行下边框 */
/* 3列网格去掉最后一行下边框 */
.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;
}
/* 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 {
display: block;
@@ -496,12 +656,6 @@
color: #999;
}
.colon {
font-size: 20px;
color: #333;
margin: 0 5px;
}
/* 适配小屏手机 */
@media (max-width: 375px) {
.stats-item {

View File

@@ -73,7 +73,7 @@ function checkStorageSpace() {
function checkUpdate(forceCheck = false) {
// 1. 准备本地版本信息
const localVersion = plus.runtime.version; // 基座版本
const staticVersion = '1.3.34'; // 静态默认版本
const staticVersion = '1.3.37'; // 静态默认版本
// const localWgtVersion = staticVersion;
const localWgtVersion = uni.getStorageSync('wgtVersion') || staticVersion; // 本地wgt版本从存储获取或用默认
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",
"apkUrl": "http://49.232.154.205:10900/fadapp-update/klp/klp.apk"
}