271 lines
6.1 KiB
Vue
271 lines
6.1 KiB
Vue
|
|
<template>
|
|||
|
|
<a-select
|
|||
|
|
:style="{ width: width }"
|
|||
|
|
:value="modelValue"
|
|||
|
|
@change="handleChange"
|
|||
|
|
placeholder="请选择鱼名称"
|
|||
|
|
mode="multiple"
|
|||
|
|
show-search
|
|||
|
|
:filter-option="filterOption"
|
|||
|
|
class="custom-fish-select"
|
|||
|
|
:dropdownMatchSelectWidth="false"
|
|||
|
|
:getPopupContainer="(triggerNode: HTMLElement) => triggerNode.parentNode"
|
|||
|
|
@dropdownVisibleChange="handleDropdownVisibleChange"
|
|||
|
|
>
|
|||
|
|
<!-- 自定义 Tag 显示名称 -->
|
|||
|
|
<template #tagRender="{ value: tagId, onClose }">
|
|||
|
|
<a-tag
|
|||
|
|
closable
|
|||
|
|
@close="onClose"
|
|||
|
|
style="margin-right: 3px; max-width: 120px"
|
|||
|
|
>
|
|||
|
|
{{ getFishNameById(tagId) }}
|
|||
|
|
</a-tag>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<template #dropdownRender>
|
|||
|
|
<div class="custom-dropdown-container">
|
|||
|
|
<!-- 左侧:可滚动的选项列表 -->
|
|||
|
|
<div class="dropdown-left-list">
|
|||
|
|
<div
|
|||
|
|
v-for="opt in filteredOptions"
|
|||
|
|
:key="opt.id"
|
|||
|
|
class="dropdown-item"
|
|||
|
|
:class="{
|
|||
|
|
'is-active': Array.isArray(modelValue) && modelValue.includes(opt.id),
|
|||
|
|
'is-hovered': opt.id === hoveredId,
|
|||
|
|
}"
|
|||
|
|
@click.stop="handleSelectOption(opt)"
|
|||
|
|
@mouseenter="hoveredId = opt.id"
|
|||
|
|
>
|
|||
|
|
<span class="item-name">{{ opt.name }}</span>
|
|||
|
|
<!-- 选中对勾 -->
|
|||
|
|
<span v-if="Array.isArray(modelValue) && modelValue.includes(opt.id)" class="check-icon">✓</span>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="filteredOptions.length === 0" class="empty-tip">
|
|||
|
|
无匹配数据
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 中间分割线 -->
|
|||
|
|
<div class="dropdown-divider"></div>
|
|||
|
|
|
|||
|
|
<!-- 右侧:固定显示的别名/详情 -->
|
|||
|
|
<div class="dropdown-right-detail">
|
|||
|
|
<div v-if="currentDetailData" class="detail-content">
|
|||
|
|
<div class="detail-title">{{ currentDetailData.name }}</div>
|
|||
|
|
<div class="detail-alias" :title="currentDetailData.alias">
|
|||
|
|
{{ currentDetailData.alias || "暂无别名" }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="detail-placeholder">请选择或悬停查看</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</a-select>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import { ref, computed } from "vue";
|
|||
|
|
|
|||
|
|
// --- Props & Emits ---
|
|||
|
|
interface Props {
|
|||
|
|
modelValue: string[]; // 接收选中的 ID 数组
|
|||
|
|
options: any[];
|
|||
|
|
width: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = defineProps<Props>();
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
(e: "update:modelValue", value: string[]): void;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
// --- State ---
|
|||
|
|
// 这里可以改为从 API 获取,目前先保留静态数据作为示例
|
|||
|
|
const options = ref<any>(props.options || []);
|
|||
|
|
|
|||
|
|
const hoveredId = ref<string | null>(null);
|
|||
|
|
|
|||
|
|
// --- Computed ---
|
|||
|
|
const filteredOptions = computed(() => {
|
|||
|
|
return options.value;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const currentDetailData = computed(() => {
|
|||
|
|
if (hoveredId.value) {
|
|||
|
|
return options.value.find((item: any) => item.id === hoveredId.value);
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// --- Methods ---
|
|||
|
|
const handleDropdownVisibleChange = (open: boolean) => {
|
|||
|
|
if (!open) {
|
|||
|
|
hoveredId.value = null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const filterOption = (input: string, option: any) => {
|
|||
|
|
if (!input) return true;
|
|||
|
|
const targetOpt = options.value.find((item: any) => item.id === option.value);
|
|||
|
|
if (!targetOpt) return false;
|
|||
|
|
|
|||
|
|
const lowerInput = input.toLowerCase();
|
|||
|
|
const nameMatch = targetOpt.name?.toLowerCase().includes(lowerInput);
|
|||
|
|
const aliasMatch = targetOpt.alias?.toLowerCase().includes(lowerInput);
|
|||
|
|
return nameMatch || aliasMatch;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSelectOption = (opt: any) => {
|
|||
|
|
let newValues: string[] = Array.isArray(props.modelValue) ? [...props.modelValue] : [];
|
|||
|
|
const index = newValues.indexOf(opt.id);
|
|||
|
|
|
|||
|
|
if (index > -1) {
|
|||
|
|
newValues.splice(index, 1);
|
|||
|
|
} else {
|
|||
|
|
newValues.push(opt.id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 触发更新
|
|||
|
|
emit("update:modelValue", newValues);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleChange = (val: any) => {
|
|||
|
|
// 处理直接通过 a-select 内部机制触发的变化(如删除 tag)
|
|||
|
|
emit("update:modelValue", val);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getFishNameById = (id: string) => {
|
|||
|
|
if (!id) return "";
|
|||
|
|
const fish = options.value.find((item: any) => item.id === id);
|
|||
|
|
return fish ? fish.name : id;
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.custom-fish-select {
|
|||
|
|
:deep(.ant-select-dropdown) {
|
|||
|
|
padding: 0 !important;
|
|||
|
|
min-width: 400px !important;
|
|||
|
|
width: auto !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.custom-dropdown-container {
|
|||
|
|
display: flex;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
height: 300px; /* 固定高度 */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-left-list {
|
|||
|
|
width: 150px;
|
|||
|
|
height: 100%;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
border-right: 1px solid #f0f0f0;
|
|||
|
|
|
|||
|
|
&::-webkit-scrollbar {
|
|||
|
|
width: 6px;
|
|||
|
|
}
|
|||
|
|
&::-webkit-scrollbar-thumb {
|
|||
|
|
background: #ccc;
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-item {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #333;
|
|||
|
|
transition: background-color 0.3s;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
&:hover {
|
|||
|
|
background-color: #f5f5f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.is-active {
|
|||
|
|
background-color: #e6f7ff;
|
|||
|
|
color: #1890ff;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.is-hovered {
|
|||
|
|
background-color: #fafafa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-name {
|
|||
|
|
display: block;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.check-icon {
|
|||
|
|
color: #1890ff;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-left: 8px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-divider {
|
|||
|
|
width: 1px;
|
|||
|
|
background-color: #e8e8e8;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dropdown-right-detail {
|
|||
|
|
width: 250px;
|
|||
|
|
height: 100%;
|
|||
|
|
padding: 16px;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
background-color: #fff;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-content {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-title {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #333;
|
|||
|
|
margin-bottom: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-alias {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #666;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
word-break: break-all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-placeholder {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 14px;
|
|||
|
|
text-align: center;
|
|||
|
|
position: absolute;
|
|||
|
|
left: 50%;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-tip {
|
|||
|
|
padding: 10px;
|
|||
|
|
color: #999;
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
</style>
|