diff --git a/.cursor/skills/dukang-release/SKILL.md b/.cursor/skills/dukang-release/SKILL.md index 1fdbeff..673d8a7 100644 --- a/.cursor/skills/dukang-release/SKILL.md +++ b/.cursor/skills/dukang-release/SKILL.md @@ -18,11 +18,13 @@ description: >- | `dev` | **staging 测试** | `/opt/dukang-staging` | 8190–8194 | `*-test.dukanghaoke.com` | | `main` | **production 生产** | `/opt/dukang` | 8090–8094 | `*.dukanghaoke.com` | -默认:`dev_jacy` → merge → `dev` → **发测试**。 -生产:`dev` 验收通过 → merge → `main` → **发生产**。勿 force push `main`/`dev`。 +默认:`dev_jacy` → merge → `dev` → merge → `main` → **发生产**。勿 force push `main`/`dev`。 -用户只说「发布」且未指明生产时 → **发测试(staging)**。 -用户说「发生产 / 上线生产」→ 用 `deploy-prod.sh`。 +> **团队约定(2026-08)**:用户说「发布 / 发版 / 直接发版」且**未特别强调**时 → **发生产(production)**。 +> 仅当用户明确说「发测试 / staging / 测试环境」时 → 发 staging。 + +用户说「发生产 / 上线生产 / 发版」→ 用 `deploy-prod.sh`。 +用户说「发测试」→ 用 `deploy-staging.sh`。 ## 标准流程(Windows) diff --git a/apps/mini-user/src/lib/nav-bar.ts b/apps/mini-user/src/lib/nav-bar.ts index 710f146..90c769d 100644 --- a/apps/mini-user/src/lib/nav-bar.ts +++ b/apps/mini-user/src/lib/nav-bar.ts @@ -106,6 +106,8 @@ export function tabNavContentStyle(metrics: NavBarMetrics): Record { const pagePad = 20; + // 标题两侧取「右侧避让胶囊」宽度,保证相对屏幕视觉居中 + const titlePad = Math.max(metrics.navBarPaddingRight, 72); return { paddingTop: `${metrics.statusBarHeight}px`, height: `${metrics.navBarHeight}px`, @@ -113,7 +115,7 @@ export function subPageNavBarStyle(metrics: NavBarMetrics): Record pickStoreId(router.params.id)); const [store, setStore] = useState(null); const [recentRedeems, setRecentRedeems] = useState([]); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(''); const [headerSolid, setHeaderSolid] = useState(false); + const storeRef = useRef(null); + storeRef.current = store; usePageScroll(({ scrollTop }) => { setHeaderSolid(scrollTop > 100); }); - useDidShow(() => { - if (!storeId) return; - request(`/stores/${storeId}`) - .then(setStore) - .catch(() => { + const loadStore = useCallback(async (id: string) => { + if (!id) { + setLoading(false); + setLoadError('缺少门店参数'); + return; + } + setLoadError(''); + if (!storeRef.current) setLoading(true); + try { + const data = await request(`/stores/${id}`); + if (!data || !data.id) { setStore(null); + setLoadError('门店不存在或暂不可见'); toast('门店不存在或暂不可见'); - }); - request(`/stores/${storeId}/recent-redeems?limit=20`) - .then((list) => setRecentRedeems(Array.isArray(list) ? list : [])) - .catch(() => setRecentRedeems([])); + return; + } + setStore(data); + } catch (e) { + const msg = e instanceof Error ? e.message : '加载失败'; + setLoadError(msg); + if (!storeRef.current) toast(msg); + } finally { + setLoading(false); + } + }, []); + + const loadRecentRedeems = useCallback(async (id: string) => { + if (!id) { + setRecentRedeems([]); + return; + } + try { + const list = await request(`/stores/${id}/recent-redeems?limit=20`); + setRecentRedeems(normalizeRecentRedeems(list)); + } catch { + setRecentRedeems([]); + } + }, []); + + const bootstrap = useCallback( + (id: string) => { + const nextId = pickStoreId(id); + if (!nextId) { + setLoading(false); + setLoadError('缺少门店参数'); + return; + } + setStoreId(nextId); + void loadStore(nextId); + void loadRecentRedeems(nextId); + }, + [loadStore, loadRecentRedeems], + ); + + // 首屏:useLoad 带 options.id,比仅用 useDidShow 更稳(H5/小程序都覆盖) + useLoad((options) => { + bootstrap(options?.id || router.params.id || ''); + }); + + useEffect(() => { + const fromRouter = pickStoreId(router.params.id); + if (fromRouter && fromRouter !== storeId) { + bootstrap(fromRouter); + } + }, [router.params.id, storeId, bootstrap]); + + // 登录态变化后回到本页:重拉详情与走马灯 + useDidShow(() => { + const id = pickStoreId(storeId || router.params.id); + if (!id) return; + void loadStore(id); + void loadRecentRedeems(id); }); const sharePayload = useMemo( @@ -200,7 +294,9 @@ export default function StoreDetailPage() { return ( - 加载中… + + {loading ? '加载中…' : loadError || '门店不存在或暂不可见'} + ); } @@ -225,8 +321,7 @@ export default function StoreDetailPage() { }).catch(() => toast('无法预览图片')); } - // 文案过短时放慢动画,过长时加快一点(仍循环) - const marqueeDurationSec = Math.max(18, Math.min(60, Math.round(marqueeText.length / 2.2))); + const marqueeDurationSec = Math.max(18, Math.min(60, Math.round((marqueeText.length || 24) / 2.2))); return ( @@ -288,15 +383,15 @@ export default function StoreDetailPage() { {marqueeText ? ( - + - {marqueeText} -    - {marqueeText} -    + {marqueeText} + {marqueeText} ) : null} diff --git a/apps/mini-user/src/styles/nav-bar.css b/apps/mini-user/src/styles/nav-bar.css index 369b2b4..dc9e210 100644 --- a/apps/mini-user/src/styles/nav-bar.css +++ b/apps/mini-user/src/styles/nav-bar.css @@ -203,7 +203,8 @@ height: var(--nav-content-height); line-height: var(--nav-content-height); text-align: center; - padding: 0 88px 0 56px; + /* 左右等宽留白,避免右侧分享/胶囊导致标题视觉偏左 */ + padding: 0 max(72px, var(--nav-padding-right, 96px)); box-sizing: border-box; opacity: 0; font-family: var(--font-headline); diff --git a/apps/mini-user/src/styles/store-detail.css b/apps/mini-user/src/styles/store-detail.css index b7b1815..5464554 100644 --- a/apps/mini-user/src/styles/store-detail.css +++ b/apps/mini-user/src/styles/store-detail.css @@ -129,16 +129,16 @@ .store-detail-marquee { margin: 0 var(--space-page) 12px; - padding: 8px 12px; + padding: 8px 0; border-radius: var(--radius-lg); background: rgba(166, 29, 36, 0.06); overflow: hidden; - white-space: nowrap; } .store-detail-marquee-track { - display: inline-flex; - align-items: center; + display: flex; + flex-direction: row; + flex-wrap: nowrap; width: max-content; animation-name: store-detail-marquee-scroll; animation-timing-function: linear; @@ -146,20 +146,21 @@ will-change: transform; } -.store-detail-marquee-text, -.store-detail-marquee-gap { - flex-shrink: 0; +.store-detail-marquee-item { + flex: 0 0 auto; + padding: 0 24px; + white-space: nowrap; font-size: 12px; - line-height: 1.4; + line-height: 1.5; color: #666; } @keyframes store-detail-marquee-scroll { from { - transform: translateX(0); + transform: translate3d(0, 0, 0); } to { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } }