+
{props.mask && (
-
+
)}
-
+
e.stopPropagation()}
>
-
-
-
{slots.default?.()}
-
+ {slots.layout
+ ? (
+ // 👉 如果有 layout 插槽,用户完全接管布局
+ slots.layout()
+ )
+ : (
+ <>
+
+
{renderContent.value}
+
+ >
+ )}
+
)}
diff --git a/code/base-project/base-vue/packages/ao-design/src/modal/modalManager.ts b/code/base-project/base-vue/packages/ao-design/src/modal/modalManager.ts
index 4c9e0b7c..fd063f89 100644
--- a/code/base-project/base-vue/packages/ao-design/src/modal/modalManager.ts
+++ b/code/base-project/base-vue/packages/ao-design/src/modal/modalManager.ts
@@ -1,23 +1,6 @@
+// modalZIndex.ts
let zIndexBase = 1000
-const modalStack: number[] = [] // 当前打开的 Modal zIndex
-export function addModal() {
- const zIndex = ++zIndexBase // 每次直接递增
- modalStack.push(zIndex)
- return zIndex
-}
-
-export function removeModal(zIndex: number) {
- const idx = modalStack.indexOf(zIndex)
- if (idx !== -1) {
- modalStack.splice(idx, 1) // 只移出 stack,不回收 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
+export function nextZIndex() {
+ return ++zIndexBase
}
diff --git a/code/base-project/base-vue/packages/ao-design/src/modal/old.tsx b/code/base-project/base-vue/packages/ao-design/src/modal/old.tsx
deleted file mode 100644
index 6719e138..00000000
--- a/code/base-project/base-vue/packages/ao-design/src/modal/old.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import type { PropType } from 'vue'
-import { computed, defineComponent, onMounted, ref, Teleport, Transition, watch } from 'vue'
-import { getPrefixCls } from '../_utils/getPrefixCls'
-import Button from '../button/index'
-import Icon from '../icon/index'
-
-export default defineComponent({
- name: 'Modal',
- props: {
- visible: {
- type: Boolean,
- default: undefined,
- },
- defaultVisible: {
- type: Boolean,
- default: false,
- },
- mask: {
- type: Boolean,
- default: true,
- },
- maskClosable: {
- type: Boolean,
- default: true,
- },
- position: {
- type: String as PropType<'top' | 'center' | 'bottom'>,
- default: 'center',
- },
- type: {
- type: String as PropType<'default' | 'info' | 'success' | 'warning' | 'danger' | 'question'>,
- default: 'default',
- },
- showClose: {
- type: Boolean,
- default: true,
- },
- confirmBtn: {
- type: null,
- default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }),
- },
- cancelBtn: {
- type: null,
- default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }),
- },
- onBeforeConfirm: {
- type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise
>,
- },
- onBeforeCancel: {
- type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise>,
- },
- destroyOnClose: {
- type: Boolean,
- default: false,
- },
- appendTo: {
- type: [String, Object] as PropType,
- default: 'body',
- },
- class: {
- type: [String, Array, Object],
- default: '',
- },
- style: {
- type: Object,
- default: () => ({}),
- },
- },
- emits: ['update:visible', 'confirm', 'cancel', 'open', 'close', 'beforeOpen', 'beforeClose'],
- setup(props, { slots, emit }) {
- const prefixCls = getPrefixCls('modal')
- const targetEl = ref(document.body)
- const confirmLoading = ref(false)
-
- const _visible = ref(props.defaultVisible)
- const _rendered = ref(props.defaultVisible)
- const _mounted = ref(props.defaultVisible)
-
- watch(() => props.visible, (val) => {
- if (val !== undefined) {
- _visible.value = val
- if (val) {
- _mounted.value = true
- _rendered.value = true
- }
- }
- }, { immediate: true })
-
- onMounted(() => {
- if (props.appendTo instanceof HTMLElement) {
- targetEl.value = props.appendTo
- }
- else if (typeof props.appendTo === 'string') {
- const el = document.querySelector(props.appendTo)
- if (el)
- targetEl.value = el as HTMLElement
- }
- })
-
- const handleMaskClick = () => {
- if (props.maskClosable) {
- emit('update:visible', false)
- }
- }
-
- const handleCancel = async () => {
- let allow = true
- if (props.onBeforeCancel) {
- try {
- allow = await new Promise((resolve) => {
- const result = props.onBeforeCancel!(ok => resolve(ok))
- if (typeof result === 'boolean')
- resolve(result)
- else if (result instanceof Promise)
- result.then(resolve).catch(() => resolve(false))
- })
- }
- catch {
- allow = false
- }
- }
-
- if (allow) {
- emit('update:visible', false)
- emit('cancel')
- confirmLoading.value = false
- }
- }
-
- const handleConfirm = async () => {
- let allow = true
- if (props.onBeforeConfirm) {
- confirmLoading.value = true
- try {
- allow = await new Promise((resolve) => {
- const result = props.onBeforeConfirm!(ok => resolve(ok))
- if (typeof result === 'boolean')
- resolve(result)
- else if (result instanceof Promise)
- result.then(resolve).catch(() => resolve(false))
- })
- }
- catch { allow = false }
- }
- if (allow) {
- emit('update:visible', false)
- emit('confirm')
- }
- confirmLoading.value = false
- }
-
- const handleBeforeEnter = () => {
- emit('beforeOpen')
- }
-
- const handleAfterEnter = () => {
- emit('open')
- }
-
- const handleBeforeLeave = () => {
- emit('beforeClose')
- }
-
- const handleAfterLeave = () => {
- _mounted.value = false
- emit('close')
- }
-
- const typeIconMap: Record = {
- success: 'ix:success-filled',
- danger: 'ix:error-filled',
- warning: 'ix:warning-filled',
- info: 'ix:info-filled',
- question: 'ix:question-filled',
- }
-
- return () => {
- const shouldRenderPopup = props.destroyOnClose
- ? _visible.value
- : _visible.value || _rendered.value
-
- return (
-
- {shouldRenderPopup && (
-
- {props.mask && (
-
-
-
- )}
-
-
-
-
-
{slots.default?.()}
-
-
-
-
-
- )}
-
- )
- }
- },
-})
diff --git a/code/base-project/base-vue/packages/ao-design/src/modal/useModal.ts b/code/base-project/base-vue/packages/ao-design/src/modal/useModal.ts
new file mode 100644
index 00000000..daae9f01
--- /dev/null
+++ b/code/base-project/base-vue/packages/ao-design/src/modal/useModal.ts
@@ -0,0 +1,158 @@
+import type { AppContext, VNode } from 'vue'
+import { createVNode, getCurrentInstance, nextTick, render } from 'vue'
+import Modal from './index'
+
+function omit, K extends keyof T>(
+ object: T,
+ keys: K[],
+): Omit {
+ const result = { ...object }
+ keys.forEach(key => delete result[key])
+ return result
+}
+
+export interface ModalOptions {
+ layout?: string | VNode | (() => VNode)
+
+ title?: string | VNode | (() => VNode)
+ content?: string | VNode | (() => VNode)
+ footer?: string | VNode | (() => VNode)
+ position?: 'top' | 'center' | 'bottom'
+ confirmBtnProps?: Record
+ cancelBtnProps?: Record
+ width?: string
+ height?: string
+ fullscreen?: boolean
+ showClose?: boolean
+ showFullscreen?: boolean
+ draggable?: boolean
+ draggableSelector?: string
+ confirmBtn?: any
+ cancelBtn?: any
+ mask?: boolean
+ maskClosable?: boolean
+ onConfirm?: () => void
+ onCancel?: () => void
+ onClose?: () => void
+}
+
+function createModal(options: ModalOptions & { type?: string }, appContext?: AppContext) {
+ let container: HTMLDivElement | null = document.createElement('div')
+ document.body.appendChild(container)
+
+ let vm: VNode
+
+ const handleConfirm = () => {
+ if (vm.component)
+ vm.component.props.visible = false
+ options.onConfirm?.()
+ }
+
+ const handleCancel = () => {
+ if (vm.component)
+ vm.component.props.visible = false
+ options.onCancel?.()
+ }
+
+ const handleClose = async () => {
+ await nextTick()
+ if (container) {
+ render(null, container)
+ document.body.removeChild(container)
+ }
+ container = null
+ options.onClose?.()
+ }
+
+ const defaultConfig = {
+ visible: true,
+ onConfirm: handleConfirm,
+ onCancel: handleCancel,
+ onClose: handleClose,
+ type: options.type ?? 'default',
+ }
+ vm = createVNode(
+ Modal,
+ {
+ ...defaultConfig,
+ ...omit(options, ['layout', 'content', 'title', 'footer', 'onConfirm', 'onCancel', 'onClose']),
+ },
+ options.layout
+ ? {
+ layout:
+ typeof options.layout === 'function'
+ ? options.layout
+ : () => options.layout,
+ }
+ : {
+ default: options.content
+ ? typeof options.content === 'function'
+ ? options.content
+ : () => options.content
+ : undefined,
+ title: options.title
+ ? typeof options.title === 'function'
+ ? options.title
+ : () => options.title
+ : undefined,
+ footer: options.footer
+ ? typeof options.footer === 'function'
+ ? options.footer
+ : () => options.footer
+ : undefined,
+ },
+ )
+
+ // 自动获取 appContext,除非手动传入
+ const instanceAppContext = appContext ?? getCurrentInstance()?.appContext ?? undefined
+ if (instanceAppContext) {
+ vm.appContext = instanceAppContext
+ }
+
+ render(vm, container)
+
+ return {
+ close: () => {
+ if (vm.component)
+ vm.component.props.visible = false
+ },
+ update: (newOptions: Partial) => {
+ if (vm.component) {
+ Object.entries(newOptions).forEach(([key, val]) => {
+ vm.component!.props[key] = val
+ })
+ }
+ },
+ }
+}
+
+type ModalType = 'default' | 'info' | 'success' | 'warning' | 'danger' | 'question'
+
+export function useModal() {
+ const open = (options: ModalOptions & { type?: ModalType }, appContext?: AppContext) =>
+ createModal({ ...options, type: options.type ?? 'default' }, appContext)
+
+ const info = (options: ModalOptions, appContext?: AppContext) =>
+ createModal({ ...options, type: 'info' }, appContext)
+
+ const success = (options: ModalOptions, appContext?: AppContext) =>
+ createModal({ ...options, type: 'success' }, appContext)
+
+ const warning = (options: ModalOptions, appContext?: AppContext) =>
+ createModal({ ...options, type: 'warning' }, appContext)
+
+ const danger = (options: ModalOptions, appContext?: AppContext) =>
+ createModal({ ...options, type: 'danger' }, appContext)
+
+ const question = (options: ModalOptions, appContext?: AppContext) =>
+ createModal({ ...options, type: 'question' }, appContext)
+
+ return {
+ open,
+ info,
+ success,
+ warning,
+ danger,
+ question,
+ }
+}
diff --git a/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx b/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx
index ed9c21df..efb99afb 100644
--- a/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx
+++ b/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx
@@ -20,7 +20,7 @@ export default defineComponent({
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
required: false,
},
- beforeConfirm: {
+ onBeforeConfirm: {
type: Function as PropType<
(done: (ok: boolean) => void) => void | boolean | Promise
>,
@@ -69,23 +69,37 @@ export default defineComponent({
return val
}
+ let promiseNumber = 0
+
const handleConfirm = async () => {
+ const currentPromiseNumber = ++promiseNumber
let allow = true
- if (props.beforeConfirm) {
+ if (props.onBeforeConfirm) {
confirmLoading.value = true
try {
allow = await new Promise((resolve) => {
- const result = props.beforeConfirm!(ok => resolve(ok))
+ let called = false
+
+ const done = (ok: boolean) => {
+ if (!called) {
+ called = true
+ resolve(ok)
+ }
+ }
+
+ const result = props.onBeforeConfirm!(done)
if (typeof result === 'boolean') {
- resolve(result)
+ done(result)
}
else if (result instanceof Promise) {
- result.then(ok => resolve(ok)).catch(() => resolve(false))
+ result
+ .then(ok => done(ok ?? true))
+ .catch(() => done(false))
}
- // 如果是用 done 回调,就等调用方触发 resolve
+ // 如果使用 done 回调,resolve 会在调用方触发
})
}
catch {
@@ -93,12 +107,12 @@ export default defineComponent({
}
}
- if (allow) {
- popVisible.value = false
- emit('confirm')
+ if (currentPromiseNumber === promiseNumber) {
+ if (allow) {
+ emit('confirm')
+ }
+ confirmLoading.value = false
}
-
- confirmLoading.value = false
}
const handleCancel = () => {
diff --git a/code/base-project/base-vue/packages/ao-design/src/spin/index.scss b/code/base-project/base-vue/packages/ao-design/src/spin/index.scss
index 745202fb..ffd39ed5 100644
--- a/code/base-project/base-vue/packages/ao-design/src/spin/index.scss
+++ b/code/base-project/base-vue/packages/ao-design/src/spin/index.scss
@@ -2,38 +2,44 @@
$spin-prefix: #{$prefix}-spin;
-.#{$spin-prefix}-wrapper {
- position: relative;
- display: inline-block;
- width: 100%;
-}
-
.#{$spin-prefix} {
- position: absolute;
- inset: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10;
-
- &-content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- color: $text-color-base;
- }
+ display: inline-block;
&-icon {
color: $primary-color;
+ font-size: 16px;
}
+
&-tip {
margin-top: 4px;
font-size: 12px;
- color: $text-color-base;
+ color: $primary-color;
}
- &.rounded {
- border-radius: inherit;
+ &.#{$spin-prefix}-has-tip {
+ text-align: center;
+ }
+
+ &.#{$spin-prefix}-loading {
+ position: relative;
+ user-select: none;
+
+ .#{$spin-prefix}-mask {
+ position: absolute;
+ inset: 0;
+ z-index: 11;
+ text-align: center;
+ background-color: $bg-color-mask-spin;
+ transition: opacity 0.1s cubic-bezier(0, 0, 1, 1);
+ user-select: none;
+
+ .#{$spin-prefix}-mask-inner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 12;
+ transform: translate(-50%, -50%);
+ }
+ }
}
}
diff --git a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx
index 00dc4281..58d4408b 100644
--- a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx
+++ b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx
@@ -1,4 +1,4 @@
-import type { PropType } from 'vue'
+import type { PropType, VNode } from 'vue'
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Icon from '../icon'
@@ -29,30 +29,50 @@ export default defineComponent({
}
return () => {
- if (!props.loading && !slots.default)
- return null
+ const tipContent = renderTip()
+ if (!props.loading || !slots.default) {
+ // loading 为 false 时的结构
+ return (
+
+
+ {tipContent &&
{tipContent}
}
+
+ )
+ }
+
+ // loading 为 true 时的结构
return (
-
- {slots.default?.()}
- {props.loading && (
-
-
-
- {renderTip() && {renderTip()}}
-
+
+ {slots.default()}
+
+
+
+
+ {tipContent &&
{tipContent}
}
- )}
+
+
)
}
diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue
index 4b2bec87..a2004fc1 100644
--- a/code/base-project/base-vue/src/App.vue
+++ b/code/base-project/base-vue/src/App.vue
@@ -1,31 +1,48 @@
-
-
-
-
- Header 拖拽示例
-
- 这是 header 拖拽
-
-
- 打开 Header 拖拽
-
+
+
+
+
+ actions
+
-
-
- 拖拽这里
+ 这里是内容
+
+
+ 这里是footer
+
+
+
+
+
- Modal 内容
-
-
-
- 打开 Ref 拖拽
-
+
+
diff --git a/code/base-project/base-vue/src/components.d.ts b/code/base-project/base-vue/src/components.d.ts
index a14b26fc..404b8a35 100644
--- a/code/base-project/base-vue/src/components.d.ts
+++ b/code/base-project/base-vue/src/components.d.ts
@@ -8,7 +8,9 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
+ ABadge: typeof import('@ao/ao-design')['Badge']
AButton: typeof import('@ao/ao-design')['Button']
+ ACard: typeof import('@ao/ao-design')['Card']
ACheckbox: typeof import('@ao/ao-design')['Checkbox']
ACheckboxGroup: typeof import('@ao/ao-design')['CheckboxGroup']
AConfigProvider: typeof import('@ao/ao-design')['ConfigProvider']