feat: 餐线营养秤8页全流程完善—开餐跳转02、采样取消回02、全局返回history.back()、02/03餐品点击进入01、04新增开餐/采样历史入口、非员工设置拦截、当天餐谱默认候选、自动人脸识别触发

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lenovo
2026-06-11 11:18:46 +08:00
co-authored by Claude Opus 4.6
parent a375da8792
commit dcf2757863
11 changed files with 5176 additions and 0 deletions
@@ -0,0 +1,926 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>开餐界面 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #0e1326;
color: #fff;
width: 1280px;
height: 720px;
overflow: hidden;
}
/* ---- 顶部导航栏 ---- */
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
}
.back-btn {
font-size: 14px;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
.page-title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.settings-btn {
font-size: 18px;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
transition: color 0.2s;
text-decoration: none;
}
.settings-btn:hover { color: #fff; }
/* ---- 主内容区:三列 ---- */
.main-area {
display: grid;
grid-template-columns: 360px 1fr 280px;
gap: 16px;
padding: 16px 20px;
height: calc(100% - 44px);
}
/* ===== 左列:餐品识别区 ===== */
.recognize-col { display: flex; flex-direction: column; gap: 12px; }
.col-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.45);
font-weight: 500;
letter-spacing: 1px;
}
.camera-frame {
flex: 1;
background: #0D1117;
border-radius: 12px;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.camera-frame__text {
color: rgba(255, 255, 255, 0.2);
font-size: 13px;
z-index: 1;
}
/* 十字虚线 */
.camera-frame::before, .camera-frame::after {
content: ""; position: absolute; pointer-events: none; z-index: 2;
}
.camera-frame::before {
top: 0; bottom: 0; left: 50%;
width: 1px; border-left: 1px dashed rgba(74, 144, 217, 0.25);
}
.camera-frame::after {
left: 0; right: 0; top: 50%;
height: 1px; border-top: 1px dashed rgba(74, 144, 217, 0.25);
}
/* 四角标记 */
.corner { position: absolute; width: 20px; height: 20px; z-index: 3; }
.corner--tl { top: 14px; left: 14px; border-top: 2px solid #4a90d9; border-left: 2px solid #4a90d9; }
.corner--tr { top: 14px; right: 14px; border-top: 2px solid #4a90d9; border-right: 2px solid #4a90d9; }
.corner--bl { bottom: 14px; left: 14px; border-bottom: 2px solid #4a90d9; border-left: 2px solid #4a90d9; }
.corner--br { bottom: 14px; right: 14px; border-bottom: 2px solid #4a90d9; border-right: 2px solid #4a90d9; }
/* 识别状态标签 */
.recognize-status {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
background: #1a2b4a;
border-radius: 8px;
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
}
.status-dot--ready { background: #52c41a; box-shadow: 0 0 6px #52c41a; }
.status-dot--detecting { background: #faad14; box-shadow: 0 0 6px #faad14; animation: pulse 1s infinite; }
.status-dot--fail { background: #ff4d4f; box-shadow: 0 0 6px #ff4d4f; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
/* ===== 中列:选择餐品区 ===== */
.dish-col { display: flex; flex-direction: column; gap: 12px; }
/* 选择餐品统一容器 */
.dish-container {
flex: 1;
background: #1a2b4a;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
}
/* 已选中的餐品展示 */
.picked-dish {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(74, 144, 217, 0.2);
border-radius: 10px;
padding: 12px 14px;
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
}
.picked-dish__emoji {
width: 52px; height: 52px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
}
.picked-dish__emoji svg {
width: 100%;
height: 100%;
}
.picked-dish__info { flex: 1; min-width: 0; }
.picked-dish__name { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
.picked-dish__desc { font-size: 12px; color: rgba(255, 255, 255, 0.4); }
/* 标题行(可切换状态) */
.candidate-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.candidate-title {
font-size: 13px;
color: rgba(255, 255, 255, 0.45);
font-weight: 500;
}
/* 餐品卡片列表 */
.candidate-list {
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
overflow-y: auto;
align-content: start;
}
.dish-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
padding: 0;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
}
.dish-card:hover {
border-color: rgba(74, 144, 217, 0.3);
background: rgba(74, 144, 217, 0.08);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.dish-card.selected {
border-color: #4a90d9;
box-shadow: 0 0 16px rgba(74, 144, 217, 0.2);
}
/* 食物图片区 */
.dish-card__img {
width: 100%;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.dish-card__img svg {
width: 100%;
height: 100%;
}
/* 信息区:左侧信息 + 右侧识别率 */
.dish-card__body {
display: flex;
align-items: stretch;
gap: 0;
}
.dish-card__info {
flex: 1;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.dish-card__name {
font-size: 13px;
font-weight: 600;
line-height: 1.3;
}
.dish-card__meta {
font-size: 11px;
}
.dish-card__price {
color: #0AC4A8;
font-weight: 600;
}
.dish-card__sim-wrap {
display: flex;
align-items: stretch;
justify-content: center;
padding: 0 10px 0 0;
flex-shrink: 0;
}
.dish-card__sim {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 14px;
color: #52c41a;
font-weight: 700;
background: rgba(82, 196, 26, 0.1);
border-radius: 6px;
text-align: center;
line-height: 1;
min-width: 44px;
}
.dish-card__sim .sim-label {
font-size: 8px;
font-weight: 400;
color: rgba(82, 196, 26, 0.6);
margin-top: 2px;
}
.dish-card__sim--search {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 10px;
color: rgba(255, 255, 255, 0.4);
font-weight: 500;
background: rgba(255, 255, 255, 0.04);
border-radius: 6px;
text-align: center;
line-height: 1;
min-width: 44px;
}
/* 搜索区域(常驻) */
.manual-search { display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
.search-wrap { position: relative; }
.search-input {
width: 100%;
padding: 10px 14px 10px 36px;
background: #121832;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #fff;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus { border-color: #4a90d9; }
.search-input::placeholder { color: rgba(255, 255, 255, 0.3); }
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: rgba(255, 255, 255, 0.3);
}
/* ===== 右列:重量区 ===== */
.weight-col { display: flex; flex-direction: column; gap: 14px; }
.weight-display {
flex: 1;
background: #1a2b4a;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.weight-label {
font-size: 13px;
color: rgba(255, 255, 255, 0.45);
}
.weight-value {
font-size: 56px;
font-weight: 700;
color: #4a90d9;
letter-spacing: 2px;
}
.weight-unit {
font-size: 16px;
font-weight: 400;
color: rgba(74, 144, 217, 0.6);
}
.clear-btn {
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.clear-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
.reidentify-btn {
width: 100%;
padding: 12px;
background: rgba(74, 144, 217, 0.12);
color: #4a90d9;
border: 1px solid rgba(74, 144, 217, 0.25);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.reidentify-btn:hover { background: rgba(74, 144, 217, 0.2); border-color: #4a90d9; }
.start-meal-btn {
width: 100%;
padding: 14px;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.start-meal-btn:hover { background: #3d7fc0; }
.start-meal-btn:disabled {
background: rgba(74, 144, 217, 0.25);
color: rgba(255, 255, 255, 0.35);
cursor: not-allowed;
}
/* ---- 闪烁效果 ---- */
@keyframes flash {
0% { opacity: 0; }
30% { opacity: 0.9; }
100% { opacity: 0; }
}
.flash-overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: #fff;
opacity: 0;
pointer-events: none;
border-radius: 12px;
z-index: 10;
}
.flash-overlay.active { animation: flash 0.5s ease-out; }
/* Toast */
.toast {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 10px 24px;
border-radius: 8px;
font-size: 13px;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s;
white-space: nowrap;
pointer-events: none;
}
.toast.show {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="index.html">&#x25C0;</a>
<span class="page-title">开餐界面</span>
<a class="settings-btn" href="04-settings.html" title="设置">&#x2699;</a>
</div>
<!-- 主内容区:左识别 - 中餐品 - 右重量 -->
<div class="main-area">
<!-- ===== 左列:餐品识别区 ===== -->
<div class="recognize-col">
<div class="col-label">&#x1F4F7; &#x9910;&#x54C1;&#x8BC6;&#x522B;</div>
<div class="camera-frame" id="cameraFrame">
<span class="camera-frame__text">摄像头预览区域</span>
<div class="corner corner--tl"></div>
<div class="corner corner--tr"></div>
<div class="corner corner--bl"></div>
<div class="corner corner--br"></div>
<div class="flash-overlay" id="flashOverlay"></div>
</div>
<div class="recognize-status" id="recognizeStatus">
<span class="status-dot status-dot--ready"></span>
<span>&#x8BC6;&#x522B;&#x5B8C;&#x6210;</span>
</div>
<button class="reidentify-btn" onclick="reIdentify()">&#x1F504; &#x91CD;&#x65B0;&#x8BC6;&#x522B;</button>
</div>
<!-- ===== 中列:选择餐品区 ===== -->
<div class="dish-col">
<div class="col-label">&#x1F37D;&#xFE0F; &#x9009;&#x62E9;&#x9910;&#x54C1;</div>
<!-- 统一容器:已选餐品 + 搜索 + 餐品列表 -->
<div class="dish-container">
<!-- 已选餐品展示 -->
<div class="picked-dish" id="pickedDish">
<div class="picked-dish__emoji" id="pickedEmoji">&#x1F357;</div>
<div class="picked-dish__info">
<div class="picked-dish__name" id="pickedName">宫保鸡丁</div>
<div class="picked-dish__desc" id="pickedDesc">荤菜 · 鸡肉 · 420kcal</div>
</div>
</div>
<!-- 常驻搜索栏 -->
<div class="manual-search">
<div class="search-wrap">
<span class="search-icon">&#x1F50D;</span>
<input type="text" class="search-input" id="searchInput" placeholder="&#x641C;&#x7D22;&#x9910;&#x54C1;&#x540D;&#x79F0;..." onkeydown="handleSearchKey(event)">
</div>
<div class="search-results" id="searchResults"></div>
</div>
<!-- 餐品列表(标题 + 卡片网格) -->
<div class="candidate-section" id="candidateSection">
<div class="candidate-header">
<span class="candidate-title" id="candidateTitle">&#x667A;&#x80FD;&#x8BC6;&#x522B;&#x5019;&#x9009;</span>
</div>
<div class="candidate-list" id="candidateList">
<!-- 餐品卡片由JS动态生成 -->
</div>
</div>
</div>
</div>
<!-- ===== 右列:重量区 ===== -->
<div class="weight-col">
<div class="col-label">&#x2696;&#xFE0F; &#x79E4;&#x91CD;&#x8BFB;&#x6570;</div>
<div class="weight-display">
<div class="weight-label">&#x5F53;&#x524D;&#x91CD;&#x91CF;</div>
<div class="weight-value"><span id="weightValue">285</span><span class="weight-unit"></span></div>
</div>
<button class="clear-btn" onclick="clearWeight()">&#x21BB; &#x91CD;&#x91CF;&#x6E05;&#x96F6;</button>
<button class="start-meal-btn" id="startBtn" onclick="startMeal()">&#x5F00; &#x9910;</button>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
/**
* 餐品数据库(模拟)
*/
var DISH_DB = [
{ id: 1, name: '&#x7EA2;&#x70E7;&#x8089;', emoji: '&#x1F373;', category: '&#x8364;&#x83DC;', mainIngredient: '&#x732A;&#x8089;', kcal: 480, price: 5 },
{ id: 2, name: '&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;', emoji: '&#x1F357;', category: '&#x8364;&#x83DC;', mainIngredient: '&#x9E21;&#x8089;', kcal: 420, price: 5 },
{ id: 3, name: '&#x9EBB;&#x5A46;&#x8C46;&#x8150;', emoji: '&#x1F9C6;', category: '&#x8364;&#x83DC;', mainIngredient: '&#x8C46;&#x8150;', kcal: 280, price: 4 },
{ id: 4, name: '&#x6E05;&#x7092;&#x65F6;&#x852C;', emoji: '&#x1F96C;', category: '&#x7D20;&#x83DC;', mainIngredient: '&#x83DC;&#x82B1;', kcal: 120, price: 3 },
{ id: 5, name: '&#x9999;&#x714E;&#x5E26;&#x9C7C;', emoji: '&#x1F41F;', category: '&#x8364;&#x83DC;', mainIngredient: '&#x5E26;&#x9C7C;', kcal: 320, price: 6 },
{ id: 6, name: '&#x7CD6;&#x918B;&#x6392;&#x9AA8;', emoji: '&#x1F356;', category: '&#x8364;&#x83DC;', mainIngredient: '&#x732A;&#x8089;', kcal: 520, price: 7 },
{ id: 7, name: '&#x849C;&#x8338;&#x897F;&#x5170;&#x82B1;', emoji: '&#x1F966;', category: '&#x7D20;&#x83DC;', mainIngredient: '&#x897F;&#x5170;&#x82B1;', kcal: 110, price: 3 },
{ id: 8, name: '&#x7C73;&#x996D;', emoji: '&#x1F35A;', category: '&#x4E3B;&#x98DF;', mainIngredient: '&#x5927;&#x7C73;', kcal: 260, price: 1.5 },
{ id: 9, name: '&#x7D2B;&#x83DC;&#x86CB;&#x82B1;&#x6C64;', emoji: '&#x1FAD5;', category: '&#x6C64;&#x54C1;', mainIngredient: '&#x7D2B;&#x83DC;', kcal: 80, price: 2 },
{ id: 10, name: '&#x897F;&#x7EA2;&#x67FF;&#x7092;&#x86CB;', emoji: '&#x1FAD4;', category: '&#x8364;&#x83DC;', mainIngredient: '&#x9E21;&#x86CB;', kcal: 180, price: 4 }
];
/* ===== 状态变量 ===== */
var selectedDish = null; // 用户确认的餐品
var currentWeight = 0; // 当前重量
var isRecognizing = false; // 是否正在识别
var isEmployee = true; // 是否已识别为员工(人脸识别+排班表判定)
/* ===== 拟真食物图片配置 ===== */
// 每道菜用专属渐变色模拟真实食物照片质感
var DISH_IMG_STYLES = {
1: { bg: ['#8B3A2A', '#D4764E', '#C05030'], label: '&#x7EA2;&#x70E7;&#x8089;', accent: '#FFB347' }, // 红烧肉
2: { bg: ['#C87030', '#E8A040', '#D08028'], label: '&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;', accent: '#4CAF50' }, // 宫保鸡丁
3: { bg: ['#706040', '#B89060', '#807050'], label: '&#x9EBB;&#x5A46;&#x8C46;&#x8150;', accent: '#E57373' }, // 麻婆豆腐
4: { bg: ['#306028', '#50A040', '#408030'], label: '&#x6E05;&#x7092;&#x65F6;&#x852C;', accent: '#8BC34A' }, // 清炒时蔬
5: { bg: ['#805020', '#B88040', '#A06830'], label: '&#x9999;&#x714E;&#x5E26;&#x9C7C;', accent: '#FF9800' }, // 香煎带鱼
6: { bg: ['#802018', '#C05030', '#A03820'], label: '&#x7CD6;&#x918B;&#x6392;&#x9AA8;', accent: '#FF5722' }, // 糖醋排骨
7: { bg: ['#207040', '#40A868', '#308848'], label: '&#x849C;&#x8338;&#x897F;&#x5170;&#x82B1;', accent: '#66BB6A' }, // 蒜蓉西兰花
8: { bg: ['#D0D0C0', '#E8E8D8', '#E0E0C8'], label: '&#x7C73;&#x996D;', accent: '#FFF8E1' }, // 米饭
9: { bg: ['#503028', '#805040', '#603830'], label: '&#x7D2B;&#x83DC;&#x86CB;&#x82B1;&#x6C64;', accent: '#CE93D8' }, // 紫菜蛋花汤
10: { bg: ['#E8A030', '#F0C040', '#D89020'], label: '&#x897F;&#x7EA2;&#x67FF;&#x7092;&#x86CB;', accent: '#FF7043' } // 西红柿炒蛋
};
/**
* 生成拟真食物图片 SVG
* @param {number} dishId 餐品ID
* @param {number} w 宽度
* @param {number} h 高度
*/
function dishFoodSVG(dishId, w, h) {
var style = DISH_IMG_STYLES[dishId] || { bg: ['#304050', '#506870', '#405060'], label: '', accent: '#4a90d9' };
var id = 'dishGrad' + dishId;
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">' +
'<defs>' +
'<linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + style.bg[0] + '"/>' +
'<stop offset="50%" stop-color="' + style.bg[1] + '"/>' +
'<stop offset="100%" stop-color="' + style.bg[2] + '"/>' +
'</linearGradient>' +
'</defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
// 模拟食物纹理(多层半透明椭圆)
'<ellipse cx="' + (w * 0.4) + '" cy="' + (h * 0.45) + '" rx="' + (w * 0.35) + '" ry="' + (h * 0.3) + '" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w * 0.6) + '" cy="' + (h * 0.55) + '" rx="' + (w * 0.3) + '" ry="' + (h * 0.25) + '" fill="rgba(0,0,0,0.1)"/>' +
'<ellipse cx="' + (w * 0.5) + '" cy="' + (h * 0.5) + '" rx="' + (w * 0.2) + '" ry="' + (h * 0.15) + '" fill="rgba(255,255,255,0.06)"/>' +
// 高光
'<ellipse cx="' + (w * 0.35) + '" cy="' + (h * 0.35) + '" rx="' + (w * 0.15) + '" ry="' + (h * 0.12) + '" fill="rgba(255,255,255,0.15)"/>' +
// 盘沿装饰线
'<ellipse cx="' + (w / 2) + '" cy="' + (h / 2) + '" rx="' + (w * 0.42) + '" ry="' + (h * 0.38) + '" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>' +
// 菜名标签(底部小字)
'<text x="' + (w / 2) + '" y="' + (h - 6) + '" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Microsoft YaHei,sans-serif">' + style.label + '</text>' +
'</svg>';
}
/* ===== DOM 引用 ===== */
var weightValueEl = document.getElementById('weightValue');
var candidateListEl = document.getElementById('candidateList');
var candidateSectionEl = document.getElementById('candidateSection');
var candidateTitleEl = document.getElementById('candidateTitle');
var searchInputEl = document.getElementById('searchInput');
var searchResultsEl = document.getElementById('searchResults');
var pickedDishEl = document.getElementById('pickedDish');
var pickedEmojiEl = document.getElementById('pickedEmoji');
var pickedNameEl = document.getElementById('pickedName');
var pickedDescEl = document.getElementById('pickedDesc');
var startBtnEl = document.getElementById('startBtn');
var recognizeStatusEl = document.getElementById('recognizeStatus');
var cameraFrameEl = document.getElementById('cameraFrame');
var flashOverlayEl = document.getElementById('flashOverlay');
var toastEl = document.getElementById('toast');
/* ===== 解析URL参数 ===== */
function getQueryParam(key) {
var query = window.location.search.substring(1);
var pairs = query.split('&');
for (var i = 0; i < pairs.length; i++) {
var parts = pairs[i].split('=');
if (parts[0] === key) return parts[1];
}
return null;
}
/* ===== 初始化 ===== */
function init() {
// 检查是否从采样页面返回(携带 sampleDishId
var sampleDishId = getQueryParam('sampleDishId');
if (sampleDishId) {
handleSampleReturn(parseInt(sampleDishId, 10));
return;
}
// 初始显示候选餐品(页面默认展示识别结果)
var initialCandidates = DISH_DB.slice(0, 5).sort(function() { return Math.random() - 0.5; });
showCandidates(initialCandidates);
}
/**
* 处理从采样页面返回的情况
*/
function handleSampleReturn(dishId) {
var dish = null;
for (var i = 0; i < DISH_DB.length; i++) {
if (DISH_DB[i].id === dishId) { dish = DISH_DB[i]; break; }
}
if (!dish) {
showToast('&#x26A0;&#xFE0F; &#x672A;&#x627E;&#x5230;&#x5BF9;&#x5E94;&#x9910;&#x54C1;&#xFF0C;&#x91CD;&#x65B0;&#x8BC6;&#x522B;', 1500);
setTimeout(function() { triggerRecognize(); }, 2000);
return;
}
// 显示识别完成状态
setRecognizeStatus('ready', '&#x8BC6;&#x522B;&#x5B8C;&#x6210;');
// 模拟重量变化
simulateWeight();
// 延迟后自动选中该餐品并启用开餐
setTimeout(function() {
selectedDish = dish;
pickedDishEl.style.display = '';
pickedEmojiEl.innerHTML = dishFoodSVG(dish.id, 52, 52);
pickedNameEl.textContent = dish.name;
pickedDescEl.textContent = dish.category + ' · ' + dish.mainIngredient + ' · ' + dish.kcal + 'kcal';
startBtnEl.disabled = false;
showToast('&#x2705; &#x5DF2;&#x81EA;&#x52A8;&#x9009;&#x4E2D;&#x91C7;&#x96C6;&#x9910;&#x54C1;&#xFF1A;' + dish.name, 2000);
}, 800);
// 不显示候选列表(因为是新采集的菜品,不在AI识别库中)
candidateSectionEl.style.display = 'none';
searchInputEl.value = dish.name;
}
/**
* 触发识别(模拟)
*/
function triggerRecognize() {
if (isRecognizing) return;
isRecognizing = true;
// 更新状态为识别中
setRecognizeStatus('detecting', '&#x8BC6;&#x522B;&#x4E2D;...');
// 闪烁效果
flashOverlayEl.classList.remove('active');
void flashOverlayEl.offsetWidth;
flashOverlayEl.classList.add('active');
// 模拟重量变化
simulateWeight();
// 2秒后出识别结果
setTimeout(function() {
isRecognizing = false;
setRecognizeStatus('ready', '&#x8BC6;&#x522B;&#x5B8C;&#x6210;');
var shuffled = DISH_DB.slice().sort(function() { return Math.random() - 0.5; });
var candidates = shuffled.slice(0, 5);
showCandidates(candidates);
}, 2000);
}
/**
* 模拟重量变化
*/
function simulateWeight() {
var target = Math.floor(Math.random() * 350) + 50;
var step = 0;
var interval = setInterval(function() {
step += Math.floor(Math.random() * 20) + 5;
if (step >= target) {
step = target;
clearInterval(interval);
currentWeight = target;
}
weightValueEl.textContent = step;
}, 100);
}
/**
* 显示候选列表(卡片形式)
*/
function showCandidates(list) {
candidateSectionEl.style.display = '';
candidateTitleEl.innerHTML = '&#x667A;&#x80FD;&#x8BC6;&#x522B;&#x5019;&#x9009;';
// 清空搜索
searchInputEl.value = '';
searchResultsEl.innerHTML = '';
if (list.length === 0) {
candidateListEl.innerHTML = '<div style="text-align:center;color:rgba(255,255,255,0.25);padding:40px 0;font-size:13px;grid-column:1/-1;">&#x7B49;&#x5F85;&#x9910;&#x54C1;&#x653E;&#x5165;&#x79E4;&#x4F53;...</div>';
return;
}
var html = '';
for (var i = 0; i < list.length; i++) {
var d = list[i];
var sim = Math.floor(Math.random() * 12) + 88; // 88%~99%
html += '<div class="dish-card" data-id="' + d.id + '" onclick="pickDish(' + d.id + ', ' + sim + ')">' +
'<div class="dish-card__img">' + dishFoodSVG(d.id, 160, 80) + '</div>' +
'<div class="dish-card__body">' +
'<div class="dish-card__info">' +
'<div class="dish-card__name">' + d.name + '</div>' +
'<div class="dish-card__meta">' +
'<span class="dish-card__price">' + d.price + '&#x5143;/100&#x514B;</span>' +
'</div>' +
'</div>' +
'<div class="dish-card__sim-wrap">' +
'<div class="dish-card__sim">' + sim + '%<span class="sim-label">&#x8BC6;&#x522B;&#x7387;</span></div>' +
'</div>' +
'</div>' +
'</div>';
}
candidateListEl.innerHTML = html;
}
/**
* 显示搜索结果(卡片形式)
*/
function showSearchResults(list) {
candidateSectionEl.style.display = '';
candidateTitleEl.innerHTML = '&#x641C;&#x7D22;&#x5230;&#x7684;&#x9910;&#x54C1;';
if (list.length === 0) {
candidateListEl.innerHTML = '<div style="text-align:center;color:rgba(255,255,255,0.25);padding:20px 0;font-size:12px;grid-column:1/-1;">&#x672A;&#x627E;&#x5230;&#x5339;&#x914D;&#x7ED3;&#x679C;</div>';
return;
}
var html = '';
for (var i = 0; i < list.length; i++) {
var d = list[i];
html += '<div class="dish-card" data-id="' + d.id + '" onclick="pickDish(' + d.id + ', 100)">' +
'<div class="dish-card__img">' + dishFoodSVG(d.id, 160, 80) + '</div>' +
'<div class="dish-card__body">' +
'<div class="dish-card__info">' +
'<div class="dish-card__name">' + d.name + '</div>' +
'<div class="dish-card__meta">' +
'<span class="dish-card__price">' + d.price + '&#x5143;/100&#x514B;</span>' +
'</div>' +
'</div>' +
'<div class="dish-card__sim-wrap">' +
'<div class="dish-card__sim--search">&#x624B;&#x52A8;<span class="sim-label">&#x641C;&#x7D22;</span></div>' +
'</div>' +
'</div>' +
'</div>';
}
candidateListEl.innerHTML = html;
}
/**
* 用户选择候选餐品
*/
function pickDish(id, sim) {
var dish = null;
for (var i = 0; i < DISH_DB.length; i++) {
if (DISH_DB[i].id === id) { dish = DISH_DB[i]; break; }
}
if (!dish) return;
selectedDish = dish;
// 更新已选展示
pickedDishEl.style.display = '';
pickedEmojiEl.innerHTML = dishFoodSVG(dish.id, 52, 52);
pickedNameEl.textContent = dish.name;
pickedDescEl.textContent = dish.category + ' · ' + dish.mainIngredient + ' · ' + dish.kcal + 'kcal';
// 高亮选中项
var items = candidateListEl.querySelectorAll('.dish-card');
for (var i = 0; i < items.length; i++) items[i].classList.remove('selected');
var target = candidateListEl.querySelector('[data-id="' + id + '"]');
if (target) target.classList.add('selected');
// 启用开餐按钮
startBtnEl.disabled = false;
showToast('&#x2705; &#x5DF2;&#x9009;&#x4E2D;&#xFF1A;' + dish.name + '&#x76F8;&#x4F3C;&#x5EA6; ' + sim + '%&#xFF09;', 1500);
}
/**
* 搜索框按键处理
*/
function handleSearchKey(event) {
if (event.key === 'Enter') {
event.preventDefault();
var kw = searchInputEl.value.replace(/^\s+|\s+$/g, '').toLowerCase();
if (!kw) {
searchResultsEl.innerHTML = '';
return;
}
var results = [];
for (var i = 0; i < DISH_DB.length; i++) {
if (DISH_DB[i].name.toLowerCase().indexOf(kw) !== -1) {
results.push(DISH_DB[i]);
}
}
// 进入搜索模式,标题切换
candidateTitleEl.innerHTML = '&#x641C;&#x7D22;&#x5230;&#x7684;&#x9910;&#x54C1;';
showSearchResults(results);
}
}
/**
* 重量清零
*/
function clearWeight() {
currentWeight = 0;
weightValueEl.textContent = '0';
selectedDish = null;
pickedDishEl.style.display = 'none';
startBtnEl.disabled = true;
showCandidates([]);
setRecognizeStatus('ready', '&#x7B49;&#x5F85;&#x653E;&#x5165;&#x9910;&#x54C1;');
// 3秒后重新检测
setTimeout(function() {
triggerRecognize();
}, 3000);
}
/**
* 重新识别
*/
function reIdentify() {
// 重置状态
selectedDish = null;
pickedDishEl.style.display = 'none';
startBtnEl.disabled = true;
currentWeight = 0;
weightValueEl.textContent = '0';
// 清空搜索
searchInputEl.value = '';
searchResultsEl.innerHTML = '';
// 触发识别
triggerRecognize();
showToast('&#x1F504; &#x6B63;&#x5728;&#x91CD;&#x65B0;&#x8BC6;&#x522B;...', 1500);
}
/**
* 开餐
*/
function startMeal() {
if (!selectedDish) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x9009;&#x62E9;&#x9910;&#x54C1;', 1500);
return;
}
// 自动提交1张图片到采样库
submitToSampling(selectedDish);
showToast('&#x2705; &#x5F00;&#x9910;&#x6210;&#x529F;&#xFF01;' + selectedDish.name + ' · ' + currentWeight + '克', 2000);
// 跳转至 02-待取餐界面
setTimeout(function() {
window.location.href = '02-dish-display.html?dishId=' + selectedDish.id;
}, 300);
}
/**
* 自动提交当前菜品1张图片到采样库
*/
function submitToSampling(dish) {
var storageKey = 'sampling_' + dish.id;
var existing = null;
try { existing = JSON.parse(localStorage.getItem(storageKey)); } catch(e) {}
if (!existing) {
existing = { dishId: dish.id, name: dish.name, count: 0, images: [] };
}
existing.count += 1;
existing.images.push({ time: new Date().toLocaleString('zh-CN') });
localStorage.setItem(storageKey, JSON.stringify(existing));
}
/**
* 更新识别状态显示
*/
function setRecognizeStatus(type, text) {
var dotClass = 'status-dot--' + type;
recognizeStatusEl.innerHTML = '<span class="status-dot ' + dotClass + '"></span><span>' + text + '</span>';
}
/**
* Toast 提示
*/
function showToast(msg, duration) {
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastEl._timer);
var t = duration || 2000;
toastEl._timer = setTimeout(function() {
toastEl.classList.remove('show');
}, t);
}
/* ===== 非员工权限拦截(设置按钮) ===== */
var settingsBtn = document.querySelector('.settings-btn');
settingsBtn.addEventListener('click', function(e) {
if (!isEmployee) {
e.preventDefault();
showToast('&#x26A0;&#xFE0F; &#x975E;&#x9910;&#x5385;&#x5458;&#x5DE5;&#xFF0C;&#x65E0;&#x6743;&#x64CD;&#x4F5C;&#x7CFB;&#x7EDF;', 2000);
return;
}
});
/* 启动 */
init();
</script>
</body>
</html>
@@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>取餐界面 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
width: 1280px;
min-height: 720px;
overflow: hidden;
/* 深蓝渐变背景 */
background: linear-gradient(135deg, #0a1628 0%, #0d1f40 50%, #080f1e 100%);
color: #fff;
}
/* ---- 顶部窗口栏 ---- */
.top-bar {
height: 32px;
background: #0a1628;
display: flex;
align-items: center;
padding: 0 16px;
font-size: 12px;
color: rgba(255,255,255,0.4);
}
/* ---- 主内容区 ---- */
.main-area {
height: calc(100vh - 32px);
position: relative;
}
/* 居中大标题 */
.center-heading {
position: absolute;
top: 60px;
left: 0;
right: 0;
text-align: center;
}
.big-text {
font-size: 56px;
font-weight: 700;
color: #FF9800;
letter-spacing: 6px;
text-shadow: 0 0 30px rgba(255,152,0,0.25), 0 2px 10px rgba(0,0,0,0.3);
}
/* 底部菜品信息 */
.dish-info {
position: absolute;
bottom: 60px;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
cursor: pointer;
padding: 16px 24px;
border-radius: 12px;
transition: background 0.2s;
}
.dish-info:hover {
background: rgba(74,144,217,0.08);
}
.dish-img-box {
width: 120px;
height: 90px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.dish-img-box svg {
width: 100%;
height: 100%;
}
.dish-info-text {
display: flex;
flex-direction: column;
gap: 8px;
}
.dish-name-row {
font-size: 32px;
font-weight: 600;
}
.dish-name-row .price-info {
font-size: 22px;
font-weight: 400;
color: rgba(255,255,255,0.7);
}
.dish-name-row .price-num {
color: #0AC4A8;
font-weight: 600;
}
.dish-cal-row {
font-size: 22px;
color: rgba(255,255,255,0.6);
}
.dish-cal-row .cal-num {
color: #0AC4A8;
font-weight: 600;
}
.dish-hint {
font-size: 12px;
color: rgba(255,255,255,0.25);
margin-top: 4px;
text-align: center;
}
/* ---- 设置按钮 ---- */
.settings-btn {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
font-size: 16px;
color: rgba(255,255,255,0.6);
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
z-index: 10;
}
.settings-btn:hover {
background: rgba(255,255,255,0.1);
color: #fff;
}
/* Toast */
.toast {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0,0,0,0.8);
color: #fff;
padding: 10px 24px;
border-radius: 8px;
font-size: 13px;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s;
white-space: nowrap;
pointer-events: none;
}
.toast.show {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<!-- 顶部窗口栏 -->
<div class="top-bar">
<span>Phone-192.168.1.165:5555</span>
</div>
<!-- 设置按钮 -->
<a class="settings-btn" id="settingsBtn" title="设置">&#x2699;</a>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- 主内容区 -->
<div class="main-area">
<!-- 中央大标题 -->
<div class="center-heading">
<div class="big-text">先识别 后取餐</div>
</div>
<!-- 底部菜品信息(点击进入开餐界面) -->
<div class="dish-info" id="dishInfo">
<div class="dish-img-box" id="dishImg"></div>
<div class="dish-info-text">
<div class="dish-name-row">
<span id="dishName">宫保鸡丁</span>
<span class="price-info">(单价 <span class="price-num" id="dishPrice">5</span> 元/100克)</span>
</div>
<div class="dish-cal-row">
热量 <span class="cal-num" id="dishKcal">158.66</span> 千卡/100克
</div>
<div class="dish-hint">点击菜品信息 &#x2192; 进入开餐界面</div>
</div>
</div>
</div>
<script>
/**
* 待取餐界面交互逻辑
*/
/* ===== 餐品数据映射(模拟后端) ===== */
var DISH_MAP = {
1: { name: '&#x7EA2;&#x70E7;&#x8089;', price: 5, kcal: 480, colors: ['#8B3A2A', '#D4764E', '#C05030'] },
2: { name: '&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;', price: 5, kcal: 420, colors: ['#C87030', '#E8A040', '#D08028'] },
3: { name: '&#x9EBB;&#x5A46;&#x8C46;&#x8150;', price: 4, kcal: 280, colors: ['#706040', '#B89060', '#807050'] },
4: { name: '&#x6E05;&#x7092;&#x65F6;&#x852C;', price: 3, kcal: 120, colors: ['#306028', '#50A040', '#408030'] },
5: { name: '&#x9999;&#x714E;&#x5E26;&#x9C7C;', price: 6, kcal: 320, colors: ['#805020', '#B88040', '#A06830'] },
6: { name: '&#x7CD6;&#x918B;&#x6392;&#x9AA8;', price: 7, kcal: 520, colors: ['#802018', '#C05030', '#A03820'] },
7: { name: '&#x849C;&#x8338;&#x897F;&#x5170;&#x82B1;', price: 3, kcal: 110, colors: ['#207040', '#40A868', '#308848'] },
8: { name: '&#x7C73;&#x996D;', price: 1.5, kcal: 260, colors: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] },
9: { name: '&#x7D2B;&#x83DC;&#x86CB;&#x82B1;&#x6C64;', price: 2, kcal: 80, colors: ['#503028', '#805040', '#603830'] },
10: { name: '&#x897F;&#x7EA2;&#x67FF;&#x7092;&#x86CB;', price: 4, kcal: 180, colors: ['#E8A030', '#F0C040', '#D89020'] }
};
/* ===== 解析 URL 参数 ===== */
function getQueryParam(key) {
var query = window.location.search.substring(1);
var pairs = query.split('&');
for (var i = 0; i < pairs.length; i++) {
var parts = pairs[i].split('=');
if (parts[0] === key) return parts[1];
}
return null;
}
/* ===== 拟真食物图片SVG ===== */
function dishFoodSVG(w, h, colors) {
var id = 'dishGrad';
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + colors[0] + '"/>' +
'<stop offset="50%" stop-color="' + colors[1] + '"/>' +
'<stop offset="100%" stop-color="' + colors[2] + '"/></linearGradient></defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
'<ellipse cx="' + (w*0.4) + '" cy="' + (h*0.45) + '" rx="' + (w*0.35) + '" ry="' + (h*0.3) + '" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w*0.6) + '" cy="' + (h*0.55) + '" rx="' + (w*0.3) + '" ry="' + (h*0.25) + '" fill="rgba(0,0,0,0.1)"/>' +
'<ellipse cx="' + (w*0.5) + '" cy="' + (h*0.5) + '" rx="' + (w*0.2) + '" ry="' + (h*0.15) + '" fill="rgba(255,255,255,0.06)"/>' +
'<ellipse cx="' + (w*0.35) + '" cy="' + (h*0.35) + '" rx="' + (w*0.15) + '" ry="' + (h*0.12) + '" fill="rgba(255,255,255,0.15)"/>' +
'<ellipse cx="' + (w/2) + '" cy="' + (h/2) + '" rx="' + (w*0.42) + '" ry="' + (h*0.38) + '" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>' +
'</svg>';
}
/* ===== 初始化 ===== */
var dishId = getQueryParam('dishId');
var dishData = (dishId && DISH_MAP[dishId]) || DISH_MAP[2]; // 默认宫保鸡丁
document.getElementById('dishImg').innerHTML = dishFoodSVG(120, 90, dishData.colors);
document.getElementById('dishName').textContent = dishData.name;
document.getElementById('dishPrice').textContent = dishData.price;
document.getElementById('dishKcal').textContent = dishData.kcal;
/* ===== 点击菜品信息 → 进入开餐界面 ===== */
document.getElementById('dishInfo').addEventListener('click', function() {
window.location.href = '01-meal-start.html';
});
/* ===== 模拟人脸识别(站在设备前自动触发) ===== */
var faceDetectTimer = null;
var isEmployee = false; // 02 默认买家态(买家身份进入)
function startFaceDetect() {
// 模拟:站在设备前 3 秒后识别成功
faceDetectTimer = setTimeout(function() {
// 识别成功 → 跳转取餐界面
window.location.href = '03-meal-pickup.html?dishId=' + dishId;
}, 3000);
}
// 页面加载后自动启动人脸识别
startFaceDetect();
/* ===== Toast 提示 ===== */
var toastEl = document.getElementById('toast');
function showToast(msg, duration) {
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastEl._timer);
var t = duration || 2000;
toastEl._timer = setTimeout(function() {
toastEl.classList.remove('show');
}, t);
}
/* ===== 设置按钮点击 → 非员工拦截 ===== */
document.getElementById('settingsBtn').addEventListener('click', function(e) {
e.preventDefault();
if (!isEmployee) {
showToast('&#x26A0;&#xFE0F; &#x975E;&#x9910;&#x5385;&#x5458;&#x5DE5;&#xFF0C;&#x65E0;&#x6743;&#x64CD;&#x4F5C;&#x7CFB;&#x7EDF;', 2000);
return;
}
window.location.href = '04-settings.html';
});
</script>
</body>
</html>
@@ -0,0 +1,289 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>取餐信息屏(大屏)- 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
width: 1280px;
height: 720px;
overflow: hidden;
background: linear-gradient(135deg, #0a1628 0%, #0d1f40 50%, #080f1e 100%);
color: #fff;
}
.main-area {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px 14px;
gap: 12px;
}
.settings-btn {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
font-size: 12px;
color: rgba(255,255,255,0.5);
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
z-index: 10;
}
.settings-btn:hover {
background: rgba(255,255,255,0.1);
color: #fff;
}
.info-row {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.info-card {
flex: 1;
background: #1a2b4a;
border-radius: 10px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
position: relative;
}
.info-card__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90d9, #3570ad);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.info-card__body { flex: 1; min-width: 0; }
.info-card__name { font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.info-card__id { font-size: 10px; color: rgba(255,255,255,0.4); margin-bottom: 4px; }
.info-card__heat { display: flex; align-items: baseline; gap: 4px; }
.info-card__heat-label { font-size: 11px; color: rgba(255,255,255,0.45); }
.info-card__heat-value { font-size: 18px; font-weight: 700; color: #0AC4A8; line-height: 1; }
.info-card__heat-unit { font-size: 12px; color: rgba(10,196,168,0.6); font-weight: 400; }
.content-row {
display: flex;
flex: 1;
gap: 12px;
min-height: 0;
}
.dish-display {
flex: 1;
background: #1a2b4a;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
}
.dish-info {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
}
.dish-img-box {
width: 120px;
height: 90px;
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.dish-img-box svg { width: 100%; height: 100%; }
.dish-info-text { display: flex; flex-direction: column; gap: 8px; }
.dish-name-row { font-size: 32px; font-weight: 600; }
.dish-name-row .price-info { font-size: 22px; font-weight: 400; color: rgba(255,255,255,0.7); }
.dish-name-row .price-num { color: #0AC4A8; font-weight: 600; }
.dish-cal-row { font-size: 22px; color: rgba(255,255,255,0.6); }
.dish-cal-row .cal-num { color: #0AC4A8; font-weight: 600; }
.pickup-section {
width: 320px;
background: #1a2b4a;
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
.pickup-section__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px 8px;
flex-shrink: 0;
}
.pickup-section__header .section-label {
font-size: 12px;
color: rgba(255,255,255,0.45);
letter-spacing: 0.5px;
}
.pickup-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0 16px;
}
.pickup-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 8px 0;
font-size: 11px;
color: rgba(255,255,255,0.4);
border-bottom: 1px solid rgba(255,255,255,0.08);
flex-shrink: 0;
}
.pickup-list {
flex: 1;
overflow-y: auto;
padding: 0;
}
.pickup-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 10px 0;
font-size: 12px;
color: rgba(255,255,255,0.7);
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.pickup-footer {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 10px 0;
font-size: 13px;
font-weight: 600;
border-top: 1px solid rgba(255,255,255,0.12);
flex-shrink: 0;
}
.pickup-footer .lbl { color: rgba(255,255,255,0.55); }
.pickup-footer .val { color: #fff; }
.pickup-footer .price-val { color: #0AC4A8; }
.toast {
position: fixed;
top: 40px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 10px 24px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s;
white-space: nowrap;
pointer-events: none;
}
.toast.show { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<div class="toast" id="toast"></div>
<div class="main-area">
<!-- 用户信息 -->
<div class="info-row">
<div class="info-card">
<div class="info-card__avatar">&#x1F464;</div>
<div class="info-card__body">
<div class="info-card__name">&#x5F20;&#x4E09;</div>
<div class="info-card__id">ID: 98369</div>
<div class="info-card__heat">
<span class="info-card__heat-label">&#x63A8;&#x8350;&#x70ED;&#x91CF;</span>
<span class="info-card__heat-value">720</span>
<span class="info-card__heat-unit">&#x5343;&#x5361;</span>
</div>
</div>
<a class="settings-btn" id="settingsBtn" title="设置">&#x2699;</a>
</div>
</div>
<!-- 左侧餐品展示 + 右侧取餐记录 -->
<div class="content-row">
<div class="dish-display">
<div class="dish-info">
<div class="dish-img-box" id="dishImg"></div>
<div class="dish-info-text">
<div class="dish-name-row">
&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;
<span class="price-info">(单价 <span class="price-num">5</span> &#x5143;/100&#x514B;</span>
</div>
<div class="dish-cal-row">
热量 <span class="cal-num">158.66</span> &#x5343;&#x5361;/100&#x514B;
</div>
</div>
</div>
</div>
<div class="pickup-section">
<div class="pickup-section__header">
<span class="section-label">&#x1F9FE; &#x53D6;&#x9910;&#x8BB0;&#x5F55;</span>
</div>
<div class="pickup-table">
<div class="pickup-header">
<span>&#x9910;&#x54C1;&#x540D;&#x79F0;</span>
<span>&#x91CD;&#x91CF;(&#x514B;)</span>
<span>&#x5C0F;&#x8BA1;(&#x5143;)</span>
</div>
<div class="pickup-list">
<div class="pickup-row">
<span>&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;</span><span>186</span><span>9.30</span>
</div>
<div class="pickup-row">
<span>&#x5927;&#x7C73;&#x996D;</span><span>200</span><span>3.00</span>
</div>
<div class="pickup-row">
<span>&#x6E05;&#x7092;&#x65F6;&#x852C;</span><span>120</span><span>3.60</span>
</div>
<div class="pickup-row">
<span>&#x7D2B;&#x83DC;&#x86CB;&#x82B1;&#x6C64;</span><span>250</span><span>5.00</span>
</div>
</div>
<div class="pickup-footer">
<span class="lbl">&#x5408;&#x8BA1;</span>
<span class="val">756&#x514B;</span>
<span class="price-val">20.90&#x5143;</span>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('settingsBtn').addEventListener('click', function(e) {
e.preventDefault();
window.location.href = '04-settings.html';
});
function dishFoodSVG(w, h, colors) {
var id = 'dg';
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + colors[0] + '"/>' +
'<stop offset="50%" stop-color="' + colors[1] + '"/>' +
'<stop offset="100%" stop-color="' + colors[2] + '"/></linearGradient></defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
'<ellipse cx="' + (w*0.4) + '" cy="' + (h*0.45) + '" rx="' + (w*0.3) + '" ry="' + (h*0.25) + '" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w*0.6) + '" cy="' + (h*0.55) + '" rx="' + (w*0.25) + '" ry="' + (h*0.2) + '" fill="rgba(0,0,0,0.1)"/>' +
'<ellipse cx="' + (w*0.35) + '" cy="' + (h*0.35) + '" rx="' + (w*0.12) + '" ry="' + (h*0.1) + '" fill="rgba(255,255,255,0.15)"/>' +
'</svg>';
}
document.getElementById('dishImg').innerHTML = dishFoodSVG(120, 90, ['#C87030', '#E8A040', '#D08028']);
</script>
</body>
</html>
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=480, initial-scale=1.0">
<title>取餐营养屏(小屏)- 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
width: 480px;
height: 320px;
overflow: hidden;
background: linear-gradient(135deg, #0a1628 0%, #0d1f40 50%, #080f1e 100%);
color: #fff;
}
.screen-frame {
width: 480px;
height: 320px;
background: linear-gradient(180deg, #1a2b4a 0%, #142038 100%);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(74, 144, 217, 0.15);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border-bottom: 1px solid rgba(255,255,255,0.06);
flex-shrink: 0;
}
.header .n-title {
font-size: 9px;
font-weight: 600;
color: #fff;
}
.header .n-meal {
font-size: 8px;
color: rgba(255,255,255,0.45);
}
.header .n-time {
font-size: 7px;
color: rgba(255,255,255,0.3);
margin-left: 6px;
}
.cyl-group {
flex: 1;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 28px;
padding: 10px 16px 8px;
}
.cyl-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.cyl-value {
font-size: 10px;
font-weight: 700;
color: #fff;
text-align: center;
line-height: 1.3;
}
.cyl-value .cyl-unit {
font-size: 7px;
font-weight: 400;
opacity: 0.7;
}
.cyl-svg { display: block; }
.cyl-label {
font-size: 8px;
color: rgba(255,255,255,0.6);
text-align: center;
}
</style>
</head>
<body>
<div class="screen-frame">
<div class="header">
<span class="n-title">&#x1F4CA; &#x672C;&#x9910;&#x6B21;&#x6444;&#x5165;</span>
<span>
<span class="n-meal">&#x5F53;&#x524D;&#x9910;&#x6B21;&#xFF1A;&#x65E9;&#x9910;</span>
<span class="n-time">06:00 ~ 10:19</span>
</span>
</div>
<div class="cyl-group">
<!-- 热量柱 -->
<div class="cyl-item">
<div class="cyl-value">1070<span class="cyl-unit">千卡</span></div>
<svg class="cyl-svg" width="72" height="190" viewBox="0 0 72 190">
<defs>
<linearGradient id="heatGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#cc1111"/>
<stop offset="50%" stop-color="#ff2020"/>
<stop offset="100%" stop-color="#cc1111"/>
</linearGradient>
</defs>
<ellipse cx="36" cy="8" rx="32" ry="7" fill="#cc1111"/>
<rect x="4" y="8" width="64" height="175" fill="url(#heatGrad)"/>
<rect x="8" y="8" width="8" height="175" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="36" cy="8" rx="30" ry="6" fill="rgba(255,255,255,0.15)"/>
<text x="36" y="178" text-anchor="middle" font-size="14">&#x1F525;</text>
</svg>
<div class="cyl-label">热量</div>
</div>
<!-- 主食柱 -->
<div class="cyl-item">
<div class="cyl-value">68<span class="cyl-unit"></span></div>
<svg class="cyl-svg" width="56" height="120" viewBox="0 0 56 120">
<defs>
<linearGradient id="stapleGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#68a868"/>
<stop offset="50%" stop-color="#88cc88"/>
<stop offset="100%" stop-color="#68a868"/>
</linearGradient>
</defs>
<ellipse cx="28" cy="8" rx="26" ry="6" fill="#68a868"/>
<rect x="2" y="8" width="52" height="105" fill="url(#stapleGrad)"/>
<rect x="6" y="8" width="6" height="105" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="28" cy="8" rx="24" ry="5" fill="rgba(255,255,255,0.15)"/>
<text x="28" y="108" text-anchor="middle" font-size="12">&#x1F35A;</text>
</svg>
<div class="cyl-label">主食</div>
</div>
<!-- 果蔬柱 -->
<div class="cyl-item">
<div class="cyl-value">68<span class="cyl-unit"></span></div>
<svg class="cyl-svg" width="56" height="120" viewBox="0 0 56 120">
<defs>
<linearGradient id="vegGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#68a868"/>
<stop offset="50%" stop-color="#88cc88"/>
<stop offset="100%" stop-color="#68a868"/>
</linearGradient>
</defs>
<ellipse cx="28" cy="8" rx="26" ry="6" fill="#68a868"/>
<rect x="2" y="8" width="52" height="105" fill="url(#vegGrad)"/>
<rect x="6" y="8" width="6" height="105" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="28" cy="8" rx="24" ry="5" fill="rgba(255,255,255,0.15)"/>
<text x="28" y="108" text-anchor="middle" font-size="12">&#x1F957;</text>
</svg>
<div class="cyl-label">果蔬</div>
</div>
<!-- 肉蛋柱 -->
<div class="cyl-item">
<div class="cyl-value">68<span class="cyl-unit"></span></div>
<svg class="cyl-svg" width="56" height="105" viewBox="0 0 56 105">
<defs>
<linearGradient id="meatGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#cc1111"/>
<stop offset="50%" stop-color="#ff2020"/>
<stop offset="100%" stop-color="#cc1111"/>
</linearGradient>
</defs>
<ellipse cx="28" cy="8" rx="26" ry="6" fill="#cc1111"/>
<rect x="2" y="8" width="52" height="90" fill="url(#meatGrad)"/>
<rect x="6" y="8" width="6" height="90" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="28" cy="8" rx="24" ry="5" fill="rgba(255,255,255,0.15)"/>
<text x="28" y="94" text-anchor="middle" font-size="12">&#x1F969;</text>
</svg>
<div class="cyl-label">肉蛋</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,523 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>取餐界面 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
width: 1280px;
height: 720px;
overflow: hidden;
background: linear-gradient(135deg, #0a1628 0%, #0d1f40 50%, #080f1e 100%);
color: #fff;
}
/* ---- 主内容区:两列 ---- */
.main-area {
height: 100%;
display: grid;
grid-template-columns: 1fr 320px;
padding: 16px 20px;
gap: 16px;
}
/* ===== 中列:信息 + 营养 ===== */
.center-column {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 用户信息 + 餐品信息 横排 */
.info-row {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.info-card {
flex: 1;
background: #1a2b4a;
border-radius: 10px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.info-card__avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90d9, #3570ad);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.info-card__body { flex: 1; min-width: 0; }
.info-card__name { font-size: 15px; font-weight: 600; }
.info-card__id { font-size: 11px; color: rgba(255,255,255,0.4); }
.info-card__right { text-align: right; flex-shrink: 0; }
.info-card__label { font-size: 10px; color: rgba(255,255,255,0.4); }
.info-card__value { font-size: 17px; font-weight: 700; color: #0AC4A8; }
.info-card__unit { font-size: 10px; font-weight: 400; color: rgba(10,196,168,0.6); }
.info-card__emoji {
width: 48px;
height: 48px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.info-card__emoji svg {
width: 100%;
height: 100%;
}
.info-card--dish {
cursor: pointer;
transition: background 0.2s;
}
.info-card--dish:hover {
background: rgba(74,144,217,0.12);
}
.info-card__dish-name { font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.info-card__dish-detail { font-size: 11px; color: rgba(255,255,255,0.45); line-height: 1.4; }
.info-card__dish-detail em { font-style: normal; color: #0AC4A8; font-weight: 600; }
/* 营养标题行 */
.nutrition-header-row {
display: flex;
align-items: center;
justify-content: space-between;
background: #1a2b4a;
border-radius: 10px;
padding: 12px 20px;
flex-shrink: 0;
}
.nutrition-header-row .n-title {
font-size: 16px;
font-weight: 600;
color: #fff;
}
.nutrition-header-row .n-meal {
font-size: 13px;
color: rgba(255,255,255,0.5);
}
.nutrition-header-row .n-time {
font-size: 12px;
color: rgba(255,255,255,0.35);
margin-left: 12px;
}
/* 圆柱形营养展示区 */
.nutrition-core {
flex: 1;
background: linear-gradient(180deg, #1a2b4a 0%, #142038 100%);
border-radius: 14px;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 40px 40px 32px;
gap: 32px;
position: relative;
overflow: hidden;
}
/* 圆柱组 */
.cyl-group {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 32px;
width: 100%;
padding-bottom: 16px;
}
/* 单个圆柱 */
.cyl-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
/* 圆柱数值(柱顶) */
.cyl-value {
font-size: 18px;
font-weight: 700;
color: #fff;
text-align: center;
line-height: 1.2;
}
.cyl-value .cyl-unit {
font-size: 12px;
font-weight: 400;
opacity: 0.7;
}
/* 圆柱SVG */
.cyl-svg { display: block; }
/* 底部标签 */
.cyl-label {
font-size: 14px;
color: rgba(255,255,255,0.7);
text-align: center;
}
/* ===== 右列:取餐记录 ===== */
.right-column {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 设置按钮 */
.settings-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
font-size: 16px;
color: rgba(255,255,255,0.6);
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
flex-shrink: 0;
}
.settings-btn:hover {
background: rgba(255,255,255,0.1);
color: #fff;
}
.pickup-section {
flex: 1;
background: #1a2b4a;
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pickup-section__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px 8px;
flex-shrink: 0;
}
.pickup-section__header .section-label {
font-size: 12px;
color: rgba(255,255,255,0.4);
letter-spacing: 0.5px;
}
.pickup-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0 16px;
}
.pickup-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 8px 0;
font-size: 12px;
color: rgba(255,255,255,0.45);
border-bottom: 1px solid rgba(255,255,255,0.08);
flex-shrink: 0;
}
.pickup-list {
flex: 1;
overflow-y: auto;
padding: 0;
}
.pickup-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 10px 0;
font-size: 13px;
color: rgba(255,255,255,0.7);
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.pickup-footer {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 10px 0;
font-size: 13px;
font-weight: 600;
border-top: 1px solid rgba(255,255,255,0.12);
flex-shrink: 0;
}
.pickup-footer .lbl { color: rgba(255,255,255,0.55); }
.pickup-footer .val { color: #fff; }
.pickup-footer .price-val { color: #0AC4A8; }
/* Toast */
.toast {
position: fixed;
top: 56px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 10px 24px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s;
white-space: nowrap;
pointer-events: none;
}
.toast.show {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<!-- Toast 提示 -->
<div class="toast" id="toast"></div>
<!-- 主内容区:两列 -->
<div class="main-area">
<!-- ===== 中列:用户信息 + 餐品信息 + 营养摄入 ===== -->
<div class="center-column">
<!-- 用户信息 + 餐品信息 横排 -->
<div class="info-row">
<div class="info-card">
<div class="info-card__avatar">&#x1F464;</div>
<div class="info-card__body">
<div class="info-card__name">&#x5F20;&#x4E09;</div>
<div class="info-card__id">ID: 98369</div>
</div>
<div class="info-card__right">
<div class="info-card__label">&#x63A8;&#x8350;&#x70ED;&#x91CF;</div>
<div class="info-card__value">720 <span class="info-card__unit">&#x5343;&#x5361;</span></div>
</div>
</div>
<div class="info-card info-card--dish" id="dishInfoCard">
<div class="info-card__emoji" id="dishEmoji"></div>
<div class="info-card__body">
<div class="info-card__dish-name">&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;</div>
<div class="info-card__dish-detail">&#x5355;&#x4EF7; <em>5</em> &#x5143;/100&#x514B; &nbsp;|&nbsp; &#x70ED;&#x91CF; <em>158.66</em> &#x5343;&#x5361;/100&#x514B;</div>
</div>
</div>
</div>
<!-- 营养标题行 -->
<div class="nutrition-header-row">
<span class="n-title">&#x1F4CA; &#x672C;&#x9910;&#x6B21;&#x6444;&#x5165;</span>
<span>
<span class="n-meal">&#x5F53;&#x524D;&#x9910;&#x6B21;&#xFF1A;&#x65E9;&#x9910;</span>
<span class="n-time">06:00 ~ 10:19</span>
</span>
</div>
<!-- 圆柱形营养展示 -->
<div class="nutrition-core">
<div class="cyl-group">
<!-- 热量柱(红色,最高) -->
<div class="cyl-item">
<div class="cyl-value">1070<span class="cyl-unit">千卡</span></div>
<svg class="cyl-svg" width="120" height="380" viewBox="0 0 120 380">
<defs>
<linearGradient id="heatGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#cc1111"/>
<stop offset="50%" stop-color="#ff2020"/>
<stop offset="100%" stop-color="#cc1111"/>
</linearGradient>
</defs>
<ellipse cx="60" cy="22" rx="56" ry="20" fill="#cc1111"/>
<rect x="4" y="22" width="112" height="336" fill="url(#heatGrad)"/>
<rect x="14" y="22" width="12" height="336" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="60" cy="22" rx="52" ry="18" fill="rgba(255,255,255,0.15)"/>
<text x="60" y="348" text-anchor="middle" font-size="28">&#x1F525;</text>
</svg>
<div class="cyl-label">热量</div>
</div>
<!-- 主食柱(浅绿) -->
<div class="cyl-item">
<div class="cyl-value">68<span class="cyl-unit"></span></div>
<svg class="cyl-svg" width="100" height="200" viewBox="0 0 100 200">
<defs>
<linearGradient id="stapleGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#68a868"/>
<stop offset="50%" stop-color="#88cc88"/>
<stop offset="100%" stop-color="#68a868"/>
</linearGradient>
</defs>
<ellipse cx="50" cy="14" rx="46" ry="14" fill="#68a868"/>
<rect x="4" y="14" width="92" height="166" fill="url(#stapleGrad)"/>
<rect x="14" y="14" width="10" height="166" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="50" cy="14" rx="42" ry="12" fill="rgba(255,255,255,0.15)"/>
<text x="50" y="170" text-anchor="middle" font-size="22">&#x1F35A;</text>
</svg>
<div class="cyl-label">主食</div>
</div>
<!-- 果蔬柱(浅绿) -->
<div class="cyl-item">
<div class="cyl-value">68<span class="cyl-unit"></span></div>
<svg class="cyl-svg" width="100" height="200" viewBox="0 0 100 200">
<defs>
<linearGradient id="vegGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#68a868"/>
<stop offset="50%" stop-color="#88cc88"/>
<stop offset="100%" stop-color="#68a868"/>
</linearGradient>
</defs>
<ellipse cx="50" cy="14" rx="46" ry="14" fill="#68a868"/>
<rect x="4" y="14" width="92" height="166" fill="url(#vegGrad)"/>
<rect x="14" y="14" width="10" height="166" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="50" cy="14" rx="42" ry="12" fill="rgba(255,255,255,0.15)"/>
<text x="50" y="170" text-anchor="middle" font-size="22">&#x1F957;</text>
</svg>
<div class="cyl-label">果蔬</div>
</div>
<!-- 肉蛋柱(红色) -->
<div class="cyl-item">
<div class="cyl-value">68<span class="cyl-unit"></span></div>
<svg class="cyl-svg" width="100" height="170" viewBox="0 0 100 170">
<defs>
<linearGradient id="meatGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#cc1111"/>
<stop offset="50%" stop-color="#ff2020"/>
<stop offset="100%" stop-color="#cc1111"/>
</linearGradient>
</defs>
<ellipse cx="50" cy="14" rx="46" ry="14" fill="#cc1111"/>
<rect x="4" y="14" width="92" height="136" fill="url(#meatGrad)"/>
<rect x="14" y="14" width="10" height="136" fill="rgba(255,255,255,0.08)"/>
<ellipse cx="50" cy="14" rx="42" ry="12" fill="rgba(255,255,255,0.15)"/>
<text x="50" y="142" text-anchor="middle" font-size="22">&#x1F969;</text>
</svg>
<div class="cyl-label">肉蛋</div>
</div>
</div>
</div>
</div>
<!-- ===== 右列:取餐记录 ===== -->
<div class="right-column">
<div class="pickup-section">
<div class="pickup-section__header">
<span class="section-label">&#x1F9FE; &#x53D6;&#x9910;&#x8BB0;&#x5F55;</span>
<a class="settings-btn" id="settingsBtn" title="设置">&#x2699;</a>
</div>
<div class="pickup-table">
<div class="pickup-header">
<span>&#x9910;&#x54C1;&#x540D;&#x79F0;</span>
<span>&#x91CD;&#x91CF;(&#x514B;)</span>
<span>&#x5C0F;&#x8BA1;(&#x5143;)</span>
</div>
<div class="pickup-list">
<div class="pickup-row">
<span>&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;</span>
<span>186</span>
<span>9.30</span>
</div>
<div class="pickup-row">
<span>&#x5927;&#x7C73;&#x996D;</span>
<span>200</span>
<span>3.00</span>
</div>
<div class="pickup-row">
<span>&#x6E05;&#x7092;&#x65F6;&#x852C;</span>
<span>120</span>
<span>3.60</span>
</div>
<div class="pickup-row">
<span>&#x7D2B;&#x83DC;&#x86CB;&#x82B1;&#x6C64;</span>
<span>250</span>
<span>5.00</span>
</div>
</div>
<div class="pickup-footer">
<span class="lbl">&#x5408;&#x8BA1;</span>
<span class="val">756&#x514B;</span>
<span class="price-val">20.90&#x5143;</span>
</div>
</div>
</div>
</div>
</div>
<script>
/**
* 取餐界面交互逻辑
*/
/* ===== DOM 引用 ===== */
var settingsBtn = document.getElementById('settingsBtn');
var toast = document.getElementById('toast');
var isEmployee = true; // 是否已识别为员工(人脸识别+排班表判定)
var dishInfoCard = document.getElementById('dishInfoCard');
/* ===== 点击餐品信息 → 进入开餐界面 ===== */
dishInfoCard.addEventListener('click', function() {
window.location.href = '01-meal-start.html';
});
/* ===== 设置按钮点击 ===== */
settingsBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!isEmployee) {
showToast('&#x26A0;&#xFE0F; &#x975E;&#x9910;&#x5385;&#x5458;&#x5DE5;&#xFF0C;&#x65E0;&#x6743;&#x64CD;&#x4F5C;&#x7CFB;&#x7EDF;', 2000);
return;
}
window.location.href = '04-settings.html';
});
/**
* Toast 提示
*/
function showToast(msg, duration) {
toast.innerHTML = msg;
toast.classList.add('show');
clearTimeout(toast._timer);
var t = duration || 2000;
toast._timer = setTimeout(function() {
toast.classList.remove('show');
}, t);
}
// 拟真食物图片
function dishFoodSVG(w, h, colors) {
var id = 'dg';
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + colors[0] + '"/>' +
'<stop offset="50%" stop-color="' + colors[1] + '"/>' +
'<stop offset="100%" stop-color="' + colors[2] + '"/></linearGradient></defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
'<ellipse cx="' + (w*0.4) + '" cy="' + (h*0.45) + '" rx="' + (w*0.3) + '" ry="' + (h*0.25) + '" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w*0.6) + '" cy="' + (h*0.55) + '" rx="' + (w*0.25) + '" ry="' + (h*0.2) + '" fill="rgba(0,0,0,0.1)"/>' +
'<ellipse cx="' + (w*0.35) + '" cy="' + (h*0.35) + '" rx="' + (w*0.12) + '" ry="' + (h*0.1) + '" fill="rgba(255,255,255,0.15)"/>' +
'</svg>';
}
// 宫保鸡丁:暖橙金
document.getElementById('dishEmoji').innerHTML = dishFoodSVG(48, 48, ['#C87030', '#E8A040', '#D08028']);
</script>
</body>
</html>
@@ -0,0 +1,508 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>设置界面 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #12162d;
color: #fff;
width: 1280px;
height: 720px;
overflow: hidden;
position: relative;
}
/* ---- 顶部导航栏 ---- */
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
}
.top-bar__title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.back-btn {
font-size: 14px;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
/* ---- 主内容区 ---- */
.main-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 44px);
gap: 24px;
padding: 24px;
}
/* ---- 按钮卡片 ---- */
.settings-card {
display: flex;
align-items: center;
gap: 20px;
width: 560px;
padding: 32px 36px;
background: #1a2b4a;
border-radius: 16px;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.settings-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
border-color: rgba(74, 144, 217, 0.3);
}
.settings-card__icon {
font-size: 32px;
width: 48px;
text-align: center;
flex-shrink: 0;
}
.settings-card__info {
flex: 1;
}
.settings-card__title {
font-size: 20px;
font-weight: 600;
color: #fff;
margin-bottom: 8px;
}
.settings-card__desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.55);
font-weight: 400;
}
.settings-card__arrow {
font-size: 20px;
color: rgba(255, 255, 255, 0.4);
flex-shrink: 0;
}
/* ===== 弹窗遮罩层 ===== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
/* ===== 弹窗容器 ===== */
.modal-container {
background: #1a2b4a;
border-radius: 16px;
padding: 32px;
width: 480px;
max-width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform: translateY(-20px);
transition: transform 0.3s;
}
.modal-overlay.active .modal-container {
transform: translateY(0);
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #fff;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 8px;
}
/* ===== 取消按钮 ===== */
.btn-cancel {
display: block;
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* ===== 减重弹窗 — 秤体重量显示 ===== */
.tare-weight-display {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 24px;
text-align: center;
}
.tare-weight-display__label {
font-size: 13px;
color: rgba(255, 255, 255, 0.45);
margin-bottom: 8px;
}
.tare-weight-display__value {
font-size: 48px;
font-weight: 700;
color: #0AC4A8;
line-height: 1;
text-shadow: 0 0 20px rgba(10, 196, 168, 0.25);
}
.tare-weight-display__unit {
font-size: 18px;
font-weight: 400;
color: rgba(10, 196, 168, 0.6);
margin-left: 4px;
}
.tare-weight-display__status {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
margin-top: 8px;
}
.tare-weight-display__status.status-ready {
color: #4CAF50;
}
/* ===== 确认按钮 ===== */
.btn-tare-confirm {
flex: 1;
padding: 12px;
background: #0AC4A8;
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-tare-confirm:hover:not(:disabled) {
background: #09b096;
}
.btn-tare-confirm:disabled {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.3);
cursor: not-allowed;
}
/* ===== Toast 提示 ===== */
.toast {
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s;
white-space: nowrap;
}
.toast.active {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="01-meal-start.html">&#x25C0; &#x8FD4;&#x56DE;</a>
<span class="top-bar__title">设置</span>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 开始开餐按钮 -->
<a class="settings-card" href="01-meal-start.html" style="background:rgba(74,144,217,0.12);border-color:rgba(74,144,217,0.3);">
<span class="settings-card__icon">&#x1F374;</span>
<div class="settings-card__info">
<div class="settings-card__title">开始开餐</div>
<div class="settings-card__desc">进入开餐界面,开始打菜售卖</div>
</div>
<span class="settings-card__arrow">&#x203A;</span>
</a>
<!-- 餐品采样卡片 -->
<a class="settings-card" href="05-dish-sampling.html">
<span class="settings-card__icon">&#x1F4F8;</span>
<div class="settings-card__info">
<div class="settings-card__title">餐品采样</div>
<div class="settings-card__desc">菜品拍照采集、营养数据录入与 AI 识别匹配</div>
</div>
<span class="settings-card__arrow"></span>
</a>
<!-- 采样历史卡片 -->
<a class="settings-card" href="06-sampling-history.html">
<span class="settings-card__icon">&#x1F4CB;</span>
<div class="settings-card__info">
<div class="settings-card__title">采样历史</div>
<div class="settings-card__desc">查看/管理已采样餐品的图片记录</div>
</div>
<span class="settings-card__arrow">&#x203A;</span>
</a>
<!-- 容器减重卡片(一次性) -->
<div class="settings-card" id="openContainerTareModal">
<span class="settings-card__icon">&#x2696;&#xFE0F;</span>
<div class="settings-card__info">
<div class="settings-card__title">容器减重</div>
<div class="settings-card__desc" id="currentContainerTareDesc">0 克</div>
</div>
<span class="settings-card__arrow">&#x203A;</span>
</div>
<!-- 勺具减重卡片(多次) -->
<div class="settings-card" id="openSpoonTareModal">
<span class="settings-card__icon">&#x1F963;</span>
<div class="settings-card__info">
<div class="settings-card__title">勺具减重</div>
<div class="settings-card__desc" id="currentSpoonTareDesc">0 克</div>
</div>
<span class="settings-card__arrow">&#x203A;</span>
</div>
</div>
<!-- ===== 容器减重弹窗(一次性) ===== -->
<div class="modal-overlay" id="containerTareModal">
<div class="modal-container">
<div class="modal-title">&#x2696;&#xFE0F; &#x5BB9;&#x5668;&#x51CF;&#x91CD;</div>
<div style="font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.6; margin-bottom: 24px;">
&#x5C06;&#x76DB;&#x653E;&#x98DF;&#x7269;&#x7684;&#x5BB9;&#x5668;&#xFF08;&#x98DF;&#x7269;&#x76C6;&#xFF09;&#x653E;&#x5728;&#x79E4;&#x4E0A;&#xFF0C;&#x5F85;&#x91CD;&#x91CF;&#x7A33;&#x5B9A;&#x540E;&#x70B9;&#x51FB;&#x300C;&#x786E;&#x8BA4;&#x300D;&#x3002;&#x8BE5;&#x51CF;&#x91CD;&#x5C5E;&#x4E8E;&#x4E00;&#x6B21;&#x6027;&#x8BBE;&#x7F6E;&#xFF0C;&#x540E;&#x7EED;&#x5F00;&#x9910;&#x65F6;&#x7CFB;&#x7EDF;&#x4F1A;&#x81EA;&#x52A8;&#x51CF;&#x53BB;&#x5BB9;&#x5668;&#x76AE;&#x91CD;&#x3002;
</div>
<div class="tare-weight-display">
<div class="tare-weight-display__label">&#x5BB9;&#x5668;&#x5F53;&#x524D;&#x91CD;&#x91CF;</div>
<div class="tare-weight-display__value" id="containerWeightValue">0 <span class="tare-weight-display__unit"></span></div>
<div class="tare-weight-display__status" id="containerWeightStatus">&#x7B49;&#x5F85;&#x653E;&#x7F6E;&#x5BB9;&#x5668;...</div>
</div>
<div style="display: flex; gap: 12px; margin-top: 24px;">
<button class="btn-cancel" onclick="closeContainerTareModal()" style="flex: 1;">&#x53D6;&#x6D88;</button>
<button class="btn-tare-confirm" id="containerTareConfirmBtn" onclick="confirmContainerTare()" disabled>&#x786E;&#x8BA4;</button>
</div>
</div>
</div>
<!-- ===== 勺具减重弹窗(多次) ===== -->
<div class="modal-overlay" id="spoonTareModal">
<div class="modal-container">
<div class="modal-title">&#x1F963; &#x52FA;&#x5177;&#x51CF;&#x91CD;</div>
<div style="font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.6; margin-bottom: 24px;">
&#x5C06;&#x53D6;&#x9910;&#x52FA;&#x5B50;&#x653E;&#x5728;&#x79E4;&#x4E0A;&#xFF0C;&#x5F85;&#x91CD;&#x91CF;&#x7A33;&#x5B9A;&#x540E;&#x70B9;&#x51FB;&#x300C;&#x786E;&#x8BA4;&#x300D;&#x3002;&#x8BE5;&#x51CF;&#x91CD;&#x5C5E;&#x4E8E;&#x591A;&#x6B21;&#x51CF;&#x91CD;&#xFF0C;&#x6BCF;&#x6B21;&#x53D6;&#x9910;&#x65F6;&#x52FA;&#x5B50;&#x62FF;&#x8D77;&#x653E;&#x4E0B;&#x90FD;&#x4F1A;&#x81EA;&#x52A8;&#x6263;&#x9664;&#x52FA;&#x5B50;&#x91CD;&#x91CF;&#x3002;
</div>
<div class="tare-weight-display">
<div class="tare-weight-display__label">&#x52FA;&#x5B50;&#x5F53;&#x524D;&#x91CD;&#x91CF;</div>
<div class="tare-weight-display__value" id="spoonWeightValue">0 <span class="tare-weight-display__unit"></span></div>
<div class="tare-weight-display__status" id="spoonWeightStatus">&#x7B49;&#x5F85;&#x653E;&#x7F6E;&#x52FA;&#x5B50;...</div>
</div>
<div style="display: flex; gap: 12px; margin-top: 24px;">
<button class="btn-cancel" onclick="closeSpoonTareModal()" style="flex: 1;">&#x53D6;&#x6D88;</button>
<button class="btn-tare-confirm" id="spoonTareConfirmBtn" onclick="confirmSpoonTare()" disabled>&#x786E;&#x8BA4;</button>
</div>
</div>
</div>
<!-- ===== Toast 提示 ===== -->
<div class="toast" id="toast"></div>
<!-- ===== 交互逻辑 ===== -->
<script>
var toast = document.getElementById('toast');
/**
* 显示 Toast 提示
* @param {string} message - 提示内容
* @param {number} [duration] - 显示时长(毫秒),默认 2500
*/
function showToast(message, duration) {
toast.innerHTML = message;
toast.classList.add('active');
clearTimeout(toast._timer);
var t = duration || 2500;
toast._timer = setTimeout(function() {
toast.classList.remove('active');
}, t);
}
/* ===== 通用:模拟秤体重量检测 ===== */
var tareWeightTimer = null;
/**
* 启动重量检测模拟
* @param {string} valueId - 重量显示元素的 ID
* @param {string} statusId - 状态文字元素的 ID
* @param {string} confirmBtnId - 确认按钮的 ID
* @param {number} targetWeight - 模拟目标重量
* @param {string} waitingText - 等待文字
* @param {string} stableText - 稳定文字
*/
function startWeightDetection(valueId, statusId, confirmBtnId, targetWeight, waitingText, stableText) {
var valueEl = document.getElementById(valueId);
var statusEl = document.getElementById(statusId);
var confirmBtn = document.getElementById(confirmBtnId);
var simWeight = 0;
var stableCount = 0;
clearInterval(tareWeightTimer);
tareWeightTimer = setInterval(function() {
simWeight = Math.max(simWeight + Math.random() * 20, targetWeight + Math.random() * 10 - 5);
if (simWeight > targetWeight - 10) {
stableCount++;
simWeight = targetWeight + Math.random() * 4 - 2;
valueEl.innerHTML = Math.round(simWeight) + ' <span class="tare-weight-display__unit">克</span>';
if (stableCount >= 3) {
statusEl.textContent = stableText;
statusEl.className = 'tare-weight-display__status status-ready';
confirmBtn.disabled = false;
} else {
statusEl.textContent = waitingText;
}
}
}, 800);
}
/* ===== 容器减重逻辑(一次性) ===== */
var containerTareModal = document.getElementById('containerTareModal');
var containerWeightValue = document.getElementById('containerWeightValue');
var containerWeightStatus = document.getElementById('containerWeightStatus');
var containerTareConfirmBtn = document.getElementById('containerTareConfirmBtn');
var currentContainerTareDesc = document.getElementById('currentContainerTareDesc');
var containerTareWeight = parseFloat(localStorage.getItem('containerTareWeight')) || 0;
if (containerTareWeight > 0) {
currentContainerTareDesc.textContent = containerTareWeight + ' 克';
}
function openContainerTareModal() {
containerTareModal.classList.add('active');
containerTareConfirmBtn.disabled = true;
containerWeightStatus.textContent = '等待放置容器...';
containerWeightStatus.className = 'tare-weight-display__status';
containerWeightValue.innerHTML = '0 <span class="tare-weight-display__unit">克</span>';
startWeightDetection('containerWeightValue', 'containerWeightStatus', 'containerTareConfirmBtn', 350, '检测中,请等待重量稳定...', '重量已稳定,可以确认');
}
function closeContainerTareModal() {
containerTareModal.classList.remove('active');
clearInterval(tareWeightTimer);
}
function confirmContainerTare() {
var weightText = containerWeightValue.textContent.replace(/\s*克/g, '').trim();
var weight = parseFloat(weightText);
if (isNaN(weight) || weight <= 0) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x653E;&#x7F6E;&#x5BB9;&#x5668;&#x5E76;&#x7B49;&#x5F85;&#x91CD;&#x91CF;&#x7A33;&#x5B9A;', 2000);
return;
}
containerTareWeight = Math.round(weight);
localStorage.setItem('containerTareWeight', containerTareWeight);
currentContainerTareDesc.textContent = containerTareWeight + ' 克';
closeContainerTareModal();
setTimeout(function() {
showToast('&#x2705; &#x5BB9;&#x5668;&#x51CF;&#x91CD;&#x5DF2;&#x8BBE;&#x7F6E;&#x4E3A; ' + containerTareWeight + ' &#x514B;', 2500);
}, 300);
}
document.getElementById('openContainerTareModal').addEventListener('click', openContainerTareModal);
containerTareModal.addEventListener('click', function(e) {
if (e.target === containerTareModal) closeContainerTareModal();
});
/* ===== 勺具减重逻辑(多次) ===== */
var spoonTareModal = document.getElementById('spoonTareModal');
var spoonWeightValue = document.getElementById('spoonWeightValue');
var spoonWeightStatus = document.getElementById('spoonWeightStatus');
var spoonTareConfirmBtn = document.getElementById('spoonTareConfirmBtn');
var currentSpoonTareDesc = document.getElementById('currentSpoonTareDesc');
var spoonTareWeight = parseFloat(localStorage.getItem('spoonTareWeight')) || 0;
if (spoonTareWeight > 0) {
currentSpoonTareDesc.textContent = spoonTareWeight + ' 克';
}
function openSpoonTareModal() {
spoonTareModal.classList.add('active');
spoonTareConfirmBtn.disabled = true;
spoonWeightStatus.textContent = '等待放置勺子...';
spoonWeightStatus.className = 'tare-weight-display__status';
spoonWeightValue.innerHTML = '0 <span class="tare-weight-display__unit">克</span>';
startWeightDetection('spoonWeightValue', 'spoonWeightStatus', 'spoonTareConfirmBtn', 45, '检测中,请等待重量稳定...', '重量已稳定,可以确认');
}
function closeSpoonTareModal() {
spoonTareModal.classList.remove('active');
clearInterval(tareWeightTimer);
}
function confirmSpoonTare() {
var weightText = spoonWeightValue.textContent.replace(/\s*克/g, '').trim();
var weight = parseFloat(weightText);
if (isNaN(weight) || weight <= 0) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x653E;&#x7F6E;&#x52FA;&#x5B50;&#x5E76;&#x7B49;&#x5F85;&#x91CD;&#x91CF;&#x7A33;&#x5B9A;', 2000);
return;
}
spoonTareWeight = Math.round(weight);
localStorage.setItem('spoonTareWeight', spoonTareWeight);
currentSpoonTareDesc.textContent = spoonTareWeight + ' 克';
closeSpoonTareModal();
setTimeout(function() {
showToast('&#x2705; &#x52FA;&#x5177;&#x51CF;&#x91CD;&#x5DF2;&#x8BBE;&#x7F6E;&#x4E3A; ' + spoonTareWeight + ' &#x514B;', 2500);
}, 300);
}
document.getElementById('openSpoonTareModal').addEventListener('click', openSpoonTareModal);
spoonTareModal.addEventListener('click', function(e) {
if (e.target === spoonTareModal) closeSpoonTareModal();
});
</script>
</body>
</html>
@@ -0,0 +1,628 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>餐品采样界面 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif; background: #0e1326; color: #fff; width: 1280px; height: 720px; overflow: hidden; }
/* ---- 顶部导航栏 ---- */
.top-bar { display: flex; align-items: center; height: 44px; background: #1a2b4a; padding: 0 20px; }
.top-bar__title { flex: 1; text-align: center; font-size: 15px; font-weight: 600; color: rgba(255,255,255,0.85); }
.back-btn { font-size: 14px; cursor: pointer; color: rgba(255,255,255,0.7); text-decoration: none; transition: color 0.2s; }
.back-btn:hover { color: #fff; }
.main-content { display: flex; gap: 16px; padding: 16px 20px; height: calc(100% - 44px); }
/* ========== 左侧:餐品选择区 ========== */
.dish-select-area { width: 400px; flex-shrink: 0; background: #1a2b4a; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; overflow-y: auto; }
.area-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; }
/* 搜索栏 */
.search-bar { margin-bottom: 10px; }
.search-input { width: 100%; padding: 8px 12px; background: #121832; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #fff; font-size: 13px; outline: none; }
.search-input:focus { border-color: #4a90d9; }
.search-input::placeholder { color: rgba(255,255,255,0.3); }
/* 分类Tab */
.filter-tabs { display: flex; gap: 6px; margin-bottom: 12px; }
.filter-tab { padding: 5px 12px; font-size: 12px; border-radius: 4px; background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.55); cursor: pointer; transition: all 0.2s; border: 1px solid transparent; }
.filter-tab.active { background: #4a90d9; color: #fff; }
.filter-tab:hover:not(.active) { border-color: rgba(255,255,255,0.15); color: rgba(255,255,255,0.75); }
/* 餐品列表 */
.dish-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.dish-item { display: flex; align-items: center; padding: 12px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; gap: 12px; cursor: pointer; transition: all 0.2s; position: relative; }
.dish-item:hover { background: rgba(74,144,217,0.08); border-color: rgba(74,144,217,0.25); }
.dish-item.selected { background: rgba(74,144,217,0.12); border-color: #4a90d9; }
.dish-item__thumb { width: 52px; height: 52px; border-radius: 8px; overflow: hidden; flex-shrink: 0; }
.dish-item__thumb svg { width: 100%; height: 100%; }
.dish-item__info { flex: 1; min-width: 0; }
.dish-item__name { font-size: 14px; font-weight: 600; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dish-item__detail { font-size: 12px; color: rgba(255,255,255,0.45); line-height: 1.4; }
/* 空状态 */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 0; color: rgba(255,255,255,0.3); }
.empty-state__icon { font-size: 40px; margin-bottom: 12px; }
.empty-state__text { font-size: 13px; }
/* ========== 右侧:拍照采集区 ========== */
.capture-area { flex: 1; background: #1a2b4a; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; gap: 16px; overflow-y: auto; }
/* 选中餐品信息 */
.selected-dish-info { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(74,144,217,0.08); border: 1px solid rgba(74,144,217,0.2); border-radius: 10px; }
.selected-dish-info__thumb { width: 44px; height: 44px; border-radius: 8px; overflow: hidden; flex-shrink: 0; }
.selected-dish-info__thumb svg { width: 100%; height: 100%; }
.selected-dish-info__name { font-size: 15px; font-weight: 600; }
.selected-dish-info__desc { font-size: 12px; color: rgba(255,255,255,0.5); margin-top: 2px; }
/* 拍照区域 */
.camera-frame-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.camera-frame { width: 100%; height: 320px; background: #0D1117; border-radius: 12px; position: relative; border: 1px solid rgba(255,255,255,0.08); display: flex; align-items: center; justify-content: center; overflow: hidden; transition: border-color 0.3s; }
.camera-frame.active { border-color: #4a90d9; }
.camera-frame__text { color: rgba(255,255,255,0.25); font-size: 14px; z-index: 1; }
.camera-frame__preview { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; display: none; z-index: 0; }
.camera-frame::before, .camera-frame::after { content: ""; position: absolute; pointer-events: none; z-index: 2; }
.camera-frame::before { top: 0; bottom: 0; left: 33.33%; width: 1px; border-left: 1px dashed rgba(74,144,217,0.3); }
.camera-frame::after { left: 0; right: 0; top: 33.33%; height: 1px; border-top: 1px dashed rgba(74,144,217,0.3); }
.camera-frame__corner { position: absolute; width: 24px; height: 24px; z-index: 3; }
.camera-frame__corner--tl { top: 12px; left: 12px; border-top: 2px solid #4a90d9; border-left: 2px solid #4a90d9; }
.camera-frame__corner--tr { top: 12px; right: 12px; border-top: 2px solid #4a90d9; border-right: 2px solid #4a90d9; }
.camera-frame__corner--bl { bottom: 12px; left: 12px; border-bottom: 2px solid #4a90d9; border-left: 2px solid #4a90d9; }
.camera-frame__corner--br { bottom: 12px; right: 12px; border-bottom: 2px solid #4a90d9; border-right: 2px solid #4a90d9; }
.btn-photo { width: 200px; padding: 14px 8px; border: none; border-radius: 10px; background: #4a90d9; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; transition: all 0.2s; }
.btn-photo:hover { background: #3d7fc0; transform: scale(1.03); }
.btn-photo:active { transform: scale(0.97); background: #3570ad; }
.btn-photo.shooting { background: #ff4d4f; pointer-events: none; }
.btn-photo.shooting:hover { background: #ff4d4f; }
/* 采集结果 */
.result-section { margin-top: auto; }
.result-section__title { font-size: 14px; font-weight: 600; color: rgba(255,255,255,0.7); margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
.result-section__count { font-weight: 400; color: rgba(255,255,255,0.45); font-size: 12px; }
.result-grid { display: grid; grid-template-columns: repeat(9, 1fr); gap: 8px; min-height: 120px; }
.result-cell { aspect-ratio: 1; background: #121832; border: 1px dashed rgba(255,255,255,0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 11px; color: rgba(255,255,255,0.2); transition: all 0.2s; overflow: hidden; cursor: pointer; position: relative; }
.result-cell:hover:not(.disabled) { border-color: rgba(74,144,217,0.3); background: rgba(74,144,217,0.06); }
.result-cell.disabled { opacity: 0.3; cursor: not-allowed; }
.result-cell.has-img { border-style: solid; border-color: #52c41a; }
.result-cell .result-thumb { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 28px; background: linear-gradient(135deg, #1a2035, #253050); border-radius: 7px; position: relative; }
/* 采集图片缩略图样式 */
.result-cell .thumb-img-wrap {
width: 100%; height: 100%;
border-radius: 7px;
position: relative;
overflow: hidden;
}
.result-cell .thumb-img-wrap svg {
width: 100%; height: 100%;
}
/* 图片编号角标 */
.result-cell .thumb-index {
position: absolute;
top: 2px; left: 2px;
width: 16px; height: 16px;
border-radius: 50%;
background: rgba(74,144,217,0.9);
color: #fff;
font-size: 9px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
}
/* 采集成功勾选标记 */
.result-cell .check-mark {
position: absolute;
bottom: 2px; right: 2px;
width: 16px; height: 16px;
border-radius: 50%;
background: rgba(82,196,26,0.9);
color: #fff;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
}
/* 图片入场动画 */
@keyframes imgAppear {
0% { transform: scale(0.5); opacity: 0; }
60% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.result-cell.just-added .thumb-img-wrap {
animation: imgAppear 0.4s ease-out;
}
.result-cell .remove-btn { position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; border-radius: 50%; background: rgba(255,77,79,0.85); color: #fff; font-size: 10px; display: none; align-items: center; justify-content: center; cursor: pointer; border: none; line-height: 1; z-index: 2; }
.result-cell.has-img:hover .remove-btn { display: flex; }
/* 底部操作按钮 */
.action-bar { display: flex; gap: 12px; margin-top: 12px; }
.action-bar .btn-save { flex: 1.5; padding: 10px; border: none; border-radius: 8px; background: #4a90d9; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.action-bar .btn-save:hover { background: #3d7fc0; }
.action-bar .btn-resample { flex: 1; padding: 10px; border: 1px solid rgba(255,193,7,0.3); border-radius: 8px; background: rgba(255,193,7,0.08); color: #ffc107; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.action-bar .btn-resample:hover { background: rgba(255,193,7,0.15); border-color: rgba(255,193,7,0.5); }
.action-bar .btn-cancel { flex: 1; padding: 10px; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.7); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.action-bar .btn-cancel:hover { background: rgba(255,255,255,0.1); color: #fff; }
.action-bar .btn-save-continue { flex: 1; padding: 10px; border: none; border-radius: 8px; background: #52c41a; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.action-bar .btn-save-continue:hover { background: #45a818; }
/* Toast */
.toast { position: fixed; top: 60px; left: 50%; transform: translateX(-50%) translateY(-20px); background: rgba(0,0,0,0.8); color: #fff; padding: 10px 24px; border-radius: 8px; font-size: 13px; z-index: 1000; opacity: 0; visibility: hidden; transition: opacity 0.3s, transform 0.3s, visibility 0.3s; white-space: nowrap; pointer-events: none; }
.toast.show { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
/* 拍照闪烁效果 */
@keyframes flash { 0% { opacity: 0; } 30% { opacity: 0.9; } 100% { opacity: 0; } }
.flash-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #fff; opacity: 0; pointer-events: none; border-radius: 12px; z-index: 10; }
.flash-overlay.active { animation: flash 0.5s ease-out; }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="javascript:history.back()">&#x25C0; &#x8FD4;&#x56DE;</a>
<span class="top-bar__title">&#x9910;&#x54C1;&#x91C7;&#x6837;</span>
<a class="back-btn" href="06-sampling-history.html" style="margin-left:auto;">&#x1F4DD; &#x67E5;&#x770B;&#x91C7;&#x6837;&#x5386;&#x53F2;</a>
</div>
<div class="main-content">
<!-- ========== 左侧:餐品选择区 ========== -->
<div class="dish-select-area">
<div class="area-title">
<span>&#x1F355; &#x9910;&#x54C1;&#x9009;&#x62E9;</span>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<input type="text" class="search-input" id="searchInput" placeholder="&#x641C;&#x7D22;&#x9910;&#x54C1;&#x540D;&#x79F0;...">
</div>
<!-- 类目Tab -->
<div class="filter-tabs" id="catTabs">
<span class="filter-tab active" data-cat="all">&#x5168;&#x90E8;</span>
<span class="filter-tab" data-cat="&#x8364;&#x83DC;">&#x8364;&#x83DC;</span>
<span class="filter-tab" data-cat="&#x7D20;&#x83DC;">&#x7D20;&#x83DC;</span>
<span class="filter-tab" data-cat="&#x4E3B;&#x98DF;">&#x4E3B;&#x98DF;</span>
<span class="filter-tab" data-cat="&#x6C64;&#x54C1;">&#x6C64;&#x54C1;</span>
<span class="filter-tab" data-cat="&#x996E;&#x54C1;">&#x996E;&#x54C1;</span>
</div>
<!-- 餐品列表 -->
<div class="dish-list" id="dishList"></div>
</div>
<!-- ========== 右侧:拍照采集区 ========== -->
<div class="capture-area">
<!-- 选中餐品信息 -->
<div class="selected-dish-info" id="selectedDishInfo">
<div class="selected-dish-info__thumb" id="selectedThumb">&#x1F373;</div>
<div>
<div class="selected-dish-info__name" id="selectedName">&#x8BF7;&#x9009;&#x62E9;&#x4E00;&#x4E2A;&#x9910;&#x54C1;</div>
<div class="selected-dish-info__desc" id="selectedDesc">&#x70B9;&#x51FB;&#x5DE6;&#x4FA7;&#x5217;&#x8868;&#x4E2D;&#x7684;&#x9910;&#x54C1;&#x8FDB;&#x884C;&#x91C7;&#x6837;</div>
</div>
</div>
<!-- 摄像头区域 -->
<div class="camera-frame-wrap">
<div class="camera-frame" id="cameraFrame">
<span class="camera-frame__text" id="cameraText">&#x6444;&#x50CF;&#x5934;&#x9884;&#x89C8;&#x533A;&#x57DF;</span>
<img class="camera-frame__preview" id="cameraPreview" alt="&#x9884;&#x89C8;">
<div class="flash-overlay" id="flashOverlay"></div>
<div class="camera-frame__corner camera-frame__corner--tl"></div>
<div class="camera-frame__corner camera-frame__corner--tr"></div>
<div class="camera-frame__corner camera-frame__corner--bl"></div>
<div class="camera-frame__corner camera-frame__corner--br"></div>
</div>
<button class="btn-photo" id="btnPhoto">&#x1F4F8;<br>&#x62CD;&#x7167;&#x91C7;&#x96C6;</button>
</div>
<!-- 采集结果 -->
<div class="result-section">
<div class="result-section__title">
<span>&#x1F4CB; &#x91C7;&#x96C6;&#x7ED3;&#x679C;</span>
<span class="result-section__count" id="resultCount">0/9</span>
</div>
<div class="result-grid" id="resultGrid"></div>
</div>
<!-- 底部操作按钮 -->
<div class="action-bar">
<button class="btn-cancel" id="btnCancel">&#x53D6;&#x6D88;</button>
<button class="btn-resample" id="btnResample">&#x21BB; &#x91CD;&#x65B0;&#x91C7;&#x6837;</button>
<button class="btn-save" id="btnSave">&#x4FDD;&#x5B58;&#x91C7;&#x96C6;</button>
<button class="btn-save-continue" id="btnSaveContinue">&#x4FDD;&#x5B58;&#x5E76;&#x7EE7;&#x7EED;</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
/**
* 餐品采样界面交互逻辑
*/
/* ===== 餐品数据 ===== */
var DISH_DATA = [
{ id: 1, name: '&#x7EA2;&#x70E7;&#x8089;', emoji: '&#x1F373;', meal: 'lunch', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x732A;&#x8089;', kcal: 480 },
{ id: 2, name: '&#x6E05;&#x7092;&#x65F6;&#x852C;', emoji: '&#x1F96C;', meal: 'lunch', catLabel: '&#x7D20;&#x83DC;', mainIngredient: '&#x83DC;&#x82B1;', kcal: 120 },
{ id: 3, name: '&#x9EBB;&#x5A46;&#x8C46;&#x8150;', emoji: '&#x1F9C6;', meal: 'lunch', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x8C46;&#x8150;', kcal: 280 },
{ id: 4, name: '&#x7CD6;&#x918B;&#x6392;&#x9AA8;', emoji: '&#x1F356;', meal: 'lunch', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x732A;&#x8089;', kcal: 520 },
{ id: 5, name: '&#x9999;&#x714E;&#x5E26;&#x9C7C;', emoji: '&#x1F41F;', meal: 'dinner', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x5E26;&#x9C7C;', kcal: 320 },
{ id: 6, name: '&#x849C;&#x8338;&#x897F;&#x5170;&#x82B1;', emoji: '&#x1F966;', meal: 'dinner', catLabel: '&#x7D20;&#x83DC;', mainIngredient: '&#x897F;&#x5170;&#x82B1;', kcal: 110 },
{ id: 7, name: '&#x897F;&#x7EA2;&#x67FF;&#x7092;&#x86CB;', emoji: '&#x1FAD4;', meal: 'breakfast', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x9E21;&#x86CB;', kcal: 180 },
{ id: 8, name: '&#x7D2B;&#x83DC;&#x86CB;&#x82B1;&#x6C64;', emoji: '&#x1FAD5;', meal: 'breakfast', catLabel: '&#x6C64;&#x54C1;', mainIngredient: '&#x7D2B;&#x83DC;', kcal: 80 },
{ id: 9, name: '&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;', emoji: '&#x1F357;', meal: 'lunch', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x9E21;&#x8089;', kcal: 420 },
{ id: 10, name: '&#x7C73;&#x996D;', emoji: '&#x1F35A;', meal: 'lunch', catLabel: '&#x4E3B;&#x98DF;', mainIngredient: '&#x5927;&#x7C73;', kcal: 260 },
{ id: 11, name: '&#x6162;&#x7096;&#x9E21;&#x6C64;', emoji: '&#x1F958;', meal: 'dinner', catLabel: '&#x6C64;&#x54C1;', mainIngredient: '&#x571F;&#x9E21;', kcal: 150 },
{ id: 12, name: '&#x624B;&#x6495;&#x5305;&#x83DC;', emoji: '&#x1F96C;', meal: 'dinner', catLabel: '&#x7D20;&#x83DC;', mainIngredient: '&#x5305;&#x83DC;', kcal: 95 },
{ id: 13, name: '&#x8C46;&#x6D46;', emoji: '&#x1F95B;', meal: 'breakfast', catLabel: '&#x996E;&#x54C1;', mainIngredient: '&#x9EC4;&#x8C46;', kcal: 60 },
{ id: 14, name: '&#x9992;&#x5934;', emoji: '&#x1F96E;', meal: 'breakfast', catLabel: '&#x4E3B;&#x98DF;', mainIngredient: '&#x5C0F;&#x9EA6;', kcal: 220 }
];
/* ===== 状态变量 ===== */
var selectedDish = null; // 当前选中餐品
var capturedImages = []; // 已采集图片(最多9张)
var lastAddedIndex = -1; // 最近一次添加的图片索引(用于入场动画)
var currentCat = 'all'; // 当前类目筛选
var searchKeyword = ''; // 搜索关键词
var MAX_IMAGES = 9; // 最多采集9张
/* ===== 拟真食物图片配置 ===== */
var DISH_IMG_STYLES = {
1: { bg: ['#8B3A2A', '#D4764E', '#C05030'] },
2: { bg: ['#306028', '#50A040', '#408030'] },
3: { bg: ['#706040', '#B89060', '#807050'] },
4: { bg: ['#802018', '#C05030', '#A03820'] },
5: { bg: ['#805020', '#B88040', '#A06830'] },
6: { bg: ['#207040', '#40A868', '#308848'] },
7: { bg: ['#E8A030', '#F0C040', '#D89020'] },
8: { bg: ['#503028', '#805040', '#603830'] },
9: { bg: ['#C87030', '#E8A040', '#D08028'] },
10: { bg: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] },
11: { bg: ['#503028', '#805040', '#603830'] },
12: { bg: ['#306028', '#50A040', '#408030'] },
13: { bg: ['#D0C060', '#E8D880', '#D8C860'] },
14: { bg: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] }
};
function dishFoodSVG(dishId, w, h) {
var style = DISH_IMG_STYLES[dishId] || { bg: ['#304050', '#506870', '#405060'] };
var id = 'dg' + dishId;
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + style.bg[0] + '"/>' +
'<stop offset="50%" stop-color="' + style.bg[1] + '"/>' +
'<stop offset="100%" stop-color="' + style.bg[2] + '"/></linearGradient></defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
'<ellipse cx="' + (w*0.4) + '" cy="' + (h*0.45) + '" rx="' + (w*0.3) + '" ry="' + (h*0.25) + '" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w*0.6) + '" cy="' + (h*0.55) + '" rx="' + (w*0.25) + '" ry="' + (h*0.2) + '" fill="rgba(0,0,0,0.1)"/>' +
'<ellipse cx="' + (w*0.35) + '" cy="' + (h*0.35) + '" rx="' + (w*0.12) + '" ry="' + (h*0.1) + '" fill="rgba(255,255,255,0.15)"/>' +
'</svg>';
}
/* ===== DOM 引用 ===== */
var dishListEl = document.getElementById('dishList');
var searchInputEl = document.getElementById('searchInput');
var catTabsEl = document.getElementById('catTabs');
var selectedThumbEl = document.getElementById('selectedThumb');
var selectedNameEl = document.getElementById('selectedName');
var selectedDescEl = document.getElementById('selectedDesc');
var cameraFrameEl = document.getElementById('cameraFrame');
var cameraTextEl = document.getElementById('cameraText');
var cameraPreviewEl = document.getElementById('cameraPreview');
var flashOverlayEl = document.getElementById('flashOverlay');
var btnPhotoEl = document.getElementById('btnPhoto');
var resultGridEl = document.getElementById('resultGrid');
var resultCountEl = document.getElementById('resultCount');
var toastEl = document.getElementById('toast');
/* ===== 解析 URL 参数 ===== */
function getQueryParam(key) {
var query = window.location.search.substring(1);
var pairs = query.split('&');
for (var i = 0; i < pairs.length; i++) {
var parts = pairs[i].split('=');
if (parts[0] === key) return parts[1];
}
return null;
}
/* ===== 初始化 ===== */
function init() {
renderDishList();
renderResultGrid();
bindEvents();
// 处理从采样历史进入的补充采集
var resampleDishId = getQueryParam('resampleDishId');
if (resampleDishId) {
var dishId = parseInt(resampleDishId);
for (var i = 0; i < DISH_DATA.length; i++) {
if (DISH_DATA[i].id === dishId) {
selectedDish = DISH_DATA[i];
renderDishList();
updateSelectedDishInfo();
showToast('&#x2705; &#x5DF2;&#x81EA;&#x52A8;&#x9009;&#x4E2D;&#xFF1A;' + selectedDish.name + '&#xFF0C;&#x53EF;&#x76F4;&#x63A5;&#x8FFD;&#x52A0;&#x91C7;&#x96C6;', 2000);
// 补充采集模式隐藏「保存并继续」按钮
document.getElementById('btnSaveContinue').style.display = 'none';
break;
}
}
}
}
/**
* 获取筛选后的餐品列表
*/
function getFilteredDishes() {
return DISH_DATA.filter(function(dish) {
// 类目筛选
if (currentCat !== 'all' && dish.catLabel !== currentCat) return false;
// 搜索关键词
if (searchKeyword && dish.name.indexOf(searchKeyword) === -1) return false;
return true;
});
}
/* ===== 渲染函数 ===== */
/**
* 渲染餐品列表
*/
function renderDishList() {
var filtered = getFilteredDishes();
if (filtered.length === 0) {
dishListEl.innerHTML = '<div class="empty-state"><div class="empty-state__icon">&#x1F50D;</div><div class="empty-state__text">&#x672A;&#x627E;&#x5230;&#x5339;&#x914D;&#x7684;&#x9910;&#x54C1;</div></div>';
return;
}
var html = '';
for (var i = 0; i < filtered.length; i++) {
var dish = filtered[i];
var isSelected = selectedDish && selectedDish.id === dish.id;
html += '<div class="dish-item' + (isSelected ? ' selected' : '') + '" data-id="' + dish.id + '">' +
'<div class="dish-item__thumb">' + dishFoodSVG(dish.id, 52, 52) + '</div>' +
'<div class="dish-item__info">' +
'<div class="dish-item__name">' + dish.name + '</div>' +
'<div class="dish-item__detail">' + dish.catLabel + ' · ' + dish.mainIngredient + ' · ' + dish.kcal + 'kcal</div>' +
'</div>' +
'</div>';
}
dishListEl.innerHTML = html;
}
/**
* 渲染采集结果网格
*/
function renderResultGrid() {
var html = '';
for (var i = 0; i < MAX_IMAGES; i++) {
if (i < capturedImages.length) {
// 已有图片 — 带缩略图效果
var isJustAdded = (i === lastAddedIndex);
html += '<div class="result-cell has-img' + (isJustAdded ? ' just-added' : '') + '" data-index="' + i + '">' +
'<div class="thumb-img-wrap">' +
'<span class="thumb-index">' + (i + 1) + '</span>' +
dishFoodSVG(capturedImages[i].id, 120, 120) +
'<span class="check-mark">&#x2714;</span>' +
'</div>' +
'<button class="remove-btn" data-index="' + i + '">&#x2716;</button>' +
'</div>';
} else if (i === capturedImages.length) {
// 下一个可添加的位置
html += '<div class="result-cell" data-index="' + i + '">+&#x6DFB;&#x52A0;</div>';
} else {
// 不可用
html += '<div class="result-cell disabled" data-index="' + i + '"></div>';
}
}
resultGridEl.innerHTML = html;
resultCountEl.textContent = capturedImages.length + '/' + MAX_IMAGES;
lastAddedIndex = -1; // 动画只播放一次
}
/**
* 更新选中餐品信息
*/
function updateSelectedDishInfo() {
if (!selectedDish) {
selectedThumbEl.innerHTML = dishFoodSVG(1, 44, 44);
selectedNameEl.textContent = '请选择一个餐品';
selectedDescEl.textContent = '点击左侧列表中的餐品进行采样';
return;
}
selectedThumbEl.innerHTML = dishFoodSVG(selectedDish.id, 44, 44);
selectedNameEl.innerHTML = selectedDish.name;
selectedDescEl.innerHTML = selectedDish.catLabel + ' · ' + selectedDish.mainIngredient + ' · ' + selectedDish.kcal + 'kcal/100g';
}
/* ===== Toast ===== */
function showToast(msg, duration) {
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastEl._timer);
var t = duration || 2000;
toastEl._timer = setTimeout(function() {
toastEl.classList.remove('show');
}, t);
}
/* ===== 事件绑定 ===== */
function bindEvents() {
/* --- 餐品列表点击 --- */
dishListEl.addEventListener('click', function(e) {
var item = e.target.closest('.dish-item');
if (!item) return;
var id = parseInt(item.getAttribute('data-id'));
var dish = null;
for (var i = 0; i < DISH_DATA.length; i++) {
if (DISH_DATA[i].id === id) { dish = DISH_DATA[i]; break; }
}
if (!dish) return;
selectedDish = dish;
renderDishList();
updateSelectedDishInfo();
showToast('&#x2705; &#x5DF2;&#x9009;&#x4E2D;&#xFF1A;' + dish.name, 1200);
});
/* --- 类目Tab切换 --- */
catTabsEl.addEventListener('click', function(e) {
var tab = e.target.closest('.filter-tab');
if (!tab) return;
currentCat = tab.getAttribute('data-cat');
var tabs = catTabsEl.querySelectorAll('.filter-tab');
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
tab.classList.add('active');
renderDishList();
});
/* --- 搜索 --- */
var searchTimer = null;
searchInputEl.addEventListener('input', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
searchKeyword = searchInputEl.value.replace(/^\s+|\s+$/g, '');
renderDishList();
}, 300);
});
/* --- 拍照按钮 --- */
btnPhotoEl.addEventListener('click', function() {
if (!selectedDish) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x9009;&#x62E9;&#x4E00;&#x4E2A;&#x9910;&#x54C1;', 1500);
return;
}
if (capturedImages.length >= MAX_IMAGES) {
showToast('&#x26A0;&#xFE0F; &#x5DF2;&#x8FBE;&#x91C7;&#x96C6;&#x4E0A;&#x9650;&#xFF08;9&#x5F20;&#xFF09;', 1500);
return;
}
// 模拟拍照
btnPhotoEl.classList.add('shooting');
cameraFrameEl.classList.add('active');
// 闪烁效果
flashOverlayEl.classList.remove('active');
void flashOverlayEl.offsetWidth; // 强制回流重启动画
flashOverlayEl.classList.add('active');
// 模拟2秒后完成拍照
setTimeout(function() {
btnPhotoEl.classList.remove('shooting');
cameraFrameEl.classList.remove('active');
// 添加模拟图片(存储菜品对象以便渲染缩略图)
capturedImages.push(selectedDish);
lastAddedIndex = capturedImages.length - 1;
renderResultGrid();
showToast('&#x1F4F8; &#x91C7;&#x96C6;&#x6210;&#x529F;&#xFF01;&#x5DF2;&#x91C7;&#x96C6; ' + capturedImages.length + '/' + MAX_IMAGES + ' &#x5F20;', 1500);
}, 2000);
});
/* --- 结果网格交互 --- */
resultGridEl.addEventListener('click', function(e) {
// 删除按钮
var removeBtn = e.target.closest('.remove-btn');
if (removeBtn) {
e.stopPropagation();
var idx = parseInt(removeBtn.getAttribute('data-index'));
capturedImages.splice(idx, 1);
renderResultGrid();
showToast('&#x1F5D1;&#xFE0F; &#x5DF2;&#x5220;&#x9664;&#x91C7;&#x96C6;&#x56FE;&#x7247;', 1200);
return;
}
// 添加按钮
var cell = e.target.closest('.result-cell');
if (!cell || cell.classList.contains('disabled')) return;
if (!selectedDish) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x9009;&#x62E9;&#x4E00;&#x4E2A;&#x9910;&#x54C1;', 1500);
return;
}
if (capturedImages.length >= MAX_IMAGES) {
showToast('&#x26A0;&#xFE0F; &#x5DF2;&#x8FBE;&#x91C7;&#x96C6;&#x4E0A;&#x9650;', 1500);
return;
}
// 触发拍照
btnPhotoEl.click();
});
/* --- 保存按钮(提交后跳转至待取餐界面) --- */
document.getElementById('btnSave').addEventListener('click', function() {
if (!selectedDish) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x9009;&#x62E9;&#x4E00;&#x4E2A;&#x9910;&#x54C1;', 1500);
return;
}
if (capturedImages.length === 0) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x81F3;&#x5C11;&#x91C7;&#x96C6;1&#x5F20;&#x56FE;&#x7247;', 1500);
return;
}
showToast('&#x2705; &#x91C7;&#x96C6;&#x6570;&#x636E;&#x5DF2;&#x4FDD;&#x5B58;&#xFF0C;&#x8DF3;&#x8F6C;&#x81F3;&#x5F85;&#x53D6;&#x9910;&#x9875;&#x9762;...', 1500);
// 1.5秒后携带餐品ID跳转至待取餐界面
setTimeout(function() {
window.location.href = '02-dish-display.html?dishId=' + selectedDish.id;
}, 1500);
});
/* --- 保存并继续按钮(提交后留在当前页,重置表单继续采集下一个餐品) --- */
document.getElementById('btnSaveContinue').addEventListener('click', function() {
if (!selectedDish) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x5148;&#x9009;&#x62E9;&#x4E00;&#x4E2A;&#x9910;&#x54C1;', 1500);
return;
}
if (capturedImages.length === 0) {
showToast('&#x26A0;&#xFE0F; &#x8BF7;&#x81F3;&#x5C11;&#x91C7;&#x96C6;1&#x5F20;&#x56FE;&#x7247;', 1500);
return;
}
showToast('&#x2705; &#x91C7;&#x96C6;&#x6570;&#x636E;&#x5DF2;&#x4FDD;&#x5B58;&#xFF0C;&#x8BF7;&#x7EE7;&#x7EED;&#x91C7;&#x96C6;&#x4E0B;&#x4E00;&#x4E2A;&#x9910;&#x54C1;', 1500);
// 重置表单,保留页面
selectedDish = null;
capturedImages = [];
cameraPreviewEl.style.display = 'none';
cameraTextEl.style.display = '';
renderDishList();
renderResultGrid();
updateSelectedDishInfo();
});
/* --- 取消按钮 → 回到待取餐界面(02) --- */
document.getElementById('btnCancel').addEventListener('click', function() {
if (capturedImages.length === 0 && !selectedDish) {
showToast('&#x65E0;&#x9700;&#x53D6;&#x6D88;&#xFF0C;&#x672A;&#x8FDB;&#x884C;&#x4EFB;&#x4F55;&#x64CD;&#x4F5C;', 1500);
return;
}
selectedDish = null;
capturedImages = [];
showToast('&#x21BB; &#x5DF2;&#x53D6;&#x6D88;&#x5F53;&#x524D;&#x91C7;&#x96C6;', 1000);
// 取消后回到待取餐界面
setTimeout(function() {
window.location.href = '02-dish-display.html';
}, 1000);
});
/* --- 重新采样按钮 --- */
document.getElementById('btnResample').addEventListener('click', function() {
if (capturedImages.length === 0 && !selectedDish) {
showToast('&#x26A0;&#xFE0F; &#x5F53;&#x524D;&#x65E0;&#x91C7;&#x6837;&#x6570;&#x636E;', 1500);
return;
}
selectedDish = null;
capturedImages = [];
cameraPreviewEl.style.display = 'none';
cameraTextEl.style.display = '';
renderDishList();
renderResultGrid();
updateSelectedDishInfo();
showToast('&#x21BB; &#x5DF2;&#x6E05;&#x7A7A;&#xFF0C;&#x8BF7;&#x91CD;&#x65B0;&#x9009;&#x62E9;&#x9910;&#x54C1;&#x5F00;&#x59CB;&#x91C7;&#x6837;', 1500);
});
}
/* 启动 */
init();
</script>
</body>
</html>
@@ -0,0 +1,537 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>采样历史 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif; background: #0e1326; color: #fff; width: 1280px; height: 720px; overflow: hidden; }
/* ---- 顶部导航栏 ---- */
.top-bar { display: flex; align-items: center; height: 44px; background: #1a2b4a; padding: 0 20px; }
.top-bar__title { flex: 1; text-align: center; font-size: 15px; font-weight: 600; color: rgba(255,255,255,0.85); }
.back-btn { font-size: 14px; cursor: pointer; color: rgba(255,255,255,0.7); text-decoration: none; transition: color 0.2s; }
.back-btn:hover { color: #fff; }
/* ---- 主内容区:左右两列 ---- */
.main-content { display: flex; gap: 16px; padding: 16px 20px; height: calc(100% - 44px); }
/* ===== 左侧:快捷查找面板 ===== */
.search-panel {
width: 260px;
flex-shrink: 0;
background: #1a2b4a;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.search-panel__title {
font-size: 14px;
font-weight: 600;
color: rgba(255,255,255,0.7);
}
.search-panel__input {
width: 100%;
padding: 10px 14px;
background: #121832;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
color: #fff;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.search-panel__input:focus { border-color: #4a90d9; }
.search-panel__input::placeholder { color: rgba(255,255,255,0.3); }
.cat-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.cat-tab {
padding: 5px 10px;
font-size: 12px;
border-radius: 4px;
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.55);
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.cat-tab.active { background: #4a90d9; color: #fff; }
.cat-tab:hover:not(.active) { border-color: rgba(255,255,255,0.15); color: rgba(255,255,255,0.75); }
/* 统计信息 */
.search-panel__stats {
margin-top: auto;
padding-top: 12px;
border-top: 1px solid rgba(255,255,255,0.06);
font-size: 12px;
color: rgba(255,255,255,0.4);
}
.search-panel__stats em { font-style: normal; color: #4a90d9; font-weight: 600; }
/* ===== 右侧:瀑布流 ===== */
.waterfall-area { flex: 1; overflow-y: auto; }
.waterfall {
column-count: 3;
column-gap: 16px;
}
.waterfall-card {
break-inside: avoid;
background: #1a2b4a;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid rgba(255,255,255,0.06);
cursor: pointer;
transition: all 0.2s;
}
.waterfall-card:hover {
border-color: rgba(74,144,217,0.3);
background: rgba(74,144,217,0.06);
transform: translateY(-2px);
}
/* 卡片头部 */
.card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.card-emoji { width: 56px; height: 56px; border-radius: 10px; overflow: hidden; flex-shrink: 0; }
.card-emoji svg { width: 100%; height: 100%; }
.card-name { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
.card-category { font-size: 12px; color: rgba(255,255,255,0.45); }
/* 卡片缩略图预览 */
.card-previews { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 12px; }
.card-preview {
aspect-ratio: 1;
border-radius: 6px;
overflow: hidden;
}
.card-preview svg { width: 100%; height: 100%; }
.card-preview.hidden { visibility: hidden; }
/* 卡片底部信息 */
.card-footer { display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: rgba(255,255,255,0.4); }
.card-count { color: #4a90d9; font-weight: 500; }
/* ---- 详情弹窗 ---- */
.modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
z-index: 999;
opacity: 0; visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal-container {
background: #1a2b4a;
border-radius: 16px;
padding: 28px;
width: 700px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
transform: translateY(-20px);
transition: transform 0.3s;
}
.modal-overlay.active .modal-container { transform: translateY(0); }
.modal-header { display: flex; align-items: center; gap: 14px; margin-bottom: 24px; }
.modal-header__left { display: flex; align-items: center; gap: 14px; flex: 1; }
.modal-header__right { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.modal-emoji { width: 52px; height: 52px; border-radius: 10px; overflow: hidden; flex-shrink: 0; }
.modal-emoji svg { width: 100%; height: 100%; }
.modal-title { font-size: 20px; font-weight: 600; }
.modal-subtitle { font-size: 13px; color: rgba(255,255,255,0.45); margin-top: 4px; }
.modal-time { font-size: 12px; color: rgba(255,255,255,0.35); text-align: right; line-height: 1.5; }
/* 采样数量徽章(仿 01 页面识别率样式) */
.modal-count-wrap { display: flex; align-items: stretch; justify-content: center; }
.modal-count { display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 3px; font-size: 14px; color: #52c41a; font-weight: 700; background: rgba(82,196,26,0.1); border-radius: 6px; text-align: center; line-height: 1; min-width: 44px; padding: 4px 8px; }
.modal-count .count-label { font-size: 10px; font-weight: 400; color: rgba(82,196,26,0.6); }
/* 图片网格 */
.image-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.image-cell {
aspect-ratio: 1;
border-radius: 10px;
overflow: hidden;
position: relative;
border: 2px solid rgba(255,255,255,0.06);
}
.image-cell svg { width: 100%; height: 100%; }
.image-cell__time {
position: absolute; bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,0.6);
color: rgba(255,255,255,0.75);
font-size: 11px; text-align: center; padding: 3px 6px;
pointer-events: none;
}
.image-cell .remove-btn {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px; border-radius: 50%;
background: rgba(255,77,79,0.9);
color: #fff; font-size: 12px;
display: none; align-items: center; justify-content: center;
cursor: pointer; border: none; line-height: 1;
}
.image-cell:hover .remove-btn { display: flex; }
/* 底部操作 */
.modal-actions { display: flex; gap: 12px; margin-top: 24px; }
.modal-actions button {
flex: 1; padding: 10px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s;
}
.modal-actions .btn-close { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.7); border: 1px solid rgba(255,255,255,0.08); }
.modal-actions .btn-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
.modal-actions .btn-resample {
background: rgba(255,193,7,0.1);
color: #ffc107;
border: 1px solid rgba(255,193,7,0.3);
}
.modal-actions .btn-resample:hover { background: rgba(255,193,7,0.2); border-color: #ffc107; }
/* Toast */
.toast {
position: fixed; top: 60px; left: 50%; transform: translateX(-50%) translateY(-20px);
background: rgba(0,0,0,0.8); color: #fff; padding: 10px 24px; border-radius: 8px;
font-size: 13px; z-index: 1000; opacity: 0; visibility: hidden;
transition: opacity 0.3s, transform 0.3s, visibility 0.3s; white-space: nowrap; pointer-events: none;
}
.toast.show { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="top-bar">
<a class="back-btn" href="javascript:history.back()">&#x25C0; &#x8FD4;&#x56DE;</a>
<span class="top-bar__title">&#x91C7;&#x6837;&#x5386;&#x53F2;</span>
<span></span>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 左侧:快捷查找面板 -->
<div class="search-panel">
<div class="search-panel__title">&#x1F50D; &#x5FEB;&#x6377;&#x67E5;&#x627E;</div>
<!-- 餐品名称搜索 -->
<input type="text" class="search-panel__input" id="searchNameInput" placeholder="&#x8F93;&#x5165;&#x9910;&#x54C1;&#x540D;&#x79F0;...">
<!-- 类目筛选 -->
<div>
<div class="search-panel__title" style="margin-bottom:8px;">&#x1F4C1; &#x6309;&#x7C7B;&#x76EE;&#x7B5B;&#x9009;</div>
<div class="cat-tabs" id="catTabs">
<span class="cat-tab active" data-cat="all">&#x5168;&#x90E8;</span>
<span class="cat-tab" data-cat="&#x8364;&#x83DC;">&#x8364;&#x83DC;</span>
<span class="cat-tab" data-cat="&#x7D20;&#x83DC;">&#x7D20;&#x83DC;</span>
<span class="cat-tab" data-cat="&#x4E3B;&#x98DF;">&#x4E3B;&#x98DF;</span>
<span class="cat-tab" data-cat="&#x6C64;&#x54C1;">&#x6C64;&#x54C1;</span>
<span class="cat-tab" data-cat="&#x996E;&#x54C1;">&#x996E;&#x54C1;</span>
</div>
</div>
<!-- 统计信息 -->
<div class="search-panel__stats">
&#x5171; <em id="totalCount">8</em> &#x6761;&#x8BB0;&#x5F55;
</div>
</div>
<!-- 右侧:瀑布流 -->
<div class="waterfall-area">
<div class="waterfall" id="waterfall"></div>
</div>
</div>
<!-- 详情弹窗 -->
<div class="modal-overlay" id="detailModal">
<div class="modal-container">
<div class="modal-header">
<div class="modal-header__left">
<div class="modal-emoji" id="modalEmoji"></div>
<div>
<div class="modal-title" id="modalTitle"></div>
<div class="modal-subtitle" id="modalSubtitle"></div>
</div>
</div>
<div class="modal-header__right">
<div class="modal-count-wrap">
<div class="modal-count" id="modalCount"></div>
</div>
<div class="modal-time" id="modalTime"></div>
</div>
</div>
<div class="image-grid" id="imageGrid"></div>
<div class="modal-actions">
<button class="btn-close" onclick="closeModal()">&#x2716; &#x5173;&#x95ED;</button>
<button class="btn-resample" id="btnResample" onclick="resampleDish()">&#x1F4F8; &#x8865;&#x5145;&#x91C7;&#x96C6;</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
/**
* 采样历史页面交互逻辑
*/
/* ===== 模拟采样历史数据 ===== */
var SAMPLING_HISTORY = [
{ id: 1, dishId: 1, name: '&#x7EA2;&#x70E7;&#x8089;', emoji: '&#x1F373;', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x732A;&#x8089;', kcal: 480, count: 6, time: '2026-06-10 08:32', images: ['&#x1F373;','&#x1F373;','&#x1F373;','&#x1F373;','&#x1F373;','&#x1F373;'] },
{ id: 2, dishId: 10, name: '&#x7C73;&#x996D;', emoji: '&#x1F35A;', catLabel: '&#x4E3B;&#x98DF;', mainIngredient: '&#x5927;&#x7C73;', kcal: 260, count: 3, time: '2026-06-10 08:28', images: ['&#x1F35A;','&#x1F35A;','&#x1F35A;'] },
{ id: 3, dishId: 7, name: '&#x897F;&#x7EA2;&#x67FF;&#x7092;&#x86CB;', emoji: '&#x1FAD4;', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x9E21;&#x86CB;', kcal: 180, count: 9, time: '2026-06-10 08:15', images: ['&#x1FAD4;','&#x1FAD4;','&#x1FAD4;','&#x1FAD4;','&#x1FAD4;','&#x1FAD4;','&#x1FAD4;','&#x1FAD4;','&#x1FAD4;'] },
{ id: 4, dishId: 13, name: '&#x8C46;&#x6D46;', emoji: '&#x1F95B;', catLabel: '&#x996E;&#x54C1;', mainIngredient: '&#x9EC4;&#x8C46;', kcal: 60, count: 4, time: '2026-06-10 08:05', images: ['&#x1F95B;','&#x1F95B;','&#x1F95B;','&#x1F95B;'] },
{ id: 5, dishId: 9, name: '&#x5BAB;&#x4FDD;&#x9E21;&#x4E01;', emoji: '&#x1F357;', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x9E21;&#x8089;', kcal: 420, count: 5, time: '2026-06-09 12:20', images: ['&#x1F357;','&#x1F357;','&#x1F357;','&#x1F357;','&#x1F357;'] },
{ id: 6, dishId: 2, name: '&#x6E05;&#x7092;&#x65F6;&#x852C;', emoji: '&#x1F96C;', catLabel: '&#x7D20;&#x83DC;', mainIngredient: '&#x83DC;&#x82B1;', kcal: 120, count: 7, time: '2026-06-09 12:05', images: ['&#x1F96C;','&#x1F96C;','&#x1F96C;','&#x1F96C;','&#x1F96C;','&#x1F96C;','&#x1F96C;'] },
{ id: 7, dishId: 5, name: '&#x9999;&#x714E;&#x5E26;&#x9C7C;', emoji: '&#x1F41F;', catLabel: '&#x8364;&#x83DC;', mainIngredient: '&#x5E26;&#x9C7C;', kcal: 320, count: 8, time: '2026-06-08 18:30', images: ['&#x1F41F;','&#x1F41F;','&#x1F41F;','&#x1F41F;','&#x1F41F;','&#x1F41F;','&#x1F41F;','&#x1F41F;'] },
{ id: 8, dishId: 11, name: '&#x6162;&#x7096;&#x9E21;&#x6C64;', emoji: '&#x1F958;', catLabel: '&#x6C64;&#x54C1;', mainIngredient: '&#x571F;&#x9E21;', kcal: 150, count: 3, time: '2026-06-08 18:10', images: ['&#x1F958;','&#x1F958;','&#x1F958;'] }
];
var currentDetailId = null; // 当前打开的详情记录ID
var currentCat = 'all'; // 当前类目筛选
var searchKeyword = ''; // 搜索关键词
/* ===== 拟真食物图片 ===== */
var DISH_IMG_STYLES = {
1: { bg: ['#8B3A2A', '#D4764E', '#C05030'] },
2: { bg: ['#306028', '#50A040', '#408030'] },
3: { bg: ['#706040', '#B89060', '#807050'] },
4: { bg: ['#802018', '#C05030', '#A03820'] },
5: { bg: ['#805020', '#B88040', '#A06830'] },
6: { bg: ['#207040', '#40A868', '#308848'] },
7: { bg: ['#E8A030', '#F0C040', '#D89020'] },
8: { bg: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] },
9: { bg: ['#C87030', '#E8A040', '#D08028'] },
10: { bg: ['#503028', '#805040', '#603830'] },
11: { bg: ['#503028', '#805040', '#603830'] },
12: { bg: ['#306028', '#50A040', '#408030'] },
13: { bg: ['#D0C060', '#E8D880', '#D8C860'] },
14: { bg: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] }
};
function dishFoodSVG(dishId, w, h) {
var style = DISH_IMG_STYLES[dishId] || { bg: ['#304050', '#506870', '#405060'] };
var id = 'dg' + dishId;
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0%" stop-color="' + style.bg[0] + '"/>' +
'<stop offset="50%" stop-color="' + style.bg[1] + '"/>' +
'<stop offset="100%" stop-color="' + style.bg[2] + '"/></linearGradient></defs>' +
'<rect width="' + w + '" height="' + h + '" fill="url(#' + id + ')"/>' +
'<ellipse cx="' + (w*0.4) + '" cy="' + (h*0.45) + '" rx="' + (w*0.3) + '" ry="' + (h*0.25) + '" fill="rgba(255,255,255,0.08)"/>' +
'<ellipse cx="' + (w*0.6) + '" cy="' + (h*0.55) + '" rx="' + (w*0.25) + '" ry="' + (h*0.2) + '" fill="rgba(0,0,0,0.1)"/>' +
'<ellipse cx="' + (w*0.35) + '" cy="' + (h*0.35) + '" rx="' + (w*0.12) + '" ry="' + (h*0.1) + '" fill="rgba(255,255,255,0.15)"/>' +
'</svg>';
}
/* ===== DOM 引用 ===== */
var waterfallEl = document.getElementById('waterfall');
var modalEl = document.getElementById('detailModal');
var modalEmojiEl = document.getElementById('modalEmoji');
var modalTitleEl = document.getElementById('modalTitle');
var modalSubtitleEl = document.getElementById('modalSubtitle');
var modalCountEl = document.getElementById('modalCount');
var modalTimeEl = document.getElementById('modalTime');
var imageGridEl = document.getElementById('imageGrid');
var toastEl = document.getElementById('toast');
var searchNameInputEl = document.getElementById('searchNameInput');
var catTabsEl = document.getElementById('catTabs');
var totalCountEl = document.getElementById('totalCount');
/**
* 获取筛选后的记录
*/
function getFilteredRecords() {
return SAMPLING_HISTORY.filter(function(record) {
if (currentCat !== 'all' && record.catLabel !== currentCat) return false;
if (searchKeyword && record.name.indexOf(searchKeyword) === -1) return false;
return true;
});
}
/**
* 渲染瀑布流
*/
function renderWaterfall() {
var filtered = getFilteredRecords();
totalCountEl.textContent = filtered.length;
if (filtered.length === 0) {
waterfallEl.innerHTML = '<div style="text-align:center;padding:80px 0;color:rgba(255,255,255,0.3);">&#x672A;&#x627E;&#x5230;&#x5339;&#x914D;&#x7684;&#x91C7;&#x6837;&#x8BB0;&#x5F55;</div>';
return;
}
var html = '';
for (var i = 0; i < filtered.length; i++) {
var record = filtered[i];
// 缩略图预览(最多显示3张)
var previewCount = Math.min(record.count, 3);
var previews = '';
for (var j = 0; j < 3; j++) {
if (j < previewCount) {
previews += '<div class="card-preview">' + dishFoodSVG(record.dishId, 60, 60) + '</div>';
} else {
previews += '<div class="card-preview hidden"></div>';
}
}
html += '<div class="waterfall-card" data-id="' + record.id + '" onclick="openDetail(' + record.id + ')">' +
'<div class="card-header">' +
'<div class="card-emoji">' + dishFoodSVG(record.dishId, 56, 56) + '</div>' +
'<div>' +
'<div class="card-name">' + record.name + '</div>' +
'<div class="card-category">' + record.catLabel + ' · ' + record.mainIngredient + ' · ' + record.kcal + 'kcal</div>' +
'</div>' +
'</div>' +
'<div class="card-previews">' + previews + '</div>' +
'<div class="card-footer">' +
'<span class="card-count">&#x5DF2;&#x91C7; ' + record.count + ' &#x5F20;</span>' +
'<span>' + record.time + '</span>' +
'</div>' +
'</div>';
}
waterfallEl.innerHTML = html;
}
/* ===== 打开详情弹窗 ===== */
function openDetail(id) {
var record = null;
for (var i = 0; i < SAMPLING_HISTORY.length; i++) {
if (SAMPLING_HISTORY[i].id === id) { record = SAMPLING_HISTORY[i]; break; }
}
if (!record) return;
currentDetailId = id;
modalEmojiEl.innerHTML = dishFoodSVG(record.dishId, 52, 52);
modalTitleEl.innerHTML = record.name;
modalSubtitleEl.innerHTML = record.catLabel + ' · ' + record.mainIngredient + ' · ' + record.kcal + 'kcal';
modalCountEl.innerHTML = record.count + '<span class="count-label">&#x5F20;</span>';
var parts = record.time.split(' ');
modalTimeEl.innerHTML = '<div>' + (parts[0] || '') + '</div><div>' + (parts[1] || '') + '</div>';
// 已满9张时隐藏补充采集按钮
var btnResample = document.getElementById('btnResample');
btnResample.style.display = record.count >= 9 ? 'none' : '';
renderImageGrid(record);
modalEl.classList.add('active');
}
/* ===== 渲染图片网格 ===== */
function renderImageGrid(record) {
var MAX_IMAGES = 9;
var html = '';
// 已采集的图片
for (var i = 0; i < record.images.length; i++) {
var canRemove = record.images.length > 1;
html += '<div class="image-cell">' +
dishFoodSVG(record.dishId, 120, 120) +
'<span class="image-cell__time">' + record.time + '</span>' +
(canRemove ? '<button class="remove-btn" data-idx="' + i + '">&#x2716;</button>' : '') +
'</div>';
}
// 空白占位
var remaining = MAX_IMAGES - record.images.length;
for (var i = 0; i < remaining; i++) {
html += '<div class="image-cell" style="opacity:0.3;"></div>';
}
imageGridEl.innerHTML = html;
}
/* ===== 删除图片 ===== */
function removeImage(idx) {
var record = null;
for (var i = 0; i < SAMPLING_HISTORY.length; i++) {
if (SAMPLING_HISTORY[i].id === currentDetailId) { record = SAMPLING_HISTORY[i]; break; }
}
if (!record || record.images.length <= 1) return;
record.images.splice(idx, 1);
record.count = record.images.length;
renderImageGrid(record);
renderWaterfall();
showToast('&#x2705; &#x5220;&#x9664;&#x6210;&#x529F;', 1200);
}
/* ===== 关闭弹窗 ===== */
function closeModal() {
modalEl.classList.remove('active');
currentDetailId = null;
}
/* ===== Toast ===== */
function showToast(msg, duration) {
toastEl.innerHTML = msg;
toastEl.classList.add('show');
clearTimeout(toastEl._timer);
var t = duration || 2000;
toastEl._timer = setTimeout(function() {
toastEl.classList.remove('show');
}, t);
}
/* ===== 事件绑定 ===== */
// 图片网格内点击删除
imageGridEl.addEventListener('click', function(e) {
var removeBtn = e.target.closest('.remove-btn');
if (removeBtn) {
e.stopPropagation();
var idx = parseInt(removeBtn.getAttribute('data-idx'));
removeImage(idx);
return;
}
});
// 点击遮罩层关闭弹窗
modalEl.addEventListener('click', function(e) {
if (e.target === modalEl) closeModal();
});
// 类目筛选Tab
catTabsEl.addEventListener('click', function(e) {
var tab = e.target.closest('.cat-tab');
if (!tab) return;
currentCat = tab.getAttribute('data-cat');
var tabs = catTabsEl.querySelectorAll('.cat-tab');
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
tab.classList.add('active');
renderWaterfall();
});
// 名称搜索
var searchTimer = null;
searchNameInputEl.addEventListener('input', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
searchKeyword = searchNameInputEl.value.replace(/^\s+|\s+$/g, '');
renderWaterfall();
}, 300);
});
/**
* 补充采集:跳转到采样页面并携带餐品ID
*/
function resampleDish() {
if (!currentDetailId) return;
var record = null;
for (var i = 0; i < SAMPLING_HISTORY.length; i++) {
if (SAMPLING_HISTORY[i].id === currentDetailId) { record = SAMPLING_HISTORY[i]; break; }
}
if (!record) return;
closeModal();
setTimeout(function() {
window.location.href = '05-dish-sampling.html?resampleDishId=' + record.dishId;
}, 300);
}
/* ===== 初始化 ===== */
renderWaterfall();
</script>
</body>
</html>
@@ -0,0 +1,401 @@
# 餐线营养秤 — 流程流转文档
> 本文档记录各页面之间的流程关系、触发条件、页面职责,用于后续生成流程图和 PRD 文档。
---
## 一、页面清单
| 编号 | 文件名 | 页面名称 | 使用角色 | 状态 |
|------|--------|----------|----------|------|
| 编号 | 文件名 | 页面名称 | 使用角色 | 状态 |
|------|--------|----------|----------|------|
| 01 | `01-meal-start.html` | 开餐界面 | 食堂工作人员 | 已完成 |
| 02 | `02-dish-display.html` | 餐品展示界面 | 就餐用户 | 已完成 |
| 03 | `03-meal-pickup.html` | 取餐界面 | 就餐用户 | 已完成 |
| 04 | `04-settings.html` | 设置界面 | 食堂工作人员 | 已完成 |
| 05 | `05-dish-sampling.html` | 餐品采样界面 | 食堂工作人员 | 已完成 |
---
## 二、角色定义
| 角色 | 说明 | 权限 |
|------|------|------|
| 员工 | 食堂工作人员,系统通过人脸识别判定 | 工作时间:切换环境、餐品采集、加餐、打餐、更换餐品 |
| 客户 | 就餐用户 | 仅可浏览餐品、取餐 |
**非员工权限拦截:** 非员工点击任何操作按钮时,Toast 提示:"您不是管理员,无权进行系统操作"
---
## 二.一 开机默认行为
```
设备开机 / 重启
自动进入 01-开餐界面(默认待机态)
└─ 等待食堂工作人员操作
├─ 放入菜品 → 打餐/开餐流程(见 2.1)
└─ 无操作 → 保持开餐界面待机
```
**关键逻辑:**
- 开机后不经过任何引导页或过渡页,直接进入 01 开餐界面
- 01 为系统默认首页,用于工作人员快速开始操作
- 就餐用户需在 02 餐品展示界面通过人脸识别进入,不属于开机默认流程
---
## 二、主流程(员工侧 — 工作时间内)
### 2.1 打餐(开餐)流程
> 打餐 = 员工将餐品倒入秤中 → 系统识别 → 开餐 → 进入正常售卖状态
```
进入 01-开餐界面
├─ 秤体检测:等待菜品放入
│ │
│ └─ 重量 > 0 且数据稳定 → 触发 AI 图像识别
│ │
│ ├─ 识别置信度 ≥ 阈值 → 自动开餐
│ │ │
│ │ ├─ 自动提交 1 张图片到采样库(用于后续 AI 训练)
│ │ └─ 进入正常售卖状态
│ │
│ ├─ 识别置信度 < 阈值 → 返回候选列表(5个)
│ │ │
│ │ ├─ 员工点击选择候选菜品 → 确认 → 开餐
│ │ └─ 自动提交 1 张图片到采样库
│ │
│ └─ 未识别到菜品 → 显示"未识别"态
│ │
│ └─ 点击"餐品采集" → 跳转 05-餐品采样
└─ 辅助:搜索栏手动搜索菜品(始终可用)
```
**关键逻辑:**
- 重量稳定判定:秤体重量 > 0 且持续 N 秒波动 < 阈值 → 视为稳定
- 自动开餐:AI 识别置信度超过阈值时,无需人工确认,自动完成开餐
- 手动选择:置信度不足时,展示候选列表供员工确认
- 开餐成功后自动提交 1 张图片到采样库,用于 AI 模型迭代
- AI 未识别时,走 05 餐品采样 → 保存后返回 01(URL 携带 `sampleDishId` 参数)
- 01 页面收到 `sampleDishId` 后自动选中该菜品并启用开餐
### 2.2 餐品采集流程
> 餐品采集 = 员工将餐品倒入秤中 → 拍照采样 → 保存到采样库
```
触发条件:01 开餐界面 AI 未识别到菜品 → 点击"餐品采集"
进入 05-餐品采样界面
├─ 左侧:选择已有餐品(分类/搜索/筛选)
│ 或新建餐品(待补充)
├─ 秤体重量判定:
│ ├─ 重量 > 0 且数据稳定 → 启用"拍照"和"保存"按钮
│ └─ 重量 = 0 → "拍照"和"保存"按钮禁用
├─ 右侧:摄像头拍照采集(最多9张)
└─ 保存采集 → 携带 sampleDishId 返回 01-开餐界面
└─ 01 收到参数后自动选中该菜品
```
### 2.3 加餐流程
> 加餐 = 售卖过程中,秤体重量增加 → 系统反算加餐重量 → Toast 通知
```
正常售卖状态中
└─ 秤体重量检测:
└─ 重量有增加 且数据稳定 → 反算加餐重量
├─ 自动记录到补餐表(时间/餐品/加餐重量)
└─ Toast 提示:"✅ 补餐成功,补餐重量 *** 克"
```
**关键逻辑:**
- 加餐判定:当前重量 > 上次稳定重量 + 阈值,且持续 N 秒波动 < 阈值
- 自动记录:无需人工操作,系统自动写入补餐记录
- Toast 通知:即时反馈加餐结果
### 2.4 更换餐品流程
```
员工在 03-取餐界面 → 点击餐品信息区域
跳转至 01-开餐界面
└─ 重新执行打餐/开餐流程(同 2.1)
└─ 开餐成功后 → 返回 03-取餐界面,展示新餐品
```
### 2.5 设置流程
```
从任意页面 → 点击设置入口
进入 04-设置界面
├─ 容器减重:打开容器减重弹窗
└─ 勺具减重:打开勺具减重弹窗
```
### 2.6 默认减重流程
> 默认减重 = 设置空盘子重量 → 后续开餐时系统自动减去皮重
```
04-设置界面 → 点击"默认减重"入口
打开减重弹窗
├─ 提示语:"请将空盘子放在秤上,待重量稳定后点击确认"
├─ 秤体重量实时显示
│ │
│ ├─ 重量 = 0 → 等待放置盘子
│ │
│ ├─ 重量 > 0 且检测中 → "检测中,请等待重量稳定..."
│ │
│ └─ 重量稳定 → 按钮启用 → 显示"重量已稳定,可以确认"
└─ 点击"确认" → 保存减重值(localStorage
├─ Toast 提示:"✅ 默认减重已设置为 ** 克"
└─ 后续开餐时:系统自动从秤体重量中减去皮重
```
**关键逻辑:**
- 减重值持久化存储在 localStorage 中
- 每次开餐/打餐时,系统自动从当前秤体重量中减去已设置的皮重
- 净重 = 秤体重量 - 默认减重
- 非员工用户点击默认减重时,Toast 拦截:"您不是管理员,无权进行系统操作"
---
## 三、主流程(客户侧 — 就餐用户)
### 3.1 取餐流程
```
用户走到设备前
进入 02-餐品展示界面(待机态)
├─ 左上摄像头:人脸识别
│ │
│ ├─ 等待面向 → 黄色标签 + 黄色人脸框
│ ├─ 正在识别 → 青色脉冲标签 + 青色人脸框
│ ├─ 识别成功 → 绿色标签 + 绿色人脸框 → 跳转 03-取餐界面
│ └─ 识别失败 → 红色标签 + 红色人脸框 → 循环重试
├─ 中央:引导文案"先识别 后取餐"
└─ 底部:当前菜品信息展示(图片、名称、单价、热量)
└─ 识别成功后自动跳转至 03
```
### 3.2 员工身份判断逻辑
```
进入 03-取餐界面
├─ 系统判断用户身份(员工 / 非员工)
│ │
│ ├─ 非员工 → 右上角身份标签显示"来访者"(黄色)
│ │ │
│ │ └─ 点击任何操作按钮 → Toast 拦截:"您不是管理员,无权进行系统操作"
│ │
│ └─ 员工 → 右上角身份标签显示"员工"(蓝色)
│ │
│ ├─ 判断当前是否在工作时间内
│ │ │
│ │ ├─ 是 → 显示完整员工操作入口
│ │ │ └─ 加餐时 Toast:"✅ 补餐成功,补餐重量 *** 克"
│ │ │
│ │ └─ 否 → 仅浏览,操作按钮受限
进入 03-取餐界面
├─ 左列(辅助信息):
│ ├─ 识别人像区:摄像头实时画面 + 人脸检测框 + 识别状态 + 身份标签
│ ├─ 用户信息区:头像 + 昵称/ID + 推荐热量
│ └─ 餐品信息区:餐品图标 + 名称 + 单价/热量
├─ 中列(核心展示):
│ ├─ 营养标题行:"本餐次摄入" + 当前餐次信息 + 时间段
│ └─ 营养核心展示区(视图形式):
│ ├─ 已摄入热量(大字 + 环形进度图)
│ └─ 四分类进度条(热量/主食/果蔬/肉蛋豆,带百分比进度条)
└─ 右列:
└─ 取餐记录区:餐品名称/重量/小计 + 合计行
└─ 右上角:设置按钮(跳转 04-设置界面)
```
**关键逻辑:**
- 03 是取餐前的识别引导页,通过人脸识别确认用户身份
- 识别成功后自动跳转至 03,无需手动操作
- 04 是取餐确认页,展示订单详情和营养摄入统计
- 员工身份通过系统判定,在工作时间内可执行加餐/更换餐品等操作
- 非员工点击任何操作按钮时被拦截,Toast 提示:"您不是管理员,无权进行系统操作"
---
## 四、页面跳转关系总览
### 4.1 员工侧(工作时间内)
```
┌──────────────┐ ┌────────────────────┐
│ 01 开餐界面 │──未识别时──→│ 05 餐品采样界面 │
│ ·打餐/开餐 │ │ ·选择/新建餐品 │
│ ·AI识别/候选 │◀─保存返回────│ ·重量>0稳定才可拍照 │
│ ·称重/自动开餐│ (sampleDishId)│ ·拍照采集(9张) │
│ ·设置入口 │ └────────────────────┘
└──────┬───────┘ ▲
│ 开餐成功后 │
▼ │ 设置完成返回
┌──────────────┐ │
│ 04 设置界面 │────┘
│ ·容器减重弹窗 │
│ ·勺具减重弹窗 │
└──────┬───────┘
│ 返回
┌────────────────────┐
│ 03 取餐界面 │
│ ·营养核心展示 │
│ ·补餐检测(重量稳定) │
│ ·点击餐品→回01换菜 │
└────────────────────┘
```
### 4.2 客户侧
```
┌────────────────────┐ 人脸识别成功 ┌──────────────────────────────────────┐
│ 02 餐品展示界面 │ ────────────→ │ 03 取餐界面 │
│ ·人脸识别引导 │ │ ·左侧:人像识别 + 用户信息 + 餐品信息 │
│ ·菜品信息展示 │ │ ·中央:营养摄入可视化(环形图+进度条) │
└────────────────────┘ │ ·右侧:取餐记录 + 合计 │
│ ·非员工操作按钮 → Toast 拦截 │
└──────────────────────────────────────┘
```
---
## 五、状态机
### 5.1 01 开餐界面状态机(打餐/开餐)
```
[IDLE 空闲等待]
│ 秤体检测到重量 > 0 且数据稳定
[WEIGHT_STABLE 重量稳定]
│ 触发 AI 图像识别
[RECOGNIZING 识别中]
│ AI 返回结果
├─ 置信度 ≥ 阈值 → [AUTO_START 自动开餐]
│ │
│ ├─ 自动提交 1 张图片到采样库
│ └─ [MEAL_STARTED 开餐成功/售卖中]
├─ 置信度 < 阈值 → [HAS_CANDIDATES 有候选]
│ │ 用户点击选择
│ ▼
│ [DISH_SELECTED 已选菜品]
│ │ 确认开餐
│ ▼
│ [READY_TO_START 可开餐] ──→ 点击开餐 ──→ [MEAL_STARTED 开餐成功]
└─ 未识别 → [NO_RESULT 未识别]
│ 点击"餐品采集"
跳转 05 采样
```
### 5.2 02 餐品展示界面状态机
```
[WAIT 等待面向]
│ 检测到用户
[SCANNING 正在识别]
│ AI 返回结果
├─ 成功 → [SUCCESS 识别成功] ──→ 自动跳转 03
└─ 失败 → [FAIL 识别失败] ──→ 返回 WAIT
```
### 5.3 03 取餐界面状态机
```
[PICKUP_IDLE 取餐待机]
├─ 秤体重量增加且稳定 → [MEAL_REPLENISH 加餐中]
│ │
│ ├─ 反算加餐重量 → 记录补餐表
│ ├─ Toast 通知
│ └─ 返回 PICKUP_IDLE
└─ 点击餐品信息 → [CHANGE_DISH 更换餐品]
└─ 跳转 01-开餐界面 → 开餐成功后返回 PICKUP_IDLE
```
### 5.4 05 餐品采样界面状态机
```
[NO_DISH 未选餐品]
│ 用户选择/新建餐品
[DISH_SELECTED 已选餐品]
├─ 重量 = 0 → 拍照/保存按钮禁用
└─ 重量 > 0 且稳定 → 拍照/保存按钮启用
│ 拍照采集
[CAPTURING 采集中] ──→ 达到上限(9张) → [CAPTURE_COMPLETE 采集完成]
│ │ 保存
│ ▼
└────────────────────────────────── 返回 01 (携带 sampleDishId)
```
---
## 六、待确认事项
1. **03 取餐后续流程** — 订单确认后的后续操作(生成营养报告、结算等)待补充
2. **02 → 03 跳转** — 识别成功后是否需要过渡动画或确认步骤
3. **重量稳定阈值** — 秤体重量稳定的判定标准(波动范围 g、持续时间 s)待确认
4. **AI 识别阈值** — 自动开餐的置信度阈值(如 85%)待确认
5. **用户身份来源** — 02/03 人脸识别匹配的用户数据来源(会员系统/临时用户),以及员工身份的判定标准
6. **工作时间段配置** — 当前默认三餐时间段,是否需要支持自定义配置
7. **菜品信息展示** — 03 底部展示的菜品信息是否为当日所有菜品,轮播还是固定展示
8. **加餐阈值** — 触发加餐判定的最小重量增量待确认
@@ -0,0 +1,747 @@
# 餐线营养秤 — 产品需求文档
> 模块位置:`other-module/canteen-device/canteen-nutrition-scale/`
---
## 一、模块概述
餐线营养秤是食堂档口终端设备(1280×720),核心功能覆盖秤体初始化、餐品采样、采样信息维护、开餐打菜、AI 菜品识别、补餐检测、取餐引导、营养数据可视化展示等。
### 1.1 整体流程总览
```
设备开机
进入 01-开餐界面(待机态)
【员工端工作流】(按顺序执行,但初始化步骤非强制)
├─ ① 初始化秤体(容器减重 / 勺具减重,按需选择,非强制)
├─ ② 餐品采样(选择已有菜品 → 拍照采集 → 关联图片到采样库)
├─ ③ 采样信息维护(查看采样历史 / 删除图片 / 新增采样)
├─ ④ 开餐(放入菜品 → AI 识别 → 确认菜品 → 进入售卖状态)
│ │
│ └─ 开餐成功后 → 跳转 02-待取餐界面(买家端)
│ │
│ └─ 买家取餐后 → 回到 01(待机态,等待下一次开餐)
└─ ⑤ 补餐(售卖/取餐过程中,重量增加 → 自动记录补餐)
```
### 1.2 开机默认页
> 营养秤开机后默认进入 **01-开餐界面**(待机态),不经过任何引导页或欢迎页。
### 1.3 页面导航结构
```
01 开餐界面 ──┬── AI 识别成功 + 确认开餐 ──→ 02 待取餐界面(买家端)
├── AI 未识别 ──→ 05 餐品采样界面
├── 搜索/候选选择 ──→ 开餐
└── 设置入口 ──→ 04 设置界面
02 待取餐界面 ──┬── 人脸识别成功(站在设备前自动触发)──→ 03 取餐界面
├── 点击底部菜品信息区域 ──→ 01 开餐界面
└── 设置按钮 ──→ 04 设置界面(非员工拦截)
03 取餐界面 ──┬── 大屏:取餐信息 + 营养可视化
├── 小屏:营养摄入柱状图
├── 设置按钮 ──→ 04 设置界面
├── 点击餐品信息卡片 ──→ 01 开餐界面
└── 取餐完成 ──→ 01 开餐界面(待机态)
04 设置界面 ──┬── 开始开餐 ──→ 01 开餐界面
├── 餐品采样 ──→ 05 餐品采样界面
├── 采样历史 ──→ 06 采样历史界面
├── 容器减重 ──→ 弹窗
├── 勺具减重 ──→ 弹窗
└── 返回 ──→ 01 开餐界面(从哪来回哪去)
05 餐品采样界面 ──┬── 保存采集 ──→ 02 待取餐界面(携带 dishId)
├── 保存并继续 ──→ 留在 05 继续采集下一个餐品
├── 取消 ──→ 02 待取餐界面
└── 查看采样历史 ──→ 06 采样历史界面
06 采样历史界面 ──┬── 补充采集 ──→ 05 餐品采样界面(携带 resampleDishId
└── 返回 ──→ 05 餐品采样界面(从哪来回哪去)
```
### 1.4 全局返回路径规则
> **核心原则**:从哪来回哪去。
- 所有页面的返回按钮必须指向进入该页面的来源页
- 若页面有多个入口(如 06 可从 05 或 04 进入),返回按钮使用浏览器 `history.back()` 实现
- 采样页面(05)的「取消」操作统一回到 02 待取餐界面(无论从何进入)
- 采样页面(05)的「保存采集」操作统一跳转 02 待取餐界面(携带 dishId)
### 1.3 状态流转
```mermaid
stateDiagram-v2
[*] --> 01_开餐界面: 开机
01_开餐界面 --> 04_设置界面: 设置入口
04_设置界面 --> 01_开餐界面: 开始开餐/返回
04_设置界面 --> 05_餐品采样: 餐品采样
04_设置界面 --> 06_采样历史: 采样历史
01_开餐界面 --> 05_餐品采样: AI未识别
05_餐品采样 --> 02_待取餐界面: 保存采集/取消
05_餐品采样 --> 05_餐品采样: 保存并继续
01_开餐界面 --> 02_待取餐界面: AI识别成功+确认开餐
02_待取餐界面 --> 03_取餐界面: 人脸识别成功(自动触发)
02_待取餐界面 --> 01_开餐界面: 点击菜品信息
03_取餐界面 --> 01_开餐界面: 取餐完成/点击餐品信息
03_取餐界面 --> 04_设置界面: 设置按钮
05_餐品采样 --> 06_采样历史: 查看采样历史
06_采样历史 --> 05_餐品采样: 补充采集
06_采样历史 --> 05_采样历史: 返回(从哪来回哪去)
```
---
## 二、角色定义
| 角色 | 说明 | 权限 |
|------|------|------|
| **员工** | 食堂工作人员,通过人脸识别 + 排班表匹配判定 | 初始化秤体、餐品采样、采样信息维护、开餐、补餐 |
| **买家** | 就餐用户,通过人脸识别判定 | 查看待取餐界面、取餐确认(结算支付在其他硬件中完成,不在本文档范围内) |
### 2.1 员工身份判定逻辑
> **判定方式**:人脸识别 + 排班表匹配
> **触发方式**:用户站在设备摄像头前自动触发,无需手动点击摄像头入口。
```
用户面向设备摄像头(自动检测)
人脸识别服务
├─ 识别成功 → 获取用户标识(工号/ID)
│ │
│ ▼
│ 查询后端排班表(默认早 09:00 ~ 晚 18:00
│ │
│ ├─ 当前时间在排班内 → 判定为「员工」
│ │ │
│ │ └─ 进入 01-开餐界面,展示员工操作入口
│ │
│ └─ 当前时间不在排班内 → 判定为「买家」
│ │
│ └─ 进入 02-待取餐界面
└─ 识别失败 → 循环重试,超时后进入默认待机态
```
**关键规则**
- 排班表由后端维护,本文档阶段仅使用默认配置(工作日 09:00~18:00
- 排班角色区分功能暂不实现,仅做「在班/不在班」二元判定
- 排班角色细分(如打餐员、补餐员、管理员等)后期再做
### 2.2 非员工权限拦截
非员工点击任何员工操作按钮时,Toast 提示:「您不是管理员,无权进行系统操作」
- 非员工点击设置按钮时,Toast 提示:「⚠️ 非餐厅员工,无权操作系统」,阻止跳转
- 适用页面:01 开餐界面、02 待取餐界面、03 取餐界面等所有含设置入口的页面
### 2.3 全局按钮反馈规则
> 所有用户可交互按钮,无论操作成功或失败,均需通过 Toast 给予明确的反馈提示。
- **成功操作**:Toast 提示操作结果(如「✅ 采集数据已保存」)
- **操作失败**:Toast 提示失败原因(如「⚠️ 请先选择一个餐品」)
- **无操作状态**:若未进行任何操作就点击按钮,Toast 提示当前状态(如「无需取消,未进行任何操作」)
---
## 三、员工端流程详解
员工操作严格按以下流程顺序执行:
```
① 初始化秤体 → ② 餐品采样 → ③ 采样信息维护 → ④ 开餐 → ⑤ 补餐
```
> **说明**:步骤 ①(初始化秤体)为非强制操作,员工可根据实际情况选择是否执行。但步骤 ②~⑤ 为按序执行的必经流程。
### 3.1 ① 初始化秤体
> **目的**:设置秤体的去皮重参数,确保后续称重数据准确。
#### 3.1.1 两种减重方式
| 减重类型 | 减重对象 | 性质 | 说明 |
|---------|---------|------|------|
| **容器减重** | 盛放食物的容器(食物盆) | 一次性 | 设置后在整个开餐周期内生效,后续每次开餐系统自动减去该皮重 |
| **勺具减重** | 取餐勺子 | 多次(动态) | 每次取餐时勺子拿起/放下都会自动扣除勺子重量 |
#### 3.1.2 减重方式差异对比
> **关键差异**:两种减重不仅在减重对象上不同,其工作原理和生效时机也完全不同。
| 对比维度 | 容器减重 | 勺具减重 |
|---------|---------|---------|
| **减重对象** | 盛放食物的容器 | 取餐勺子 |
| **生效性质** | 设置后持久生效,直到更换容器 | 每次取餐循环动态扣除 |
| **扣除时机** | 称重时从总重中扣除 | 取餐时:勺子拿起 → 称重 → 勺子放下 → 反算净重 |
| **重量变化检测** | 开餐瞬间:`净重 = 当前重量 - 容器皮重` | 取餐过程中:`取走重量 = 勺子放下后重量 - 勺子拿起时重量 - 勺具皮重` |
| **设置持久化** | `localStorage` 持久存储,重启后仍有效 | `localStorage` 持久存储,重启后仍有效 |
| **重新设置** | 更换不同容器时需重新设置 | 更换不同勺子时需重新设置 |
#### 3.1.3 设置界面入口
04 设置界面可通过以下方式进入:
- 01-开餐界面 → 顶部右上角设置按钮
- 02-待取餐界面 → 右上角设置按钮(非员工拦截)
- 03-取餐界面 → 取餐记录区设置按钮(非员工拦截)
#### 3.1.4 容器减重流程
```
04-设置界面 → 点击「容器减重」
打开容器减重弹窗
├─ 提示语:「将盛放食物的容器(食物盆)放在秤上,待重量稳定后点击『确认』」
├─ 秤体重量实时显示
│ │
│ ├─ 重量 = 0 → 「等待放置容器...」
│ ├─ 重量 > 0 且检测中 → 「检测中,请等待重量稳定...」
│ └─ 重量稳定 → 按钮启用 → 「重量已稳定,可以确认」
└─ 点击「确认」→ 保存减重值
├─ Toast:「✅ 容器减重已设置为 ** 克」
└─ 后续开餐时:系统自动从秤体重量中减去容器皮重
```
**稳定判定标准**:重量波动 < 5g,持续 3 秒。
#### 3.1.5 勺具减重流程
```
04-设置界面 → 点击「勺具减重」
打开勺具减重弹窗
├─ 提示语:「将取餐勺子放在秤上,待重量稳定后点击『确认』。设置后每次取餐时勺子拿起放下都会自动扣除勺子重量。」
├─ 秤体重量实时显示
│ │
│ ├─ 重量 = 0 → 「等待放置勺子...」
│ ├─ 重量 > 0 且检测中 → 「检测中,请等待重量稳定...」
│ └─ 重量稳定 → 按钮启用 → 「重量已稳定,可以确认」
└─ 点击「确认」→ 保存减重值
├─ Toast:「✅ 勺具减重已设置为 ** 克」
└─ 每次取餐时:系统自动在勺子拿起/放下循环中扣除勺子皮重
```
**稳定判定标准**:重量波动 < 5g,持续 3 秒。
#### 3.1.6 非强制规则
- 容器减重和勺具减重均为**按需选择**,非强制操作
- 员工可以只做容器减重、只做勺具减重、两者都做、或都不做
- 未设置减重时,系统以秤体原始重量进行计算(`净重 = 当前重量`
- 已设置减重时,系统自动扣除:`净重 = 当前重量 - 容器皮重 - 勺具皮重`
### 3.2 ② 餐品采样
> **目的**:将后台数据库中已有的菜品与实际菜品图片关联在一起,为 AI 摄像头识别提供训练样本。
#### 3.2.1 触发入口
- 01-开餐界面:AI 未识别到菜品 → 点击「餐品采集」→ 跳转 05-餐品采样
#### 3.2.2 采样流程
```
进入 05-餐品采样界面
├─ 左侧:选择已有餐品(分类 Tab / 搜索 / 列表)
│ │
│ └─ 点击已有餐品 → 选中 → 启用右侧拍照区
│ 餐品列表由后端数据库提供,不支持新建餐品
├─ 右侧:摄像头拍照采集(最多 9 张)
│ │
│ ├─ 点击「拍照采集」→ 拍照 → 图片进入采集结果网格
│ └─ 点击网格中的图片 → 可删除该张图片
└─ 底部操作按钮
├─ 点击「取消」→ Toast 反馈 → 跳转至 02-待取餐界面
├─ 点击「重新采样」→ 删除该餐品全部已采图片,清空表单,保留餐品选中状态,Toast 反馈
├─ 点击「保存采集」→ 提交数据 → 跳转至 02-待取餐界面(携带 dishId)
│ (补充采集模式下隐藏此按钮)
└─ 点击「保存并继续」→ 提交数据 → 重置表单继续采集下一个餐品,Toast 反馈
(补充采集模式下隐藏此按钮)
```
#### 3.2.3 采样前置条件
| 条件 | 说明 |
|------|------|
| 已选中餐品 | 左侧列表必须选中一个餐品 |
| 至少采集 1 张图片 | 采集结果网格不能为空 |
#### 3.2.4 重新采样 vs 补充采集
> 两者操作对象不同,不可混淆。
| 操作 | 触发位置 | 行为 | 结果 |
|------|---------|------|------|
| **重新采样** | 05-餐品采样界面 → 点击「重新采样」 | 删除该餐品已采集的**全部**图片,清空采集结果网格,保留餐品选中状态 | 覆盖式重新采集,之前采样的图片全部作废 |
| **补充采集** | 06-采样历史 → 点击「补充采集」 | 跳转 05 并携带 `resampleDishId`,页面**自动选中该餐品**并加载已有图片,用户可直接追加拍照 | 追加式采集,最多 9 张,满额后不可追加 |
#### 3.2.5 底部按钮差异
| 按钮 | 正常采样模式 | 补充采集模式 | 行为 |
|------|------------|------------|------|
| 取消 | 显示 | 显示 | 清空当前操作状态 |
| 重新采样 | 显示 | 显示 | 清空该餐品全部已采图片 |
| 保存采集 | 显示 | 显示 | 提交数据 → 跳转至 02-待取餐界面 |
| 保存并继续 | 显示 | **隐藏** | 提交数据 → 重置表单继续采集下一个餐品 |
#### 3.2.6 餐品字段说明(后端预置)
> 餐品信息由后端数据库预置,采样页面仅做选择关联,不提供新建功能。
| 字段 | 类型 | 说明 |
|------|------|------|
| 餐品 ID | number | 唯一标识 |
| 餐品名称 | string | 如「红烧肉」、「宫保鸡丁」 |
| 餐品分类 | string | 热菜 / 凉菜 / 主食 / 汤羹 / 饮品 / 水果 / 其他 |
| 主食材 | string | 如「猪肉」、「鸡肉」、「豆腐」 |
| 热量(kcal | number | 每 100 克热量 |
| 单价(元/100 克) | number | 每 100 克价格,保留 2 位小数 |
### 3.3 ③ 采样信息维护
> **目的**:管理已采样的菜品记录,包括查看、删除图片、补充采集。
#### 3.3.1 入口
- 05-餐品采样界面 → 顶部右上角「📝 查看采样历史」→ 跳转 06-采样历史
- 04-设置界面 → 「采样历史」卡片 → 跳转 06-采样历史
#### 3.3.2 采样历史列表展示
```
进入 06-采样历史界面
└─ 餐品卡片列表
├─ 每张卡片展示:菜品图片 + 名称
└─ 点击卡片 → 弹出详情弹窗
├─ 详情内容:已采集图片网格(最多 9 格)
├─ 副标题:品类 · 主食材 · 热量
├─ 采样数量:置于弹窗右上角,仿 01 页面识别率徽章样式(数字 + 「张」横向排列)
├─ 采样时间:置于采样数量右侧,日期和时间分两行显示
├─ 每张图片底部悬浮显示采样时间
└─ 支持操作:
├─ 单张图片删除(点击「✕」按钮)
├─ 全部清空
└─ 补充采集(跳转 05,携带 dishId)
```
#### 3.3.3 操作权限
| 操作 | 是否允许 | 说明 |
|------|---------|------|
| 查看采样记录 | ✅ | 浏览已采样菜品及图片 |
| 删除单张图片 | ✅ | 从详情弹窗中删除单张采集图片 |
| 清空全部图片 | ✅ | 删除该菜品的所有采集图片 |
| 补充采集 | ✅ | 跳转 05-餐品采样,为已有菜品追加采集 |
| **编辑字段** | ❌ | 品类、主食材、热量、单价等字段均不可编辑 |
| **删除整条采样记录** | ❌ | 采样记录一旦建立,不可删除整条记录 |
| **全部清空** | ❌ | 不提供全部清空操作 |
### 3.4 ④ 开餐
> **目的**:员工将菜品放入秤中,系统通过 AI 图像识别判定菜品,完成开餐并进入售卖状态。
#### 3.4.1 开餐前置条件
| 条件 | 说明 |
|------|------|
| 员工身份已确认 | 人脸识别 + 排班表匹配通过 |
| 秤体重量 > 0 且数据稳定 | 菜品已放在秤上 |
| AI 识别完成 或 手动选择菜品 | 系统已识别或员工已选中菜品 |
#### 3.4.2 开餐流程
```
进入 01-开餐界面
├─ 秤体检测:等待菜品放入
│ │
│ └─ 重量 > 0 且数据稳定(波动 < 5g 持续 3 秒)→ 触发 AI 图像识别
│ │
│ ├─ 识别置信度 ≥ 阈值(≥ 80%)→ 自动选中识别率最高的菜品 → 启用开餐按钮
│ │ │
│ │ └─ 点击「开餐」→ 开餐成功
│ │ │
│ │ ├─ 自动提交 1 张图片到采样库(localStorage 暂存)
│ │ └─ 跳转 02-待取餐界面(携带 dishId)
│ │
│ ├─ 识别置信度 < 阈值 → 展示 5 个候选菜品
│ │ │
│ │ ├─ 员工点击选择候选菜品 → 默认选中识别率最高的 → 启用开餐按钮
│ │ │ │
│ │ │ └─ 点击「开餐」→ 开餐成功 → 自动提交采样库 → 跳转 02
│ │ │
│ │ └─ 自动提交 1 张图片到采样库
│ │
│ └─ 未识别到菜品 → 显示「未识别」态
│ │
│ └─ 点击「餐品采集」→ 跳转 05-餐品采样
│ │
│ └─ 保存后携带 sampleDishId 返回 01 → 自动选中该餐品 → 启用开餐
└─ 辅助:搜索栏手动搜索菜品(始终可用)
└─ 搜索后默认选中搜索结果中第一个餐品 → 启用开餐按钮
```
#### 3.4.3 开餐成功后行为
1. 系统记录开餐信息(时间、菜品、重量、操作员)
2. 自动提交 1 张图片到采样库(用于 AI 模型迭代训练)
3. **跳转至 02-待取餐界面**,展示当前开餐绑定的菜品信息
4. 买家只能在开餐成功后的 02 界面进行人脸识别取餐
#### 3.4.4 关键业务规则
| 规则 | 说明 |
|------|------|
| 一秤一菜 | 一个秤体上只放置一种菜品,不存在多菜品共存场景 |
| 开餐绑定菜品 | 开餐时选定的菜品,即为买家取餐时获取的菜品 |
| 净重计算 | `净重 = 秤体重量 - 容器皮重 - 勺具皮重`(未设置的皮重项为 0 |
| 重量稳定判定 | 重量波动 < 5g,持续 3 秒 |
| AI 识别阈值 | 置信度 ≥ 80% 时自动选中菜品并启用开餐按钮 |
| 自动选中(AI 识别) | 识别到菜品时,默认选中识别率最高的那个 |
| 候选餐品范围 | 默认读取当天餐谱中的餐品作为候选列表,非全量展示;仅当用户主动搜索时才进行全量搜索 |
| 自动选中(手动搜索) | 搜索后默认选中搜索结果中第一个餐品 |
| 自动选中(采样返回) | 从 05 页面携带 `sampleDishId` 返回 01 时,自动选中该餐品并启用开餐 |
| 开餐自动提交采样库 | 每次开餐成功自动提交 1 张图片到采样库(localStorage 暂存) |
| 开餐页面跳转 | 开餐成功后跳转至 02-待取餐界面(携带 dishId |
| 01 开餐界面入口 | 共 4 个入口:①开机默认进入 ②02/03 点击菜品信息进入 ③04 设置页「开始开餐」按钮进入 ④05 采样页保存后跳转到 02(不直接回 01) |
### 3.5 ⑤ 补餐
> **目的**:售卖/取餐过程中,当秤体重量增加时,系统自动检测并记录补餐。
#### 3.5.1 补餐流程
```
正常售卖/取餐状态中(02 或 03 界面)
└─ 秤体重量检测:
└─ 重量有增加 且数据稳定 → 反算补餐重量
├─ 自动记录到补餐表(时间 / 餐品 / 补餐重量)
└─ Toast 提示:「✅ 补餐成功,补餐重量 *** 克」
```
#### 3.5.2 补餐判定逻辑
| 规则 | 说明 |
|------|------|
| 触发条件 | 当前重量 > 上次稳定重量 + 100g |
| 稳定判定 | 重量波动 < 5g,持续 3 秒 |
| 归属判定 | 一秤一菜,补餐重量直接归属当前开餐绑定的菜品 |
| 自动记录 | 无需人工操作,系统自动写入补餐记录 |
| Toast 通知 | 即时反馈补餐结果 |
#### 3.5.3 补餐记录
补餐记录表包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| 补餐时间 | timestamp | 补餐发生的具体时间 |
| 餐品 ID | number | 当前开餐绑定的菜品 ID |
| 补餐重量(g) | number | 增加的净重(已扣除皮重) |
| 操作员 | string | 执行开餐的员工 ID |
---
## 四、买家端流程详解
> **说明**:买家的结算支付环节在其他硬件中完成,不在本文档范围内。本文档仅描述取餐前的引导流程。
### 4.1 02-待取餐界面
> **定位**:开餐成功后的待机页面,引导买家进行人脸识别取餐。
#### 4.1.1 页面展示
| 区域 | 内容 |
|------|------|
| 中央 | 引导文案:「先识别 后取餐」(大字展示) |
| 底部 | 当前开餐绑定的菜品信息:图片、名称、单价(元/100克)、热量 |
#### 4.1.2 进入条件
- 01-开餐界面 → 开餐成功 → 自动跳转至 02(携带 dishId)
- 05-餐品采样界面 → 保存采集 / 取消 → 跳转至 02
- 02 展示的开餐菜品即为 01 中确认的菜品
#### 4.1.3 页面交互
- 买家站在设备前 → 自动触发人脸识别 → 识别成功 → 跳转 03-取餐界面(无需手动点击)
- 点击底部菜品信息区域 → 跳转 01-开餐界面(员工端入口)
- 点击右上角设置按钮 → 员工跳转 04-设置界面;非员工 Toast 提示「⚠️ 非餐厅员工,无权操作系统」
### 4.2 03-取餐界面
> **定位**:人脸识别成功后展示取餐信息、营养摄入可视化、取餐记录。
#### 4.2.1 进入条件
- 02-待取餐界面 → 人脸识别成功 → 自动跳转至 03
#### 4.2.2 页面展示
| 区域 | 内容 |
|------|------|
| 左列 | 识别人像区 + 用户信息区 + 餐品信息区(点击可进入 01 开餐界面) |
| 中列 | 营养摄入可视化(环形图 + 四分类进度条) |
| 右列 | 取餐记录区(餐品名称 / 重量 / 小计 + 合计行)+ 设置按钮(非员工拦截) |
#### 4.2.3 取餐完成
- 买家取走菜品后,系统记录取餐数据
- 结算支付在其他硬件中完成
- 取餐完成后,系统回到 01-开餐界面(待机态)
#### 4.2.4 页面交互
- 点击餐品信息卡片 → 跳转 01-开餐界面
- 点击设置按钮 → 员工跳转 04-设置界面;非员工 Toast 提示「⚠️ 非餐厅员工,无权操作系统」
---
## 五、页面清单
| 编号 | 文件名 | 页面名称 | 使用角色 | 状态 |
|------|--------|----------|----------|------|
| 01 | `01-meal-start.html` | 开餐界面 | 员工 | 已完成 |
| 02 | `02-dish-display.html` | 待取餐界面 | 买家 | 已完成 |
| 03 | `03-meal-pickup.html` | 取餐界面(单屏) | 买家 | 已完成 |
| 03-L | `03-meal-pickup-dual-large.html` | 取餐界面(双屏·大屏 1280×720) | 买家 | 已完成 |
| 03-S | `03-meal-pickup-dual-small.html` | 取餐界面(双屏·小屏 480×320) | 买家 | 已完成 |
| 04 | `04-settings.html` | 设置界面 | 员工 | 已完成 |
| 05 | `05-dish-sampling.html` | 餐品采样界面 | 员工 | 已完成 |
| 06 | `06-sampling-history.html` | 采样历史界面 | 员工 | 已完成 |
---
## 六、状态机
### 6.1 设备全局状态机
```mermaid
stateDiagram-v2
[*] --> IDLE: 开机
IDLE --> INIT_SCALE: 员工选择初始化
INIT_SCALE --> IDLE: 初始化完成(非强制,可随时跳过)
IDLE --> SAMPLING: 员工进入采样
SAMPLING --> IDLE: 采样保存返回
IDLE --> MEAL_STARTED: 开餐成功
MEAL_STARTED --> WAIT_PICKUP: 进入待取餐界面
WAIT_PICKUP --> PICKUP: 人脸识别成功
PICKUP --> MEAL_STARTED: 补餐检测(重量增加)
PICKUP --> IDLE: 取餐完成
```
### 6.2 01 开餐界面状态机
```
[IDLE 空闲等待]
│ 秤体检测到重量 > 0 且数据稳定
[WEIGHT_STABLE 重量稳定]
│ 触发 AI 图像识别
[RECOGNIZING 识别中]
│ AI 返回结果
├─ 置信度 ≥ 阈值 → [AUTO_START 自动开餐]
│ │
│ ├─ 自动提交 1 张图片到采样库
│ └─ [MEAL_STARTED 开餐成功] → 跳转 02
├─ 置信度 < 阈值 → [HAS_CANDIDATES 有候选]
│ │ 员工点击选择
│ ▼
│ [DISH_SELECTED 已选菜品] → 启用开餐按钮
│ │
│ └─ 点击开餐 → [MEAL_STARTED] → 跳转 02
└─ 未识别 → [NO_RESULT 未识别]
│ 点击「餐品采集」
跳转 05 采样
```
### 6.3 05 餐品采样界面状态机
```
[NO_DISH 未选餐品]
│ 用户从列表中选择餐品
[DISH_SELECTED 已选餐品]
├─ 重量 = 0 → 拍照/保存按钮禁用
└─ 重量 > 0 且稳定 → 拍照/保存按钮启用
│ 拍照采集
[CAPTURING 采集中] ──→ 达到上限(9张) → [CAPTURE_COMPLETE 采集完成]
│ │ 保存采集
│ ├─→ 跳转 02 (携带 dishId)
│ │ 保存并继续
│ ├─→ 留在 05 (重置表单)
│ └─→ 取消
│ 跳转 02
```
### 6.4 03 取餐界面状态机
```
[PICKUP_IDLE 取餐待机]
├─ 秤体重量增加且稳定 → [MEAL_REPLENISH 补餐中]
│ │
│ ├─ 反算补餐重量 → 记录补餐表
│ ├─ Toast 通知
│ └─ 返回 PICKUP_IDLE
└─ 取餐完成 → [PICKUP_DONE]
└─ 跳转 01-开餐界面(待机态)
```
---
## 七、数据字段表
### 7.1 餐品数据(后端预置,采样页面仅做选择关联)
| 字段 | 类型 | 说明 |
|------|------|------|
| 餐品 ID | number | 唯一标识 |
| 餐品名称 | string | 如「红烧肉」、「宫保鸡丁」 |
| 餐品分类 | string | 热菜 / 凉菜 / 主食 / 汤羹 / 饮品 / 水果 / 其他 |
| 主食材 | string | 如「猪肉」、「鸡肉」、「豆腐」 |
| 热量(kcal | number | 每 100 克热量 |
| 单价(元/100 克) | number | 每 100 克价格,保留 2 位小数 |
### 7.2 容器减重(04-设置界面)
| 字段 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 皮重(克) | number | 是 | 由秤体自动读取,≥ 0 |
| 稳定判定 | boolean | 是 | 重量波动 < 5g 持续 3 秒 |
### 7.3 勺具减重(04-设置界面)
| 字段 | 类型 | 必填 | 校验规则 |
|------|------|------|---------|
| 皮重(克) | number | 是 | 由秤体自动读取,≥ 0 |
| 稳定判定 | boolean | 是 | 重量波动 < 5g 持续 3 秒 |
### 7.4 采样保存提交
> 员工在 05-餐品采样界面点击「保存采集」时提交的数据。
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 餐品 ID | number | 是 | 当前选中餐品的 ID |
| 采集图片列表 | array&lt;string&gt; | 是 | 已拍摄的图片数据(URL 或 Base64 |
| 采样时间 | timestamp | 是 | 保存时的时间戳 |
| 操作员工 ID | string | 是 | 执行采样的员工标识 |
| 菜品重量(g | number | 是 | 采样时秤体的净重(已扣除皮重) |
### 7.5 补餐记录(系统自动写入)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 补餐时间 | timestamp | 是 | 补餐发生的具体时间 |
| 餐品 ID | number | 是 | 当前开餐绑定的菜品 ID |
| 补餐重量(g | number | 是 | 增加的净重(已扣除皮重) |
| 操作员 | string | 是 | 执行开餐的员工 ID |
---
## 八、采样库核心规则
### 8.1 反样本机制
> 目的:防止误操作导致无效图片污染采样库,提升 AI 训练数据质量。
| 反样本类型 | 说明 | 处理方式 |
|-----------|------|---------|
| **空秤图片** | 秤体上无任何菜品,重量 = 0 | 禁止拍照采集,Toast:「秤体上无菜品,无法采样」 |
| **纯水图片** | 仅放置水杯/液体,无固体菜品 | 标记为反样本,单独存储,不纳入正常训练集 |
| **异常物体** | 明显非菜品的物品(餐具、手机等) | 标记为反样本 |
| **极端遮挡** | 菜品被容器/盖子遮挡超过 70% | 提示:「请确保菜品完全可见后再采样」 |
| **重复帧** | 连续两张图片相似度 > 98% | 自动丢弃后一张,Toast:「图片过于相似,已跳过」 |
**反样本存储策略**
- 反样本图片单独存放在 `anti_sample/` 目录
- 数据库中标记 `is_anti_sample: true`
- 采样历史界面以不同样式展示(灰色边框 + 「反样本」标签)
### 8.2 图片存储上限与覆盖策略
| 参数 | 值 | 说明 |
|------|-----|------|
| **单菜品图片上限** | 50 张 | 超过此数量后触发覆盖逻辑 |
| **覆盖策略** | 相似度覆盖 | 新图片与已有图片对比,覆盖相似度最低的那张 |
| **相似度算法** | 余弦相似度(特征向量) | 通过图片特征提取模型计算 |
| **覆盖前确认** | 弹窗确认 | 展示将被覆盖的图片,员工确认后执行 |
---
## 九、关键技术参数
| 参数 | 当前值 | 状态 |
|------|--------|------|
| 重量稳定判定:波动阈值 | < 5g | 已确认 |
| 重量稳定判定:持续时间 | 3 秒 | 已确认 |
| AI 识别置信度阈值(自动开餐) | ≥ 80% | 已确认 |
| 补餐触发最小重量增量 | 100g | 已确认 |
| 人脸识别超时时间 | 无限制 | 已确认 |
| 排班表时间段配置 | 工作日 09:00~18:00 | 后端默认,前端暂不配置 |
---
## 十、变更日志
| 日期 | 版本 | 变更内容 |
|------|------|---------|
| 2026-06-10 | v1.0 | 初始创建,覆盖 6 个页面的基础需求描述 |
| 2026-06-10 | v1.0 | 新增采样库核心规则:反样本机制 + 50 张上限与相似度覆盖策略 |
| 2026-06-10 | v2.0 | 重构 PRD:按员工端流程重写(初始化秤体 → 采样 → 采样维护 → 开餐 → 补餐),明确两种减重方式差异,明确买家取餐流程与开餐绑定关系,明确采样历史仅支持删除+新增、不可编辑 |
| 2026-06-11 | v2.1 | 去掉「新建餐品」功能:餐品全部由后端数据库预置,采样页面仅做选择关联;设置页去掉环境切换和餐品采样入口,仅保留两个减重入口;身份判定逻辑明确为「排班表+人脸匹配」,排班角色细分后期实现;买家取餐每次均需重新人脸识别;补餐最小增量 100g;设置页减重卡片副标题去掉「一次性/多次」提示,初始显示 0 克 |
| 2026-06-11 | v2.2 | 采样保存提交字段确认:餐品 ID + 采集图片列表 + 采样时间 + 操作员工 ID + 菜品重量;明确重新采样(清空全部图片覆盖式重采)与补充采集(追加式,最多 9 张)的区别;修复 06 采样历史页面补充采集跳转路径 Bug(06→05);补充采集自动选中餐品无需手动选择;去掉「全部清空」操作 |
| 2026-06-11 | v2.3 | 采样页面新增「保存并继续」按钮(提交后留在当前页继续采集下一个餐品);「保存采集」按钮改为跳转至 02-待取餐界面(携带 dishId);补充采集模式隐藏「保存并继续」按钮;新增全局按钮 Toast 反馈规则(所有按钮操作均需 Toast 反馈成功/失败状态) |
| 2026-06-11 | v2.4 | 06 采样历史详情弹窗重新布局:餐品信息(品类·主食材·热量)放左侧,采样数量仿 01 识别率徽章样式置于右上角,采样时间单独置于最右侧 |
| 2026-06-11 | v2.5 | 06 采样历史详情弹窗优化:采样数量徽章由纵向排列改为横向排列(数字 + 「张」同行显示);采样时间由单行改为日期 / 时间两行显示 |
| 2026-06-11 | v2.6 | 01 开餐界面流程补全:开餐成功自动提交 1 张图片到采样库并跳转至 02-待取餐界面(携带 dishId);修复采样页面返回时 sampleDishId 参数未处理导致不自动选中的 BUG;新增非员工点击设置按钮拦截(Toast 提示「非餐厅员工,无权操作系统」);明确自动选中规则(AI 识别选中识别率最高、搜索选中第一个结果、采样返回自动选中对应餐品);补全重量稳定判定(波动 < 5g 持续 3 秒)到开餐流程描述;明确营养秤开机默认进入 01-开餐界面 |
| 2026-06-11 | v2.7 | 全局返回路径规则:从哪来回哪去;04 设置界面新增「开始开餐」按钮和「采样历史」入口,返回按钮改为回 01;05 采样页「取消」操作改为回到 02 待取餐界面,「保存采集」同样跳转 02;05/06 返回按钮改为 history.back()(遵循从哪来回哪去);02/03 页面餐品信息区域增加点击进入 01 开餐界面的交互;02 页面补全 dishId 参数处理 + 自动人脸识别触发(站在设备前自动识别)+ 识别成功跳转 03 + 新增设置按钮(非员工拦截);03 设置按钮修复跳转 04 + 非员工拦截;06 采样历史新增 04 设置页面入口;人脸识别明确为自动触发,无需手动摄像头入口;01 开餐界面候选餐品默认读取当天餐谱,搜索时才全量搜索 |
@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280, initial-scale=1.0">
<title>餐线营养秤 - 蚁熊智能餐养平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Microsoft YaHei", "PingFang SC", sans-serif;
background: #0e1326;
color: #fff;
width: 1280px;
height: 720px;
overflow: hidden;
}
.top-bar {
display: flex;
align-items: center;
height: 44px;
background: #1a2b4a;
padding: 0 20px;
}
.back-btn {
font-size: 14px;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
.top-bar__title {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
}
.main-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100% - 44px);
gap: 16px;
padding: 24px;
}
.card {
display: flex;
align-items: center;
gap: 20px;
width: 480px;
padding: 24px 28px;
background: #1a2b4a;
border-radius: 14px;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
border-color: rgba(74, 144, 217, 0.3);
}
.card__icon { font-size: 28px; width: 40px; text-align: center; flex-shrink: 0; }
.card__info { flex: 1; }
.card__title { font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 4px; }
.card__desc { font-size: 12px; color: rgba(255, 255, 255, 0.5); }
.card__arrow { font-size: 18px; color: rgba(255, 255, 255, 0.4); flex-shrink: 0; }
</style>
</head>
<body>
<div class="top-bar">
<a class="back-btn" href="../index.html">&#x25C0; &#x8FD4;&#x56DE;</a>
<span class="top-bar__title">&#x9910;&#x7EBF;&#x8425;&#x517B;&#x79E4;</span>
</div>
<div class="main-content">
<a class="card" href="01-meal-start.html">
<span class="card__icon">&#x1F37D;&#xFE0F;</span>
<div class="card__info">
<div class="card__title">开餐界面</div>
<div class="card__desc">开餐操作、今日菜品概览与就餐引导</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="02-dish-display.html">
<span class="card__icon">&#x1F4CB;</span>
<div class="card__info">
<div class="card__title">餐品展示界面</div>
<div class="card__desc">菜品详情、营养成分与热量展示</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="03-meal-pickup.html">
<span class="card__icon">&#x1F9FE;</span>
<div class="card__info">
<div class="card__title">取餐界面(单屏)</div>
<div class="card__desc">取餐确认、订单生成与营养报告</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="03-meal-pickup-dual-large.html">
<span class="card__icon">&#x1F5A5;&#xFE0F;</span>
<div class="card__info">
<div class="card__title">取餐界面(双屏·大屏 1280×720)</div>
<div class="card__desc">大屏取餐信息展示、用户信息与取餐记录</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="03-meal-pickup-dual-small.html">
<span class="card__icon">&#x1F4CA;</span>
<div class="card__info">
<div class="card__title">取餐界面(双屏·小屏 480×320</div>
<div class="card__desc">小屏营养摄入柱状图与推荐热量展示</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="04-settings.html">
<span class="card__icon">&#x2699;&#xFE0F;</span>
<div class="card__info">
<div class="card__title">设置界面</div>
<div class="card__desc">环境切换、设备参数配置</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="05-dish-sampling.html">
<span class="card__icon">&#x1F4F8;</span>
<div class="card__info">
<div class="card__title">餐品采样界面</div>
<div class="card__desc">菜品拍照采集、营养数据录入与 AI 识别匹配</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
<a class="card" href="06-sampling-history.html">
<span class="card__icon">&#x1F4DD;</span>
<div class="card__info">
<div class="card__title">采样历史</div>
<div class="card__desc">历史采样记录查看、采集图片管理</div>
</div>
<span class="card__arrow">&#x203A;</span>
</a>
</div>
</body>
</html>