Files
web_template/web/dist/food1/index.html
2026-07-10 10:07:06 +00:00

1074 lines
58 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{
--orange:#FF6B35;
--orange-dark:#F0521B;
--orange-light:#FF8455;
--yellow:#FFD23F;
--yellow-light:#FFE57A;
--orange-soft:#FFF1EA;
--bg:#F7F8FA;
--white:#FFFFFF;
--text:#1F2329;
--text-sub:#646A73;
--text-light:#8F959E;
--border:#EFF0F1;
--shadow-sm:0 2px 8px rgba(255,107,53,.06);
--shadow:0 6px 20px rgba(255,107,53,.10);
--shadow-lg:0 12px 40px rgba(255,107,53,.16);
--radius:16px;
--radius-sm:10px;
--radius-lg:24px;
--maxw:1200px;
--grad-orange:linear-gradient(135deg,#FF6B35 0%,#FF9A3D 100%);
--grad-yellow:linear-gradient(135deg,#FFD23F 0%,#FFB323 100%);
--grad-hero:linear-gradient(135deg,#FF6B35 0%,#FF9A3D 45%,#FFD23F 100%);
--grad-cart:linear-gradient(135deg,#FF6B35 0%,#FF8455 100%);
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei","Helvetica Neue",Arial,sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;-webkit-font-smoothing:antialiased}
img{display:block;max-width:100%}
button{font-family:inherit;cursor:pointer;border:none;background:none;outline:none}
input{font-family:inherit;outline:none;border:none}
a{color:inherit;text-decoration:none}
ul{list-style:none}
/* ========== 通用 ========== */
.container{max-width:var(--maxw);margin:0 auto;padding:0 20px}
.hidden{display:none!important}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:#ddd;border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:#ccc}
/* ========== 顶部导航栏 ========== */
.navbar{position:sticky;top:0;z-index:100;background:var(--white);box-shadow:0 1px 0 rgba(0,0,0,.04)}
.navbar-inner{display:flex;align-items:center;gap:20px;height:64px}
.logo{display:flex;align-items:center;gap:8px;flex-shrink:0;cursor:pointer}
.logo-icon{width:38px;height:38px;border-radius:11px;background:var(--grad-orange);display:flex;align-items:center;justify-content:center;box-shadow:0 4px 10px rgba(255,107,53,.35)}
.logo-icon svg{width:22px;height:22px;fill:#fff}
.logo-text{font-size:20px;font-weight:800;color:var(--orange);letter-spacing:.5px}
.nav-search{flex:1;max-width:480px;position:relative}
.nav-search input{width:100%;height:40px;padding:0 44px 0 18px;border-radius:20px;background:var(--orange-soft);font-size:14px;color:var(--text);transition:.2s}
.nav-search input::placeholder{color:#C9A99A}
.nav-search input:focus{background:#FFE9DF;box-shadow:0 0 0 3px rgba(255,107,53,.12)}
.nav-search .s-icon{position:absolute;right:14px;top:50%;transform:translateY(-50%);width:18px;height:18px;color:var(--orange);pointer-events:none}
.nav-right{display:flex;align-items:center;gap:18px;margin-left:auto}
.nav-loc{display:flex;align-items:center;gap:5px;color:var(--text-sub);font-size:13px;font-weight:600}
.nav-loc svg{width:15px;height:15px;color:var(--orange);fill:var(--orange)}
.nav-cart{position:relative;display:flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:12px;background:var(--orange-soft);cursor:pointer;transition:.2s}
.nav-cart:hover{background:#FFE0D2}
.nav-cart svg{width:22px;height:22px;color:var(--orange)}
.cart-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background:var(--orange);color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;border:2px solid #fff;line-height:1}
.cart-badge.show{display:flex}
.cart-badge:not(.show){display:none}
/* ========== Hero Banner ========== */
.hero{background:var(--grad-hero);position:relative;overflow:hidden;padding:48px 0 56px}
.hero::before{content:"";position:absolute;top:-60px;right:-40px;width:280px;height:280px;border-radius:50%;background:rgba(255,255,255,.12)}
.hero::after{content:"";position:absolute;bottom:-80px;left:-50px;width:220px;height:220px;border-radius:50%;background:rgba(255,255,255,.10)}
.hero-inner{position:relative;z-index:2;text-align:center}
.hero h1{color:#fff;font-size:38px;font-weight:800;letter-spacing:1px;text-shadow:0 2px 8px rgba(0,0,0,.12);margin-bottom:8px}
.hero p{color:rgba(255,255,255,.92);font-size:16px;margin-bottom:24px;font-weight:500}
.hero-search{max-width:560px;margin:0 auto;display:flex;background:#fff;border-radius:28px;padding:6px;box-shadow:0 10px 30px rgba(0,0,0,.15)}
.hero-search input{flex:1;height:44px;padding:0 20px;font-size:15px;color:var(--text)}
.hero-search input::placeholder{color:#bbb}
.hero-search button{height:44px;padding:0 30px;border-radius:24px;background:var(--grad-orange);color:#fff;font-size:15px;font-weight:700;transition:.2s;box-shadow:0 4px 12px rgba(255,107,53,.4)}
.hero-search button:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(255,107,53,.5)}
.hero-tags{display:flex;justify-content:center;gap:10px;margin-top:18px;flex-wrap:wrap}
.hero-tag{padding:5px 14px;border-radius:14px;background:rgba(255,255,255,.22);color:#fff;font-size:12px;font-weight:600;backdrop-filter:blur(4px);cursor:pointer;transition:.2s}
.hero-tag:hover{background:rgba(255,255,255,.35)}
/* ========== 分类网格 ========== */
.section{padding:36px 0}
.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.section-title{font-size:22px;font-weight:800;color:var(--text);display:flex;align-items:center;gap:8px}
.section-title::before{content:"";width:5px;height:22px;border-radius:3px;background:var(--grad-orange)}
.section-sub{font-size:13px;color:var(--text-light)}
.cat-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:16px}
.cat-item{display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:var(--radius);cursor:pointer;transition:.25s;box-shadow:var(--shadow-sm);border:2px solid transparent}
.cat-item:hover{transform:translateY(-4px);box-shadow:var(--shadow);border-color:var(--orange-soft)}
.cat-item.active{border-color:var(--orange);background:var(--orange-soft)}
.cat-icon{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 14px rgba(0,0,0,.12);transition:.25s}
.cat-item:hover .cat-icon{transform:scale(1.08) rotate(-5deg)}
.cat-icon svg{width:30px;height:30px;fill:#fff}
.cat-name{font-size:14px;font-weight:700;color:var(--text)}
.cat-count{font-size:11px;color:var(--text-light)}
/* ========== 推荐餐厅横向滚动 ========== */
.featured-scroll{display:flex;gap:16px;overflow-x:auto;padding-bottom:8px;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}
.featured-scroll::-webkit-scrollbar{height:4px}
.feature-card{min-width:280px;max-width:280px;background:#fff;border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm);cursor:pointer;transition:.25s;scroll-snap-align:start;display:flex;flex-direction:column}
.feature-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
.feature-cover{height:130px;position:relative;display:flex;align-items:center;justify-content:center}
.feature-cover svg{width:54px;height:54px;fill:rgba(255,255,255,.9);filter:drop-shadow(0 2px 6px rgba(0,0,0,.15))}
.feature-rank{position:absolute;top:10px;left:10px;width:26px;height:26px;border-radius:8px;background:rgba(0,0,0,.45);color:#fff;font-size:12px;font-weight:800;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
.feature-rank.top1{background:var(--grad-yellow);color:#8B5A00}
.feature-rank.top2{background:linear-gradient(135deg,#C0C0C0,#E8E8E8);color:#555}
.feature-rank.top3{background:linear-gradient(135deg,#CD7F32,#E8A87C);color:#fff}
.feature-body{padding:14px;flex:1;display:flex;flex-direction:column}
.feature-name{font-size:15px;font-weight:700;color:var(--text);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.feature-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px;min-height:20px}
.feature-tag{font-size:10px;padding:2px 7px;border-radius:6px;background:var(--orange-soft);color:var(--orange);font-weight:600}
.feature-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-sub);margin-top:auto}
.feature-meta .star{color:var(--yellow);font-weight:700}
.feature-meta .dot{color:#ddd}
/* ========== 餐厅列表 ========== */
.list-toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;flex-wrap:wrap;gap:12px}
.sort-tabs{display:flex;gap:6px;background:#fff;padding:5px;border-radius:12px;box-shadow:var(--shadow-sm)}
.sort-tab{padding:7px 16px;border-radius:8px;font-size:13px;font-weight:600;color:var(--text-sub);transition:.2s;white-space:nowrap}
.sort-tab.active{background:var(--grad-orange);color:#fff;box-shadow:0 3px 8px rgba(255,107,53,.35)}
.sort-tab:not(.active):hover{background:var(--orange-soft);color:var(--orange)}
.result-info{font-size:13px;color:var(--text-light)}
.result-info b{color:var(--orange)}
.rest-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}
.rest-card{display:flex;gap:14px;background:#fff;border-radius:var(--radius);padding:14px;box-shadow:var(--shadow-sm);cursor:pointer;transition:.25s;position:relative;overflow:hidden}
.rest-card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--grad-orange);transform:scaleY(0);transform-origin:top;transition:.25s}
.rest-card:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg)}
.rest-card:hover::before{transform:scaleY(1)}
.rest-logo{width:92px;height:92px;border-radius:14px;flex-shrink:0;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.rest-logo svg{width:44px;height:44px;fill:rgba(255,255,255,.92);filter:drop-shadow(0 2px 4px rgba(0,0,0,.15))}
.rest-logo .status-badge{position:absolute;bottom:0;left:0;right:0;text-align:center;font-size:10px;color:#fff;padding:2px;background:rgba(0,0,0,.35);font-weight:600}
.rest-logo .status-badge.closed{background:rgba(120,120,120,.7)}
.rest-info{flex:1;min-width:0;display:flex;flex-direction:column}
.rest-name{font-size:16px;font-weight:700;color:var(--text);margin-bottom:4px;display:flex;align-items:center;gap:6px}
.rest-name .status-dot{width:7px;height:7px;border-radius:50%;background:#34C759;flex-shrink:0}
.rest-name .status-dot.closed{background:#999}
.rest-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:6px}
.rest-tag{font-size:10px;padding:2px 7px;border-radius:6px;background:var(--orange-soft);color:var(--orange);font-weight:600}
.rest-rate{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-sub);margin-bottom:6px}
.rest-rate .star{color:var(--yellow);font-weight:700;font-size:13px}
.rest-rate .sales{color:var(--text-light)}
.rest-meta{display:flex;align-items:center;gap:0;font-size:11px;color:var(--text-sub);margin-top:auto;flex-wrap:wrap}
.rest-meta span{display:flex;align-items:center;gap:3px;padding-right:8px;margin-right:8px;border-right:1px solid var(--border)}
.rest-meta span:last-child{border-right:none}
.rest-meta .free{color:var(--orange);font-weight:700}
/* ========== 骨架屏 ========== */
.skeleton-card{display:flex;gap:14px;background:#fff;border-radius:var(--radius);padding:14px;box-shadow:var(--shadow-sm)}
.sk-block{background:linear-gradient(90deg,#f0f0f0 25%,#e6e6e6 37%,#f0f0f0 63%);background-size:400% 100%;animation:sk 1.4s ease infinite;border-radius:8px}
@keyframes sk{0%{background-position:100% 50%}100%{background-position:0 50%}}
.sk-logo{width:92px;height:92px;border-radius:14px;flex-shrink:0}
.sk-info{flex:1;display:flex;flex-direction:column;gap:8px;padding-top:4px}
.sk-line{height:14px;border-radius:7px}
.sk-line.w60{width:60%}
.sk-line.w40{width:40%}
.sk-line.w80{width:80%}
.sk-cat{width:56px;height:56px;border-radius:50%}
.cat-grid.skeleton{display:grid;grid-template-columns:repeat(6,1fr);gap:16px}
.cat-sk{display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:var(--radius);box-shadow:var(--shadow-sm)}
.featured-sk{display:flex;gap:16px;overflow:hidden}
.featured-sk .sk-block{min-width:280px;height:230px;border-radius:var(--radius)}
/* ========== 餐厅详情 ========== */
.detail-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:200;opacity:0;pointer-events:none;transition:.3s;backdrop-filter:blur(2px)}
.detail-overlay.show{opacity:1;pointer-events:auto}
.detail-panel{position:fixed;top:0;right:0;bottom:0;width:100%;max-width:960px;background:var(--bg);z-index:201;transform:translateX(100%);transition:.35s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;box-shadow:-8px 0 40px rgba(0,0,0,.2)}
.detail-panel.show{transform:translateX(0)}
.detail-header{position:relative;height:200px;display:flex;align-items:flex-end;padding:20px;color:#fff;overflow:hidden;flex-shrink:0}
.detail-header-bg{position:absolute;inset:0;z-index:0}
.detail-header-bg::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.1) 0%,rgba(0,0,0,.45) 100%)}
.detail-header-content{position:relative;z-index:2;width:100%}
.detail-close{position:absolute;top:16px;right:16px;z-index:5;width:36px;height:36px;border-radius:50%;background:rgba(0,0,0,.35);color:#fff;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(6px);transition:.2s}
.detail-close:hover{background:rgba(0,0,0,.55);transform:rotate(90deg)}
.detail-close svg{width:18px;height:18px}
.detail-name{font-size:24px;font-weight:800;margin-bottom:6px;text-shadow:0 2px 6px rgba(0,0,0,.3)}
.detail-tags{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
.detail-tag{font-size:11px;padding:3px 9px;border-radius:6px;background:rgba(255,255,255,.25);backdrop-filter:blur(4px);font-weight:600}
.detail-notice{font-size:12px;color:rgba(255,255,255,.9);display:flex;align-items:center;gap:5px;margin-top:4px}
.detail-notice svg{width:14px;height:14px;fill:#fff;flex-shrink:0}
.detail-info-row{display:flex;gap:0;background:#fff;padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0}
.detail-info-item{flex:1;text-align:center;border-right:1px solid var(--border)}
.detail-info-item:last-child{border-right:none}
.detail-info-item .v{font-size:18px;font-weight:800;color:var(--orange)}
.detail-info-item .v.yellow{color:#F5A623}
.detail-info-item .l{font-size:11px;color:var(--text-light);margin-top:2px}
.detail-body{flex:1;display:flex;overflow:hidden;min-height:0}
.menu-sidebar{width:100px;background:#F2F3F5;overflow-y:auto;flex-shrink:0;-webkit-overflow-scrolling:touch}
.menu-cat-item{padding:16px 8px;text-align:center;font-size:13px;color:var(--text-sub);cursor:pointer;position:relative;transition:.2s;font-weight:600;line-height:1.4}
.menu-cat-item.active{background:#fff;color:var(--orange);font-weight:700}
.menu-cat-item.active::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:18px;border-radius:0 2px 2px 0;background:var(--orange)}
.menu-cat-item:not(.active):hover{background:rgba(255,255,255,.4)}
.menu-list{flex:1;overflow-y:auto;padding:12px 16px 100px;-webkit-overflow-scrolling:touch}
.menu-section{margin-bottom:8px}
.menu-section-title{font-size:15px;font-weight:800;color:var(--text);padding:12px 4px 10px;position:sticky;top:0;background:var(--bg);z-index:2}
.menu-item{display:flex;gap:12px;background:#fff;border-radius:var(--radius);padding:12px;margin-bottom:10px;box-shadow:var(--shadow-sm);transition:.2s}
.menu-item:hover{box-shadow:var(--shadow)}
.menu-item-img{width:88px;height:88px;border-radius:12px;flex-shrink:0;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.menu-item-img svg{width:40px;height:40px;fill:rgba(255,255,255,.92);filter:drop-shadow(0 2px 4px rgba(0,0,0,.15))}
.menu-item-img .discount{position:absolute;top:0;left:0;background:var(--orange);color:#fff;font-size:9px;padding:2px 6px;font-weight:700;border-radius:0 0 8px 0}
.menu-item-info{flex:1;min-width:0;display:flex;flex-direction:column}
.menu-item-name{font-size:15px;font-weight:700;color:var(--text);margin-bottom:3px}
.menu-item-desc{font-size:12px;color:var(--text-light);margin-bottom:5px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.menu-item-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:5px}
.menu-item-tag{font-size:10px;padding:1px 6px;border-radius:5px;background:var(--orange-soft);color:var(--orange);font-weight:600}
.menu-item-bottom{display:flex;align-items:center;justify-content:space-between;margin-top:auto}
.menu-item-meta{display:flex;flex-direction:column;gap:2px}
.menu-item-price{display:flex;align-items:baseline;gap:5px}
.menu-item-price .now{font-size:18px;font-weight:800;color:var(--orange)}
.menu-item-price .now small{font-size:12px}
.menu-item-price .old{font-size:12px;color:var(--text-light);text-decoration:line-through}
.menu-item-stat{font-size:11px;color:var(--text-light)}
.menu-item-stat .star{color:var(--yellow);font-weight:700}
.add-btn{width:32px;height:32px;border-radius:50%;background:var(--grad-orange);color:#fff;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 8px rgba(255,107,53,.4);transition:.2s;flex-shrink:0}
.add-btn:hover{transform:scale(1.1) rotate(90deg)}
.add-btn svg{width:18px;height:18px;stroke:#fff;stroke-width:2.5;fill:none}
.qty-control{display:flex;align-items:center;gap:8px;flex-shrink:0}
.qty-control button{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;transition:.2s}
.qty-control .minus{background:#fff;border:1.5px solid var(--orange);color:var(--orange)}
.qty-control .minus:hover{background:var(--orange-soft)}
.qty-control .plus{background:var(--grad-orange);color:#fff;box-shadow:0 2px 6px rgba(255,107,53,.4)}
.qty-control .num{font-size:15px;font-weight:700;color:var(--text);min-width:18px;text-align:center}
/* ========== 购物车浮层 ========== */
.cart-bar{position:fixed;left:0;right:0;bottom:0;z-index:150;pointer-events:none}
.cart-bar-inner{max-width:var(--maxw);margin:0 auto;padding:0 20px;pointer-events:none}
.cart-bar-box{background:#2A2A2E;border-radius:30px;display:flex;align-items:center;height:60px;box-shadow:0 -4px 24px rgba(0,0,0,.2);pointer-events:auto;overflow:hidden;transition:.3s;position:relative}
.cart-bar-box.empty{background:#3A3A3E}
.cart-icon-wrap{width:60px;height:60px;flex-shrink:0;display:flex;align-items:center;justify-content:center;position:relative;margin-left:8px}
.cart-icon-circle{width:48px;height:48px;border-radius:50%;background:#5A5A5E;display:flex;align-items:center;justify-content:center;transition:.25s;cursor:pointer}
.cart-icon-circle.active{background:var(--grad-orange);box-shadow:0 4px 14px rgba(255,107,53,.5)}
.cart-icon-circle svg{width:24px;height:24px;color:#fff}
.cart-icon-circle.active svg{animation:bounce .4s}
@keyframes bounce{0%,100%{transform:scale(1)}50%{transform:scale(1.25)}}
.cart-total{flex:1;color:#fff;padding-left:14px;display:flex;flex-direction:column;justify-content:center}
.cart-total.empty .t{color:#999}
.cart-total .t{font-size:17px;font-weight:800}
.cart-total .t small{font-size:12px;font-weight:500}
.cart-total .s{font-size:11px;color:#bbb}
.cart-checkout{height:60px;padding:0 28px;background:var(--grad-yellow);color:#8B5A00;font-size:16px;font-weight:800;display:flex;align-items:center;cursor:pointer;transition:.2s;flex-shrink:0}
.cart-checkout:hover{filter:brightness(1.05)}
.cart-checkout.disabled{background:#5A5A5E;color:#888;cursor:not-allowed}
.cart-bar .badge{position:absolute;top:2px;right:2px;min-width:20px;height:20px;padding:0 5px;border-radius:10px;background:var(--orange);color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;border:2px solid #2A2A2E;line-height:1}
.cart-bar .badge:not(.show){display:none}
/* 购物车详情弹出 */
.cart-detail{position:fixed;left:0;right:0;bottom:60px;z-index:149;background:#fff;border-radius:20px 20px 0 0;box-shadow:0 -8px 30px rgba(0,0,0,.15);transform:translateY(120%);transition:.3s;max-height:60vh;display:flex;flex-direction:column}
.cart-detail.show{transform:translateY(0)}
.cart-detail-mask{position:fixed;inset:0;top:-60vh;background:rgba(0,0,0,.4);z-index:148;opacity:0;pointer-events:none;transition:.3s}
.cart-detail-mask.show{opacity:1;pointer-events:auto}
.cart-detail-head{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);flex-shrink:0}
.cart-detail-title{font-size:15px;font-weight:700}
.cart-clear{font-size:13px;color:var(--text-light);display:flex;align-items:center;gap:4px;cursor:pointer}
.cart-clear svg{width:14px;height:14px}
.cart-detail-list{overflow-y:auto;padding:8px 20px 16px;flex:1}
.cart-detail-item{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--border)}
.cart-detail-item:last-child{border-bottom:none}
.cart-detail-item .name{flex:1;font-size:14px;font-weight:600}
.cart-detail-item .price{font-size:14px;font-weight:800;color:var(--orange);margin-right:8px}
.cart-detail-empty{text-align:center;padding:40px 0;color:var(--text-light)}
.cart-detail-empty svg{width:60px;height:60px;fill:#ddd;margin-bottom:10px}
/* ========== Toast ========== */
.toast-wrap{position:fixed;top:80px;left:50%;transform:translateX(-50%);z-index:999;display:flex;flex-direction:column;gap:8px;pointer-events:none}
.toast{background:rgba(0,0,0,.82);color:#fff;padding:10px 20px;border-radius:24px;font-size:14px;font-weight:500;box-shadow:0 6px 20px rgba(0,0,0,.25);display:flex;align-items:center;gap:8px;animation:toastIn .3s;backdrop-filter:blur(6px)}
.toast.out{animation:toastOut .3s forwards}
.toast svg{width:16px;height:16px}
.toast.success svg{fill:#4CAF50}
.toast.warn svg{fill:var(--yellow)}
@keyframes toastIn{from{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:translateY(0)}}
@keyframes toastOut{to{opacity:0;transform:translateY(-12px)}}
/* ========== 页脚 ========== */
.footer{background:linear-gradient(135deg,#FF6B35 0%,#F0521B 100%);color:#fff;padding:40px 0 24px;margin-top:40px}
.footer-inner{text-align:center}
.footer-logo{font-size:22px;font-weight:800;margin-bottom:8px;display:inline-flex;align-items:center;gap:8px}
.footer-logo svg{width:24px;height:24px;fill:#fff}
.footer-links{display:flex;justify-content:center;gap:24px;margin:16px 0;font-size:13px;opacity:.92}
.footer-links a{cursor:pointer;transition:.2s}
.footer-links a:hover{opacity:.7;text-decoration:underline}
.footer-copy{font-size:12px;opacity:.75;border-top:1px solid rgba(255,255,255,.2);padding-top:16px;margin-top:8px}
/* ========== 空状态 ========== */
.empty-state{text-align:center;padding:60px 20px;color:var(--text-light)}
.empty-state svg{width:80px;height:80px;fill:#E0E0E0;margin-bottom:14px}
.empty-state p{font-size:15px;margin-bottom:4px}
.empty-state .sub{font-size:13px;color:var(--text-light)}
/* ========== 加载更多 ========== */
.load-more{text-align:center;padding:20px}
.load-more button{padding:10px 32px;border-radius:24px;background:#fff;color:var(--orange);font-size:14px;font-weight:600;box-shadow:var(--shadow-sm);transition:.2s;border:1px solid var(--orange-soft)}
.load-more button:hover{background:var(--orange-soft)}
.load-more button:disabled{color:var(--text-light);cursor:default}
/* ========== 响应式 ========== */
@media(max-width:900px){
.rest-grid{grid-template-columns:1fr}
.cat-grid{grid-template-columns:repeat(4,1fr)}
}
@media(max-width:768px){
.hero h1{font-size:28px}
.hero p{font-size:14px}
.navbar-inner{gap:10px}
.nav-loc{display:none}
.logo-text{font-size:18px}
.section{padding:28px 0}
.section-title{font-size:19px}
.container{padding:0 14px}
.detail-panel{max-width:100%}
.menu-sidebar{width:84px}
.menu-item-img{width:76px;height:76px}
.rest-logo{width:80px;height:80px}
.rest-logo svg{width:38px;height:38px}
}
@media(max-width:560px){
.cat-grid{grid-template-columns:repeat(3,1fr);gap:10px}
.hero{padding:32px 0 40px}
.hero-search button{padding:0 20px;font-size:14px}
.feature-card{min-width:240px;max-width:240px}
.rest-card{padding:12px;gap:10px}
.rest-name{font-size:15px}
.detail-header{height:170px}
.detail-name{font-size:20px}
.nav-search{max-width:none}
.sort-tabs{overflow-x:auto;-webkit-overflow-scrolling:touch}
}
</style>
</head>
<body>
<!-- ========== 顶部导航栏 ========== -->
<header class="navbar">
<div class="container navbar-inner">
<div class="logo" onclick="goHome()">
<div class="logo-icon">
<svg viewBox="0 0 24 24"><path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z"/></svg>
</div>
<span class="logo-text">美味达</span>
</div>
<div class="nav-search">
<input id="navSearchInput" type="text" placeholder="搜索餐厅或美食" onkeydown="if(event.key==='Enter')doSearch(this.value)">
<svg class="s-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<div class="nav-right">
<div class="nav-loc">
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 0 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
<span>当前位置</span>
</div>
<div class="nav-cart" onclick="scrollToCart()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>
<span id="navCartBadge" class="cart-badge">0</span>
</div>
</div>
</div>
</header>
<!-- ========== Hero Banner ========== -->
<section class="hero">
<div class="container hero-inner">
<h1>美味触手可及</h1>
<p>周边好店 · 极速配送 · 优惠不停</p>
<div class="hero-search">
<input id="heroSearchInput" type="text" placeholder="想吃什么?搜搜看吧" onkeydown="if(event.key==='Enter')doSearch(this.value)">
<button onclick="doSearch(document.getElementById('heroSearchInput').value)">搜索</button>
</div>
<div class="hero-tags" id="heroTags"></div>
</div>
</section>
<!-- ========== 分类网格 ========== -->
<section class="section">
<div class="container">
<div class="section-head">
<div class="section-title">餐厅分类</div>
<div class="section-sub">选择分类,发现好店</div>
</div>
<div id="catGrid" class="cat-grid">
<div class="cat-grid skeleton">
<div class="cat-sk" style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:16px;box-shadow:0 2px 8px rgba(255,107,53,.06)"><div class="sk-block sk-cat"></div><div class="sk-block" style="width:40px;height:12px"></div></div>
<div class="cat-sk" style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:16px;box-shadow:0 2px 8px rgba(255,107,53,.06)"><div class="sk-block sk-cat"></div><div class="sk-block" style="width:40px;height:12px"></div></div>
<div class="cat-sk" style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:16px;box-shadow:0 2px 8px rgba(255,107,53,.06)"><div class="sk-block sk-cat"></div><div class="sk-block" style="width:40px;height:12px"></div></div>
<div class="cat-sk" style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:16px;box-shadow:0 2px 8px rgba(255,107,53,.06)"><div class="sk-block sk-cat"></div><div class="sk-block" style="width:40px;height:12px"></div></div>
<div class="cat-sk" style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:16px;box-shadow:0 2px 8px rgba(255,107,53,.06)"><div class="sk-block sk-cat"></div><div class="sk-block" style="width:40px;height:12px"></div></div>
<div class="cat-sk" style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:18px 8px;background:#fff;border-radius:16px;box-shadow:0 2px 8px rgba(255,107,53,.06)"><div class="sk-block sk-cat"></div><div class="sk-block" style="width:40px;height:12px"></div></div>
</div>
</div>
</div>
</section>
<!-- ========== 推荐餐厅 ========== -->
<section class="section" style="padding-top:0">
<div class="container">
<div class="section-head">
<div class="section-title">为你推荐</div>
<div class="section-sub">人气好店,不容错过</div>
</div>
<div id="featuredScroll" class="featured-scroll">
<div class="featured-sk">
<div class="sk-block"></div><div class="sk-block"></div><div class="sk-block"></div><div class="sk-block"></div>
</div>
</div>
</div>
</section>
<!-- ========== 餐厅列表 ========== -->
<section class="section" style="padding-top:0" id="restSection">
<div class="container">
<div class="section-head">
<div class="section-title" id="listTitle">全部餐厅</div>
</div>
<div class="list-toolbar">
<div class="sort-tabs" id="sortTabs">
<div class="sort-tab active" data-sort="">默认排序</div>
<div class="sort-tab" data-sort="rating">评分最高</div>
<div class="sort-tab" data-sort="sales">销量最高</div>
<div class="sort-tab" data-sort="delivery_time">配送最快</div>
</div>
<div class="result-info">共 <b id="resultTotal">0</b> 家餐厅</div>
</div>
<div id="restGrid" class="rest-grid"></div>
<div id="loadMore" class="load-more hidden">
<button id="loadMoreBtn" onclick="loadMoreRestaurants()">加载更多</button>
</div>
</div>
</section>
<!-- ========== 餐厅详情 ========== -->
<div class="detail-overlay" id="detailOverlay" onclick="closeDetail()"></div>
<div class="detail-panel" id="detailPanel">
<div class="detail-header" id="detailHeader">
<div class="detail-header-bg" id="detailHeaderBg"></div>
<button class="detail-close" onclick="closeDetail()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
<div class="detail-header-content" id="detailHeaderContent"></div>
</div>
<div class="detail-info-row" id="detailInfoRow"></div>
<div class="detail-body" id="detailBody">
<div class="menu-sidebar" id="menuSidebar"></div>
<div class="menu-list" id="menuList"></div>
</div>
</div>
<!-- ========== 购物车详情 ========== -->
<div class="cart-detail-mask" id="cartDetailMask" onclick="toggleCartDetail(false)"></div>
<div class="cart-detail" id="cartDetail">
<div class="cart-detail-head">
<span class="cart-detail-title" id="cartDetailTitle">购物车</span>
<span class="cart-clear" onclick="clearCart()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
清空
</span>
</div>
<div class="cart-detail-list" id="cartDetailList"></div>
</div>
<!-- ========== 购物车浮层 ========== -->
<div class="cart-bar" id="cartBar" style="display:none">
<div class="cart-bar-inner">
<div class="cart-bar-box empty" id="cartBarBox">
<div class="cart-icon-wrap">
<div class="cart-icon-circle" id="cartIconCircle" onclick="toggleCartDetail()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>
</div>
<span class="badge" id="cartBadge">0</span>
</div>
<div class="cart-total empty" id="cartTotal">
<span class="t">¥0</span>
<span class="s" id="cartSub">配送费¥0 · 另需约¥0</span>
</div>
<div class="cart-checkout disabled" id="checkoutBtn" onclick="checkout()">去结算</div>
</div>
</div>
</div>
<!-- ========== Toast ========== -->
<div class="toast-wrap" id="toastWrap"></div>
<!-- ========== 页脚 ========== -->
<footer class="footer">
<div class="container footer-inner">
<div class="footer-logo">
<svg viewBox="0 0 24 24"><path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z"/></svg>
美味达
</div>
<div class="footer-links">
<a onclick="goHome()">首页</a>
<a onclick="toast('订单功能演示中','warn')">我的订单</a>
<a onclick="toast('商家入驻功能演示中','warn')">商家入驻</a>
<a onclick="toast('客服中心演示中','warn')">联系客服</a>
</div>
<div class="footer-copy">© 2026 美味达外卖平台 · 美味触手可及 · 本页面为演示模板</div>
</div>
</footer>
<script>
/* ==========================================================
数据与状态
========================================================== */
const API = '/api/v1/food';
const state = {
categories: [],
featured: [],
restaurants: [],
total: 0,
page: 1,
pageSize: 12,
currentCategory: 0, // 0 = 全部
keyword: '',
sort: '',
loading: false,
// 详情
currentRestaurant: null,
menuData: null,
// 购物车: { restaurantId, restaurantName, deliveryFee, minOrder, items: {itemId: {item, qty}} }
cart: null,
};
/* 分类配色与图标配置(icon 字段映射) */
const CAT_STYLES = {
chinese: {grad:'linear-gradient(135deg,#FF6B35,#FF9A3D)', svg:'<path d="M3 3v18h18V3H3zm9 14a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/>'},
japanese: {grad:'linear-gradient(135deg,#FF8FA3,#FF6B9D)', svg:'<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><path d="M12 6v12"/>'},
western: {grad:'linear-gradient(135deg,#FFD23F,#FFB323)', svg:'<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7z"/>'},
hotpot: {grad:'linear-gradient(135deg,#FF4757,#FF6B35)', svg:'<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/>'},
dessert: {grad:'linear-gradient(135deg,#FF6B9D,#FFD23F)', svg:'<path d="M12 3 10.7 8.3 5.3 9.6l4 3.9-.9 5.4 4.6-2.6 4.6 2.6-.9-5.4 4-3.9-5.4-1.3z"/>'},
healthy: {grad:'linear-gradient(135deg,#5BB318,#7CC576)', svg:'<path d="M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-8 2.25-13 3.25S2 11.5 2 13.5s1.75 3.75 1.75 3.75C4 8 17 8 17 8z"/>'},
};
const DEFAULT_CAT_STYLE = {grad:'linear-gradient(135deg,#FF6B35,#FFD23F)', svg:'<circle cx="12" cy="12" r="9"/>'};
/* 餐厅 logo 渐变色组(按 id 取模分配) */
const REST_GRADIENTS = [
'linear-gradient(135deg,#FF6B35,#FF9A3D)',
'linear-gradient(135deg,#FF4757,#FF6B35)',
'linear-gradient(135deg,#FFD23F,#FF9A3D)',
'linear-gradient(135deg,#FF8FA3,#FF6B9D)',
'linear-gradient(135deg,#5BB318,#7CC576)',
'linear-gradient(135deg,#FF9A3D,#FFD23F)',
'linear-gradient(135deg,#FF6B9D,#FFD23F)',
'linear-gradient(135deg,#F0521B,#FF8455)',
];
const REST_SVGS = [
'<path d="M3 3v18h18V3H3zm9 14a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/>',
'<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z"/>',
'<path d="M12 3 10.7 8.3 5.3 9.6l4 3.9-.9 5.4 4.6-2.6 4.6 2.6-.9-5.4 4-3.9-5.4-1.3z"/>',
'<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 0 1 0-5 2.5 2.5 0 0 1 0 5z"/>',
'<path d="M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-8 2.25-13 3.25S2 11.5 2 13.5s1.75 3.75 1.75 3.75C4 8 17 8 17 8z"/>',
'<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><path d="M12 6v12"/>',
'<path d="M21 11H3v8h18v-8zm-2-7H5v5h14V4z"/>',
'<path d="M20 8h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.2 2.2A5.99 5.99 0 0 0 12 5c-.47 0-.93.06-1.39.15L8.41 3 7 4.41l1.62 1.63A5.99 5.99 0 0 0 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z"/>',
];
/* 菜品图片渐变色组 */
const ITEM_GRADIENTS = [
'linear-gradient(135deg,#FF6B35,#FF9A3D)',
'linear-gradient(135deg,#FFD23F,#FFB323)',
'linear-gradient(135deg,#FF8FA3,#FF6B9D)',
'linear-gradient(135deg,#5BB318,#7CC576)',
'linear-gradient(135deg,#FF4757,#FF6B35)',
'linear-gradient(135deg,#FF9A3D,#FFD23F)',
];
const ITEM_SVGS = [
'<path d="M3 3v18h18V3H3zm9 14a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/>',
'<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7z"/>',
'<path d="M12 3 10.7 8.3 5.3 9.6l4 3.9-.9 5.4 4.6-2.6 4.6 2.6-.9-5.4 4-3.9-5.4-1.3z"/>',
'<path d="M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-8 2.25-13 3.25S2 11.5 2 13.5s1.75 3.75 1.75 3.75C4 8 17 8 17 8z"/>',
'<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 0 1 0-5 2.5 2.5 0 0 1 0 5z"/>',
'<path d="M20 8h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.2 2.2A5.99 5.99 0 0 0 12 5c-.47 0-.93.06-1.39.15L8.41 3 7 4.41l1.62 1.63A5.99 5.99 0 0 0 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z"/>',
];
/* ==========================================================
工具函数
========================================================== */
function $(id){return document.getElementById(id)}
function esc(s){return (s==null?'':String(s)).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function parseTags(t){try{return JSON.parse(t||'[]')}catch(e){return []}}
function safeNum(n,d=0){n=Number(n);return isNaN(n)?d:n}
function money(n){return safeNum(n).toFixed(1).replace(/\.0$/,'')}
function catStyle(icon){return CAT_STYLES[icon]||DEFAULT_CAT_STYLE}
function restGrad(id){return REST_GRADIENTS[(id-1)%REST_GRADIENTS.length]}
function restSvg(id){return REST_SVGS[(id-1)%REST_SVGS.length]}
function itemGrad(id){return ITEM_GRADIENTS[(id-1)%ITEM_GRADIENTS.length]}
function itemSvg(id){return ITEM_SVGS[(id-1)%ITEM_SVGS.length]}
/* Toast */
function toast(msg,type){
const wrap=$('toastWrap');
const el=document.createElement('div');
el.className='toast '+(type||'');
const icons={success:'<svg viewBox="0 0 24 24"><path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>',warn:'<svg viewBox="0 0 24 24"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>'};
el.innerHTML=(icons[type]||'')+'<span>'+esc(msg)+'</span>';
wrap.appendChild(el);
setTimeout(()=>{el.classList.add('out');setTimeout(()=>el.remove(),300)},2000);
}
/* ==========================================================
API 请求
========================================================== */
async function api(path){
try{
const res=await fetch(API+path);
if(!res.ok) throw new Error('HTTP '+res.status);
const json=await res.json();
if(json.code!==0) throw new Error(json.message||'接口错误');
return json.data;
}catch(e){
console.error('API error:',path,e);
throw e;
}
}
/* ==========================================================
初始化
========================================================== */
async function init(){
try{
const home=await api('/home');
state.categories=home.categories||[];
state.featured=home.featured||[];
renderHeroTags();
renderCategories();
renderFeatured();
await loadRestaurants(true);
}catch(e){
toast('数据加载失败,请检查服务','warn');
$('catGrid').innerHTML='<div class="empty-state" style="grid-column:1/-1"><p>数据加载失败</p><p class="sub">请确保后端服务已启动</p></div>';
}
}
/* Hero 热门标签 */
function renderHeroTags(){
const hot=['麻辣','川菜','奶茶','汉堡','烤肉','沙拉'];
$('heroTags').innerHTML=hot.map(t=>`<span class="hero-tag" onclick="doSearch('${t}')">${t}</span>`).join('');
}
/* 分类网格 */
function renderCategories(){
const html=state.categories.map(c=>{
const s=catStyle(c.icon);
return `<div class="cat-item ${state.currentCategory===c.id?'active':''}" onclick="selectCategory(${c.id})">
<div class="cat-icon" style="background:${s.grad}"><svg viewBox="0 0 24 24">${s.svg}</svg></div>
<span class="cat-name">${esc(c.name)}</span>
</div>`;
}).join('');
$('catGrid').innerHTML=html;
}
/* 推荐餐厅横向滚动 */
function renderFeatured(){
if(!state.featured.length){$('featuredScroll').innerHTML='';return}
const html=state.featured.map((r,i)=>{
const tags=parseTags(r.tags).slice(0,2);
return `<div class="feature-card" onclick="openDetail(${r.id})">
<div class="feature-cover" style="background:${restGrad(r.id)}">
<div class="feature-rank ${i<3?'top'+(i+1):''}">${i+1}</div>
<svg viewBox="0 0 24 24">${restSvg(r.id)}</svg>
</div>
<div class="feature-body">
<div class="feature-name">${esc(r.name)}</div>
<div class="feature-tags">${tags.map(t=>`<span class="feature-tag">${esc(t)}</span>`).join('')}</div>
<div class="feature-meta">
<span class="star">★ ${money(r.rating)}</span>
<span class="dot">·</span>
<span>月售${r.sales}</span>
<span class="dot">·</span>
<span>${r.delivery_time}分钟</span>
</div>
</div>
</div>`;
}).join('');
$('featuredScroll').innerHTML=html;
}
/* ==========================================================
餐厅列表
========================================================== */
function buildQuery(){
const p=new URLSearchParams();
if(state.currentCategory) p.set('category_id',state.currentCategory);
if(state.keyword) p.set('keyword',state.keyword);
if(state.sort) p.set('sort',state.sort);
p.set('page',state.page);
p.set('page_size',state.pageSize);
return p.toString();
}
async function loadRestaurants(reset){
if(state.loading) return;
state.loading=true;
if(reset){
state.page=1;
state.restaurants=[];
$('restGrid').innerHTML=renderSkeleton(4);
}
try{
const data=await api('/restaurants?'+buildQuery());
const list=data.list||[];
if(reset){state.restaurants=list}else{state.restaurants=state.restaurants.concat(list)}
state.total=safeNum(data.total,0);
renderRestaurantList();
}catch(e){
$('restGrid').innerHTML='<div class="empty-state" style="grid-column:1/-1"><p>加载餐厅失败</p><p class="sub">请稍后重试</p></div>';
}
state.loading=false;
}
function loadMoreRestaurants(){
state.page++;
loadRestaurants(false);
}
function renderSkeleton(n){
let html='';
for(let i=0;i<n;i++){
html+=`<div class="skeleton-card"><div class="sk-block sk-logo"></div><div class="sk-info"><div class="sk-block sk-line w60"></div><div class="sk-block sk-line w40"></div><div class="sk-block sk-line w80"></div><div class="sk-block sk-line w40"></div></div></div>`;
}
return html;
}
function renderRestaurantList(){
$('resultTotal').textContent=state.total;
const grid=$('restGrid');
if(!state.restaurants.length){
grid.innerHTML='<div class="empty-state" style="grid-column:1/-1"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15.93A7.001 7.001 0 0 1 5 12c0-.39.04-.77.1-1.14L10 15.74v.19c0 .74.21 1.43.57 2.02.14-.02.28-.02.43-.02zm6.9-2.54c-.26-1.05-1.2-1.84-2.35-1.84-.16 0-.31.02-.46.05L12.5 11.5V9c1.1 0 2-.9 2-2V5c0-.55-.45-1-1-1h-1c-.55 0-1 .45-1 1v2c0 1.1.9 2 2 2v2.5l-3.5 1.05c-.6.18-1 .7-1 1.3v.39l-3.5 3.5C4.42 16.45 4 15.27 4 14c0-.29.02-.58.05-.86L8 9.19V9c0-1.1.9-2 2-2V5c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1v2c0 1.1.9 2 2 2v.19l-3.05 3.05C6.84 12.5 8.27 12 10 12c1.73 0 3.16.5 4.05 1.24L8 11.5V11c0-1.1-.9-2-2-2V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v2c0 1.1.9 2 2 2v.5L8 13.05V13c0-.6.4-1.12 1-1.3l3.5-1.05v-.15c0-.6.4-1.12 1-1.3L17 8.5V8c0-1.1.9-2 2-2V4c0-.55-.45-1-1-1h-1c-.55 0-1 .45-1 1z"/></svg><p>没有找到相关餐厅</p><p class="sub">换个关键词或分类试试</p></div>';
$('loadMore').classList.add('hidden');
return;
}
grid.innerHTML=state.restaurants.map(r=>{
const tags=parseTags(r.tags).slice(0,3);
const open=r.status===1;
return `<div class="rest-card" onclick="openDetail(${r.id})">
<div class="rest-logo" style="background:${restGrad(r.id)}">
<svg viewBox="0 0 24 24">${restSvg(r.id)}</svg>
<div class="status-badge ${open?'':'closed'}">${open?'营业中':'休息中'}</div>
</div>
<div class="rest-info">
<div class="rest-name"><span class="status-dot ${open?'':'closed'}"></span>${esc(r.name)}</div>
<div class="rest-tags">${tags.map(t=>`<span class="rest-tag">${esc(t)}</span>`).join('')}</div>
<div class="rest-rate"><span class="star">★ ${money(r.rating)}</span><span class="sales">月售 ${r.sales}</span></div>
<div class="rest-meta">
<span>起送 ¥${money(r.min_order)}</span>
<span>${r.delivery_fee==0?'<span class="free">免配送费</span>':'配送 ¥'+money(r.delivery_fee)}</span>
<span>${r.delivery_time}分钟</span>
<span>${esc(r.distance)}</span>
</div>
</div>
</div>`;
}).join('');
// 加载更多
const hasMore=state.restaurants.length<state.total;
$('loadMore').classList.toggle('hidden',!hasMore);
}
/* 分类筛选 */
function selectCategory(id){
state.currentCategory = state.currentCategory===id ? 0 : id;
document.querySelectorAll('.cat-item').forEach((el,i)=>{
el.classList.toggle('active', state.categories[i]&&state.categories[i].id===state.currentCategory);
});
const cat=state.categories.find(c=>c.id===state.currentCategory);
$('listTitle').textContent = cat ? cat.name+'餐厅' : '全部餐厅';
loadRestaurants(true);
$('restSection').scrollIntoView({behavior:'smooth',block:'start'});
}
/* 排序 */
document.querySelectorAll('.sort-tab').forEach(tab=>{
tab.addEventListener('click',()=>{
document.querySelectorAll('.sort-tab').forEach(t=>t.classList.remove('active'));
tab.classList.add('active');
state.sort=tab.dataset.sort;
loadRestaurants(true);
});
});
/* 搜索 */
function doSearch(kw){
state.keyword=(kw||'').trim();
$('navSearchInput').value=state.keyword;
$('heroSearchInput').value=state.keyword;
loadRestaurants(true);
$('restSection').scrollIntoView({behavior:'smooth',block:'start'});
}
function goHome(){
state.currentCategory=0;
state.keyword='';
state.sort='';
$('navSearchInput').value='';
$('heroSearchInput').value='';
$('listTitle').textContent='全部餐厅';
document.querySelectorAll('.sort-tab').forEach((t,i)=>t.classList.toggle('active',i===0));
document.querySelectorAll('.cat-item').forEach(el=>el.classList.remove('active'));
loadRestaurants(true);
window.scrollTo({top:0,behavior:'smooth'});
}
/* ==========================================================
餐厅详情
========================================================== */
async function openDetail(id){
$('detailOverlay').classList.add('show');
$('detailPanel').classList.add('show');
document.body.style.overflow='hidden';
// loading 占位
$('detailHeaderBg').style.background=restGrad(id);
$('detailHeaderContent').innerHTML='<div style="color:#fff;font-size:16px">加载中...</div>';
$('detailInfoRow').innerHTML='';
$('menuSidebar').innerHTML='<div style="padding:20px;color:#999;text-align:center">加载中</div>';
$('menuList').innerHTML='';
try{
const data=await api('/restaurants/'+id+'/menu');
state.currentRestaurant=data.restaurant||{};
state.menuData=data;
renderDetail();
}catch(e){
$('detailHeaderContent').innerHTML='<div style="color:#fff;font-size:16px">加载失败</div>';
toast('详情加载失败','warn');
}
}
function closeDetail(){
$('detailOverlay').classList.remove('show');
$('detailPanel').classList.remove('show');
document.body.style.overflow='';
}
function renderDetail(){
const r=state.currentRestaurant;
const tags=parseTags(r.tags);
$('detailHeaderBg').style.background=restGrad(r.id);
$('detailHeaderContent').innerHTML=`
<div class="detail-name">${esc(r.name)}</div>
<div class="detail-tags">${tags.map(t=>`<span class="detail-tag">${esc(t)}</span>`).join('')}<span class="detail-tag">${esc(r.category_name)}</span></div>
${r.notice?`<div class="detail-notice"><svg viewBox="0 0 24 24"><path d="M18 11v2h4v-2h-4zm-2 6.59 3.29-3.29-1.41-1.41L14.59 16 16 17.59zM12 4C7.03 4 3 8.03 3 13s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zm-1-11h2v6h-2zm0 8h2v2h-2z"/></svg>${esc(r.notice)}</div>`:''}
`;
$('detailInfoRow').innerHTML=`
<div class="detail-info-item"><div class="v yellow">★ ${money(r.rating)}</div><div class="l">评分</div></div>
<div class="detail-info-item"><div class="v">${r.sales}</div><div class="l">月售</div></div>
<div class="detail-info-item"><div class="v">${r.delivery_time}<small style="font-size:11px">分钟</small></div><div class="l">配送时间</div></div>
<div class="detail-info-item"><div class="v">${esc(r.distance)}</div><div class="l">距离</div></div>
<div class="detail-info-item"><div class="v">¥${money(r.min_order)}</div><div class="l">起送</div></div>
<div class="detail-info-item"><div class="v">${r.delivery_fee==0?'免':('¥'+money(r.delivery_fee))}</div><div class="l">配送费</div></div>
`;
renderMenu();
}
function renderMenu(){
const cats=state.menuData.categories||[];
const items=state.menuData.menu_items||[];
// 侧边栏
$('menuSidebar').innerHTML=cats.map((c,i)=>`<div class="menu-cat-item ${i===0?'active':''}" data-cat="${c.id}" onclick="scrollToCat(${c.id})">${esc(c.name)}</div>`).join('');
// 菜品列表
let html='';
cats.forEach(cat=>{
const catItems=items.filter(it=>it.category_id===cat.id);
if(!catItems.length) return;
html+=`<div class="menu-section" id="catsec-${cat.id}">
<div class="menu-section-title">${esc(cat.name)}</div>`;
catItems.forEach(it=>{
const tags=parseTags(it.tags).slice(0,2);
const hasOld=it.original_price>0 && it.original_price>it.price;
const qty=getQty(it.id);
html+=`<div class="menu-item">
<div class="menu-item-img" style="background:${itemGrad(it.id)}">
${hasOld?'<span class="discount">折</span>':''}
<svg viewBox="0 0 24 24">${itemSvg(it.id)}</svg>
</div>
<div class="menu-item-info">
<div class="menu-item-name">${esc(it.name)}</div>
${it.description?`<div class="menu-item-desc">${esc(it.description)}</div>`:''}
<div class="menu-item-tags">${tags.map(t=>`<span class="menu-item-tag">${esc(t)}</span>`).join('')}</div>
<div class="menu-item-bottom">
<div class="menu-item-meta">
<div class="menu-item-price">
<span class="now">¥<small></small>${money(it.price)}</span>
${hasOld?`<span class="old">¥${money(it.original_price)}</span>`:''}
</div>
<div class="menu-item-stat"><span class="star">★ ${money(it.rating)}</span> · 月售${it.sales}</div>
</div>
${qty>0?`<div class="qty-control">
<button class="minus" onclick="changeQty(${it.id},-1,event)">−</button>
<span class="num">${qty}</span>
<button class="plus" onclick="changeQty(${it.id},1,event)">+</button>
</div>`:`<button class="add-btn" onclick="addToCart(${it.id},event)">
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
</button>`}
</div>
</div>
</div>`;
});
html+=`</div>`;
});
$('menuList').innerHTML=html || '<div class="empty-state"><p>暂无菜品</p></div>';
// 滚动联动监听
setupScrollSync();
}
/* 滚动联动 */
let scrollSyncOn=true;
function setupScrollSync(){
const list=$('menuList');
list.onscroll=()=>{
if(!scrollSyncOn) return;
const cats=state.menuData.categories||[];
let activeId=cats.length?cats[0].id:0;
for(const c of cats){
const sec=$('catsec-'+c.id);
if(sec && sec.offsetTop-60<=list.scrollTop){activeId=c.id}
}
document.querySelectorAll('.menu-cat-item').forEach(el=>{
el.classList.toggle('active',Number(el.dataset.cat)===activeId);
});
};
}
function scrollToCat(catId){
const sec=$('catsec-'+catId);
const list=$('menuList');
if(sec){
scrollSyncOn=false;
list.scrollTo({top:sec.offsetTop-10,behavior:'smooth'});
document.querySelectorAll('.menu-cat-item').forEach(el=>{
el.classList.toggle('active',Number(el.dataset.cat)===catId);
});
setTimeout(()=>{scrollSyncOn=true},400);
}
}
/* ==========================================================
购物车
========================================================== */
function getQty(itemId){
if(!state.cart||!state.cart.items[itemId]) return 0;
return state.cart.items[itemId].qty;
}
function addToCart(itemId,e){
if(e){e.stopPropagation()}
const items=state.menuData.menu_items||[];
const item=items.find(it=>it.id===itemId);
if(!item) return;
const r=state.currentRestaurant;
// 跨店清空提示
if(state.cart && state.cart.restaurantId!==r.id && Object.keys(state.cart.items).length){
if(!confirm('购物车已有「'+state.cart.restaurantName+'」的菜品,是否清空并加入新店菜品?')) return;
state.cart=null;
}
if(!state.cart){
state.cart={restaurantId:r.id,restaurantName:r.name,deliveryFee:r.delivery_fee,minOrder:r.min_order,items:{}};
}
if(state.cart.items[itemId]){
state.cart.items[itemId].qty++;
}else{
state.cart.items[itemId]={item:item,qty:1};
}
toast('已加入「'+item.name+'」','success');
renderMenu();
renderCart();
}
function changeQty(itemId,delta,e){
if(e){e.stopPropagation()}
if(!state.cart||!state.cart.items[itemId]) return;
state.cart.items[itemId].qty+=delta;
if(state.cart.items[itemId].qty<=0){
delete state.cart.items[itemId];
}
renderMenu();
renderCart();
if(state.cart&&!Object.keys(state.cart.items).length){
toggleCartDetail(false);
}
}
function clearCart(){
if(!state.cart||!Object.keys(state.cart.items).length) return;
if(!confirm('确定清空购物车吗?')) return;
state.cart=null;
renderMenu();
renderCart();
toggleCartDetail(false);
toast('购物车已清空','warn');
}
function cartCount(){
if(!state.cart) return 0;
return Object.values(state.cart.items).reduce((s,v)=>s+v.qty,0);
}
function cartTotal(){
if(!state.cart) return 0;
return Object.values(state.cart.items).reduce((s,v)=>s+v.item.price*v.qty,0);
}
function renderCart(){
const count=cartCount();
const total=cartTotal();
const bar=$('cartBar');
if(count>0){
bar.style.display='block';
$('cartBarBox').classList.remove('empty');
$('cartIconCircle').classList.add('active');
$('cartTotal').classList.remove('empty');
$('cartBadge').classList.add('show');
$('navCartBadge').classList.add('show');
$('cartBadge').textContent=count;
$('navCartBadge').textContent=count;
$('cartTotal').querySelector('.t').innerHTML='¥<small></small>'+money(total);
const r=state.currentRestaurant||{deliveryFee:state.cart?state.cart.deliveryFee:0,minOrder:state.cart?state.cart.minOrder:0};
const fee=state.cart?state.cart.deliveryFee:0;
const min=state.cart?state.cart.minOrder:0;
$('cartSub').textContent = total>=min ? ('配送费¥'+money(fee)) : ('还差¥'+money(min-total)+'起送');
const canCheckout=total>=min;
$('checkoutBtn').classList.toggle('disabled',!canCheckout);
$('checkoutBtn').textContent=canCheckout?'去结算':'差¥'+money(min-total)+'起送';
}else{
bar.style.display='none';
$('navCartBadge').classList.remove('show');
}
renderCartDetail();
}
function renderCartDetail(){
if(!state.cart||!Object.keys(state.cart.items).length){
$('cartDetailList').innerHTML='<div class="cart-detail-empty"><svg viewBox="0 0 24 24"><path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49A1 1 0 0 0 20 4H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg><p>购物车是空的</p><p class="sub">快去选些好吃的吧</p></div>';
return;
}
$('cartDetailTitle').textContent='购物车 · '+state.cart.restaurantName;
const html=Object.values(state.cart.items).map(v=>{
const it=v.item;
return `<div class="cart-detail-item">
<div style="width:44px;height:44px;border-radius:10px;background:${itemGrad(it.id)};display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg viewBox="0 0 24 24" style="width:22px;height:22px;fill:rgba(255,255,255,.92)">${itemSvg(it.id)}</svg>
</div>
<span class="name">${esc(it.name)}</span>
<span class="price">¥${money(it.price)}</span>
<div class="qty-control">
<button class="minus" onclick="changeQty(${it.id},-1)">−</button>
<span class="num">${v.qty}</span>
<button class="plus" onclick="changeQty(${it.id},1)">+</button>
</div>
</div>`;
}).join('');
$('cartDetailList').innerHTML=html;
}
function toggleCartDetail(force){
const show = force===undefined ? !$('cartDetail').classList.contains('show') : force;
if(show && (!state.cart||!Object.keys(state.cart.items).length)){toast('购物车是空的','warn');return}
$('cartDetail').classList.toggle('show',show);
$('cartDetailMask').classList.toggle('show',show);
}
function scrollToCart(){
if(!state.cart||!Object.keys(state.cart.items).length){toast('购物车是空的','warn');return}
toggleCartDetail(true);
}
function checkout(){
if(!state.cart||!Object.keys(state.cart.items).length) return;
const total=cartTotal();
if(total<(state.cart?state.cart.minOrder:0)){toast('未达到起送价','warn');return}
toast('订单提交成功(演示)¥'+money(total),'success');
state.cart=null;
renderMenu();
renderCart();
toggleCartDetail(false);
}
/* ==========================================================
启动
========================================================== */
document.addEventListener('keydown',e=>{
if(e.key==='Escape') closeDetail();
});
init();
</script>
</body>
</html>