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 Group } from './group'
export { default as Icon } from './icon' export { default as Icon } from './icon'
export { default as Input } from './input' export { default as Input } from './input'
export { default as Modal } from './modal'
export { default as Popconfirm } from './popconfirm' export { default as Popconfirm } from './popconfirm'
export { default as Popup } from './popup' export { default as Popup } from './popup'
export { default as Radio } from './radio/radio' export { default as Radio } from './radio/radio'
@@ -19,6 +19,7 @@
@use './checkbox/index.scss' as *; @use './checkbox/index.scss' as *;
@use './switch/index.scss' as *; @use './switch/index.scss' as *;
@use './spin/index.scss' as *; @use './spin/index.scss' as *;
@use './modal/index.scss' as *;
[class*='#{variables.$prefix}-'] { [class*='#{variables.$prefix}-'] {
-webkit-box-sizing: border-box; -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, type: Boolean,
default: false, default: false,
}, },
loading: { loading: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
beforeConfirm: {
type: Function as PropType<() => boolean | Promise<boolean>>,
},
class: { class: {
type: [String, Array, Object], type: [String, Array, Object],
default: '', default: '',
@@ -48,22 +52,44 @@ export default defineComponent({
setup(props, { emit, slots }) { setup(props, { emit, slots }) {
const prefixCls = getPrefixCls('switch') const prefixCls = getPrefixCls('switch')
const _loading = ref(false)
const _checked = ref(props.modelValue) const _checked = ref(props.modelValue)
const isLoading = computed(() => props.loading || _loading.value)
watch( watch(
() => props.modelValue, () => props.modelValue,
(val) => { (val) => {
_checked.value = val _checked.value = val
}, },
) )
const isChecked = computed(() => _checked.value === props.checkedValue) const isChecked = computed(() => _checked.value === props.checkedValue)
const toggle = (e: Event) => { const toggle = async (e: Event) => {
const newValue = isChecked.value ? props.uncheckedValue : props.checkedValue if (props.disabled || isLoading.value)
_checked.value = newValue return
emit('update:modelValue', newValue) _loading.value = true
emit('change', newValue, e) 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(() => { const displayContent = computed(() => {
@@ -96,13 +122,13 @@ export default defineComponent({
{ {
[`${prefixCls}-checked`]: isChecked.value, [`${prefixCls}-checked`]: isChecked.value,
[`${prefixCls}-disabled`]: props.disabled, [`${prefixCls}-disabled`]: props.disabled,
[`${prefixCls}-loading`]: props.loading, [`${prefixCls}-loading`]: isLoading.value,
}, },
]} ]}
onClick={toggle} onClick={toggle}
> >
<span class={`${prefixCls}-handle`}> <span class={`${prefixCls}-handle`}>
{props.loading && <Spin loading size={12} />} {isLoading.value && <Spin loading size={12} />}
</span> </span>
<span class={`${prefixCls}-text-holder`}>{displayContent.value}</span> <span class={`${prefixCls}-text-holder`}>{displayContent.value}</span>
<span class={`${prefixCls}-text`}>{displayContent.value}</span> <span class={`${prefixCls}-text`}>{displayContent.value}</span>
+51 -17
View File
@@ -1,24 +1,58 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
const value = ref('') const AModal1Visible = ref(false)
const AModal2Visible = ref(false)
const AModal3Visible = ref(false)
</script> </script>
<template> <template>
<div class="main"> <button @click="AModal1Visible = true">
<ASwitch v-model="value" loading checked-value="1" unchecked-value="2" checked-content="1" unchecked-content="2" /> 打开第 1 个 AModal
{{ value }} </button>
</div>
</template>
<style lang="scss" scoped> <!-- 第 1 个 AModal -->
.main { <AModal v-model:visible="AModal1Visible" mask-closable>
width: 100%; <template #title>
height: 800px; 第 1 个 AModal
display: flex; </template>
align-items: center; <p>这是第 1 个弹窗内容</p>
justify-content: center; <template #footer>
flex-direction: column; <button @click="AModal2Visible = true">
gap: 16px; 打开第 2 个 AModal
} </button>
</style> <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'] AInput: typeof import('@ao/ao-design')['Input']
ALoading: typeof import('@ao/ao-design')['Loading'] ALoading: typeof import('@ao/ao-design')['Loading']
AMessage: typeof import('@ao/ao-design')['Message'] AMessage: typeof import('@ao/ao-design')['Message']
AModal: typeof import('@ao/ao-design')['Modal']
APopconfirm: typeof import('@ao/ao-design')['Popconfirm'] APopconfirm: typeof import('@ao/ao-design')['Popconfirm']
APopup: typeof import('@ao/ao-design')['Popup'] APopup: typeof import('@ao/ao-design')['Popup']
ARadio: typeof import('@ao/ao-design')['Radio'] 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);
}
<div style="text-align: center">订舱委托书</div> </style>
</head>
<body>
<button id="openFirst">打开第一个弹窗</button>
公司编号:CNNYYY230108 <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>
<div style="text-align: center"> <div class="dialog-wrapper" id="wrapper2" style="display: none">
<table border="1"> <div class="backdrop"></div>
<tbody> <dialog id="dialog2">
<tr> <p>这是第二个弹窗</p>
<td colspan="3"> <button id="closeSecond">关闭</button>
发货人 JIANGXIARTSENCHEMICALCO.,LTD SULPHUR&PHOSPHORUS CHEMICAL BASE GUIXI,JIANGXI,P.R.CHINA </dialog>
</td> </div>
</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>
<div style="text-align: center"> <script>
<table border="1"> const openFirstBtn = document.getElementById('openFirst')
<tbody> const dialog1 = document.getElementById('dialog1')
<tr> const wrapper1 = document.getElementById('wrapper1')
<td rowspan="3">唛头:NOMARK</td> const openSecondBtn = document.getElementById('openSecond')
<td>TOTAL:</td> const dialog2 = document.getElementById('dialog2')
<td>1</td> const wrapper2 = document.getElementById('wrapper2')
<td></td> const closeFirstBtn = document.getElementById('closeFirst')
<td>18000.00</td> const closeSecondBtn = document.getElementById('closeSecond')
<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>
联系人:裘珊珊电话:0574-87237893QQ:1597608137传真:0574-87135310宁波市海曙区药行街42号银亿环球中心B座24楼 // 打开第一个弹窗
openFirstBtn.addEventListener('click', () => {
wrapper1.style.display = 'block'
dialog1.show() // 注意:这里用 show() 而不是 showModal()
})
// 打开第二个弹窗
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>