处理 token 过期问题
This commit is contained in:
+85
-1
@@ -2,13 +2,26 @@
|
|||||||
// const baseUrl = "https://api.collect.xyh.muzili.top/"; //这里放置url域名32044
|
// const baseUrl = "https://api.collect.xyh.muzili.top/"; //这里放置url域名32044
|
||||||
const baseUrl = "https://bsh.info.hb.hnzhwlkj.cn/"; //这里放置url域名32044
|
const baseUrl = "https://bsh.info.hb.hnzhwlkj.cn/"; //这里放置url域名32044
|
||||||
// const baseUrl = "https://114.215.24.240:8106/api/v1/"; //这里放置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
|
export function request(options){//封装的requset
|
||||||
let { url,data,method } = options;
|
let { url,data,method } = options;
|
||||||
data = data || {},
|
data = data || {},
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: ''
|
title: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve,rejext)=>{
|
return new Promise((resolve,rejext)=>{
|
||||||
uni.request({//这里是uniapp网络请求的api
|
uni.request({//这里是uniapp网络请求的api
|
||||||
url: baseUrl + url, //仅为示例,并非真实接口地址。
|
url: baseUrl + url, //仅为示例,并非真实接口地址。
|
||||||
@@ -17,14 +30,85 @@ export function request(options){//封装的requset
|
|||||||
method:method||"GET",//默认请求方式是get
|
method:method||"GET",//默认请求方式是get
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
uni.hideLoading();
|
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)
|
resolve(res)
|
||||||
},
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
rejext(err)
|
||||||
|
},
|
||||||
complete: (data)=>{
|
complete: (data)=>{
|
||||||
uni.hideLoading();
|
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
|
export function requestx(options){//封装的requset
|
||||||
let { url,data,method } = options;
|
let { url,data,method } = options;
|
||||||
data = data || {};
|
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>
|
||||||
|
|
||||||
+12
-2
@@ -148,12 +148,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad() {
|
onLoad() {
|
||||||
if (!uni.getStorageSync('token')) {
|
// 检查是否有 token,没有则先登录
|
||||||
|
const token = uni.getStorageSync('token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// 没有 token,先登录
|
||||||
this.mini_login().then(() => {
|
this.mini_login().then(() => {
|
||||||
this.bannerListfn()
|
this.bannerListfn()
|
||||||
this.$refs.paging && this.$refs.paging.reload()
|
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.bannerListfn()
|
||||||
this.$refs.paging && this.$refs.paging.reload()
|
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