2026-04-22 17:53:20 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-select
|
2026-04-24 15:31:32 +08:00
|
|
|
|
ref="selectRef"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
:style="{ width: width }"
|
|
|
|
|
|
:value="modelValue"
|
2026-04-24 15:31:32 +08:00
|
|
|
|
:options="options"
|
|
|
|
|
|
:loading="loading"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
@change="handleChange"
|
2026-04-24 15:31:32 +08:00
|
|
|
|
@search="handleSearch"
|
|
|
|
|
|
placeholder="请选择鱼种类"
|
|
|
|
|
|
:mode="multiple ? 'multiple' : undefined"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
show-search
|
2026-04-24 15:31:32 +08:00
|
|
|
|
:filter-option="false"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
class="custom-fish-select"
|
|
|
|
|
|
:dropdownMatchSelectWidth="false"
|
|
|
|
|
|
:getPopupContainer="(triggerNode: HTMLElement) => triggerNode.parentNode"
|
|
|
|
|
|
@dropdownVisibleChange="handleDropdownVisibleChange"
|
2026-04-24 15:31:32 +08:00
|
|
|
|
:max-tag-count="multiple ? 1 : undefined"
|
|
|
|
|
|
:open="open"
|
|
|
|
|
|
@update:open="open = $event"
|
|
|
|
|
|
:field-names="{ label: 'name', value: 'id' }"
|
2026-04-22 17:53:20 +08:00
|
|
|
|
>
|
2026-04-24 15:31:32 +08:00
|
|
|
|
<!-- 自定义 Tag 显示名称 (仅在多选时生效) -->
|
|
|
|
|
|
<template #tagRender="{ value: tagId, onClose }" v-if="multiple">
|
|
|
|
|
|
<a-tag closable @close="onClose" style="margin-right: 3px; max-width: 120px">
|
2026-04-22 17:53:20 +08:00
|
|
|
|
{{ 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="{
|
2026-04-24 15:31:32 +08:00
|
|
|
|
'is-active': isSelected(opt.id),
|
2026-04-22 17:53:20 +08:00
|
|
|
|
'is-hovered': opt.id === hoveredId,
|
|
|
|
|
|
}"
|
|
|
|
|
|
@click.stop="handleSelectOption(opt)"
|
|
|
|
|
|
@mouseenter="hoveredId = opt.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="item-name">{{ opt.name }}</span>
|
|
|
|
|
|
<!-- 选中对勾 -->
|
2026-04-24 15:31:32 +08:00
|
|
|
|
<span v-if="isSelected(opt.id)" class="check-icon">✓</span>
|
2026-04-22 17:53:20 +08:00
|
|
|
|
</div>
|
2026-04-24 15:31:32 +08:00
|
|
|
|
<div v-if="filteredOptions.length === 0" class="empty-tip">无匹配数据</div>
|
2026-04-22 17:53:20 +08:00
|
|
|
|
</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>
|
2026-04-24 15:31:32 +08:00
|
|
|
|
import { ref, onMounted, computed, watch } from "vue";
|
|
|
|
|
|
import { getFishDictoryDropdown } from "@/api/select";
|
2026-04-27 09:28:16 +08:00
|
|
|
|
import { useShuJuTianBaoStore } from "@/store/modules/shuJuTianBao";
|
|
|
|
|
|
const shuJuTianBaoStore = useShuJuTianBaoStore();
|
2026-04-22 17:53:20 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Props & Emits ---
|
|
|
|
|
|
interface Props {
|
2026-04-24 15:31:32 +08:00
|
|
|
|
modelValue: string | string[]; // 支持字符串(单选)或数组(多选)
|
|
|
|
|
|
width?: string;
|
|
|
|
|
|
multiple?: boolean; // 控制是否多选
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 15:31:32 +08:00
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
|
multiple: false, // 默认单选,根据需求调整
|
|
|
|
|
|
});
|
2026-04-22 17:53:20 +08:00
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
2026-04-27 09:28:16 +08:00
|
|
|
|
(e: "update:modelValue", value: string | string[], opt: any): void;
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
// --- State ---
|
2026-04-24 15:31:32 +08:00
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const options = ref<any[]>([]);
|
|
|
|
|
|
const searchKeyword = ref<string>("");
|
2026-04-22 17:53:20 +08:00
|
|
|
|
const hoveredId = ref<string | null>(null);
|
2026-04-24 15:31:32 +08:00
|
|
|
|
const open = ref(false); // 控制下拉框显隐
|
2026-04-22 17:53:20 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Computed ---
|
|
|
|
|
|
const filteredOptions = computed(() => {
|
2026-04-24 15:31:32 +08:00
|
|
|
|
if (!searchKeyword.value) {
|
|
|
|
|
|
return options.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
const lowerKeyword = searchKeyword.value.toLowerCase();
|
|
|
|
|
|
return options.value.filter((item: any) => {
|
|
|
|
|
|
const nameMatch = item.name?.toLowerCase().includes(lowerKeyword);
|
|
|
|
|
|
const aliasMatch = item.alias?.toLowerCase().includes(lowerKeyword);
|
|
|
|
|
|
return nameMatch || aliasMatch;
|
|
|
|
|
|
});
|
2026-04-22 17:53:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const currentDetailData = computed(() => {
|
|
|
|
|
|
if (hoveredId.value) {
|
|
|
|
|
|
return options.value.find((item: any) => item.id === hoveredId.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// --- Methods ---
|
2026-04-24 15:31:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 辅助函数:判断是否选中
|
|
|
|
|
|
const isSelected = (id: string) => {
|
|
|
|
|
|
if (props.multiple) {
|
|
|
|
|
|
return Array.isArray(props.modelValue) && props.modelValue.includes(id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return props.modelValue === id;
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-24 15:31:32 +08:00
|
|
|
|
const handleSearch = (value: string) => {
|
|
|
|
|
|
searchKeyword.value = value;
|
|
|
|
|
|
};
|
2026-04-22 17:53:20 +08:00
|
|
|
|
|
2026-04-24 15:31:32 +08:00
|
|
|
|
const handleDropdownVisibleChange = (val: boolean) => {
|
|
|
|
|
|
open.value = val;
|
|
|
|
|
|
if (!val) {
|
|
|
|
|
|
hoveredId.value = null;
|
|
|
|
|
|
searchKeyword.value = "";
|
|
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectOption = (opt: any) => {
|
2026-04-24 15:31:32 +08:00
|
|
|
|
if (props.multiple) {
|
|
|
|
|
|
// --- 多选逻辑 ---
|
|
|
|
|
|
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); // 选中
|
|
|
|
|
|
}
|
2026-04-27 09:28:16 +08:00
|
|
|
|
emit("update:modelValue", newValues, opt);
|
2026-04-22 17:53:20 +08:00
|
|
|
|
} else {
|
2026-04-24 15:31:32 +08:00
|
|
|
|
// --- 单选逻辑 ---
|
|
|
|
|
|
// 关键:单选模式下,直接发射当前 ID,覆盖旧值
|
|
|
|
|
|
// 如果点击的是已选中的项,则清空(可选行为,视需求而定)
|
|
|
|
|
|
if (props.modelValue === opt.id) {
|
2026-04-27 09:28:16 +08:00
|
|
|
|
emit("update:modelValue", "", opt); // 取消选中
|
2026-04-24 15:31:32 +08:00
|
|
|
|
} else {
|
2026-04-27 09:28:16 +08:00
|
|
|
|
emit("update:modelValue", opt.id, opt); // 选中新项
|
2026-04-24 15:31:32 +08:00
|
|
|
|
// 单选模式下,选择后通常希望关闭下拉框
|
|
|
|
|
|
open.value = false;
|
|
|
|
|
|
}
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleChange = (val: any) => {
|
2026-04-24 15:31:32 +08:00
|
|
|
|
// 当 a-select 内部触发 change 时(例如删除 Tag)
|
|
|
|
|
|
// 在单选模式下,如果用户通过键盘或删除操作改变了值,这里也会捕获
|
2026-04-27 10:35:06 +08:00
|
|
|
|
emit("update:modelValue", val, '');
|
2026-04-22 17:53:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getFishNameById = (id: string) => {
|
|
|
|
|
|
if (!id) return "";
|
|
|
|
|
|
const fish = options.value.find((item: any) => item.id === id);
|
|
|
|
|
|
return fish ? fish.name : id;
|
|
|
|
|
|
};
|
2026-04-24 15:31:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听 multiple 变化,确保模式切换时状态正确
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.multiple,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
// 如果从多选变为单选,且 modelValue 是数组,取第一个值或清空
|
|
|
|
|
|
if (!newVal && Array.isArray(props.modelValue)) {
|
2026-04-27 09:28:16 +08:00
|
|
|
|
emit("update:modelValue", props.modelValue || null, options.value);
|
2026-04-24 15:31:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 如果从单选变为多选,且 modelValue 是字符串,转为数组
|
|
|
|
|
|
if (newVal && typeof props.modelValue === "string") {
|
2026-04-27 09:28:16 +08:00
|
|
|
|
emit("update:modelValue", props.modelValue ? [props.modelValue] : [], {});
|
2026-04-24 15:31:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-04-27 09:28:16 +08:00
|
|
|
|
const init = () => {
|
|
|
|
|
|
let data = shuJuTianBaoStore.getFishOption();
|
|
|
|
|
|
if (data.length === 0) {
|
2026-04-24 15:31:32 +08:00
|
|
|
|
loading.value = true;
|
|
|
|
|
|
getFishDictoryDropdown()
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
options.value = res.data || [];
|
|
|
|
|
|
loading.value = false;
|
2026-04-27 09:28:16 +08:00
|
|
|
|
shuJuTianBaoStore.setFishOption(options.value);
|
2026-04-24 15:31:32 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
});
|
2026-04-27 09:28:16 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
options.value = data;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
init();
|
2026-04-24 15:31:32 +08:00
|
|
|
|
});
|
2026-04-22 17:53:20 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2026-04-24 15:31:32 +08:00
|
|
|
|
/* 样式保持不变 */
|
2026-04-22 17:53:20 +08:00
|
|
|
|
.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;
|
2026-04-24 15:31:32 +08:00
|
|
|
|
height: 300px;
|
2026-04-22 17:53:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dropdown-left-list {
|
|
|
|
|
|
width: 150px;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
border-right: 1px solid #f0f0f0;
|
2026-04-24 15:31:32 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
&::-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;
|
|
|
|
|
|
}
|
2026-04-24 15:31:32 +08:00
|
|
|
|
|
2026-04-22 17:53:20 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-04-24 15:31:32 +08:00
|
|
|
|
</style>
|