169 lines
4.4 KiB
Vue
169 lines
4.4 KiB
Vue
|
|
<template>
|
||
|
|
<view class="multi-wrap">
|
||
|
|
<view class="label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
||
|
|
<view class="options" :class="hasOtherSelected ? 'with-other' : ''">
|
||
|
|
<view
|
||
|
|
v-for="opt in displayOptions"
|
||
|
|
:key="opt.value"
|
||
|
|
class="opt"
|
||
|
|
:class="isSelected(opt.value) ? 'active' : ''"
|
||
|
|
@click="toggle(opt.value)"
|
||
|
|
>
|
||
|
|
{{ opt.label }}
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
<view v-if="hasOtherSelected" class="other">
|
||
|
|
<input
|
||
|
|
:disabled="disableChange"
|
||
|
|
:value="otherText"
|
||
|
|
class="other-input"
|
||
|
|
placeholder="请补充其它内容"
|
||
|
|
placeholder-class="form__placeholder"
|
||
|
|
maxlength="50"
|
||
|
|
@input="onOtherInput"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { computed } from 'vue';
|
||
|
|
|
||
|
|
const emits = defineEmits(['change']);
|
||
|
|
const props = defineProps({
|
||
|
|
form: { type: Object, default: () => ({}) },
|
||
|
|
range: { type: Array, default: () => [] },
|
||
|
|
name: { default: '' },
|
||
|
|
required: { type: Boolean, default: false },
|
||
|
|
title: { default: '' },
|
||
|
|
disableChange: { type: Boolean, default: false },
|
||
|
|
otherList: { type: Array, default: () => ['其他'] },
|
||
|
|
});
|
||
|
|
|
||
|
|
function normalizeOptions(options) {
|
||
|
|
if (!Array.isArray(options)) return [];
|
||
|
|
if (!options.length) return [];
|
||
|
|
if (typeof options[0] === 'string') return options.filter(Boolean).map((i) => ({ label: String(i), value: String(i) }));
|
||
|
|
return options
|
||
|
|
.map((i) => {
|
||
|
|
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||
|
|
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||
|
|
if (!label && (value === undefined || value === null || value === '')) return null;
|
||
|
|
return { label: String(label || value), value: String(value) };
|
||
|
|
})
|
||
|
|
.filter(Boolean);
|
||
|
|
}
|
||
|
|
|
||
|
|
const displayOptions = computed(() => normalizeOptions(props.range));
|
||
|
|
const valueList = computed(() => (Array.isArray(props.form?.[props.title]) ? props.form[props.title] : []));
|
||
|
|
|
||
|
|
const otherValue = computed(() => {
|
||
|
|
const selected = valueList.value.map(String);
|
||
|
|
const rangeValues = new Set(displayOptions.value.map((i) => String(i.value)));
|
||
|
|
const extra = selected.filter((i) => !rangeValues.has(String(i)));
|
||
|
|
return extra[0] || '';
|
||
|
|
});
|
||
|
|
|
||
|
|
const hasOtherSelected = computed(() => {
|
||
|
|
const selected = valueList.value.map(String);
|
||
|
|
return selected.some((v) => props.otherList.map(String).includes(v));
|
||
|
|
});
|
||
|
|
|
||
|
|
const otherText = computed(() => otherValue.value);
|
||
|
|
|
||
|
|
function isSelected(v) {
|
||
|
|
return valueList.value.map(String).includes(String(v));
|
||
|
|
}
|
||
|
|
|
||
|
|
function emitValue(next) {
|
||
|
|
emits('change', { title: props.title, value: next });
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggle(v) {
|
||
|
|
if (props.disableChange) return;
|
||
|
|
const str = String(v);
|
||
|
|
let next = valueList.value.map(String);
|
||
|
|
|
||
|
|
if (next.includes(str)) {
|
||
|
|
next = next.filter((i) => i !== str);
|
||
|
|
} else {
|
||
|
|
next.push(str);
|
||
|
|
}
|
||
|
|
|
||
|
|
const isOther = props.otherList.map(String).includes(str);
|
||
|
|
if (!isOther) {
|
||
|
|
// 如果取消了 other 的选择,清理 other 文本
|
||
|
|
const stillHasOther = next.some((i) => props.otherList.map(String).includes(i));
|
||
|
|
if (!stillHasOther) {
|
||
|
|
const rangeValues = new Set(displayOptions.value.map((i) => String(i.value)));
|
||
|
|
next = next.filter((i) => rangeValues.has(i));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
emitValue(next);
|
||
|
|
}
|
||
|
|
|
||
|
|
function onOtherInput(e) {
|
||
|
|
const text = String(e?.detail?.value || '').trim();
|
||
|
|
const rangeValues = new Set(displayOptions.value.map((i) => String(i.value)));
|
||
|
|
let next = valueList.value.map(String).filter((i) => rangeValues.has(i));
|
||
|
|
const hasOther = valueList.value.map(String).some((i) => props.otherList.map(String).includes(i));
|
||
|
|
if (hasOther) {
|
||
|
|
next.push(...props.otherList.map(String).slice(0, 1));
|
||
|
|
if (text) next.push(text);
|
||
|
|
}
|
||
|
|
emitValue(next);
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
@import '../cell-style.css';
|
||
|
|
|
||
|
|
.multi-wrap {
|
||
|
|
padding: 24rpx 30rpx;
|
||
|
|
border-bottom: 1px solid #eee;
|
||
|
|
}
|
||
|
|
|
||
|
|
.label {
|
||
|
|
font-size: 28rpx;
|
||
|
|
line-height: 42rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.options {
|
||
|
|
margin-top: 18rpx;
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 18rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opt {
|
||
|
|
min-width: 150rpx;
|
||
|
|
padding: 12rpx 20rpx;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
border-radius: 8rpx;
|
||
|
|
font-size: 26rpx;
|
||
|
|
text-align: center;
|
||
|
|
color: #333;
|
||
|
|
background: #fff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opt.active {
|
||
|
|
background: rgba(93, 138, 255, 0.12);
|
||
|
|
border-color: rgba(93, 138, 255, 0.35);
|
||
|
|
color: #5d8aff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.other {
|
||
|
|
margin-top: 18rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.other-input {
|
||
|
|
width: 100%;
|
||
|
|
font-size: 28rpx;
|
||
|
|
padding: 18rpx 20rpx;
|
||
|
|
border: 1px solid #eee;
|
||
|
|
border-radius: 8rpx;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|