index.vue 3.8 KB
<template>
  <uni-popup ref="popup" type="bottom" :mask-click="false" :safe-area="true">
    <view class="sheet">
      <view class="sheet-header">
        <text class="cancel" @click="onCancel">取消</text>
        <text class="title">{{ title }}</text>
        <text class="ok" @click="onConfirm">确认</text>
      </view>
      <view class="sheet-body">
        <view
          v-for="(opt, i) in options"
          :key="i"
          :class="['option', { selected: isSelected(opt) }]"
          @click="toggle(opt)"
        >
          <text class="label">{{ displayOf(opt) }}</text>
        </view>
      </view>
    </view>
  </uni-popup>
</template>

<script>
export default {
  name: 'MultiSelectSheet',
  props: {
    visible: { type: Boolean, default: false },
    title: { type: String, default: '请选择' },
    // 统一约定:options 为 [{ label: string, value: string|number }]
    options: { type: Array, default: () => [] },
    // 接收逗号分隔的字符串 '111,222,333'
    value: { type: String, default: '' }
  },
  data() {
    return {
      innerValue: [] // 内部维护数组形式
    }
  },
  watch: {
    value(v) { this.initInnerValue(v) },
    visible(v) { v ? this.open() : this.close() }
  },
  mounted() {
    if (this.visible) this.open()
  },
  methods: {
    initInnerValue(val) {
      if (!val) {
        this.innerValue = []
      } else {
        this.innerValue = String(val).split(',').filter(x => x)
      }
    },
    open() {
      this.initInnerValue(this.value)
      this.$refs.popup && this.$refs.popup.open()
      this.$emit('update:visible', true)
    },
    close() {
      this.$refs.popup && this.$refs.popup.close()
      this.$emit('update:visible', false)
    },
    displayOf(opt) {
      if (!opt) return ''
      return opt.label != null ? String(opt.label) : ''
    },
    valueOf(opt) {
      if (!opt) return ''
      return opt.value != null ? String(opt.value) : ''
    },
    isSelected(opt) {
      const v = this.valueOf(opt)
      return this.innerValue.includes(v)
    },
    toggle(opt) {
      const v = this.valueOf(opt)
      const idx = this.innerValue.indexOf(v)
      if (idx > -1) {
        this.innerValue.splice(idx, 1)
      } else {
        this.innerValue.push(v)
      }
    },
    onCancel() {
      this.close()
      this.$emit('cancel')
    },
    onConfirm() {
      const selectedValues = this.innerValue
      // 保持原始选项顺序
      const sortedValues = []
      const sortedLabels = []
      
      this.options.forEach(opt => {
        const v = this.valueOf(opt)
        if (selectedValues.includes(v)) {
          sortedValues.push(v)
          sortedLabels.push(this.displayOf(opt))
        }
      })

      const valStr = sortedValues.join(',')
      const labelStr = sortedLabels.join(',')
      
      this.$emit('confirm', { value: valStr, label: labelStr })
      this.$emit('input', valStr)
      this.$emit('update:value', valStr)
      this.close()
    }
  }
}
</script>

<style lang="scss" scoped>
.sheet {
  width: 100%;
  max-height: 45vh;
  background: #fff;
  border-radius: 20rpx 20rpx 0 0;
  display: flex;
  flex-direction: column;
}
.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 30rpx 32rpx;
  border-bottom: 1rpx solid #f0f0f0;
}
.title {
  font-size: 36rpx;
  font-weight: 600;
}
.cancel {
  color: rgba(0,0,0,0.6);
  font-size: 28rpx;
}
.ok {
  color: $theme-primary;
  font-size: 28rpx;
}
.sheet-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 32rpx;
}
.option {
  padding: 20rpx;
  line-height: 40rpx;
  background: #fff;
  text-align: center;
  border-radius: 12rpx;
  font-size: 32rpx;
  margin-bottom: 20rpx;
  .label {
    color: rgba(0,0,0,0.6);
    font-size: 32rpx;
  }
  &.selected {
    background: #f3f3f3;
    .label {
      color: rgba(0,0,0,0.9);
    }
  }
}
</style>