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-secondarycomponent-hover: var(--ao-bg-color-secondarycomponent-hover);
|
||||||
|
|
||||||
$bg-color-mask: var(--ao-bg-color-mask);
|
$bg-color-mask: var(--ao-bg-color-mask);
|
||||||
|
$bg-color-mask-spin: var(--ao-bg-color-mask-spin);
|
||||||
|
|
||||||
// shadow
|
// shadow
|
||||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
--ao-bg-color-secondarycomponent-hover: #5e5e5e;
|
--ao-bg-color-secondarycomponent-hover: #5e5e5e;
|
||||||
|
|
||||||
--ao-bg-color-mask: rgba(23, 23, 26, 0.6);
|
--ao-bg-color-mask: rgba(23, 23, 26, 0.6);
|
||||||
|
--ao-bg-color-mask-spin: rgba(51, 51, 51, 0.6);
|
||||||
|
|
||||||
// shadow阴影
|
// shadow阴影
|
||||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
--ao-bg-color-secondarycomponent-hover: #c6c6c6;
|
--ao-bg-color-secondarycomponent-hover: #c6c6c6;
|
||||||
|
|
||||||
--ao-bg-color-mask: rgba(29, 33, 41, 0.6);
|
--ao-bg-color-mask: rgba(29, 33, 41, 0.6);
|
||||||
|
--ao-bg-color-mask-spin: rgba(255, 255, 255, 0.6);
|
||||||
|
|
||||||
// shadow阴影
|
// shadow阴影
|
||||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
// 基础/下层 投影 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 ConfigProvider } from './_config'
|
||||||
|
export { default as Badge } from './badge'
|
||||||
export { default as Button } from './button'
|
export { default as Button } from './button'
|
||||||
|
export { default as Card } from './card'
|
||||||
export { default as Checkbox } from './checkbox/checkbox'
|
export { default as Checkbox } from './checkbox/checkbox'
|
||||||
export { default as CheckboxGroup } from './checkbox/checkbox-group'
|
export { default as CheckboxGroup } from './checkbox/checkbox-group'
|
||||||
|
export { default as Collapse } from './collapse'
|
||||||
export { default as Drawer } from './drawer'
|
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'
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
@use './_styles/index.scss' as *;
|
@use './_styles/index.scss' as *;
|
||||||
@use './_styles/variables' as variables;
|
@use './_styles/variables' as variables;
|
||||||
|
|
||||||
|
@use './badge/index.scss' as *;
|
||||||
@use './button/index.scss' as *;
|
@use './button/index.scss' as *;
|
||||||
@use './input/index.scss' as *;
|
@use './card/index.scss' as *;
|
||||||
@use './textarea/index.scss' as *;
|
@use './collapse/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 './checkbox/index.scss' as *;
|
@use './checkbox/index.scss' as *;
|
||||||
@use './switch/index.scss' as *;
|
@use './drawer/index.scss' as *;
|
||||||
@use './spin/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 './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}-'] {
|
[class*='#{variables.$prefix}-'] {
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { App, Component } from 'vue'
|
|||||||
import * as comps from './components'
|
import * as comps from './components'
|
||||||
|
|
||||||
import { useMessage } from './message'
|
import { useMessage } from './message'
|
||||||
|
import { useModal } from './modal/useModal'
|
||||||
import { useNotification } from './notification'
|
import { useNotification } from './notification'
|
||||||
// 按需导出组件(可单独 import)
|
// 按需导出组件(可单独 import)
|
||||||
export * from './components'
|
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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
.#{$modal-prefix} {
|
.#{$modal-prefix} {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -52,6 +53,7 @@ $btn-prefix: '#{$prefix}-btn';
|
|||||||
height: 300px;
|
height: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -61,15 +63,35 @@ $btn-prefix: '#{$prefix}-btn';
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
word-break: break-all;
|
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 {
|
&-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; // 防止内容过长撑破布局
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-type {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
&-info {
|
&-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 {
|
&-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 12px 16px;
|
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 { computed, defineComponent, onMounted, ref, Teleport, Transition, watch } from 'vue'
|
||||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
import Button from '../button/index'
|
import Button from '../button/index'
|
||||||
import Icon from '../icon/index'
|
import Icon from '../icon/index'
|
||||||
|
import { nextZIndex } from './modalManager'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Modal',
|
name: 'Modal',
|
||||||
props: {
|
props: {
|
||||||
|
title: {
|
||||||
|
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
visible: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
@@ -35,13 +44,13 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
confirmBtn: {
|
confirmBtnProps: {
|
||||||
type: null,
|
type: null,
|
||||||
default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
cancelBtn: {
|
cancelBtnProps: {
|
||||||
type: null,
|
type: null,
|
||||||
default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
destroyOnClose: {
|
destroyOnClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -51,9 +60,13 @@ export default defineComponent({
|
|||||||
type: [String, Object] as PropType<string | HTMLElement>,
|
type: [String, Object] as PropType<string | HTMLElement>,
|
||||||
default: 'body',
|
default: 'body',
|
||||||
},
|
},
|
||||||
|
showFullscreen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: undefined,
|
||||||
},
|
},
|
||||||
draggable: {
|
draggable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -66,7 +79,33 @@ export default defineComponent({
|
|||||||
draggableSelector: {
|
draggableSelector: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
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: {
|
class: {
|
||||||
type: [String, Array, Object],
|
type: [String, Array, Object],
|
||||||
default: '',
|
default: '',
|
||||||
@@ -88,6 +127,11 @@ export default defineComponent({
|
|||||||
const isMounted = ref(isVisible.value)
|
const isMounted = ref(isVisible.value)
|
||||||
const isRootVisible = 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) => {
|
watch(isVisible, (value) => {
|
||||||
if (_visible.value !== value)
|
if (_visible.value !== value)
|
||||||
_visible.value = value
|
_visible.value = value
|
||||||
@@ -102,6 +146,7 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
zIndex.value = nextZIndex()
|
||||||
if (props.appendTo instanceof HTMLElement) {
|
if (props.appendTo instanceof HTMLElement) {
|
||||||
targetEl.value = props.appendTo
|
targetEl.value = props.appendTo
|
||||||
}
|
}
|
||||||
@@ -112,21 +157,98 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleMaskClick = () => {
|
let promiseNumber = 0
|
||||||
if (props.maskClosable)
|
|
||||||
emit('update:visible', false)
|
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 () => {
|
const handleCancel = async (e?: Event) => {
|
||||||
emit('update:visible', false)
|
const currentPromiseNumber = ++promiseNumber
|
||||||
emit('cancel')
|
let allow = true
|
||||||
confirmLoading.value = false
|
|
||||||
}
|
if (props.onBeforeCancel) {
|
||||||
const handleConfirm = async () => {
|
confirmLoading.value = true
|
||||||
emit('update:visible', false)
|
|
||||||
emit('confirm')
|
try {
|
||||||
confirmLoading.value = false
|
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 handleAfterEnter = () => emit('open')
|
||||||
const handleAfterLeave = () => {
|
const handleAfterLeave = () => {
|
||||||
isRootVisible.value = false
|
isRootVisible.value = false
|
||||||
@@ -135,6 +257,10 @@ export default defineComponent({
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMaskClick = () => {
|
||||||
|
if (props.maskClosable)
|
||||||
|
handleCancel()
|
||||||
|
}
|
||||||
// 拖拽逻辑
|
// 拖拽逻辑
|
||||||
const dragData = { dragging: false, offsetX: 0, offsetY: 0 }
|
const dragData = { dragging: false, offsetX: 0, offsetY: 0 }
|
||||||
|
|
||||||
@@ -193,6 +319,12 @@ export default defineComponent({
|
|||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFullscreen = () => {
|
||||||
|
if (props.fullscreen === undefined) {
|
||||||
|
_fullscreen.value = !isFullscreen.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const typeIconMap: Record<string, string> = {
|
const typeIconMap: Record<string, string> = {
|
||||||
success: 'ix:success-filled',
|
success: 'ix:success-filled',
|
||||||
danger: 'ix:error-filled',
|
danger: 'ix:error-filled',
|
||||||
@@ -201,49 +333,159 @@ export default defineComponent({
|
|||||||
question: 'ix:question-filled',
|
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 () => (
|
return () => (
|
||||||
<Teleport to={targetEl.value}>
|
<Teleport to={targetEl.value}>
|
||||||
{isMounted.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 && (
|
{props.mask && (
|
||||||
<Transition name={`${prefixCls}-mask-fade`} appear>
|
<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>
|
||||||
)}
|
)}
|
||||||
<Transition name={`${prefixCls}-content-fade`} appear onAfterEnter={handleAfterEnter} onAfterLeave={handleAfterLeave}>
|
<Transition
|
||||||
|
name={`${prefixCls}-content-fade`}
|
||||||
|
appear
|
||||||
|
onAfterEnter={handleAfterEnter}
|
||||||
|
onAfterLeave={handleAfterLeave}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-show={isVisible.value}
|
v-show={isVisible.value}
|
||||||
class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]}
|
class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`]}
|
||||||
style={{ ...props.style }}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
onMousedown={handleMouseDown}
|
onMousedown={handleMouseDown}
|
||||||
class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}
|
class={[prefixCls, isFullscreen.value && `${prefixCls}-fullscreen`, props.class]}
|
||||||
|
style={{ ...(props.style as object), ...modalStyle.value }}
|
||||||
>
|
>
|
||||||
<div
|
{slots.layout
|
||||||
class={`${prefixCls}-header`}
|
? (
|
||||||
style={{
|
// 👉 如果有 layout 插槽,用户完全接管布局
|
||||||
cursor: props.draggable && (!props.draggableSelector || props.draggableSelector === `${prefixCls}-header`) ? 'move' : undefined,
|
slots.layout()
|
||||||
}}
|
)
|
||||||
>
|
: (
|
||||||
{props.type !== 'default' && <Icon class={[`${prefixCls}-icon`, `${prefixCls}-icon-${props.type}`]} icon={typeIconMap[props.type]} />}
|
<>
|
||||||
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
|
<div
|
||||||
{props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />}
|
class={`${prefixCls}-header`}
|
||||||
</div>
|
style={{
|
||||||
|
cursor:
|
||||||
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
|
props.draggable
|
||||||
<div class={`${prefixCls}-footer`}>
|
&& (!props.draggableSelector || props.draggableSelector === `${prefixCls}-header`)
|
||||||
{slots.footer?.() ?? (
|
? 'move'
|
||||||
<>
|
: undefined,
|
||||||
<Button {...props.cancelBtn} onClick={handleCancel}>{props.cancelBtn.label}</Button>
|
}}
|
||||||
<Button loading={confirmLoading.value} {...props.confirmBtn} onClick={handleConfirm}>{props.confirmBtn.label}</Button>
|
>
|
||||||
</>
|
{slots.header
|
||||||
)}
|
? slots.header()
|
||||||
</div>
|
: (
|
||||||
|
<>
|
||||||
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|||||||
@@ -1,23 +1,6 @@
|
|||||||
|
// modalZIndex.ts
|
||||||
let zIndexBase = 1000
|
let zIndexBase = 1000
|
||||||
const modalStack: number[] = [] // 当前打开的 Modal zIndex
|
|
||||||
|
|
||||||
export function addModal() {
|
export function nextZIndex() {
|
||||||
const zIndex = ++zIndexBase // 每次直接递增
|
return ++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
|
|
||||||
}
|
}
|
||||||
@@ -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),
|
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
beforeConfirm: {
|
onBeforeConfirm: {
|
||||||
type: Function as PropType<
|
type: Function as PropType<
|
||||||
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
||||||
>,
|
>,
|
||||||
@@ -69,23 +69,37 @@ export default defineComponent({
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let promiseNumber = 0
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
|
const currentPromiseNumber = ++promiseNumber
|
||||||
let allow = true
|
let allow = true
|
||||||
|
|
||||||
if (props.beforeConfirm) {
|
if (props.onBeforeConfirm) {
|
||||||
confirmLoading.value = true
|
confirmLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
allow = await new Promise<boolean>((resolve) => {
|
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') {
|
if (typeof result === 'boolean') {
|
||||||
resolve(result)
|
done(result)
|
||||||
}
|
}
|
||||||
else if (result instanceof Promise) {
|
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 {
|
catch {
|
||||||
@@ -93,12 +107,12 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allow) {
|
if (currentPromiseNumber === promiseNumber) {
|
||||||
popVisible.value = false
|
if (allow) {
|
||||||
emit('confirm')
|
emit('confirm')
|
||||||
|
}
|
||||||
|
confirmLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmLoading.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
|||||||
@@ -2,38 +2,44 @@
|
|||||||
|
|
||||||
$spin-prefix: #{$prefix}-spin;
|
$spin-prefix: #{$prefix}-spin;
|
||||||
|
|
||||||
.#{$spin-prefix}-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$spin-prefix} {
|
.#{$spin-prefix} {
|
||||||
position: absolute;
|
display: inline-block;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
&-icon {
|
||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tip {
|
&-tip {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: $text-color-base;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.rounded {
|
&.#{$spin-prefix}-has-tip {
|
||||||
border-radius: inherit;
|
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 { defineComponent } from 'vue'
|
||||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
import Icon from '../icon'
|
import Icon from '../icon'
|
||||||
@@ -29,30 +29,50 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!props.loading && !slots.default)
|
const tipContent = renderTip()
|
||||||
return null
|
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div class={`${prefixCls}-wrapper`}>
|
<div
|
||||||
{slots.default?.()}
|
class={[
|
||||||
{props.loading && (
|
prefixCls,
|
||||||
<div
|
`${prefixCls}-loading`,
|
||||||
class={`${prefixCls}`}
|
]}
|
||||||
style={{
|
>
|
||||||
background: slots.default ? `rgba(255,255,255,${props.opacity})` : 'transparent',
|
{slots.default()}
|
||||||
}}
|
|
||||||
>
|
<div class={`${prefixCls}-mask`}>
|
||||||
<div class={`${prefixCls}-content`}>
|
<div class={`${prefixCls}-mask-inner`}>
|
||||||
<Icon
|
<Icon
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
size={props.size}
|
size={props.size}
|
||||||
spin
|
spin
|
||||||
class={`${prefixCls}-icon`}
|
class={`${prefixCls}-icon`}
|
||||||
/>
|
/>
|
||||||
{renderTip() && <span class={`${prefixCls}-tip`}>{renderTip()}</span>}
|
{tipContent && <div class={`${prefixCls}-tip`}>{tipContent}</div>}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,48 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const visible1 = ref(false)
|
const loading = ref(false)
|
||||||
const visible2 = ref(false)
|
|
||||||
const dragHandle = ref<HTMLElement | null>(null)
|
function toggleLoading() {
|
||||||
|
loading.value = !loading.value
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 默认 header 拖拽 -->
|
<div class="main">
|
||||||
<AModal v-model:visible="visible1" :draggable="true">
|
<!-- 卡片 -->
|
||||||
<template #title>
|
<ACard
|
||||||
Header 拖拽示例
|
:loading="loading"
|
||||||
</template>
|
title="标题"
|
||||||
<p>这是 header 拖拽</p>
|
sub-title="副标题"
|
||||||
</AModal>
|
style="width: 400px;"
|
||||||
<AButton @click="visible1 = true">
|
hover-shadow
|
||||||
打开 Header 拖拽
|
cover="https://tdesign.gtimg.com/site/source/card-demo.png"
|
||||||
</AButton>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<p>Modal 内容</p>
|
|
||||||
</AModal>
|
|
||||||
|
|
||||||
<AButton @click="visible2 = true">
|
|
||||||
打开 Ref 拖拽
|
|
||||||
</AButton>
|
|
||||||
</template>
|
</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 */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ABadge: typeof import('@ao/ao-design')['Badge']
|
||||||
AButton: typeof import('@ao/ao-design')['Button']
|
AButton: typeof import('@ao/ao-design')['Button']
|
||||||
|
ACard: typeof import('@ao/ao-design')['Card']
|
||||||
ACheckbox: typeof import('@ao/ao-design')['Checkbox']
|
ACheckbox: typeof import('@ao/ao-design')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('@ao/ao-design')['CheckboxGroup']
|
ACheckboxGroup: typeof import('@ao/ao-design')['CheckboxGroup']
|
||||||
AConfigProvider: typeof import('@ao/ao-design')['ConfigProvider']
|
AConfigProvider: typeof import('@ao/ao-design')['ConfigProvider']
|
||||||
|
|||||||
Reference in new issue
Block a user