新增: 全局对话框服务 AppDialog,Promise 风格 appAlert/appConfirm/appPrompt 替代原生弹窗
This commit is contained in:
@@ -9,6 +9,7 @@ import { store } from '../../core/store'
|
|||||||
import { generate, polish, chat, beautifyPage, isConfigured } from '../../core/ai'
|
import { generate, polish, chat, beautifyPage, isConfigured } from '../../core/ai'
|
||||||
import { elementTypes } from '../../core/sample'
|
import { elementTypes } from '../../core/sample'
|
||||||
import { renderMd } from '../../core/markdown'
|
import { renderMd } from '../../core/markdown'
|
||||||
|
import { appPrompt, appConfirm } from '../../core/dialog'
|
||||||
import OutlinePanel from './OutlinePanel.vue'
|
import OutlinePanel from './OutlinePanel.vue'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -292,7 +293,7 @@ function onStop() {
|
|||||||
/* ---------- 生成整套 ---------- */
|
/* ---------- 生成整套 ---------- */
|
||||||
async function onGenerate() {
|
async function onGenerate() {
|
||||||
if (busy.value) return
|
if (busy.value) return
|
||||||
const topic = inputText.value.trim() || prompt('请输入演示主题,例如「远程办公的兴起与未来」')
|
const topic = inputText.value.trim() || await appPrompt('生成整套', { message: '请输入演示主题', placeholder: '例如「远程办公的兴起与未来」' })
|
||||||
if (!topic) return
|
if (!topic) return
|
||||||
if (!isConfigured()) { toast('请先配置 API Key'); emit('open-settings'); return }
|
if (!isConfigured()) { toast('请先配置 API Key'); emit('open-settings'); return }
|
||||||
inputText.value = ''
|
inputText.value = ''
|
||||||
@@ -345,9 +346,9 @@ async function onPolish() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 清空 ---------- */
|
/* ---------- 清空 ---------- */
|
||||||
function onClear() {
|
async function onClear() {
|
||||||
if (!chatLog.value.length) { toast('对话已是空的'); return }
|
if (!chatLog.value.length) { toast('对话已是空的'); return }
|
||||||
if (!confirm('清空当前 PPT 的对话记录?')) return
|
if (!(await appConfirm('清空对话记录?', '将清空当前 PPT 的对话记录', { danger: true, okText: '清空' }))) return
|
||||||
chatLog.value = []
|
chatLog.value = []
|
||||||
store.clearChat(currentChatId.value)
|
store.clearChat(currentChatId.value)
|
||||||
renderMsgs.value = []
|
renderMsgs.value = []
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<!-- =====================================================================
|
||||||
|
AppDialog.vue — 全局统一对话框宿主(替代原生 alert/confirm/prompt)
|
||||||
|
由 core/dialog.ts 的模块级状态驱动;在 App.vue 模板放置 <AppDialog /> 接线
|
||||||
|
===================================================================== -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick } from 'vue'
|
||||||
|
import { dialogState, resolveDialog } from '../../core/dialog'
|
||||||
|
|
||||||
|
const inputValue = ref('')
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
watch(() => dialogState.open.value, async (v) => {
|
||||||
|
if (!v) return
|
||||||
|
inputValue.value = dialogState.options.value?.defaultValue || ''
|
||||||
|
await nextTick()
|
||||||
|
if (dialogState.options.value?.type === 'prompt') {
|
||||||
|
inputRef.value?.focus()
|
||||||
|
inputRef.value?.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMaskKeydown(e: KeyboardEvent) {
|
||||||
|
// confirm/alert 无输入框,键盘焦点在 mask 上时也支持 Enter/Esc
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault(); e.stopPropagation()
|
||||||
|
resolveDialog({ ok: true, value: inputValue.value })
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault(); e.stopPropagation()
|
||||||
|
resolveDialog({ ok: false, value: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="dialogState.open.value && dialogState.options.value"
|
||||||
|
class="modal-mask"
|
||||||
|
tabindex="-1"
|
||||||
|
@keydown="onMaskKeydown"
|
||||||
|
@click.self="resolveDialog({ ok: false, value: '' })"
|
||||||
|
>
|
||||||
|
<div class="modal app-dialog">
|
||||||
|
<h3>{{ dialogState.options.value.title }}</h3>
|
||||||
|
<p v-if="dialogState.options.value.message" class="modal-tip">{{ dialogState.options.value.message }}</p>
|
||||||
|
<div v-if="dialogState.options.value.type === 'prompt'" class="form-row">
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="inputValue"
|
||||||
|
:placeholder="dialogState.options.value.placeholder || ''"
|
||||||
|
@keydown.enter.prevent="resolveDialog({ ok: true, value: inputValue })"
|
||||||
|
@keydown.esc.prevent="resolveDialog({ ok: false, value: '' })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button v-if="dialogState.options.value.type !== 'alert'" class="btn" @click="resolveDialog({ ok: false, value: '' })">取消</button>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
:class="dialogState.options.value.danger ? 'danger' : 'primary'"
|
||||||
|
@click="resolveDialog({ ok: true, value: inputValue })"
|
||||||
|
>{{ dialogState.options.value.okText || '确定' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { store, resolveBg } from '../../core/store'
|
import { store, resolveBg } from '../../core/store'
|
||||||
|
import { appConfirm } from '../../core/dialog'
|
||||||
import ElementView from './ElementView.vue'
|
import ElementView from './ElementView.vue'
|
||||||
|
|
||||||
const slides = computed(() => store.slides.value)
|
const slides = computed(() => store.slides.value)
|
||||||
@@ -12,10 +13,12 @@ const currentIndex = computed(() => store.currentIndex.value)
|
|||||||
function onClickItem(i: number) {
|
function onClickItem(i: number) {
|
||||||
store.setCurrentIndex(i)
|
store.setCurrentIndex(i)
|
||||||
}
|
}
|
||||||
function onClickDel(i: number, e: Event) {
|
async function onClickDel(i: number, e: Event) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
if (await appConfirm('删除这页幻灯片?', '第 ' + (i + 1) + ' 页,可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) {
|
||||||
store.delSlide(i)
|
store.delSlide(i)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function onClickAdd() {
|
function onClickAdd() {
|
||||||
store.addSlide(store.getCurrentIndex())
|
store.addSlide(store.getCurrentIndex())
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,7 @@ function onClickAdd() {
|
|||||||
:key="el.id"
|
:key="el.id"
|
||||||
:el="el"
|
:el="el"
|
||||||
:bg="slide.background"
|
:bg="slide.background"
|
||||||
|
thumb
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { store } from '../../core/store'
|
import { store } from '../../core/store'
|
||||||
import { themes } from '../../core/sample'
|
import { themes } from '../../core/sample'
|
||||||
|
import { appConfirm } from '../../core/dialog'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'present'): void
|
(e: 'present'): void
|
||||||
@@ -29,15 +30,17 @@ function onThemeChange() {
|
|||||||
store.setTheme(currentTheme.value)
|
store.setTheme(currentTheme.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function action(a: string) {
|
async function action(a: string) {
|
||||||
switch (a) {
|
switch (a) {
|
||||||
case 'add-slide': store.addSlide(store.getCurrentIndex()); break
|
case 'add-slide': store.addSlide(store.getCurrentIndex()); break
|
||||||
case 'dup-slide': store.dupSlide(); break
|
case 'dup-slide': store.dupSlide(); break
|
||||||
case 'del-slide':
|
case 'del-slide':
|
||||||
if (store.delSlide()) { /* ok */ }
|
if (await appConfirm('删除这页幻灯片?', '第 ' + (store.getCurrentIndex() + 1) + ' 页,可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) {
|
||||||
|
store.delSlide()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'reset':
|
case 'reset':
|
||||||
if (confirm('重置为内置示例?当前编辑内容将丢失(可用 Ctrl+Z 撤销)。')) {
|
if (await appConfirm('重置为内置示例?', '当前编辑内容将丢失(可用 Ctrl+Z 撤销)', { danger: true })) {
|
||||||
store.reset()
|
store.reset()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { LibItem, Slide } from '../../core/types'
|
import type { LibItem, Slide } from '../../core/types'
|
||||||
import { store, resolveBg } from '../../core/store'
|
import { store, resolveBg } from '../../core/store'
|
||||||
|
import { appPrompt, appConfirm } from '../../core/dialog'
|
||||||
import ElementView from '../editor/ElementView.vue'
|
import ElementView from '../editor/ElementView.vue'
|
||||||
|
|
||||||
const props = defineProps<{ visible: boolean }>()
|
const props = defineProps<{ visible: boolean }>()
|
||||||
@@ -91,9 +92,9 @@ function onOpen(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRename(id: string) {
|
async function onRename(id: string) {
|
||||||
const cur = store.getLibrary().find(x => x.id === id)
|
const cur = store.getLibrary().find(x => x.id === id)
|
||||||
const name = prompt('重命名为', cur ? cur.name : '')
|
const name = await appPrompt('重命名', { defaultValue: cur ? cur.name : '' })
|
||||||
if (name != null && name.trim()) {
|
if (name != null && name.trim()) {
|
||||||
store.renameInLibrary(id, name.trim())
|
store.renameInLibrary(id, name.trim())
|
||||||
bump()
|
bump()
|
||||||
@@ -106,8 +107,8 @@ function onDuplicate(id: string) {
|
|||||||
bump()
|
bump()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDelete(id: string) {
|
async function onDelete(id: string) {
|
||||||
if (confirm('删除这份演示?此操作不可撤销。')) {
|
if (await appConfirm('删除这份演示?', '此操作不可撤销', { danger: true, okText: '删除' })) {
|
||||||
store.deleteFromLibrary(id)
|
store.deleteFromLibrary(id)
|
||||||
toast('已删除')
|
toast('已删除')
|
||||||
bump()
|
bump()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { store, resolveBg } from '../../core/store'
|
import { store, resolveBg } from '../../core/store'
|
||||||
|
import { appConfirm } from '../../core/dialog'
|
||||||
import type { PageTemplate } from '../../core/types'
|
import type { PageTemplate } from '../../core/types'
|
||||||
import ElementView from '../editor/ElementView.vue'
|
import ElementView from '../editor/ElementView.vue'
|
||||||
|
|
||||||
@@ -65,8 +66,8 @@ function onPick(tpl: PageTemplate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 删除自存模板 ---------- */
|
/* ---------- 删除自存模板 ---------- */
|
||||||
function onDelete(tpl: PageTemplate) {
|
async function onDelete(tpl: PageTemplate) {
|
||||||
if (confirm('删除模板「' + tpl.name + '」?')) {
|
if (await appConfirm('删除模板', '「' + tpl.name + '」删除后不可恢复', { danger: true, okText: '删除' })) {
|
||||||
store.deleteTemplate(tpl.id)
|
store.deleteTemplate(tpl.id)
|
||||||
bump()
|
bump()
|
||||||
emit('toast', '已删除')
|
emit('toast', '已删除')
|
||||||
@@ -162,20 +163,24 @@ function onDelete(tpl: PageTemplate) {
|
|||||||
.tpl-grid {
|
.tpl-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 1em;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 卡片宽 ≈ (720 - 48 padding - 2×12 gap) / 3 ≈ 212px → scale = 212/1280 ≈ 0.166 */
|
||||||
.tpl-card {
|
.tpl-card {
|
||||||
|
--tpl-scale: 0.166;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 2px solid rgba(100, 116, 139, .2);
|
border: 1px solid var(--ui-border, #e2e8f0);
|
||||||
transition: border-color .15s, transform .15s;
|
box-shadow: 0 1px 3px rgba(15, 23, 42, .06);
|
||||||
|
transition: border-color .15s, transform .15s, box-shadow .15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tpl-card:hover {
|
.tpl-card:hover {
|
||||||
border-color: var(--primary, #4f46e5);
|
border-color: var(--ui-primary, #4f46e5);
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 23, 42, .12);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +197,7 @@ function onDelete(tpl: PageTemplate) {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 1280px;
|
width: 1280px;
|
||||||
height: 720px;
|
height: 720px;
|
||||||
transform: scale(0.094); /* 约 120px 宽 */
|
transform: scale(var(--tpl-scale));
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -201,25 +206,29 @@ function onDelete(tpl: PageTemplate) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .4em;
|
gap: .4em;
|
||||||
padding: .5em .6em;
|
padding: .45em .6em;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tpl-name {
|
.tpl-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tpl-badge {
|
.tpl-badge {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
background: rgba(100, 116, 139, .15);
|
background: rgba(100, 116, 139, .15);
|
||||||
color: var(--muted, #64748b);
|
color: var(--ui-muted, #64748b);
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tpl-badge.user {
|
.tpl-badge.user {
|
||||||
background: rgba(79, 70, 229, .12);
|
background: var(--ui-primary-soft, #eef2ff);
|
||||||
color: var(--primary, #4f46e5);
|
color: var(--ui-primary, #4f46e5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tpl-del {
|
.tpl-del {
|
||||||
@@ -227,10 +236,13 @@ function onDelete(tpl: PageTemplate) {
|
|||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tpl-del:hover {
|
.tpl-del:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: #e11d48;
|
color: var(--ui-danger, #e11d48);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
* dialog.ts — 全局对话框服务(替代原生 alert/confirm/prompt)
|
||||||
|
*
|
||||||
|
* Promise 风格:await appConfirm('删除?') → boolean
|
||||||
|
* 渲染走 AppDialogHost(主 app 内 Teleport 到 body),
|
||||||
|
* App.vue 模板放置 <AppDialogHost /> 即接线,无需手动 mount
|
||||||
|
* ===================================================================== */
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface DialogOptions {
|
||||||
|
type: 'alert' | 'confirm' | 'prompt'
|
||||||
|
title: string
|
||||||
|
message?: string
|
||||||
|
/** prompt 默认值 */
|
||||||
|
defaultValue?: string
|
||||||
|
/** input placeholder */
|
||||||
|
placeholder?: string
|
||||||
|
/** 确认按钮文案(默认「确定」) */
|
||||||
|
okText?: string
|
||||||
|
/** 危险操作样式(确认按钮红色) */
|
||||||
|
danger?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogResult { ok: boolean; value: string }
|
||||||
|
type Resolver = (r: DialogResult) => void
|
||||||
|
|
||||||
|
/* ---------- 模块级响应式状态(AppDialogHost 渲染消费) ---------- */
|
||||||
|
export const dialogState = {
|
||||||
|
open: ref(false),
|
||||||
|
options: ref<DialogOptions | null>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolver: Resolver | null = null
|
||||||
|
|
||||||
|
/** 由 AppDialogHost 的 close 事件回调 */
|
||||||
|
export function resolveDialog(r: DialogResult) {
|
||||||
|
dialogState.open.value = false
|
||||||
|
dialogState.options.value = null
|
||||||
|
if (resolver) { resolver(r); resolver = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(opts: DialogOptions): Promise<DialogResult> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
resolver = resolve
|
||||||
|
dialogState.options.value = opts
|
||||||
|
dialogState.open.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 对外 API ---------- */
|
||||||
|
|
||||||
|
/** 信息提示(替代 alert) */
|
||||||
|
export function appAlert(title: string, message?: string) {
|
||||||
|
return show({ type: 'alert', title, message })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认(替代 confirm)→ Promise<boolean> */
|
||||||
|
export function appConfirm(title: string, message?: string, opts?: { okText?: string; danger?: boolean }) {
|
||||||
|
return show({ type: 'confirm', title, message, okText: opts?.okText, danger: opts?.danger }).then(r => r.ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入(替代 prompt)→ Promise<string | null>(取消为 null) */
|
||||||
|
export function appPrompt(title: string, opts?: { message?: string; defaultValue?: string; placeholder?: string }) {
|
||||||
|
return show({ type: 'prompt', title, message: opts?.message, defaultValue: opts?.defaultValue, placeholder: opts?.placeholder })
|
||||||
|
.then(r => (r.ok ? r.value : null))
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* dialog.test.ts — 全局对话框服务与宿主组件闭环验证(Teleport 后从 document 查询) */
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
|
import AppDialog from '../src/components/common/AppDialog.vue'
|
||||||
|
import { appAlert, appConfirm, appPrompt, dialogState, resolveDialog } from '../src/core/dialog'
|
||||||
|
|
||||||
|
/** 触发 body 上(Teleport)按钮点击 */
|
||||||
|
function clickBtn(idx: number) {
|
||||||
|
const btns = document.body.querySelectorAll('.modal-actions button')
|
||||||
|
;(btns[idx] as HTMLElement).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dialog 服务 + AppDialog 宿主', () => {
|
||||||
|
it('appPrompt 打开对话框 → 输入值提交', async () => {
|
||||||
|
const wrapper = mount(AppDialog, { attachTo: document.body })
|
||||||
|
const p = appPrompt('生成整套', { placeholder: '主题' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(dialogState.open.value).toBe(true)
|
||||||
|
const input = document.body.querySelector('.modal-mask input') as HTMLInputElement
|
||||||
|
expect(input).not.toBeNull()
|
||||||
|
input.value = '远程办公'
|
||||||
|
input.dispatchEvent(new Event('input'))
|
||||||
|
await flushPromises()
|
||||||
|
clickBtn(1) // 确定
|
||||||
|
await expect(p).resolves.toBe('远程办公')
|
||||||
|
expect(dialogState.open.value).toBe(false)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appConfirm 取消 → false;确定 → true', async () => {
|
||||||
|
const wrapper = mount(AppDialog, { attachTo: document.body })
|
||||||
|
const p1 = appConfirm('删除?', '不可恢复', { danger: true })
|
||||||
|
await flushPromises()
|
||||||
|
clickBtn(0) // 取消
|
||||||
|
await expect(p1).resolves.toBe(false)
|
||||||
|
|
||||||
|
const p2 = appConfirm('删除?')
|
||||||
|
await flushPromises()
|
||||||
|
clickBtn(1) // 确定
|
||||||
|
await expect(p2).resolves.toBe(true)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appAlert 无输入框,单按钮关闭', async () => {
|
||||||
|
const wrapper = mount(AppDialog, { attachTo: document.body })
|
||||||
|
const p = appAlert('提示', '内容')
|
||||||
|
await flushPromises()
|
||||||
|
expect(document.body.querySelector('.modal-mask input')).toBeNull()
|
||||||
|
expect(document.body.querySelectorAll('.modal-actions button').length).toBe(1)
|
||||||
|
clickBtn(0)
|
||||||
|
await expect(p).resolves.toEqual({ ok: true, value: '' })
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('对话框 DOM 挂到 body(Teleport)且带 modal-mask/modal 类', async () => {
|
||||||
|
const wrapper = mount(AppDialog, { attachTo: document.body })
|
||||||
|
appAlert('标题')
|
||||||
|
await flushPromises()
|
||||||
|
const mask = document.body.querySelector('.modal-mask')
|
||||||
|
expect(mask).not.toBeNull()
|
||||||
|
expect(mask!.querySelector('.modal')).not.toBeNull()
|
||||||
|
resolveDialog({ ok: true, value: '' })
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user