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:
co-authored by
Claude Opus 4.6
parent
a375da8792
commit
dcf2757863
@@ -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">◀</a>
|
||||
<span class="page-title">开餐界面</span>
|
||||
<a class="settings-btn" href="04-settings.html" title="设置">⚙</a>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区:左识别 - 中餐品 - 右重量 -->
|
||||
<div class="main-area">
|
||||
|
||||
<!-- ===== 左列:餐品识别区 ===== -->
|
||||
<div class="recognize-col">
|
||||
<div class="col-label">📷 餐品识别</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>识别完成</span>
|
||||
</div>
|
||||
<button class="reidentify-btn" onclick="reIdentify()">🔄 重新识别</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== 中列:选择餐品区 ===== -->
|
||||
<div class="dish-col">
|
||||
<div class="col-label">🍽️ 选择餐品</div>
|
||||
|
||||
<!-- 统一容器:已选餐品 + 搜索 + 餐品列表 -->
|
||||
<div class="dish-container">
|
||||
|
||||
<!-- 已选餐品展示 -->
|
||||
<div class="picked-dish" id="pickedDish">
|
||||
<div class="picked-dish__emoji" id="pickedEmoji">🍗</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">🔍</span>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="搜索餐品名称..." 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">智能识别候选</span>
|
||||
</div>
|
||||
<div class="candidate-list" id="candidateList">
|
||||
<!-- 餐品卡片由JS动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 右列:重量区 ===== -->
|
||||
<div class="weight-col">
|
||||
<div class="col-label">⚖️ 秤重读数</div>
|
||||
<div class="weight-display">
|
||||
<div class="weight-label">当前重量</div>
|
||||
<div class="weight-value"><span id="weightValue">285</span><span class="weight-unit">克</span></div>
|
||||
</div>
|
||||
<button class="clear-btn" onclick="clearWeight()">↻ 重量清零</button>
|
||||
<button class="start-meal-btn" id="startBtn" onclick="startMeal()">开 餐</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 餐品数据库(模拟)
|
||||
*/
|
||||
var DISH_DB = [
|
||||
{ id: 1, name: '红烧肉', emoji: '🍳', category: '荤菜', mainIngredient: '猪肉', kcal: 480, price: 5 },
|
||||
{ id: 2, name: '宫保鸡丁', emoji: '🍗', category: '荤菜', mainIngredient: '鸡肉', kcal: 420, price: 5 },
|
||||
{ id: 3, name: '麻婆豆腐', emoji: '🧆', category: '荤菜', mainIngredient: '豆腐', kcal: 280, price: 4 },
|
||||
{ id: 4, name: '清炒时蔬', emoji: '🥬', category: '素菜', mainIngredient: '菜花', kcal: 120, price: 3 },
|
||||
{ id: 5, name: '香煎带鱼', emoji: '🐟', category: '荤菜', mainIngredient: '带鱼', kcal: 320, price: 6 },
|
||||
{ id: 6, name: '糖醋排骨', emoji: '🍖', category: '荤菜', mainIngredient: '猪肉', kcal: 520, price: 7 },
|
||||
{ id: 7, name: '蒜茸西兰花', emoji: '🥦', category: '素菜', mainIngredient: '西兰花', kcal: 110, price: 3 },
|
||||
{ id: 8, name: '米饭', emoji: '🍚', category: '主食', mainIngredient: '大米', kcal: 260, price: 1.5 },
|
||||
{ id: 9, name: '紫菜蛋花汤', emoji: '🫕', category: '汤品', mainIngredient: '紫菜', kcal: 80, price: 2 },
|
||||
{ id: 10, name: '西红柿炒蛋', emoji: '🫔', category: '荤菜', mainIngredient: '鸡蛋', 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: '红烧肉', accent: '#FFB347' }, // 红烧肉
|
||||
2: { bg: ['#C87030', '#E8A040', '#D08028'], label: '宫保鸡丁', accent: '#4CAF50' }, // 宫保鸡丁
|
||||
3: { bg: ['#706040', '#B89060', '#807050'], label: '麻婆豆腐', accent: '#E57373' }, // 麻婆豆腐
|
||||
4: { bg: ['#306028', '#50A040', '#408030'], label: '清炒时蔬', accent: '#8BC34A' }, // 清炒时蔬
|
||||
5: { bg: ['#805020', '#B88040', '#A06830'], label: '香煎带鱼', accent: '#FF9800' }, // 香煎带鱼
|
||||
6: { bg: ['#802018', '#C05030', '#A03820'], label: '糖醋排骨', accent: '#FF5722' }, // 糖醋排骨
|
||||
7: { bg: ['#207040', '#40A868', '#308848'], label: '蒜茸西兰花', accent: '#66BB6A' }, // 蒜蓉西兰花
|
||||
8: { bg: ['#D0D0C0', '#E8E8D8', '#E0E0C8'], label: '米饭', accent: '#FFF8E1' }, // 米饭
|
||||
9: { bg: ['#503028', '#805040', '#603830'], label: '紫菜蛋花汤', accent: '#CE93D8' }, // 紫菜蛋花汤
|
||||
10: { bg: ['#E8A030', '#F0C040', '#D89020'], label: '西红柿炒蛋', 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('⚠️ 未找到对应餐品,重新识别', 1500);
|
||||
setTimeout(function() { triggerRecognize(); }, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示识别完成状态
|
||||
setRecognizeStatus('ready', '识别完成');
|
||||
|
||||
// 模拟重量变化
|
||||
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('✅ 已自动选中采集餐品:' + dish.name, 2000);
|
||||
}, 800);
|
||||
|
||||
// 不显示候选列表(因为是新采集的菜品,不在AI识别库中)
|
||||
candidateSectionEl.style.display = 'none';
|
||||
searchInputEl.value = dish.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发识别(模拟)
|
||||
*/
|
||||
function triggerRecognize() {
|
||||
if (isRecognizing) return;
|
||||
isRecognizing = true;
|
||||
|
||||
// 更新状态为识别中
|
||||
setRecognizeStatus('detecting', '识别中...');
|
||||
|
||||
// 闪烁效果
|
||||
flashOverlayEl.classList.remove('active');
|
||||
void flashOverlayEl.offsetWidth;
|
||||
flashOverlayEl.classList.add('active');
|
||||
|
||||
// 模拟重量变化
|
||||
simulateWeight();
|
||||
|
||||
// 2秒后出识别结果
|
||||
setTimeout(function() {
|
||||
isRecognizing = false;
|
||||
setRecognizeStatus('ready', '识别完成');
|
||||
|
||||
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 = '智能识别候选';
|
||||
|
||||
// 清空搜索
|
||||
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;">等待餐品放入秤体...</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 + '元/100克</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="dish-card__sim-wrap">' +
|
||||
'<div class="dish-card__sim">' + sim + '%<span class="sim-label">识别率</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
candidateListEl.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示搜索结果(卡片形式)
|
||||
*/
|
||||
function showSearchResults(list) {
|
||||
candidateSectionEl.style.display = '';
|
||||
candidateTitleEl.innerHTML = '搜索到的餐品';
|
||||
|
||||
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;">未找到匹配结果</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 + '元/100克</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="dish-card__sim-wrap">' +
|
||||
'<div class="dish-card__sim--search">手动<span class="sim-label">搜索</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('✅ 已选中:' + dish.name + '(相似度 ' + sim + '%)', 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 = '搜索到的餐品';
|
||||
showSearchResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 重量清零
|
||||
*/
|
||||
function clearWeight() {
|
||||
currentWeight = 0;
|
||||
weightValueEl.textContent = '0';
|
||||
selectedDish = null;
|
||||
pickedDishEl.style.display = 'none';
|
||||
startBtnEl.disabled = true;
|
||||
showCandidates([]);
|
||||
setRecognizeStatus('ready', '等待放入餐品');
|
||||
// 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('🔄 正在重新识别...', 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开餐
|
||||
*/
|
||||
function startMeal() {
|
||||
if (!selectedDish) {
|
||||
showToast('⚠️ 请先选择餐品', 1500);
|
||||
return;
|
||||
}
|
||||
// 自动提交1张图片到采样库
|
||||
submitToSampling(selectedDish);
|
||||
showToast('✅ 开餐成功!' + 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('⚠️ 非餐厅员工,无权操作系统', 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="设置">⚙</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">点击菜品信息 → 进入开餐界面</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 待取餐界面交互逻辑
|
||||
*/
|
||||
|
||||
/* ===== 餐品数据映射(模拟后端) ===== */
|
||||
var DISH_MAP = {
|
||||
1: { name: '红烧肉', price: 5, kcal: 480, colors: ['#8B3A2A', '#D4764E', '#C05030'] },
|
||||
2: { name: '宫保鸡丁', price: 5, kcal: 420, colors: ['#C87030', '#E8A040', '#D08028'] },
|
||||
3: { name: '麻婆豆腐', price: 4, kcal: 280, colors: ['#706040', '#B89060', '#807050'] },
|
||||
4: { name: '清炒时蔬', price: 3, kcal: 120, colors: ['#306028', '#50A040', '#408030'] },
|
||||
5: { name: '香煎带鱼', price: 6, kcal: 320, colors: ['#805020', '#B88040', '#A06830'] },
|
||||
6: { name: '糖醋排骨', price: 7, kcal: 520, colors: ['#802018', '#C05030', '#A03820'] },
|
||||
7: { name: '蒜茸西兰花', price: 3, kcal: 110, colors: ['#207040', '#40A868', '#308848'] },
|
||||
8: { name: '米饭', price: 1.5, kcal: 260, colors: ['#D0D0C0', '#E8E8D8', '#E0E0C8'] },
|
||||
9: { name: '紫菜蛋花汤', price: 2, kcal: 80, colors: ['#503028', '#805040', '#603830'] },
|
||||
10: { name: '西红柿炒蛋', 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('⚠️ 非餐厅员工,无权操作系统', 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">👤</div>
|
||||
<div class="info-card__body">
|
||||
<div class="info-card__name">张三</div>
|
||||
<div class="info-card__id">ID: 98369</div>
|
||||
<div class="info-card__heat">
|
||||
<span class="info-card__heat-label">推荐热量</span>
|
||||
<span class="info-card__heat-value">720</span>
|
||||
<span class="info-card__heat-unit">千卡</span>
|
||||
</div>
|
||||
</div>
|
||||
<a class="settings-btn" id="settingsBtn" title="设置">⚙</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">
|
||||
宫保鸡丁
|
||||
<span class="price-info">(单价 <span class="price-num">5</span> 元/100克)</span>
|
||||
</div>
|
||||
<div class="dish-cal-row">
|
||||
热量 <span class="cal-num">158.66</span> 千卡/100克
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pickup-section">
|
||||
<div class="pickup-section__header">
|
||||
<span class="section-label">🧾 取餐记录</span>
|
||||
</div>
|
||||
<div class="pickup-table">
|
||||
<div class="pickup-header">
|
||||
<span>餐品名称</span>
|
||||
<span>重量(克)</span>
|
||||
<span>小计(元)</span>
|
||||
</div>
|
||||
<div class="pickup-list">
|
||||
<div class="pickup-row">
|
||||
<span>宫保鸡丁</span><span>186</span><span>9.30</span>
|
||||
</div>
|
||||
<div class="pickup-row">
|
||||
<span>大米饭</span><span>200</span><span>3.00</span>
|
||||
</div>
|
||||
<div class="pickup-row">
|
||||
<span>清炒时蔬</span><span>120</span><span>3.60</span>
|
||||
</div>
|
||||
<div class="pickup-row">
|
||||
<span>紫菜蛋花汤</span><span>250</span><span>5.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pickup-footer">
|
||||
<span class="lbl">合计</span>
|
||||
<span class="val">756克</span>
|
||||
<span class="price-val">20.90元</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">📊 本餐次摄入</span>
|
||||
<span>
|
||||
<span class="n-meal">当前餐次:早餐</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">🔥</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">🍚</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">🥗</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">🥩</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">👤</div>
|
||||
<div class="info-card__body">
|
||||
<div class="info-card__name">张三</div>
|
||||
<div class="info-card__id">ID: 98369</div>
|
||||
</div>
|
||||
<div class="info-card__right">
|
||||
<div class="info-card__label">推荐热量</div>
|
||||
<div class="info-card__value">720 <span class="info-card__unit">千卡</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">宫保鸡丁</div>
|
||||
<div class="info-card__dish-detail">单价 <em>5</em> 元/100克 | 热量 <em>158.66</em> 千卡/100克</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 营养标题行 -->
|
||||
<div class="nutrition-header-row">
|
||||
<span class="n-title">📊 本餐次摄入</span>
|
||||
<span>
|
||||
<span class="n-meal">当前餐次:早餐</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">🔥</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">🍚</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">🥗</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">🥩</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">🧾 取餐记录</span>
|
||||
<a class="settings-btn" id="settingsBtn" title="设置">⚙</a>
|
||||
</div>
|
||||
<div class="pickup-table">
|
||||
<div class="pickup-header">
|
||||
<span>餐品名称</span>
|
||||
<span>重量(克)</span>
|
||||
<span>小计(元)</span>
|
||||
</div>
|
||||
<div class="pickup-list">
|
||||
<div class="pickup-row">
|
||||
<span>宫保鸡丁</span>
|
||||
<span>186</span>
|
||||
<span>9.30</span>
|
||||
</div>
|
||||
<div class="pickup-row">
|
||||
<span>大米饭</span>
|
||||
<span>200</span>
|
||||
<span>3.00</span>
|
||||
</div>
|
||||
<div class="pickup-row">
|
||||
<span>清炒时蔬</span>
|
||||
<span>120</span>
|
||||
<span>3.60</span>
|
||||
</div>
|
||||
<div class="pickup-row">
|
||||
<span>紫菜蛋花汤</span>
|
||||
<span>250</span>
|
||||
<span>5.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pickup-footer">
|
||||
<span class="lbl">合计</span>
|
||||
<span class="val">756克</span>
|
||||
<span class="price-val">20.90元</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('⚠️ 非餐厅员工,无权操作系统', 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">◀ 返回</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">🍴</span>
|
||||
<div class="settings-card__info">
|
||||
<div class="settings-card__title">开始开餐</div>
|
||||
<div class="settings-card__desc">进入开餐界面,开始打菜售卖</div>
|
||||
</div>
|
||||
<span class="settings-card__arrow">›</span>
|
||||
</a>
|
||||
|
||||
<!-- 餐品采样卡片 -->
|
||||
<a class="settings-card" href="05-dish-sampling.html">
|
||||
<span class="settings-card__icon">📸</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">📋</span>
|
||||
<div class="settings-card__info">
|
||||
<div class="settings-card__title">采样历史</div>
|
||||
<div class="settings-card__desc">查看/管理已采样餐品的图片记录</div>
|
||||
</div>
|
||||
<span class="settings-card__arrow">›</span>
|
||||
</a>
|
||||
|
||||
<!-- 容器减重卡片(一次性) -->
|
||||
<div class="settings-card" id="openContainerTareModal">
|
||||
<span class="settings-card__icon">⚖️</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">›</span>
|
||||
</div>
|
||||
|
||||
<!-- 勺具减重卡片(多次) -->
|
||||
<div class="settings-card" id="openSpoonTareModal">
|
||||
<span class="settings-card__icon">🥣</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">›</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 容器减重弹窗(一次性) ===== -->
|
||||
<div class="modal-overlay" id="containerTareModal">
|
||||
<div class="modal-container">
|
||||
<div class="modal-title">⚖️ 容器减重</div>
|
||||
|
||||
<div style="font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.6; margin-bottom: 24px;">
|
||||
将盛放食物的容器(食物盆)放在秤上,待重量稳定后点击「确认」。该减重属于一次性设置,后续开餐时系统会自动减去容器皮重。
|
||||
</div>
|
||||
|
||||
<div class="tare-weight-display">
|
||||
<div class="tare-weight-display__label">容器当前重量</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">等待放置容器...</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 24px;">
|
||||
<button class="btn-cancel" onclick="closeContainerTareModal()" style="flex: 1;">取消</button>
|
||||
<button class="btn-tare-confirm" id="containerTareConfirmBtn" onclick="confirmContainerTare()" disabled>确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 勺具减重弹窗(多次) ===== -->
|
||||
<div class="modal-overlay" id="spoonTareModal">
|
||||
<div class="modal-container">
|
||||
<div class="modal-title">🥣 勺具减重</div>
|
||||
|
||||
<div style="font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.6; margin-bottom: 24px;">
|
||||
将取餐勺子放在秤上,待重量稳定后点击「确认」。该减重属于多次减重,每次取餐时勺子拿起放下都会自动扣除勺子重量。
|
||||
</div>
|
||||
|
||||
<div class="tare-weight-display">
|
||||
<div class="tare-weight-display__label">勺子当前重量</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">等待放置勺子...</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 24px;">
|
||||
<button class="btn-cancel" onclick="closeSpoonTareModal()" style="flex: 1;">取消</button>
|
||||
<button class="btn-tare-confirm" id="spoonTareConfirmBtn" onclick="confirmSpoonTare()" disabled>确认</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('⚠️ 请先放置容器并等待重量稳定', 2000);
|
||||
return;
|
||||
}
|
||||
containerTareWeight = Math.round(weight);
|
||||
localStorage.setItem('containerTareWeight', containerTareWeight);
|
||||
currentContainerTareDesc.textContent = containerTareWeight + ' 克';
|
||||
closeContainerTareModal();
|
||||
setTimeout(function() {
|
||||
showToast('✅ 容器减重已设置为 ' + containerTareWeight + ' 克', 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('⚠️ 请先放置勺子并等待重量稳定', 2000);
|
||||
return;
|
||||
}
|
||||
spoonTareWeight = Math.round(weight);
|
||||
localStorage.setItem('spoonTareWeight', spoonTareWeight);
|
||||
currentSpoonTareDesc.textContent = spoonTareWeight + ' 克';
|
||||
closeSpoonTareModal();
|
||||
setTimeout(function() {
|
||||
showToast('✅ 勺具减重已设置为 ' + spoonTareWeight + ' 克', 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()">◀ 返回</a>
|
||||
<span class="top-bar__title">餐品采样</span>
|
||||
<a class="back-btn" href="06-sampling-history.html" style="margin-left:auto;">📝 查看采样历史</a>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- ========== 左侧:餐品选择区 ========== -->
|
||||
<div class="dish-select-area">
|
||||
<div class="area-title">
|
||||
<span>🍕 餐品选择</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="搜索餐品名称...">
|
||||
</div>
|
||||
|
||||
<!-- 类目Tab -->
|
||||
<div class="filter-tabs" id="catTabs">
|
||||
<span class="filter-tab active" data-cat="all">全部</span>
|
||||
<span class="filter-tab" data-cat="荤菜">荤菜</span>
|
||||
<span class="filter-tab" data-cat="素菜">素菜</span>
|
||||
<span class="filter-tab" data-cat="主食">主食</span>
|
||||
<span class="filter-tab" data-cat="汤品">汤品</span>
|
||||
<span class="filter-tab" data-cat="饮品">饮品</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">🍳</div>
|
||||
<div>
|
||||
<div class="selected-dish-info__name" id="selectedName">请选择一个餐品</div>
|
||||
<div class="selected-dish-info__desc" id="selectedDesc">点击左侧列表中的餐品进行采样</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 摄像头区域 -->
|
||||
<div class="camera-frame-wrap">
|
||||
<div class="camera-frame" id="cameraFrame">
|
||||
<span class="camera-frame__text" id="cameraText">摄像头预览区域</span>
|
||||
<img class="camera-frame__preview" id="cameraPreview" alt="预览">
|
||||
<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">📸<br>拍照采集</button>
|
||||
</div>
|
||||
|
||||
<!-- 采集结果 -->
|
||||
<div class="result-section">
|
||||
<div class="result-section__title">
|
||||
<span>📋 采集结果</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">取消</button>
|
||||
<button class="btn-resample" id="btnResample">↻ 重新采样</button>
|
||||
<button class="btn-save" id="btnSave">保存采集</button>
|
||||
<button class="btn-save-continue" id="btnSaveContinue">保存并继续</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 餐品采样界面交互逻辑
|
||||
*/
|
||||
|
||||
/* ===== 餐品数据 ===== */
|
||||
var DISH_DATA = [
|
||||
{ id: 1, name: '红烧肉', emoji: '🍳', meal: 'lunch', catLabel: '荤菜', mainIngredient: '猪肉', kcal: 480 },
|
||||
{ id: 2, name: '清炒时蔬', emoji: '🥬', meal: 'lunch', catLabel: '素菜', mainIngredient: '菜花', kcal: 120 },
|
||||
{ id: 3, name: '麻婆豆腐', emoji: '🧆', meal: 'lunch', catLabel: '荤菜', mainIngredient: '豆腐', kcal: 280 },
|
||||
{ id: 4, name: '糖醋排骨', emoji: '🍖', meal: 'lunch', catLabel: '荤菜', mainIngredient: '猪肉', kcal: 520 },
|
||||
{ id: 5, name: '香煎带鱼', emoji: '🐟', meal: 'dinner', catLabel: '荤菜', mainIngredient: '带鱼', kcal: 320 },
|
||||
{ id: 6, name: '蒜茸西兰花', emoji: '🥦', meal: 'dinner', catLabel: '素菜', mainIngredient: '西兰花', kcal: 110 },
|
||||
{ id: 7, name: '西红柿炒蛋', emoji: '🫔', meal: 'breakfast', catLabel: '荤菜', mainIngredient: '鸡蛋', kcal: 180 },
|
||||
{ id: 8, name: '紫菜蛋花汤', emoji: '🫕', meal: 'breakfast', catLabel: '汤品', mainIngredient: '紫菜', kcal: 80 },
|
||||
{ id: 9, name: '宫保鸡丁', emoji: '🍗', meal: 'lunch', catLabel: '荤菜', mainIngredient: '鸡肉', kcal: 420 },
|
||||
{ id: 10, name: '米饭', emoji: '🍚', meal: 'lunch', catLabel: '主食', mainIngredient: '大米', kcal: 260 },
|
||||
{ id: 11, name: '慢炖鸡汤', emoji: '🥘', meal: 'dinner', catLabel: '汤品', mainIngredient: '土鸡', kcal: 150 },
|
||||
{ id: 12, name: '手撕包菜', emoji: '🥬', meal: 'dinner', catLabel: '素菜', mainIngredient: '包菜', kcal: 95 },
|
||||
{ id: 13, name: '豆浆', emoji: '🥛', meal: 'breakfast', catLabel: '饮品', mainIngredient: '黄豆', kcal: 60 },
|
||||
{ id: 14, name: '馒头', emoji: '🥮', meal: 'breakfast', catLabel: '主食', mainIngredient: '小麦', 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('✅ 已自动选中:' + selectedDish.name + ',可直接追加采集', 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">🔍</div><div class="empty-state__text">未找到匹配的餐品</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">✔</span>' +
|
||||
'</div>' +
|
||||
'<button class="remove-btn" data-index="' + i + '">✖</button>' +
|
||||
'</div>';
|
||||
} else if (i === capturedImages.length) {
|
||||
// 下一个可添加的位置
|
||||
html += '<div class="result-cell" data-index="' + i + '">+添加</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('✅ 已选中:' + 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('⚠️ 请先选择一个餐品', 1500);
|
||||
return;
|
||||
}
|
||||
if (capturedImages.length >= MAX_IMAGES) {
|
||||
showToast('⚠️ 已达采集上限(9张)', 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('📸 采集成功!已采集 ' + capturedImages.length + '/' + MAX_IMAGES + ' 张', 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('🗑️ 已删除采集图片', 1200);
|
||||
return;
|
||||
}
|
||||
// 添加按钮
|
||||
var cell = e.target.closest('.result-cell');
|
||||
if (!cell || cell.classList.contains('disabled')) return;
|
||||
if (!selectedDish) {
|
||||
showToast('⚠️ 请先选择一个餐品', 1500);
|
||||
return;
|
||||
}
|
||||
if (capturedImages.length >= MAX_IMAGES) {
|
||||
showToast('⚠️ 已达采集上限', 1500);
|
||||
return;
|
||||
}
|
||||
// 触发拍照
|
||||
btnPhotoEl.click();
|
||||
});
|
||||
|
||||
/* --- 保存按钮(提交后跳转至待取餐界面) --- */
|
||||
document.getElementById('btnSave').addEventListener('click', function() {
|
||||
if (!selectedDish) {
|
||||
showToast('⚠️ 请先选择一个餐品', 1500);
|
||||
return;
|
||||
}
|
||||
if (capturedImages.length === 0) {
|
||||
showToast('⚠️ 请至少采集1张图片', 1500);
|
||||
return;
|
||||
}
|
||||
showToast('✅ 采集数据已保存,跳转至待取餐页面...', 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('⚠️ 请先选择一个餐品', 1500);
|
||||
return;
|
||||
}
|
||||
if (capturedImages.length === 0) {
|
||||
showToast('⚠️ 请至少采集1张图片', 1500);
|
||||
return;
|
||||
}
|
||||
showToast('✅ 采集数据已保存,请继续采集下一个餐品', 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('无需取消,未进行任何操作', 1500);
|
||||
return;
|
||||
}
|
||||
selectedDish = null;
|
||||
capturedImages = [];
|
||||
showToast('↻ 已取消当前采集', 1000);
|
||||
// 取消后回到待取餐界面
|
||||
setTimeout(function() {
|
||||
window.location.href = '02-dish-display.html';
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
/* --- 重新采样按钮 --- */
|
||||
document.getElementById('btnResample').addEventListener('click', function() {
|
||||
if (capturedImages.length === 0 && !selectedDish) {
|
||||
showToast('⚠️ 当前无采样数据', 1500);
|
||||
return;
|
||||
}
|
||||
selectedDish = null;
|
||||
capturedImages = [];
|
||||
cameraPreviewEl.style.display = 'none';
|
||||
cameraTextEl.style.display = '';
|
||||
renderDishList();
|
||||
renderResultGrid();
|
||||
updateSelectedDishInfo();
|
||||
showToast('↻ 已清空,请重新选择餐品开始采样', 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()">◀ 返回</a>
|
||||
<span class="top-bar__title">采样历史</span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<div class="main-content">
|
||||
|
||||
<!-- 左侧:快捷查找面板 -->
|
||||
<div class="search-panel">
|
||||
<div class="search-panel__title">🔍 快捷查找</div>
|
||||
|
||||
<!-- 餐品名称搜索 -->
|
||||
<input type="text" class="search-panel__input" id="searchNameInput" placeholder="输入餐品名称...">
|
||||
|
||||
<!-- 类目筛选 -->
|
||||
<div>
|
||||
<div class="search-panel__title" style="margin-bottom:8px;">📁 按类目筛选</div>
|
||||
<div class="cat-tabs" id="catTabs">
|
||||
<span class="cat-tab active" data-cat="all">全部</span>
|
||||
<span class="cat-tab" data-cat="荤菜">荤菜</span>
|
||||
<span class="cat-tab" data-cat="素菜">素菜</span>
|
||||
<span class="cat-tab" data-cat="主食">主食</span>
|
||||
<span class="cat-tab" data-cat="汤品">汤品</span>
|
||||
<span class="cat-tab" data-cat="饮品">饮品</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="search-panel__stats">
|
||||
共 <em id="totalCount">8</em> 条记录
|
||||
</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()">✖ 关闭</button>
|
||||
<button class="btn-resample" id="btnResample" onclick="resampleDish()">📸 补充采集</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 采样历史页面交互逻辑
|
||||
*/
|
||||
|
||||
/* ===== 模拟采样历史数据 ===== */
|
||||
var SAMPLING_HISTORY = [
|
||||
{ id: 1, dishId: 1, name: '红烧肉', emoji: '🍳', catLabel: '荤菜', mainIngredient: '猪肉', kcal: 480, count: 6, time: '2026-06-10 08:32', images: ['🍳','🍳','🍳','🍳','🍳','🍳'] },
|
||||
{ id: 2, dishId: 10, name: '米饭', emoji: '🍚', catLabel: '主食', mainIngredient: '大米', kcal: 260, count: 3, time: '2026-06-10 08:28', images: ['🍚','🍚','🍚'] },
|
||||
{ id: 3, dishId: 7, name: '西红柿炒蛋', emoji: '🫔', catLabel: '荤菜', mainIngredient: '鸡蛋', kcal: 180, count: 9, time: '2026-06-10 08:15', images: ['🫔','🫔','🫔','🫔','🫔','🫔','🫔','🫔','🫔'] },
|
||||
{ id: 4, dishId: 13, name: '豆浆', emoji: '🥛', catLabel: '饮品', mainIngredient: '黄豆', kcal: 60, count: 4, time: '2026-06-10 08:05', images: ['🥛','🥛','🥛','🥛'] },
|
||||
{ id: 5, dishId: 9, name: '宫保鸡丁', emoji: '🍗', catLabel: '荤菜', mainIngredient: '鸡肉', kcal: 420, count: 5, time: '2026-06-09 12:20', images: ['🍗','🍗','🍗','🍗','🍗'] },
|
||||
{ id: 6, dishId: 2, name: '清炒时蔬', emoji: '🥬', catLabel: '素菜', mainIngredient: '菜花', kcal: 120, count: 7, time: '2026-06-09 12:05', images: ['🥬','🥬','🥬','🥬','🥬','🥬','🥬'] },
|
||||
{ id: 7, dishId: 5, name: '香煎带鱼', emoji: '🐟', catLabel: '荤菜', mainIngredient: '带鱼', kcal: 320, count: 8, time: '2026-06-08 18:30', images: ['🐟','🐟','🐟','🐟','🐟','🐟','🐟','🐟'] },
|
||||
{ id: 8, dishId: 11, name: '慢炖鸡汤', emoji: '🥘', catLabel: '汤品', mainIngredient: '土鸡', kcal: 150, count: 3, time: '2026-06-08 18:10', images: ['🥘','🥘','🥘'] }
|
||||
];
|
||||
|
||||
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);">未找到匹配的采样记录</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">已采 ' + record.count + ' 张</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">张</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 + '">✖</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('✅ 删除成功', 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<string> | 是 | 已拍摄的图片数据(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">◀ 返回</a>
|
||||
<span class="top-bar__title">餐线营养秤</span>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<a class="card" href="01-meal-start.html">
|
||||
<span class="card__icon">🍽️</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">开餐界面</div>
|
||||
<div class="card__desc">开餐操作、今日菜品概览与就餐引导</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="02-dish-display.html">
|
||||
<span class="card__icon">📋</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">餐品展示界面</div>
|
||||
<div class="card__desc">菜品详情、营养成分与热量展示</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="03-meal-pickup.html">
|
||||
<span class="card__icon">🧾</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">取餐界面(单屏)</div>
|
||||
<div class="card__desc">取餐确认、订单生成与营养报告</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="03-meal-pickup-dual-large.html">
|
||||
<span class="card__icon">🖥️</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">取餐界面(双屏·大屏 1280×720)</div>
|
||||
<div class="card__desc">大屏取餐信息展示、用户信息与取餐记录</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="03-meal-pickup-dual-small.html">
|
||||
<span class="card__icon">📊</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">取餐界面(双屏·小屏 480×320)</div>
|
||||
<div class="card__desc">小屏营养摄入柱状图与推荐热量展示</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="04-settings.html">
|
||||
<span class="card__icon">⚙️</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">设置界面</div>
|
||||
<div class="card__desc">环境切换、设备参数配置</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="05-dish-sampling.html">
|
||||
<span class="card__icon">📸</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">餐品采样界面</div>
|
||||
<div class="card__desc">菜品拍照采集、营养数据录入与 AI 识别匹配</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
<a class="card" href="06-sampling-history.html">
|
||||
<span class="card__icon">📝</span>
|
||||
<div class="card__info">
|
||||
<div class="card__title">采样历史</div>
|
||||
<div class="card__desc">历史采样记录查看、采集图片管理</div>
|
||||
</div>
|
||||
<span class="card__arrow">›</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user