From c7de728c18a61f09d601a26cfec378bdbaaa6252 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 3 Jul 2026 22:24:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(site):=20=E5=AF=BC=E8=88=AA=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=80=81=E4=B8=8B=E6=8B=89=E8=8F=9C=E5=8D=95=20+=20?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=BF=A1=E6=81=AF/=E9=80=80=E5=87=BA?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=EF=BC=8C=E7=99=BB=E5=BD=95=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E7=9B=B4=E8=B7=B3=20App?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 导航「进入系统 · 名字」改为下拉:个人信息 / 进入系统 / 退出登录,导航可重入刷新(window.jiuNavRender) - 新增 /profile/(账号 / 门店 / 授权三卡,authFetch 实时拉取)与 /logout/(撤销会话 + 清本地凭证) - 登录成功默认落地 /profile/(?next= 站内白名单不变),不再直跳 /app/ - auth.js 增加 jiuAuth.clear();site.css 含本轮下拉 / profile / 特惠卡样式 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o --- web/_includes/nav.njk | 4 +- web/assets/auth.js | 9 ++++- web/assets/nav.js | 64 ++++++++++++++++++++++++++---- web/assets/site.css | 39 +++++++++++++++++- web/login.njk | 6 +-- web/logout.njk | 39 ++++++++++++++++++ web/profile.njk | 92 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 237 insertions(+), 16 deletions(-) create mode 100644 web/logout.njk create mode 100644 web/profile.njk diff --git a/web/_includes/nav.njk b/web/_includes/nav.njk index bc9b2b2..eee9025 100644 --- a/web/_includes/nav.njk +++ b/web/_includes/nav.njk @@ -25,7 +25,7 @@ 价格 下载 帮助 - 注册门店 - 登录 + 注册门店 + 登录 diff --git a/web/assets/auth.js b/web/assets/auth.js index cf26346..d28232f 100644 --- a/web/assets/auth.js +++ b/web/assets/auth.js @@ -44,5 +44,12 @@ /* 是否可视为登录:access 过期但 refresh 还在(7 天),authFetch 会自动续 */ function loggedIn() { return !!(get('access_token') || get('refresh_token')); } - window.jiuAuth = { get: get, set: set, refresh: refresh, authFetch: authFetch, loggedIn: loggedIn }; + /* 退出登录:清凭证与身份,保留 shop_no/username 供登录页回填 */ + function clear() { + ['access_token', 'refresh_token', 'user_id', 'real_name', 'role', 'shop_id'].forEach(function (k) { + localStorage.removeItem('flutter.' + k); + }); + } + + window.jiuAuth = { get: get, set: set, refresh: refresh, authFetch: authFetch, loggedIn: loggedIn, clear: clear }; })(); diff --git a/web/assets/nav.js b/web/assets/nav.js index f105973..87e3a6a 100644 --- a/web/assets/nav.js +++ b/web/assets/nav.js @@ -1,5 +1,6 @@ /* 官网导航交互:汉堡菜单 + 登录态感知(Web 版 App 与官网同域时, - localStorage['flutter.*'] 有登录信息 → CTA 变「进入系统」)。零依赖。 */ + localStorage['flutter.*'] 有登录信息 → CTA 变「进入系统 · 姓名」下拉菜单)。 + renderAuth 可重复调用(暴露为 window.jiuNavRender,退出登录页清凭证后刷新导航)。零依赖。 */ (function () { function getAuthUser() { if (!window.jiuAuth || !jiuAuth.loggedIn()) return null; @@ -28,15 +29,62 @@ }); } - // 登录态:CTA 换成「进入系统」 - var user = getAuthUser(); - if (user) { + // 登录态渲染:桌面 CTA + 移动菜单(data-auth 标记的项按状态重建) + var MENU_ITEMS = + '个人信息' + + '进入系统' + + '
' + + '退出登录'; + + function renderAuth() { + var user = getAuthUser(); var cta = document.getElementById('nav-cta'); if (cta) { - cta.innerHTML = - '进入系统 · ' + - (user.realName || user.username).replace(/[<>&"]/g, '') + - ''; + if (user) { + cta.innerHTML = + ''; + } else { + cta.innerHTML = + '登录' + + '注册门店'; + } + } + if (mobile) { + Array.prototype.slice.call(mobile.querySelectorAll('[data-auth]')).forEach(function (el) { el.remove(); }); + var frag = document.createElement('div'); + frag.innerHTML = user + ? MENU_ITEMS + : '注册门店登录'; + Array.prototype.slice.call(frag.children).forEach(function (el) { + if (el.tagName === 'A') { el.setAttribute('data-auth', ''); mobile.appendChild(el); } + }); } } + + // 下拉开合(事件委托,renderAuth 重建 DOM 后依然有效) + document.addEventListener('click', function (e) { + var wrap = document.getElementById('nav-user'); + if (!wrap) return; + var btn = document.getElementById('nav-user-btn'); + if (btn && btn.contains(e.target)) { + var open = wrap.classList.toggle('open'); + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + } else if (!wrap.contains(e.target)) { + wrap.classList.remove('open'); + } + }); + document.addEventListener('keydown', function (e) { + if (e.key !== 'Escape') return; + var wrap = document.getElementById('nav-user'); + if (wrap) wrap.classList.remove('open'); + }); + + renderAuth(); + window.jiuNavRender = renderAuth; })(); diff --git a/web/assets/site.css b/web/assets/site.css index 5784219..873a1d4 100644 --- a/web/assets/site.css +++ b/web/assets/site.css @@ -64,6 +64,16 @@ h1,h2,h3{color:var(--heading); line-height:1.3; margin:0;} .nav-mobile{display:none; border-top:1px solid var(--border); background:var(--surface); padding:10px 24px 16px;} .nav-mobile a{display:block; padding:10px 0; color:var(--text); font-size:15px; border-bottom:1px solid var(--border-subtle); text-decoration:none !important;} .nav-mobile.open{display:block;} +.nav-user{position:relative;} +.nav-user-btn{display:inline-flex; align-items:center; gap:7px; cursor:pointer;} +.nav-user-btn svg{width:13px; height:13px; transition:transform .15s;} +.nav-user.open .nav-user-btn svg{transform:rotate(180deg);} +.nav-menu{display:none; position:absolute; right:0; top:calc(100% + 8px); min-width:158px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r-md); box-shadow:var(--sh-2); padding:6px; z-index:50;} +.nav-user.open .nav-menu{display:block;} +.nav-menu a{display:block; padding:9px 12px; border-radius:var(--r-sm); font-size:14px; color:var(--text); text-decoration:none !important;} +.nav-menu a:hover{background:var(--bg);} +.nav-menu a.danger{color:var(--danger);} +.nav-menu .sep{height:1px; background:var(--border-subtle); margin:5px 4px;} /* ---------- Hero(品牌渐变 = 登录页品牌面板同源) ---------- */ .hero{background:var(--mk-hero-grad); color:var(--on-accent); padding:84px 0 0; overflow:hidden;} @@ -129,6 +139,14 @@ table.roles .n{color:var(--faint);} .plans.p4{grid-template-columns:repeat(4,1fr); gap:16px;} .plans.p4 .plan{padding:26px 22px;} .plans.p4 .plan .price{font-size:30px;} +.plans.p5{grid-template-columns:repeat(5,1fr); gap:14px;} +.plans.p5 .plan{padding:24px 18px;} +.plans.p5 .plan .price{font-size:28px;} +.plan.promo{border-color:var(--warn); box-shadow:var(--sh-2); position:relative; background:linear-gradient(180deg,var(--warn-bg),var(--surface) 55%);} +.plan.promo::before{content:'🔥 新店专享'; position:absolute; top:-11px; left:50%; transform:translateX(-50%); background:var(--warn); color:#fff; font-size:var(--fs-xs); font-weight:700; padding:3px 12px; border-radius:var(--r-pill); white-space:nowrap;} +.plan.promo .price{color:var(--warn);} +.plan .price .was{font-size:14px; color:var(--faint); font-weight:400; text-decoration:line-through; font-family:var(--font); margin-left:6px;} +.plan .btn.disabled{pointer-events:none; opacity:.55;} .plan{background:var(--surface); border:1px solid var(--border); border-radius:var(--r-xl); padding:28px 26px; display:flex; flex-direction:column;} .plan.hot{border-color:var(--primary); box-shadow:var(--sh-2); position:relative;} .plan.hot::before{content:'推荐'; position:absolute; top:-11px; left:50%; transform:translateX(-50%); background:var(--primary); color:var(--on-primary); font-size:var(--fs-xs); font-weight:700; padding:3px 12px; border-radius:var(--r-pill);} @@ -169,12 +187,12 @@ table.roles .n{color:var(--faint);} /* ---------- 响应式 ---------- */ @media (max-width:1024px){ .grid.c4{grid-template-columns:repeat(2,1fr);} - .plans,.plans.p4{grid-template-columns:repeat(2,1fr);} + .plans,.plans.p4,.plans.p5{grid-template-columns:repeat(2,1fr);} .split{grid-template-columns:1fr;} .footer-grid{grid-template-columns:1fr 1fr;} } @media (max-width:640px){ - .plans,.plans.p4{grid-template-columns:1fr; max-width:440px; margin:0 auto;} + .plans,.plans.p4,.plans.p5{grid-template-columns:1fr; max-width:440px; margin:0 auto;} } @media (max-width:720px){ .nav-links,.nav-cta .btn.ghost{display:none;} @@ -248,6 +266,11 @@ textarea.input{height:auto; min-height:64px; padding:9px 11px; resize:vertical;} .seg-btn{border:none; background:transparent; padding:8px 18px; border-radius:calc(var(--r-md) - 3px); font-size:var(--fs-body); color:var(--muted); cursor:pointer; font-family:var(--font); display:inline-flex; align-items:center; gap:7px;} .seg-btn.on{background:var(--surface); color:var(--heading); font-weight:600; box-shadow:var(--sh-1);} .seg-tip{font-size:var(--fs-xs); color:var(--success); background:var(--ok-soft); padding:1px 8px; border-radius:var(--r-pill); font-weight:600;} +.seg-btn.promo{color:var(--warn); font-weight:700;} +.seg-btn.promo.on{color:var(--warn);} +.seg-btn.used,.seg-btn:disabled{color:var(--faint); font-weight:400; cursor:not-allowed; opacity:.7;} +.promo-banner{background:var(--warn-bg); border:1px solid var(--warn); border-radius:var(--r-md); padding:12px 14px; font-size:var(--fs-sm); color:var(--text); margin:10px 0 4px; line-height:1.7;} +.promo-banner b{display:block; color:var(--warn); font-size:var(--fs-body);} .seg-label{font-size:var(--fs-sm); color:var(--muted); margin:14px 0 8px;} .co-feats{list-style:none; padding:0; margin:12px 0 4px; display:flex; flex-direction:column; gap:8px; font-size:14px; color:var(--text);} .co-feats li{display:flex; gap:8px; align-items:flex-start;} @@ -281,6 +304,18 @@ textarea.input{height:auto; min-height:64px; padding:9px 11px; resize:vertical;} .sum-total .val{font-size:var(--fs-display); font-weight:800; color:var(--primary); font-family:var(--font-mono); letter-spacing:-.5px;} .co-summary .fine{font-size:var(--fs-xs); color:var(--faint); text-align:center; margin-top:12px; line-height:1.6;} +/* ---------- 个人信息(/profile/) ---------- */ +.pf-row{display:flex; justify-content:space-between; gap:16px; padding:9px 0; font-size:var(--fs-body); border-bottom:1px solid var(--border-subtle);} +.pf-row:last-child{border-bottom:none; padding-bottom:2px;} +.pf-row .k{color:var(--muted); flex:none;} +.pf-row .v{color:var(--heading); font-weight:500; text-align:right; word-break:break-all;} +.pf-row .v.mono{font-family:var(--font-mono);} +.pf-badge{display:inline-flex; padding:2px 10px; border-radius:var(--r-pill); font-size:var(--fs-sm); font-weight:600;} +.pf-badge.ok{background:var(--ok-soft); color:var(--success);} +.pf-badge.warn{background:var(--warn-bg); color:var(--warn);} +.pf-badge.bad{background:var(--danger-bg); color:var(--danger);} +.pf-acts{display:flex; gap:10px; flex-wrap:wrap; margin-top:4px;} + /* ---------- 支付结果页 ---------- */ .result{text-align:center; max-width:520px; margin:0 auto; padding:24px 16px;} .result .ring{width:72px; height:72px; border-radius:50%; display:flex; align-items:center; justify-content:center; margin:0 auto 18px;} diff --git a/web/login.njk b/web/login.njk index 80660d7..c1535f6 100644 --- a/web/login.njk +++ b/web/login.njk @@ -6,7 +6,7 @@ permalink: /login/ --- {# 官网内登录(不跳 App):POST /api/v1/auth/login,成功后把凭证写入与 Web 版 App(shared_preferences_web)共享的 localStorage['flutter.*'] - (值为 JSON 编码,与插件格式一致),再进入 /app/ 即已登录。 #} + (值为 JSON 编码,与插件格式一致),默认落地 /profile/;后续打开 /app/ 即已登录。 #}
@@ -67,9 +67,9 @@ permalink: /login/ .then(function (res) { if (!res.ok) throw new Error((res.j && res.j.error) || '登录失败,请检查账号信息'); persist(res.j.data || {}, shop); - // ?next= 仅允许站内路径(防开放跳转) + // ?next= 仅允许站内路径(防开放跳转);默认留在官网(个人信息页),不跳 App var next = new URLSearchParams(location.search).get('next') || ''; - location.href = (next.charAt(0) === '/' && next.indexOf('//') !== 0) ? next : '/app/'; + location.href = (next.charAt(0) === '/' && next.indexOf('//') !== 0) ? next : '/profile/'; }) .catch(function (e) { showErr(e && e.message ? e.message : '无法连接服务器,请稍后重试'); diff --git a/web/logout.njk b/web/logout.njk new file mode 100644 index 0000000..92bbf92 --- /dev/null +++ b/web/logout.njk @@ -0,0 +1,39 @@ +--- +layout: base.njk +title: 退出登录 +description: 退出岩美酒库管理系统的官网登录状态。 +permalink: /logout/ +--- +{# 退出登录:POST /api/v1/auth/logout 撤销服务端会话(尽力而为), + 再清 localStorage 凭证(保留门店编号/账号供下次登录回填),刷新导航登录态。 #} +
+
+
+

正在退出…

+

正在撤销当前登录会话,请稍候。

+ +
+
+ + diff --git a/web/profile.njk b/web/profile.njk new file mode 100644 index 0000000..8c4d48f --- /dev/null +++ b/web/profile.njk @@ -0,0 +1,92 @@ +--- +layout: base.njk +title: 个人信息 +description: 查看当前登录账号、门店与授权状态。 +permalink: /profile/ +--- +{# 个人信息(需登录):账号信息来自 localStorage;门店与授权实时拉 + GET /api/v1/shop/info(原样 JSON)与 GET /api/v1/license/info({data}包裹)。 #} +
+
首页/个人信息
+

个人信息

+ +
+

账号

+
姓名
+
登录账号
+
角色
+
门店编号
+
+ +
+

门店

+
门店名称加载中…
+
负责人
+
联系电话
+
门店地址
+
+ +
+

授权

+
状态加载中…
+
授权类型
+
到期时间
+
已绑定设备
+ +
+ + +
+ +