feat:移除ElSelect组件
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
75
src/pages/pc/components/Select/Option.vue
Normal file
75
src/pages/pc/components/Select/Option.vue
Normal 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>
|
||||
311
src/pages/pc/components/Select/Select.vue
Normal file
311
src/pages/pc/components/Select/Select.vue
Normal 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>
|
||||
5
src/pages/pc/components/Select/index.ts
Normal file
5
src/pages/pc/components/Select/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Select from './Select.vue';
|
||||
import Option from './Option.vue';
|
||||
|
||||
export {Select, Option};
|
||||
export default Select;
|
||||
Reference in New Issue
Block a user