u
This commit is contained in:
1 parent
d37d92996a
commit
a8f3e58a27
25 files changed
+1401
-399
No files matched your search
@@ -103,6 +103,7 @@ $bg-color-secondarycomponent: var(--ao-bg-color-secondarycomponent);
|
||||
$bg-color-secondarycomponent-hover: var(--ao-bg-color-secondarycomponent-hover);
|
||||
|
||||
$bg-color-mask: var(--ao-bg-color-mask);
|
||||
$bg-color-mask-spin: var(--ao-bg-color-mask-spin);
|
||||
|
||||
// shadow
|
||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
--ao-bg-color-secondarycomponent-hover: #5e5e5e;
|
||||
|
||||
--ao-bg-color-mask: rgba(23, 23, 26, 0.6);
|
||||
--ao-bg-color-mask-spin: rgba(51, 51, 51, 0.6);
|
||||
|
||||
// shadow阴影
|
||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
--ao-bg-color-secondarycomponent-hover: #c6c6c6;
|
||||
|
||||
--ao-bg-color-mask: rgba(29, 33, 41, 0.6);
|
||||
--ao-bg-color-mask-spin: rgba(255, 255, 255, 0.6);
|
||||
|
||||
// shadow阴影
|
||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<ABadge count="12">
|
||||
<AButton>测试</AButton>
|
||||
</ABadge>
|
||||
|
||||
<ABadge type="dot">
|
||||
<AButton>测试</AButton>
|
||||
</ABadge>
|
||||
|
||||
<ABadge type="custom">
|
||||
<template #content>
|
||||
<AIcon icon="material-symbols:10k-outline-rounded" />
|
||||
</template>
|
||||
<AButton>测试</AButton>
|
||||
</ABadge>
|
||||
|
||||
<ABadge count="12" :offset="[10, -5]">
|
||||
<AButton>测试</AButton>
|
||||
</ABadge>
|
||||
|
||||
<ABadge count="12" color="blue">
|
||||
<AButton>测试</AButton>
|
||||
</ABadge>
|
||||
|
||||
<ABadge count="12" type="ribbon" ribbon-text="123" background-color="red">
|
||||
<div
|
||||
style="
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
"
|
||||
>
|
||||
测试卡片
|
||||
</div>
|
||||
</ABadge>
|
||||
|
||||
<ABadge count="12" type="ribbon" ribbon-text="123" background-color="red" color="blue">
|
||||
<div
|
||||
style="
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
"
|
||||
>
|
||||
测试卡片
|
||||
</div>
|
||||
</ABadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
height: 800px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
@use '../_styles/_variables.scss' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$badge-prefix: '#{$prefix}-badge';
|
||||
|
||||
.#{$badge-prefix} {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.#{$badge-prefix}-dot,
|
||||
.#{$badge-prefix}-circle,
|
||||
.#{$badge-prefix}-custom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: $text-color-anti;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
transform: translate(50%, -50%);
|
||||
transform-origin: 100% 0;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.#{$badge-prefix}-circle {
|
||||
padding: 0px 6px;
|
||||
min-width: 8px;
|
||||
height: 20px;
|
||||
background-color: $danger-color;
|
||||
line-height: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 0 2px $bg-color-container;
|
||||
}
|
||||
|
||||
.#{$badge-prefix}-dot {
|
||||
right: 1px;
|
||||
margin-top: 1px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: $danger-color;
|
||||
box-shadow: 0 0 0 2px $bg-color-container;
|
||||
}
|
||||
|
||||
.#{$badge-prefix}-custom {
|
||||
color: $text-color-base;
|
||||
background-color: $bg-color-container;
|
||||
}
|
||||
|
||||
.#{$badge-prefix}-ribbon {
|
||||
margin: 0;
|
||||
padding: 0 6px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
white-space: nowrap;
|
||||
background-color: $primary-color;
|
||||
border-radius: 4px;
|
||||
inset-inline-end: -8px;
|
||||
border-end-end-radius: 0;
|
||||
color: $primary-color;
|
||||
|
||||
&-text {
|
||||
color: $text-color-anti;
|
||||
}
|
||||
|
||||
&-corner {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 8px 8px 0 0;
|
||||
border-color: currentColor transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Badge',
|
||||
props: {
|
||||
type: {
|
||||
type: String as () => 'circle' | 'dot' | 'custom' | 'ribbon',
|
||||
default: 'circle',
|
||||
},
|
||||
count: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
},
|
||||
minCount: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: undefined,
|
||||
},
|
||||
maxCount: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
},
|
||||
showZero: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ribbonText: {
|
||||
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||
default: '',
|
||||
},
|
||||
offset: {
|
||||
type: Array as PropType<(string | number)[]>,
|
||||
default: undefined,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
class: {
|
||||
type: [String, Array, Object],
|
||||
default: '',
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const prefixCls = getPrefixCls('badge')
|
||||
|
||||
const displayCount = computed(() => {
|
||||
if (props.type !== 'circle')
|
||||
return props.count
|
||||
|
||||
let countNum = Number(props.count)
|
||||
const maxNum = props.maxCount != null ? Number(props.maxCount) : null
|
||||
const minNum = props.minCount != null ? Number(props.minCount) : null
|
||||
|
||||
const isCountNumeric = Number.isFinite(countNum)
|
||||
const isMaxNumeric = maxNum !== null && Number.isFinite(maxNum)
|
||||
const isMinNumeric = minNum !== null && Number.isFinite(minNum)
|
||||
|
||||
// 如果传了 minCount,showZero 不生效
|
||||
if (!isMinNumeric && !props.showZero && countNum === 0)
|
||||
return undefined
|
||||
|
||||
// 先处理 minCount
|
||||
if (isCountNumeric && isMinNumeric && countNum < (minNum as number)) {
|
||||
countNum = minNum as number
|
||||
}
|
||||
|
||||
// 再处理 maxCount
|
||||
if (isCountNumeric && isMaxNumeric && countNum > (maxNum as number)) {
|
||||
return `${maxNum}+`
|
||||
}
|
||||
|
||||
return countNum
|
||||
})
|
||||
|
||||
return () => {
|
||||
const supStyle: Record<string, string> = {}
|
||||
|
||||
if (props.offset && props.offset.length === 2) {
|
||||
const [x, y] = props.offset
|
||||
supStyle.top = typeof y === 'number' ? `${y}px` : y
|
||||
supStyle.right = typeof x === 'number' ? `${x}px` : x
|
||||
}
|
||||
|
||||
// backgroundColor
|
||||
if (props.backgroundColor) {
|
||||
if (props.type === 'ribbon') {
|
||||
supStyle.backgroundColor = props.backgroundColor
|
||||
supStyle.color = props.backgroundColor
|
||||
}
|
||||
else {
|
||||
supStyle.color = props.backgroundColor
|
||||
}
|
||||
}
|
||||
const ribbonTextStyle: Record<string, string> = {}
|
||||
|
||||
// color
|
||||
if (props.color) {
|
||||
if (props.type === 'ribbon') {
|
||||
ribbonTextStyle.color = props.color
|
||||
}
|
||||
else {
|
||||
supStyle.color = props.color
|
||||
}
|
||||
}
|
||||
|
||||
let supNode = null
|
||||
|
||||
if (props.type === 'dot') {
|
||||
supNode = <sup class={`${prefixCls}-dot`} style={supStyle}></sup>
|
||||
}
|
||||
else if (props.type === 'circle' && displayCount.value !== undefined) {
|
||||
supNode = (
|
||||
<sup class={`${prefixCls}-circle`} style={supStyle}>
|
||||
{displayCount.value}
|
||||
</sup>
|
||||
)
|
||||
}
|
||||
else if (props.type === 'custom') {
|
||||
supNode = (
|
||||
<sup class={`${prefixCls}-custom`} style={supStyle}>
|
||||
{slots.content?.()}
|
||||
</sup>
|
||||
)
|
||||
}
|
||||
else if (props.type === 'ribbon') {
|
||||
const renderRibbonText = () => {
|
||||
if (slots.ribbon)
|
||||
return slots.ribbon()
|
||||
if (typeof props.ribbonText === 'function')
|
||||
return props.ribbonText()
|
||||
return props.ribbonText
|
||||
}
|
||||
|
||||
supNode = (
|
||||
<sup class={`${prefixCls}-ribbon`} style={supStyle}>
|
||||
<div class={`${prefixCls}-ribbon-text`} style={ribbonTextStyle}>
|
||||
{renderRibbonText()}
|
||||
</div>
|
||||
<div class={`${prefixCls}-ribbon-corner`}></div>
|
||||
</sup>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={[prefixCls, props.class]} style={{ ...props.style }}>
|
||||
{slots.default?.()}
|
||||
{supNode}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
function toggleLoading() {
|
||||
loading.value = !loading.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main">
|
||||
<!-- 卡片 -->
|
||||
<ACard
|
||||
:loading="loading"
|
||||
title="标题"
|
||||
sub-title="副标题"
|
||||
style="width: 400px;"
|
||||
hover-shadow
|
||||
cover="https://tdesign.gtimg.com/site/source/card-demo.png"
|
||||
>
|
||||
<template #actions>
|
||||
actions
|
||||
</template>
|
||||
|
||||
这里是内容
|
||||
|
||||
<template #footer>
|
||||
这里是footer
|
||||
</template>
|
||||
</ACard>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<button style="margin-top: 16px;" @click="toggleLoading">
|
||||
{{ loading ? '关闭 Loading' : '开启 Loading' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
height: 800px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
@use '../_styles/_variables.scss' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$card-prefix: '#{$prefix}-card';
|
||||
|
||||
.#{$card-prefix} {
|
||||
font: 14px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
border-radius: $border-radius;
|
||||
background-color: $bg-color-container;
|
||||
color: $text-color-base;
|
||||
|
||||
&.#{$card-prefix}-hover {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
box-shadow: $shadow-surface;
|
||||
}
|
||||
|
||||
.#{$card-prefix}-cover {
|
||||
margin-top: -1px;
|
||||
margin-inline-start: -1px;
|
||||
margin-inline-end: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.#{$card-prefix}-border {
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.#{$card-prefix}-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&.#{$card-prefix}-separator {
|
||||
border-color: $separator-color;
|
||||
}
|
||||
|
||||
.#{$card-prefix}-header-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
.#{$card-prefix}-title {
|
||||
font-size: 16px;
|
||||
color: $text-color-base;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
.#{$card-prefix}-subtitle {
|
||||
font-size: 14px;
|
||||
color: $text-color-secondary;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$card-prefix}-actions {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$card-prefix}-body {
|
||||
display: flow-root;
|
||||
padding: 16px;
|
||||
}
|
||||
.#{$card-prefix}-footer {
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
&.#{$card-prefix}-separator {
|
||||
border-color: $separator-color;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.#{$card-prefix}-cover {
|
||||
display: flow-root;
|
||||
img {
|
||||
display: flow-root;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Spin from '../spin/index'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Card',
|
||||
props: {
|
||||
title: {
|
||||
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||
default: '',
|
||||
},
|
||||
subTitle: {
|
||||
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||
default: '',
|
||||
},
|
||||
hoverShadow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 分割线
|
||||
separator: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 边框线
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
cover: {
|
||||
type: String,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingProp: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
class: {
|
||||
type: [String, Array, Object],
|
||||
default: '',
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const prefixCls = getPrefixCls('card')
|
||||
|
||||
const renderHeader = () => {
|
||||
if (slots.header) {
|
||||
return <div class={`${prefixCls}-header`}>{slots.header()}</div>
|
||||
}
|
||||
|
||||
const titleContent = slots.title?.() ?? (typeof props.title === 'function' ? props.title() : props.title)
|
||||
const subTitleContent = slots.subTitle?.() ?? (typeof props.subTitle === 'function' ? props.subTitle() : props.subTitle)
|
||||
const actionsContent = slots.actions?.()
|
||||
|
||||
if (!titleContent && !subTitleContent && !actionsContent)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-header`,
|
||||
{ [`${prefixCls}-separator`]: props.separator },
|
||||
]}
|
||||
>
|
||||
<div class={`${prefixCls}-header-wrapper`}>
|
||||
<div>
|
||||
{titleContent && <div class={`${prefixCls}-title`}>{titleContent}</div>}
|
||||
{subTitleContent && <div class={`${prefixCls}-sub-title`}>{subTitleContent}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{actionsContent && <div class={`${prefixCls}-header-actions`}>{actionsContent}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderBody = () => {
|
||||
if (!slots.default)
|
||||
return null
|
||||
return <div class={`${prefixCls}-body`}>{slots.default()}</div>
|
||||
}
|
||||
|
||||
const renderFooter = () => {
|
||||
if (!slots.footer)
|
||||
return null
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-footer`,
|
||||
{ [`${prefixCls}-separator`]: props.separator },
|
||||
]}
|
||||
>
|
||||
{slots.footer()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const renderCover = () => {
|
||||
if (slots.cover) {
|
||||
return <div class={`${prefixCls}-cover`}>{slots.cover()}</div>
|
||||
}
|
||||
|
||||
if (props.cover) {
|
||||
return (
|
||||
<div class={`${prefixCls}-cover`}>
|
||||
<img src={props.cover} alt="" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return () => {
|
||||
const cardNode = (
|
||||
<div
|
||||
class={[
|
||||
prefixCls,
|
||||
props.class,
|
||||
{ [`${prefixCls}-hover`]: props.hoverShadow },
|
||||
{ [`${prefixCls}-border`]: props.border },
|
||||
]}
|
||||
style={{ ...props.style }}
|
||||
>
|
||||
{renderHeader()}
|
||||
{renderBody()}
|
||||
{renderCover()}
|
||||
{renderFooter()}
|
||||
</div>
|
||||
)
|
||||
|
||||
// 如果 loading 就包一层 Spin
|
||||
if (props.loading) {
|
||||
return (
|
||||
<Spin {...props.loadingProp} loading={props.loading}>
|
||||
{cardNode}
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
return cardNode
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
@use '../_styles/_variables.scss' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$collapse-prefix: '#{$prefix}-collapse';
|
||||
|
||||
.#{$collapse-prefix} {
|
||||
font-size: 14px;
|
||||
color: $text-color-base;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border: solid 1px $border-color;
|
||||
border-bottom: 0;
|
||||
background: $bg-color-container;
|
||||
|
||||
.#{$collapse-prefix}-panel {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Collapse',
|
||||
props: {
|
||||
class: {
|
||||
type: [String, Array, Object],
|
||||
default: '',
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: [],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('collapse')
|
||||
return () => (
|
||||
<div class={[prefixCls, props.class]} style={{ ...props.style }}>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,10 @@
|
||||
export { default as ConfigProvider } from './_config'
|
||||
export { default as Badge } from './badge'
|
||||
export { default as Button } from './button'
|
||||
export { default as Card } from './card'
|
||||
export { default as Checkbox } from './checkbox/checkbox'
|
||||
export { default as CheckboxGroup } from './checkbox/checkbox-group'
|
||||
export { default as Collapse } from './collapse'
|
||||
export { default as Drawer } from './drawer'
|
||||
export { default as Group } from './group'
|
||||
export { default as Icon } from './icon'
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
@use './_styles/index.scss' as *;
|
||||
@use './_styles/variables' as variables;
|
||||
|
||||
@use './badge/index.scss' as *;
|
||||
@use './button/index.scss' as *;
|
||||
@use './input/index.scss' as *;
|
||||
@use './textarea/index.scss' as *;
|
||||
@use './group/index.scss' as *;
|
||||
@use './popup/index.scss' as *;
|
||||
@use './popconfirm/index.scss' as *;
|
||||
@use './icon/index.scss' as *;
|
||||
@use './message/index.scss' as *;
|
||||
@use './notification/index.scss' as *;
|
||||
@use './drawer/index.scss' as *;
|
||||
@use './tooltip/index.scss' as *;
|
||||
@use './tag/index.scss' as *;
|
||||
@use './space/index.scss' as *;
|
||||
@use './separator/index.scss' as *;
|
||||
@use './radio/index.scss' as *;
|
||||
@use './card/index.scss' as *;
|
||||
@use './collapse/index.scss' as *;
|
||||
@use './checkbox/index.scss' as *;
|
||||
@use './switch/index.scss' as *;
|
||||
@use './spin/index.scss' as *;
|
||||
@use './drawer/index.scss' as *;
|
||||
@use './group/index.scss' as *;
|
||||
@use './icon/index.scss' as *;
|
||||
@use './input/index.scss' as *;
|
||||
@use './message/index.scss' as *;
|
||||
@use './modal/index.scss' as *;
|
||||
@use './notification/index.scss' as *;
|
||||
@use './popconfirm/index.scss' as *;
|
||||
@use './popup/index.scss' as *;
|
||||
@use './radio/index.scss' as *;
|
||||
@use './separator/index.scss' as *;
|
||||
@use './space/index.scss' as *;
|
||||
@use './spin/index.scss' as *;
|
||||
@use './switch/index.scss' as *;
|
||||
@use './tag/index.scss' as *;
|
||||
@use './textarea/index.scss' as *;
|
||||
@use './tooltip/index.scss' as *;
|
||||
|
||||
[class*='#{variables.$prefix}-'] {
|
||||
-webkit-box-sizing: border-box;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { App, Component } from 'vue'
|
||||
import * as comps from './components'
|
||||
|
||||
import { useMessage } from './message'
|
||||
import { useModal } from './modal/useModal'
|
||||
import { useNotification } from './notification'
|
||||
// 按需导出组件(可单独 import)
|
||||
export * from './components'
|
||||
@@ -40,4 +41,4 @@ const AoDesign = {
|
||||
},
|
||||
}
|
||||
|
||||
export { AoDesign, useMessage, useNotification }
|
||||
export { AoDesign, useMessage, useModal, useNotification }
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { useModal } from '@ao/ao-design'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const visible1 = ref(false)
|
||||
const visible2 = ref(false)
|
||||
const visible3 = ref(false)
|
||||
|
||||
function onConfirm() {
|
||||
alert('你点击了确认')
|
||||
visible3.value = false
|
||||
}
|
||||
|
||||
const modal = useModal()
|
||||
|
||||
function showInfo() {
|
||||
modal.info({
|
||||
title: '提示',
|
||||
content: '这是信息提示内容',
|
||||
confirmBtn: { label: '确定' },
|
||||
cancelBtn: { label: '取消' },
|
||||
})
|
||||
}
|
||||
|
||||
function showSuccess() {
|
||||
modal.success({
|
||||
title: '成功',
|
||||
content: '操作成功!',
|
||||
})
|
||||
}
|
||||
|
||||
function showCustom() {
|
||||
modal.open({
|
||||
title: '自定义 Modal',
|
||||
content: '这里是自定义内容',
|
||||
width: '500px',
|
||||
height: '300px',
|
||||
draggable: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h2>Modal 示例</h2>
|
||||
|
||||
<!-- header 拖拽 -->
|
||||
<AButton @click="visible1 = true">
|
||||
打开 Header 拖拽
|
||||
</AButton>
|
||||
<AModal v-model:visible="visible1" :draggable="true">
|
||||
<template #title>
|
||||
Header 拖拽示例
|
||||
</template>
|
||||
<p>这是 header 拖拽</p>
|
||||
</AModal>
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- 自定义拖拽区域 -->
|
||||
<AButton @click="visible2 = true">
|
||||
打开 Ref 拖拽
|
||||
</AButton>
|
||||
<AModal v-model:visible="visible2" :draggable="true" draggable-selector=".drag-area">
|
||||
<div class="drag-area" style="padding:10px; background:#ffdede; cursor:move">
|
||||
拖拽这里
|
||||
</div>
|
||||
<p>Modal 内容</p>
|
||||
</AModal>
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- 自定义 layout 覆盖 header+body+footer -->
|
||||
<AButton @click="visible3 = true">
|
||||
打开自定义 Layout Modal
|
||||
</AButton>
|
||||
<AModal v-model:visible="visible3">
|
||||
<template #layout>
|
||||
<div style="padding: 20px; text-align: center;">
|
||||
<h3 style="margin-bottom: 16px;">
|
||||
🎉 自定义 Layout
|
||||
</h3>
|
||||
<p>这里完全覆盖 header、body、footer</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<AButton @click="visible3 = false">
|
||||
关闭
|
||||
</AButton>
|
||||
<AButton type="primary" style="margin-left: 8px;" @click="onConfirm">
|
||||
确认
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AModal>
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- useModal 调用 -->
|
||||
<AButton @click="showInfo">
|
||||
Info Modal
|
||||
</AButton>
|
||||
<AButton style="margin-left: 8px;" @click="showSuccess">
|
||||
Success Modal
|
||||
</AButton>
|
||||
<AButton style="margin-left: 8px;" @click="showCustom">
|
||||
Custom Modal
|
||||
</AButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -41,6 +41,7 @@ $btn-prefix: '#{$prefix}-btn';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
|
||||
.#{$modal-prefix} {
|
||||
background: #fff;
|
||||
@@ -52,6 +53,7 @@ $btn-prefix: '#{$prefix}-btn';
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: auto;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
@@ -61,15 +63,35 @@ $btn-prefix: '#{$prefix}-btn';
|
||||
font-size: 16px;
|
||||
padding: 12px 16px;
|
||||
word-break: break-all;
|
||||
.#{$modal-prefix}-icon:first-of-type {
|
||||
margin-left: auto;
|
||||
}
|
||||
.#{$modal-prefix}-icon {
|
||||
flex-shrink: 0;
|
||||
color: $text-color-assist;
|
||||
cursor: pointer;
|
||||
border-radius: $border-radius;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
& + .#{$modal-prefix}-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $default-color-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
flex: 1;
|
||||
min-width: 0; // 防止内容过长撑破布局
|
||||
}
|
||||
|
||||
&-icon {
|
||||
&-type {
|
||||
font-size: 24px;
|
||||
margin-right: 6px;
|
||||
&-info {
|
||||
@@ -89,19 +111,6 @@ $btn-prefix: '#{$prefix}-btn';
|
||||
}
|
||||
}
|
||||
|
||||
&-close {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
color: $text-color-assist;
|
||||
cursor: pointer;
|
||||
border-radius: $border-radius;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
&:hover {
|
||||
background-color: $default-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
flex: 1 1 auto;
|
||||
padding: 12px 16px;
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { PropType, VNode } 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'
|
||||
import { nextZIndex } from './modalManager'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
props: {
|
||||
title: {
|
||||
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||
default: '',
|
||||
},
|
||||
content: {
|
||||
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||
default: '',
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
@@ -35,13 +44,13 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
confirmBtn: {
|
||||
confirmBtnProps: {
|
||||
type: null,
|
||||
default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }),
|
||||
default: () => ({}),
|
||||
},
|
||||
cancelBtn: {
|
||||
cancelBtnProps: {
|
||||
type: null,
|
||||
default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }),
|
||||
default: () => ({}),
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
@@ -51,9 +60,13 @@ export default defineComponent({
|
||||
type: [String, Object] as PropType<string | HTMLElement>,
|
||||
default: 'body',
|
||||
},
|
||||
showFullscreen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: undefined,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
@@ -66,7 +79,33 @@ export default defineComponent({
|
||||
draggableSelector: {
|
||||
type: String,
|
||||
default: '',
|
||||
}, // 拖拽区域选择器
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
maxHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
onBeforeConfirm: {
|
||||
type: Function as PropType<
|
||||
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
||||
>,
|
||||
},
|
||||
onBeforeCancel: {
|
||||
type: Function as PropType<
|
||||
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
||||
>,
|
||||
},
|
||||
class: {
|
||||
type: [String, Array, Object],
|
||||
default: '',
|
||||
@@ -88,6 +127,11 @@ export default defineComponent({
|
||||
const isMounted = ref(isVisible.value)
|
||||
const isRootVisible = ref(isVisible.value)
|
||||
|
||||
const _fullscreen = ref(false)
|
||||
const isFullscreen = computed(() => props.fullscreen ?? _fullscreen.value)
|
||||
|
||||
const zIndex = ref(0)
|
||||
|
||||
watch(isVisible, (value) => {
|
||||
if (_visible.value !== value)
|
||||
_visible.value = value
|
||||
@@ -102,6 +146,7 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
zIndex.value = nextZIndex()
|
||||
if (props.appendTo instanceof HTMLElement) {
|
||||
targetEl.value = props.appendTo
|
||||
}
|
||||
@@ -112,21 +157,98 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const handleMaskClick = () => {
|
||||
if (props.maskClosable)
|
||||
emit('update:visible', false)
|
||||
let promiseNumber = 0
|
||||
|
||||
const handleConfirm = async (e?: Event) => {
|
||||
const currentPromiseNumber = ++promiseNumber
|
||||
let allow = true
|
||||
|
||||
if (props.onBeforeConfirm) {
|
||||
confirmLoading.value = true
|
||||
|
||||
try {
|
||||
allow = await new Promise<boolean>((resolve) => {
|
||||
let called = false
|
||||
|
||||
const done = (ok: boolean) => {
|
||||
if (!called) {
|
||||
called = true
|
||||
resolve(ok)
|
||||
}
|
||||
}
|
||||
|
||||
const result = props.onBeforeConfirm!(done)
|
||||
|
||||
if (typeof result === 'boolean') {
|
||||
done(result)
|
||||
}
|
||||
else if (result instanceof Promise) {
|
||||
result
|
||||
.then(ok => done(ok ?? true))
|
||||
.catch(() => done(false))
|
||||
}
|
||||
// 如果使用 done 回调,resolve 会在调用方触发
|
||||
})
|
||||
}
|
||||
catch {
|
||||
allow = false
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPromiseNumber === promiseNumber) {
|
||||
if (allow) {
|
||||
emit('update:visible', false)
|
||||
emit('confirm', e)
|
||||
}
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
emit('update:visible', false)
|
||||
emit('cancel')
|
||||
confirmLoading.value = false
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
emit('update:visible', false)
|
||||
emit('confirm')
|
||||
confirmLoading.value = false
|
||||
const handleCancel = async (e?: Event) => {
|
||||
const currentPromiseNumber = ++promiseNumber
|
||||
let allow = true
|
||||
|
||||
if (props.onBeforeCancel) {
|
||||
confirmLoading.value = true
|
||||
|
||||
try {
|
||||
allow = await new Promise<boolean>((resolve) => {
|
||||
let called = false
|
||||
|
||||
const done = (ok: boolean) => {
|
||||
if (!called) {
|
||||
called = true
|
||||
resolve(ok)
|
||||
}
|
||||
}
|
||||
|
||||
const result = props.onBeforeCancel!(done)
|
||||
|
||||
if (typeof result === 'boolean') {
|
||||
done(result)
|
||||
}
|
||||
else if (result instanceof Promise) {
|
||||
result
|
||||
.then(ok => done(ok ?? true))
|
||||
.catch(() => done(false))
|
||||
}
|
||||
// 如果使用 done 回调,resolve 会在调用方触发
|
||||
})
|
||||
}
|
||||
catch {
|
||||
allow = false
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPromiseNumber === promiseNumber) {
|
||||
if (allow) {
|
||||
emit('update:visible', false)
|
||||
emit('cancel', e)
|
||||
}
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAfterEnter = () => emit('open')
|
||||
const handleAfterLeave = () => {
|
||||
isRootVisible.value = false
|
||||
@@ -135,6 +257,10 @@ export default defineComponent({
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleMaskClick = () => {
|
||||
if (props.maskClosable)
|
||||
handleCancel()
|
||||
}
|
||||
// 拖拽逻辑
|
||||
const dragData = { dragging: false, offsetX: 0, offsetY: 0 }
|
||||
|
||||
@@ -193,6 +319,12 @@ export default defineComponent({
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const updateFullscreen = () => {
|
||||
if (props.fullscreen === undefined) {
|
||||
_fullscreen.value = !isFullscreen.value
|
||||
}
|
||||
}
|
||||
|
||||
const typeIconMap: Record<string, string> = {
|
||||
success: 'ix:success-filled',
|
||||
danger: 'ix:error-filled',
|
||||
@@ -201,49 +333,159 @@ export default defineComponent({
|
||||
question: 'ix:question-filled',
|
||||
}
|
||||
|
||||
const modalStyle = computed(() => {
|
||||
// 如果 fullscreen 模式,直接返回空对象
|
||||
if (isFullscreen.value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const res: Record<string, string> = {}
|
||||
if (props.width !== undefined)
|
||||
res.width = props.width
|
||||
if (props.height !== undefined)
|
||||
res.height = props.height
|
||||
if (props.maxWidth !== undefined)
|
||||
res.maxWidth = props.maxWidth
|
||||
if (props.maxHeight !== undefined)
|
||||
res.maxHeight = props.maxHeight
|
||||
return res
|
||||
})
|
||||
|
||||
const renderTitle = computed(() => {
|
||||
if (slots.title)
|
||||
return slots.title()
|
||||
if (typeof props.title === 'function')
|
||||
return props.title()
|
||||
if (typeof props.title === 'string')
|
||||
return props.title
|
||||
return props.title // 假设是 VNode
|
||||
})
|
||||
|
||||
const renderContent = computed(() => {
|
||||
if (slots.default)
|
||||
return slots.default()
|
||||
if (typeof props.content === 'function')
|
||||
return props.content()
|
||||
if (typeof props.content === 'string')
|
||||
return props.content
|
||||
return props.content // 假设是 VNode
|
||||
})
|
||||
|
||||
// 在 setup 里定义一个合并函数
|
||||
const mergedconfirmBtnProps = computed(() => ({
|
||||
label: '确认',
|
||||
type: 'solid',
|
||||
status: 'primary',
|
||||
size: 'small',
|
||||
...(props.confirmBtnProps || {}),
|
||||
}))
|
||||
|
||||
const mergedcancelBtnProps = computed(() => ({
|
||||
label: '取消',
|
||||
type: 'solid',
|
||||
status: 'default',
|
||||
size: 'small',
|
||||
...(props.cancelBtnProps || {}),
|
||||
}))
|
||||
|
||||
return () => (
|
||||
<Teleport to={targetEl.value}>
|
||||
{isMounted.value && (
|
||||
<div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
|
||||
<div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: zIndex.value }}>
|
||||
{props.mask && (
|
||||
<Transition name={`${prefixCls}-mask-fade`} appear>
|
||||
<div v-show={isVisible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
|
||||
<div
|
||||
v-show={isVisible.value}
|
||||
class={`${prefixCls}-mask`}
|
||||
onClick={handleMaskClick}
|
||||
/>
|
||||
</Transition>
|
||||
)}
|
||||
<Transition name={`${prefixCls}-content-fade`} appear onAfterEnter={handleAfterEnter} onAfterLeave={handleAfterLeave}>
|
||||
<Transition
|
||||
name={`${prefixCls}-content-fade`}
|
||||
appear
|
||||
onAfterEnter={handleAfterEnter}
|
||||
onAfterLeave={handleAfterLeave}
|
||||
>
|
||||
<div
|
||||
v-show={isVisible.value}
|
||||
class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]}
|
||||
style={{ ...props.style }}
|
||||
class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`]}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
onMousedown={handleMouseDown}
|
||||
class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}
|
||||
class={[prefixCls, isFullscreen.value && `${prefixCls}-fullscreen`, props.class]}
|
||||
style={{ ...(props.style as object), ...modalStyle.value }}
|
||||
>
|
||||
<div
|
||||
class={`${prefixCls}-header`}
|
||||
style={{
|
||||
cursor: props.draggable && (!props.draggableSelector || props.draggableSelector === `${prefixCls}-header`) ? 'move' : undefined,
|
||||
}}
|
||||
>
|
||||
{props.type !== 'default' && <Icon class={[`${prefixCls}-icon`, `${prefixCls}-icon-${props.type}`]} icon={typeIconMap[props.type]} />}
|
||||
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
|
||||
{props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />}
|
||||
</div>
|
||||
|
||||
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{slots.footer?.() ?? (
|
||||
<>
|
||||
<Button {...props.cancelBtn} onClick={handleCancel}>{props.cancelBtn.label}</Button>
|
||||
<Button loading={confirmLoading.value} {...props.confirmBtn} onClick={handleConfirm}>{props.confirmBtn.label}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{slots.layout
|
||||
? (
|
||||
// 👉 如果有 layout 插槽,用户完全接管布局
|
||||
slots.layout()
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div
|
||||
class={`${prefixCls}-header`}
|
||||
style={{
|
||||
cursor:
|
||||
props.draggable
|
||||
&& (!props.draggableSelector || props.draggableSelector === `${prefixCls}-header`)
|
||||
? 'move'
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{slots.header
|
||||
? slots.header()
|
||||
: (
|
||||
<>
|
||||
{props.type !== 'default' && (
|
||||
<Icon
|
||||
class={[`${prefixCls}-type`, `${prefixCls}-type-${props.type}`]}
|
||||
icon={typeIconMap[props.type]}
|
||||
/>
|
||||
)}
|
||||
<span class={`${prefixCls}-title`}>{renderTitle.value}</span>
|
||||
{props.showFullscreen && (
|
||||
<Icon
|
||||
class={`${prefixCls}-icon`}
|
||||
icon={isFullscreen.value ? 'ix:full-screen-exit' : 'ix:full-screen'}
|
||||
onClick={updateFullscreen}
|
||||
/>
|
||||
)}
|
||||
{props.showClose && (
|
||||
<Icon
|
||||
class={`${prefixCls}-icon`}
|
||||
icon="ix:close"
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div class={`${prefixCls}-body`}>{renderContent.value}</div>
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{slots.footer?.() ?? (
|
||||
<>
|
||||
<Button {...mergedcancelBtnProps.value} onClick={handleCancel}>
|
||||
{mergedcancelBtnProps.value.label}
|
||||
</Button>
|
||||
<Button
|
||||
loading={confirmLoading.value}
|
||||
{...mergedconfirmBtnProps.value}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{mergedconfirmBtnProps.value.label}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</Teleport>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<boolean>>,
|
||||
},
|
||||
onBeforeCancel: {
|
||||
type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>>,
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
appendTo: {
|
||||
type: [String, Object] as PropType<string | HTMLElement>,
|
||||
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<HTMLElement>(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<boolean>((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<boolean>((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<string, string> = {
|
||||
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 (
|
||||
<Teleport to={targetEl.value}>
|
||||
{shouldRenderPopup && (
|
||||
<div v-show={_mounted.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
|
||||
{props.mask && (
|
||||
<Transition name={`${prefixCls}-mask-fade`} appear>
|
||||
<div v-show={_visible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
|
||||
</Transition>
|
||||
)}
|
||||
<Transition
|
||||
name={`${prefixCls}-content-fade`}
|
||||
appear
|
||||
onBeforeEnter={handleBeforeEnter}
|
||||
onAfterEnter={handleAfterEnter}
|
||||
onBeforeLeave={handleBeforeLeave}
|
||||
onAfterLeave={handleAfterLeave}
|
||||
>
|
||||
<div v-show={_visible.value} class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]} style={{ ...props.style }}>
|
||||
<div class={`${prefixCls}`}>
|
||||
<div class={`${prefixCls}-header`}>
|
||||
{props.type !== 'default' && <Icon class={[`${prefixCls}-icon`, `${prefixCls}-icon-${props.type}`]} icon={typeIconMap[props.type]} />}
|
||||
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
|
||||
{props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />}
|
||||
</div>
|
||||
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{slots.footer?.() ?? (
|
||||
<>
|
||||
<Button {...props.cancelBtn} onClick={handleCancel}>{props.cancelBtn.label}</Button>
|
||||
<Button loading={confirmLoading.value} {...props.confirmBtn} onClick={handleConfirm}>{props.confirmBtn.label}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Teleport>
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { AppContext, VNode } from 'vue'
|
||||
import { createVNode, getCurrentInstance, nextTick, render } from 'vue'
|
||||
import Modal from './index'
|
||||
|
||||
function omit<T extends Record<string, any>, K extends keyof T>(
|
||||
object: T,
|
||||
keys: K[],
|
||||
): Omit<T, K> {
|
||||
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<string, any>
|
||||
cancelBtnProps?: Record<string, any>
|
||||
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<ModalOptions>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<boolean>
|
||||
>,
|
||||
@@ -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<boolean>((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 = () => {
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
class={[
|
||||
prefixCls,
|
||||
tipContent ? `${prefixCls}-has-tip` : '',
|
||||
]}
|
||||
>
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
size={props.size}
|
||||
class={`${prefixCls}-icon`}
|
||||
spin
|
||||
/>
|
||||
{tipContent && <div class={`${prefixCls}-tip`}>{tipContent}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// loading 为 true 时的结构
|
||||
return (
|
||||
<div class={`${prefixCls}-wrapper`}>
|
||||
{slots.default?.()}
|
||||
{props.loading && (
|
||||
<div
|
||||
class={`${prefixCls}`}
|
||||
style={{
|
||||
background: slots.default ? `rgba(255,255,255,${props.opacity})` : 'transparent',
|
||||
}}
|
||||
>
|
||||
<div class={`${prefixCls}-content`}>
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
size={props.size}
|
||||
spin
|
||||
class={`${prefixCls}-icon`}
|
||||
/>
|
||||
{renderTip() && <span class={`${prefixCls}-tip`}>{renderTip()}</span>}
|
||||
</div>
|
||||
<div
|
||||
class={[
|
||||
prefixCls,
|
||||
`${prefixCls}-loading`,
|
||||
]}
|
||||
>
|
||||
{slots.default()}
|
||||
|
||||
<div class={`${prefixCls}-mask`}>
|
||||
<div class={`${prefixCls}-mask-inner`}>
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
size={props.size}
|
||||
spin
|
||||
class={`${prefixCls}-icon`}
|
||||
/>
|
||||
{tipContent && <div class={`${prefixCls}-tip`}>{tipContent}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,48 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const visible1 = ref(false)
|
||||
const visible2 = ref(false)
|
||||
const dragHandle = ref<HTMLElement | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
function toggleLoading() {
|
||||
loading.value = !loading.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 默认 header 拖拽 -->
|
||||
<AModal v-model:visible="visible1" :draggable="true">
|
||||
<template #title>
|
||||
Header 拖拽示例
|
||||
</template>
|
||||
<p>这是 header 拖拽</p>
|
||||
</AModal>
|
||||
<AButton @click="visible1 = true">
|
||||
打开 Header 拖拽
|
||||
</AButton>
|
||||
<div class="main">
|
||||
<!-- 卡片 -->
|
||||
<ACard
|
||||
:loading="loading"
|
||||
title="标题"
|
||||
sub-title="副标题"
|
||||
style="width: 400px;"
|
||||
hover-shadow
|
||||
cover="https://tdesign.gtimg.com/site/source/card-demo.png"
|
||||
>
|
||||
<template #actions>
|
||||
actions
|
||||
</template>
|
||||
|
||||
<AModal v-model:visible="visible2" :draggable="true" draggable-selector=".drag-area">
|
||||
<div class="drag-area" style="padding:10px; background:#ffdede; cursor:move">
|
||||
拖拽这里
|
||||
这里是内容
|
||||
|
||||
<template #footer>
|
||||
这里是footer
|
||||
</template>
|
||||
</ACard>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<button style="margin-top: 16px;" @click="toggleLoading">
|
||||
{{ loading ? '关闭 Loading' : '开启 Loading' }}
|
||||
</button>
|
||||
</div>
|
||||
<p>Modal 内容</p>
|
||||
</AModal>
|
||||
|
||||
<AButton @click="visible2 = true">
|
||||
打开 Ref 拖拽
|
||||
</AButton>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
height: 800px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -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']
|
||||
|
||||
Reference in new issue
Block a user