处理 token 过期问题
This commit is contained in:
+84
@@ -2,6 +2,19 @@
|
||||
// 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 || {},
|
||||
@@ -17,7 +30,41 @@ 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();
|
||||
@@ -25,6 +72,43 @@ export function request(options){//封装的requset
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 重新登录方法
|
||||
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 || {};
|
||||
|
||||
@@ -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>
|
||||
|
||||
+11
-1
@@ -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 {
|
||||
// 有 token,直接加载数据
|
||||
// 如果 token 过期,API 层会自动重新登录
|
||||
this.bannerListfn()
|
||||
this.$refs.paging && this.$refs.paging.reload()
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user