WholeProcessPlatform/frontend/src/components/fishSearch/index.vue

271 lines
6.1 KiB
Vue
Raw Normal View History

2026-04-22 17:53:20 +08:00
<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>