feat(modal): update modal
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -32,6 +32,7 @@ declare module 'vue' {
|
||||
IconWrapper: typeof import('./src/components/IconWrapper.vue')['default']
|
||||
MiniModal: typeof import('./src/components/MiniModal.vue')['default']
|
||||
Modal: typeof import('./src/components/Modal/Modal.vue')['default']
|
||||
NewModal: typeof import('./src/components/Modal/NewModal.vue')['default']
|
||||
PopConfirm: typeof import('./src/components/PopConfirm.vue')['default']
|
||||
Practice: typeof import('./src/components/Practice/Practice.vue')['default']
|
||||
RepeatSetting: typeof import('./src/components/Toolbar/RepeatSetting.vue')['default']
|
||||
|
||||
15
src/App.vue
15
src/App.vue
@@ -5,8 +5,13 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import {SaveKey} from "@/types.ts"
|
||||
import Practice from "@/components/Practice/Practice.vue"
|
||||
import AddArticle from "@/components/Practice/AddArticle.vue";
|
||||
import {useEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
// 查询当前系统主题颜色
|
||||
const match: MediaQueryList = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
// 监听系统主题变化
|
||||
@@ -22,12 +27,22 @@ watch(store.$state, (n) => {
|
||||
localStorage.setItem(SaveKey, JSON.stringify(n))
|
||||
})
|
||||
|
||||
useStartKeyboardEventListener()
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
if (store.theme !== 'auto') {
|
||||
document.documentElement.setAttribute('data-theme', store.theme)
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener('keyup', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
let lastItem = runtimeStore.modalList.pop()
|
||||
console.log('la',lastItem)
|
||||
lastItem && lastItem.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from "vue";
|
||||
import {onMounted, toRef, watch} from "vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import {Icon} from '@iconify/vue';
|
||||
import {useEsc} from "@/hooks/event.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
|
||||
interface IProps {
|
||||
modelValue: boolean,
|
||||
modelValue?: boolean,
|
||||
showClose?: boolean,
|
||||
title?: string,
|
||||
subTitle?: string,
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
modelValue: true,
|
||||
modelValue: undefined,
|
||||
showClose: true,
|
||||
fullScreen: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -21,59 +24,90 @@ const emit = defineEmits([
|
||||
'close'
|
||||
])
|
||||
|
||||
let visible = $ref(false)
|
||||
let openTime = $ref(Date.now())
|
||||
let maskRef = $ref<HTMLDivElement>(null)
|
||||
let modalRef = $ref<HTMLDivElement>(null)
|
||||
|
||||
function close() {
|
||||
emit('update:modelValue', false)
|
||||
emit('close',)
|
||||
//记录停留时间,避免时间太短,弹框闪烁
|
||||
let stayTime = Date.now() - openTime;
|
||||
let closeTime = 300;
|
||||
if (stayTime < 500) {
|
||||
closeTime += 500 - stayTime;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
maskRef?.classList.toggle('bounce-out');
|
||||
modalRef?.classList.toggle('bounce-out');
|
||||
}, 500 - stayTime);
|
||||
|
||||
setTimeout(() => {
|
||||
emit('update:modelValue', false)
|
||||
emit('close')
|
||||
visible = false
|
||||
resolve(true)
|
||||
}, closeTime)
|
||||
});
|
||||
}
|
||||
|
||||
useEsc(close)
|
||||
watch(() => props.modelValue, n => {
|
||||
if (n) {
|
||||
visible = true
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue === undefined) {
|
||||
visible = true
|
||||
}
|
||||
})
|
||||
|
||||
useEsc(close, () => props.modelValue)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="bounce">
|
||||
<div class="modal-root" v-if="props.modelValue">
|
||||
<div class="modal-mask" @click="close"></div>
|
||||
<div class="modal">
|
||||
<Tooltip title="关闭">
|
||||
<Icon @click="close"
|
||||
v-if="showClose"
|
||||
class="close hvr-grow pointer"
|
||||
width="20" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</Tooltip>
|
||||
<div class="modal-header" v-if="props.title">
|
||||
<div class="title">{{ props.title }}</div>
|
||||
<div class="sub-title" v-if="props.subTitle">{{ props.subTitle }}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-root" v-if="visible">
|
||||
<div class="modal-mask"
|
||||
ref="maskRef"
|
||||
v-if="!fullScreen"
|
||||
@click="close"></div>
|
||||
<div class="modal"
|
||||
ref="modalRef"
|
||||
:class="[
|
||||
fullScreen?'full':'window'
|
||||
]"
|
||||
>
|
||||
<Tooltip title="关闭">
|
||||
<Icon @click="close"
|
||||
v-if="showClose"
|
||||
class="close hvr-grow pointer"
|
||||
width="20" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</Tooltip>
|
||||
<div class="modal-header" v-if="props.title">
|
||||
<div class="title">{{ props.title }}</div>
|
||||
<div class="sub-title" v-if="props.subTitle">{{ props.subTitle }}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/colors";
|
||||
|
||||
.bounce-enter-active {
|
||||
animation: fade-in 0.3s;
|
||||
}
|
||||
|
||||
.bounce-enter-active .modal {
|
||||
animation: bounce-in 0.3s;
|
||||
}
|
||||
|
||||
.bounce-leave-active {
|
||||
animation: fade-in 0.3s reverse;
|
||||
}
|
||||
|
||||
.bounce-leave-active .modal {
|
||||
animation: bounce-out 0.3s;
|
||||
}
|
||||
$modal-mask-bg: rgba(#000, .45);
|
||||
$radius: 16rem;
|
||||
$time: 0.3s;
|
||||
$header-height: 60rem;
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
@@ -89,14 +123,14 @@ useEsc(close)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-out {
|
||||
@keyframes bounce-in-full {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +143,6 @@ useEsc(close)
|
||||
}
|
||||
}
|
||||
|
||||
$modal-mask-bg: rgba(#000, .45);
|
||||
$radius: 16rem;
|
||||
$time: 0.3s;
|
||||
$header-height: 60rem;
|
||||
|
||||
.modal-root {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -133,19 +162,46 @@ $header-height: 60rem;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: $modal-mask-bg;
|
||||
transition: background 0.3s;
|
||||
animation: fade-in $time;
|
||||
|
||||
&.bounce-out {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.window {
|
||||
//width: 75vw;
|
||||
//height: 70vh;
|
||||
box-shadow: var(--color-main-bg) 0 0 10rem 1rem;
|
||||
border-radius: $radius;
|
||||
animation: bounce-in $time ease-out;
|
||||
|
||||
&.bounce-out {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
animation: bounce-in-full $time ease-out;
|
||||
|
||||
&.bounce-out {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
//background: white;
|
||||
background: var(--color-main-bg);
|
||||
//box-shadow: var(--color-main-bg) 0 0 10rem 1rem;
|
||||
//width: 75vw;
|
||||
//height: 70vh;
|
||||
border-radius: $radius;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform $time, opacity $time;
|
||||
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
|
||||
@@ -15,6 +15,8 @@ import EditAbleText from "@/components/EditAbleText.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useDisableEventListener, useEsc} from "@/hooks/event.ts";
|
||||
import {Action} from "element-plus";
|
||||
import Modal from "@/components/Modal/Modal.vue";
|
||||
|
||||
interface IProps {
|
||||
article?: Article
|
||||
@@ -34,9 +36,6 @@ const TranslateEngineOptions = [
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
useDisableEventListener()
|
||||
useEsc(() => {
|
||||
emit('close')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (article.text.trim()) {
|
||||
@@ -45,6 +44,9 @@ onMounted(() => {
|
||||
if (!article.textCustomTranslateIsFormat) {
|
||||
let r = getSplitTranslateText(article.textCustomTranslate)
|
||||
if (r) article.textCustomTranslate = r
|
||||
ElMessageBox.alert('检测到本地翻译未格式化,已自动格式', '提示', {
|
||||
confirmButtonText: '好的',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +54,6 @@ onMounted(() => {
|
||||
updateSentenceTranslate()
|
||||
})
|
||||
|
||||
|
||||
async function startNetworkTranslate() {
|
||||
if (!article.title.trim()) {
|
||||
return ElMessage.error('请填写标题!')
|
||||
@@ -198,20 +199,26 @@ watch(() => article.useTranslateType, () => {
|
||||
updateSections(article)
|
||||
}
|
||||
}
|
||||
if (article.useTranslateType === TranslateType.custom) {
|
||||
if (article.useTranslateType === TranslateType.network) {
|
||||
if (article.textNetworkTranslate.trim()) {
|
||||
updateLocalSentenceTranslate(article, article.textNetworkTranslate)
|
||||
} else {
|
||||
updateSections(article)
|
||||
}
|
||||
}
|
||||
if (article.useTranslateType === TranslateType.none) {
|
||||
updateSections(article)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Modal @close="$emit('close')"
|
||||
title="设置"
|
||||
:full-screen="true"
|
||||
subTitle="修改立即生效,实时保存">
|
||||
<div class="add-article" @click.stop="null">
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
@@ -336,7 +343,7 @@ watch(() => article.useTranslateType, () => {
|
||||
width="20" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -57,6 +57,7 @@ watch(() => store.load, n => {
|
||||
|
||||
function getCurrentPractice() {
|
||||
if (store.isArticle) {
|
||||
return
|
||||
let tempArticle = {...DefaultArticle, ...store.currentDict.articles[store.currentDict.chapterIndex]}
|
||||
console.log('article', tempArticle)
|
||||
if (tempArticle.sections.length) {
|
||||
@@ -121,7 +122,6 @@ onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
useStartKeyboardEventListener()
|
||||
|
||||
function write() {
|
||||
console.log('write')
|
||||
|
||||
@@ -24,8 +24,8 @@ const emit = defineEmits([
|
||||
])
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.openStatModal, (stat: DisplayStatistics) => {
|
||||
currentStat = {...DefaultDisplayStatistics, ...stat}
|
||||
statModalIsOpen = true
|
||||
currentStat = stat
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,7 +37,9 @@ function options(emitType: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="statModalIsOpen" @close="options('next')">
|
||||
<Modal
|
||||
v-model="statModalIsOpen"
|
||||
@close="options('next')">
|
||||
<div class="statistics">
|
||||
<header>
|
||||
<div class="title">{{ store.currentDict.name }}</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'close',
|
||||
])
|
||||
|
||||
let currentSelectDict: Dict = $ref(store.currentDict)
|
||||
@@ -64,7 +64,7 @@ function changeDict() {
|
||||
|
||||
function close() {
|
||||
console.log('close')
|
||||
emit('update:modelValue', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function resetChapterList() {
|
||||
@@ -73,8 +73,7 @@ function resetChapterList() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :modelValue="props.modelValue"
|
||||
:show-close="false"
|
||||
<Modal :show-close="false"
|
||||
@close="close">
|
||||
<div class="slide">
|
||||
<div class="slide-list" :class="`step${step}`">
|
||||
@@ -110,7 +109,7 @@ function resetChapterList() {
|
||||
<div class="desc">{{ i.description }}</div>
|
||||
<div class="num">{{ i.length }}词</div>
|
||||
|
||||
<Icon icon="octicon:arrow-right-24" v-if="currentSelectDict.name === i.name"
|
||||
<Icon icon="octicon:arrow-right-24" v-if="currentSelectDict.name === i.name"
|
||||
@click.stop="step = 1"
|
||||
class="go" width="20" color="#929596"/>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/Modal/Modal.vue"
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {GITHUB} from "@/config/ENV.ts";
|
||||
|
||||
interface IProps {
|
||||
modelValue: boolean,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
modelValue: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'close',
|
||||
])
|
||||
const store = useBaseStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:modelValue="props.modelValue"
|
||||
@close="emit('update:modelValue',false)"
|
||||
@close="emit('close')"
|
||||
title="反馈">
|
||||
<div class="feedback-modal">
|
||||
<div>
|
||||
@@ -31,7 +20,7 @@ const store = useBaseStore()
|
||||
<p>or</p>
|
||||
<div class="github">
|
||||
<span>在<a :href="GITHUB" target="_blank">Github</a>上给我提一个
|
||||
<a :href="`${GITHUB}/issues`" target="_blank">Issue</a>
|
||||
<a :href="`${GITHUB}/issues`" target="_blank">Issue</a>
|
||||
</span>
|
||||
<div class="options">
|
||||
<BaseButton>
|
||||
|
||||
@@ -10,16 +10,8 @@ import {useDisableEventListener} from "@/hooks/event.ts";
|
||||
const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
interface IProps {
|
||||
modelValue: boolean,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
modelValue: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'close',
|
||||
])
|
||||
|
||||
// useDisableEventListener()
|
||||
@@ -29,9 +21,9 @@ useWatchAllSound()
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:modelValue="props.modelValue"
|
||||
@close="emit('update:modelValue',false)"
|
||||
title="设置" subTitle="修改立即生效,实时保存">
|
||||
@close="emit('close')"
|
||||
title="设置"
|
||||
subTitle="修改立即生效,实时保存">
|
||||
<div class="setting-modal">
|
||||
<div class="tabs">
|
||||
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
|
||||
@@ -140,7 +132,7 @@ useWatchAllSound()
|
||||
<div class="row">
|
||||
<label class="item-title">章节乱序</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.value1"
|
||||
<el-switch v-model="settingStore.show"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
|
||||
@@ -44,7 +44,7 @@ watch(() => settingStore.showToolbar, n => {
|
||||
<header ref="headerRef">
|
||||
<div class="content">
|
||||
<div class="info" @click="showDictModal = true">
|
||||
{{ store.dictTitle }} {{ practiceStore.repeatNumber ? ' 复习错词' : ''}}
|
||||
{{ store.dictTitle }} {{ practiceStore.repeatNumber ? ' 复习错词' : '' }}
|
||||
</div>
|
||||
<div class="options">
|
||||
<Tooltip title="切换主题">
|
||||
@@ -102,9 +102,9 @@ watch(() => settingStore.showToolbar, n => {
|
||||
color="#999"/>
|
||||
</Tooltip>
|
||||
</header>
|
||||
<DictModal v-model="showDictModal"/>
|
||||
<SettingModal v-model="showSettingModal"/>
|
||||
<FeedbackModal v-model="showFeedbackModal"/>
|
||||
<DictModal v-if="showDictModal" @close="showDictModal = false"/>
|
||||
<SettingModal v-if="showSettingModal" @close="showSettingModal = false"/>
|
||||
<FeedbackModal v-if="showFeedbackModal" @close="showFeedbackModal = false"/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -35,7 +35,6 @@ function save() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -100,10 +99,6 @@ function save() {
|
||||
<BaseButton size="small" @click="save">确定</BaseButton>
|
||||
</div>
|
||||
</MiniModal>
|
||||
<Modal
|
||||
title="A private Conversation!"
|
||||
v-model="showCustomTranslateModal">
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {onMounted, onUnmounted, toRef, toValue, watch} from "vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
|
||||
export function useWindowClick(cb: () => void) {
|
||||
onMounted(() => {
|
||||
@@ -53,13 +54,34 @@ export function useDisableEventListener() {
|
||||
})
|
||||
}
|
||||
|
||||
export function useEsc(close: () => void) {
|
||||
onMounted(() => {
|
||||
window.addEventListener('keyup', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
close()
|
||||
export function useEsc(close: () => void, watchVal?: any) {
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const id = $ref(Date.now())
|
||||
|
||||
watch(watchVal, n => {
|
||||
if (n) {
|
||||
runtimeStore.modalList.push({id, close})
|
||||
} else {
|
||||
let rIndex = runtimeStore.modalList.findIndex(item => item.id === id)
|
||||
if (rIndex > 0) {
|
||||
runtimeStore.modalList.splice(rIndex, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (watchVal() === undefined) {
|
||||
runtimeStore.modalList.push({id, close})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (watchVal() === undefined) {
|
||||
let rIndex = runtimeStore.modalList.findIndex(item => item.id === id)
|
||||
if (rIndex > 0) {
|
||||
runtimeStore.modalList.splice(rIndex, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {defineStore} from "pinia"
|
||||
|
||||
export interface RuntimeState {
|
||||
disableEventListener: boolean
|
||||
disableEventListener: boolean,
|
||||
modalList: Array<{ id: string | number, close: Function }>
|
||||
}
|
||||
|
||||
export const useRuntimeStore = defineStore('runtime', {
|
||||
state: (): RuntimeState => {
|
||||
return {
|
||||
disableEventListener: false
|
||||
}
|
||||
},
|
||||
state: (): RuntimeState => {
|
||||
return {
|
||||
disableEventListener: false,
|
||||
modalList: []
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user