fix:color
This commit is contained in:
@@ -33,14 +33,9 @@ provide('tabIndex', computed(() => tabIndex))
|
||||
<style scoped lang="scss">
|
||||
|
||||
.panel {
|
||||
border-radius: .5rem;
|
||||
width: var(--panel-width);
|
||||
background: var(--color-second);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
@apply shadow-lg flex flex-col h-full rounded-xl;
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<div class="inline-flex w-full relative"
|
||||
:class="[disabled && 'disabled']"
|
||||
>
|
||||
<div class="inline-flex w-full relative" :class="[disabled && 'disabled']">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="innerValue"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
:rows="rows"
|
||||
:disabled="disabled"
|
||||
:style="textareaStyle"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md outline-none resize-none transition-colors duration-200 box-border"
|
||||
@input="handleInput"
|
||||
ref="textareaRef"
|
||||
v-model="innerValue"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
:rows="rows"
|
||||
:disabled="disabled"
|
||||
:style="textareaStyle"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md outline-none resize-none transition-colors duration-200 box-border"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<!-- 字数统计 -->
|
||||
<span
|
||||
v-if="showWordLimit && maxlength"
|
||||
class="absolute bottom-1 right-2 text-xs text-gray-400 select-none"
|
||||
v-if="showWordLimit && maxlength"
|
||||
class="absolute bottom-1 right-2 text-xs text-gray-400 select-none"
|
||||
>
|
||||
{{ innerValue.length }} / {{ maxlength }}
|
||||
</span>
|
||||
@@ -24,36 +22,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, computed, nextTick} from "vue"
|
||||
|
||||
import { ref, watch, computed, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string,
|
||||
placeholder?: string,
|
||||
maxlength?: number,
|
||||
rows?: number,
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
maxlength?: number
|
||||
rows?: number
|
||||
autosize: boolean | { minRows?: number; maxRows?: number }
|
||||
showWordLimit?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const innerValue = ref(props.modelValue ?? "")
|
||||
watch(() => props.modelValue, v => (innerValue.value = v ?? ""))
|
||||
const innerValue = ref(props.modelValue ?? '')
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
v => (innerValue.value = v ?? '')
|
||||
)
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
|
||||
// 样式(用于控制高度)
|
||||
const textareaStyle = computed(() => {
|
||||
return props.autosize ? {height: "auto"} : {}
|
||||
return props.autosize ? { height: 'auto' } : {}
|
||||
})
|
||||
|
||||
// 输入处理
|
||||
const handleInput = (e: Event) => {
|
||||
const val = (e.target as HTMLTextAreaElement).value
|
||||
innerValue.value = val
|
||||
emit("update:modelValue", val)
|
||||
emit('update:modelValue', val)
|
||||
if (props.autosize) nextTick(resizeTextarea)
|
||||
}
|
||||
|
||||
@@ -61,36 +61,38 @@ const handleInput = (e: Event) => {
|
||||
const resizeTextarea = () => {
|
||||
if (!textareaRef.value) return
|
||||
const el = textareaRef.value
|
||||
el.style.height = "auto"
|
||||
el.style.height = 'auto'
|
||||
let height = el.scrollHeight
|
||||
let overflow = "hidden"
|
||||
let overflow = 'hidden'
|
||||
|
||||
if (typeof props.autosize === "object") {
|
||||
const {minRows, maxRows} = props.autosize
|
||||
if (typeof props.autosize === 'object') {
|
||||
const { minRows, maxRows } = props.autosize
|
||||
const lineHeight = 24 // 行高约等于 24px
|
||||
if (minRows) height = Math.max(height, minRows * lineHeight)
|
||||
if (maxRows) {
|
||||
const maxHeight = maxRows * lineHeight
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
overflow = "auto" // 超出时允许滚动
|
||||
overflow = 'auto' // 超出时允许滚动
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
el.style.height = height + "px"
|
||||
el.style.height = height + 'px'
|
||||
el.style.overflowY = overflow
|
||||
}
|
||||
|
||||
watch(innerValue, () => {
|
||||
if (props.autosize) nextTick(resizeTextarea)
|
||||
}, {immediate: true})
|
||||
|
||||
watch(
|
||||
innerValue,
|
||||
() => {
|
||||
if (props.autosize) nextTick(resizeTextarea)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
textarea {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const selectHandler = inject('selectHandler', null);
|
||||
|
||||
// 计算当前选项是否被选中
|
||||
const isSelected = computed(() => {
|
||||
return selectValue === props.value;
|
||||
return selectValue.value === props.value;
|
||||
});
|
||||
|
||||
// 点击选项时调用ElSelect提供的方法
|
||||
@@ -45,20 +45,16 @@ watch(() => props.value, () => {}, { immediate: true });
|
||||
|
||||
<style scoped lang="scss">
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
@apply flex items-center px-2 py-1 cursor-pointer transition-all duration-300;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-third);
|
||||
background-color: var(--color-fourth);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
background-color: var(--color-third);
|
||||
background-color: var(--color-fifth);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
|
||||
@@ -1,197 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useAttrs, useSlots, VNode, watch} from 'vue';
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
useAttrs,
|
||||
useSlots,
|
||||
VNode,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { useWindowClick } from '@/hooks/event.ts'
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
label: string
|
||||
value: any
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
options?: Option[];
|
||||
}>();
|
||||
modelValue: any
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
options?: Option[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const attrs = useAttrs();
|
||||
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 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 || '请选择';
|
||||
});
|
||||
return selectedOption.value ? selectedOption.value.label : props.placeholder || '请选择'
|
||||
})
|
||||
|
||||
const updateDropdownPosition = async () => {
|
||||
if (!selectRef.value || !dropdownRef.value) return;
|
||||
if (!selectRef.value || !dropdownRef.value) return
|
||||
|
||||
// 等待 DOM 完全渲染(尤其是下拉框高度)
|
||||
await nextTick();
|
||||
await new Promise(requestAnimationFrame);
|
||||
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;
|
||||
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;
|
||||
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
|
||||
};
|
||||
};
|
||||
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;
|
||||
if (props.disabled) return
|
||||
|
||||
isOpen.value = !isOpen.value;
|
||||
isOpen.value = !isOpen.value
|
||||
|
||||
if (isOpen.value) {
|
||||
await nextTick();
|
||||
await new Promise(requestAnimationFrame);
|
||||
await updateDropdownPosition();
|
||||
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;
|
||||
};
|
||||
selectedOption.value = { value, label }
|
||||
emit('update:modelValue', value)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
let selectValue = $ref(props.modelValue);
|
||||
let selectValue = ref(props.modelValue)
|
||||
|
||||
provide('selectValue', selectValue);
|
||||
provide('selectHandler', selectOption);
|
||||
provide('selectValue', selectValue)
|
||||
provide('selectHandler', selectOption)
|
||||
|
||||
useWindowClick((e: PointerEvent) => {
|
||||
if (!e) return;
|
||||
if (!e) return
|
||||
if (
|
||||
selectRef.value &&
|
||||
!selectRef.value.contains(e.target as Node) &&
|
||||
dropdownRef.value &&
|
||||
!dropdownRef.value.contains(e.target as Node)
|
||||
selectRef.value &&
|
||||
!selectRef.value.contains(e.target as Node) &&
|
||||
dropdownRef.value &&
|
||||
!dropdownRef.value.contains(e.target as Node)
|
||||
) {
|
||||
isOpen.value = false;
|
||||
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;
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
selectValue.value = 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
|
||||
}
|
||||
const option = list.find(opt => opt.props.value === newValue);
|
||||
if (option) {
|
||||
selectedOption.value = option.props;
|
||||
if (props.options) {
|
||||
const option = props.options.find(opt => opt.value === newValue)
|
||||
if (option) {
|
||||
selectedOption.value = option
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (props.options) {
|
||||
const option = props.options.find(opt => opt.value === newValue);
|
||||
if (option) {
|
||||
selectedOption.value = option;
|
||||
}
|
||||
}
|
||||
}, {immediate: true});
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => props.options, (newOptions) => {
|
||||
if (newOptions && props.modelValue) {
|
||||
const option = newOptions.find(opt => opt.value === props.modelValue);
|
||||
if (option) {
|
||||
selectedOption.value = option;
|
||||
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);
|
||||
};
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const onScrollOrResize = () => {
|
||||
if (isOpen.value) updateDropdownPosition();
|
||||
};
|
||||
if (isOpen.value) updateDropdownPosition()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScrollOrResize, true);
|
||||
window.addEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
window.addEventListener('scroll', onScrollOrResize, true)
|
||||
window.addEventListener('resize', onScrollOrResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', onScrollOrResize, true);
|
||||
window.removeEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
window.removeEventListener('scroll', onScrollOrResize, true)
|
||||
window.removeEventListener('resize', onScrollOrResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="select"
|
||||
v-bind="attrs"
|
||||
:class="{ 'is-disabled': disabled, 'is-active': isOpen, 'is-reverse': isReverse }"
|
||||
ref="selectRef"
|
||||
>
|
||||
<div class="select__wrapper" @click="toggleDropdown">
|
||||
<div class="select" ref="selectRef">
|
||||
<div
|
||||
class="select__wrapper"
|
||||
:class="{ disabled: disabled, active: isOpen }"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<div class="select__label" :class="{ 'is-placeholder': !selectedOption }">
|
||||
{{ displayValue }}
|
||||
</div>
|
||||
<div class="select__suffix">
|
||||
<IconFluentChevronLeft20Filled
|
||||
class="arrow"
|
||||
:class="{ 'is-reverse': isOpen }"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
<IconFluentChevronLeft20Filled
|
||||
class="select__arrow"
|
||||
:class="{ 'is-reverse': isOpen }"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<teleport to="body">
|
||||
<transition :name="isReverse ? 'zoom-in-bottom' : 'zoom-in-top'" :key="isReverse ? 'bottom' : 'top'">
|
||||
<div
|
||||
class="select__dropdown"
|
||||
v-if="isOpen"
|
||||
ref="dropdownRef"
|
||||
:style="dropdownStyle"
|
||||
>
|
||||
<ul class="select__options">
|
||||
<li
|
||||
v-if="options"
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="select__option"
|
||||
:class="{
|
||||
'is-selected': option.value === modelValue,
|
||||
'is-disabled': option.disabled
|
||||
}"
|
||||
@click="handleOptionClick(option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</li>
|
||||
<slot v-else></slot>
|
||||
</ul>
|
||||
<transition
|
||||
:name="isReverse ? 'zoom-in-bottom' : 'zoom-in-top'"
|
||||
:key="isReverse ? 'bottom' : 'top'"
|
||||
>
|
||||
<div class="select__dropdown" v-if="isOpen" ref="dropdownRef" :style="dropdownStyle">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
@@ -200,24 +188,27 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
@apply relative w-full;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@apply flex items-center justify-between rounded-md cursor-pointer transition-all duration-300;
|
||||
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);
|
||||
&:not(.disabled):hover {
|
||||
border-color: #3c89e8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #1668dc !important;
|
||||
box-shadow: 0 0 2px 1px #166ce4;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-fourth);
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,17 +223,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
&__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&__arrow {
|
||||
color: #999;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.3s;
|
||||
|
||||
.arrow {
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.is-reverse {
|
||||
&.is-reverse {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
@@ -251,44 +237,17 @@ onBeforeUnmount(() => {
|
||||
.select__dropdown {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-input-bg);
|
||||
border: 1px solid var(--color-input-border);
|
||||
background-color: var(--color-third);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.select__options {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select__option {
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-item-hover);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
background-color: var(--color-item-active);
|
||||
}
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
@apply shadow-lg;
|
||||
}
|
||||
|
||||
/* 往下展开的动画 */
|
||||
.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);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -301,8 +260,9 @@ onBeforeUnmount(() => {
|
||||
/* 往上展开的动画 */
|
||||
.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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from "vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import { useEventListener } from "@/hooks/event.ts";
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import Tooltip from '@/components/base/Tooltip.vue'
|
||||
import { useEventListener } from '@/hooks/event.ts'
|
||||
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
|
||||
export interface ModalProps {
|
||||
modelValue?: boolean,
|
||||
showClose?: boolean,
|
||||
title?: string,
|
||||
content?: string,
|
||||
fullScreen?: boolean;
|
||||
modelValue?: boolean
|
||||
showClose?: boolean
|
||||
title?: string
|
||||
content?: string
|
||||
fullScreen?: boolean
|
||||
padding?: boolean
|
||||
footer?: boolean
|
||||
header?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string,
|
||||
keyboard?: boolean,
|
||||
closeOnClickBg?: boolean,
|
||||
cancelButtonText?: string
|
||||
keyboard?: boolean
|
||||
closeOnClickBg?: boolean
|
||||
confirm?: any
|
||||
beforeClose?: any
|
||||
}
|
||||
@@ -32,15 +32,10 @@ const props = withDefaults(defineProps<ModalProps>(), {
|
||||
header: true,
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
keyboard: true
|
||||
keyboard: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'close',
|
||||
'ok',
|
||||
'cancel',
|
||||
])
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'ok', 'cancel'])
|
||||
|
||||
let confirmButtonLoading = $ref(false)
|
||||
let zIndex = $ref(999)
|
||||
@@ -56,21 +51,21 @@ async function close() {
|
||||
return
|
||||
}
|
||||
if (props.beforeClose) {
|
||||
if (!await props.beforeClose()) {
|
||||
if (!(await props.beforeClose())) {
|
||||
return
|
||||
}
|
||||
}
|
||||
//记录停留时间,避免时间太短,弹框闪烁
|
||||
let stayTime = Date.now() - openTime;
|
||||
let closeTime = 300;
|
||||
let stayTime = Date.now() - openTime
|
||||
let closeTime = 300
|
||||
if (stayTime < 500) {
|
||||
closeTime += 500 - stayTime;
|
||||
closeTime += 500 - stayTime
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
maskRef?.classList.toggle('bounce-out');
|
||||
modalRef?.classList.toggle('bounce-out');
|
||||
}, 500 - stayTime);
|
||||
maskRef?.classList.toggle('bounce-out')
|
||||
modalRef?.classList.toggle('bounce-out')
|
||||
}, 500 - stayTime)
|
||||
|
||||
setTimeout(() => {
|
||||
emit('update:modelValue', false)
|
||||
@@ -82,20 +77,23 @@ async function close() {
|
||||
runtimeStore.modalList.splice(rIndex, 1)
|
||||
}
|
||||
}, closeTime)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, n => {
|
||||
// console.log('n', n)
|
||||
if (n) {
|
||||
id = Date.now()
|
||||
runtimeStore.modalList.push({ id, close })
|
||||
zIndex = 999 + runtimeStore.modalList.length
|
||||
visible = true
|
||||
} else {
|
||||
close()
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
n => {
|
||||
// console.log('n', n)
|
||||
if (n) {
|
||||
id = Date.now()
|
||||
runtimeStore.modalList.push({ id, close })
|
||||
zIndex = 999 + runtimeStore.modalList.length
|
||||
visible = true
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue === undefined) {
|
||||
@@ -139,32 +137,30 @@ async function cancel() {
|
||||
emit('cancel')
|
||||
await close()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="modal-root" :style="{'z-index': zIndex}" v-if="visible">
|
||||
<div class="modal-mask"
|
||||
ref="maskRef"
|
||||
v-if="!fullScreen"
|
||||
@click.stop="closeOnClickBg && close()"></div>
|
||||
<div class="modal"
|
||||
ref="modalRef"
|
||||
:class="[
|
||||
fullScreen?'full':'window'
|
||||
]"
|
||||
>
|
||||
<div class="modal-root" :style="{ 'z-index': zIndex }" v-if="visible">
|
||||
<div
|
||||
class="modal-mask"
|
||||
ref="maskRef"
|
||||
v-if="!fullScreen"
|
||||
@click.stop="closeOnClickBg && close()"
|
||||
></div>
|
||||
<div class="modal" ref="modalRef" :class="[fullScreen ? 'full' : 'window']">
|
||||
<Tooltip title="关闭">
|
||||
<IconFluentDismiss20Regular @click="close"
|
||||
v-if="showClose"
|
||||
class="close cursor-pointer"
|
||||
width="24"/>
|
||||
<IconFluentDismiss20Regular
|
||||
@click="close"
|
||||
v-if="showClose"
|
||||
class="close cursor-pointer"
|
||||
width="24"
|
||||
/>
|
||||
</Tooltip>
|
||||
<div class="modal-header" v-if="header">
|
||||
<div class="title">{{ props.title }}</div>
|
||||
</div>
|
||||
<div class="modal-body" :class="{padding}">
|
||||
<div class="modal-body" :class="{ padding }">
|
||||
<slot></slot>
|
||||
<div v-if="content" class="content max-h-60vh">{{ content }}</div>
|
||||
</div>
|
||||
@@ -174,10 +170,8 @@ async function cancel() {
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
|
||||
<BaseButton
|
||||
id="dialog-ok"
|
||||
:loading="confirmButtonLoading"
|
||||
@click="ok">{{ confirmButtonText }}
|
||||
<BaseButton id="dialog-ok" :loading="confirmButtonLoading" @click="ok"
|
||||
>{{ confirmButtonText }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,12 +181,7 @@ async function cancel() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
$modal-mask-bg: rgba(#000, .6);
|
||||
$radius: .5rem;
|
||||
$time: 0.3s;
|
||||
$header-height: 4rem;
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
@@ -224,38 +213,21 @@ $header-height: 4rem;
|
||||
}
|
||||
|
||||
.modal-root {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
@apply fixed top-0 left-0 z-999 flex items-center justify-center w-full h-full overflow-hidden;
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: $modal-mask-bg;
|
||||
transition: background 0.3s;
|
||||
@apply fixed top-0 left-0 w-full h-full transition-all duration-300;
|
||||
background: rgba(#000, 0.6);
|
||||
animation: fade-in $time;
|
||||
|
||||
&.bounce-out {
|
||||
background: transparent;
|
||||
@apply bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.window {
|
||||
//width: 75vw;
|
||||
//height: 70vh;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: $radius;
|
||||
animation: bounce-in $time ease-out;
|
||||
@apply shadow-lg rounded-lg;
|
||||
|
||||
&.bounce-out {
|
||||
opacity: 0;
|
||||
@@ -263,8 +235,7 @@ $header-height: 4rem;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@apply w-full h-full;
|
||||
animation: bounce-in-full $time ease-out;
|
||||
|
||||
&.bounce-out {
|
||||
@@ -274,61 +245,34 @@ $header-height: 4rem;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
background: var(--color-second);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform $time, opacity $time;
|
||||
@apply relative bg-second overflow-hidden flex flex-col transition-all duration-300;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 1.2rem;
|
||||
top: 1.2rem;
|
||||
z-index: 999;
|
||||
@apply absolute right-1.2rem top-1.2rem z-999;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--modal-padding);
|
||||
padding-bottom: 0;
|
||||
border-radius: $radius $radius 0 0;
|
||||
@apply flex justify-between items-center p-5 pb-0 rounded-t-lg;
|
||||
|
||||
.title {
|
||||
color: var(--color-font-1);
|
||||
font-weight: bold;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.8rem;
|
||||
@apply font-bold text-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
box-sizing: border-box;
|
||||
color: var(--color-main-text);
|
||||
font-weight: 400;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7rem;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@apply box-border text-main-text font-normal text-base leading-6 w-full flex-1 overflow-hidden flex;
|
||||
|
||||
&.padding {
|
||||
padding: .2rem var(--modal-padding);
|
||||
@apply p-1 px-5;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 25rem;
|
||||
padding: .2rem 1.6rem 1.6rem;
|
||||
@apply w-64 p-2 px-4 pb-4;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--modal-padding);
|
||||
@apply flex justify-between p-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ defineExpose({scrollBottom})
|
||||
.list {
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
background: var(--color-second);
|
||||
color: var(--color-font-1);
|
||||
border-radius: .5rem;
|
||||
margin-bottom: .6rem;
|
||||
@@ -199,13 +199,14 @@ defineExpose({scrollBottom})
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-third);
|
||||
.right {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
background: var(--color-fourth);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ShortcutKey } from "@/types/types.ts";
|
||||
import { SoundFileOptions } from "@/config/env.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { ShortcutKey } from '@/types/types.ts'
|
||||
import { SoundFileOptions } from '@/config/env.ts'
|
||||
import { getAudioFileUrl, usePlayAudio } from '@/hooks/sound.ts'
|
||||
import Switch from '@/components/base/Switch.vue'
|
||||
import { Option, Select } from '@/components/base/select'
|
||||
import Textarea from '@/components/base/Textarea.vue'
|
||||
import VolumeIcon from '@/components/icon/VolumeIcon.vue'
|
||||
import Slider from '@/components/base/Slider.vue'
|
||||
import SettingItem from '@/pages/setting/SettingItem.vue'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
@@ -19,11 +18,9 @@ const simpleWords = $computed({
|
||||
get: () => store.simpleWords.join(','),
|
||||
set: v => {
|
||||
try {
|
||||
store.simpleWords = v.split(',');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
store.simpleWords = v.split(',')
|
||||
} catch (e) {}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -32,81 +29,71 @@ const simpleWords = $computed({
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<div>
|
||||
<SettingItem title="忽略大小写"
|
||||
desc="开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的"
|
||||
<SettingItem
|
||||
title="忽略大小写"
|
||||
desc="开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreCase"/>
|
||||
<Switch v-model="settingStore.ignoreCase" />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="允许默写模式下显示提示"
|
||||
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
|
||||
<SettingItem
|
||||
title="允许默写模式下显示提示"
|
||||
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
|
||||
>
|
||||
<Switch v-model="settingStore.allowWordTip"/>
|
||||
<Switch v-model="settingStore.allowWordTip" />
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="简单词过滤"
|
||||
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
|
||||
<SettingItem
|
||||
title="简单词过滤"
|
||||
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreSimpleWord"/>
|
||||
<Switch v-model="settingStore.ignoreSimpleWord" />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="简单词列表"
|
||||
class="items-start!"
|
||||
v-if="settingStore.ignoreSimpleWord"
|
||||
>
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
<SettingItem title="简单词列表" class="items-start!" v-if="settingStore.ignoreSimpleWord">
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords"
|
||||
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<div class="line"></div>
|
||||
<SettingItem main-title="音效"/>
|
||||
<SettingItem title="单词/句子发音口音"
|
||||
desc="仅单词生效,文章固定美音"
|
||||
>
|
||||
<Select v-model="settingStore.soundType"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Option label="美音" value="us"/>
|
||||
<Option label="英音" value="uk"/>
|
||||
<SettingItem main-title="音效" />
|
||||
<SettingItem title="单词/句子发音口音" desc="仅单词生效,文章固定美音">
|
||||
<Select v-model="settingStore.soundType" placeholder="请选择" class="w-50!">
|
||||
<Option label="美音" value="us" />
|
||||
<Option label="英音" value="uk" />
|
||||
</Select>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="按键音">
|
||||
<Switch v-model="settingStore.keyboardSound"/>
|
||||
<Switch v-model="settingStore.keyboardSound" />
|
||||
</SettingItem>
|
||||
<SettingItem title="按键音效">
|
||||
<Select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Select v-model="settingStore.keyboardSoundFile" placeholder="请选择" class="w-50!">
|
||||
<Option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
<VolumeIcon :time="100" @click="usePlayAudio(getAudioFileUrl(item.value)[0])" />
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%"/>
|
||||
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%" />
|
||||
</SettingItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineAsyncComponent } from "vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import CommonSetting from "@/components/setting/CommonSetting.vue";
|
||||
import WordSetting from "@/components/setting/WordSetting.vue";
|
||||
import ArticleSettting from "@/components/setting/ArticleSettting.vue";
|
||||
import ArticleSetting from "@/components/setting/ArticleSetting.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
@@ -39,7 +39,7 @@ let show = $ref(false)
|
||||
<div class="content">
|
||||
<CommonSetting v-if="tabIndex === 0"/>
|
||||
<WordSetting v-if="tabIndex === 1"/>
|
||||
<ArticleSettting v-if="tabIndex === 2"/>
|
||||
<ArticleSetting v-if="tabIndex === 2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user