u
This commit is contained in:
1 parent
b24015fbae
commit
6d4f1ee402
19 files changed
+782
-108
No files matched your search
+2
-2
@@ -49,9 +49,9 @@
|
||||
"@use '../_styles/_variables.scss' as *;",
|
||||
"@use 'sass:color';",
|
||||
"",
|
||||
"$${1:component}-prefix: #{$prefix}-${1:component};",
|
||||
"$${1:component}-prefix: '#{$$prefix}-${1:component}';",
|
||||
"",
|
||||
".#{${1:component}-prefix} {",
|
||||
".#{$${1:component}-prefix} {",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PropType } from 'vue'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Icon from '../icon'
|
||||
@@ -19,6 +20,7 @@ export default defineComponent({
|
||||
iconPosition: { type: String as () => 'left' | 'right', default: 'left' },
|
||||
size: { type: String as () => 'small' | 'medium' | 'large', default: 'medium' },
|
||||
block: { type: Boolean, default: false },
|
||||
onClick: { type: Function as PropType<(event: MouseEvent) => void> }, // 注意这里不要加 ?
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const prefixCls = getPrefixCls('btn')
|
||||
@@ -39,7 +41,11 @@ export default defineComponent({
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<button class={cls.value} disabled={props.disabled || props.loading}>
|
||||
<button
|
||||
class={cls.value}
|
||||
disabled={props.disabled || props.loading}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.iconPosition === 'left' && (props.loading || props.icon) && (
|
||||
<Icon
|
||||
icon={props.loading ? 'mingcute:loading-line' : props.icon}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export { default as ConfigProvider } from './_config'
|
||||
export { default as Button } from './button'
|
||||
export { default as Drawer } from './drawer'
|
||||
export { default as Group } from './group'
|
||||
export { default as Icon } from './icon'
|
||||
export { default as Input } from './input'
|
||||
export { default as Popconfirm } from './popconfirm'
|
||||
export { default as Popup } from './popup'
|
||||
export { default as Spin } from './spin'
|
||||
export { default as Textarea } from './textarea'
|
||||
@@ -0,0 +1,143 @@
|
||||
@use '../_styles/_variables.scss' as *;
|
||||
|
||||
$drawer-prefix: '#{$prefix}-drawer';
|
||||
|
||||
.#{$drawer-prefix} {
|
||||
z-index: 1500;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
opacity 0.25s ease;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 46px;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text-color-base;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
box-sizing: border-box;
|
||||
|
||||
.#{$drawer-prefix}-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;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
box-sizing: border-box;
|
||||
background-color: $bg-color-control;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&-top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&-bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-open {
|
||||
pointer-events: auto;
|
||||
|
||||
.#{$drawer-prefix}-mask {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.#{$drawer-prefix}-right .#{$drawer-prefix}-wrapper {
|
||||
transform: translateX(0);
|
||||
}
|
||||
&.#{$drawer-prefix}-left .#{$drawer-prefix}-wrapper {
|
||||
transform: translateX(0);
|
||||
}
|
||||
&.#{$drawer-prefix}-top .#{$drawer-prefix}-wrapper {
|
||||
transform: translateY(0);
|
||||
}
|
||||
&.#{$drawer-prefix}-bottom .#{$drawer-prefix}-wrapper {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&-right .#{$drawer-prefix}-wrapper {
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
&-left .#{$drawer-prefix}-wrapper {
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
&-top .#{$drawer-prefix}-wrapper {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
&-bottom .#{$drawer-prefix}-wrapper {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { PropType } from 'vue'
|
||||
import { defineComponent, onMounted, ref, Teleport } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Button from '../button/index'
|
||||
import Icon from '../icon/index'
|
||||
|
||||
type DrawerSize = 'small' | 'medium' | 'large'
|
||||
type SizeType = number | string | DrawerSize
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Drawer',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placement: {
|
||||
type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
|
||||
default: 'right',
|
||||
},
|
||||
size: {
|
||||
type: [Number, String] as PropType<SizeType>,
|
||||
default: 'medium',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmBtn: {
|
||||
type: null,
|
||||
default: () => ({ label: '确认', type: 'solid', status: 'primary' }),
|
||||
},
|
||||
cancelBtn: {
|
||||
type: null,
|
||||
default: () => ({ label: '取消', type: 'solid', status: 'default' }),
|
||||
},
|
||||
asyncConfirm: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maskClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
appendTo: {
|
||||
type: [String, Object] as PropType<string | HTMLElement>,
|
||||
default: 'body',
|
||||
},
|
||||
},
|
||||
emits: ['update:visible', 'confirm', 'cancel'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('drawer')
|
||||
const confirmLoading = ref<boolean>(false)
|
||||
|
||||
const close = () => {
|
||||
emit('update:visible', false)
|
||||
confirmLoading.value = false
|
||||
}
|
||||
|
||||
const handleMaskClick = () => {
|
||||
if (props.maskClose) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
const resolveSize = (value: SizeType, defaultPx = 300) => {
|
||||
if (typeof value === 'number')
|
||||
return `${value}px`
|
||||
if (typeof value === 'string') {
|
||||
const presetMap: Record<DrawerSize, string> = {
|
||||
small: '300px',
|
||||
medium: '500px',
|
||||
large: '700px',
|
||||
}
|
||||
if (presetMap[value as DrawerSize])
|
||||
return presetMap[value as DrawerSize]
|
||||
return value
|
||||
}
|
||||
return `${defaultPx}px`
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (props.asyncConfirm) {
|
||||
confirmLoading.value = true
|
||||
emit('confirm', close, () => {
|
||||
confirmLoading.value = false
|
||||
})
|
||||
}
|
||||
else {
|
||||
close()
|
||||
emit('confirm')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
close()
|
||||
emit('cancel')
|
||||
}
|
||||
// 挂载目标位置
|
||||
const targetEl = ref<HTMLElement>(document.body)
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
// 判断是否是 body
|
||||
const isBody = props.appendTo === 'body'
|
||||
|
||||
return () => (
|
||||
<Teleport to={targetEl.value}>
|
||||
<div
|
||||
class={[prefixCls, `${prefixCls}-${props.placement}`, props.visible && `${prefixCls}-open`]}
|
||||
style={{
|
||||
position: isBody ? 'fixed' : 'absolute',
|
||||
}}
|
||||
>
|
||||
<div class={`${prefixCls}-mask`} onClick={handleMaskClick}></div>
|
||||
|
||||
<div
|
||||
class={[`${prefixCls}-wrapper`]}
|
||||
style={{
|
||||
width: ['left', 'right'].includes(props.placement)
|
||||
? resolveSize(props.size)
|
||||
: '100%',
|
||||
height: ['top', 'bottom'].includes(props.placement)
|
||||
? resolveSize(props.size)
|
||||
: '100%',
|
||||
}}
|
||||
>
|
||||
<div class={`${prefixCls}-header`}>
|
||||
{slots.header
|
||||
? slots.header()
|
||||
: (
|
||||
<>
|
||||
<span>{props.title}</span>
|
||||
{props.showClose && (
|
||||
<Icon
|
||||
class={`${prefixCls}-close`}
|
||||
icon="ix:close"
|
||||
onClick={() => emit('update:visible', false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class={`${prefixCls}-body`}>
|
||||
{slots.default ? slots.default() : null}
|
||||
</div>
|
||||
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{slots.footer
|
||||
? slots.footer()
|
||||
: (
|
||||
<>
|
||||
<Button {...props.cancelBtn} block onClick={handleCancel}>
|
||||
{props.cancelBtn.label}
|
||||
</Button>
|
||||
<Button loading={confirmLoading.value} block {...props.confirmBtn} onClick={handleConfirm}>
|
||||
{props.confirmBtn.label}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -5,6 +5,8 @@
|
||||
@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 *;
|
||||
@@ -3,29 +3,28 @@
|
||||
|
||||
$message-prefix: #{$prefix}-message;
|
||||
|
||||
.#{$message-prefix}-wrapper {
|
||||
position: fixed;
|
||||
z-index: 6000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.#{$message-prefix} {
|
||||
&-wrapper {
|
||||
position: fixed;
|
||||
z-index: 6000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
&-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
width: fit-content;
|
||||
vertical-align: middle;
|
||||
border-radius: $border-radius;
|
||||
padding: 6px 12px;
|
||||
background-color: $bg-color-container;
|
||||
color: $text-color-base;
|
||||
box-shadow: $shadow-overlay;
|
||||
box-sizing: border-box;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
&-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
width: fit-content;
|
||||
vertical-align: middle;
|
||||
border-radius: $border-radius;
|
||||
padding: 6px 12px;
|
||||
background-color: $bg-color-container;
|
||||
color: $text-color-base;
|
||||
box-shadow: $shadow-overlay;
|
||||
box-sizing: border-box;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
&-message {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -40,8 +39,8 @@ $message-prefix: #{$prefix}-message;
|
||||
color: $text-color-assist;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
&:hover {
|
||||
background-color: $default-color-hover;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { VNode } from 'vue'
|
||||
import { createApp, h } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import MessageItem from './item'
|
||||
@@ -16,7 +17,7 @@ type Placement
|
||||
type MessageType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'question'
|
||||
|
||||
interface MessageOptions {
|
||||
message: string
|
||||
message: string | VNode | (() => VNode)
|
||||
type?: MessageType
|
||||
placement?: Placement
|
||||
duration?: number
|
||||
|
||||
@@ -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/index'
|
||||
@@ -9,7 +9,10 @@ export default defineComponent({
|
||||
name: 'MessageItem',
|
||||
props: {
|
||||
type: { type: String, default: 'info' },
|
||||
message: { type: String, required: true },
|
||||
message: {
|
||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||
required: true,
|
||||
},
|
||||
placement: { type: String, required: true },
|
||||
showClose: { type: Boolean, default: false },
|
||||
onClose: {
|
||||
@@ -26,10 +29,14 @@ export default defineComponent({
|
||||
question: 'ix:question-filled',
|
||||
loading: 'mingcute:loading-line',
|
||||
}
|
||||
|
||||
const renderMessage = (message: string | VNode | (() => VNode)) => {
|
||||
if (typeof message === 'function')
|
||||
return message()
|
||||
return message
|
||||
}
|
||||
return () => (
|
||||
<div
|
||||
class={`${prefixCls}-item`}
|
||||
class={`${prefixCls}`}
|
||||
>
|
||||
<Icon
|
||||
icon={iconMap[props.type ?? 'info']}
|
||||
@@ -37,7 +44,9 @@ export default defineComponent({
|
||||
class={`${prefixCls}-icon ${prefixCls}-${props.type ?? 'info'}`}
|
||||
spin={props.type === 'loading'}
|
||||
/>
|
||||
<span class={`${prefixCls}-content`}>{props.message}</span>
|
||||
<span class={`${prefixCls}-message`}>
|
||||
{renderMessage(props.message)}
|
||||
</span>
|
||||
{props.showClose
|
||||
? (
|
||||
<Icon
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { useNotification } from '@ao/ao-design'
|
||||
import { h } from 'vue'
|
||||
|
||||
const notification = useNotification()
|
||||
|
||||
function showCustomMessage() {
|
||||
notification.success({
|
||||
title: '自定义消息',
|
||||
message: () =>
|
||||
h('div', { style: { color: 'red', position: 'relative' } }, [
|
||||
h('p', {}, '这里是自定义内容,可以是任意 VNode'),
|
||||
h('strong', {}, '加粗文本'),
|
||||
// 右下角按钮容器
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
position: 'absolute',
|
||||
bottom: '4px',
|
||||
right: '4px',
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
style: { fontSize: '12px', padding: '2px 6px' },
|
||||
onClick: () => alert('Button A clicked'),
|
||||
},
|
||||
'A',
|
||||
),
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
style: { fontSize: '12px', padding: '2px 6px' },
|
||||
onClick: () => alert('Button B clicked'),
|
||||
},
|
||||
'B',
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
type: 'success',
|
||||
showClose: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="demo">
|
||||
<AButton status="primary" @click="showCustomMessage">
|
||||
自定义 h 函数消息
|
||||
</AButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@ $notification-prefix: #{$prefix}-notification;
|
||||
z-index: 6000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.#{$notification-prefix} {
|
||||
@@ -54,7 +54,7 @@ $notification-prefix: #{$prefix}-notification;
|
||||
margin-left: 6px;
|
||||
color: $text-color-assist;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
border-radius: $border-radius;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
&:hover {
|
||||
@@ -62,7 +62,7 @@ $notification-prefix: #{$prefix}-notification;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
&-message {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: $text-color-secondary;
|
||||
|
||||
@@ -12,7 +12,7 @@ type Placement
|
||||
type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'question'
|
||||
|
||||
interface NotificationOptions {
|
||||
content: string | VNode | (() => VNode)
|
||||
message: string | VNode | (() => VNode)
|
||||
|
||||
title: string
|
||||
subTitle?: string
|
||||
@@ -63,7 +63,7 @@ export function useNotification(): MessageFn {
|
||||
opts: NotificationOptions,
|
||||
defaultType: NotificationType = 'info',
|
||||
): NotificationHandle => {
|
||||
const { content, title, subTitle, type = defaultType, placement = 'top-end', duration = 3000, showClose } = opts
|
||||
const { message, title, subTitle, type = defaultType, placement = 'top-end', duration = 3000, showClose } = opts
|
||||
|
||||
const container = getContainer(placement)
|
||||
let timer: number | undefined
|
||||
@@ -75,7 +75,7 @@ export function useNotification(): MessageFn {
|
||||
type,
|
||||
title,
|
||||
subTitle,
|
||||
content,
|
||||
message,
|
||||
placement,
|
||||
showClose,
|
||||
style: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Icon from '../icon/index'
|
||||
|
||||
@@ -10,7 +10,7 @@ export default defineComponent({
|
||||
props: {
|
||||
type: { type: String, default: 'info' },
|
||||
title: { type: String, required: true },
|
||||
content: {
|
||||
message: {
|
||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||
required: true,
|
||||
},
|
||||
@@ -31,10 +31,10 @@ export default defineComponent({
|
||||
question: 'ix:question-filled',
|
||||
loading: 'mingcute:loading-line',
|
||||
}
|
||||
const renderContent = (content: string | VNode | (() => VNode)) => {
|
||||
if (typeof content === 'function')
|
||||
return content()
|
||||
return content
|
||||
const renderMessage = (message: string | VNode | (() => VNode)) => {
|
||||
if (typeof message === 'function')
|
||||
return message()
|
||||
return message
|
||||
}
|
||||
return () => (
|
||||
<div
|
||||
@@ -67,8 +67,8 @@ export default defineComponent({
|
||||
: null}
|
||||
</div>
|
||||
|
||||
<div class={`${prefixCls}-content`}>
|
||||
{renderContent(props.content)}
|
||||
<div class={`${prefixCls}-message`}>
|
||||
{renderMessage(props.message)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
@use '../_styles/_variables.scss' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$popconfirm-prefix: '#{$prefix}-popconfirm';
|
||||
|
||||
.#{$popconfirm-prefix}-wrapper {
|
||||
padding: 12px 12px;
|
||||
}
|
||||
|
||||
.#{$popconfirm-prefix} {
|
||||
&-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
&-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&-title-wrapper {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
max-width: 320px;
|
||||
color: $text-color-base;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
&-description {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
&-footer {
|
||||
text-align: right;
|
||||
margin-top: 16px;
|
||||
> * + * {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
&-info {
|
||||
color: $primary-color;
|
||||
}
|
||||
&-success {
|
||||
color: $success-color;
|
||||
}
|
||||
&-warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
&-error {
|
||||
color: $danger-color;
|
||||
}
|
||||
&-question {
|
||||
color: $primary-color;
|
||||
}
|
||||
&-loading {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { defineComponent, ref, watch } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Button from '../button/index'
|
||||
import Icon from '../icon/index'
|
||||
import Popup from '../popup/index'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Popconfirm',
|
||||
props: {
|
||||
type: {
|
||||
type: String as PropType<'info' | 'success' | 'error' | 'warning' | 'question' | 'loading'>,
|
||||
default: 'info',
|
||||
},
|
||||
title: {
|
||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||
required: false,
|
||||
},
|
||||
asyncConfirm: {
|
||||
type: Boolean,
|
||||
default: false, // 默认同步关闭,不显示 loading
|
||||
},
|
||||
confirmBtn: {
|
||||
type: null,
|
||||
default: () => ({ label: '确认', size: 'small', type: 'solid', status: 'primary' }),
|
||||
},
|
||||
cancelBtn: {
|
||||
type: null,
|
||||
default: () => ({ label: '取消', size: 'small', type: 'text', status: 'default' }),
|
||||
},
|
||||
placement: {
|
||||
type: String as PropType<
|
||||
'top' | 'bottom' | 'left' | 'right'
|
||||
| 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
||||
| 'left-start' | 'left-end' | 'right-start' | 'right-end'
|
||||
>,
|
||||
default: 'top',
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['confirm', 'cancel'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('popconfirm')
|
||||
|
||||
const popVisible = ref<boolean>(false)
|
||||
|
||||
const confirmLoading = ref<boolean>(false)
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
success: 'ix:success-filled',
|
||||
error: 'ix:error-filled',
|
||||
warning: 'ix:warning-filled',
|
||||
info: 'ix:info-filled',
|
||||
question: 'ix:question-filled',
|
||||
loading: 'mingcute:loading-line',
|
||||
}
|
||||
|
||||
const renderNode = (val: any) => {
|
||||
if (typeof val === 'function')
|
||||
return val()
|
||||
return val
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (props.asyncConfirm) {
|
||||
confirmLoading.value = true
|
||||
|
||||
emit('confirm', () => { // resolve
|
||||
popVisible.value = false
|
||||
confirmLoading.value = false
|
||||
}, () => { // reject
|
||||
confirmLoading.value = false
|
||||
})
|
||||
}
|
||||
else {
|
||||
popVisible.value = false
|
||||
emit('confirm')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
popVisible.value = false
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
// 渲染内容块
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div class={`${prefixCls}-wrapper`}>
|
||||
<div class={`${prefixCls}-main`}>
|
||||
{slots.icon
|
||||
? (
|
||||
<span class={`${prefixCls}-icon`}>{slots.icon()}</span>
|
||||
)
|
||||
: (
|
||||
<Icon
|
||||
icon={iconMap[props.type ?? 'info']}
|
||||
size={22}
|
||||
class={`${prefixCls}-icon ${prefixCls}-${props.type ?? 'info'}`}
|
||||
spin={props.type === 'loading'}
|
||||
/>
|
||||
)}
|
||||
<div class={`${prefixCls}-title-wrapper`}>
|
||||
<span class={`${prefixCls}-title`}>
|
||||
{slots.title ? slots.title() : renderNode(props.title)}
|
||||
</span>
|
||||
<span class={`${prefixCls}-description`}>
|
||||
{slots.description
|
||||
? (
|
||||
<div class={`${prefixCls}-description`}>{slots.description()}</div>
|
||||
)
|
||||
: (
|
||||
props.description && <div class={`${prefixCls}-description`}>{renderNode(props.description)}</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{slots.footer
|
||||
? (
|
||||
slots.footer()
|
||||
)
|
||||
: (
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
<Button {...props.cancelBtn} onClick={handleCancel}>
|
||||
{props.cancelBtn.label}
|
||||
</Button>
|
||||
|
||||
<Button loading={confirmLoading.value} {...props.confirmBtn} onClick={handleConfirm}>
|
||||
{props.confirmBtn.label}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<Popup
|
||||
v-model:visible={popVisible.value}
|
||||
placement={props.placement}
|
||||
showArrow={props.showArrow}
|
||||
content={renderContent}
|
||||
trigger="click"
|
||||
>
|
||||
{{
|
||||
default: () => slots.default?.(),
|
||||
}}
|
||||
</Popup>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -7,7 +7,6 @@ $popup-prefix: '#{$prefix}-popup';
|
||||
position: absolute;
|
||||
background-color: $bg-color-container;
|
||||
color: $text-color-base;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: $border-radius;
|
||||
z-index: 1000;
|
||||
@@ -29,7 +28,9 @@ $popup-prefix: '#{$prefix}-popup';
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
// 上方弹出,箭头在底部
|
||||
&[data-popup-placement^='top'] > &-arrow {
|
||||
bottom: -4px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { arrow, autoUpdate, flip, offset, useFloating } from '@floating-ui/vue'
|
||||
|
||||
import {
|
||||
@@ -17,6 +17,8 @@ import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
export default defineComponent({
|
||||
name: 'APopup',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
|
||||
trigger: {
|
||||
type: [String, Array] as PropType<
|
||||
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
|
||||
@@ -33,7 +35,7 @@ export default defineComponent({
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
@@ -60,18 +62,19 @@ export default defineComponent({
|
||||
default: () => ({}),
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
emits: ['update:visible'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('popup')
|
||||
|
||||
const triggerRef = ref<HTMLElement | null>(null) // 触发 引用
|
||||
const floatingRef = ref<HTMLElement | null>(null) // float 引用
|
||||
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
|
||||
const visible = ref(false)
|
||||
const hasRendered = ref(false)
|
||||
const localVisible = ref(props.visible)
|
||||
const hasRendered = ref(props.visible)
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
const { floatingStyles, middlewareData, placement, update } = useFloating(
|
||||
@@ -132,25 +135,43 @@ export default defineComponent({
|
||||
floatEl.style.width = `${triggerEl.offsetWidth}px`
|
||||
}
|
||||
|
||||
// 准备浮动框
|
||||
const prepareFloating = async () => {
|
||||
const showPopup = async () => {
|
||||
if (!hasRendered.value)
|
||||
hasRendered.value = true
|
||||
await nextTick()
|
||||
|
||||
fitFloatingWidth()
|
||||
localVisible.value = true
|
||||
|
||||
setupAutoUpdate()
|
||||
emit('update:visible', true)
|
||||
}
|
||||
|
||||
const showPopup = async () => {
|
||||
await prepareFloating()
|
||||
visible.value = true
|
||||
}
|
||||
const hidePopup = async () => {
|
||||
localVisible.value = false
|
||||
|
||||
const hidePopup = () => {
|
||||
visible.value = false
|
||||
clearAutoUpdate()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const togglePopup = async () => {
|
||||
await prepareFloating()
|
||||
visible.value = !visible.value
|
||||
if (!hasRendered.value)
|
||||
hasRendered.value = true
|
||||
await nextTick()
|
||||
|
||||
fitFloatingWidth()
|
||||
|
||||
localVisible.value = !localVisible.value
|
||||
await nextTick()
|
||||
|
||||
if (localVisible.value) {
|
||||
setupAutoUpdate()
|
||||
}
|
||||
else {
|
||||
clearAutoUpdate()
|
||||
}
|
||||
|
||||
emit('update:visible', localVisible.value)
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
@@ -181,29 +202,57 @@ export default defineComponent({
|
||||
const triggerEl = getDOM(triggerRef.value)
|
||||
const floatingEl = getDOM(floatingRef.value)
|
||||
if (!triggerEl?.contains(target) && !floatingEl?.contains(target)) {
|
||||
visible.value = false
|
||||
localVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 Teleport 挂载目标
|
||||
const getTeleportTarget = () => {
|
||||
if (props.appendTo instanceof HTMLElement)
|
||||
return props.appendTo
|
||||
if (typeof props.appendTo === 'string') {
|
||||
const targetEl = ref<HTMLElement>(document.body)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.appendTo instanceof HTMLElement) {
|
||||
targetEl.value = props.appendTo
|
||||
}
|
||||
else if (typeof props.appendTo === 'string') {
|
||||
const el = document.querySelector(props.appendTo)
|
||||
if (el)
|
||||
return el
|
||||
targetEl.value = el as HTMLElement
|
||||
}
|
||||
return 'body'
|
||||
}
|
||||
})
|
||||
|
||||
// 渲染 trigger slot
|
||||
const renderTriggerSlot = () => {
|
||||
const slotContent = slots.reference?.() || slots.default?.() || []
|
||||
if (slotContent.length === 1)
|
||||
return cloneVNode(slotContent[0], { ref: triggerRef, ...eventProps })
|
||||
// 只有一个子节点时
|
||||
if (slotContent.length === 1) {
|
||||
const vnode = slotContent[0]
|
||||
return cloneVNode(vnode, {
|
||||
...eventProps,
|
||||
ref: (el: any) => {
|
||||
// 使用 getDOM 解析真实 DOM
|
||||
triggerRef.value = getDOM(el)
|
||||
|
||||
// 保留用户传入的 ref
|
||||
const originalRef = (vnode as any).ref
|
||||
if (typeof originalRef === 'function') {
|
||||
originalRef(el)
|
||||
}
|
||||
else if (originalRef && typeof originalRef === 'object') {
|
||||
;(originalRef as any).value = el
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 多个子节点时,包一层 span
|
||||
return (
|
||||
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
||||
<span
|
||||
style="display:inline-flex;"
|
||||
{...eventProps}
|
||||
ref={(el) => {
|
||||
triggerRef.value = getDOM(el)
|
||||
}}
|
||||
>
|
||||
{slotContent.map(v => cloneVNode(v))}
|
||||
</span>
|
||||
)
|
||||
@@ -218,24 +267,29 @@ export default defineComponent({
|
||||
clearAutoUpdate()
|
||||
})
|
||||
|
||||
watch(visible, async (val) => {
|
||||
if (val) {
|
||||
setupAutoUpdate()
|
||||
}
|
||||
else {
|
||||
clearAutoUpdate()
|
||||
}
|
||||
})
|
||||
watch(() => props.visible, (val) => {
|
||||
localVisible.value = val
|
||||
}, { immediate: true })
|
||||
|
||||
const renderContent = (content: string | VNode | (() => VNode)) => {
|
||||
if (typeof content === 'function')
|
||||
return content()
|
||||
if (typeof content === 'string') {
|
||||
return <div class={`${prefixCls}-content`}>{content}</div>
|
||||
}
|
||||
return content
|
||||
}
|
||||
return () => {
|
||||
// 懒加载
|
||||
const shouldRenderPopup = props.destroyOnClose ? visible.value : hasRendered.value
|
||||
const shouldRenderPopup = props.destroyOnClose
|
||||
? localVisible.value // 需要销毁时,只在显示时渲染
|
||||
: localVisible.value || hasRendered.value // 不销毁时保持已渲染
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderTriggerSlot()}
|
||||
|
||||
<Teleport to={getTeleportTarget()}>
|
||||
<Teleport to={targetEl.value}>
|
||||
{shouldRenderPopup && (
|
||||
<div
|
||||
style={{
|
||||
@@ -250,7 +304,7 @@ export default defineComponent({
|
||||
appear
|
||||
>
|
||||
<div
|
||||
v-show={visible.value}
|
||||
v-show={localVisible.value}
|
||||
ref={floatingRef}
|
||||
class={[prefixCls, props.popupClass]}
|
||||
style={{
|
||||
@@ -260,7 +314,7 @@ export default defineComponent({
|
||||
}}
|
||||
data-popup-placement={placement.value}
|
||||
>
|
||||
{slots.content ? slots.content() : props.content}
|
||||
{slots.content ? slots.content() : renderContent(props.content)}
|
||||
{props.showArrow && (
|
||||
<span
|
||||
class={`${prefixCls}-arrow`}
|
||||
|
||||
@@ -7,7 +7,7 @@ const notification = useNotification()
|
||||
function showCustomMessage() {
|
||||
notification.success({
|
||||
title: '自定义消息',
|
||||
content: () =>
|
||||
message: () =>
|
||||
h('div', { style: { color: 'red', position: 'relative' } }, [
|
||||
h('p', {}, '这里是自定义内容,可以是任意 VNode'),
|
||||
h('strong', {}, '加粗文本'),
|
||||
@@ -23,29 +23,11 @@ function showCustomMessage() {
|
||||
gap: '4px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
style: { fontSize: '12px', padding: '2px 6px' },
|
||||
onClick: () => alert('Button A clicked'),
|
||||
},
|
||||
'A',
|
||||
),
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
style: { fontSize: '12px', padding: '2px 6px' },
|
||||
onClick: () => alert('Button B clicked'),
|
||||
},
|
||||
'B',
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
type: 'success',
|
||||
duration: 0,
|
||||
showClose: true,
|
||||
duration: 0,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,11 +10,13 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('@ao/ao-design')['Button']
|
||||
AConfigProvider: typeof import('@ao/ao-design')['ConfigProvider']
|
||||
ADrawer: typeof import('@ao/ao-design')['Drawer']
|
||||
AGroup: typeof import('@ao/ao-design')['Group']
|
||||
AIcon: typeof import('@ao/ao-design')['Icon']
|
||||
AInput: typeof import('@ao/ao-design')['Input']
|
||||
ALoading: typeof import('@ao/ao-design')['Loading']
|
||||
AMessage: typeof import('@ao/ao-design')['Message']
|
||||
APopconfirm: typeof import('@ao/ao-design')['Popconfirm']
|
||||
APopup: typeof import('@ao/ao-design')['Popup']
|
||||
ASpin: typeof import('@ao/ao-design')['Spin']
|
||||
ATextarea: typeof import('@ao/ao-design')['Textarea']
|
||||
|
||||
Reference in new issue
Block a user