u
This commit is contained in:
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>
|
||||
+241
-25
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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']
|
||||
|
||||
Reference in new issue
Block a user