This commit is contained in:
oneao committed 2025-09-23 17:29:46 +08:00
1 parent 9c76e2769a
commit f285ff776f
16 files changed
+884 -89

No files matched your search

@@ -76,7 +76,7 @@
--ao-bg-color-component: #393939;
--ao-border-color-component: #5e5e5e;
--ao-bg-color-secondarycomponent: #4b4b4b;
--ao-bg-color-secondarycomponent: #2c2c2c;
--ao-bg-color-secondarycomponent-hover: #5e5e5e;
--ao-bg-color-mask: rgba(23, 23, 26, 0.6);
@@ -77,7 +77,7 @@
--ao-bg-color-component: #e8e8e8;
--ao-border-color-component: #ddd;
--ao-bg-color-secondarycomponent: #ddd;
--ao-bg-color-secondarycomponent: #f3f3f3;
--ao-bg-color-secondarycomponent-hover: #c6c6c6;
--ao-bg-color-mask: rgba(29, 33, 41, 0.6);
@@ -0,0 +1,69 @@
import type { VNode, VNodeChild } from 'vue'
import { Fragment } from 'vue'
/**
* 判断 vnode 是否匹配目标组件
* @param vnode VNode
* @param componentType 组件对象
* @param componentName 组件名称字符串(可选)
*/
function isTargetComponent(vnode: VNode, componentType: any, componentName?: string) {
if (!vnode)
return false
if (vnode.type === componentType)
return true
if (componentName && typeof vnode.type === 'object' && (vnode.type as any).name === componentName) {
return true
}
return false
}
/**
* 递归获取指定组件的 props 和插槽函数(不执行函数 prop)
* @param nodes VNode 数组
* @param componentType 目标组件对象
* @param componentName 目标组件名称字符串(可选)
*/
export function getComponentInnerData(
nodes: VNode[],
componentType: any,
componentName?: string,
): { item: Record<string, any>, slots: Record<string, () => VNodeChild | VNodeChild[] | null> }[] {
const result: { item: Record<string, any>, slots: Record<string, () => VNodeChild | VNodeChild[] | null> }[] = []
function traverse(vnode: VNode) {
if (!vnode)
return
if (vnode.type === Fragment && Array.isArray(vnode.children)) {
vnode.children.forEach(child => traverse(child as VNode))
return
}
if (isTargetComponent(vnode, componentType, componentName)) {
const item: Record<string, any> = {}
const slots: Record<string, () => VNodeChild | VNodeChild[] | null> = {}
if (vnode.props)
Object.assign(item, vnode.props)
if (vnode.children && typeof vnode.children === 'object' && !Array.isArray(vnode.children)) {
for (const key in vnode.children) {
const slotFn = vnode.children[key]
if (typeof slotFn === 'function') {
slots[key] = slotFn as () => VNodeChild | VNodeChild[] | null
}
}
}
result.push({ item, slots })
}
if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => traverse(child as VNode))
}
}
nodes.forEach(traverse)
return result
}
@@ -6,10 +6,14 @@ export { default as Checkbox } from './checkbox/checkbox'
export { default as CheckboxGroup } from './checkbox/checkbox-group'
export { default as Collapse } from './collapse/collapse'
export { default as CollapseGroup } from './collapse/collapse-group'
export { default as Descriptions } from './descriptions/descriptions'
export { default as DescriptionsGroup } from './descriptions/descriptions-group'
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 List } from './list/list'
export { default as ListItem } from './list/list-item'
export { default as Modal } from './modal'
export { default as Popconfirm } from './popconfirm'
export { default as Popup } from './popup'
@@ -0,0 +1,107 @@
<script setup lang="ts">
import { ref } from 'vue'
// 示例数据
const demoData = ref([
{ label: '用户名', content: 'zhangsan', span: 1 },
{ label: '邮箱', content: 'zhangsan@example.com', span: 1 },
{ label: '手机号', content: '123456789', span: 1 },
{ label: '地址', content: '北京市海淀区中关村', span: 1 },
])
</script>
<template>
<div class="demo-container">
<!-- 1. Row 布局 -->
<div class="demo-block">
<h3>Row 布局</h3>
<ADescriptionsGroup
:data="demoData"
layout="row"
title="Row Layout"
:columns="3"
/>
</div>
<!-- 2. Row Vertical 布局 -->
<div class="demo-block">
<h3>Row Vertical 布局</h3>
<ADescriptionsGroup
:data="demoData"
layout="row-vertical"
title="Row Vertical Layout"
:columns="3"
/>
</div>
<!-- 3. Column 布局 -->
<div class="demo-block">
<h3>Column 布局</h3>
<ADescriptionsGroup
:data="demoData"
layout="column"
title="Column Layout"
:columns="3"
/>
</div>
<!-- 4. Column Vertical 布局 -->
<div class="demo-block">
<h3>Column Vertical 布局</h3>
<ADescriptionsGroup
:data="demoData"
layout="column-vertical"
title="Column Vertical Layout"
:columns="3"
/>
</div>
<!-- 5. 单独使用 ADescriptions -->
<div class="demo-block">
<h3>ADescriptions 单独使用</h3>
<ADescriptionsGroup title="单独 Descriptions" :columns="3">
<ADescriptions label="用户名">
zhangsan
</ADescriptions>
<ADescriptions label="邮箱">
zhangsan@example.com
</ADescriptions>
<ADescriptions label="手机号">
123456789
</ADescriptions>
<ADescriptions label="地址">
<div style="background-color: red;">
测试
</div>
</ADescriptions>
</ADescriptionsGroup>
</div>
</div>
</template>
<style>
.demo-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
gap: 40px;
}
.demo-block {
width: 80%;
max-width: 900px;
background: #fff;
padding: 20px 30px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.demo-block h3 {
margin-bottom: 16px;
font-weight: 600;
font-size: 18px;
color: #333;
}
</style>
@@ -1,11 +1,13 @@
import type { PropType, VNode } from 'vue'
import { computed, defineComponent } from 'vue'
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import { getComponentInnerData } from '../_utils/vnode'
import Descriptions from './descriptions'
interface DescriptionsData {
label: string | VNode | (() => VNode)
content: string | VNode | (() => VNode)
span: number
label: string | VNode | (() => string | VNode)
content: string | VNode | (() => string | VNode)
span?: number
}
export default defineComponent({
@@ -15,13 +17,17 @@ export default defineComponent({
type: Array as PropType<DescriptionsData[]>,
default: () => [],
},
title: {
header: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium',
},
tableLayout: {
type: String as PropType<'fixed' | 'auto'>,
default: 'fixed',
},
layout: {
type: String as PropType<'row' | 'row-vertical' | 'column' | 'column-vertical'>,
default: 'row',
@@ -30,6 +36,28 @@ export default defineComponent({
type: Number,
default: 3,
},
border: {
type: Boolean,
default: true,
},
colon: {
type: Boolean,
default: false,
},
labelClass: {
type: [String, Array, Object],
},
labelStyle: {
type: Object,
default: () => ({}),
},
contentClass: {
type: [String, Array, Object],
},
contentStyle: {
type: Object,
default: () => ({}),
},
class: {
type: [String, Array, Object],
default: '',
@@ -40,36 +68,224 @@ export default defineComponent({
},
},
emits: [],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('descriptions-group')
const renderHeader = () => {
const title = slots.title
? slots.title()
: typeof props.title === 'function'
? props.title()
: props.title
setup(props, { slots }) {
const prefixCls = getPrefixCls('descriptions')
return <div class={`${prefixCls}-header`}>{title}</div>
const renderLabel = (item: DescriptionsData) => {
if (typeof item.label === 'function')
return item.label()
return item.label
}
const childrenVNodes = computed(() => {
const nodes = slots.default?.() ?? []
return nodes.filter(node =>
node.type && (typeof node.type === 'string' || typeof node.type === 'object' || typeof node.type === 'function'),
)
})
const renderContent = (item: DescriptionsData) => {
if (typeof item.content === 'function')
return item.content()
return item.content
}
const groupedData = computed(() => {
c
})
const getRows = () => {
const dataList: DescriptionsData[] = []
// 1️⃣ 构建基础数据列表
if (props.data && props.data.length) {
props.data.forEach((item) => {
dataList.push({
label: item.label,
content: item.content,
span: item.span ?? 1, // 默认 span = 1
})
})
}
else if (slots.default) {
const childrenData = getComponentInnerData(slots.default(), Descriptions)
childrenData.forEach(({ item, slots }) => {
dataList.push({
label: slots.label ? slots.label() : item.label,
content: slots.content
? slots.content()
: slots.default
? slots.default()
: item.content,
span: item.span ?? 1,
})
})
}
// 2️⃣ 如果是列布局,直接一行返回
if (props.layout === 'column' || props.layout === 'column-vertical') {
return [dataList]
}
// 3️⃣ Row 布局按列分行
const res: DescriptionsData[][] = []
let temp: DescriptionsData[] = []
let currentSpan = 0
const addRow = () => {
if (temp.length) {
// 填充剩余列到最后一个 item
const restSpan = props.columns - currentSpan
if (restSpan > 0) {
const lastItem = temp[temp.length - 1]
lastItem.span = (lastItem.span ?? 1) + restSpan
}
res.push(temp)
}
}
dataList.forEach((item) => {
const span = Math.min(item.span ?? 1, props.columns)
if (currentSpan + span > props.columns) {
addRow()
temp = []
currentSpan = 0
}
temp.push({ ...item, span })
currentSpan += span
})
// 最后一行
addRow()
return res
}
const label = (node: DescriptionsData) => {
const labelClass = [`${prefixCls}-label`]
let colspan = 1
// 水平布局下
if (props.layout === 'row') {
colspan = 1
}
else if (props.layout === 'row-vertical') {
colspan = node.span ?? 1
}
return (
<td
colspan={colspan}
class={[labelClass, props.labelClass]}
style={props.labelStyle}
>
{renderLabel(node)}
{props.colon ? ':' : null}
</td>
)
}
const content = (node: DescriptionsData) => {
const contentClass = [`${prefixCls}-content`]
let colspan = 1
if (props.layout === 'row') {
const span = node.span ?? 1
colspan = span * 2 - 1
}
else if (props.layout === 'row-vertical') {
colspan = node.span ?? 1
}
return (
<td
colspan={colspan}
class={[contentClass, props.contentClass]}
style={props.contentStyle}
>
{renderContent(node)}
</td>
)
}
const row = (data: DescriptionsData[]) => {
return (
<tr>
{
data.map(node => (
<>
{label(node)}
{content(node)}
</>
))
}
</tr>
)
}
const rowVertical = (data: DescriptionsData[]) => {
return (
<>
<tr>{data.map(node => label(node))}</tr>
<tr>{data.map(node => content(node))}</tr>
</>
)
}
const column = (data: DescriptionsData[]) => {
return (
<>
{data.map(node => (
<tr>
{label(node)}
{content(node)}
</tr>
))}
</>
)
}
const columnVertical = (data: DescriptionsData[]) => {
return (
<>
{data.map(node => (
<>
<tr>{label(node)}</tr>
<tr>{content(node)}</tr>
</>
))}
</>
)
}
const renderHeader = () => {
const header = slots.header
? slots.header()
: typeof props.header === 'function'
? props.header()
: props.header
return <div class={`${prefixCls}-header`}>{header}</div>
}
const renderBody = () => {
return (
<table class={[
`${prefixCls}-body`,
`${prefixCls}-body-${props.tableLayout}`,
`${prefixCls}-size-${props.size}`,
{ [`${prefixCls}-body-border`]: props.border },
]}
>
<tbody>
{getRows().map((item) => {
if (props.layout === 'row') {
return row(item)
}
else if (props.layout === 'row-vertical') {
return rowVertical(item)
}
else if (props.layout === 'column') {
return column(item)
}
else if (props.layout === 'column-vertical') {
return columnVertical(item)
}
// 默认 fallback
return row(item)
})}
</tbody>
</table>
)
@@ -77,8 +293,8 @@ export default defineComponent({
return () => (
<div class={[prefixCls, props.class]} style={{ ...props.style }}>
{renderHeader}
{renderBody}
{renderHeader()}
{renderBody()}
</div>
)
},
@@ -2,16 +2,13 @@ import type { PropType, VNode } from 'vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Descriptions',
name: 'AoDescriptions',
props: {
label: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
type: [String, Object, Function] as PropType<string | VNode | (() => string | VNode)>,
},
content: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
},
default: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
type: [String, Object, Function] as PropType<string | VNode | (() => string | VNode)>,
},
span: {
type: Number,
@@ -4,4 +4,88 @@
$descriptions-prefix: '#{$prefix}-descriptions';
.#{$descriptions-prefix} {
font-size: 14px;
color: $text-color-base;
box-sizing: border-box;
margin: 0;
padding: 0;
list-style: none;
.#{$descriptions-prefix}-header {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: $text-color-base;
}
.#{$descriptions-prefix}-body {
border-collapse: collapse;
font-size: 14px;
width: 100%;
&.#{$descriptions-prefix}-body-fixed {
table-layout: fixed;
}
&.#{$descriptions-prefix}-body-auto {
table-layout: auto;
}
}
.#{$descriptions-prefix}-label {
color: $text-color-placeholder;
word-break: break-all;
}
.#{$descriptions-prefix}-content {
word-break: break-all;
}
.#{$descriptions-prefix}-size-small {
.#{$descriptions-prefix}-label,
.#{$descriptions-prefix}-content {
padding: 8px 0;
}
}
.#{$descriptions-prefix}-size-medium {
.#{$descriptions-prefix}-label,
.#{$descriptions-prefix}-content {
padding: 12px 0;
}
}
.#{$descriptions-prefix}-size-large {
.#{$descriptions-prefix}-label,
.#{$descriptions-prefix}-content {
padding: 18px 0;
}
}
.#{$descriptions-prefix}-body-border {
> tbody > tr > td {
border: 1px solid $border-color-component;
}
.#{$descriptions-prefix}-label {
background-color: $bg-color-secondarycomponent;
}
&.#{$descriptions-prefix}-size-small {
.#{$descriptions-prefix}-label,
.#{$descriptions-prefix}-content {
padding: 8px 16px;
}
}
&.#{$descriptions-prefix}-size-medium {
.#{$descriptions-prefix}-label,
.#{$descriptions-prefix}-content {
padding: 12px 16px;
}
}
&.#{$descriptions-prefix}-size-large {
.#{$descriptions-prefix}-label,
.#{$descriptions-prefix}-content {
padding: 16px;
}
}
}
}
@@ -7,6 +7,7 @@
@use './collapse/index.scss' as *;
@use './checkbox/index.scss' as *;
@use './drawer/index.scss' as *;
@use './descriptions/index.scss' as *;
@use './group/index.scss' as *;
@use './icon/index.scss' as *;
@use './input/index.scss' as *;
@@ -23,6 +24,7 @@
@use './tag/index.scss' as *;
@use './textarea/index.scss' as *;
@use './tooltip/index.scss' as *;
@use './list/index.scss' as *;
[class*='#{variables.$prefix}-'] {
-webkit-box-sizing: border-box;
@@ -0,0 +1,65 @@
@use '../_styles/_variables.scss' as *;
@use 'sass:color';
$list-prefix: '#{$prefix}-list';
.#{$list-prefix} {
font-size: 14px;
margin: 0;
padding: 0;
list-style: none;
color: $text-color-base;
background: $bg-color-container;
.#{$list-prefix}-header,
.#{$list-prefix}-footer {
background: $bg-color-container;
padding: 12px 16px;
}
.#{$list-prefix}-body {
overflow: auto;
list-style: none;
padding: 0;
margin: 0;
}
.#{$list-prefix}-item {
font-size: 14px;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: flex-start;
position: relative;
.#{$list-prefix}-item-prefix {
margin-right: 20px;
flex: 0 0 auto;
}
.#{$list-prefix}-item-content {
flex: 1 1 auto;
min-width: 0;
overflow-y: auto;
}
.#{$list-prefix}-item-suffix {
margin-left: 20px;
flex: 0 0 auto;
}
&::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
}
}
&.#{$list-prefix}-separator {
.#{$list-prefix}-item::after {
background: $separator-color;
}
}
}
@@ -0,0 +1,18 @@
import type { PropType, VNode } from 'vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'AoComponent',
props: {
prefix: {
type: [String, Object, Function] as PropType<string | VNode | (() => string | VNode)>,
},
content: {
type: [String, Object, Function] as PropType<string | VNode | (() => string | VNode)>,
},
suffix: {
type: [String, Object, Function] as PropType<string | VNode | (() => string | VNode)>,
},
},
})
@@ -0,0 +1,161 @@
import type { PropType, VNode } from 'vue'
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import { getComponentInnerData } from '../_utils/vnode'
import ListItem from './list-item'
interface ListItemData {
prefix: string | VNode | (() => string | VNode)
content: string | VNode | (() => string | VNode)
suffix: string | VNode | (() => string | VNode)
}
export default defineComponent({
name: 'List',
props: {
data: {
type: Array as PropType<ListItemData[]>,
default: () => [],
},
header: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
},
content: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
},
footer: {
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
},
separator: {
type: Boolean,
default: true,
},
onScroll: {
type: Function as PropType<(options: { e: Event, scrollTop: number, scrollBottom: number }) => void>,
},
class: {
type: [String, Array, Object],
default: '',
},
style: {
type: Object,
default: () => ({}),
},
},
emits: [],
setup(props, { slots }) {
const prefixCls = getPrefixCls('list')
const getRows = () => {
const dataList: ListItemData[] = []
// 1️⃣ 构建基础数据列表
if (props.data && props.data.length) {
props.data.forEach((item) => {
dataList.push({
prefix: item.prefix,
content: item.content,
suffix: item.suffix,
})
})
}
else if (slots.default) {
const childrenData = getComponentInnerData(slots.default(), ListItem)
childrenData.forEach(({ item, slots }) => {
dataList.push({
prefix: slots.prefix ? slots.prefix() : item.prefix,
content: slots.content
? slots.content()
: slots.default
? slots.default()
: item.content,
suffix: slots.suffix ? slots.suffix() : item.suffix,
})
})
}
return dataList
}
const renderItemPrefix = (item: ListItemData) => {
if (typeof item.prefix === 'function')
return item.prefix()
return item.prefix
}
const renderItemContent = (item: ListItemData) => {
if (typeof item.content === 'function')
return item.content()
return item.content
}
const renderItemSuffix = (item: ListItemData) => {
if (typeof item.suffix === 'function')
return item.suffix()
return item.suffix
}
const renderHeader = () => {
const content = slots.header
? slots.header()
: typeof props.header === 'function'
? props.header()
: props.header
return content ? <div class={`${prefixCls}-header`}>{content}</div> : null
}
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement
const scrollTop = target.scrollTop
const scrollHeight = target.scrollHeight
const clientHeight = target.clientHeight
const scrollBottom = scrollHeight - scrollTop - clientHeight
// 触发滚动回调
props.onScroll?.({ e, scrollTop, scrollBottom })
}
const renderBody = () => {
return (
<div
class={`${prefixCls}-body`}
onScroll={handleScroll}
>
<ul>
{getRows().map((item, index) => (
<li class={`${prefixCls}-item`} key={index}>
<div class={`${prefixCls}-item-prefix`}>{renderItemPrefix(item)}</div>
<div class={`${prefixCls}-item-content`}>{renderItemContent(item)}</div>
<div class={`${prefixCls}-item-suffix`}>{renderItemSuffix(item)}</div>
</li>
))}
</ul>
</div>
)
}
const renderFooter = () => {
const content = slots.footer
? slots.footer()
: typeof props.footer === 'function'
? props.footer()
: props.footer
return content ? <div class={`${prefixCls}-footer`}>{content}</div> : null
}
return () => (
<div
class={[
prefixCls,
props.class,
props.separator ? `${prefixCls}-separator` : '',
]}
style={{ ...props.style }}
>
{renderHeader()}
{renderBody()}
{renderFooter()}
</div>
)
},
})
@@ -43,3 +43,15 @@ $spin-prefix: #{$prefix}-spin;
}
}
}
.#{$spin-prefix}-horizontal {
display: flex;
align-items: center;
gap: 8px; // 图标和 tip 之间间距
}
.#{$spin-prefix}-vertical {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px; // 图标和 tip 之间间距
}
@@ -1,24 +1,40 @@
import type { PropType, VNode } from 'vue'
import { defineComponent } from 'vue'
import { defineComponent, onUnmounted, ref } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Icon from '../icon'
export default defineComponent({
name: 'Spin',
props: {
loading: { type: Boolean, default: false },
opacity: { type: Number as PropType<number>, default: 0.6 },
rounded: { type: Boolean, default: true },
icon: { type: String as PropType<string>, default: 'mingcute:loading-line' },
size: { type: Number as PropType<number>, default: 16 },
rotate: { type: Number as PropType<number>, default: 360 },
loading: {
type: Boolean,
default: false,
},
icon: {
type: String as PropType<string>,
default: 'mingcute:loading-fill',
},
size: {
type: Number as PropType<number>,
default: 16,
},
tip: {
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
type: [String, Object, Function] as PropType<string | VNode | (() => VNode)>,
default: '',
},
tipPosition: {
type: String as PropType<'top' | 'right' | 'bottom' | 'left'>,
default: 'bottom',
},
delay: {
type: Number,
default: 0,
},
},
setup(props, { slots }) {
const prefixCls = getPrefixCls('spin')
const showLoading = ref(false)
let timer: number | null = null
const renderTip = () => {
if (slots.tip)
@@ -28,51 +44,72 @@ export default defineComponent({
return props.tip
}
return () => {
const tipContent = renderTip()
if (!props.loading || !slots.default) {
// loading 为 false 时的结构
return (
<div
class={[
prefixCls,
tipContent ? `${prefixCls}-has-tip` : '',
]}
>
<Icon
icon={props.icon}
size={props.size}
class={`${prefixCls}-icon`}
spin
/>
{tipContent && <div class={`${prefixCls}-tip`}>{tipContent}</div>}
</div>
)
// 每次渲染时处理 delay
const updateShowLoading = () => {
if (timer)
clearTimeout(timer)
if (props.loading) {
if (props.delay > 0) {
timer = window.setTimeout(() => {
showLoading.value = true
}, props.delay)
}
else {
showLoading.value = true
}
}
else {
showLoading.value = false
}
}
// loading 为 true 时的结构
return (
<div
class={[
prefixCls,
`${prefixCls}-loading`,
]}
>
{slots.default()}
onUnmounted(() => {
if (timer)
clearTimeout(timer)
})
<div class={`${prefixCls}-mask`}>
<div class={`${prefixCls}-mask-inner`}>
<Icon
icon={props.icon}
size={props.size}
spin
class={`${prefixCls}-icon`}
/>
{tipContent && <div class={`${prefixCls}-tip`}>{tipContent}</div>}
return () => {
// 更新延迟显示状态
updateShowLoading()
const tipContent = renderTip()
const isHorizontal = props.tipPosition === 'left' || props.tipPosition === 'right'
const containerClass = [
prefixCls,
tipContent ? `${prefixCls}-has-tip` : '',
tipContent ? `${prefixCls}-tip-${props.tipPosition}` : '',
]
const tipNode = tipContent ? <div class={`${prefixCls}-tip`}>{tipContent}</div> : null
const iconNode = <Icon icon={props.icon} size={props.size} spin class={`${prefixCls}-icon`} />
const content = isHorizontal
? (
<div class={`${prefixCls}-horizontal`}>
{props.tipPosition === 'left' && tipNode}
{iconNode}
{props.tipPosition === 'right' && tipNode}
</div>
</div>
)
: (
<div class={`${prefixCls}-vertical`}>
{props.tipPosition === 'top' && tipNode}
{iconNode}
{props.tipPosition === 'bottom' && tipNode}
</div>
)
return (
<div class={containerClass} style={{ position: 'relative' }}>
{/* 默认内容立即渲染 */}
{slots.default && slots.default()}
{/* 延迟显示的 loading 遮罩 */}
{showLoading.value && (
<div class={`${prefixCls}-mask`}>
<div class={`${prefixCls}-mask-inner`}>{content}</div>
</div>
)}
</div>
)
}
+26 -8
View File
@@ -1,17 +1,35 @@
<script setup lang="ts">
const items = Array.from({ length: 10 }, (_, i) => ({
prefix: `图标${i + 1}`,
content: `这是内容${i + 1}`,
suffix: `操作${i + 1}`,
}))
</script>
<template>
<div>
content
<t-descriptions-item label="Name">
TDesign
</t-descriptions-item>
<div class="main">
<AList header="列表 Header" footer="列表 Footer" style="width: 1000px;height:400px">
<AListItem v-for="(item, index) in items" :key="index">
<template #prefix>
{{ item.prefix }}
</template>
{{ item.content }}
<template #suffix>
{{ item.suffix }}
</template>
</AListItem>
</AList>
</div>
</template>
<style lang="scss" scoped>
.main {
height: 800px;
display: flex;
align-items: center;
justify-content: center;
background-color: #eeeeee;
flex-direction: column;
gap:16px;
}
</style>
+5
View File
@@ -16,10 +16,15 @@ declare module 'vue' {
ACollapse: typeof import('@ao/ao-design')['Collapse']
ACollapseGroup: typeof import('@ao/ao-design')['CollapseGroup']
AConfigProvider: typeof import('@ao/ao-design')['ConfigProvider']
ADescriptions: typeof import('@ao/ao-design')['Descriptions']
'ADescriptions.Item': typeof import('@ao/ao-design')['Descriptions.Item']
ADescriptionsGroup: typeof import('@ao/ao-design')['DescriptionsGroup']
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']
AList: typeof import('@ao/ao-design')['List']
AListItem: typeof import('@ao/ao-design')['ListItem']
ALoading: typeof import('@ao/ao-design')['Loading']
AMessage: typeof import('@ao/ao-design')['Message']
AModal: typeof import('@ao/ao-design')['Modal']