Files
jiu/backend/internal/handler/templates/public_product.html
T
wangjia 0de9ba7c21 feat(backend): 公开页自适应满屏 + 主题切换对齐 App ThemePickerPill
- 页面 flex 撑满视口(100dvh):页眉贴顶、页脚贴底、信息区弹性填充,
  底部不再留大片空白;内容超屏时自然滚动(移动端上下滑不受影响)
- 主题切换从 ◐ 循环钮改为与 App 一致的衬衫图标 pill + 下拉单选
  (A·经典蓝 / B·琥珀 / C·酒窖,勾选态),图标 path 取自原型 icons.js #i-shirt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-07 11:55:08 +08:00

325 lines
17 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">
<title>{{.PageTitle}}</title>{{.OGTags}}
<style>
/* 设计系统单一真源:变量名与三主题(a 经典蓝浅 / b 琥珀深 / c 酒窖暖浅)色值
逐字取自 .superpowers/prototype/tokens.css。SSR 页自包含无法直接引 tokens.css
改配色须先改原型令牌再同步此处。--glow/--seal-bg 为本页派生变量(主色低透明度/表面高透明度)。 */
:root{
--r-md:6px; --r-lg:10px; --r-xl:14px; --r-pill:999px;
--fs-display:28px; --fs-h1:21px; --fs-title:15px; --fs-body:13px; --fs-sm:12px; --fs-xs:11px;
--font:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei","Inter",sans-serif;
--font-mono:"JetBrains Mono","SF Mono",ui-monospace,monospace;
--scrim:rgba(15,20,30,.42);
/* 主题 A(默认) */
--bg:#F5F7FA; --surface:#FFFFFF; --border:#DCE2EB; --border-subtle:#ECEFF4;
--text:#232934; --heading:#1B2430; --muted:#6E7888; --faint:#99A3B3;
--primary:#2563AC; --primary-dark:#154072; --on-primary:#FFFFFF; --brand50:#EEF4FB; --accent:#8B2331;
--shadow:rgba(20,40,70,.20); --sh-1:0 1px 3px var(--shadow);
--glow1:rgba(37,99,172,.05); --glow2:rgba(21,64,114,.04); --seal-bg:rgba(255,255,255,.78);
}
:root[data-theme="b"]{
--bg:#15151C; --surface:#1E1E27; --border:#2B2B37; --border-subtle:#26262F;
--text:#E8E8EE; --heading:#F6F6F9; --muted:#9A9AA8; --faint:#6E6E7E;
--primary:#F5A300; --primary-dark:#D98C00; --on-primary:#1A1408; --brand50:#2A2212; --accent:#F87171;
--shadow:rgba(0,0,0,.55);
--glow1:rgba(245,163,0,.04); --glow2:rgba(248,113,113,.03); --seal-bg:rgba(30,30,39,.78);
}
:root[data-theme="c"]{
--bg:#FAF5EC; --surface:#FFFDF8; --border:#EADDC9; --border-subtle:#F1E8D8;
--text:#33271F; --heading:#241A14; --muted:#917A64; --faint:#B9A488;
--primary:#7C1F2C; --primary-dark:#5F1621; --on-primary:#F7E9D6; --brand50:#F3E7D2; --accent:#A23522;
--shadow:rgba(74,20,32,.22);
--glow1:rgba(124,31,44,.05); --glow2:rgba(181,134,47,.04); --seal-bg:rgba(255,253,248,.78);
}
*{box-sizing:border-box;margin:0;padding:0;}
html{background:var(--bg);height:100%;}
body{
min-height:100%;
background-color:var(--bg);
background-image:
radial-gradient(circle at 12% 0%, var(--glow1), transparent 38%),
radial-gradient(circle at 100% 100%, var(--glow2), transparent 38%);
color:var(--text);
font:var(--fs-title)/1.75 var(--font);
-webkit-font-smoothing:antialiased;
-webkit-tap-highlight-color:transparent;
transition:background-color .25s,color .25s;
}
/* 自适应满屏:页眉贴顶、页脚贴底、中间信息弹性填充;内容超屏时自然滚动 */
.page{max-width:560px;margin:0 auto;padding:0 16px 24px;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;}
a{color:var(--primary);text-decoration:none;}
/* 顶部题签 */
.mast{
display:flex;align-items:center;justify-content:center;gap:10px;
padding:18px 0 12px;color:var(--muted);font-size:var(--fs-sm);letter-spacing:.3em;
border-bottom:1px solid var(--border);position:relative;
}
.mast a{color:var(--primary-dark);font-weight:700;letter-spacing:.3em;}
/* 主题选择器:与 App ThemePickerPill 同构(衬衫图标 + 主题名 + ▾ + 下拉单选) */
.thm{
position:absolute;right:0;top:50%;transform:translateY(-58%);
display:flex;align-items:center;gap:6px;height:30px;padding:0 11px;
border-radius:var(--r-pill);border:1px solid var(--border);background:var(--surface);
color:var(--text);font-size:var(--fs-sm);font-family:var(--font);cursor:pointer;letter-spacing:0;
}
.thm svg{width:15px;height:15px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
.thm svg.cv{width:13px;height:13px;}
.thm-menu{
position:absolute;right:0;top:calc(100% + 6px);z-index:50;min-width:136px;padding:4px;
background:var(--surface);border:1px solid var(--border);border-radius:var(--r-md);
box-shadow:0 4px 14px var(--shadow);
}
.thm-menu[hidden]{display:none;}
.thm-menu button{
display:flex;align-items:center;gap:4px;width:100%;height:36px;padding:0 10px;
border:none;background:none;color:var(--text);font-size:var(--fs-body);
font-family:var(--font);cursor:pointer;border-radius:4px;text-align:left;
}
.thm-menu button:hover{background:var(--brand50);}
.thm-menu button.on{color:var(--primary);font-weight:600;}
.thm-menu .ck{width:16px;color:var(--primary);font-weight:700;flex:none;}
/* 图片区(永远存在)+ 右上「正品」章(--accent 酒红) */
.hero{margin:18px 0 0;position:relative;animation:rise .5s ease both;}
.frame{position:relative;background:var(--surface);border:1px solid var(--border);border-radius:var(--r-xl);overflow:hidden;box-shadow:var(--sh-1);}
.gallery{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none;}
.gallery::-webkit-scrollbar{display:none;}
.gallery img{width:100%;flex:0 0 100%;scroll-snap-align:center;aspect-ratio:1/1;object-fit:contain;background:var(--surface);display:block;cursor:zoom-in;}
.placeholder{
aspect-ratio:16/10;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:10px;color:var(--muted);font-size:var(--fs-sm);letter-spacing:.2em;background:var(--brand50);
}
.placeholder svg{width:44px;height:66px;stroke:var(--primary);opacity:.4;}
.seal{
position:absolute;top:12px;right:12px;z-index:3;width:56px;height:56px;border-radius:var(--r-pill);
border:2px solid var(--accent);color:var(--accent);background:var(--seal-bg);
display:flex;align-items:center;justify-content:center;transform:rotate(8deg);
font-size:14px;font-weight:700;letter-spacing:.16em;text-indent:.16em;
box-shadow:inset 0 0 0 2px var(--seal-bg);
}
.dots{position:absolute;left:0;right:0;bottom:10px;display:flex;justify-content:center;gap:6px;z-index:3;}
.dots i{width:6px;height:6px;border-radius:var(--r-pill);background:var(--border);transition:all .25s;}
.dots i.on{background:var(--primary);width:16px;border-radius:3px;}
/* 全屏灯箱(深底 = --toast-bg 提浓) */
.lb{position:fixed;inset:0;z-index:99;background:rgba(31,35,48,.96);}
.lb[hidden]{display:none;}
.lb-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;height:100%;scrollbar-width:none;}
.lb-track::-webkit-scrollbar{display:none;}
.lb-track img{width:100vw;height:100%;flex:0 0 100vw;scroll-snap-align:center;object-fit:contain;}
.lb-x{position:absolute;top:16px;right:16px;z-index:2;width:38px;height:38px;border-radius:var(--r-pill);
border:1px solid rgba(255,255,255,.35);background:var(--scrim);color:#fff;font-size:16px;cursor:pointer;}
.lb-n{position:absolute;bottom:22px;left:0;right:0;text-align:center;color:rgba(255,255,255,.85);
font-size:var(--fs-sm);letter-spacing:.2em;font-family:var(--font-mono);}
/* 题名 */
.head{text-align:center;padding:20px 6px 2px;animation:rise .5s ease .06s both;}
h1{font-size:var(--fs-h1);line-height:1.45;font-weight:700;color:var(--heading);}
.sub{color:var(--muted);font-size:var(--fs-body);letter-spacing:.12em;margin-top:5px;}
.price{margin-top:10px;color:var(--accent);font-size:var(--fs-display);font-weight:700;}
.price small{font-size:var(--fs-sm);color:var(--muted);font-weight:400;letter-spacing:.08em;}
.chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:12px;}
.chip{font-size:var(--fs-sm);color:var(--primary);border-radius:var(--r-pill);padding:2px 11px;background:var(--brand50);}
/* 分节 */
.sec{margin-top:22px;animation:rise .5s ease .12s both;}
.sec-t{
display:flex;align-items:center;gap:12px;color:var(--primary-dark);font-size:var(--fs-body);
letter-spacing:.3em;text-indent:.3em;font-weight:700;justify-content:center;margin-bottom:10px;
}
.sec-t::before,.sec-t::after{content:"";height:1px;background:var(--border);flex:1;max-width:56px;}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r-xl);padding:4px 16px;box-shadow:var(--sh-1);}
/* 信息行:label / value 两端对齐(App 列表行式) */
.row{display:flex;align-items:baseline;justify-content:space-between;gap:14px;padding:11px 0;font-size:14px;}
.row+.row{border-top:1px solid var(--border-subtle);}
.row span{color:var(--muted);flex:none;}
.row b{font-weight:600;text-align:right;word-break:break-all;color:var(--text);}
.row b.mono,.mono{font-family:var(--font-mono);font-size:var(--fs-body);}
.desc{padding:13px 2px;font-size:14px;color:var(--text);white-space:pre-wrap;text-align:justify;}
.more{display:block;text-align:center;padding:12px 0 10px;font-size:13.5px;font-weight:600;border-top:1px solid var(--border-subtle);}
/* 页脚:flex 撑到视口底部 */
.foot{margin-top:auto;padding-top:30px;text-align:center;color:var(--muted);font-size:var(--fs-sm);line-height:2;animation:rise .5s ease .18s both;}
.foot-rule{display:flex;align-items:center;gap:12px;margin-bottom:8px;color:var(--border);}
.foot-rule::before,.foot-rule::after{content:"";height:1px;background:var(--border);flex:1;}
.foot a{color:var(--muted);}
.foot .brand{color:var(--primary);font-weight:600;}
.foot .icp{font-size:var(--fs-xs);opacity:.9;}
.empty{text-align:center;padding:80px 20px 60px;animation:rise .5s ease both;}
.empty h1{font-size:19px;margin-bottom:10px;}
.empty p{color:var(--muted);font-size:var(--fs-body);}
@keyframes rise{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
@media (prefers-reduced-motion:reduce){*{animation:none!important;}}
</style>
<script>
/* 首屏前定主题防闪烁:记忆值 > 系统深色(→b) > a */
(function(){try{
var t=localStorage.getItem('jiu-pub-theme');
if(!t&&window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches)t='b';
if(t==='b'||t==='c')document.documentElement.setAttribute('data-theme',t);
}catch(e){}})();
</script>
</head>
<body>
<div class="page">
<header class="mast"><a href="https://jiu.51yanmei.com">岩美酒库</a>
<button class="thm" id="thm" aria-label="切换主题">
<svg viewBox="0 0 24 24"><path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"/></svg>
<span id="thmName">经典蓝</span>
<svg class="cv" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div class="thm-menu" id="thmMenu" hidden>
<button data-t="a"><span class="ck"></span>A · 经典蓝</button>
<button data-t="b"><span class="ck"></span>B · 琥珀</button>
<button data-t="c"><span class="ck"></span>C · 酒窖</button>
</div>
</header>
{{if .Found}}
<div class="hero">
<div class="frame">
{{if .Images}}
<div class="gallery" id="gal">
{{range $i, $img := .Images}}<img src="{{$img}}" alt="{{$.Name}}" decoding="async"{{if $i}} loading="lazy"{{end}}>{{end}}
</div>
{{if gt (len .Images) 1}}<div class="dots">{{range .Images}}<i></i>{{end}}</div>{{end}}
{{else}}
<div class="placeholder">
<svg viewBox="0 0 40 64" fill="none" stroke-width="1.4" stroke-linecap="round">
<path d="M16 2h8M17 2v14c0 3 6 3 6 0V2M17 12c-6 5-9 9-9 16v28a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4V28c0-7-3-11-9-16"/>
<path d="M8 38h24" stroke-dasharray="2 3"/>
</svg>
<span>实物图整理中</span>
</div>
{{end}}
<div class="seal">正品</div>
</div>
</div>
<div class="head">
<h1>{{.Name}}</h1>
{{if or .Series .Spec}}<div class="sub">{{.Series}}{{if and .Series .Spec}} · {{end}}{{.Spec}}</div>{{end}}
{{if .Price}}<div class="price">{{.Price}}{{if .Unit}} <small>/ {{.Unit}}</small>{{end}}</div>{{end}}
{{if .Keywords}}<div class="chips">{{range .Keywords}}<span class="chip">{{.}}</span>{{end}}</div>{{end}}
</div>
<div class="sec">
<div class="sec-t">商品信息</div>
<div class="card">
{{if .Code}}<div class="row"><span>商品编号</span><b class="mono">{{.Code}}</b></div>{{end}}
{{if .Series}}<div class="row"><span>系列</span><b>{{.Series}}</b></div>{{end}}
{{if .Spec}}<div class="row"><span>规格</span><b>{{.Spec}}</b></div>{{end}}
{{if .Origin}}<div class="row"><span>产地</span><b>{{.Origin}}</b></div>{{end}}
<div class="row"><span>保质期</span><b>{{.ShelfLife}}</b></div>
<div class="row"><span>储存方式</span><b>{{.Storage}}</b></div>
</div>
</div>
{{if .HasBatch}}
<div class="sec">
<div class="sec-t">批次信息</div>
<div class="card">
{{if .ProdDate}}<div class="row"><span>生产日期</span><b class="mono">{{.ProdDate}}</b></div>{{end}}
{{if .BatchNo}}<div class="row"><span>批次号</span><b class="mono">{{.BatchNo}}</b></div>{{end}}
{{if .InDate}}<div class="row"><span>入库日期</span><b class="mono">{{.InDate}}</b></div>{{end}}
</div>
</div>
{{end}}
{{if .DescBody}}
<div class="sec">
<div class="sec-t">{{.DescTitle}}</div>
<div class="card"><div class="desc">{{.DescBody}}</div></div>
</div>
{{end}}
{{if .HasShop}}
<div class="sec">
<div class="sec-t">门店信息</div>
<div class="card">
<div class="row"><span>门店</span><b>{{if .ShopCode}}<a href="/shop/{{.ShopCode}}">{{.ShopName}} </a>{{else}}{{.ShopName}}{{end}}</b></div>
{{if .ShopPhone}}<div class="row"><span>电话</span><b class="mono"><a href="tel:{{.ShopPhone}}">{{.ShopPhone}}</a></b></div>{{end}}
{{if .ShopAddr}}<div class="row"><span>地址</span><b>{{.ShopAddr}}</b></div>{{end}}
{{if .ShopHours}}<div class="row"><span>营业时间</span><b>{{.ShopHours}}</b></div>{{end}}
{{if .ShopWx}}<div class="row"><span>微信</span><b class="mono">{{.ShopWx}}</b></div>{{end}}
{{if .ShopCode}}<a class="more" href="/shop/{{.ShopCode}}">查看本店更多商品 </a>{{end}}
</div>
</div>
{{end}}
{{else}}
<div class="empty">
<h1>商品不存在或已下架</h1>
<p>请确认链接来源,或联系门店核实。</p>
</div>
{{end}}
<footer class="foot">
<div class="foot-rule"></div>
<div>技术支持:<a class="brand" href="https://jiu.51yanmei.com">岩美科技</a> · <a class="icp" href="https://beian.miit.gov.cn" target="_blank" rel="noopener">京ICP备2026039814号</a></div>
</footer>
</div>
<script>
(function(){
var NAMES={a:'经典蓝',b:'琥珀',c:'酒窖'},root=document.documentElement,
btn=document.getElementById('thm'),menu=document.getElementById('thmMenu'),
label=document.getElementById('thmName');
if(!btn||!menu)return;
var opts=[].slice.call(menu.children);
function cur(){return root.getAttribute('data-theme')||'a';}
function paint(){
label.textContent=NAMES[cur()];
opts.forEach(function(b){var on=b.getAttribute('data-t')===cur();
b.className=on?'on':'';b.querySelector('.ck').textContent=on?'✓':'';});
}
btn.addEventListener('click',function(e){e.stopPropagation();menu.hidden=!menu.hidden;});
opts.forEach(function(b){b.addEventListener('click',function(e){
e.stopPropagation();
var t=b.getAttribute('data-t');
if(t==='a')root.removeAttribute('data-theme');else root.setAttribute('data-theme',t);
try{localStorage.setItem('jiu-pub-theme',t);}catch(err){}
paint();menu.hidden=true;
});});
document.addEventListener('click',function(){menu.hidden=true;});
paint();
})();
</script>
{{if .Images}}
<div class="lb" id="lb" hidden>
<button class="lb-x" id="lbx" aria-label="关闭"></button>
<div class="lb-track" id="lbt">
{{range .Images}}<img src="{{.}}" alt="" loading="lazy">{{end}}
</div>
<div class="lb-n" id="lbn"></div>
</div>
<script>
(function(){
var gal=document.getElementById('gal'),lb=document.getElementById('lb'),
lbt=document.getElementById('lbt'),lbn=document.getElementById('lbn');
if(!gal||!lb)return;
var imgs=[].slice.call(gal.children),n=imgs.length,
dots=[].slice.call(document.querySelectorAll('.dots i'));
function idx(el){return Math.min(n-1,Math.round(el.scrollLeft/el.clientWidth));}
function paint(){var i=idx(gal);for(var k=0;k<dots.length;k++)dots[k].className=k===i?'on':'';}
if(dots.length){gal.addEventListener('scroll',function(){requestAnimationFrame(paint);},{passive:true});}
function num(){lbn.textContent=(idx(lbt)+1)+' / '+n;}
imgs.forEach(function(im,i){im.addEventListener('click',function(){
lb.hidden=false;document.body.style.overflow='hidden';
lbt.scrollLeft=lbt.clientWidth*i;num();
});});
lbt.addEventListener('scroll',function(){requestAnimationFrame(num);},{passive:true});
function close(){lb.hidden=true;document.body.style.overflow='';}
document.getElementById('lbx').addEventListener('click',close);
lb.addEventListener('click',function(e){if(e.target===lb||e.target===lbt)close();});
})();
</script>
{{end}}
</body>
</html>