处理 token 过期问题

This commit is contained in:
浅念
2026-03-13 10:18:07 +08:00
parent 1fa9dfd61e
commit 4fbe2cd6b5
4 changed files with 389 additions and 3 deletions
+85 -1
View File
@@ -2,13 +2,26 @@
// const baseUrl = "https://api.collect.xyh.muzili.top/"; //这里放置url域名32044
const baseUrl = "https://bsh.info.hb.hnzhwlkj.cn/"; //这里放置url域名32044
// const baseUrl = "https://114.215.24.240:8106/api/v1/"; //这里放置url域名32044
// 是否正在重新登录中
let isRelogining = false
// 等待重新登录完成的队列
let loginPendingQueue = []
// 执行队列中的请求
function flushLoginPendingQueue() {
loginPendingQueue.forEach(callback => callback && callback())
loginPendingQueue = []
isRelogining = false
}
export function request(options){//封装的requset
let { url,data,method } = options;
data = data || {},
uni.showLoading({
title: ''
});
return new Promise((resolve,rejext)=>{
uni.request({//这里是uniapp网络请求的api
url: baseUrl + url, //仅为示例,并非真实接口地址。
@@ -17,14 +30,85 @@ export function request(options){//封装的requset
method:method||"GET",//默认请求方式是get
success: (res) => {
uni.hideLoading();
// 处理 token 过期情况 (根据后端返回的 code 判断)
// 常见的过期 code: 401, 403, 或后端自定义的 code
if (res.statusCode === 401 || res.statusCode === 403 ||
(res.data && (res.data.code === 401 || res.data.code === 403 || res.data.code === -100))) {
// 如果正在重新登录,将当前请求加入队列
if (isRelogining) {
loginPendingQueue.push(() => {
request(options).then(resolve).catch(rejext)
})
return
}
// 标记正在重新登录
isRelogining = true
// 重新登录
reLogin().then(() => {
// 登录成功后,重新发起当前请求
request(options).then(resolve).catch(rejext)
flushLoginPendingQueue()
}).catch((err) => {
// 登录失败,清空队列
flushLoginPendingQueue()
rejext(err)
})
return
}
resolve(res)
},
fail: (err) => {
uni.hideLoading();
rejext(err)
},
complete: (data)=>{
uni.hideLoading();
}
});
})
}
// 重新登录方法
function reLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (loginRes) => {
uni.request({
url: baseUrl + 'miniapp/login',
method: 'POST',
data: { code: loginRes.code },
header: {
'content-type': 'application/x-www-form-urlencoded'
},
success: (res) => {
if (res.data.code === 200) {
// 更新本地存储
uni.setStorageSync('openid', res.data.data.openid)
uni.setStorageSync('token', res.data.data.token)
uni.setStorageSync('user_id', res.data.data.id)
uni.setStorageSync('is_login', !!res.data.data.mobile)
resolve(res.data.data)
} else {
reject(res.data)
}
},
fail: (err) => {
reject(err)
}
})
},
fail: (err) => {
reject(err)
}
})
})
}
export function requestx(options){//封装的requset
let { url,data,method } = options;
data = data || {};
+292
View File
@@ -0,0 +1,292 @@
<template>
<view class="multi-select-search">
<view class="input-wrapper" @click.stop="toggleDropdown">
<view class="selected-tags" v-if="selectedItems.length > 0">
<view class="tag-item" v-for="(item, index) in selectedItems" :key="index">
<text class="tag-text">{{item.model}}</text>
<view class="tag-close" @click.stop="removeItem(index)">
<u-icon name="close" size="14" color="#999"></u-icon>
</view>
</view>
</view>
<input
class="search-input"
type="text"
v-model="searchText"
:placeholder="selectedItems.length > 0 ? '' : placeholder"
@input="onSearchInput"
@focus="onFocus"
@blur="onBlur"
@confirm="onConfirm"
/>
<view class="input-icon" @click.stop="toggleDropdown">
<u-icon :name="showDropdown ? 'arrow-up' : 'arrow-down'" size="16" color="#999"></u-icon>
</view>
</view>
<view class="dropdown" v-if="showDropdown" @click.stop>
<scroll-view scroll-y="true" class="dropdown-scroll">
<view
class="dropdown-item"
v-for="(item, index) in filteredOptions"
:key="index"
@click="selectItem(item)"
>
<view class="item-content">
<text>{{ item.model }}</text>
<u-icon v-if="isSelected(item)" name="checkmark" size="16" color="#206ff4"></u-icon>
</view>
</view>
<view class="dropdown-empty" v-if="filteredOptions.length === 0 && searchText">
<text class="empty-text">未找到匹配项,按回车键添加 {{ searchText }}</text>
</view>
<view class="dropdown-empty" v-if="filteredOptions.length === 0 && !searchText && options.length === 0">
<text class="empty-text">暂无选项,可直接输入型号</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'MultiSelectSearch',
props: {
value: {
type: Array,
default: () => []
},
options: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请输入型号'
}
},
data() {
return {
showDropdown: false,
searchText: '',
selectedItems: []
}
},
watch: {
value: {
handler(newVal) {
if (Array.isArray(newVal)) {
this.selectedItems = [...newVal]
}
},
immediate: true
}
},
computed: {
filteredOptions() {
// 先过滤掉已选中的项
const unselectedOptions = this.options.filter(item => !this.isSelected(item))
if (!this.searchText || !this.searchText.trim()) {
return unselectedOptions
}
const searchLower = this.searchText.trim().toLowerCase()
return unselectedOptions.filter(item => {
// 支持对象和字符串两种格式
const itemText = (typeof item === 'object' && item !== null && item.model)
? item.model
: String(item)
return itemText.toLowerCase().includes(searchLower)
})
}
},
methods: {
toggleDropdown() {
this.showDropdown = !this.showDropdown
},
onFocus() {
this.showDropdown = true
},
onBlur() {
setTimeout(() => {
this.showDropdown = false
}, 200)
},
onSearchInput() {
this.showDropdown = true
},
onConfirm() {
if (this.searchText && this.searchText.trim()) {
const trimmedText = this.searchText.trim()
// 如果用户直接输入,创建一个简单的对象或字符串
// 根据 options 的格式决定
const newItem = this.options.length > 0 && typeof this.options[0] === 'object'
? { model: trimmedText } // 如果 options 是对象数组,创建对象
: trimmedText // 否则使用字符串
if (!this.isSelected(newItem)) {
this.addItem(newItem)
this.searchText = ''
this.showDropdown = false
}
}
},
selectItem(item) {
this.addItem(item)
this.searchText = ''
this.showDropdown = false
},
addItem(item) {
if (!this.isSelected(item)) {
this.selectedItems.push(item)
this.$emit('input', [...this.selectedItems])
this.$emit('change', [...this.selectedItems])
}
},
removeItem(index) {
this.selectedItems.splice(index, 1)
this.$emit('input', [...this.selectedItems])
this.$emit('change', [...this.selectedItems])
},
isSelected(item) {
// 支持对象和字符串两种格式的比较
if (typeof item === 'object' && item !== null) {
// 如果是对象,比较 id 或 model
const itemId = item.id || item.model
return this.selectedItems.some(selected => {
if (typeof selected === 'object' && selected !== null) {
return (selected.id || selected.model) === itemId
}
return selected === itemId
})
} else {
// 如果是字符串,直接比较
return this.selectedItems.some(selected => {
if (typeof selected === 'object' && selected !== null) {
return (selected.id || selected.model) === item
}
return selected === item
})
}
}
}
}
</script>
<style lang="scss" scoped>
.multi-select-search {
position: relative;
width: 100%;
}
.input-wrapper {
position: relative;
min-height: 88rpx;
padding: 20rpx 80rpx 20rpx 24rpx;
border: 1px solid #DCDFE6;
border-radius: 12rpx;
background: #fff;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16rpx;
box-sizing: border-box;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-item {
display: flex;
align-items: center;
background: #F0F2F5;
border-radius: 8rpx;
padding: 8rpx 16rpx;
gap: 12rpx;
}
.tag-text {
font-size: 28rpx;
color: #2F323A;
}
.tag-close {
display: flex;
align-items: center;
cursor: pointer;
}
.search-input {
flex: 1;
min-width: 200rpx;
font-size: 28rpx;
color: #2F323A;
height: 56rpx;
line-height: 56rpx;
}
.input-icon {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
}
.dropdown {
position: absolute;
top: calc(100% + 16rpx);
left: 0;
right: 0;
background: #fff;
border: 1px solid #EBEEF5;
border-radius: 12rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.1);
z-index: 1000;
max-height: 600rpx;
}
.dropdown-scroll {
max-height: 600rpx;
}
.dropdown-item {
padding: 24rpx 32rpx;
border-bottom: 1px solid #F0F2F5;
cursor: pointer;
&:last-child {
border-bottom: none;
}
&:active {
background: #F7F8FB;
}
}
.item-content {
display: flex;
align-items: center;
justify-content: space-between;
text {
font-size: 28rpx;
color: #2F323A;
}
}
.dropdown-empty {
padding: 24rpx 32rpx;
text-align: center;
}
.empty-text {
font-size: 24rpx;
color: #999;
}
</style>
+12 -2
View File
@@ -148,12 +148,22 @@
}
},
onLoad() {
if (!uni.getStorageSync('token')) {
// 检查是否有 token,没有则先登录
const token = uni.getStorageSync('token');
if (!token) {
// 没有 token,先登录
this.mini_login().then(() => {
this.bannerListfn()
this.$refs.paging && this.$refs.paging.reload()
}).catch(() => {
// 登录失败也加载数据(可能是游客模式)
this.bannerListfn()
this.$refs.paging && this.$refs.paging.reload()
})
}else{
} else {
// 有 token,直接加载数据
// 如果 token 过期,API 层会自动重新登录
this.bannerListfn()
this.$refs.paging && this.$refs.paging.reload()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB