diff --git a/App.vue b/App.vue index f4eedfd..388ac50 100644 --- a/App.vue +++ b/App.vue @@ -74,7 +74,10 @@ export default { const isFirst = uni.getStorageSync('is_first_launch') if (!isFirst || isFirst === 'true') { // 首次使用 → 引导页 - uni.reLaunch({ url: '/pages/onboarding/onboarding' }) + // 延迟一帧再跳:启动早期首页还未就绪,立即 reLaunch 可能报 timeout + setTimeout(() => { + uni.reLaunch({ url: '/pages/onboarding/onboarding', fail: () => {} }) + }, 100) } // 否则正常进入首页(index) } diff --git a/common/api.js b/common/api.js index 01fdcd5..6e783b4 100644 --- a/common/api.js +++ b/common/api.js @@ -9,6 +9,22 @@ import UniAppWebSocket from './uni-websocket' // ========================================== export const API_HOST = 'https://dev.wash-painting.cn' +// ========================================== +// 401 跳登录页防重锁:token 失效时多个接口会同时返回 401, +// 各自 reLaunch 会并发冲突导致 reLaunch:fail timeout,只放行第一次 +// ========================================== +let authRedirectLock = false +function redirectToLogin() { + if (authRedirectLock) return + authRedirectLock = true + setTimeout(() => { authRedirectLock = false }, 3000) + const pages = getCurrentPages() + const current = pages.length ? pages[pages.length - 1].route : '' + // 已在登录页则不重复跳转 + if (current === 'pages/login/login') return + uni.reLaunch({ url: '/pages/login/login', fail: () => {} }) +} + // ========================================== // HTTP 请求函数(供 SDK 内部调用) // 参考 goodBooth 项目的 fetchsomething @@ -49,7 +65,7 @@ function httpRequest(url, params) { uni.removeStorageSync('refresh_token') uni.removeStorageSync('is_logged_in') if (!silent) { - uni.reLaunch({ url: '/pages/login/login' }) + redirectToLogin() uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2500 }) } reject({ msg: '登录已过期,请重新登录', code }) diff --git a/common/constants.js b/common/constants.js index 66a60e4..f3852ac 100644 --- a/common/constants.js +++ b/common/constants.js @@ -38,16 +38,44 @@ export const BRANDS = { { name: '江小白', product: '表达瓶', degree: 40 } ], beer: [ + // 国产主流 { name: '青岛啤酒', product: '经典', degree: 4.3 }, + { name: '青岛啤酒', product: '全麦白啤', degree: 4.7 }, + { name: '青岛啤酒', product: '黑啤', degree: 5.5 }, { name: '雪花', product: '勇闯天涯', degree: 3.3 }, + { name: '雪花', product: '纯生', degree: 3.6 }, + { name: '雪花', product: 'superX', degree: 4.0 }, { name: '百威', product: '经典', degree: 5.0 }, + { name: '百威', product: '锦兔', degree: 5.0 }, + { name: '哈尔滨', product: '冰纯', degree: 4.5 }, + { name: '哈尔滨', product: '小麦王', degree: 3.6 }, + { name: '燕京', product: 'U8', degree: 2.5 }, + { name: '燕京', product: '原浆白啤', degree: 4.8 }, + { name: '乌苏', product: '大乌苏', degree: 4.0 }, + { name: '珠江', product: '纯生', degree: 3.6 }, + { name: '泰山', product: '7天原浆', degree: 4.7 }, + // 国际品牌 { name: '喜力', product: '星银', degree: 5.0 }, { name: '科罗娜', product: 'Extra', degree: 4.5 }, { name: '嘉士伯', product: '特醇', degree: 5.0 }, - { name: '燕京', product: 'U8', degree: 2.5 }, - { name: '哈尔滨', product: '冰纯', degree: 4.5 }, - { name: '精酿', product: '自选', degree: 6.0 }, - { name: '朝日', product: '超爽', degree: 5.0 } + { name: '朝日', product: '超爽', degree: 5.0 }, + { name: '麒麟', product: '一番榨', degree: 5.0 }, + { name: '三得利', product: '高端麦芽', degree: 5.5 }, + { name: '札幌', product: '黑标', degree: 5.0 }, + { name: '虎牌', product: '经典', degree: 5.0 }, + { name: '生力', product: '清爽', degree: 5.0 }, + // 德式/白啤 + { name: '福佳', product: '白啤', degree: 4.9 }, + { name: '1664', product: '白啤', degree: 5.0 }, + { name: '柏龙', product: '小麦啤', degree: 5.5 }, + { name: '艾丁格', product: '小麦啤', degree: 5.3 }, + // 精酿 + { name: '鹅岛', product: 'IPA', degree: 5.9 }, + { name: '熊猫精酿', product: '蜂蜜生姜艾尔', degree: 5.0 }, + { name: '京A', product: '工人淡色艾尔', degree: 5.0 }, + { name: '拳击猫', product: 'TKO IPA', degree: 6.5 }, + { name: '高大师', product: '婴儿肥IPA', degree: 5.6 }, + { name: '精酿', product: '自选', degree: 6.0 } ], wine: [ { name: '拉菲', product: '传奇', degree: 13 }, diff --git a/pages/index/index.vue b/pages/index/index.vue index cea7497..3f398ce 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -190,10 +190,10 @@ export default { } }, onShow() { - // 首次启动跳转引导页 + // 首次启动跳转引导页由 App.vue onLaunch 统一处理, + // 这里不再重复 reLaunch,避免两个跳转并发导致 reLaunch:fail timeout const isFirst = uni.getStorageSync('is_first_launch') if (isFirst === 'true') { - uni.reLaunch({ url: '/pages/onboarding/onboarding' }) return } this.loadData() diff --git a/pages/record/record.vue b/pages/record/record.vue index 16dca2f..e7eb7cb 100644 --- a/pages/record/record.vue +++ b/pages/record/record.vue @@ -48,13 +48,18 @@ 品牌 - {{ brand.name }} + + {{ brandList.filter(b => b.name === brand.name).length > 1 ? brand.name + '·' + brand.product : brand.name }} + + + + {{ showAllBrands ? '收起 ▴' : '更多 ▾' }} @@ -81,14 +86,16 @@ - + {{ currentDrink.unit === 'ml' ? 'ml' : currentDrink.unit }} @@ -97,6 +104,8 @@ class="degree-input text-num" type="digit" v-model="degreeStr" + placeholder="度数" + placeholder-class="input-placeholder" @input="onAmountChange" /> ° @@ -339,7 +348,10 @@ export default { currentDrink: { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 }, drinks: [], brandList: [], + showAllBrands: false, + maxVisibleBrands: 8, amountStr: '', + amountFocused: false, degreeStr: '', // Step 2 @@ -363,6 +375,10 @@ export default { } }, computed: { + // 品牌列表默认只展示前 maxVisibleBrands 个,点“更多”后展开全部 + visibleBrands() { + return this.showAllBrands ? this.brandList : this.brandList.slice(0, this.maxVisibleBrands) + }, currentCups() { const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit) return calcStandardCups(ml, parseFloat(this.degreeStr) || 0) @@ -423,6 +439,7 @@ export default { this.currentDrink.degree = cat.defaultDegree this.degreeStr = String(cat.defaultDegree) this.brandList = BRANDS[cat.id] || [] + this.showAllBrands = false }, selectBrand(brand) { this.currentDrink.brand = brand.name @@ -522,6 +539,7 @@ export default { this.amountStr = '' this.degreeStr = '' this.brandList = [] + this.showAllBrands = false }, async publishRecord() { const recordData = { @@ -725,6 +743,13 @@ export default { margin-bottom: $sp-md; } +/* 更多/收起按钮:虚线边框与普通品牌标签区分 */ +.tag-more { + border: 2rpx dashed var(--border-faint, rgba(255, 255, 255, 0.2)); + background: transparent; + color: $amber; +} + .input-row { margin-bottom: $sp-sm; } @@ -767,9 +792,19 @@ export default { .amount-inline { display: flex; - align-items: baseline; + align-items: center; gap: $sp-sm; flex: 1; + background: $bg-elevated; + border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.12)); + border-radius: $radius-md; + padding: $sp-xs $sp-lg; + transition: border-color $duration-fast $ease-out; +} + +/* 聚焦时高亮边框,强化可输入感知(小程序不支持 :has(),由容器类控制) */ +.amount-focus-wrap { + border-color: $amber; } .degree-inline { @@ -796,20 +831,27 @@ export default { } .amount-input { - font-size: $fs-3xl; + font-size: $fs-2xl; font-weight: $fw-black; color: $text-primary; flex: 1; background: transparent; border: none; padding: 0; - height: 80rpx; - line-height: 80rpx; + height: 72rpx; + line-height: 72rpx; +} + +.amount-placeholder { + color: $text-tertiary; + font-size: $fs-base; + font-weight: $fw-normal; } .amount-unit { font-size: $fs-md; color: $text-secondary; + flex-shrink: 0; } .unit-switcher { @@ -849,11 +891,13 @@ export default { } .degree-input { - width: 100rpx; + width: 120rpx; text-align: center; background: $bg-elevated; border-radius: $radius-md; + border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.12)); padding: $sp-sm; + height: 72rpx; color: $text-primary; font-size: $fs-lg; }