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>