fix: 修复登录环境选择器选中框错位并升至1.1.16

This commit is contained in:
2026-10-31 22:20:44 +08:00
parent 640dcf3493
commit 7adec1b960
4 changed files with 608 additions and 14 deletions
+589
View File
@@ -0,0 +1,589 @@
<template>
<view class="u-select">
<u-popup :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex">
<view class="u-select">
<view class="u-select__header" @touchmove.stop.prevent="">
<view
class="u-select__header__cancel u-select__header__btn"
:style="{ color: cancelColor }"
hover-class="u-hover-class"
:hover-stay-time="150"
@tap="getResult('cancel')"
>
{{cancelText}}
</view>
<view class="u-select__header__title">
{{title}}
</view>
<view
class="u-select__header__confirm u-select__header__btn"
:style="{ color: moving ? cancelColor : confirmColor }"
hover-class="u-hover-class"
:hover-stay-time="150"
@touchmove.stop=""
@tap.stop="getResult('confirm')"
>
{{confirmText}}
</view>
</view>
<view class="u-select__search" v-if="showSearch" @tap.stop>
<u-icon name="search" size="32" color="#909399"></u-icon>
<input
class="u-select__search__input"
:value="keyword"
:placeholder="searchPlaceholder"
confirm-type="search"
:adjust-position="true"
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<u-icon
v-if="keyword"
name="close-circle-fill"
size="28"
color="#c0c4cc"
@tap.stop="clearSearch"
></u-icon>
</view>
<view v-if="loading" class="u-select__loading">搜索中...</view>
<view class="u-select__body" :style="hasCustomItemHeight ? bodyStyle : null">
<view v-if="isColumnEmpty && !loading" class="u-select__empty">无匹配选项</view>
<picker-view
v-else-if="value && !isColumnEmpty"
:key="pickerKey"
@change="columnChange"
class="u-select__body__picker-view"
:style="pickerViewStyle"
:indicator-style="indicatorStyle"
:value="defaultSelector"
@pickstart="pickstart"
@pickend="pickend"
>
<picker-view-column v-for="(item, index) in columnData" :key="'col-' + pickerKey + '-' + index">
<view
class="u-select__body__picker-view__item"
:style="hasCustomItemHeight ? itemStyle : null"
v-for="(item1, index1) in item"
:key="item1[valueName] != null ? item1[valueName] : index1"
>
<view class="u-line-1" :style="hasCustomItemHeight ? itemStyle : null">{{ item1[labelName] }}</view>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* select 列选择器(本地覆盖:选项支持搜索)
* @description 此选择器用于单列,多列,多列联动的选择场景。
* @tutorial http://uviewui.com/components/select.html
* @property {String} mode 模式选择,"single-column"-单列模式,"mutil-column"-多列模式,"mutil-column-auto"-多列联动模式
* @property {Array} list 列数据,数组形式,见官网说明
* @property {Boolean} v-model 布尔值变量,用于控制选择器的弹出与收起
* @property {Boolean} show-search 是否显示搜索框(默认true)
* @property {String} search-placeholder 搜索框占位文字
* @event {Function} confirm 点击确定按钮,返回当前选择的值
* @example <u-select v-model="show" :list="list"></u-select>
*/
export default {
props: {
list: {
type: Array,
default() {
return [];
}
},
border: {
type: Boolean,
default: true
},
value: {
type: Boolean,
default: false
},
cancelColor: {
type: String,
default: '#606266'
},
confirmColor: {
type: String,
default: '#2979ff'
},
zIndex: {
type: [String, Number],
default: 0
},
safeAreaInsetBottom: {
type: Boolean,
default: false
},
maskCloseAble: {
type: Boolean,
default: true
},
defaultValue: {
type: Array,
default() {
return [0];
}
},
mode: {
type: String,
default: 'single-column'
},
valueName: {
type: String,
default: 'value'
},
labelName: {
type: String,
default: 'label'
},
childName: {
type: String,
default: 'children'
},
title: {
type: String,
default: ''
},
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确认'
},
showSearch: {
type: Boolean,
default: true
},
searchPlaceholder: {
type: String,
default: '搜索'
},
remote: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
debounce: {
type: Number,
default: 400
},
/** 选项行高(rpx)。App 端需与 indicator 同步,避免选中框错位 */
itemHeight: {
type: [Number, String],
default: 0
},
/** 可见行数,用于计算滚轮高度;需为奇数,选中行才能居中 */
visibleItemCount: {
type: [Number, String],
default: 5
}
},
data() {
return {
defaultSelector: [0],
columnData: [],
selectValue: [],
lastSelectIndex: [],
columnNum: 0,
moving: false,
keyword: '',
pickerKey: 0,
// 勿用 _ 前缀:Vue2 不会把 data 代理到 this
searchTimer: null,
lastSearchEmit: '',
lastSearchAt: 0
};
},
watch: {
value: {
immediate: true,
handler(val) {
if (val) {
this.keyword = '';
this.applyPickerData();
if (this.remote) this.emitRemoteSearch('');
} else {
this.clearSearchTimer();
this.lastSearchEmit = '';
this.lastSearchAt = 0;
}
}
},
list: {
handler() {
if (!this.value) return;
this.applyPickerData();
},
},
loading(val) {
// loading 从 true→false 时再刷一次滚轮,避免原生 picker-view 漏更
if (!val && this.value && this.remote) this.applyPickerData();
},
},
beforeDestroy() {
this.clearSearchTimer();
},
computed: {
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
},
isColumnEmpty() {
return !this.columnData || !this.columnData.length || !this.columnData[0] || !this.columnData[0].length;
},
hasCustomItemHeight() {
return Number(this.itemHeight) > 0;
},
itemHeightRpx() {
return Number(this.itemHeight) || 70;
},
itemHeightPx() {
return uni.upx2px(this.itemHeightRpx);
},
visibleCount() {
const n = Number(this.visibleItemCount) || 5;
return n % 2 === 0 ? n + 1 : n;
},
indicatorStyle() {
if (!this.hasCustomItemHeight) return '';
return `height: ${this.itemHeightPx}px;`;
},
itemStyle() {
return {
height: `${this.itemHeightPx}px`,
lineHeight: `${this.itemHeightPx}px`,
};
},
pickerViewStyle() {
if (!this.hasCustomItemHeight) return null;
return { height: `${this.itemHeightPx * this.visibleCount}px` };
},
bodyStyle() {
return this.pickerViewStyle;
},
},
methods: {
pickstart() {
// #ifdef MP-WEIXIN
this.moving = true;
// #endif
},
pickend() {
// #ifdef MP-WEIXIN
this.moving = false;
// #endif
},
readInputValue(e) {
if (typeof e === 'string' || typeof e === 'number') return String(e);
if (e && e.detail && e.detail.value != null) return String(e.detail.value);
if (e && e.target && e.target.value != null) return String(e.target.value);
return '';
},
clearSearchTimer() {
if (this.searchTimer) {
clearTimeout(this.searchTimer);
this.searchTimer = null;
}
},
emitRemoteSearch(keyword) {
this.clearSearchTimer();
const key = keyword == null ? '' : String(keyword).trim();
const now = Date.now();
if (key === this.lastSearchEmit && now - this.lastSearchAt < 80) return;
this.lastSearchEmit = key;
this.lastSearchAt = now;
this.$emit('search', key);
},
scheduleRemoteSearch() {
this.clearSearchTimer();
this.searchTimer = setTimeout(() => {
this.emitRemoteSearch(this.keyword);
}, this.debounce);
},
onSearchInput(e) {
this.keyword = this.readInputValue(e);
if (this.remote) this.scheduleRemoteSearch();
else this.applyFilter();
},
onSearchConfirm(e) {
this.keyword = this.readInputValue(e);
if (this.remote) this.emitRemoteSearch(this.keyword);
else this.applyFilter();
},
clearSearch() {
this.keyword = '';
if (this.remote) this.emitRemoteSearch('');
else this.applyFilter();
},
matchKeyword(text) {
const keyword = (this.keyword || '').trim().toLowerCase();
if (!keyword) return true;
return String(text == null ? '' : text).toLowerCase().indexOf(keyword) !== -1;
},
filterItems(list) {
if (!Array.isArray(list)) return [];
if (this.remote) return list.slice();
const keyword = (this.keyword || '').trim();
if (!keyword) return list.slice();
return list.filter(item => {
return this.matchKeyword(item && item[this.labelName]) || this.matchKeyword(item && item[this.valueName]);
});
},
applyPickerData() {
this.init();
this.pickerKey += 1;
},
applyFilter() {
this.applyPickerData();
},
init() {
this.setColumnNum();
this.setDefaultSelector();
this.setColumnData();
this.setSelectValue();
},
setDefaultSelector() {
const columnNum = this.columnNum || 1;
const defaultValue = Array.isArray(this.defaultValue) ? this.defaultValue.slice() : [];
this.defaultSelector = defaultValue.length === columnNum
? defaultValue
: Array(columnNum).fill(0);
this.lastSelectIndex = this.defaultSelector.slice();
},
setColumnNum() {
if(this.mode == 'single-column') this.columnNum = 1;
else if(this.mode == 'mutil-column') this.columnNum = this.list.length;
else if(this.mode == 'mutil-column-auto') {
let num = 1;
let column = this.list;
while(column[0] && column[0][this.childName]) {
column = column[0] ? column[0][this.childName] : {};
num ++;
}
this.columnNum = num;
}
},
setColumnData() {
let data = [];
this.selectValue = [];
if(this.mode == 'mutil-column-auto') {
const sourceList = this.filterItems(this.list);
if (!sourceList.length) {
this.columnData = [[]];
return;
}
let selector = this.defaultSelector.length ? this.defaultSelector[0] : 0;
if (selector >= sourceList.length) selector = 0;
let column = sourceList[selector];
for (let i = 0; i < this.columnNum; i++) {
if (i == 0) {
data[i] = sourceList;
column = column ? column[this.childName] : [];
} else {
data[i] = column || [];
const nextIndex = this.defaultSelector[i] || 0;
column = (column && column[nextIndex]) ? column[nextIndex][this.childName] : [];
}
}
} else if(this.mode == 'single-column') {
data[0] = this.filterItems(this.list);
} else {
data = (this.list || []).map(col => this.filterItems(Array.isArray(col) ? col : []));
}
this.columnData = data.slice();
},
setSelectValue() {
let tmp = null;
for(let i = 0; i < this.columnNum; i++) {
const col = this.columnData[i] || [];
tmp = col[this.defaultSelector[i]];
let data = {
value: tmp ? tmp[this.valueName] : null,
label: tmp ? tmp[this.labelName] : null
};
if(tmp && tmp.extra !== undefined) data.extra = tmp.extra;
this.selectValue.push(data)
}
},
columnChange(e) {
let index = null;
let columnIndex = e.detail.value;
this.selectValue = [];
this.defaultSelector = columnIndex;
if(this.mode == 'mutil-column-auto') {
this.lastSelectIndex.map((val, idx) => {
if (val != columnIndex[idx]) index = idx;
});
for (let i = index + 1; i < this.columnNum; i++) {
this.columnData[i] = this.columnData[i - 1][i - 1 == index ? columnIndex[index] : 0][this.childName];
this.defaultSelector[i] = 0;
}
columnIndex.map((item, idx) => {
let data = this.columnData[idx][columnIndex[idx]];
let tmp = {
value: data ? data[this.valueName] : null,
label: data ? data[this.labelName] : null,
};
if(data && data.extra !== undefined) tmp.extra = data.extra;
this.selectValue.push(tmp);
})
this.lastSelectIndex = columnIndex;
} else if(this.mode == 'single-column') {
let data = this.columnData[0][columnIndex[0]];
let tmp = {
value: data ? data[this.valueName] : null,
label: data ? data[this.labelName] : null,
};
if(data && data.extra !== undefined) tmp.extra = data.extra;
this.selectValue.push(tmp);
} else if(this.mode == 'mutil-column') {
columnIndex.map((item, idx) => {
let data = this.columnData[idx][columnIndex[idx]];
let tmp = {
value: data ? data[this.valueName] : null,
label: data ? data[this.labelName] : null,
};
if(data && data.extra !== undefined) tmp.extra = data.extra;
this.selectValue.push(tmp);
})
}
},
close() {
this.$emit('input', false);
this.$set(this, 'defaultSelector', [0]);
this.keyword = '';
this.lastSearchEmit = '';
this.lastSearchAt = 0;
},
getResult(event = null) {
// #ifdef MP-WEIXIN
if (this.moving) return;
// #endif
if (event === 'confirm' && this.isColumnEmpty) {
this.$u.toast && this.$u.toast('无匹配选项');
return;
}
if (event) this.$emit(event, this.selectValue);
this.close();
},
selectHandler() {
this.$emit('click');
}
}
};
</script>
<style scoped lang="scss">
@import "~uview-ui/libs/css/style.components.scss";
.u-select {
&__action {
position: relative;
line-height: $u-form-item-height;
height: $u-form-item-height;
&__icon {
position: absolute;
right: 20rpx;
top: 50%;
transition: transform .4s;
transform: translateY(-50%);
z-index: 1;
&--reverse {
transform: rotate(-180deg) translateY(50%);
}
}
}
&__hader {
&__title {
color: $u-content-color;
}
}
&--border {
border-radius: 6rpx;
border-radius: 4px;
border: 1px solid $u-form-item-border-color;
}
&__header {
@include vue-flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 40rpx;
}
&__search {
@include vue-flex;
align-items: center;
margin: 0 24rpx 16rpx;
padding: 0 24rpx;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
box-sizing: border-box;
&__input {
flex: 1;
height: 80rpx;
margin-left: 12rpx;
font-size: 30rpx;
color: #303133;
}
}
&__loading {
padding: 0 24rpx 8rpx;
font-size: 26rpx;
color: #909399;
}
&__empty {
@include vue-flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 30rpx;
color: #909399;
}
&__body {
width: 100%;
height: 500rpx;
overflow: hidden;
background-color: #fff;
&__picker-view {
height: 100%;
box-sizing: border-box;
&__item {
@include vue-flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $u-main-color;
padding: 0 8rpx;
}
}
}
}
</style>
+2 -2
View File
@@ -2,8 +2,8 @@
"name" : "浩拓织厂", "name" : "浩拓织厂",
"appid" : "__UNI__F79F300", "appid" : "__UNI__F79F300",
"description" : "周尚-浩拓织厂", "description" : "周尚-浩拓织厂",
"versionName" : "1.1.15", "versionName" : "1.1.16",
"versionCode" : 115, "versionCode" : 116,
"transformPx" : false, "transformPx" : false,
"app-plus" : { "app-plus" : {
"optimization" : { "optimization" : {
+1
View File
@@ -1,5 +1,6 @@
{ {
"easycom": { "easycom": {
"^u-select$": "@/components/u-select/u-select.vue",
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
}, },
"pages": [ "pages": [
+16 -12
View File
@@ -104,8 +104,9 @@
<!-- </view>--> <!-- </view>-->
<!-- </view>--> <!-- </view>-->
<!-- </div>--> <!-- </div>-->
<u-picker class="login-env-picker" v-model="showApiUrlPicker" mode="selector" :range="apiUrlList" range-key="label" <u-select class="login-env-picker" v-model="showApiUrlPicker" :list="apiUrlList" :default-value="apiUrlDefaultIndex"
@confirm="onApiUrlConfirm" @cancel="showApiUrlPicker = false"></u-picker> title="选择服务器" :show-search="false" :item-height="88" :visible-item-count="5"
@confirm="onApiUrlConfirm" @cancel="showApiUrlPicker = false"></u-select>
<!-- 版本信息显示 --> <!-- 版本信息显示 -->
<view class="version-info"> <view class="version-info">
<text class="version-text">版本: {{ appVersion }}</text> <text class="version-text">版本: {{ appVersion }}</text>
@@ -165,6 +166,12 @@ export default {
}, },
envName() { envName() {
return process.env.VUE_APP_ENV_NAME || ''; return process.env.VUE_APP_ENV_NAME || '';
},
apiUrlDefaultIndex() {
const list = this.apiUrlList || [];
let index = list.findIndex(item => item.value === this.selectedApiUrl);
if (index < 0) index = list.findIndex(item => item.label === '正式环境');
return [index >= 0 ? index : 0];
} }
}, },
@@ -212,9 +219,8 @@ export default {
}, },
onApiUrlConfirm(e) { onApiUrlConfirm(e) {
console.log('e', JSON.stringify(e)); const selectedValue = e && e[0] && e[0].value;
const selectedValue = this.apiUrlList[e[0]].value; if (selectedValue == null) return;
console.log('selectedValue', selectedValue)
this.selectedApiUrl = selectedValue; this.selectedApiUrl = selectedValue;
this.showApiUrlPicker = false; this.showApiUrlPicker = false;
@@ -598,21 +604,19 @@ export default {
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.login-env-picker { .login-env-picker {
::v-deep .u-picker-header, ::v-deep .u-select__header,
::v-deep .u-btn-picker { ::v-deep .u-select__header__btn {
height: 100rpx; height: 100rpx;
font-size: 36rpx; font-size: 36rpx;
} }
::v-deep .u-picker-body { ::v-deep .u-select__header__title {
height: 560rpx; font-size: 36rpx;
} }
::v-deep .u-column-item, ::v-deep .u-select__body__picker-view__item,
::v-deep .u-line-1 { ::v-deep .u-line-1 {
height: 88rpx;
font-size: 40rpx; font-size: 40rpx;
line-height: 88rpx;
} }
} }
</style> </style>