feat:移除ElSelect组件

This commit is contained in:
zyronon
2025-08-13 01:40:58 +08:00
parent f3c79bfb26
commit 7986d25c28
6 changed files with 424 additions and 29 deletions

View File

@@ -4,19 +4,19 @@ import {ref, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
import {getShortcutKey, useDisableEventListener, useEventListener} from "@/hooks/event.ts";
import {cloneDeep} from "@/utils";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, shakeCommonDict} from "@/utils";
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
import {GITHUB} from "@/config/ENV.ts";
import dayjs from "dayjs";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {ElSwitch, ElSelect, ElOption, ElSlider, ElRadioGroup, ElRadio, ElInputNumber} from 'element-plus'
import {ElInputNumber, ElRadio, ElRadioGroup, ElSlider, ElSwitch} from 'element-plus'
import Toast from '@/pages/pc/components/Toast/Toast.ts'
import {Option, Select} from "@/pages/pc/components/Select";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -185,12 +185,13 @@ function importData(e) {
<div class="row">
<label class="sub-title">单词/句子发音口音</label>
<div class="wrapper">
<ElSelect v-model="settingStore.wordSoundType"
placeholder="请选择"
<Select v-model="settingStore.wordSoundType"
placeholder="请选择"
class="w-50!"
>
<ElOption label="美音" value="us"/>
<ElOption label="英音" value="uk"/>
</ElSelect>
<Option label="美音" value="us"/>
<Option label="英音" value="uk"/>
</Select>
</div>
</div>
<div class="row">
@@ -221,10 +222,11 @@ function importData(e) {
<div class="row">
<label class="item-title">按键音效</label>
<div class="wrapper">
<ElSelect v-model="settingStore.keyboardSoundFile"
placeholder="请选择"
<Select v-model="settingStore.keyboardSoundFile"
placeholder="请选择"
class="w-50!"
>
<ElOption
<Option
v-for="item in SoundFileOptions"
:key="item.value"
:label="item.label"
@@ -236,8 +238,8 @@ function importData(e) {
:time="100"
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
</div>
</ElOption>
</ElSelect>
</Option>
</Select>
</div>
</div>
<div class="row">

View File

@@ -16,6 +16,7 @@ import BaseIcon from "@/components/BaseIcon.vue";
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
import {getDefaultArticle} from "@/types/func.ts";
import copy from "copy-to-clipboard";
import {Option, Select} from "@/pages/pc/components/Select";
interface IProps {
article?: Article,
@@ -344,15 +345,15 @@ function setStartTime(val: Sentence, i: number, j: number) {
<BaseButton @click="startNetworkTranslate"
:loading="progress!==0 && progress !== 100">翻译
</BaseButton>
<ElSelect v-model="networkTranslateEngine"
<Select v-model="networkTranslateEngine"
>
<ElOption
<Option
v-for="item in TranslateEngineOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</Select>
{{ progress }}%
</div>
<div class="flex items-center">

View File

@@ -3,13 +3,14 @@
import {Dict, DictId, DictType} from "@/types/types.ts";
import {cloneDeep} from "@/utils";
import {ElForm, ElFormItem, ElInput, ElSelect, ElOption, FormInstance, FormRules} from "element-plus";
import {ElForm, ElFormItem, ElInput, FormInstance, FormRules} from "element-plus";
import Toast from '@/pages/pc/components/Toast/Toast.ts'
import {onMounted, reactive} from "vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useBaseStore} from "@/stores/base.ts";
import BaseButton from "@/components/BaseButton.vue";
import {getDefaultDict} from "@/types/func.ts";
import {Option, Select} from "@/pages/pc/components/Select";
const props = defineProps<{
isAdd: boolean,
@@ -104,20 +105,20 @@ onMounted(() => {
<ElInput v-model="dictForm.description" type="textarea"/>
</ElFormItem>
<ElFormItem label="原文语言">
<ElSelect v-model="dictForm.language" placeholder="请选择选项">
<ElOption label="英语" value="en"/>
<ElOption label="德语" value="de"/>
<ElOption label="日语" value="ja"/>
<ElOption label="代码" value="code"/>
</ElSelect>
<Select v-model="dictForm.language" placeholder="请选择选项">
<Option label="英语" value="en"/>
<Option label="德语" value="de"/>
<Option label="日语" value="ja"/>
<Option label="代码" value="code"/>
</Select>
</ElFormItem>
<ElFormItem label="译文语言">
<ElSelect v-model="dictForm.translateLanguage" placeholder="请选择选项">
<ElOption label="中文" value="zh-CN"/>
<ElOption label="英语" value="en"/>
<ElOption label="德语" value="de"/>
<ElOption label="日语" value="ja"/>
</ElSelect>
<Select v-model="dictForm.translateLanguage" placeholder="请选择选项">
<Option label="中文" value="zh-CN"/>
<Option label="英语" value="en"/>
<Option label="德语" value="de"/>
<Option label="日语" value="ja"/>
</Select>
</ElFormItem>
<div class="center">
<base-button type="info" @click="emit('close')">关闭</base-button>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import { inject, computed, watch } from 'vue';
const props = defineProps<{
label: string;
value: any;
disabled?: boolean;
}>();
// 通过inject获取ElSelect提供的数据和方法
const selectValue = inject('selectValue', null);
const selectHandler = inject('selectHandler', null);
// 计算当前选项是否被选中
const isSelected = computed(() => {
return selectValue === props.value;
});
// 点击选项时调用ElSelect提供的方法
const handleClick = () => {
if (props.disabled) return;
if (selectHandler) {
selectHandler(props.value, props.label);
}
};
// 监听props变化确保在props更新时重新计算isSelected
watch(() => props.value, () => {}, { immediate: true });
</script>
<template>
<li
class="el-option"
:class="{
'is-selected': isSelected,
'is-disabled': disabled
}"
@click="handleClick"
>
<slot>
<span class="el-option__label">{{ label }}</span>
</slot>
</li>
</template>
<style scoped lang="scss">
.el-option {
display: flex;
align-items: center;
padding: 0.2rem 1rem;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: var(--color-third);
}
&.is-selected {
color: var(--color-select-bg);
font-weight: bold;
background-color: var(--color-third);
}
&.is-disabled {
color: #c0c4cc;
cursor: not-allowed;
}
&__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@@ -0,0 +1,311 @@
<script setup lang="ts">
import {ref, computed, watch, provide, useSlots, VNode, nextTick, useAttrs, onMounted, onBeforeUnmount} from 'vue';
import {Icon} from "@iconify/vue";
import {useWindowClick} from "@/hooks/event.ts";
interface Option {
label: string;
value: any;
disabled?: boolean;
}
const props = defineProps<{
modelValue: any;
placeholder?: string;
disabled?: boolean;
options?: Option[];
}>();
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
const isOpen = ref(false);
const isReverse = ref(false);
const dropdownStyle = ref({}); // Teleport 用的样式
const selectedOption = ref<Option | null>(null);
const selectRef = ref<HTMLDivElement | null>(null);
const dropdownRef = ref<HTMLDivElement | null>(null);
const slots = useSlots();
const displayValue = computed(() => {
return selectedOption.value
? selectedOption.value.label
: props.placeholder || '请选择';
});
const updateDropdownPosition = async () => {
if (!selectRef.value || !dropdownRef.value) return;
// 等待 DOM 完全渲染(尤其是下拉框高度)
await nextTick();
await new Promise(requestAnimationFrame);
const rect = selectRef.value.getBoundingClientRect();
const dropdownHeight = dropdownRef.value.offsetHeight;
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
isReverse.value = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
dropdownStyle.value = {
position: 'fixed',
left: rect.left + 'px',
width: rect.width + 'px',
top: !isReverse.value
? rect.bottom + 5 + 'px'
: 'auto',
bottom: isReverse.value
? window.innerHeight - rect.top + 5 + 'px'
: 'auto',
zIndex: 9999
};
};
const toggleDropdown = async () => {
if (props.disabled) return;
isOpen.value = !isOpen.value;
if (isOpen.value) {
await nextTick();
await new Promise(requestAnimationFrame);
await updateDropdownPosition();
}
};
const selectOption = (value: any, label: string) => {
selectedOption.value = {value, label};
emit('update:modelValue', value);
isOpen.value = false;
};
let selectValue = $ref(props.modelValue);
provide('selectValue', selectValue);
provide('selectHandler', selectOption);
useWindowClick((e: PointerEvent) => {
if (!e) return;
if (
selectRef.value &&
!selectRef.value.contains(e.target as Node) &&
dropdownRef.value &&
!dropdownRef.value.contains(e.target as Node)
) {
isOpen.value = false;
}
});
watch(() => props.modelValue, (newValue) => {
selectValue = newValue;
if (slots.default) {
let slot = slots.default();
let list = [];
if (slot.length === 1) {
list = Array.from(slot[0].children as Array<VNode>);
} else {
list = slot;
}
const option = list.find(opt => opt.props.value === newValue);
if (option) {
selectedOption.value = option.props;
}
return;
}
if (props.options) {
const option = props.options.find(opt => opt.value === newValue);
if (option) {
selectedOption.value = option;
}
}
}, {immediate: true});
watch(() => props.options, (newOptions) => {
if (newOptions && props.modelValue) {
const option = newOptions.find(opt => opt.value === props.modelValue);
if (option) {
selectedOption.value = option;
}
}
}, {immediate: true});
const handleOptionClick = (option: Option) => {
if (option.disabled) return;
selectOption(option.value, option.label);
};
const onScrollOrResize = () => {
if (isOpen.value) updateDropdownPosition();
};
onMounted(() => {
window.addEventListener('scroll', onScrollOrResize, true);
window.addEventListener('resize', onScrollOrResize);
});
onBeforeUnmount(() => {
window.removeEventListener('scroll', onScrollOrResize, true);
window.removeEventListener('resize', onScrollOrResize);
});
</script>
<template>
<div
class="custom-select"
v-bind="attrs"
:class="{ 'is-disabled': disabled, 'is-active': isOpen, 'is-reverse': isReverse }"
ref="selectRef"
>
<div class="custom-select__wrapper" @click="toggleDropdown">
<div class="custom-select__label" :class="{ 'is-placeholder': !selectedOption }">
{{ displayValue }}
</div>
<div class="custom-select__suffix">
<Icon
icon="mdi:chevron-down"
:class="{ 'is-reverse': isOpen }"
width="16"
/>
</div>
</div>
<teleport to="body">
<transition :name="isReverse ? 'zoom-in-bottom' : 'zoom-in-top'" :key="isReverse ? 'bottom' : 'top'">
<div
class="custom-select__dropdown"
v-if="isOpen"
ref="dropdownRef"
:style="dropdownStyle"
>
<ul class="custom-select__options">
<li
v-if="options"
v-for="(option, index) in options"
:key="index"
class="custom-select__option"
:class="{
'is-selected': option.value === modelValue,
'is-disabled': option.disabled
}"
@click="handleOptionClick(option)"
>
{{ option.label }}
</li>
<slot v-else></slot>
</ul>
</div>
</transition>
</teleport>
</div>
</template>
<style scoped lang="scss">
.custom-select {
position: relative;
width: 100%;
font-size: 1rem;
&__wrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 2rem;
padding: 0 0.5rem;
border: 1px solid var(--color-input-border);
border-radius: 0.25rem;
background-color: var(--color-input-bg, #fff);
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: var(--color-select-bg);
}
}
&__label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.is-placeholder {
color: #999;
}
}
&__suffix {
display: flex;
align-items: center;
color: #999;
transition: transform 0.3s;
.is-reverse {
transform: rotate(180deg);
}
}
}
.custom-select__dropdown {
max-height: 200px;
overflow-y: auto;
background-color: #fff;
border: 1px solid var(--color-input-border);
border-radius: 0.25rem;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.custom-select__options {
margin: 0;
padding: 0;
list-style: none;
}
.custom-select__option {
padding: 0.5rem;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
&.is-selected {
color: var(--color-select-bg);
font-weight: bold;
background-color: #f5f7fa;
}
}
.is-disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 往下展开的动画 */
.zoom-in-top-enter-active,
.zoom-in-top-leave-active {
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center top;
}
.zoom-in-top-enter-from,
.zoom-in-top-leave-to {
opacity: 0;
transform: scaleY(0);
}
/* 往上展开的动画 */
.zoom-in-bottom-enter-active,
.zoom-in-bottom-leave-active {
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
transform-origin: center bottom;
}
.zoom-in-bottom-enter-from,
.zoom-in-bottom-leave-to {
opacity: 0;
transform: scaleY(0);
}
</style>

View File

@@ -0,0 +1,5 @@
import Select from './Select.vue';
import Option from './Option.vue';
export {Select, Option};
export default Select;