This commit is contained in:
oneao committed 2025-09-12 17:32:27 +08:00
1 parent 14f9844430
commit 785bce1907
27 files changed
+1051 -175

No files matched your search

@@ -13,30 +13,48 @@ $default-color-hover: var(--ao-default-color-hover); // hover态
$default-color-focus: var(--ao-default-color-focus); // focus态,包括鼠标和键盘
$default-color-active: var(--ao-default-color-active); // 点击态
$default-color-disabled: var(--ao-default-color-disabled); // 禁用态
$default-color-light: var(--ao-default-color-light);
$default-color-light-active: var(--ao-default-color-light-active);
// 基础颜色的扩展 用于 hover / 聚焦 / 禁用 / 点击 等状态
$primary-color-hover: var(--ao-primary-color-hover); // hover态
$primary-color-focus: var(--ao-primary-color-focus); // focus态,包括鼠标和键盘
$primary-color-active: var(--ao-primary-color-active); // 点击态
$primary-color-disabled: var(--ao-primary-color-disabled); // 禁用态
$primary-color-light: var(--ao-primary-color-light);
$primary-color-light-active: var(--ao-primary-color-light-active);
// 警告色扩展
$warning-color-hover: var(--ao-warning-color-hover);
$warning-color-focus: var(--ao-warning-color-focus);
$warning-color-active: var(--ao-warning-color-active);
$warning-color-disabled: var(--ao-warning-color-disabled);
$warning-color-light: var(--ao-warning-color-light);
$warning-color-light-active: var(--ao-warning-color-light-active);
// 失败/错误色扩展
$danger-color-hover: var(--ao-danger-color-hover);
$danger-color-focus: var(--ao-danger-color-focus);
$danger-color-active: var(--ao-danger-color-active);
$danger-color-disabled: var(--ao-danger-color-disabled);
$danger-color-light: var(--ao-danger-color-light);
$danger-color-light-active: var(--ao-danger-color-light-active);
// 成功色扩展
$success-color-hover: var(--ao-success-color-hover);
$success-color-focus: var(--ao-success-color-focus);
$success-color-active: var(--ao-success-color-active);
$success-color-disabled: var(--ao-success-color-disabled);
$success-color-light: var(--ao-success-color-light);
$success-color-light-active: var(--ao-success-color-light-active);
// 状态背景色
$status-base-bg-color: var(--ao-status-base-bg-color);
$status-anti-bg-color: var(--ao-status-anti-bg-color);
$status-info-bg-color: var(--ao-status-info-bg-color);
$status-success-bg-color: var(--ao-status-success-bg-color);
$status-warning-bg-color: var(--ao-status-warning-bg-color);
$status-danger-bg-color: var(--ao-status-danger-bg-color);
// 文本颜色
$text-color-base: var(--ao-text-color-base); // 色彩-文字-主要
@@ -9,26 +9,43 @@
--ao-default-color-focus: #505050;
--ao-default-color-active: #5c5c5c;
--ao-default-color-disabled: #2c2c2c;
--ao-default-color-light: #f3f3f3;
--ao-default-color-light-active: #ddd;
--ao-primary-color-hover: #2667d4;
--ao-primary-color-focus: #173463;
--ao-primary-color-active: #699ef5;
--ao-primary-color-disabled: #143975;
--ao-primary-color-light: #1b2f51;
--ao-primary-color-light-active: #173463;
--ao-warning-color-hover: #a75d2b;
--ao-warning-color-focus: #582f21;
--ao-warning-color-active: #dc7633;
--ao-warning-color-disabled: #733c23;
--ao-warning-color-light: #4f2a1d;
--ao-warning-color-light-active: #582f21;
--ao-danger-color-hover: #a03f46;
--ao-danger-color-focus: #5e2a2d;
--ao-danger-color-active: #de6670;
--ao-danger-color-disabled: #703439;
--ao-danger-color-light: #472324;
--ao-danger-color-light-active: #5e2a2d;
--ao-success-color-hover: #0d7a55;
--ao-success-color-focus: #1a4230;
--ao-success-color-active: #43af8a;
--ao-success-color-disabled: #17533d;
--ao-success-color-light: #193a2a;
--ao-success-color-light-active: #1a4230;
--ao-status-base-bg-color: #242424;
--ao-status-anti-bg-color: #c6cacd; // 反色,用于tooltip默认背景色
--ao-status-info-bg-color: #233448;
--ao-status-success-bg-color: #253929;
--ao-status-warning-bg-color: #453522;
--ao-status-danger-bg-color: #442927;
--ao-text-color-base: rgba(255, 255, 255, 90%); /* 主要文字 */
--ao-text-color-secondary: rgba(255, 255, 255, 55%); /* 次要文字 / 辅助信息 */
@@ -60,10 +77,9 @@
// 基础/下层 投影 hover 使用的组件包括:表格 /
--ao-shadow-surface: 0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%);
// 中层投影 下拉 使用的组件包括:下拉菜单 / 气泡确认框 / 选择器 /
--ao-shadow-popup: 0 8px 10px rgba(0, 0, 0, 12%), 0 3px 14px rgba(0, 0, 0, 10%), 0 5px 5px rgba(0, 0, 0, 16%);
--ao-shadow-popup: 0 0 4px rgba(0, 0, 0, 0.04), 0 0 8px rgba(0, 0, 0, 0.03);
// 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知
--ao-shadow-overlay: 0 16px 24px rgba(0, 0, 0, 14%), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%);
--ao-shadow-overlay: inset 0 0 0 1px hsla(0, 0%, 100%, 0.1), 0 4px 14px rgba(0, 0, 0, 0.25);
// 内投影
--ao-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--ao-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
@@ -10,26 +10,43 @@
--ao-default-color-focus: #cfcfcf;
--ao-default-color-active: #ababab;
--ao-default-color-disabled: #eeeeee;
--ao-default-color-light: #f3f3f3;
--ao-default-color-light-active: #ddd;
--ao-primary-color-hover: #366ef4;
--ao-primary-color-focus: #d9e1ff;
--ao-primary-color-active: #003cab;
--ao-primary-color-disabled: #b5c7ff;
--ao-primary-color-light: #f2f3ff;
--ao-primary-color-light-active: #d9e1ff;
--ao-warning-color-hover: #fa9550;
--ao-warning-color-focus: #ffd9c2;
--ao-warning-color-active: #be5a00;
--ao-warning-color-disabled: #ffb98c;
--ao-warning-color-light: #fff1e9;
--ao-warning-color-light-active: #ffd9c2;
--ao-danger-color-hover: #f6685d;
--ao-danger-color-focus: #ffd8d2;
--ao-danger-color-active: #ad352f;
--ao-danger-color-disabled: #ffb9b0;
--ao-danger-color-light: #fff0ed;
--ao-danger-color-light-active: #ffd8d2;
--ao-success-color-hover: #56c08d;
--ao-success-color-focus: #c6f3d7;
--ao-success-color-active: #008858;
--ao-success-color-disabled: #92dab2;
--ao-success-color-light: #e3f9e9;
--ao-success-color-light-active: #c6f3d7;
--ao-status-base-bg-color: #fff;
--ao-status-anti-bg-color: #41464c; // 反色,用于tooltip默认背景色
--ao-status-info-bg-color: #f2f3ff;
--ao-status-success-bg-color: #ecf7ec;
--ao-status-warning-bg-color: #fff8ea;
--ao-status-danger-bg-color: #fef2ed;
--ao-text-color-base: rgba(0, 0, 0, 90%); /* 主要文字 */
--ao-text-color-secondary: rgba(0, 0, 0, 60%); /* 次要文字 / 辅助信息 */
@@ -61,13 +78,10 @@
// 基础/下层 投影 hover 使用的组件包括:表格 /
--ao-shadow-surface: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
// 中层投影 下拉 使用的组件包括:下拉菜单 / 气泡确认框 / 选择器 /
--ao-shadow-popup:
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
// 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知
--ao-shadow-overlay:
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
--ao-shadow-popup: 0 0 4px rgba(0, 0, 0, 0.04), 0 0 8px rgba(0, 0, 0, 0.03);
// 内投影
// 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知
--ao-shadow-overlay: 0 0 1px rgba(0, 0, 0, 0.3), 0 4px 14px rgba(0, 0, 0, 0.1);
--ao-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--ao-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--ao-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
@@ -7,4 +7,7 @@ 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 Tag } from './tag'
export { default as TagGroup } from './tag-group'
export { default as Textarea } from './textarea'
export { default as Tooltip } from './tooltip'
@@ -10,3 +10,6 @@
@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 './tag-group/index.scss' as *;
@@ -21,6 +21,8 @@ function showMessageType(type: any) {
message.success({
message: `这里是 ${type} 提示`,
type,
duration: 0,
theme: 'light',
})
}
</script>
@@ -76,6 +78,9 @@ function showMessageType(type: any) {
</div>
<div class="row">
<AButton status="primary" @click="showMessageType('default')">
default
</AButton>
<AButton status="primary" @click="showMessageType('info')">
info
</AButton>
@@ -85,7 +90,7 @@ function showMessageType(type: any) {
<AButton status="primary" @click="showMessageType('warning')">
warning
</AButton>
<AButton status="primary" @click="showMessageType('error')">
<AButton status="primary" @click="showMessageType('danger')">
error
</AButton>
<AButton status="primary" @click="showMessageType('loading')">
@@ -47,22 +47,34 @@ $message-prefix: #{$prefix}-message;
}
&-info {
color: $primary-color;
.#{$message-prefix}-icon {
color: $primary-color;
}
}
&-success {
color: $success-color;
.#{$message-prefix}-icon {
color: $success-color;
}
}
&-warning {
color: $warning-color;
.#{$message-prefix}-icon {
color: $warning-color;
}
}
&-error {
color: $danger-color;
&-danger {
.#{$message-prefix}-icon {
color: $danger-color;
}
}
&-question {
color: $primary-color;
.#{$message-prefix}-icon {
color: $primary-color;
}
}
&-loading {
color: $primary-color;
.#{$message-prefix}-icon {
color: $primary-color;
}
}
&-placement {
&-top {
@@ -128,3 +140,27 @@ $message-prefix: #{$prefix}-message;
}
}
}
.#{$message-prefix}-theme-normal.#{$message-prefix} {
background-color: $status-base-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-default {
background-color: $status-base-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-info {
background-color: $status-info-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-success {
background-color: $status-success-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-warning {
background-color: $status-warning-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-danger {
background-color: $status-danger-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-loading {
background-color: $status-info-bg-color;
}
.#{$message-prefix}-theme-light.#{$message-prefix}-question {
background-color: $status-info-bg-color;
}
@@ -14,11 +14,13 @@ type Placement
| 'bottom-end'
| 'center'
type MessageType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'question'
type MessageType = 'default' | 'info' | 'success' | 'warning' | 'danger' | 'loading' | 'question'
type MessageTheme = 'normal' | 'light'
interface MessageOptions {
message: string | VNode | (() => VNode)
type?: MessageType
theme?: MessageTheme
placement?: Placement
duration?: number
showClose?: boolean
@@ -30,10 +32,11 @@ interface MessageHandle {
interface MessageFn {
(msg: string | MessageOptions): MessageHandle
open: (msg: string | MessageOptions) => MessageHandle
info: (msg: string | MessageOptions) => MessageHandle
success: (msg: string | MessageOptions) => MessageHandle
warning: (msg: string | MessageOptions) => MessageHandle
error: (msg: string | MessageOptions) => MessageHandle
danger: (msg: string | MessageOptions) => MessageHandle
closeAll: () => void
}
@@ -68,13 +71,13 @@ function getTransform(placement: Placement, hidden: boolean) {
export function useMessage(): MessageFn {
const createMessage = (
msg: string | MessageOptions,
defaultType: MessageType = 'info',
defaultType: MessageType = 'default',
): MessageHandle => {
const opts: MessageOptions = typeof msg === 'string'
? { message: msg, type: defaultType }
: { ...msg, type: msg.type || defaultType }
const { message, type, placement = 'top', duration = 3000, showClose } = opts
const { message, type, theme, placement = 'top', duration = 3000, showClose } = opts
const container = getContainer(placement)
let timer: number | undefined
let handle: MessageHandle
@@ -83,6 +86,7 @@ export function useMessage(): MessageFn {
render() {
return h(MessageItem, {
type,
theme,
message,
placement,
showClose,
@@ -140,10 +144,11 @@ export function useMessage(): MessageFn {
}
const api = ((msg: string | MessageOptions) => createMessage(msg)) as MessageFn
api.open = msg => createMessage(msg, 'default')
api.info = msg => createMessage(msg, 'info')
api.success = msg => createMessage(msg, 'success')
api.warning = msg => createMessage(msg, 'warning')
api.error = msg => createMessage(msg, 'error')
api.danger = msg => createMessage(msg, 'danger')
api.closeAll = () => {
instances.slice().forEach(instance => instance.close())
}
@@ -8,7 +8,8 @@ const prefixCls = getPrefixCls('message')
export default defineComponent({
name: 'MessageItem',
props: {
type: { type: String, default: 'info' },
type: { type: String, default: 'default' },
theme: { type: String, default: 'normal' },
message: {
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
required: true,
@@ -23,7 +24,7 @@ export default defineComponent({
setup(props) {
const iconMap: Record<string, string> = {
success: 'ix:success-filled',
error: 'ix:error-filled',
danger: 'ix:error-filled',
warning: 'ix:warning-filled',
info: 'ix:info-filled',
question: 'ix:question-filled',
@@ -36,14 +37,16 @@ export default defineComponent({
}
return () => (
<div
class={`${prefixCls}`}
class={`${prefixCls} ${prefixCls}-theme-${props.theme ?? 'normal'} ${prefixCls}-${props.type ?? 'default'}`}
>
<Icon
icon={iconMap[props.type ?? 'info']}
size={20}
class={`${prefixCls}-icon ${prefixCls}-${props.type ?? 'info'}`}
spin={props.type === 'loading'}
/>
{props.type !== 'default' && (
<Icon
icon={iconMap[props.type ?? 'info']}
size={20}
class={`${prefixCls}-icon`}
spin={props.type === 'loading'}
/>
)}
<span class={`${prefixCls}-message`}>
{renderMessage(props.message)}
</span>
@@ -1,59 +1,85 @@
<script setup lang="ts">
import { useNotification } from '@ao/ao-design'
import { h } from 'vue'
const notification = useNotification()
function showCustomMessage() {
function showNotification(placement: any) {
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,
title: `这里是 ${placement} 标题`,
message: `这里是 ${placement} 提示`,
placement,
})
}
function closeOne() {
}
function showNotificationType(type: any) {
notification.success({
theme: 'light',
title: `这里是 ${type} 标题`,
message: `这里是 ${type} 提示`,
type,
duration: 0,
})
}
</script>
<template>
<div class="demo">
<AButton status="primary" @click="showCustomMessage">
自定义 h 函数消息
</AButton>
<div class="row">
<AButton status="primary" @click="showNotification('top-start')">
Top Start
</AButton>
<AButton status="primary" @click="showNotification('top-end')">
Top End
</AButton>
</div>
<div class="row">
<AButton status="primary" @click="showNotification('bottom-start')">
Bottom Start
</AButton>
<AButton status="primary" @click="showNotification('bottom-end')">
Bottom End
</AButton>
</div>
<div class="row">
<AButton status="primary" @click="notification.closeAll">
Close All
</AButton>
<AButton
status="primary" @click="closeOne"
>
Show No Close
</AButton>
</div>
<div class="row">
<AButton status="primary" @click="showNotificationType('default')">
default
</AButton>
<AButton status="primary" @click="showNotificationType('info')">
info
</AButton>
<AButton status="primary" @click="showNotificationType('success')">
success
</AButton>
<AButton status="primary" @click="showNotificationType('warning')">
warning
</AButton>
<AButton status="primary" @click="showNotificationType('danger')">
error
</AButton>
<AButton status="primary" @click="showNotificationType('loading')">
loading
</AButton>
<AButton status="primary" @click="showNotificationType('question')">
question
</AButton>
</div>
</div>
</template>
@@ -65,4 +91,9 @@ function showCustomMessage() {
gap: 16px;
margin-top: 40px;
}
.row {
display: flex;
gap: 12px;
}
</style>
@@ -24,7 +24,6 @@ $notification-prefix: #{$prefix}-notification;
border: 0;
margin: 0;
display: flex;
background-color: #ecf7ec;
&-icon {
text-align: center;
@@ -47,8 +46,6 @@ $notification-prefix: #{$prefix}-notification;
&-title {
font-size: 16px;
font-weight: 600;
line-height: 1;
small {
margin: 0px 6px;
font-size: 14px;
@@ -76,26 +73,35 @@ $notification-prefix: #{$prefix}-notification;
word-break: break-all;
}
&-footer {
}
&-info {
color: $primary-color;
.#{$notification-prefix}-icon {
color: $primary-color;
}
}
&-success {
color: $success-color;
.#{$notification-prefix}-icon {
color: $success-color;
}
}
&-warning {
color: $warning-color;
.#{$notification-prefix}-icon {
color: $warning-color;
}
}
&-error {
color: $danger-color;
&-danger {
.#{$notification-prefix}-icon {
color: $danger-color;
}
}
&-question {
color: $primary-color;
.#{$notification-prefix}-icon {
color: $primary-color;
}
}
&-loading {
color: $primary-color;
.#{$notification-prefix}-icon {
color: $primary-color;
}
}
&-placement {
@@ -125,3 +131,27 @@ $notification-prefix: #{$prefix}-notification;
}
}
}
.#{$notification-prefix}-theme-normal.#{$notification-prefix} {
background-color: $status-base-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-default {
background-color: $status-base-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-info {
background-color: $status-info-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-success {
background-color: $status-success-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-warning {
background-color: $status-warning-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-danger {
background-color: $status-danger-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-loading {
background-color: $status-info-bg-color;
}
.#{$notification-prefix}-theme-light.#{$notification-prefix}-question {
background-color: $status-info-bg-color;
}
@@ -9,13 +9,14 @@ type Placement
| 'bottom-start'
| 'bottom-end'
type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'loading' | 'question'
type NotificationType = 'default' | 'info' | 'success' | 'warning' | 'danger' | 'loading' | 'question'
type NotificationTheme = 'normal' | 'light'
interface NotificationOptions {
message: string | VNode | (() => VNode)
title: string
subTitle?: string
theme?: NotificationTheme
type?: NotificationType
placement?: Placement
duration?: number
@@ -28,10 +29,11 @@ interface NotificationHandle {
interface MessageFn {
(msg: NotificationOptions): NotificationHandle
open: (msg: NotificationOptions) => NotificationHandle
info: (msg: NotificationOptions) => NotificationHandle
success: (msg: NotificationOptions) => NotificationHandle
warning: (msg: NotificationOptions) => NotificationHandle
error: (msg: NotificationOptions) => NotificationHandle
danger: (msg: NotificationOptions) => NotificationHandle
closeAll: () => void
}
@@ -65,9 +67,9 @@ function getTransform(placement: Placement, hidden: boolean) {
export function useNotification(): MessageFn {
const createNotification = (
opts: NotificationOptions,
defaultType: NotificationType = 'info',
defaultType: NotificationType = 'default',
): NotificationHandle => {
const { message, title, subTitle, type = defaultType, placement = 'top-end', duration = 3000, showClose } = opts
const { message, title, subTitle, type = defaultType, theme, placement = 'top-end', duration = 3000, showClose } = opts
const container = getContainer(placement)
let timer: number | undefined
@@ -77,6 +79,7 @@ export function useNotification(): MessageFn {
render() {
return h(MessageItem, {
type,
theme,
title,
subTitle,
message,
@@ -136,10 +139,11 @@ export function useNotification(): MessageFn {
}
const api = ((msg: NotificationOptions) => createNotification(msg)) as MessageFn
api.open = msg => createNotification(msg, 'default')
api.info = msg => createNotification(msg, 'info')
api.success = msg => createNotification(msg, 'success')
api.warning = msg => createNotification(msg, 'warning')
api.error = msg => createNotification(msg, 'error')
api.danger = msg => createNotification(msg, 'danger')
api.closeAll = () => {
instances.slice().forEach(instance => instance.close())
}
@@ -8,7 +8,8 @@ const prefixCls = getPrefixCls('notification')
export default defineComponent({
name: 'NotificationItem',
props: {
type: { type: String, default: 'info' },
type: { type: String, default: 'default' },
theme: { type: String, default: 'normal' },
title: { type: String, required: true },
message: {
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
@@ -25,7 +26,7 @@ export default defineComponent({
setup(props) {
const iconMap: Record<string, string> = {
success: 'ix:success-filled',
error: 'ix:error-filled',
danger: 'ix:error-filled',
warning: 'ix:warning-filled',
info: 'ix:info-filled',
question: 'ix:question-filled',
@@ -38,15 +39,16 @@ export default defineComponent({
}
return () => (
<div
class={`${prefixCls}`}
class={`${prefixCls} ${prefixCls}-theme-${props.theme ?? 'normal'} ${prefixCls}-${props.type ?? 'info'}`}
>
<Icon
icon={iconMap[props.type ?? 'info']}
size={22}
class={`${prefixCls}-icon ${prefixCls}-${props.type ?? 'info'}`}
spin={props.type === 'loading'}
/>
{props.type !== 'default' && (
<Icon
icon={iconMap[props.type ?? 'info']}
size={22}
class={`${prefixCls}-icon`}
spin={props.type === 'loading'}
/>
)}
<div class={`${prefixCls}-main`}>
<div class={`${prefixCls}-title-wrapper`}>
<span class={`${prefixCls}-title`}>
@@ -23,14 +23,16 @@ $popup-prefix: '#{$prefix}-popup';
content: '';
position: absolute;
inset: 0;
background: $bg-color-container;
background-color: $bg-color-container;
transform: rotate(45deg);
transform-origin: center;
}
}
&-content {
padding: 4px 8px;
}
// 上方弹出,箭头在底部
&[data-popup-placement^='top'] > &-arrow {
bottom: -4px;
@@ -53,11 +53,11 @@ export default defineComponent({
type: Boolean,
default: false,
},
popupClass: {
class: {
type: [String, Array, Object],
default: '',
},
popupStyle: {
style: {
type: Object,
default: () => ({}),
},
@@ -65,6 +65,8 @@ export default defineComponent({
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
default: '',
},
showDelay: { type: Number, default: 0 },
hideDelay: { type: Number, default: 0 },
},
emits: ['update:visible'],
setup(props, { slots, emit }) {
@@ -75,6 +77,10 @@ export default defineComponent({
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
const localVisible = ref(props.visible)
const hasRendered = ref(props.visible)
// 定时器
let showTimer: number | null = null
let hideTimer: number | null = null
let cleanup: (() => void) | null = null
const { floatingStyles, middlewareData, placement, update } = useFloating(
@@ -135,43 +141,65 @@ export default defineComponent({
floatEl.style.width = `${triggerEl.offsetWidth}px`
}
const clearTimers = () => {
if (showTimer) {
clearTimeout(showTimer)
showTimer = null
}
if (hideTimer) {
clearTimeout(hideTimer)
hideTimer = null
}
}
const showPopup = async () => {
if (!hasRendered.value)
hasRendered.value = true
await nextTick()
clearTimers()
showTimer = window.setTimeout(async () => {
if (!hasRendered.value)
hasRendered.value = true
await nextTick()
fitFloatingWidth()
localVisible.value = true
setupAutoUpdate()
emit('update:visible', true)
fitFloatingWidth()
localVisible.value = true
setupAutoUpdate()
emit('update:visible', true)
}, props.showDelay)
}
const hidePopup = async () => {
localVisible.value = false
clearAutoUpdate()
emit('update:visible', false)
clearTimers()
hideTimer = window.setTimeout(() => {
localVisible.value = false
clearAutoUpdate()
emit('update:visible', false)
}, props.hideDelay)
}
const togglePopup = async () => {
clearTimers()
if (!hasRendered.value)
hasRendered.value = true
await nextTick()
fitFloatingWidth()
localVisible.value = !localVisible.value
await nextTick()
if (localVisible.value) {
setupAutoUpdate()
if (!localVisible.value) {
// 需要显示
showTimer = window.setTimeout(async () => {
localVisible.value = true
await nextTick()
setupAutoUpdate()
emit('update:visible', true)
}, props.showDelay)
}
else {
clearAutoUpdate()
// 需要隐藏
hideTimer = window.setTimeout(() => {
localVisible.value = false
clearAutoUpdate()
emit('update:visible', false)
}, props.hideDelay)
}
emit('update:visible', localVisible.value)
}
// 事件绑定
@@ -306,11 +334,11 @@ export default defineComponent({
<div
v-show={localVisible.value}
ref={floatingRef}
class={[prefixCls, props.popupClass]}
class={[prefixCls, props.class]}
style={{
...floatingStyles.value,
position: 'absolute',
...props.popupStyle,
...props.style,
}}
data-popup-placement={placement.value}
>
@@ -0,0 +1,12 @@
@use '../_styles/_variables.scss' as *;
@use 'sass:color';
$tag-prefix: '#{$prefix}-tag';
$tag-group-prefix: '#{$prefix}-tag-group';
.#{$tag-group-prefix} {
> .#{$tag-prefix}:not(:last-child) {
margin-right: 8px;
}
}
@@ -0,0 +1,73 @@
import type { PropType, VNode } from 'vue'
import { cloneVNode, defineComponent, ref } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Tag from '../tag/index'
export default defineComponent({
name: 'TagGroup',
props: {
modelValue: { type: Array as PropType<(string | number)[]>, default: () => [] },
multiple: { type: Boolean, default: true },
options: {
type: Array as PropType<Array<{ label: string, value: string | number }>>,
default: () => [],
},
checkedProps: {
type: Object as PropType<{
type?: string
status?: string
}>,
default: () => ({
type: 'solid',
status: 'primary',
}),
},
uncheckedProps: {
type: Object as PropType<{
type?: string
status?: string
}>,
default: () => ({
type: 'solid',
status: 'default',
}),
},
class: {
type: [String, Array, Object],
default: '',
},
style: {
type: Object,
default: () => ({}),
},
},
emits: [],
setup(props, { slots }) {
const prefixCls = getPrefixCls('tag-group')
const selectedValues = ref([...props.modelValue])
const renderSlotTags = () => {
const slotNodes = slots.default?.() || []
return slotNodes.map((vnode: VNode) => {
// 判断是否是 Tag 组件
if (vnode.type === Tag) {
const value = (vnode.props as any)?.value
const checked = selectedValues.value.includes(value)
return cloneVNode(vnode, {
checked,
'type': checked ? props.checkedProps.type : props.uncheckedProps.type,
'status': checked ? props.checkedProps.status : props.uncheckedProps.status,
'onUpdate:checked': (val: boolean) => handleTagClick(value, val),
})
}
return vnode
})
}
return () => (
<div class={[prefixCls, props.class]} style={{ ...props.style }}>
{slots.default?.()}
</div>
)
},
})
@@ -0,0 +1,121 @@
<script setup lang="ts">
</script>
<template>
<div class="demo">
<div class="row">
<ATag size="small">
small
</ATag>
<ATag size="medium">
medium
</ATag>
<ATag size="large">
large
</ATag>
</div>
<div class="row">
<ATag shape="round">
round
</ATag>
<ATag shape="mark">
mark
</ATag>
<ATag shape="square">
square
</ATag>
</div>
<div class="row">
<ATag type="solid">
solid
</ATag>
<ATag type="light">
light
</ATag>
<ATag type="outline">
outline
</ATag>
<ATag type="outline-light">
outline-light
</ATag>
</div>
<div class="row">
<ATag type="solid" status="primary">
solid
</ATag>
<ATag type="light" status="primary">
light
</ATag>
<ATag type="outline" status="primary">
outline
</ATag>
<ATag type="outline-light" status="primary">
outline-light
</ATag>
</div>
<div class="row">
<ATag type="solid" status="danger">
solid
</ATag>
<ATag type="light" status="danger">
light
</ATag>
<ATag type="outline" status="danger">
outline
</ATag>
<ATag type="outline-light" status="danger">
outline-light
</ATag>
</div>
<div class="row">
<ATag type="solid" status="warning">
solid
</ATag>
<ATag type="light" status="warning">
light
</ATag>
<ATag type="outline" status="warning">
outline
</ATag>
<ATag type="outline-light" status="warning">
outline-light
</ATag>
</div>
<div class="row">
<ATag type="solid" status="success">
solid
</ATag>
<ATag type="light" status="success">
light
</ATag>
<ATag type="outline" status="success">
outline
</ATag>
<ATag type="outline-light" status="success">
outline-light
</ATag>
</div>
</div>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
margin-top: 40px;
}
.row {
display: flex;
gap: 12px;
}
</style>
@@ -0,0 +1,157 @@
@use '../_styles/_variables.scss' as *;
@use 'sass:color';
$tag-prefix: '#{$prefix}-tag';
.#{$tag-prefix} {
box-sizing: border-box;
margin: 0;
padding: 0;
list-style: none;
display: inline-flex;
align-items: center;
flex-direction: row;
border: 1px solid transparent;
white-space: nowrap;
border-radius: $border-color;
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
&-size-large {
padding: 0 8px;
height: 24px;
font-size: 13px;
}
&-size-medium {
padding: 0 6px;
height: 22px;
font-size: 12px;
}
&-size-small {
padding: 0 4px;
height: 20px;
font-size: 12px;
}
&-shape-round {
border-radius: 12px;
}
&-shape-mark {
border-radius: 0px 12px 12px 0;
}
&-icon {
margin-right: 4px;
}
&-close {
margin-left: 4px;
cursor: pointer;
transition: filter 0.2s cubic-bezier(0.38, 0, 0.24, 1);
color: currentColor;
filter: brightness(1.2); /* 比父色亮 20% */
&:hover {
filter: brightness(0.8); /* 比父色暗 20% */
}
}
&-type-solid {
&.#{$tag-prefix}-status-default {
color: $text-color-base;
background-color: $default-color;
}
&.#{$tag-prefix}-status-primary {
color: $text-color-anti;
background-color: $primary-color;
}
&.#{$tag-prefix}-status-success {
color: $text-color-anti;
background-color: $success-color;
}
&.#{$tag-prefix}-status-warning {
color: $text-color-anti;
background-color: $warning-color;
}
&.#{$tag-prefix}-status-danger {
color: $text-color-anti;
background-color: $danger-color;
}
}
&-type-light {
&.#{$tag-prefix}-status-default {
color: $text-color-base;
background-color: $default-color-light;
}
&.#{$tag-prefix}-status-primary {
color: $primary-color;
background-color: $primary-color-light;
}
&.#{$tag-prefix}-status-success {
color: $success-color;
background-color: $success-color-light;
}
&.#{$tag-prefix}-status-warning {
color: $warning-color;
background-color: $warning-color-light;
}
&.#{$tag-prefix}-status-danger {
color: $danger-color;
background-color: $danger-color-light;
}
}
&-type-outline {
&.#{$tag-prefix}-status-default {
color: $text-color-base;
background-color: transparent;
border-color: $default-color;
}
&.#{$tag-prefix}-status-primary {
color: $primary-color;
background-color: transparent;
border-color: $primary-color;
}
&.#{$tag-prefix}-status-success {
color: $success-color;
background-color: transparent;
border-color: $success-color;
}
&.#{$tag-prefix}-status-warning {
color: $warning-color;
background-color: transparent;
border-color: $warning-color;
}
&.#{$tag-prefix}-status-danger {
color: $danger-color;
background-color: transparent;
border-color: $danger-color;
}
}
&-type-outline-light {
&.#{$tag-prefix}-status-default {
color: $text-color-base;
background-color: $default-color-light;
border-color: $default-color;
}
&.#{$tag-prefix}-status-primary {
color: $primary-color;
background-color: $primary-color-light;
border-color: $primary-color;
}
&.#{$tag-prefix}-status-success {
color: $success-color;
background-color: $success-color-light;
border-color: $success-color;
}
&.#{$tag-prefix}-status-warning {
color: $warning-color;
background-color: $warning-color-light;
border-color: $warning-color;
}
&.#{$tag-prefix}-status-danger {
color: $danger-color;
background-color: $danger-color-light;
border-color: $danger-color;
}
}
}
@@ -0,0 +1,83 @@
import type { PropType } from 'vue'
import { defineComponent, ref } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Icon from '../icon/index'
export default defineComponent({
name: 'Tag',
props: {
shape: {
type: String as PropType<'square' | 'round' | 'mark'>,
default: 'square',
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium',
},
type: {
type: String as () => 'solid' | 'light' | 'outline' | 'outline-light',
default: 'solid',
},
status: {
type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'default',
default: 'default',
},
showClose: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
class: {
type: [String, Array, Object],
default: '',
},
style: {
type: Object,
default: () => ({}),
},
},
emits: ['click', 'close'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('tag')
const visible = ref(true)
const handleClick = (event: MouseEvent) => {
emit('click', event)
}
const handleClose = (event: MouseEvent) => {
event.stopPropagation()
visible.value = false // 点击关闭隐藏 Tag
emit('close', event)
}
return () =>
visible.value && (
<div
class={[
prefixCls,
`${prefixCls}-size-${props.size}`,
`${prefixCls}-shape-${props.shape}`,
`${prefixCls}-type-${props.type}`,
`${prefixCls}-status-${props.status}`,
props.class,
]}
style={{ ...props.style }}
onClick={handleClick}
>
{props.icon && <Icon icon={props.icon} class={`${prefixCls}-icon`} />}
<span>{slots.default?.()}</span>
{!props.disabled && props.showClose && (
<Icon icon="ix:close" class={`${prefixCls}-close`} onClick={handleClose} />
)}
</div>
)
},
})
@@ -0,0 +1,55 @@
<script setup lang="ts">
</script>
<template>
<div class="demo">
<div class="row">
<ATooltip trigger="click" type="default" content="defaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefaultdefault">
<AButton status="primary">
default
</AButton>
</ATooltip>
<ATooltip trigger="click" type="anti" content="anti">
<AButton status="primary">
anti
</AButton>
</ATooltip>
<ATooltip trigger="click" type="info" content="info">
<AButton status="primary">
info
</AButton>
</ATooltip>
<ATooltip trigger="click" type="success" content="success">
<AButton status="primary">
success
</AButton>
</ATooltip>
<ATooltip trigger="click" type="danger" content="danger">
<AButton status="primary">
danger
</AButton>
</ATooltip>
<ATooltip trigger="click" type="warning" content="warning">
<AButton status="primary">
warning
</AButton>
</ATooltip>
</div>
</div>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
margin-top: 40px;
}
.row {
display: flex;
gap: 12px;
}
</style>
@@ -0,0 +1,88 @@
@use '../_styles/_variables.scss' as *;
@use 'sass:color';
$popup-prefix: '#{$prefix}-popup';
$tooltip-prefix: '#{$prefix}-tooltip';
.#{$tooltip-prefix} {
margin: 0;
padding: 0;
color: $text-color-base;
&-wrapper {
background-color: $status-base-bg-color;
padding: 4px 8px;
word-break: break-all;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-base-bg-color;
}
&-default {
.#{$tooltip-prefix}-wrapper {
background-color: $status-base-bg-color;
color: $text-color-base;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-base-bg-color;
}
}
&-anti {
.#{$tooltip-prefix}-wrapper {
background-color: $status-anti-bg-color;
color: $text-color-anti;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-anti-bg-color;
}
}
&-info {
.#{$tooltip-prefix}-wrapper {
background-color: $status-info-bg-color;
color: $text-color-base;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-info-bg-color;
}
}
&-success {
.#{$tooltip-prefix}-wrapper {
background-color: $status-success-bg-color;
color: $text-color-base;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-success-bg-color;
}
}
&-danger {
.#{$tooltip-prefix}-wrapper {
background-color: $status-danger-bg-color;
color: $text-color-base;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-danger-bg-color;
}
}
&-warning {
.#{$tooltip-prefix}-wrapper {
background-color: $status-warning-bg-color;
color: $text-color-base;
}
.#{$popup-prefix}-arrow::before {
background-color: $status-warning-bg-color;
}
}
&-anti.#{$tooltip-prefix},
&-info.#{$tooltip-prefix},
&-success.#{$tooltip-prefix},
&-danger.#{$tooltip-prefix},
&-warning.#{$tooltip-prefix} {
box-shadow: none;
.#{$popup-prefix}-arrow::before {
box-shadow: none;
}
}
}
@@ -0,0 +1,108 @@
import type { PropType, VNode } from 'vue'
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Popup from '../popup/index'
export default defineComponent({
name: 'Tooltip',
props: {
type: {
type: String as PropType<'default' | 'anti' | 'info' | 'success' | 'danger' | 'warning'>,
default: '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',
},
trigger: {
type: [String, Array] as PropType<
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
>,
default: 'hover',
},
showArrow: {
type: Boolean,
default: true,
},
autoFitPopupMinWidth: {
type: Boolean,
default: false,
},
autoFitPopupWidth: {
type: Boolean,
default: false,
},
destroyOnClose: {
type: Boolean,
default: false,
},
content: {
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
default: '',
},
showDelay: { type: Number, default: 0 },
hideDelay: { type: Number, default: 0 },
class: {
type: [String, Array, Object],
default: '',
},
style: {
type: Object,
default: () => ({}),
},
maxWidth: {
type: [Number, String] as PropType<number | string>,
default: '200px',
},
},
emits: [],
setup(props, { slots }) {
const prefixCls = getPrefixCls('tooltip')
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
}
const renderTooltip = () => {
return (
<div
class={[`${prefixCls}-wrapper`, props.class]}
style={{
maxWidth: props.maxWidth,
...props.style,
}}
>
{slots.content ? slots.content() : renderContent(props.content)}
</div>
)
}
return () => (
<Popup
trigger={props.trigger}
placement={props.placement}
showArrow={props.showArrow}
autoFitPopupMinWidth={props.autoFitPopupMinWidth}
autoFitPopupWidth={props.autoFitPopupWidth}
destroyOnClose={props.destroyOnClose}
content={renderTooltip}
showDelay={props.showDelay}
hideDelay={props.hideDelay}
class={`${prefixCls} ${prefixCls}-${props.type}`}
>
{{
default: () => slots.default?.(),
}}
</Popup>
)
},
})