Files
kitchen/web_order/index.html
2026-04-17 07:00:26 +08:00

826 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🍽️ 点餐助手 - 小妈厨房</title>
<style>
:root {
--primary: #007AFF;
--primary-light: rgba(0,122,255,0.1);
--green: #34C759;
--green-light: rgba(52,199,89,0.1);
--orange: #FF9500;
--orange-light: rgba(255,149,0,0.1);
--red: #FF3B30;
--red-light: rgba(255,59,48,0.1);
--bg: #F2F2F7;
--card: rgba(255,255,255,0.72);
--text: #1C1C1E;
--text2: #3C3C43;
--text3: #8E8E93;
--border: rgba(60,60,67,0.08);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--shadow: 0 2px 12px rgba(0,0,0,0.06);
--font: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #000000;
--card: rgba(44,44,46,0.72);
--text: #FFFFFF;
--text2: #EBEBF5;
--text3: #8E8E93;
--border: rgba(84,84,88,0.65);
--shadow: 0 2px 12px rgba(0,0,0,0.3);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 16px;
}
.header {
text-align: center;
padding: 24px 0 16px;
}
.header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
}
.header .subtitle {
font-size: 14px;
color: var(--text3);
margin-top: 4px;
}
.card {
background: var(--card);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: var(--radius-lg);
border: 0.5px solid var(--border);
padding: 16px;
margin-bottom: 12px;
box-shadow: var(--shadow);
animation: fadeIn 0.3s ease-out;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
}
.status-draft { background: var(--orange-light); color: var(--orange); }
.status-active { background: var(--green-light); color: var(--green); }
.status-completed { background: var(--primary-light); color: var(--primary); }
.status-cancelled { background: var(--red-light); color: var(--red); }
.order-no {
font-size: 12px;
color: var(--text3);
font-family: 'SF Mono', Menlo, monospace;
}
.order-time {
font-size: 12px;
color: var(--text3);
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.info-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: rgba(142,142,147,0.06);
border-radius: var(--radius-sm);
font-size: 14px;
}
.info-chip-icon {
font-size: 16px;
}
.info-chip-label {
font-size: 11px;
color: var(--text3);
}
.info-chip-value {
font-weight: 600;
font-size: 14px;
}
.item-list { list-style: none; }
.item-row {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 0.5px solid var(--border);
}
.item-row:last-child { border-bottom: none; }
.item-source {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background: var(--primary-light);
color: var(--primary);
margin-right: 8px;
white-space: nowrap;
}
.item-name {
flex: 1;
font-size: 15px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-qty {
font-size: 14px;
color: var(--text2);
margin: 0 8px;
min-width: 30px;
text-align: center;
}
.item-price {
font-size: 15px;
font-weight: 600;
color: var(--primary);
}
.item-detail {
font-size: 12px;
color: var(--text3);
margin-top: 2px;
padding-left: 0;
}
.total-section {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--border);
margin-top: 8px;
}
.total-label {
font-size: 15px;
color: var(--text2);
}
.total-amount {
font-size: 22px;
font-weight: 700;
color: var(--primary);
}
.note-section {
margin-top: 8px;
padding: 10px;
background: rgba(142,142,147,0.06);
border-radius: var(--radius-sm);
}
.note-label {
font-size: 12px;
color: var(--text3);
margin-bottom: 4px;
}
.note-text {
font-size: 14px;
color: var(--text2);
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 56px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: var(--text3);
}
.action-bar {
display: flex;
gap: 8px;
margin-top: 12px;
}
.action-btn {
flex: 1;
padding: 14px;
border: none;
border-radius: var(--radius-md);
font-size: 15px;
font-weight: 600;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.action-btn:active { opacity: 0.8; }
.action-btn-primary {
background: var(--primary);
color: white;
}
.action-btn-secondary {
background: var(--primary-light);
color: var(--primary);
}
.action-btn-green {
background: var(--green);
color: white;
}
.meta-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text3);
margin-top: 8px;
}
.debug-panel {
margin-top: 16px;
padding: 12px;
background: rgba(142,142,147,0.06);
border-radius: var(--radius-md);
font-family: 'SF Mono', Menlo, monospace;
font-size: 11px;
color: var(--text3);
word-break: break-all;
}
.debug-panel summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 8px;
}
.sse-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text3);
margin-top: 4px;
}
.sse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
animation: pulse 2s infinite;
}
.sse-dot.disconnected { background: var(--red); animation: none; }
.screenshot-area {
background: var(--bg);
padding: 16px;
border-radius: var(--radius-lg);
margin-bottom: 12px;
}
.screenshot-area .card {
margin-bottom: 8px;
}
.screenshot-area .card:last-child {
margin-bottom: 0;
}
.divider {
height: 0.5px;
background: var(--border);
margin: 8px 0;
}
.desc-text {
font-size: 13px;
color: var(--text3);
line-height: 1.5;
margin-top: 4px;
}
.people-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: var(--radius-sm);
background: var(--green-light);
color: var(--green);
font-size: 13px;
font-weight: 600;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: rgba(0,0,0,0.75);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-size: 14px;
opacity: 0;
transition: all 0.3s ease;
z-index: 9999;
pointer-events: none;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
@media print {
.action-bar, .debug-panel, .sse-indicator, .refresh-btn { display: none !important; }
body { background: white; }
.card { box-shadow: none; border: 1px solid #eee; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍽️ 点餐助手</h1>
<div class="subtitle" id="subtitle">加载中...</div>
<div class="sse-indicator">
<span class="sse-dot" id="sseDot"></span>
<span id="sseStatus">连接中...</span>
</div>
</div>
<div id="screenshotArea" class="screenshot-area">
<div id="content">
<div class="empty-state">
<div class="empty-icon"></div>
<div class="empty-text">正在加载点单信息...</div>
</div>
</div>
</div>
<div class="action-bar">
<button class="action-btn action-btn-secondary" onclick="loadOrder()">🔄 刷新</button>
<button class="action-btn action-btn-primary" onclick="screenshotOrder()">📸 截图保存</button>
<button class="action-btn action-btn-green" onclick="shareOrder()">📤 分享</button>
</div>
<details class="debug-panel">
<summary>🔧 调试信息</summary>
<div id="debug-info">等待数据...</div>
</details>
</div>
<div class="toast" id="toast"></div>
<script>
var API_BASE = 'https://eat.wktyl.com/api/kitchen/kitchen.php';
var SSE_URL = 'https://eat.wktyl.com/api/kitchen/kitchen_sse.php';
var eventSource = null;
var pollTimer = null;
var currentOrder = null;
function showToast(msg) {
var t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(function() { t.classList.remove('show'); }, 2000);
}
function getQueryParam(name) {
var url = new URL(window.location.href);
return url.searchParams.get(name);
}
function formatTime(isoStr) {
if (!isoStr) return '';
try {
var d = new Date(isoStr);
var y = d.getFullYear();
var m = String(d.getMonth()+1).padStart(2,'0');
var day = String(d.getDate()).padStart(2,'0');
var h = String(d.getHours()).padStart(2,'0');
var min = String(d.getMinutes()).padStart(2,'0');
return y + '-' + m + '-' + day + ' ' + h + ':' + min;
} catch(e) { return isoStr; }
}
function formatRelativeTime(isoStr) {
if (!isoStr) return '';
try {
var d = new Date(isoStr);
var now = new Date();
var diff = now - d;
var mins = Math.floor(diff / 60000);
if (mins < 1) return '刚刚';
if (mins < 60) return mins + '分钟前';
var hours = Math.floor(mins / 60);
if (hours < 24) return hours + '小时前';
var days = Math.floor(hours / 24);
if (days === 1) return '昨天';
if (days < 7) return days + '天前';
return (d.getMonth()+1) + '月' + d.getDate() + '日';
} catch(e) { return isoStr; }
}
function renderOrder(order) {
currentOrder = order;
var statusMap = {
0: { label: '草稿', icon: '📝', cls: 'status-draft' },
1: { label: '进行中', icon: '🟢', cls: 'status-active' },
2: { label: '已完成', icon: '✅', cls: 'status-completed' },
3: { label: '已取消', icon: '❌', cls: 'status-cancelled' },
};
var typeMap = { 0: '🧑 用户点餐', 1: '🏪 商家推单' };
var sourceMap = {
0: { label: '浏览', icon: '📖' },
1: { label: '搜索', icon: '🔍' },
2: { label: '手动', icon: '✏️' },
3: { label: '推荐', icon: '⭐' },
};
var st = statusMap[order.status] || statusMap[0];
var items = order.items || [];
var total = items.reduce(function(s, i) { return s + (i.price || 0) * i.quantity; }, 0);
var totalQty = items.reduce(function(s, i) { return s + i.quantity; }, 0);
document.getElementById('subtitle').textContent =
typeMap[order.type] + ' · ' + formatRelativeTime(order.createdAt);
var html = '';
// 订单状态卡片
html += '<div class="card">';
html += '<div class="order-header">';
html += '<div class="order-header-left">';
html += '<span class="status-badge ' + st.cls + '">' + st.icon + ' ' + st.label + '</span>';
html += '<span class="order-no">' + escapeHtml(order.orderNo) + '</span>';
html += '</div>';
html += '<span class="order-time">' + formatTime(order.createdAt) + '</span>';
html += '</div>';
// 信息网格:桌号 + 人数
html += '<div class="info-grid">';
if (order.tableNo) {
html += '<div class="info-chip">';
html += '<span class="info-chip-icon">🪑</span>';
html += '<div><div class="info-chip-label">桌号</div>';
html += '<div class="info-chip-value">' + escapeHtml(order.tableNo) + '</div></div>';
html += '</div>';
}
if (order.peopleCount) {
html += '<div class="info-chip">';
html += '<span class="info-chip-icon">👥</span>';
html += '<div><div class="info-chip-label">用餐人数</div>';
html += '<div class="info-chip-value">' + order.peopleCount + ' 人</div></div>';
html += '</div>';
}
if (!order.tableNo && !order.peopleCount) {
html += '<div class="info-chip">';
html += '<span class="info-chip-icon">📋</span>';
html += '<div><div class="info-chip-label">记录</div>';
html += '<div class="info-chip-value">#' + (order.recordCount || 0) + '</div></div>';
html += '</div>';
}
if (order.tableNo && !order.peopleCount) {
html += '<div class="info-chip">';
html += '<span class="info-chip-icon">📋</span>';
html += '<div><div class="info-chip-label">记录</div>';
html += '<div class="info-chip-value">#' + (order.recordCount || 0) + '</div></div>';
html += '</div>';
}
html += '</div>';
html += '</div>';
// 菜品列表
if (items.length === 0) {
html += '<div class="card"><div class="empty-state">';
html += '<div class="empty-icon">📭</div>';
html += '<div class="empty-text">暂无菜品</div>';
html += '</div></div>';
} else {
html += '<div class="card">';
html += '<div style="font-size:15px;font-weight:600;margin-bottom:8px;">🥘 菜品明细</div>';
html += '<ul class="item-list">';
items.forEach(function(item, idx) {
var src = sourceMap[item.source] || sourceMap[2];
html += '<li class="item-row">';
html += '<span class="item-source">' + src.icon + ' ' + src.label + '</span>';
html += '<span class="item-name">' + escapeHtml(item.name) + '</span>';
html += '<span class="item-qty">×' + item.quantity + '</span>';
html += '<span class="item-price">' + (item.price != null ? '¥' + (item.price * item.quantity).toFixed(1) : '-') + '</span>';
html += '</li>';
if (item.ingredients || item.note) {
html += '<li class="item-detail">';
if (item.ingredients) html += '🥘 ' + escapeHtml(item.ingredients) + ' ';
if (item.note) html += '💬 ' + escapeHtml(item.note);
html += '</li>';
}
});
html += '</ul>';
html += '<div class="total-section">';
html += '<span class="total-label">共 ' + totalQty + ' 道菜' + (order.peopleCount ? ' · ' + order.peopleCount + '人用餐' : '') + '</span>';
html += '<span class="total-amount">¥' + total.toFixed(1) + '</span>';
html += '</div>';
html += '</div>';
}
// 备注
if (order.note) {
html += '<div class="card"><div class="note-section">';
html += '<div class="note-label">💬 备注</div>';
html += '<div class="note-text">' + escapeHtml(order.note) + '</div>';
html += '</div></div>';
}
// 底部描述
html += '<div class="card">';
html += '<div style="font-size:13px;color:var(--text3);line-height:1.6;">';
html += '📱 由「小妈厨房」点餐助手生成<br>';
html += '🕐 下单时间: ' + formatTime(order.createdAt);
if (order.updatedAt && order.updatedAt !== order.createdAt) {
html += '<br>🔄 更新时间: ' + formatTime(order.updatedAt);
}
html += '</div>';
html += '</div>';
document.getElementById('content').innerHTML = html;
document.getElementById('debug-info').textContent = JSON.stringify(order, null, 2);
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function screenshotOrder() {
showToast('📸 请使用系统截图功能保存');
// 尝试使用 Web Share API
if (navigator.share && currentOrder) {
var items = currentOrder.items || [];
var total = items.reduce(function(s, i) { return s + (i.price || 0) * i.quantity; }, 0);
var text = '🍽️ 点餐助手 - ' + (currentOrder.orderNo || '') + '\n';
if (currentOrder.tableNo) text += '🪑 桌号: ' + currentOrder.tableNo + '\n';
if (currentOrder.peopleCount) text += '👥 人数: ' + currentOrder.peopleCount + '\n';
text += '\n';
items.forEach(function(item) {
text += '· ' + item.name + ' ×' + item.quantity;
if (item.price) text += ' ¥' + (item.price * item.quantity).toFixed(1);
text += '\n';
});
text += '\n合计: ¥' + total.toFixed(1) + '\n';
if (currentOrder.note) text += '备注: ' + currentOrder.note + '\n';
navigator.share({
title: '点餐助手 - ' + currentOrder.orderNo,
text: text,
}).catch(function() {});
return;
}
// Fallback: 尝试打印
window.print();
}
function shareOrder() {
if (navigator.share && currentOrder) {
var items = currentOrder.items || [];
var total = items.reduce(function(s, i) { return s + (i.price || 0) * i.quantity; }, 0);
var text = '🍽️ 点餐助手 - ' + (currentOrder.orderNo || '') + '\n';
if (currentOrder.tableNo) text += '🪑 桌号: ' + currentOrder.tableNo + '\n';
if (currentOrder.peopleCount) text += '👥 人数: ' + currentOrder.peopleCount + '\n';
text += '\n';
items.forEach(function(item) {
text += '· ' + item.name + ' ×' + item.quantity;
if (item.price) text += ' ¥' + (item.price * item.quantity).toFixed(1);
text += '\n';
});
text += '\n合计: ¥' + total.toFixed(1) + '\n';
if (currentOrder.note) text += '备注: ' + currentOrder.note + '\n';
navigator.share({
title: '点餐助手 - ' + currentOrder.orderNo,
text: text,
url: window.location.href,
}).catch(function() {});
} else {
// Fallback: 复制链接
navigator.clipboard.writeText(window.location.href).then(function() {
showToast('✅ 链接已复制到剪贴板');
}).catch(function() {
showToast('❌ 复制失败,请手动复制链接');
});
}
}
async function loadOrder() {
var orderId = getQueryParam('id');
if (!orderId) {
document.getElementById('content').innerHTML =
'<div class="card"><div class="empty-state">' +
'<div class="empty-icon">🔍</div>' +
'<div class="empty-text">缺少订单ID参数<br><small>请从App端扫码打开</small></div>' +
'</div></div>';
document.getElementById('debug-info').textContent = 'URL: ' + window.location.href + '\n缺少 id 参数';
return;
}
document.getElementById('subtitle').textContent = '加载中...';
try {
var resp = await fetch(API_BASE + '?act=get&id=' + encodeURIComponent(orderId));
if (resp.ok) {
var result = await resp.json();
if (result.code === 200 && result.data) {
renderOrder(result.data);
} else {
throw new Error(result.message || '订单不存在');
}
} else {
throw new Error('HTTP ' + resp.status);
}
} catch(e) {
document.getElementById('debug-info').textContent = 'API请求失败: ' + e.message + '\n尝试本地参数...';
tryLocalFallback(orderId);
}
}
function tryLocalFallback(orderId) {
var dataStr = getQueryParam('data');
if (dataStr) {
try {
var order = JSON.parse(decodeURIComponent(dataStr));
renderOrder(order);
return;
} catch(e2) {
document.getElementById('debug-info').textContent += '\n本地参数解析失败: ' + e2.message;
}
}
var demoOrder = {
id: orderId,
orderNo: 'OD_DEMO_' + Date.now().toString().slice(-6),
type: 0,
status: 1,
items: [
{ id: '1', name: '红烧肉', source: 0, quantity: 1, price: 38, ingredients: '五花肉、酱油、冰糖', note: null },
{ id: '2', name: '番茄炒蛋', source: 2, quantity: 2, price: 18, ingredients: null, note: '少盐' },
{ id: '3', name: '紫菜蛋花汤', source: 3, quantity: 1, price: 12, ingredients: null, note: null },
],
note: '三人用餐',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdBy: 'demo',
tableNo: 'A3',
peopleCount: 3,
recordCount: 1,
};
renderOrder(demoOrder);
document.getElementById('debug-info').textContent += '\n使用Demo数据展示';
}
function initSSE() {
var orderId = getQueryParam('id');
if (!orderId) return;
try {
eventSource = new EventSource(SSE_URL + '?order_id=' + encodeURIComponent(orderId));
eventSource.addEventListener('connected', function(e) {
updateSSEStatus(true, 'SSE已连接');
});
eventSource.addEventListener('order_update', function(e) {
try {
var order = JSON.parse(e.data);
renderOrder(order);
} catch(err) {
console.error('SSE数据解析失败:', err);
}
});
eventSource.addEventListener('order_deleted', function(e) {
document.getElementById('content').innerHTML =
'<div class="card"><div class="empty-state">' +
'<div class="empty-icon">🗑️</div>' +
'<div class="empty-text">订单已被删除</div>' +
'</div></div>';
});
eventSource.addEventListener('heartbeat', function(e) {
updateSSEStatus(true, 'SSE连接中');
});
eventSource.addEventListener('close', function(e) {
updateSSEStatus(false, 'SSE超时重连中...');
setTimeout(initSSE, 3000);
});
eventSource.onerror = function() {
updateSSEStatus(false, 'SSE断开');
eventSource.close();
eventSource = null;
setTimeout(initSSE, 5000);
};
} catch(e) {
console.error('SSE初始化失败:', e);
updateSSEStatus(false, 'SSE不可用');
}
}
function updateSSEStatus(connected, text) {
var dot = document.getElementById('sseDot');
var status = document.getElementById('sseStatus');
if (dot) dot.className = 'sse-dot' + (connected ? '' : ' disconnected');
if (status) status.textContent = text;
}
loadOrder();
initSSE();
pollTimer = setInterval(function() {
if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
loadOrder();
}
}, 30000);
</script>
</body>
</html>