feat(modal): update modal

This commit is contained in:
zyronon
2023-09-21 18:03:56 +08:00
parent 0c5d08e15f
commit 0e6676a5af
13 changed files with 199 additions and 119 deletions

1
components.d.ts vendored
View File

@@ -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']

View File

@@ -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>

View File

@@ -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;

View File

@@ -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">

View File

@@ -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')

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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=""

View File

@@ -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">

View File

@@ -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>

View File

@@ -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)
}
}
})
}

View File

@@ -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: []
}
},
})