init
This commit is contained in:
341
components/inlineSelect/inlineSelect.vue
Normal file
341
components/inlineSelect/inlineSelect.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<view
|
||||
class="inline-select"
|
||||
:class="{ 'is-disabled': disabled, 'is-error': hasError }"
|
||||
:tabindex="disabled ? -1 : 0"
|
||||
@keydown.enter="handleTriggerClick"
|
||||
@keydown.space.prevent="handleTriggerClick"
|
||||
@keydown.esc="onCancel"
|
||||
>
|
||||
<view class="inline-select__trigger" :style="customStyle" @click="handleTriggerClick">
|
||||
<view class="inline-select__content">
|
||||
<view class="inline-select__value" :class="{ 'is-placeholder': !hasValue }">{{ displayValue }}</view>
|
||||
<slot name="suffix">
|
||||
<view class="inline-select__indicator">
|
||||
<text v-if="loading" class="inline-select__loading"></text>
|
||||
<text v-else class="inline-select__arrow" :class="{ 'is-active': show_popup }"></text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<nut-popup v-model:visible="show_popup" position="bottom" safe-area-inset-bottom :close-on-click-overlay="closeOnClickOverlay">
|
||||
<nut-picker
|
||||
:columns="columns"
|
||||
:field-names="fieldNames"
|
||||
v-bind="$attrs"
|
||||
v-model="currentPickerValue"
|
||||
@confirm="onConfirm"
|
||||
@change="onChange"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
|
||||
<view v-if="columns.length === 0" class="inline-select__empty">{{ emptyText }}</view>
|
||||
<template v-else-if="$slots.option" #default="{ option }">
|
||||
<slot name="option" :option="option"></slot>
|
||||
</template>
|
||||
</nut-picker>
|
||||
</nut-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* @component InlineSelect
|
||||
* @description 行内选择组件 - 基于 NutUI Picker 的移动端下拉选择器,提供了丰富的自定义选项和事件处理能力。
|
||||
* 该组件适用于在表单中进行单项选择的场景,支持自定义样式、异步加载、键盘操作等特性。
|
||||
*
|
||||
* @property {String|Number} modelValue - 选择器的当前值,支持v-model双向绑定
|
||||
* @property {String} [title='请选择'] - 选择器标题,显示在弹出层顶部
|
||||
* @property {String} [placeholder='请选择'] - 选择框占位文本,未选择时显示
|
||||
* @property {Object} [fieldNames={ text: 'name', value: 'value' }] - 自定义字段名映射,用于适配不同数据结构
|
||||
* @property {String} [fieldNames.text='name'] - 选项显示文本的字段名
|
||||
* @property {String} [fieldNames.value='value'] - 选项值的字段名
|
||||
* @property {Array<{[key: string]: any}>} columns - 选项数据列表(必填),每个选项对象必须包含text和value字段,支持自定义字段名
|
||||
* @property {String|Number} [defaultValue=''] - 默认选中值,当modelValue未设置时生效
|
||||
* @property {Boolean} [disabled=false] - 是否禁用选择器,禁用时无法打开选择弹窗
|
||||
* @property {Boolean} [closeOnClickOverlay=true] - 是否在点击遮罩层时关闭弹窗
|
||||
* @property {Object} [customStyle={}] - 自定义样式对象,用于定制选择器外观
|
||||
* @property {Boolean} [loading=false] - 是否显示加载状态,加载时显示loading图标
|
||||
* @property {String} [emptyText='暂无可选项'] - 无选项时显示的文本
|
||||
* @property {String} [theme='default'] - 主题样式,可选值:'default'|'primary'|'success'|'warning'|'danger'
|
||||
*
|
||||
* @event {Function} update:modelValue - 值更新时触发,参数: (value: string|number)
|
||||
* @event {Function} change - 选项改变时触发,参数: { columnIndex: number, selectedValue: Array, selectedOptions: Array }
|
||||
* @event {Function} confirm - 点击确定按钮时触发,参数: { value: string|number, option: Object }
|
||||
* @event {Function} cancel - 点击取消按钮时触发
|
||||
* @event {Function} error - 选中值无效时触发,参数: { value: string|number, message: string }
|
||||
*
|
||||
* @slot suffix - 选择框后置内容,默认显示箭头图标
|
||||
* @slot option - 自定义选项渲染,参数: { option: Object }
|
||||
*
|
||||
* @see 更多信息请参考 NutUI Picker 组件文档
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, shallowRef, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '未选择',
|
||||
},
|
||||
fieldNames: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
text: 'name',
|
||||
value: 'value',
|
||||
};
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
defaultValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeOnClickOverlay: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无可选项',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'confirm', 'cancel', 'error']);
|
||||
|
||||
// 状态管理
|
||||
const show_popup = ref(false);
|
||||
const currentValue = shallowRef('');
|
||||
const hasError = ref(false);
|
||||
const optionsCache = new Map();
|
||||
|
||||
// 初始化currentValue
|
||||
const initCurrentValue = () => {
|
||||
if (props.modelValue !== undefined && props.modelValue !== null) {
|
||||
currentValue.value = props.modelValue;
|
||||
} else if (props.defaultValue !== undefined && props.defaultValue !== null) {
|
||||
currentValue.value = props.defaultValue;
|
||||
emit('update:modelValue', props.defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(initCurrentValue);
|
||||
|
||||
// 监听columns变化,重新初始化currentValue
|
||||
watch(
|
||||
() => props.columns,
|
||||
() => {
|
||||
initCurrentValue();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const currentPickerValue = ref([]);
|
||||
// 监听columns变化,重新初始化currentValue
|
||||
watch(
|
||||
() => currentValue.value,
|
||||
() => {
|
||||
currentPickerValue.value = [currentValue.value];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 计算属性
|
||||
const hasValue = computed(() => currentValue.value !== '' && currentValue.value !== undefined && currentValue.value !== null);
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (!hasValue.value || !props.columns.length) return props.placeholder;
|
||||
|
||||
const cachedOption = optionsCache.get(currentValue.value);
|
||||
if (cachedOption) {
|
||||
hasError.value = false;
|
||||
return cachedOption[props.fieldNames.text];
|
||||
}
|
||||
|
||||
const selectedItem = props.columns.find(item => item[props.fieldNames.value] === currentValue.value);
|
||||
if (selectedItem) {
|
||||
optionsCache.set(currentValue.value, selectedItem);
|
||||
hasError.value = false;
|
||||
return selectedItem[props.fieldNames.text];
|
||||
}
|
||||
|
||||
hasError.value = true;
|
||||
emit('error', { value: currentValue.value, message: '选项不存在' });
|
||||
return props.placeholder;
|
||||
});
|
||||
|
||||
// 事件处理
|
||||
const handleTriggerClick = () => {
|
||||
if (props.disabled) return;
|
||||
show_popup.value = true;
|
||||
};
|
||||
|
||||
const onChange = ({ columnIndex, selectedValue, selectedOptions }) => {
|
||||
emit('change', { columnIndex, selectedValue, selectedOptions });
|
||||
};
|
||||
|
||||
const onConfirm = ({ selectedValue, selectedOptions }) => {
|
||||
if (!Array.isArray(selectedValue) || !Array.isArray(selectedOptions) || !selectedValue.length || !selectedOptions.length) return;
|
||||
|
||||
const value = selectedValue[0];
|
||||
const option = selectedOptions[0];
|
||||
|
||||
if (value === undefined || option === undefined) return;
|
||||
|
||||
currentValue.value = value;
|
||||
emit('update:modelValue', value);
|
||||
emit('confirm', { value, option });
|
||||
show_popup.value = false;
|
||||
};
|
||||
const onCancel = () => {
|
||||
show_popup.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
// 监听modelValue的变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
if (newValue !== undefined && newValue !== null && newValue !== currentValue.value) {
|
||||
currentValue.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.inline-select {
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&:focus:not(.is-disabled) .inline-select__trigger {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
.inline-select__trigger {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-error .inline-select__trigger {
|
||||
border-color: var(--theme-error-color, #f56c6c);
|
||||
}
|
||||
|
||||
&__trigger {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--theme-bg-color, #fff);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
background-color: var(--theme-hover-bg-color, #f5f5f5);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
&__value {
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
color: var(--theme-text-color, #333);
|
||||
|
||||
&.is-placeholder {
|
||||
color: var(--theme-placeholder-color, #999);
|
||||
}
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 12rpx solid transparent;
|
||||
border-top-color: var(--theme-arrow-color, #999);
|
||||
transform: translateY(25%) rotate(0deg);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.is-active {
|
||||
transform: translateY(25%) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__loading {
|
||||
display: inline-block;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border: 3rpx solid var(--theme-loading-color, #2d8cf0);
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: inline-select-loading 0.8s infinite linear;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding: 64rpx;
|
||||
text-align: center;
|
||||
color: var(--theme-empty-color, #999);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes inline-select-loading {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--theme-color-rgb: 45, 140, 240;
|
||||
--theme-bg-color: #fff;
|
||||
--theme-text-color: #333;
|
||||
--theme-border-color: #dcdfe6;
|
||||
--theme-placeholder-color: #999;
|
||||
--theme-hover-bg-color: #f5f5f5;
|
||||
--theme-arrow-color: #999;
|
||||
--theme-loading-color: #2d8cf0;
|
||||
--theme-empty-color: #999;
|
||||
--theme-error-color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user