|
|
<script lang="ts" setup> import { defineEmits, defineProps, onBeforeMount, onMounted, ref, toRefs, watchEffect } from 'vue'
const props = defineProps({ optionsLeft: { type: Array as () => System.Option[], required: true, }, options: { type: Array as () => System.Option[], required: true, }, selectedValue: { type: [String, Number, Boolean, Object], default: null, }, placeholder: { type: String, default: '请选择', }, searchable: { type: Boolean, default: true, }, defaultValue: { type: Number, default: 0, }, })
const emits = defineEmits(['confirm', 'cancel']) const optionsList = ref<HTMLUListElement | null>(null) const { optionsLeft, options } = toRefs(props)
const tempSelectedLeftValue = ref('positivePressure')
const tempSelectedRightValue = ref(props.defaultValue)
const tempSelectedValue = ref<string[]>(['positivePressure', '10%'])
const filteredOptionsLeft = ref<System.Option[]>([])
const filteredOptionsRight = ref<System.Option[]>([])
onBeforeMount(() => { filteredOptionsLeft.value = optionsLeft.value.filter(item => item.value) filteredOptionsRight.value = options.value.filter(item => item.value) })
onMounted(() => { scrollToSelectedItem() })
const scrollToSelectedItem = () => { if (!optionsList.value) { return } // 获取选中的li元素(带selected类的)
const selectedLi = optionsList.value.querySelector('li.selected') if (selectedLi) { const containerRect = optionsList.value.getBoundingClientRect() const itemRect = selectedLi.getBoundingClientRect() const offsetTop = itemRect.top - containerRect.top const scrollTop = offsetTop - (containerRect.height - itemRect.height) / 2 optionsList.value.scrollTop = scrollTop + 10 } }
const selectOptionLeft = (option: System.Option) => { if (option.value === 'constantPressure') { tempSelectedValue.value = [option.value] filteredOptionsRight.value = [] } else { tempSelectedValue.value[0] = option.value filteredOptionsRight.value = options.value.filter(item => item.value) } tempSelectedLeftValue.value = option.value }
const selectOption = (option: System.Option) => { tempSelectedRightValue.value = option.value tempSelectedValue.value[1] = option.value }
const confirmSelection = () => { emits('confirm', tempSelectedValue.value) }
const handleCancel = () => { emits('cancel') }
watchEffect(() => { tempSelectedRightValue.value = props.defaultValue }) </script>
<template> <div class="modal-overlay" @click.self="handleCancel"> <div class="modal-container"> <div> <div class="modal-header"> <h3>{{ placeholder }}</h3> <button class="close-btn" @click="handleCancel"> <i class="fa fa-times"></i> </button> </div> <div class="modal-main"> <div class="modal-content"> <ul class="options-list"> <li v-for="(option, index) in filteredOptionsLeft" :key="option.value || index" :class="{ selected: option.value === tempSelectedLeftValue }" @click="selectOptionLeft(option)" > {{ option.label }} </li> </ul> <div v-if="!filteredOptionsLeft.length" class="no-results"> 没有找到匹配项 </div> </div> <div ref="optionsList" class="modal-content-right"> <ul class="options-list"> <li v-for="(option, index) in filteredOptionsRight" :key="option.value || index" :class="{ selected: option.value === tempSelectedRightValue }" @click="selectOption(option)" > {{ option.label }} </li> </ul> <div v-if="!filteredOptionsRight.length" class="no-results"> 没有找到匹配项 </div> </div> </div> </div> <div class="modal-footer"> <button class="cancel-btn" @click="handleCancel">取消</button> <button class="confirm-btn" @click="confirmSelection">确定</button> </div> </div> </div> </template>
<style scoped> .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 200; }
.modal-container { background-color: white; border-radius: 12px; width: 90%; max-width: 400px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); animation: fadeIn 0.2s ease-out; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; }
.modal-header h3 { margin: 0; font-size: 18px; font-weight: 500; color: #1e293b; }
.close-btn { background: none; border: none; cursor: pointer; font-size: 18px; color: #94a3b8; transition: color 0.2s ease; }
.close-btn:hover { color: #64748b; }
.modal-content { flex: 1; overflow-y: auto; padding: 10px 0; max-height: 15vw; } .modal-content-right{ flex: 1; overflow-y: auto; padding: 10px 0; max-height: 15vw; }
.search-box { position: relative; padding: 8px 16px; }
.search-box input { width: 100%; padding: 10px 32px 10px 12px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s ease; }
.search-box input:focus { border-color: #3b82f6; }
.search-box i { position: absolute; right: 28px; top: 50%; transform: translateY(-50%); color: #94a3b8; }
.options-list { list-style: none; margin: 0; padding: 0; }
.options-list li { padding: 14px 20px; font-size: 16px; color: #334155; cursor: pointer; transition: background-color 0.2s ease; text-align: center; }
.options-list li:hover { background-color: #f1f5f9; }
.options-list li.selected { background-color: #a4c4f1; color: #0284c7; font-weight: 500; text-align: center; }
.no-results { padding: 16px 20px; font-size: 14px; color: #94a3b8; text-align: center; }
.modal-footer { padding: 12px 16px; border-top: 1px solid #e2e8f0; display: flex; gap: 12px; }
.modal-footer button { flex: 1; padding: 10px 16px; border-radius: 8px; font-size: 16px; cursor: pointer; transition: all 0.2s ease; }
.cancel-btn { background-color: #ffffff; border: 1px solid #e2e8f0; color: #64748b; }
.cancel-btn:hover { background-color: #f8fafc; }
.confirm-btn { background-color: #3b82f6; border: none; color: white; }
.confirm-btn:hover { background-color: #2563eb; }
.modal-main { display: grid; grid-template-columns: 1fr 1fr; }
@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } </style>
|