mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 14:33:11 +08:00
feat(descriptions): 新增 FaDescriptions 组件及示例
This commit is contained in:
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
@@ -93,6 +93,14 @@ const routes: RouteRecordRaw = {
|
||||
title: '右键菜单',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'descriptions',
|
||||
name: 'componentExampleDescriptions',
|
||||
component: () => import('@/views/component_example/descriptions.vue'),
|
||||
meta: {
|
||||
title: '描述列表',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'divider',
|
||||
name: 'componentExampleDivider',
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ declare module 'vue' {
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
@@ -86,6 +87,7 @@ declare global {
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDescriptions: typeof import('@fantastic-admin/components')['FaDescriptions']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { descriptions } from '@fantastic-admin/components/examples'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FaPageHeader title="描述列表" description="FaDescriptions" />
|
||||
<FaPageMain
|
||||
v-for="example in descriptions"
|
||||
:key="example.title"
|
||||
:code="example.componentRaw"
|
||||
:title="example.title"
|
||||
>
|
||||
<component :is="example.component" />
|
||||
</FaPageMain>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,6 +16,7 @@ const BASIC_COMPONENT_NAMES = [
|
||||
'FaCheckboxGroup',
|
||||
'FaCollapsible',
|
||||
'FaContextMenu',
|
||||
'FaDescriptions',
|
||||
'FaDivider',
|
||||
'FaDrawer',
|
||||
'FaDropdown',
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# FaDescriptions 描述列表
|
||||
|
||||
纯展示信息组件,用于详情页、资料卡片、审核信息等只读数据场景。支持横向与纵向布局、边框、跨列、具名插槽和单项扩展字段。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 用户、订单、商品等详情信息展示
|
||||
- 表单提交后的只读确认信息
|
||||
- 弹窗中的轻量信息摘要
|
||||
- 卡片内的结构化字段展示
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `items` | `DescriptionItem[]` | `[]` | 描述项配置 |
|
||||
| `column` | `number` | `3` | 每行展示的描述项数量 |
|
||||
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | 布局方向 |
|
||||
| `border` | `boolean` | `false` | 是否显示边框和单元格分割线 |
|
||||
| `labelWidth` | `string \| number` | - | label 区域宽度,仅横向布局生效 |
|
||||
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | 单元格密度 |
|
||||
| `emptyText` | `string` | `'-'` | value 为空时的占位文本 |
|
||||
| `class` | `HTMLAttributes['class']` | - | 根节点 class |
|
||||
| `labelClass` | `HTMLAttributes['class']` | - | 全局 label 单元格 class |
|
||||
| `valueClass` | `HTMLAttributes['class']` | - | 全局 value 单元格 class |
|
||||
|
||||
## DescriptionItem
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `key` | `string` | 插槽标识。使用具名插槽时需要提供,组件不会自动转换 |
|
||||
| `label` | `string` | label 文本 |
|
||||
| `value` | `string \| number \| boolean \| null \| undefined` | value 文本 |
|
||||
| `span` | `number` | 占用几个描述项列,默认 `1` |
|
||||
| `class` | `HTMLAttributes['class']` | 当前项 label 和 value 的公共 class |
|
||||
| `labelClass` | `HTMLAttributes['class']` | 当前项 label 单元格 class |
|
||||
| `valueClass` | `HTMLAttributes['class']` | 当前项 value 单元格 class |
|
||||
|
||||
`DescriptionItem` 支持扩展字段,扩展字段会原样保留在插槽参数的 `item` 中。
|
||||
|
||||
## Slots
|
||||
|
||||
| 名称 | 说明 | 作用域参数 |
|
||||
|------|------|------------|
|
||||
| `label-{key}` | 自定义指定项 label | `item`、`index`、`label`、`value` |
|
||||
| `value-{key}` | 自定义指定项 value | `item`、`index`、`label`、`value` |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `key` 只用于生成具名插槽,不用于读取数据。
|
||||
2. `key` 不会自动转换,建议使用字母、数字、短横线和下划线。
|
||||
3. `value` 为 `null`、`undefined` 或空字符串时显示 `emptyText`;`0` 和 `false` 会正常显示。
|
||||
4. `span` 表示占用几个描述项列,超出 `column` 时会按 `column` 处理。
|
||||
5. 最后一行不足 `column` 时会自动补齐空单元格,保证边框网格完整。
|
||||
6. 组件不内置标题、空状态和点击事件,可与 `FaCard`、`FaEmpty` 等组件组合使用。
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
// 组件实际使用时无需手动导入,框架会自动导入
|
||||
import FaDescriptions from '../index.vue'
|
||||
|
||||
const items = [
|
||||
{ label: 'Username', value: 'kooriokami' },
|
||||
{ label: 'Telephone', value: '18100000000' },
|
||||
{ label: 'Place', value: 'Suzhou' },
|
||||
{ label: 'Remarks', value: 'School' },
|
||||
{ label: 'Address', value: 'No.1188, Wuzhong Avenue', span: 2 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDescriptions :items="items" label-width="120px" />
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
// 组件实际使用时无需手动导入,框架会自动导入
|
||||
import FaDescriptions from '../index.vue'
|
||||
|
||||
const items = [
|
||||
{ label: 'Username', value: 'kooriokami' },
|
||||
{ label: 'Telephone', value: '18100000000' },
|
||||
{ label: 'Place', value: 'Suzhou' },
|
||||
{ label: 'Remarks', value: 'School' },
|
||||
{ label: 'Address', value: 'No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu Province', span: 2 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDescriptions :items="items" border label-width="120px" />
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { DescriptionItem } from '../index.vue'
|
||||
// 组件实际使用时无需手动导入,框架会自动导入
|
||||
import FaIcon from '../../icon/index.vue'
|
||||
import FaTag from '../../tag/index.vue'
|
||||
import FaDescriptions from '../index.vue'
|
||||
|
||||
type ProfileItem = DescriptionItem<{
|
||||
icon: string
|
||||
tagVariant?: 'default' | 'secondary' | 'destructive' | 'outline'
|
||||
}>
|
||||
|
||||
const items: ProfileItem[] = [
|
||||
{ key: 'username', label: 'Username', value: 'kooriokami', icon: 'i-lucide:user' },
|
||||
{ key: 'telephone', label: 'Telephone', value: '18100000000', icon: 'i-lucide:smartphone' },
|
||||
{ key: 'place', label: 'Place', value: 'Suzhou', icon: 'i-lucide:map-pin' },
|
||||
{ key: 'remarks', label: 'Remarks', value: 'School', icon: 'i-lucide:notebook-text', tagVariant: 'outline' },
|
||||
{ key: 'address', label: 'Address', value: 'No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu Province', icon: 'i-lucide:building-2', span: 2 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDescriptions :items="items" border label-width="200px">
|
||||
<template #label-username="{ item, label }">
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<FaIcon :name="item.icon" class="size-4" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label-telephone="{ item, label }">
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<FaIcon :name="item.icon" class="size-4" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label-place="{ item, label }">
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<FaIcon :name="item.icon" class="size-4" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label-remarks="{ item, label }">
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<FaIcon :name="item.icon" class="size-4" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label-address="{ item, label }">
|
||||
<div class="inline-flex gap-2 items-center">
|
||||
<FaIcon :name="item.icon" class="size-4" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #value-remarks="{ item, value }">
|
||||
<FaTag :variant="item.tagVariant">
|
||||
{{ value }}
|
||||
</FaTag>
|
||||
</template>
|
||||
</FaDescriptions>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
// 组件实际使用时无需手动导入,框架会自动导入
|
||||
import FaDescriptions from '../index.vue'
|
||||
|
||||
const items = [
|
||||
{ label: '早餐', value: '苹果' },
|
||||
{ label: '早午餐', value: '苹果' },
|
||||
{ label: '午餐', value: '苹果' },
|
||||
{ label: '晚餐', value: '苹果' },
|
||||
{ label: '正餐', value: '苹果' },
|
||||
{ label: '夜宵', value: '苹果' },
|
||||
{ label: '早餐', value: '苹果' },
|
||||
{ label: '早午餐', value: '苹果', span: 2 },
|
||||
{ label: '晚餐', value: '苹果', span: 3 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaDescriptions :items="items" :column="6" direction="vertical" border />
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
import Basic from './_basic.vue'
|
||||
import BasicRaw from './_basic.vue?raw'
|
||||
import Border from './_border.vue'
|
||||
import BorderRaw from './_border.vue?raw'
|
||||
import Slot from './_slot.vue'
|
||||
import SlotRaw from './_slot.vue?raw'
|
||||
import Vertical from './_vertical.vue'
|
||||
import VerticalRaw from './_vertical.vue?raw'
|
||||
|
||||
export default [
|
||||
{
|
||||
title: '基础',
|
||||
component: Basic,
|
||||
componentRaw: BasicRaw,
|
||||
},
|
||||
{
|
||||
title: '边框',
|
||||
component: Border,
|
||||
componentRaw: BorderRaw,
|
||||
},
|
||||
{
|
||||
title: '纵向布局',
|
||||
component: Vertical,
|
||||
componentRaw: VerticalRaw,
|
||||
},
|
||||
{
|
||||
title: '自定义插槽',
|
||||
component: Slot,
|
||||
componentRaw: SlotRaw,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,381 @@
|
||||
<script lang="ts">
|
||||
import type { CSSProperties, HTMLAttributes } from 'vue'
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="TItem extends DescriptionItem = DescriptionItem">
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '#utils'
|
||||
|
||||
export type DescriptionValue = string | number | boolean | null | undefined
|
||||
export type DescriptionDirection = 'horizontal' | 'vertical'
|
||||
export type DescriptionSize = 'sm' | 'default' | 'lg'
|
||||
|
||||
export interface DescriptionItemBase {
|
||||
key?: string
|
||||
label?: string
|
||||
value?: DescriptionValue
|
||||
span?: number
|
||||
class?: HTMLAttributes['class']
|
||||
labelClass?: HTMLAttributes['class']
|
||||
valueClass?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export type DescriptionItem<
|
||||
TExtra extends Record<string, any> = Record<string, any>,
|
||||
> = DescriptionItemBase & Omit<TExtra, keyof DescriptionItemBase>
|
||||
|
||||
export interface DescriptionsProps<
|
||||
TItem extends DescriptionItem = DescriptionItem,
|
||||
> {
|
||||
items?: TItem[]
|
||||
column?: number
|
||||
direction?: DescriptionDirection
|
||||
border?: boolean
|
||||
labelWidth?: string | number
|
||||
size?: DescriptionSize
|
||||
emptyText?: string
|
||||
class?: HTMLAttributes['class']
|
||||
labelClass?: HTMLAttributes['class']
|
||||
valueClass?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export interface DescriptionSlotProps<
|
||||
TItem extends DescriptionItem = DescriptionItem,
|
||||
> {
|
||||
item: TItem
|
||||
index: number
|
||||
label: TItem['label']
|
||||
value: TItem['value']
|
||||
}
|
||||
|
||||
type LabelSlotName = `label-${string}`
|
||||
type ValueSlotName = `value-${string}`
|
||||
type DescriptionsSlots<TItem extends DescriptionItem = DescriptionItem> = {
|
||||
[key: LabelSlotName]: ((props: DescriptionSlotProps<TItem>) => any) | undefined
|
||||
} & {
|
||||
[key: ValueSlotName]: ((props: DescriptionSlotProps<TItem>) => any) | undefined
|
||||
}
|
||||
|
||||
interface ResolvedDescriptionItem<TItem extends DescriptionItem = DescriptionItem> {
|
||||
item: TItem
|
||||
index: number
|
||||
span: number
|
||||
}
|
||||
|
||||
interface DescriptionRow<TItem extends DescriptionItem = DescriptionItem> {
|
||||
items: ResolvedDescriptionItem<TItem>[]
|
||||
span: number
|
||||
rest: number
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'BuiltInDescriptions',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<DescriptionsProps<TItem>>(), {
|
||||
column: 3,
|
||||
direction: 'horizontal',
|
||||
border: false,
|
||||
size: 'default',
|
||||
emptyText: '-',
|
||||
})
|
||||
|
||||
defineSlots<DescriptionsSlots<TItem>>()
|
||||
|
||||
const cellSizeClasses: Record<DescriptionSize, string> = {
|
||||
sm: 'px-3 py-2 text-xs',
|
||||
default: 'px-4 py-3 text-sm',
|
||||
lg: 'px-5 py-4 text-base',
|
||||
}
|
||||
|
||||
const normalizedColumn = computed(() => normalizeColumn(props.column))
|
||||
|
||||
const rows = computed(() => createRows(props.items ?? [], normalizedColumn.value))
|
||||
|
||||
const rootClass = computed(() => cn(
|
||||
'w-full overflow-hidden',
|
||||
props.border && 'rounded-lg border',
|
||||
props.class,
|
||||
))
|
||||
|
||||
const tableClass = computed(() => cn(
|
||||
'w-full table-fixed border-collapse',
|
||||
!props.border && 'border-separate border-spacing-0',
|
||||
))
|
||||
|
||||
const labelColStyle = computed<CSSProperties | undefined>(() => {
|
||||
if (props.direction !== 'horizontal') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const width = resolveSize(props.labelWidth)
|
||||
return width ? { width } : undefined
|
||||
})
|
||||
|
||||
function normalizeColumn(column?: number) {
|
||||
const resolved = Number(column)
|
||||
return Number.isFinite(resolved) && resolved > 0 ? Math.floor(resolved) : 1
|
||||
}
|
||||
|
||||
function normalizeSpan(span: number | undefined, column: number) {
|
||||
const resolved = Number(span)
|
||||
|
||||
if (!Number.isFinite(resolved) || resolved <= 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return Math.min(Math.floor(resolved), column)
|
||||
}
|
||||
|
||||
function createRows(items: TItem[], column: number): DescriptionRow<TItem>[] {
|
||||
const rows: DescriptionRow<TItem>[] = []
|
||||
let currentItems: ResolvedDescriptionItem<TItem>[] = []
|
||||
let currentSpan = 0
|
||||
|
||||
function pushCurrentRow() {
|
||||
if (!currentItems.length) {
|
||||
return
|
||||
}
|
||||
|
||||
rows.push({
|
||||
items: currentItems,
|
||||
span: currentSpan,
|
||||
rest: Math.max(column - currentSpan, 0),
|
||||
})
|
||||
currentItems = []
|
||||
currentSpan = 0
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const span = normalizeSpan(item.span, column)
|
||||
|
||||
if (currentItems.length && currentSpan + span > column) {
|
||||
pushCurrentRow()
|
||||
}
|
||||
|
||||
currentItems.push({
|
||||
item,
|
||||
index,
|
||||
span,
|
||||
})
|
||||
currentSpan += span
|
||||
|
||||
if (currentSpan >= column) {
|
||||
pushCurrentRow()
|
||||
}
|
||||
})
|
||||
|
||||
pushCurrentRow()
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
function resolveSize(size?: string | number) {
|
||||
if (size == null || size === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return typeof size === 'number' ? `${size}px` : size
|
||||
}
|
||||
|
||||
function isEmptyValue(value: DescriptionValue) {
|
||||
return value === null || value === undefined || value === ''
|
||||
}
|
||||
|
||||
function resolveValue(value: DescriptionValue) {
|
||||
return isEmptyValue(value) ? props.emptyText : value
|
||||
}
|
||||
|
||||
function getLabelSlotName(item: TItem): LabelSlotName | undefined {
|
||||
return item.key ? `label-${item.key}` : undefined
|
||||
}
|
||||
|
||||
function getValueSlotName(item: TItem): ValueSlotName | undefined {
|
||||
return item.key ? `value-${item.key}` : undefined
|
||||
}
|
||||
|
||||
function getRowClass(isLastRow: boolean) {
|
||||
return cn(props.border && !isLastRow && 'border-b')
|
||||
}
|
||||
|
||||
function getBaseCellClass(isLastCell: boolean) {
|
||||
return cn(
|
||||
'align-middle break-words',
|
||||
cellSizeClasses[props.size],
|
||||
props.border && !isLastCell && 'border-r',
|
||||
)
|
||||
}
|
||||
|
||||
function getLabelCellClass(entry: ResolvedDescriptionItem<TItem>, isLastCell: boolean) {
|
||||
return cn(
|
||||
getBaseCellClass(isLastCell),
|
||||
'text-left font-medium text-muted-foreground',
|
||||
props.border && 'bg-muted/50',
|
||||
entry.item.class,
|
||||
props.labelClass,
|
||||
entry.item.labelClass,
|
||||
)
|
||||
}
|
||||
|
||||
function getValueCellClass(entry: ResolvedDescriptionItem<TItem>, isLastCell: boolean) {
|
||||
return cn(
|
||||
getBaseCellClass(isLastCell),
|
||||
'text-foreground',
|
||||
entry.item.class,
|
||||
props.valueClass,
|
||||
entry.item.valueClass,
|
||||
)
|
||||
}
|
||||
|
||||
function getPlaceholderCellClass(isLastCell: boolean) {
|
||||
return cn(
|
||||
getBaseCellClass(isLastCell),
|
||||
props.border && 'bg-background',
|
||||
)
|
||||
}
|
||||
|
||||
function isLastContentCell(row: DescriptionRow<TItem>, index: number) {
|
||||
return index === row.items.length - 1 && row.rest === 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="rows.length" :class="rootClass" data-slot="descriptions">
|
||||
<table :class="tableClass" data-slot="descriptions-table">
|
||||
<colgroup v-if="direction === 'horizontal'">
|
||||
<template v-for="columnIndex in normalizedColumn" :key="columnIndex">
|
||||
<col :style="labelColStyle">
|
||||
<col>
|
||||
</template>
|
||||
</colgroup>
|
||||
<colgroup v-else>
|
||||
<col v-for="columnIndex in normalizedColumn" :key="columnIndex">
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
<template v-if="direction === 'vertical'">
|
||||
<template v-for="(row, rowIndex) in rows" :key="rowIndex">
|
||||
<tr :class="getRowClass(false)" data-slot="descriptions-label-row">
|
||||
<th
|
||||
v-for="(entry, entryIndex) in row.items"
|
||||
:key="`label-${entry.index}`"
|
||||
:colspan="entry.span"
|
||||
:class="getLabelCellClass(entry, isLastContentCell(row, entryIndex))"
|
||||
scope="col"
|
||||
data-slot="descriptions-label"
|
||||
>
|
||||
<slot
|
||||
v-if="getLabelSlotName(entry.item)"
|
||||
:name="getLabelSlotName(entry.item)"
|
||||
:item="entry.item"
|
||||
:index="entry.index"
|
||||
:label="entry.item.label"
|
||||
:value="entry.item.value"
|
||||
>
|
||||
{{ entry.item.label }}
|
||||
</slot>
|
||||
<template v-else>
|
||||
{{ entry.item.label }}
|
||||
</template>
|
||||
</th>
|
||||
<td
|
||||
v-if="row.rest"
|
||||
:colspan="row.rest"
|
||||
:class="getPlaceholderCellClass(true)"
|
||||
aria-hidden="true"
|
||||
data-slot="descriptions-placeholder"
|
||||
/>
|
||||
</tr>
|
||||
<tr :class="getRowClass(rowIndex === rows.length - 1)" data-slot="descriptions-value-row">
|
||||
<td
|
||||
v-for="(entry, entryIndex) in row.items"
|
||||
:key="`value-${entry.index}`"
|
||||
:colspan="entry.span"
|
||||
:class="getValueCellClass(entry, isLastContentCell(row, entryIndex))"
|
||||
data-slot="descriptions-value"
|
||||
>
|
||||
<slot
|
||||
v-if="getValueSlotName(entry.item)"
|
||||
:name="getValueSlotName(entry.item)"
|
||||
:item="entry.item"
|
||||
:index="entry.index"
|
||||
:label="entry.item.label"
|
||||
:value="entry.item.value"
|
||||
>
|
||||
{{ resolveValue(entry.item.value) }}
|
||||
</slot>
|
||||
<template v-else>
|
||||
{{ resolveValue(entry.item.value) }}
|
||||
</template>
|
||||
</td>
|
||||
<td
|
||||
v-if="row.rest"
|
||||
:colspan="row.rest"
|
||||
:class="getPlaceholderCellClass(true)"
|
||||
aria-hidden="true"
|
||||
data-slot="descriptions-placeholder"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="(row, rowIndex) in rows"
|
||||
:key="rowIndex"
|
||||
:class="getRowClass(rowIndex === rows.length - 1)"
|
||||
data-slot="descriptions-row"
|
||||
>
|
||||
<template v-for="(entry, entryIndex) in row.items" :key="entry.index">
|
||||
<th
|
||||
:class="getLabelCellClass(entry, false)"
|
||||
scope="row"
|
||||
data-slot="descriptions-label"
|
||||
>
|
||||
<slot
|
||||
v-if="getLabelSlotName(entry.item)"
|
||||
:name="getLabelSlotName(entry.item)"
|
||||
:item="entry.item"
|
||||
:index="entry.index"
|
||||
:label="entry.item.label"
|
||||
:value="entry.item.value"
|
||||
>
|
||||
{{ entry.item.label }}
|
||||
</slot>
|
||||
<template v-else>
|
||||
{{ entry.item.label }}
|
||||
</template>
|
||||
</th>
|
||||
<td
|
||||
:colspan="entry.span * 2 - 1"
|
||||
:class="getValueCellClass(entry, isLastContentCell(row, entryIndex))"
|
||||
data-slot="descriptions-value"
|
||||
>
|
||||
<slot
|
||||
v-if="getValueSlotName(entry.item)"
|
||||
:name="getValueSlotName(entry.item)"
|
||||
:item="entry.item"
|
||||
:index="entry.index"
|
||||
:label="entry.item.label"
|
||||
:value="entry.item.value"
|
||||
>
|
||||
{{ resolveValue(entry.item.value) }}
|
||||
</slot>
|
||||
<template v-else>
|
||||
{{ resolveValue(entry.item.value) }}
|
||||
</template>
|
||||
</td>
|
||||
</template>
|
||||
<td
|
||||
v-if="row.rest"
|
||||
:colspan="row.rest * 2"
|
||||
:class="getPlaceholderCellClass(true)"
|
||||
aria-hidden="true"
|
||||
data-slot="descriptions-placeholder"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ export { default as checkboxGroup } from './basic/checkbox-group/_examples'
|
||||
export { default as checkbox } from './basic/checkbox/_examples'
|
||||
export { default as collapsible } from './basic/collapsible/_examples'
|
||||
export { default as contextMenu } from './basic/context-menu/_examples'
|
||||
export { default as descriptions } from './basic/descriptions/_examples'
|
||||
export { default as divider } from './basic/divider/_examples'
|
||||
export { default as drawer } from './basic/drawer/_examples'
|
||||
export { default as dropdown } from './basic/dropdown/_examples'
|
||||
|
||||
@@ -9,6 +9,8 @@ export { default as FaCheckboxGroup } from './basic/checkbox-group/index.vue'
|
||||
export { default as FaCheckbox } from './basic/checkbox/index.vue'
|
||||
export { default as FaCollapsible } from './basic/collapsible/index.vue'
|
||||
export { default as FaContextMenu } from './basic/context-menu/index.vue'
|
||||
export { default as FaDescriptions } from './basic/descriptions/index.vue'
|
||||
export type { DescriptionDirection, DescriptionItem, DescriptionItemBase, DescriptionSize, DescriptionSlotProps, DescriptionsProps, DescriptionValue } from './basic/descriptions/index.vue'
|
||||
export { default as FaDivider } from './basic/divider/index.vue'
|
||||
export type { DrawerEmits, DrawerProps } from './basic/drawer/index'
|
||||
export { useDrawer as useFaDrawer } from './basic/drawer/index'
|
||||
|
||||
Reference in New Issue
Block a user