This commit is contained in:
oneao committed 2025-09-17 21:55:31 +08:00
1 parent 27263365ad
commit 8eedf281fd
9 files changed
+338 -83

No files matched your search

@@ -6,6 +6,7 @@ export { default as Drawer } from './drawer'
export { default as Group } from './group'
export { default as Icon } from './icon'
export { default as Input } from './input'
export { default as Modal } from './modal'
export { default as Popconfirm } from './popconfirm'
export { default as Popup } from './popup'
export { default as Radio } from './radio/radio'
@@ -19,6 +19,7 @@
@use './checkbox/index.scss' as *;
@use './switch/index.scss' as *;
@use './spin/index.scss' as *;
@use './modal/index.scss' as *;
[class*='#{variables.$prefix}-'] {
-webkit-box-sizing: border-box;
@@ -0,0 +1,67 @@
@use '../_styles/_variables.scss' as *;
$modal-prefix: '#{$prefix}-modal';
.#{$modal-prefix}-wrapper {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.#{$modal-prefix}-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
}
.#{$modal-prefix} {
position: relative;
z-index: 1001;
background: #fff;
border-radius: 8px;
min-width: 300px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
}
.#{$modal-prefix}-header {
padding: 12px 16px;
font-weight: 600;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.#{$modal-prefix}-close {
border: none;
background: transparent;
font-size: 18px;
cursor: pointer;
}
.#{$modal-prefix}-body {
padding: 16px;
}
.#{$modal-prefix}-footer {
padding: 12px 16px;
border-top: 1px solid #eee;
text-align: right;
}
.#{$modal-prefix}-fade-enter-active,
.#{$modal-prefix}-fade-leave-active {
transition: opacity 0.2s ease;
}
.#{$modal-prefix}-fade-enter-from,
.#{$modal-prefix}-fade-leave-to {
opacity: 0;
}
@@ -0,0 +1,85 @@
import { defineComponent, ref, Teleport, Transition, watch } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import { addModal, getMaskZIndex, removeModal } from './modalManager'
export default defineComponent({
name: 'Modal',
props: {
visible: { type: Boolean, default: false },
mask: { type: Boolean, default: true },
maskClosable: { type: Boolean, default: true },
class: { type: [String, Array, Object], default: '' },
style: { type: Object, default: () => ({}) },
lazy: { type: Boolean, default: true }, // 是否懒加载
},
emits: ['update:visible', 'close'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('modal')
const zIndex = ref(0)
const isMounted = ref(false) // DOM 是否挂载
// 打开 Modal
const openModal = () => {
zIndex.value = addModal()
isMounted.value = true
}
// 关闭 Modal
const closeModal = () => {
removeModal(zIndex.value)
zIndex.value = 0
// 不卸载 DOM,仍保持 isMounted = true
}
watch(
() => props.visible,
(val) => {
if (val)
openModal()
else closeModal()
},
{ immediate: true },
)
const handleMaskClick = () => {
if (props.maskClosable) {
emit('update:visible', false)
emit('close')
}
}
const handleClose = () => {
emit('update:visible', false)
emit('close')
}
return () => (
<Teleport to="body">
{/* DOM 永远挂载,只用 v-show 控制显示 */}
<div
class={`${prefixCls}-wrapper`}
style={{
zIndex: zIndex.value,
display: props.visible ? 'flex' : 'none',
}}
>
{props.mask && (
<div
class={`${prefixCls}-mask`}
style={{ zIndex: getMaskZIndex() }}
onClick={handleMaskClick}
/>
)}
<div class={[prefixCls, props.class]} style={{ ...props.style, zIndex: zIndex.value }}>
<div class={`${prefixCls}-header`}>
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
<button class={`${prefixCls}-close`} onClick={handleClose}>×</button>
</div>
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
{slots.footer && <div class={`${prefixCls}-footer`}>{slots.footer()}</div>}
</div>
</div>
</Teleport>
)
},
})
@@ -0,0 +1,31 @@
let zIndexBase = 1000
const modalStack: number[] = [] // 当前打开的 Modal zIndex
const freeZIndex: number[] = [] // 已关闭 Modal 可复用 zIndex
export function addModal() {
let zIndex: number
if (freeZIndex.length > 0) {
zIndex = freeZIndex.pop()!
}
else {
zIndex = ++zIndexBase
}
modalStack.push(zIndex)
return zIndex
}
export function removeModal(zIndex: number) {
const idx = modalStack.indexOf(zIndex)
if (idx !== -1) {
modalStack.splice(idx, 1)
freeZIndex.push(zIndex) // 回收 zIndex
}
}
export function getTopZIndex() {
return modalStack.length > 0 ? modalStack[modalStack.length - 1] : zIndexBase
}
export function getMaskZIndex() {
return modalStack.length > 0 ? modalStack[modalStack.length - 1] - 1 : zIndexBase
}
@@ -31,10 +31,14 @@ export default defineComponent({
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
beforeConfirm: {
type: Function as PropType<() => boolean | Promise<boolean>>,
},
class: {
type: [String, Array, Object],
default: '',
@@ -48,22 +52,44 @@ export default defineComponent({
setup(props, { emit, slots }) {
const prefixCls = getPrefixCls('switch')
const _loading = ref(false)
const _checked = ref(props.modelValue)
const isLoading = computed(() => props.loading || _loading.value)
watch(
() => props.modelValue,
(val) => {
_checked.value = val
},
)
const isChecked = computed(() => _checked.value === props.checkedValue)
const toggle = (e: Event) => {
const newValue = isChecked.value ? props.uncheckedValue : props.checkedValue
_checked.value = newValue
const toggle = async (e: Event) => {
if (props.disabled || isLoading.value)
return
emit('update:modelValue', newValue)
emit('change', newValue, e)
_loading.value = true
let allow = true
if (props.beforeConfirm) {
try {
allow = await Promise.resolve(props.beforeConfirm())
}
catch {
allow = false
}
}
if (allow) {
const newValue = isChecked.value ? props.uncheckedValue : props.checkedValue
_checked.value = newValue
emit('update:modelValue', newValue)
emit('change', newValue, e)
}
_loading.value = false
}
const displayContent = computed(() => {
@@ -96,13 +122,13 @@ export default defineComponent({
{
[`${prefixCls}-checked`]: isChecked.value,
[`${prefixCls}-disabled`]: props.disabled,
[`${prefixCls}-loading`]: props.loading,
[`${prefixCls}-loading`]: isLoading.value,
},
]}
onClick={toggle}
>
<span class={`${prefixCls}-handle`}>
{props.loading && <Spin loading size={12} />}
{isLoading.value && <Spin loading size={12} />}
</span>
<span class={`${prefixCls}-text-holder`}>{displayContent.value}</span>
<span class={`${prefixCls}-text`}>{displayContent.value}</span>
+51 -17
View File
@@ -1,24 +1,58 @@
<script setup lang="ts">
import { ref } from 'vue'
const value = ref('')
const AModal1Visible = ref(false)
const AModal2Visible = ref(false)
const AModal3Visible = ref(false)
</script>
<template>
<div class="main">
<ASwitch v-model="value" loading checked-value="1" unchecked-value="2" checked-content="1" unchecked-content="2" />
{{ value }}
</div>
</template>
<button @click="AModal1Visible = true">
打开第 1 个 AModal
</button>
<style lang="scss" scoped>
.main {
width: 100%;
height: 800px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 16px;
}
</style>
<!-- 第 1 个 AModal -->
<AModal v-model:visible="AModal1Visible" mask-closable>
<template #title>
第 1 个 AModal
</template>
<p>这是第 1 个弹窗内容</p>
<template #footer>
<button @click="AModal2Visible = true">
打开第 2 个 AModal
</button>
<button @click="AModal1Visible = false">
关闭
</button>
</template>
</AModal>
<!-- 第 2 个 AModal -->
<AModal v-model:visible="AModal2Visible" mask-closable>
<template #title>
第 2 个 AModal
</template>
<p>这是第 2 个弹窗内容</p>
<template #footer>
<button @click="AModal3Visible = true">
打开第 3 个 AModal
</button>
<button @click="AModal2Visible = false">
关闭
</button>
</template>
</AModal>
<!-- 第 3 个 AModal -->
<AModal v-model:visible="AModal3Visible" mask-closable>
<template #title>
第 3 个 AModal
</template>
<p>这是第 3 个弹窗内容</p>
<template #footer>
<button @click="AModal3Visible = false">
关闭
</button>
</template>
</AModal>
</template>
+1
View File
@@ -18,6 +18,7 @@ declare module 'vue' {
AInput: typeof import('@ao/ao-design')['Input']
ALoading: typeof import('@ao/ao-design')['Loading']
AMessage: typeof import('@ao/ao-design')['Message']
AModal: typeof import('@ao/ao-design')['Modal']
APopconfirm: typeof import('@ao/ao-design')['Popconfirm']
APopup: typeof import('@ao/ao-design')['Popup']
ARadio: typeof import('@ao/ao-design')['Radio']
+68 -59
View File
@@ -1,64 +1,73 @@
# 驰诺国际货运有限公司
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dialog Multi-Level Demo</title>
<style>
dialog {
border: none;
border-radius: 8px;
padding: 20px;
width: 300px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
</style>
</head>
<body>
<button id="openFirst">打开第一个弹窗</button>
<div style="text-align: center">订舱委托书</div>
<div class="dialog-wrapper" id="wrapper1" style="display: none">
<div class="backdrop"></div>
<dialog id="dialog1">
<p>这是第一个弹窗</p>
<button id="openSecond">打开第二个弹窗</button>
<button id="closeFirst">关闭</button>
</dialog>
</div>
公司编号:CNNYYY230108
<div class="dialog-wrapper" id="wrapper2" style="display: none">
<div class="backdrop"></div>
<dialog id="dialog2">
<p>这是第二个弹窗</p>
<button id="closeSecond">关闭</button>
</dialog>
</div>
<div style="text-align: center">
<table border="1">
<tbody>
<tr>
<td colspan="3">
发货人 JIANGXIARTSENCHEMICALCO.,LTD SULPHUR&PHOSPHORUS CHEMICAL BASE GUIXI,JIANGXI,P.R.CHINA
</td>
</tr>
<tr>
<td colspan="3">
收货人 ARTSEN?CHEMICAL?AMERICA?LLC 19?LOVELAND?STREET MADISON,?NJ?07940 T:?908-419-0704??F:973-520-8290
ATTN:?WALTER?CHENG
</td>
</tr>
<tr>
<td colspan="3">
通知人 ARTSEN?CHEMICAL?AMERICA?LLC 19?LOVELAND?STREET MADISON,?NJ?07940 T:?908-419-0704??F:973-520-8290
ATTN:?WALTER?CHENG
</td>
</tr>
<tr>
<td>船公司</td>
<td>船期 2023/01/20</td>
<td>航名/航次 XIN DA YANG ZHOU 086E</td>
</tr>
<tr>
<td>起运港 NINGBO, CHINA</td>
<td colspan="2">目的港 HOUSTON</td>
</tr>
</tbody>
</table>
</div>
<script>
const openFirstBtn = document.getElementById('openFirst')
const dialog1 = document.getElementById('dialog1')
const wrapper1 = document.getElementById('wrapper1')
const openSecondBtn = document.getElementById('openSecond')
const dialog2 = document.getElementById('dialog2')
const wrapper2 = document.getElementById('wrapper2')
const closeFirstBtn = document.getElementById('closeFirst')
const closeSecondBtn = document.getElementById('closeSecond')
<div style="text-align: center">
<table border="1">
<tbody>
<tr>
<td rowspan="3">唛头:NOMARK</td>
<td>TOTAL:</td>
<td>1</td>
<td></td>
<td>18000.00</td>
<td>30.00</td>
</tr>
<tr>
<td colspan="5">箱量:1X20'TKFCL</td>
</tr>
<tr>
<td colspan="5">运输条款:FREIGHTPREPAID</td>
</tr>
<tr>
<td colspan="6">备注:</td>
</tr>
</tbody>
</table>
</div>
// 打开第一个弹窗
openFirstBtn.addEventListener('click', () => {
wrapper1.style.display = 'block'
dialog1.show() // 注意:这里用 show() 而不是 showModal()
})
联系人:裘珊珊电话:0574-87237893QQ:1597608137传真:0574-87135310宁波市海曙区药行街42号银亿环球中心B座24楼
// 打开第二个弹窗
openSecondBtn.addEventListener('click', () => {
wrapper2.style.display = 'block'
dialog2.show() // 用 show() 避免冲突
})
// 关闭第一个弹窗
closeFirstBtn.addEventListener('click', () => {
dialog1.close()
wrapper1.style.display = 'none'
})
// 关闭第二个弹窗
closeSecondBtn.addEventListener('click', () => {
dialog2.close()
wrapper2.style.display = 'none'
})
</script>
</body>
</html>