-
+
{{ item.dishName }}
-
+
当前余量
@@ -119,72 +119,62 @@ export default {
nutritionList: [], // 营养秤实时数据
unmadeList: [], // 未制作列表
madeList: [], // 已制作列表
- speechQueue: [], // 语音播报队列 [{dishName, spoken}]
- speaking: false, // 是否正在播报
+ warningAudio: null, // 预加载的告警音频实例
+ flashTimers: {}, // 每个菜品独立的30秒定时器 { dishName: timerId }
+ flashedOut: {}, // 已过30秒的菜品 { dishName: true }
+ announcedDishes: {}, // 已播报过的菜品 { dishName: true }
}
},
created() {
+ // 预加载告警音频
+ try {
+ this.warningAudio = new Audio(require('@/assets/audio/mealSurplusInformation.mp3'));
+ this.warningAudio.load();
+ } catch (e) {
+ console.error('[Audio] 初始化失败:', e);
+ }
+
this.getKitchenInfo();
},
+ watch: {
+ mealTypeName(newVal, oldVal) {
+ if (newVal && newVal !== oldVal) {
+ // 餐次变化,重置所有菜品计时器和播报记录
+ Object.values(this.flashTimers).forEach(clearTimeout);
+ this.flashTimers = {};
+ this.flashedOut = {};
+ this.announcedDishes = {};
+ }
+ }
+ },
methods: {
- // 语音播报:Web Speech API 依次播报队列中未播报的菜品
- speakWarning() {
- if (this.speaking) return;
- const unspoken = this.speechQueue.filter(item => !item.spoken);
- if (!unspoken.length) return;
+ // 播放本地语音告警(仅在30秒窗口内、且未播报过的菜品才触发)
+ playWarningAudio() {
+ const newItems = this.nutritionList.filter(
+ item => item.lowStock && !this.flashedOut[item.dishName] && !this.announcedDishes[item.dishName]
+ );
+ if (!newItems.length) return;
- this.speaking = true;
- let index = 0;
+ if (!this.warningAudio) return;
- const utterance = new SpeechSynthesisUtterance();
- utterance.lang = 'zh-CN';
- utterance.rate = 0.9;
- utterance.volume = 1;
+ // 标记已播报(电视端由 WebView/Kiosk 配置允许自动播放)
+ newItems.forEach(item => {
+ this.$set(this.announcedDishes, item.dishName, true);
+ });
- const playNext = () => {
- if (index >= unspoken.length) {
- this.speaking = false;
- return;
- }
- const item = unspoken[index];
- index++;
- utterance.text = `${item.dishName}、余量不足,请注意补餐`;
- console.log('[TTS] 开始播报:', utterance.text);
- utterance.onend = () => {
- item.spoken = true;
- console.log('[TTS] 播报完成:', item.dishName);
- playNext();
- };
- utterance.onerror = (e) => {
- console.error('[TTS] 播报错误:', e.error);
- item.spoken = true;
- playNext();
- };
- speechSynthesis.speak(utterance);
- };
-
- playNext();
+ this.warningAudio.currentTime = 0;
+ this.warningAudio.play().catch(e => {
+ console.error('[Audio] 播放失败,请检查设备自动播放策略:', e.message);
+ });
},
getKitchenInfo() {
var params = {};
var _this = this;
- let date = new Date();
- let year = date.getFullYear();
- let month = date.getMonth() + 1;
- let day = date.getDate();
- let hour = date.getHours();
- let minute = date.getMinutes();
- let second = date.getSeconds();
- const times = "现在是:" + year + "年" + month + "月" + day + "号," + hour + "点" + minute + "分" + second + "秒";
- _this.speechQueue.push({dishName: times, spoken: false});
-
httpRequest("GET", "https://dev.yixiong-tech.com:8081/nutrition/neglect/large-screen/kitchen", params, {
"X-DEVICE-CODE": "XIAOMI-00999",
"authorization": "57ee87183f2a4fa59683ec9ef41c8f5d"
}).then(function(res) {
- _this.mealTypeName = "";
- _this.mealTime = "";
_this.nutritionList = [];
_this.unmadeList = [];
_this.madeList = [];
@@ -215,13 +205,38 @@ export default {
}
}
- // 余量不足的菜品直接推入播报队列
- _this.nutritionList.forEach(function(item) {
- if (item.lowStock) {
- _this.speechQueue.push({dishName: item.dishName, spoken: false});
+ // 清理已恢复正常菜品的所有定时器和标记(以便再次告警时重新闪烁+播报)
+ var currentLowStockNames = _this.nutritionList
+ .filter(function(item) { return item.lowStock; })
+ .map(function(item) { return item.dishName; });
+
+ Object.keys(_this.flashTimers).forEach(function(dishName) {
+ if (currentLowStockNames.indexOf(dishName) === -1) {
+ clearTimeout(_this.flashTimers[dishName]);
+ _this.$delete(_this.flashTimers, dishName);
}
});
- _this.speakWarning();
+ Object.keys(_this.flashedOut).forEach(function(dishName) {
+ if (currentLowStockNames.indexOf(dishName) === -1) {
+ _this.$delete(_this.flashedOut, dishName);
+ }
+ });
+ Object.keys(_this.announcedDishes).forEach(function(dishName) {
+ if (currentLowStockNames.indexOf(dishName) === -1) {
+ _this.$delete(_this.announcedDishes, dishName);
+ }
+ });
+ // 为每个新出现的lowStock菜品启动独立30秒计时器
+ _this.nutritionList.forEach(function(item) {
+ if (item.lowStock && !_this.flashedOut[item.dishName] && !_this.flashTimers[item.dishName]) {
+ _this.$set(_this.flashTimers, item.dishName, setTimeout(function() {
+ _this.$set(_this.flashedOut, item.dishName, true);
+ _this.$delete(_this.flashTimers, item.dishName);
+ }, 30000));
+ }
+ });
+ // 有菜品在闪烁窗口内才播报语音
+ _this.playWarningAudio();
setTimeout(function() {
_this.getKitchenInfo();
@@ -244,6 +259,10 @@ export default {
clearInterval(this.timer);
this.timer = null;
}
+ Object.values(this.flashTimers).forEach(clearTimeout);
+ this.flashTimers = {};
+ this.flashedOut = {};
+ this.announcedDishes = {};
}
}
@@ -506,6 +525,56 @@ export default {
}
}
+ &.lowStock {
+ background-color: #220203;
+ border: 2px solid #FF3232;
+ animation: lowStockFlash .3s infinite alternate;
+
+ .content_l_list_name {
+ > div:nth-of-type(1) {
+ background-color: #FF3232;
+ }
+
+ > div:nth-of-type(2) {
+ color: #FFC8C8;
+ }
+
+ .content_l_list_name_lowStock {
+ background-image: url("@/assets/images/mealSurplusInformation/danger_name_icon.png");
+ }
+ }
+
+ .content_l_list_margin {
+ color: #FFC8C8;
+
+ span {
+ color: #FF3232;
+ }
+ }
+
+ .content_l_list_status_ {
+ width: 200px;
+ background-color: #5A0A0A;
+ animation: lowStockStatusFlash .3s infinite alternate;
+
+ .content_l_list_status_icon {
+ background-image: url("@/assets/images/mealSurplusInformation/danger_status_icon.png");
+ }
+
+ .content_l_list_status_text {
+ color: #FF6464;
+ }
+ }
+
+ .content_l_list_progress {
+ background-color: rgba(200, 50, 50, .3);
+
+ > div {
+ background-color: #FF3232;
+ }
+ }
+ }
+
&.noData {
background-color: #0F1A29;
border: 2px solid #263249;
@@ -529,7 +598,7 @@ export default {
line-height: 24px;
text-align: center;
}
- }
+ }
}
}
@@ -640,4 +709,12 @@ export default {
}
}
}
+@keyframes lowStockFlash {
+ from { border-color: #FF3232; background-color: #220203; }
+ to { border-color: #801919; background-color: #110102; }
+}
+@keyframes lowStockStatusFlash {
+ from { background-color: #5A0A0A; }
+ to { background-color: #2D0505; }
+}