首页/ phph5/ 甜趣蛋糕 · 烘焙蛋糕店 PHP+H5 商城 + 商家后台/ 代码预览

💻 代码预览 · 甜趣蛋糕 · 烘焙蛋糕店 PHP+H5 商城 + 商家后台

ph_mall · 上传者 驰旭教育
← 返回详情
r117/public/app.js · 约 38302 字符
1/* 甜趣蛋糕 H5 · 前端逻辑(在线走真实 PHP API,离线走 SEED + localStorage 兜底) */2(function () {3  'use strict';4  const $ = (s, r) => (r || document).querySelector(s);5  const view = $('#view'), topbar = $('#topbar'), tabbar = $('#tabbar');6  let member = SEED.member;7  let cur = { view: 'home', pid: 0, selSize: '', selFlavor: '', lastFilter: {} };89  /* ---------- 工具 ---------- */10  function toast(msg) {11    const t = $('#toast'); t.textContent = msg; t.classList.add('show');12    clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove('show'), 1600);13  }14  function money(n) { return '¥' + (Math.round(n * 100) / 100); }15  function esc(s) { return (s == null ? '' : String(s)).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }16  function openSheet(html) { $('#sheet').innerHTML = html; $('#mask').classList.add('show'); }17  function closeSheet() { $('#mask').classList.remove('show'); }18  $('#mask').addEventListener('click', e => { if (e.target.id === 'mask') closeSheet(); });1920  function setTop(title, back) {21    let h = '';22    if (back) h += '<div class="back" onclick="SC.back()">‹</div>';23    if (title) h += '<div class="title">' + esc(title) + '</div>';24    else h += '<div class="title"></div>';25    h += '<div class="act" onclick="SC.home()">🏠</div>';26    topbar.innerHTML = h;27    SC._back = back || null;28  }2930  /* ---------- API:在线优先,离线 SEED 兜底 ---------- */31  async function api(action, payload, method) {32    method = method || 'GET';33    try {34      const opt = { method, headers: { 'Content-Type': 'application/json' } };35      let url = 'index.php?c=api&a=' + action;36      if (method === 'GET' && payload) {37        url += '&' + new URLSearchParams(payload).toString();38      } else if (payload) {39        opt.body = JSON.stringify(payload);40      }41      const r = await fetch(url, opt);42      if (!r.ok) throw 0;43      const j = await r.json();44      if (j && j.ok) return j;45      throw 0;46    } catch (e) {47      return seedApi(action, payload || {}, method);48    }49  }5051  /* ---------- 离线 SEED / localStorage 兜底 ---------- */52  const LS = {53    get(k, d) { try { return JSON.parse(localStorage.getItem(k)) || d; } catch (e) { return d; } },54    set(k, v) { localStorage.setItem(k, JSON.stringify(v)); }55  };56  function ensureDemo() {57    if (!localStorage.getItem('sc_orders')) {58      LS.set('sc_orders', [{59        id: 1001, order_no: '2026081500123000101', member_id: 2, contact_name: '阿慕', phone: '13800000002',60        address: '西安市雁塔区科技路 18 号', delivery_type: 'delivery', delivery_time: '昨天下午6点', card_msg: '生日快乐!',61        remark: '', coupon_id: 0, coupon_amount: 0, points_used: 0, total_amount: 336, pay_amount: 336,62        status: 'done', created_at: '2026-08-15 18:02:11',63        items: [{ name: '芒果千层慕斯', size: '8寸', flavor: '原味', price: 158, qty: 1, subtotal: 158 },64                { name: '海盐芝士蛋糕', size: '8寸', flavor: '原味', price: 178, qty: 1, subtotal: 178 }]65      }]);66    }67    if (!localStorage.getItem('sc_mycoupons')) {68      LS.set('sc_mycoupons', [{ id: 11, coupon_id: 2, name: '满199减30', desc: '满199减30', status: 'unused', type: 'full_reduce' },69                               { id: 12, coupon_id: 5, name: '无门槛5元券', desc: '无门槛¥5', status: 'unused', type: 'no_threshold' }]);70    }71  }72  function seedApi(action, p, method) {73    method = method || 'GET';74    const S = SEED;75    switch (action) {76      case 'banners': return { ok: 1, data: S.banners };77      case 'categories': return { ok: 1, scenes: S.scenes, kinds: S.kinds, sizes: S.sizes };78      case 'products': {79        let list = S.products.slice();80        if (p.kind) list = list.filter(x => x.kind_id == p.kind);81        if (p.scene) list = list.filter(x => (x.scenes || '').split('|').includes(p.scene));82        if (p.kw) { const k = (p.kw || '').toLowerCase(); list = list.filter(x => (x => x.name.toLowerCase().includes(k) || (x.tags || '').toLowerCase().includes(k))(x)); }83        if (p.tag) list = list.filter(x => x[p.tag] == 1);84        if (p.sort === 'sales') list.sort((a, b) => b.sales - a.sales);85        else if (p.sort === 'price_asc') list.sort((a, b) => a.price - b.price);86        else if (p.sort === 'price_desc') list.sort((a, b) => b.price - a.price);87        else if (p.sort === 'rating') list.sort((a, b) => b.rating - a.rating);88        else list.sort((a, b) => (b.is_hot - a.is_hot) || (b.sales - a.sales));89        return { ok: 1, data: list };90      }91      case 'product_detail': {92        const pr = S.products.find(x => x.id == p.id);93        if (!pr) return { ok: 0 };94        return { ok: 1, product: pr, skus: pr.skus, reviews: S.reviews.filter(r => r.product_id == p.id) };95      }96      case 'search': {97        const k = (p.kw || '').toLowerCase();98        const list = S.products.filter(x => x.name.toLowerCase().includes(k) || (x.tags || '').toLowerCase().includes(k) || (x.scenes || '').toLowerCase().includes(k));99        return { ok: 1, data: list };100      }101      case 'recommend': return { ok: 1, data: S.products.filter(x => x.is_recommend || x.is_hot).slice(0, 8) };102      case 'member_info': return { ok: 1, member: member, coupons_count: LS.get('sc_mycoupons', []).filter(c => c.status === 'unused').length };103      case 'coupon_list': return { ok: 1, data: S.coupons };104      case 'settings': return { ok: 1, data: S.settings };105      case 'cart_list': {106        const cart = LS.get('sc_cart', []);107        let total = 0; const data = cart.map(c => { c.subtotal = Math.round(c.price * c.qty * 100) / 100; total += c.subtotal; return c; });108        return { ok: 1, data, total: Math.round(total * 100) / 100 };109      }110      case 'cart_add': {111        const cart = LS.get('sc_cart', []);112        const ex = cart.find(c => c.product_id == p.product_id && c.size == p.size && c.flavor == p.flavor);113        if (ex) ex.qty += p.qty; else cart.push({ product_id: p.product_id, size: p.size, flavor: p.flavor, price: p.price, qty: p.qty, name: p.name, images: p.images });114        LS.set('sc_cart', cart); return { ok: 1, msg: '已加入购物车' };115      }116      case 'cart_update': {117        const cart = LS.get('sc_cart', []); const it = cart.find(c => c.product_id == p.product_id && c.size == p.size && c.flavor == p.flavor); if (it) it.qty = p.qty; LS.set('sc_cart', cart); return { ok: 1 };118      }119      case 'cart_remove': {120        let cart = LS.get('sc_cart', []); cart = cart.filter(c => !(c.product_id == p.product_id && c.size == p.size && c.flavor == p.flavor)); LS.set('sc_cart', cart); return { ok: 1 };121      }122      case 'coupon_receive': {123        const mine = LS.get('sc_mycoupons', []); const c = S.coupons.find(x => x.id == p.coupon_id);124        if (mine.find(m => m.coupon_id == p.coupon_id)) return { ok: 0, msg: '您已领取过该券' };125        mine.push({ id: Date.now(), coupon_id: c.id, name: c.name, desc: c.desc, type: c.type, status: 'unused' });126        LS.set('sc_mycoupons', mine); return { ok: 1, msg: '领取成功' };127      }128      case 'coupon_my': return { ok: 1, data: LS.get('sc_mycoupons', []) };129      case 'order_create': {130        ensureDemo();131        const orders = LS.get('sc_orders', []);132        const oid = Date.now(); const no = '2026' + oid.toString().slice(-10);133        orders.unshift({ id: oid, order_no: no, member_id: 2, contact_name: p.contact_name, phone: p.phone, address: p.address,134          delivery_type: p.delivery_type, delivery_time: p.delivery_time, card_msg: p.card_msg, remark: p.remark, coupon_id: 0, coupon_amount: 0, points_used: 0,135          total_amount: p.total_amount, pay_amount: p.pay_amount, status: 'paid', created_at: new Date().toISOString().slice(0, 19).replace('T', ' '), items: p.items });136        LS.set('sc_orders', orders); LS.set('sc_cart', []); return { ok: 1, order_id: oid, order_no: no, pay_amount: p.pay_amount };137      }138      case 'order_list': ensureDemo(); return { ok: 1, data: LS.get('sc_orders', []) };139      case 'order_detail': ensureDemo(); { const o = LS.get('sc_orders', []).find(x => x.id == p.id); return o ? { ok: 1, order: o } : { ok: 0 }; }140      case 'order_cancel': { const o = LS.get('sc_orders', []); const it = o.find(x => x.id == p.id); if (it) it.status = 'canceled'; LS.set('sc_orders', o); return { ok: 1 }; }141      case 'order_review': { const o = LS.get('sc_orders', []); const it = o.find(x => x.id == p.order_id); if (it) it.status = 'done'; LS.set('sc_orders', o); return { ok: 1 }; }142      case 'custom_submit': return { ok: 1, msg: '定制需求已提交,客服将尽快与您确认' };143      default: return { ok: 0, msg: 'offline' };144    }145  }146147  /* ---------- 购物车操作 ---------- */148  async function addToCart(product, size, flavor, qty) {149    const sku = product.skus.find(s => s.size === size && s.flavor === flavor) || product.skus[0];150    const price = sku.price;151    const r = await api('cart_add', { member_id: member.id, product_id: product.id, sku_id: sku.id || 0, size, flavor, price, qty, name: product.name, images: product.images }, 'POST');152    if (r.ok) { toast('已加入购物车 🛒'); updateBadge(); }153    else toast(r.msg || '添加失败');154  }155  async function updateBadge() {156    const r = await api('cart_list', { member_id: member.id });157    const n = r.ok ? r.data.length : 0;158    const b = $('#cartBadge'); if (b) { b.textContent = n; b.style.display = n ? 'inline-block' : 'none'; }159  }160161  /* ---------- Tab / 路由 ---------- */162  function renderTabbar() {163    const tabs = [['home', '🏠', '首页'], ['category', '🍰', '分类'], ['cart', '🛒', '购物车'], ['member', '👤', '我的']];164    tabbar.innerHTML = tabs.map(t => '<div class="tab ' + (cur.view === t[0] ? 'on' : '') + '" onclick="SC.go(\'' + t[0] + '\')"><span class="ic">' + t[1] + '</span>' + t[2] + (t[0] === 'cart' ? '<span class="badge" id="cartBadge" style="display:none">0</span>' : '') + '</div>').join('');165  }166  const SC = {167    home() { render('home'); },168    back() { if (SC._back) SC._back(); else render('home'); },169    go(v) { if (['home', 'category', 'cart', 'member'].includes(v)) render(v); else location.hash = '#' + v; }170  };171172  async function render(v) {173    if (v) cur.view = v.replace(/:.*/, '');174    renderTabbar();175    if (cur.view === 'home') return renderHome();176    if (cur.view === 'category') return renderCategory();177    if (cur.view === 'detail') return renderDetail();178    if (cur.view === 'cart') return renderCart();179    if (cur.view === 'checkout') return renderCheckout();180    if (cur.view === 'orders') return renderOrders();181    if (cur.view === 'order') return renderOrderDetail();182    if (cur.view === 'member') return renderMember();183    if (cur.view === 'coupons') return renderCoupons();184    if (cur.view === 'custom') return renderCustom();185    if (cur.view === 'search') return renderSearch();186    if (cur.view === 'help') return renderHelp();187  }188189  const STATUS = { pending: ['待支付', 'st-pending'], paid: ['已支付·待制作', 'st-paid'], making: ['制作中', 'st-making'], delivering: ['配送中', 'st-delivering'], done: ['已完成', 'st-done'], canceled: ['已取消', 'st-canceled'], refund: ['退款', 'st-refund'] };190191  /* ---------- 首页 ---------- */192  async function renderHome() {193    setTop('甜趣蛋糕', null);194    const [b, p, rec] = await Promise.all([api('banners'), api('products'), api('recommend')]);195    const banners = b.ok ? b.data : [];196    const scenes = SEED.scenes;197    const hot = p.ok ? p.data.filter(x => x.is_hot).slice(0, 4) : [];198    const newp = p.ok ? p.data.filter(x => x.is_new).slice(0, 4) : [];199    const recs = rec.ok ? rec.data : [];200    view.innerHTML =201      '<div class="searchbox" onclick="SC.go(\'search\')">🔍 <input placeholder="搜索蛋糕 / 场景 / 口味" readonly></div>' +202      '<div class="banner"><div class="slide" id="bslide"><div class="emo" id="bemo">🎂</div><div><div class="t" id="bt"></div><div class="s" id="bs"></div></div></div></div>' +203      '<div class="dots" id="bdots"></div>' +204      '<div class="quick">' + scenes.map((s, i) => '<div class="q" onclick="SC.go(\'category\')"><div class="emo">' + ['🎂','💍','🎉','☕','🧒','💕','🎊','🎁'][i] + '</div><div class="l">' + s + '</div></div>').join('') + '</div>' +205      sec('热销榜单', '全部', 'category') + grid(hot) +206      sec('新品上市', '全部', 'category') + grid(newp) +207      sec('为你推荐', '', null) + grid(recs) +208      '<div class="note">🎂 新鲜手工现做 · 提前 ' + SEED.settings.delivery_advance_hours + ' 小时预约 · 满' + SEED.settings.free_delivery_amount + '元免配送费</div>';209    let bi = 0;210    function showB() { const x = banners[bi % banners.length]; $('#bemo').textContent = x.image; $('#bt').textContent = x.title; $('#bs').textContent = '点击查看对应专区'; }211    if (banners.length) {212      showB();213      const dots = $('#bdots'); dots.innerHTML = banners.map((_, i) => '<span class="' + (i === 0 ? 'on' : '') + '"></span>').join('');214      setInterval(() => { bi++; showB(); [...dots.children].forEach((d, i) => d.classList.toggle('on', i === bi % banners.length)); }, 3000);215    }216  }217  function sec(t, more, goto) { return '<div class="sec-h"><div class="t">' + t + '</div>' + (more ? '<div class="more" onclick="SC.go(\'' + goto + '\')">' + more + ' ›</div>' : '<div></div>') + '</div>'; }218  function grid(list) {219    if (!list || !list.length) return '<div class="empty"><span class="e">🍰</span>暂无商品</div>';220    return '<div class="grid">' + list.map(p => {221      const tag = p.is_new ? '<span class="tag new">新品</span>' : (p.is_hot ? '<span class="tag hot">热销</span>' : '');222      return '<div class="pcard" onclick="SC.go(\'detail:' + p.id + '\')"><div class="ph">' + tag + (p.images[0] || '🎂') + '</div>' +223        '<div class="body"><div class="nm">' + esc(p.name) + '</div>' +224        '<div class="meta">已售' + p.sales + ' · ' + p.rating + '★</div>' +225        '<span class="price">' + money(p.price) + (p.original_price ? '<small>' + money(p.original_price) + '</small>' : '') + '</span>' +226        '<button class="add" onclick="event.stopPropagation();SC.quickAdd(' + p.id + ')">+</button></div></div>';227    }).join('') + '</div>';228  }229  SC.quickAdd = async function (id) {230    const r = await api('product_detail', { id });231    if (!r.ok) return toast('商品不存在');232    addToCart(r.product, r.skus[0].size, r.skus[0].flavor, 1);233  };234235  /* ---------- 分类 ---------- */236  async function renderCategory() {237    setTop('全部分类', () => render('home'));238    const [c, p] = await Promise.all([api('categories'), api('products', cur.lastFilter)]);239    const cats = c.ok ? c : { scenes: SEED.scenes, kinds: SEED.kinds, sizes: SEED.sizes };240    const f = cur.lastFilter;241    view.innerHTML =242      '<div class="fbar" id="fkind">' + '<div class="c ' + (!f.kind ? 'on' : '') + '" data-k="0">全部</div>' + cats.kinds.map(k => '<div class="c ' + (f.kind == k.id ? 'on' : '') + '" data-k="' + k.id + '">' + k.name + '</div>').join('') + '</div>' +243      '<div class="fbar" id="fscene">' + cats.scenes.map(s => '<div class="c ' + (f.scene === s ? 'on' : '') + '" data-s="' + esc(s) + '">' + s + '</div>').join('') + '</div>' +244      '<div class="sortbar" id="fsort"><div class="c ' + (!f.sort ? 'on' : '') + '" data-o="">综合</div><div class="c ' + (f.sort === 'sales' ? 'on' : '') + '" data-o="sales">销量</div><div class="c ' + (f.sort === 'price_asc' ? 'on' : '') + '" data-o="price_asc">价格↑</div><div class="c ' + (f.sort === 'price_desc' ? 'on' : '') + '" data-o="price_desc">价格↓</div><div class="c ' + (f.sort === 'rating' ? 'on' : '') + '" data-o="rating">好评</div></div>' +245      '<div id="plist"></div>';246    const reload = async () => {247      const r = await api('products', cur.lastFilter);248      $('#plist').innerHTML = grid(r.ok ? r.data : []);249    };250    $('#fkind').onclick = e => { const t = e.target.closest('.c'); if (!t) return; cur.lastFilter.kind = +t.dataset.k || 0; renderCategory(); };251    $('#fscene').onclick = e => { const t = e.target.closest('.c'); if (!t) return; cur.lastFilter.scene = cur.lastFilter.scene === t.dataset.s ? '' : t.dataset.s; renderCategory(); };252    $('#fsort').onclick = e => { const t = e.target.closest('.c'); if (!t) return; cur.lastFilter.sort = t.dataset.o; renderCategory(); };253    reload();254  }255256  /* ---------- 详情 ---------- */257  async function renderDetail() {258    const r = await api('product_detail', { id: cur.pid });259    if (!r.ok) { view.innerHTML = '<div class="empty"><span class="e">🚫</span>商品不存在</div>'; return; }260    const pr = r.product, skus = r.skus, revs = r.reviews || [];261    cur.selSize = skus[0].size; cur.selFlavor = skus[0].flavor;262    setTop(pr.name, () => render('home'));263    const sizes = [...new Set(skus.map(s => s.size))];264    const flavors = [...new Set(skus.map(s => s.flavor))];265    const curSku = () => skus.find(s => s.size === cur.selSize && s.flavor === cur.selFlavor) || skus[0];266    view.innerHTML =267      '<div class="dph">' + (pr.images[0] || '🎂') + '</div>' +268      '<div class="dname">' + esc(pr.name) + '</div>' +269      '<div class="dmeta">已售 ' + pr.sales + ' · ' + pr.rating + '★ · 库存 ' + pr.stock + '</div>' +270      '<div class="dprice">' + money(curSku().price) + (pr.original_price ? '<small>' + money(pr.original_price) + '</small>' : '') + (pr.is_new ? ' <span class="muted" style="font-size:12px">新品</span>' : '') + '</div>' +271      '<div class="attr"><div class="row"><div class="lab">尺寸</div><div class="opts" id="oSz">' + sizes.map(s => '<div class="opt ' + (s === cur.selSize ? 'on' : '') + '" data-s="' + esc(s) + '">' + s + '</div>').join('') + '</div></div>' +272      '<div class="row"><div class="lab">口味</div><div class="opts" id="oFl">' + flavors.map(f => '<div class="opt ' + (f === cur.selFlavor ? 'on' : '') + '" data-f="' + esc(f) + '">' + f + '</div>').join('') + '</div></div>' +273      '<div class="row"><div class="lab">数量</div><div class="qty"><button onclick="SC.chg(-1)">−</button><span id="qnum">1</span><button onclick="SC.chg(1)">+</button></div></div></div>' +274      '<div class="desc"><h4>商品介绍</h4>' + esc(pr.description) +275      '<h4>配料表</h4>' + esc(pr.ingredients) + '<h4>过敏原提示</h4>' + esc(pr.allergen) +276      '<h4>保质期</h4>' + esc(pr.shelf_life) + '<h4>食用建议</h4>' + esc(pr.advice) + '</div>' +277      (revs.length ? '<div class="sec-h mt12"><div class="t">用户评价 (' + revs.length + ')</div></div>' + revs.map(rv => '<div class="field" style="box-shadow:none;border:1px solid var(--line)"><div class="flex between"><b>' + esc(rv.username) + '</b><span class="brand">' + '★'.repeat(rv.rating) + '</span></div><div class="mt8">' + esc(rv.content) + '</div></div>').join('') : '') +278      '<div style="height:70px"></div>' +279      '<div class="bar2"><div class="price" id="bprice">' + money(curSku().price) + '</div>' +280      '<button class="btn ghost" onclick="SC.buyNow()">立即购买</button>' +281      '<button class="btn pri" onclick="SC.addCart()">加入购物车</button></div>';282    const refresh = () => { const sk = curSku(); $('#bprice').textContent = money(sk.price); };283    $('#oSz').onclick = e => { const t = e.target.closest('.opt'); if (!t) return; cur.selSize = t.dataset.s; [...$('#oSz').children].forEach(o => o.classList.toggle('on', o === t)); refresh(); };284    $('#oFl').onclick = e => { const t = e.target.closest('.opt'); if (!t) return; cur.selFlavor = t.dataset.f; [...$('#oFl').children].forEach(o => o.classList.toggle('on', o === t)); refresh(); };285    SC._qty = 1;286    SC.chg = d => { SC._qty = Math.max(1, SC._qty + d); $('#qnum').textContent = SC._qty; };287    SC.addCart = () => addToCart(pr, cur.selSize, cur.selFlavor, SC._qty);288    SC.buyNow = async () => { await addToCart(pr, cur.selSize, cur.selFlavor, SC._qty); render('cart'); };289    window._curProduct = pr;290  }291292  /* ---------- 购物车 ---------- */293  async function renderCart() {294    setTop('购物车', () => render('home'));295    const r = await api('cart_list', { member_id: member.id });296    const list = r.ok ? r.data : [];297    if (!list.length) { view.innerHTML = '<div class="empty"><span class="e">🛒</span>购物车还是空的<br>去挑一款喜欢的蛋糕吧</div>'; return; }298    view.innerHTML = '<div id="clist"></div>' +299      '<div class="bar2"><div class="price" id="ctotal">合计 ' + money(r.total) + '</div><button class="btn pri" onclick="SC.checkout()">去结算</button></div>';300    const draw = () => {301      $('#clist').innerHTML = list.map(c => '<div class="citem" data-p="' + c.product_id + '" data-s="' + esc(c.size) + '" data-f="' + esc(c.flavor) + '">' +302        '<div class="ph">' + (c.images && c.images[0] || '🎂') + '</div>' +303        '<div class="info"><div class="nm">' + esc(c.name) + '</div><div class="sp">' + esc(c.size) + ' · ' + esc(c.flavor) + '</div>' +304        '<div class="pr">' + money(c.price) + ' <span class="qty" style="float:right"><button onclick="SC.cqty(' + c.product_id + ',\'' + esc(c.size) + '\',\'' + esc(c.flavor) + '\',-1)">−</button><span>' + c.qty + '</span><button onclick="SC.cqty(' + c.product_id + ',\'' + esc(c.size) + '\',\'' + esc(c.flavor) + '\',1)">+</button></span></div></div>' +305        '<div onclick="SC.cdel(' + c.product_id + ',\'' + esc(c.size) + '\',\'' + esc(c.flavor) + '\')" style="color:#bbb;font-size:20px;cursor:pointer">🗑</div></div>').join('');306    };307    draw();308    SC.cqty = async (pid, sz, fl, d) => {309      const it = list.find(c => c.product_id == pid && c.size === sz && c.flavor === fl);310      const nq = Math.max(1, it.qty + d);311      await api('cart_update', { product_id: pid, size: sz, flavor: fl, qty: nq }, 'POST');312      it.qty = nq; const t = list.reduce((a, c) => a + c.price * c.qty, 0); $('#ctotal').textContent = '合计 ' + money(t); draw();313    };314    SC.cdel = async (pid, sz, fl) => { await api('cart_remove', { product_id: pid, size: sz, flavor: fl }, 'POST'); renderCart(); updateBadge(); };315    SC.checkout = () => render('checkout');316  }317318  /* ---------- 结算 ---------- */319  async function renderCheckout() {320    setTop('确认订单', () => render('cart'));321    const [cart, set] = await Promise.all([api('cart_list', { member_id: member.id }), api('settings')]);322    const list = cart.ok ? cart.data : [];323    if (!list.length) { view.innerHTML = '<div class="empty"><span class="e">🧾</span>购物车为空</div>'; return; }324    const set0 = set.ok ? set.data : SEED.settings;325    const fee = list.reduce((a, c) => a + c.price * c.qty, 0) < (+set0.free_delivery_amount) ? (+set0.delivery_fee) : 0;326    const total = list.reduce((a, c) => a + c.price * c.qty, 0) + fee;327    cur._fee = fee; cur._total = total; cur._items = list;328    view.innerHTML =329      '<div class="field"><label>收货人</label><input id="cn" value="' + esc(member.username) + '"></div>' +330      '<div class="field"><label>手机号</label><input id="cp" value="' + esc(member.phone) + '"></div>' +331      '<div class="field"><label>配送地址</label><input id="ca" value="' + esc(set0.address) + '"></div>' +332      '<div class="field"><label>配送方式</label><select id="dt"><option value="delivery">同城配送(' + (+set0.delivery_fee) + '元,满' + set0.free_delivery_amount + '免)</option><option value="pickup">到店自提(免配送费)</option></select></div>' +333      '<div class="field"><label>期望送达时间</label><input id="dtime" placeholder="如:明天下午3点"></div>' +334      '<div class="field"><label>贺卡留言</label><textarea id="card" placeholder="写下想说的话…"></textarea></div>' +335      '<div class="field"><label>订单备注</label><input id="remark" placeholder="忌口 / 特殊要求"></div>' +336      list.map(c => '<div class="citem"><div class="ph">' + (c.images && c.images[0] || '🎂') + '</div><div class="info"><div class="nm">' + esc(c.name) + '</div><div class="sp">' + esc(c.size) + ' · ' + esc(c.flavor) + ' × ' + c.qty + '</div><div class="pr">' + money(c.price * c.qty) + '</div></div></div>').join('') +337      '<div class="note">配送费 ' + money(fee) + ' · 合计 <b class="brand">' + money(total) + '</b>(演示环境支付为模拟)</div>' +338      '<button class="btn pri block" onclick="SC.submitOrder()">提交订单 ' + money(total) + '</button>';339  }340  SC.submitOrder = async function () {341    const items = cur._items.map(c => ({ product_id: c.product_id, sku_id: c.sku_id || 0, qty: c.qty }));342    const r = await api('order_create', {343      member_id: member.id, contact_name: $('#cn').value, phone: $('#cp').value, address: $('#ca').value,344      delivery_type: $('#dt').value, delivery_time: $('#dtime').value, card_msg: $('#card').value, remark: $('#remark').value,345      items, total_amount: cur._total, pay_amount: cur._total346    }, 'POST');347    if (r.ok) { toast('下单成功 🎉'); updateBadge(); openSheet('<div class="sheet"><h3>🎉 支付成功(演示)</h3><p class="muted">订单号 ' + r.order_no + '<br>实付 ' + money(r.pay_amount) + '</p><button class="btn pri block mt12" onclick="closeSheet();SC.go(\'orders\')">查看我的订单</button></div>'); }348    else toast(r.msg || '下单失败');349  };350351  /* ---------- 订单列表 ---------- */352  async function renderOrders() {353    setTop('我的订单', () => render('member'));354    const r = await api('order_list', { member_id: member.id });355    const list = r.ok ? r.data : [];356    if (!list.length) { view.innerHTML = '<div class="empty"><span class="e">📦</span>还没有订单</div>'; return; }357    view.innerHTML = list.map(o => {358      const st = STATUS[o.status] || ['', ''];359      return '<div class="ord" onclick="SC.go(\'order:' + o.id + '\')"><div class="h"><span>订单号 ' + o.order_no + '</span><span class="st ' + st[1] + '">' + st[0] + '</span></div>' +360        o.items.map(it => '<div class="it"><div class="ph">🎂</div><div>' + esc(it.name) + ' <span class="muted">' + esc(it.size) + '·' + esc(it.flavor) + ' ×' + it.qty + '</span></div></div>').join('') +361        '<div class="total">实付 <b>' + money(o.pay_amount) + '</b></div></div>';362    }).join('');363  }364  async function renderOrderDetail() {365    const r = await api('order_detail', { id: cur.oid });366    if (!r.ok) { view.innerHTML = '<div class="empty"><span class="e">🚫</span>订单不存在</div>'; return; }367    const o = r.order; const st = STATUS[o.status] || ['', ''];368    setTop('订单详情', () => render('orders'));369    const steps = ['paid', 'making', 'delivering', 'done'];370    const idx = steps.indexOf(o.status);371    const prog = idx >= 0 ? '<div class="prog">' + [['🧾', '已支付'], ['👩‍🍳', '制作中'], ['🚚', '配送中'], ['✅', '已完成']].map((s, i) => '<div class="p ' + (i <= idx ? 'on' : '') + '"><div class="dot">' + s[0] + '</div><div class="l">' + s[1] + '</div></div>').join('') + '</div>' : '';372    view.innerHTML = '<div class="ord" style="cursor:default"><div class="h"><span>订单号 ' + o.order_no + '</span><span class="st ' + st[1] + '">' + st[0] + '</span></div>' +373      o.items.map(it => '<div class="it"><div class="ph">🎂</div><div>' + esc(it.name) + ' <span class="muted">' + esc(it.size) + '·' + esc(it.flavor) + ' ×' + it.qty + '</span></div><div class="pr">' + money(it.price * it.qty) + '</div></div>').join('') + '</div>' +374      prog +375      '<div class="field"><label>收货人</label><div>' + esc(o.contact_name) + ' · ' + esc(o.phone) + '</div></div>' +376      '<div class="field"><label>地址</label><div>' + esc(o.address) + '</div></div>' +377      (o.delivery_time ? '<div class="field"><label>期望送达</label><div>' + esc(o.delivery_time) + '</div></div>' : '') +378      (o.card_msg ? '<div class="field"><label>贺卡留言</label><div>' + esc(o.card_msg) + '</div></div>' : '') +379      '<div class="field"><label>实付金额</label><div class="brand bold" style="font-size:16px">' + money(o.pay_amount) + '</div></div>' +380      (o.status === 'paid' || o.status === 'making' ? '<button class="btn ghost block" onclick="SC.cancelOrder(' + o.id + ')">取消订单</button>' : '') +381      (o.status === 'done' ? '<button class="btn pri block" onclick="SC.reviewOrder(' + o.id + ',' + (o.items[0] ? o.items[0].product_id : 0) + ')">评价晒单</button>' : '');382    SC.cancelOrder = async id => { await api('order_cancel', { id }, 'POST'); toast('已取消'); render('orders'); };383    SC.reviewOrder = async (oid, pid) => {384      openSheet('<div class="sheet"><span class="close" onclick="closeSheet()">×</span><h3>评价晒单</h3><div id="rstars" class="flex" style="gap:6px;font-size:24px;margin:10px 0">★★★☆☆</div><textarea id="rcontent" placeholder="说说蛋糕怎么样~"></textarea><button class="btn pri block mt12" onclick="SC.doReview(' + oid + ',' + pid + ')">提交评价</button></div>');385      const stars = $('#rstars'); let rating = 3;386      const paint = () => stars.textContent = '★'.repeat(rating) + '☆'.repeat(5 - rating);387      stars.onclick = () => { rating = Math.min(5, rating + 1); paint(); };388      SC.doReview = async (oid, pid) => { await api('order_review', { order_id: oid, product_id: pid, member_id: member.id, rating, content: $('#rcontent').value }, 'POST'); closeSheet(); toast('感谢评价 ❤'); render('orders'); };389    };390  }391392  /* ---------- 会员中心 ---------- */393  async function renderMember() {394    setTop('个人中心', () => render('home'));395    const [m, c] = await Promise.all([api('member_info', { member_id: member.id }), api('coupon_my', { member_id: member.id })]);396    const mem = m.ok ? m.member : member;397    const myC = c.ok ? c.data : [];398    view.innerHTML =399      '<div class="mhero"><div class="nm">' + esc(mem.username) + ' <span class="lv">' + esc(mem.level_name) + '</span></div>' +400      '<div class="row"><div class="c"><div class="n">' + mem.points + '</div><div class="l">积分</div></div>' +401      '<div class="c"><div class="n">' + money(mem.balance) + '</div><div class="l">储值余额</div></div>' +402      '<div class="c"><div class="n">' + mem.growth + '</div><div class="l">成长值</div></div>' +403      '<div class="c"><div class="n">' + myC.filter(x => x.status === 'unused').length + '</div><div class="l">优惠券</div></div></div></div>' +404      '<div class="mlist mt12">' +405      '<div class="it" onclick="SC.go(\'orders\')"><span class="e">📦</span><span class="t">我的订单</span><span class="r">›</span></div>' +406      '<div class="it" onclick="SC.go(\'coupons\')"><span class="e">🎟️</span><span class="t">我的优惠券</span><span class="r">' + myC.filter(x => x.status === 'unused').length + ' 张 ›</span></div>' +407      '<div class="it" onclick="SC.go(\'custom\')"><span class="e">🎨</span><span class="t">定制蛋糕</span><span class="r">›</span></div>' +408      '<div class="it" onclick="SC.go(\'help\')"><span class="e">ℹ️</span><span class="t">帮助与关于</span><span class="r">›</span></div>' +409      '</div>' +410      '<div class="note mt12">🎂 会员日福利 · 积分抵现(100 积分=1 元,单笔最高抵 10%)· 生日专属券 · 免费升磅</div>';411  }412413  /* ---------- 优惠券 ---------- */414  async function renderCoupons() {415    setTop('优惠券', () => render('member'));416    const [list, mine] = await Promise.all([api('coupon_list'), api('coupon_my', { member_id: member.id })]);417    const cs = list.ok ? list.data : [];418    const my = mine.ok ? mine.data : [];419    view.innerHTML = sec('领券中心', '', null) +420      cs.map(c => '<div class="coupon"><div class="l"><div class="v">' + (c.type === 'discount' ? (c.discount * 10) : '¥' + c.value) + (c.type === 'discount' ? '<small>折</small>' : '') + '</div><div style="font-size:10px;opacity:.9">' + (c.type === 'newcomer' ? '新人' : c.type === 'birthday' ? '生日' : '券') + '</div></div>' +421        '<div class="r"><div class="nm">' + esc(c.name) + '</div><div class="cond">' + c.desc + ' · 有效期' + c.valid_days + '天</div></div>' +422        '<div class="get"><button onclick="SC.recv(' + c.id + ')">领取</button></div></div>').join('') +423      sec('我的优惠券', '', null) +424      (my.length ? my.map(c => '<div class="coupon ' + (c.status !== 'unused' ? 'used' : '') + '"><div class="l"><div class="v">' + (c.desc || '').replace('满', '').replace('减', '¥') + '</div></div><div class="r"><div class="nm">' + esc(c.name) + '</div><div class="cond">' + (c.status === 'unused' ? '未使用' : c.status === 'used' ? '已使用' : '已过期') + '</div></div></div>').join('') : '<div class="muted" style="padding:10px">还没有领取优惠券</div>');425    SC.recv = async function (id) { const r = await api('coupon_receive', { member_id: member.id, coupon_id: id }, 'POST'); toast(r.msg || (r.ok ? '领取成功' : '失败')); if (r.ok) renderCoupons(); };426  }427428  /* ---------- 定制蛋糕 ---------- */429  async function renderCustom() {430    setTop('定制蛋糕', () => render('member'));431    view.innerHTML =432      '<div class="note">🎨 上传参考图、写下祝福语与造型,专属甜品师为您 1 对 1 设计</div>' +433      '<div class="field"><label>参考图(可上传一张)</label><div class="upbox" id="upbox">点击上传图片</div><input type="file" id="upfile" accept="image/*" style="display:none"></div>' +434      '<div class="field"><label>祝福语 / 文字内容</label><input id="ctext" placeholder="如:宝贝生日快乐"></div>' +435      '<div class="field"><label>造型偏好</label><div class="chips" id="cshape">' + ['城堡', '小动物', '鲜花', '卡通', '字母', '其它'].map(s => '<div class="c" data-s="' + s + '">' + s + '</div>').join('') + '</div></div>' +436      '<div class="field"><label>尺寸</label><div class="chips" id="csize">' + SEED.sizes.map(s => '<div class="c" data-s="' + esc(s) + '">' + s + '</div>').join('') + '</div></div>' +437      '<div class="field"><label>特殊要求</label><textarea id="cnote" placeholder="奶油颜色、忌口、配送时间…"></textarea></div>' +438      '<button class="btn pri block" onclick="SC.submitCustom()">提交定制需求</button>';439    let img = '', shape = '', size = '8寸';440    $('#upbox').onclick = () => $('#upfile').click();441    $('#upfile').onchange = e => { const f = e.target.files[0]; if (!f) return; const rd = new FileReader(); rd.onload = ev => { img = ev.target.result; $('#upbox').innerHTML = '<img src="' + img + '">'; }; rd.readAsDataURL(f); };442    $('#cshape').onclick = e => { const t = e.target.closest('.c'); if (!t) return; shape = t.dataset.s; [...e.currentTarget.children].forEach(o => o.classList.toggle('on', o === t)); };443    $('#csize').onclick = e => { const t = e.target.closest('.c'); if (!t) return; size = t.dataset.s; [...e.currentTarget.children].forEach(o => o.classList.toggle('on', o === t)); };444    SC.submitCustom = async () => {445      const r = await api('custom_submit', { member_id: member.id, image: img, text: $('#ctext').value, shape, size, note: $('#cnote').value }, 'POST');446      if (r.ok) { toast(r.msg); render('member'); } else toast(r.msg || '提交失败');447    };448  }449450  /* ---------- 搜索 ---------- */451  async function renderSearch() {452    setTop('搜索', () => render('home'));453    view.innerHTML = '<div class="searchbox" style="background:#fff;border:1px solid var(--line)"><span style="color:#999">🔍</span><input id="skw" placeholder="搜索蛋糕 / 场景 / 口味" style="color:#333"></div>' +454      '<div class="sec-h mt12"><div class="t">热门搜索</div></div><div class="chips">' + ['生日蛋糕', '草莓', '慕斯', '芝士', '无糖', '儿童', '巧克力'].map(s => '<div class="c" onclick="SC.doSearch(\'' + s + '\')">' + s + '</div>').join('') + '</div>' +455      '<div id="sres"></div>';456    const doS = async kw => { const r = await api('search', { kw }); $('#sres').innerHTML = sec('搜索结果', '', null) + grid(r.ok ? r.data : []); };457    $('#skw').addEventListener('input', e => { if (e.target.value) doS(e.target.value); });458    SC.doSearch = doS;459  }460461  /* ---------- 帮助 ---------- */462  async function renderHelp() {463    setTop('帮助与关于', () => render('member'));464    const s = SEED.settings;465    view.innerHTML =466      '<div class="mhero" style="background:linear-gradient(135deg,#ffb6c8,#ff8fab)"><div class="nm">' + esc(s.shop_name) + '</div><div class="row"><div class="c"><div class="n">' + esc(s.phone) + '</div><div class="l">客服电话</div></div><div class="c"><div class="n">' + esc(s.open_time) + '</div><div class="l">营业时间</div></div></div></div>' +467      '<div class="field"><label>门店地址</label><div>' + esc(s.address) + '</div></div>' +468      '<div class="field"><label>配送说明</label><div>' + esc(s.notice) + '</div></div>' +469      '<div class="mlist"><div class="it"><span class="e">🚚</span><span class="t">配送范围</span><span class="r">门店 ' + s.delivery_range_km + ' 公里内</span></div>' +470      '<div class="it"><span class="e">💰</span><span class="t">配送费</span><span class="r">' + s.delivery_fee + ' 元(满' + s.free_delivery_amount + '免)</span></div>' +471      '<div class="it"><span class="e">⏰</span><span class="t">提前预约</span><span class="r">' + s.delivery_advance_hours + ' 小时</span></div>' +472      '<div class="it"><span class="e">↩️</span><span class="t">售后退换</span><span class="r">破损/口味不符可退</span></div></div>' +473      '<div class="note mt12">本模板为「蛋糕店 PHP + H5 全栈演示」:前端 H5 + 后端 PHP MVC + SQLite,下载解压后 <code>php -S</code> 即可运行。演示数据仅供学习参考。</div>';474  }475476  /* ---------- 启动 ---------- */477  function route() {478    const h = location.hash.replace('#', '');479    if (h.startsWith('detail:')) { cur.view = 'detail'; cur.pid = +h.split(':')[1]; }480    else if (h.startsWith('order:')) { cur.view = 'order'; cur.oid = +h.split(':')[1]; }481    else if (h) cur.view = h;482    else cur.view = 'home';483    render();484  }485  window.SC = SC;486  window.addEventListener('hashchange', route);487  ensureDemo();488  updateBadge();489  route();490})();491