Skip to content

Commit c3c704e

Browse files
refactor: universal Modal component (#666)
* refactor: setting page * style: implement responsive layout for settings page * feat: add setting dialog on game page * feat: abstract modal popup component * refactor: all modals using Modal component * chore: fix rebase conflicts * fix: optimize Modal related component --------- Co-authored-by: fengstats <feng2860984180@163.com>
1 parent 3c6d8a5 commit c3c704e

17 files changed

+396
-549
lines changed

apps/client/components/Navbar.vue

+9-8
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,17 @@
7878
</div>
7979
</div>
8080
</header>
81-
<MainMessageBox
82-
v-model:isShowModal="isShowModal"
83-
title="提示"
84-
content="是否确认退出登录?"
85-
@confirm="signOut()"
86-
/>
81+
8782
<UserMenu
8883
v-model:open="isOpenUserMenu"
8984
@logout="handleLogout"
90-
>
91-
</UserMenu>
85+
/>
86+
<MainMessageBox
87+
v-model:show-modal="isShowModal"
88+
content="是否确认退出登录?"
89+
confirm-btn-text="确认"
90+
@confirm="signOut"
91+
/>
9292
</template>
9393

9494
<script setup lang="ts">
@@ -117,6 +117,7 @@ const HEADER_OPTIONS = [
117117
{ name: "联系我们", href: "#contact" },
118118
];
119119
120+
// TODO: 设置需要固定导航栏的页面
120121
const isStickyNavBar = computed(() => ["index", "User-Setting"].includes(route.name as string));
121122
const isScrolled = computed(() => y.value >= SCROLL_THRESHOLD);
122123
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<script lang="ts" setup>
2+
import { ref, watchEffect } from "vue";
3+
4+
defineOptions({ name: "Modal" });
5+
6+
/**
7+
* @param {boolean} showModal - 必传,是否显示弹窗,默认不显示
8+
* @param {boolean} modal - 可选,是否显示遮罩层,默认不显示
9+
* @param {string} modalColor - 可选,设置遮罩层背景色,默认 30% 黑色透明
10+
* @param {string} offsetTop - 可选,距离顶部的偏移量,默认 -8vh,因为默认使用 modal-middle 只能往上走来调整
11+
* @param {string} twClass - 可选,给 modal-box 补充一些 Tailwind CSS
12+
* @param {boolean} closeOnClickModal - 可选,是否允许点击遮罩层关闭弹窗,默认不允许
13+
*/
14+
const props = withDefaults(
15+
defineProps<{
16+
showModal: boolean;
17+
modal?: boolean;
18+
modalColor?: string;
19+
offsetTop?: string;
20+
twClass?: string;
21+
closeOnClickModal?: boolean;
22+
}>(),
23+
{
24+
showModal: false,
25+
modal: false,
26+
modalColor: "rgba(0, 0, 0, 0.3)",
27+
offsetTop: "-8vh",
28+
twClass: "",
29+
closeOnClickModal: false,
30+
},
31+
);
32+
33+
const emits = defineEmits(["close"]);
34+
35+
const modalRef = ref<HTMLDialogElement | null>(null);
36+
37+
// 检查 modalRef 是否存在
38+
function checkModalRef() {
39+
return !!modalRef.value;
40+
}
41+
42+
function show() {
43+
modalRef.value?.show();
44+
}
45+
46+
function close() {
47+
modalRef.value?.close();
48+
}
49+
50+
function showModal() {
51+
modalRef.value?.showModal();
52+
}
53+
54+
function handleClick(e: MouseEvent) {
55+
if (!checkModalRef()) return;
56+
57+
if (props.closeOnClickModal && e.target === modalRef.value) {
58+
handleClose();
59+
}
60+
}
61+
62+
function handleOpen() {
63+
if (!checkModalRef()) return;
64+
65+
props.modal ? showModal() : show();
66+
}
67+
68+
function handleClose() {
69+
if (!checkModalRef()) return;
70+
71+
close();
72+
emits("close");
73+
}
74+
75+
watchEffect(() => {
76+
if (!checkModalRef()) return;
77+
78+
// 处理外层传入的 showModal 改变时,控制弹框的显示/隐藏
79+
if (props.showModal) {
80+
props.modal ? showModal() : show();
81+
} else {
82+
close();
83+
}
84+
});
85+
86+
// 外部可以设置 ref 后调用 open/close 方法控制弹框
87+
defineExpose({
88+
open: handleOpen,
89+
close: handleClose,
90+
});
91+
</script>
92+
93+
<template>
94+
<dialog
95+
ref="modalRef"
96+
class="modal"
97+
@click="handleClick"
98+
>
99+
<div
100+
class="modal-box bg-white dark:bg-gray-800"
101+
:class="twClass"
102+
:style="{ marginTop: offsetTop }"
103+
>
104+
<slot />
105+
</div>
106+
</dialog>
107+
</template>
108+
109+
<style scoped>
110+
dialog::backdrop {
111+
background-color: v-bind(modalColor);
112+
}
113+
</style>

apps/client/components/common/ProgressBar.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="rounded-lg border dark:border-slate-400">
2+
<div class="rounded-lg border border-gray-300 dark:border-gray-600">
33
<div
44
class="h-full rounded-lg bg-gradient-to-r from-emerald-200 to-emerald-400 transition-all dark:from-emerald-300 dark:to-emerald-500"
55
:style="{ width: `${percentage}%` }"

apps/client/components/main/AuthRequired.vue

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
<template>
2-
<dialog
3-
className="modal top-[-8vh]"
4-
:open="authRequireModalState"
5-
>
2+
<CommonModal :show-modal="authRequireModalState">
63
<div className="modal-box">
74
<h3 className="font-bold text-lg mb-4">✨ 友情提示</h3>
85
<p class="py-4 text-center text-xl">注册以进行下一课的学习哦~ 😊</p>
@@ -21,7 +18,7 @@
2118
</button>
2219
</div>
2320
</div>
24-
</dialog>
21+
</CommonModal>
2522
</template>
2623

2724
<script setup lang="ts">

apps/client/components/main/Game.vue

+7-8
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,22 @@
1010
<MainSummary />
1111
<MainShare />
1212
<MainAuthRequired />
13-
<MessageBox
14-
:content="messageContent"
15-
v-model:isShowModal="isMessageShow"
13+
<!-- TODO: 暂时先不提示(有些用户正在移动端的场景下使用)-->
14+
<!-- <MainMessageBox
15+
v-model:show-modal="isMessageShow"
1616
cancel-btn-text="确定"
17-
confirmBtnText=""
18-
></MessageBox>
17+
:content="messageContent"
18+
/> -->
1919
</template>
2020

2121
<script setup lang="ts">
2222
import { onMounted } from "vue";
2323
2424
import { courseTimer } from "~/composables/courses/courseTimer";
25-
import { useDeviceTip } from "~/composables/main/game";
25+
// import { useDeviceTip } from "~/composables/main/game";
2626
import { GameMode, useGameMode } from "~/composables/user/gameMode";
27-
import MessageBox from "./MessageBox/MessageBox.vue";
2827
29-
const { isMessageShow, messageContent } = useDeviceTip();
28+
// const { isMessageShow, messageContent } = useDeviceTip();
3029
const { currentGameMode } = useGameMode();
3130
3231
onMounted(() => {
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<script setup lang="ts">
2+
import { useVModel } from "@vueuse/core";
3+
4+
const props = withDefaults(
5+
defineProps<{
6+
showModal: boolean;
7+
modal?: boolean;
8+
title?: string;
9+
content?: string;
10+
cancelBtnText?: string;
11+
confirmBtnText?: string;
12+
}>(),
13+
{
14+
showModal: false,
15+
modal: true,
16+
title: "提示",
17+
content: "你确定吗?",
18+
cancelBtnText: "取消",
19+
confirmBtnText: "",
20+
},
21+
);
22+
const emits = defineEmits(["confirm", "update:showModal"]);
23+
24+
// 可以在这个地方直接更新外层 showModal
25+
const isShowModal = useVModel(props, "showModal", emits);
26+
27+
function handleCancel() {
28+
isShowModal.value = false;
29+
}
30+
31+
function handleConfirm() {
32+
emits("confirm");
33+
handleCancel();
34+
}
35+
</script>
36+
37+
<template>
38+
<CommonModal
39+
:show-modal="isShowModal"
40+
:modal="modal"
41+
:close-on-click-modal="true"
42+
@close="handleCancel"
43+
>
44+
<h3 class="text-lg font-bold">{{ title }}</h3>
45+
<p class="py-4">{{ content }}</p>
46+
<div class="modal-action">
47+
<button
48+
class="btn mr-2"
49+
@click="handleCancel"
50+
>
51+
{{ cancelBtnText }}
52+
</button>
53+
<!-- TODO: 后续看看有没有更好的方案 -->
54+
<button
55+
v-if="confirmBtnText"
56+
class="btn"
57+
@click="handleConfirm"
58+
>
59+
{{ confirmBtnText }}
60+
</button>
61+
</div>
62+
</CommonModal>
63+
</template>

apps/client/components/main/MessageBox/MessageBox.vue

-48
This file was deleted.

apps/client/components/main/MessageBox/tests/message-box.spec.ts

-59
This file was deleted.

0 commit comments

Comments
 (0)