fix(app): 解决页面跳转冲突和登录重定向问题
- 在App.vue中延迟引导页跳转以避免首页未就绪导致的timeout错误 - 添加401跳登录页防重锁机制,防止多个接口同时返回401时的并发冲突 - 移除index.vue中的重复跳转逻辑,统一由App.vue处理首次启动跳转 - 优化record.vue中的品牌选择界面,添加"更多"展开功能和视觉改进 - 改进用量和度数输入框的样式和交互体验
This commit is contained in:
@@ -74,7 +74,10 @@ export default {
|
|||||||
const isFirst = uni.getStorageSync('is_first_launch')
|
const isFirst = uni.getStorageSync('is_first_launch')
|
||||||
if (!isFirst || isFirst === 'true') {
|
if (!isFirst || isFirst === 'true') {
|
||||||
// 首次使用 → 引导页
|
// 首次使用 → 引导页
|
||||||
uni.reLaunch({ url: '/pages/onboarding/onboarding' })
|
// 延迟一帧再跳:启动早期首页还未就绪,立即 reLaunch 可能报 timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: '/pages/onboarding/onboarding', fail: () => {} })
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
// 否则正常进入首页(index)
|
// 否则正常进入首页(index)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -9,6 +9,22 @@ import UniAppWebSocket from './uni-websocket'
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
export const API_HOST = 'https://dev.wash-painting.cn'
|
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 内部调用)
|
// HTTP 请求函数(供 SDK 内部调用)
|
||||||
// 参考 goodBooth 项目的 fetchsomething
|
// 参考 goodBooth 项目的 fetchsomething
|
||||||
@@ -49,7 +65,7 @@ function httpRequest(url, params) {
|
|||||||
uni.removeStorageSync('refresh_token')
|
uni.removeStorageSync('refresh_token')
|
||||||
uni.removeStorageSync('is_logged_in')
|
uni.removeStorageSync('is_logged_in')
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
uni.reLaunch({ url: '/pages/login/login' })
|
redirectToLogin()
|
||||||
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2500 })
|
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2500 })
|
||||||
}
|
}
|
||||||
reject({ msg: '登录已过期,请重新登录', code })
|
reject({ msg: '登录已过期,请重新登录', code })
|
||||||
|
|||||||
+32
-4
@@ -38,16 +38,44 @@ export const BRANDS = {
|
|||||||
{ name: '江小白', product: '表达瓶', degree: 40 }
|
{ name: '江小白', product: '表达瓶', degree: 40 }
|
||||||
],
|
],
|
||||||
beer: [
|
beer: [
|
||||||
|
// 国产主流
|
||||||
{ name: '青岛啤酒', product: '经典', degree: 4.3 },
|
{ name: '青岛啤酒', product: '经典', degree: 4.3 },
|
||||||
|
{ name: '青岛啤酒', product: '全麦白啤', degree: 4.7 },
|
||||||
|
{ name: '青岛啤酒', product: '黑啤', degree: 5.5 },
|
||||||
{ name: '雪花', product: '勇闯天涯', degree: 3.3 },
|
{ 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: 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: '星银', degree: 5.0 },
|
||||||
{ name: '科罗娜', product: 'Extra', degree: 4.5 },
|
{ name: '科罗娜', product: 'Extra', degree: 4.5 },
|
||||||
{ name: '嘉士伯', product: '特醇', degree: 5.0 },
|
{ name: '嘉士伯', product: '特醇', degree: 5.0 },
|
||||||
{ name: '燕京', product: 'U8', degree: 2.5 },
|
{ name: '朝日', product: '超爽', degree: 5.0 },
|
||||||
{ name: '哈尔滨', product: '冰纯', degree: 4.5 },
|
{ name: '麒麟', product: '一番榨', degree: 5.0 },
|
||||||
{ name: '精酿', product: '自选', degree: 6.0 },
|
{ name: '三得利', product: '高端麦芽', degree: 5.5 },
|
||||||
{ name: '朝日', product: '超爽', degree: 5.0 }
|
{ 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: [
|
wine: [
|
||||||
{ name: '拉菲', product: '传奇', degree: 13 },
|
{ name: '拉菲', product: '传奇', degree: 13 },
|
||||||
|
|||||||
@@ -190,10 +190,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
// 首次启动跳转引导页
|
// 首次启动跳转引导页由 App.vue onLaunch 统一处理,
|
||||||
|
// 这里不再重复 reLaunch,避免两个跳转并发导致 reLaunch:fail timeout
|
||||||
const isFirst = uni.getStorageSync('is_first_launch')
|
const isFirst = uni.getStorageSync('is_first_launch')
|
||||||
if (isFirst === 'true') {
|
if (isFirst === 'true') {
|
||||||
uni.reLaunch({ url: '/pages/onboarding/onboarding' })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.loadData()
|
this.loadData()
|
||||||
|
|||||||
+56
-12
@@ -48,13 +48,18 @@
|
|||||||
<text class="section-label">品牌</text>
|
<text class="section-label">品牌</text>
|
||||||
<view class="brand-tags">
|
<view class="brand-tags">
|
||||||
<view
|
<view
|
||||||
v-for="brand in brandList"
|
v-for="brand in visibleBrands"
|
||||||
:key="brand.name"
|
:key="brand.name + brand.product"
|
||||||
class="tag"
|
class="tag"
|
||||||
:class="{ 'tag-active': currentDrink.brand === brand.name }"
|
:class="{ 'tag-active': currentDrink.brand === brand.name && currentDrink.product === brand.product }"
|
||||||
@click="selectBrand(brand)"
|
@click="selectBrand(brand)"
|
||||||
>
|
>
|
||||||
<text>{{ brand.name }}</text>
|
<!-- 同品牌多产品时展示产品名以便区分 -->
|
||||||
|
<text>{{ brandList.filter(b => b.name === brand.name).length > 1 ? brand.name + '·' + brand.product : brand.name }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 更多:默认只展示前几个品牌,点击展开全部 -->
|
||||||
|
<view v-if="brandList.length > maxVisibleBrands" class="tag tag-more" @click="showAllBrands = !showAllBrands">
|
||||||
|
<text>{{ showAllBrands ? '收起 ▴' : '更多 ▾' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="input-row">
|
<view class="input-row">
|
||||||
@@ -81,14 +86,16 @@
|
|||||||
|
|
||||||
<!-- 用量 + 度数 一行 -->
|
<!-- 用量 + 度数 一行 -->
|
||||||
<view class="amount-degree-row">
|
<view class="amount-degree-row">
|
||||||
<view class="amount-inline">
|
<view class="amount-inline" :class="{ 'amount-focus-wrap': amountFocused }">
|
||||||
<input
|
<input
|
||||||
class="amount-input text-num"
|
class="amount-input text-num"
|
||||||
type="digit"
|
type="digit"
|
||||||
v-model="amountStr"
|
v-model="amountStr"
|
||||||
placeholder="0"
|
placeholder="输入用量"
|
||||||
placeholder-class="input-placeholder"
|
placeholder-class="amount-placeholder"
|
||||||
@input="onAmountChange"
|
@input="onAmountChange"
|
||||||
|
@focus="amountFocused = true"
|
||||||
|
@blur="amountFocused = false"
|
||||||
/>
|
/>
|
||||||
<text class="amount-unit">{{ currentDrink.unit === 'ml' ? 'ml' : currentDrink.unit }}</text>
|
<text class="amount-unit">{{ currentDrink.unit === 'ml' ? 'ml' : currentDrink.unit }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -97,6 +104,8 @@
|
|||||||
class="degree-input text-num"
|
class="degree-input text-num"
|
||||||
type="digit"
|
type="digit"
|
||||||
v-model="degreeStr"
|
v-model="degreeStr"
|
||||||
|
placeholder="度数"
|
||||||
|
placeholder-class="input-placeholder"
|
||||||
@input="onAmountChange"
|
@input="onAmountChange"
|
||||||
/>
|
/>
|
||||||
<text class="degree-symbol">°</text>
|
<text class="degree-symbol">°</text>
|
||||||
@@ -339,7 +348,10 @@ export default {
|
|||||||
currentDrink: { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 },
|
currentDrink: { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 },
|
||||||
drinks: [],
|
drinks: [],
|
||||||
brandList: [],
|
brandList: [],
|
||||||
|
showAllBrands: false,
|
||||||
|
maxVisibleBrands: 8,
|
||||||
amountStr: '',
|
amountStr: '',
|
||||||
|
amountFocused: false,
|
||||||
degreeStr: '',
|
degreeStr: '',
|
||||||
|
|
||||||
// Step 2
|
// Step 2
|
||||||
@@ -363,6 +375,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// 品牌列表默认只展示前 maxVisibleBrands 个,点“更多”后展开全部
|
||||||
|
visibleBrands() {
|
||||||
|
return this.showAllBrands ? this.brandList : this.brandList.slice(0, this.maxVisibleBrands)
|
||||||
|
},
|
||||||
currentCups() {
|
currentCups() {
|
||||||
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
|
const ml = unitToMl(parseFloat(this.amountStr) || 0, this.currentDrink.unit)
|
||||||
return calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
|
return calcStandardCups(ml, parseFloat(this.degreeStr) || 0)
|
||||||
@@ -423,6 +439,7 @@ export default {
|
|||||||
this.currentDrink.degree = cat.defaultDegree
|
this.currentDrink.degree = cat.defaultDegree
|
||||||
this.degreeStr = String(cat.defaultDegree)
|
this.degreeStr = String(cat.defaultDegree)
|
||||||
this.brandList = BRANDS[cat.id] || []
|
this.brandList = BRANDS[cat.id] || []
|
||||||
|
this.showAllBrands = false
|
||||||
},
|
},
|
||||||
selectBrand(brand) {
|
selectBrand(brand) {
|
||||||
this.currentDrink.brand = brand.name
|
this.currentDrink.brand = brand.name
|
||||||
@@ -522,6 +539,7 @@ export default {
|
|||||||
this.amountStr = ''
|
this.amountStr = ''
|
||||||
this.degreeStr = ''
|
this.degreeStr = ''
|
||||||
this.brandList = []
|
this.brandList = []
|
||||||
|
this.showAllBrands = false
|
||||||
},
|
},
|
||||||
async publishRecord() {
|
async publishRecord() {
|
||||||
const recordData = {
|
const recordData = {
|
||||||
@@ -725,6 +743,13 @@ export default {
|
|||||||
margin-bottom: $sp-md;
|
margin-bottom: $sp-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 更多/收起按钮:虚线边框与普通品牌标签区分 */
|
||||||
|
.tag-more {
|
||||||
|
border: 2rpx dashed var(--border-faint, rgba(255, 255, 255, 0.2));
|
||||||
|
background: transparent;
|
||||||
|
color: $amber;
|
||||||
|
}
|
||||||
|
|
||||||
.input-row {
|
.input-row {
|
||||||
margin-bottom: $sp-sm;
|
margin-bottom: $sp-sm;
|
||||||
}
|
}
|
||||||
@@ -767,9 +792,19 @@ export default {
|
|||||||
|
|
||||||
.amount-inline {
|
.amount-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: $sp-sm;
|
gap: $sp-sm;
|
||||||
flex: 1;
|
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 {
|
.degree-inline {
|
||||||
@@ -796,20 +831,27 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.amount-input {
|
.amount-input {
|
||||||
font-size: $fs-3xl;
|
font-size: $fs-2xl;
|
||||||
font-weight: $fw-black;
|
font-weight: $fw-black;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 80rpx;
|
height: 72rpx;
|
||||||
line-height: 80rpx;
|
line-height: 72rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-placeholder {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: $fs-base;
|
||||||
|
font-weight: $fw-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-unit {
|
.amount-unit {
|
||||||
font-size: $fs-md;
|
font-size: $fs-md;
|
||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-switcher {
|
.unit-switcher {
|
||||||
@@ -849,11 +891,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.degree-input {
|
.degree-input {
|
||||||
width: 100rpx;
|
width: 120rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: $bg-elevated;
|
background: $bg-elevated;
|
||||||
border-radius: $radius-md;
|
border-radius: $radius-md;
|
||||||
|
border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.12));
|
||||||
padding: $sp-sm;
|
padding: $sp-sm;
|
||||||
|
height: 72rpx;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-size: $fs-lg;
|
font-size: $fs-lg;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user