Vue3描述列表(Descriptions)

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介: 该组件库包含 `Descriptions` 和 `DescriptionsItem` 两种组件,需配合使用。`Descriptions` 用于构建描述列表,提供标题、操作区、边框、垂直布局等配置;`DescriptionsItem` 作为列表项,可自定义标签和内容样式。支持响应式布局,并集成了多个实用工具函数,如节流、事件监听等。可通过属性灵活调整列表样式与布局。

包含两种组件:DescriptionsDescriptionsItem(必须搭配使用!)

效果如下图:在线预览

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

APIs

Descriptions

参数 说明 类型 默认值
title 描述列表的标题,显示在最顶部 string | slot undefined
extra 描述列表的操作区域,显示在右上方 string | slot undefined
bordered 是否展示边框 boolean false
vertical 是否使用垂直描述列表 boolean false
size 设置列表的大小 ‘default’ | ‘middle’ | ‘small’ ‘default’
column 一行的 DescriptionItems 数量,可以写成数值或支持响应式的对象写法 { xs: 8, sm: 16, md: 24 } number | Responsive {xs: 1, sm: 2, md: 3}
labelStyle 自定义标签样式,优先级低于 DescriptionItemslabelStyle CSSProperties {}
contentStyle 自定义内容样式,优先级低于 DescriptionItemscontentStyle CSSProperties {}

Responsive Type

名称 说明 类型 默认值
xs <576px 响应式栅格 number undefined
sm ≥576px 响应式栅格 number undefined
md ≥768px 响应式栅格 number undefined
lg ≥992px 响应式栅格 number undefined
xl ≥1200px 响应式栅格 number undefined
xxl ≥1600px 响应式栅格 number undefined

DescriptionsItem

参数 说明 类型 默认值
label 内容的描述标签 string | slot undefined
span 包含列的数量,当使用水平列表且未设置 span 时等效于 span: 1,但最后一行的最后一项,会包含该行剩余的所有列数 number undefined
labelStyle 自定义标签样式,优先级高于 DescriptionlabelStyle CSSProperties {}
contentStyle 自定义内容样式,优先级高于 DescriptioncontentStyle CSSProperties {}

其中引入使用了以下工具函数:

创建 Descriptions.vue 组件

<script setup lang="ts">
import { computed, nextTick, ref, watch, onMounted } from 'vue'
import type { CSSProperties } from 'vue'
import { throttle, useEventListener, useMutationObserver, useSlotsExist } from '../utils'
interface Responsive {
  xs?: number // <576px 响应式栅格
  sm?: number // ≥576px 响应式栅格
  md?: number // ≥768px 响应式栅格
  lg?: number // ≥992px 响应式栅格
  xl?: number // ≥1200px 响应式栅格
  xxl?: number // ≥1600px 响应式栅格
}
interface Props {
  title?: string // 描述列表的标题,显示在最顶部 string | slot
  extra?: string // 描述列表的操作区域,显示在右上方 string | slot
  bordered?: boolean // 是否展示边框
  vertical?: boolean // 是否使用垂直描述列表
  size?: 'default' | 'middle' | 'small' // 设置列表的大小
  column?: number | Responsive // 一行的 DescriptionItems 数量,可以写成数值或支持响应式的对象写法 { xs: 8, sm: 16, md: 24 }
  labelStyle?: CSSProperties // 自定义标签样式,优先级低于 DescriptionItems 的 labelStyle
  contentStyle?: CSSProperties // 自定义内容样式,优先级低于 DescriptionItems 的 contentStyle
}
const props = withDefaults(defineProps<Props>(), {
  title: undefined,
  extra: undefined,
  bordered: false,
  vertical: false,
  size: 'default',
  column: () => ({ xs: 1, sm: 2, md: 3 }),
  labelStyle: () => ({}),
  contentStyle: () => ({})
})
const defaultSlotsRef = ref() // 所有渲染的 DescriptionsItems 节点引用
const defaultSlots = ref(true) // 用于刷新 <slot></slot>
const observer = ref() // defaultSlotsRef 监听器
const stopObservation = ref(true) // 停止观察器
const children = ref<any[]>() // DescriptionsItems 节点
const tdCols = ref() // 放置 DescriptionsItems 节点的模板引用数组
const thVerticalCols = ref() // 放置垂直列表的 DescriptionsItems 节点的 th 模板引用数组
const tdVerticalCols = ref() // 放置垂直列表的 DescriptionsItems 节点的 td 模板引用数组
const trBorderedRows = ref() // 放置 DescriptionsItems 节点的模板引用数组(带边框)
const thVerticalBorderedRows = ref() // 放置垂直列表的 DescriptionsItems 节点的 th 模板引用数组(带边框)
const tdVerticalBorderedRows = ref() // 放置垂直列表的 DescriptionsItems 节点的 td 模板引用数组(带边框)
const groupItems = ref<any[]>([]) // 处理后的 DescriptionsItems 节点数组
const viewportWidth = ref(window.innerWidth)
function getViewportWidth() {
  viewportWidth.value = window.innerWidth
}
const throttleEvent = throttle(getViewportWidth, 100)
useEventListener(window, 'resize', throttleEvent)
const slotsExist = useSlotsExist(['title', 'extra'])
const showHeader = computed(() => {
  return slotsExist.title || slotsExist.extra || props.title || props.extra
})
const responsiveColumn = computed(() => {
  if (typeof props.column === 'object') {
    if (viewportWidth.value >= 1600 && props.column.xxl) {
      return props.column.xxl
    }
    if (viewportWidth.value >= 1200 && props.column.xl) {
      return props.column.xl
    }
    if (viewportWidth.value >= 992 && props.column.lg) {
      return props.column.lg
    }
    if (viewportWidth.value >= 768 && props.column.md) {
      return props.column.md
    }
    if (viewportWidth.value >= 576 && props.column.sm) {
      return props.column.sm
    }
    if (viewportWidth.value < 576 && props.column.xs) {
      return props.column.xs
    }
    return 1
  }
  return props.column
})
watch(
  () => [props.bordered, props.vertical, responsiveColumn.value, props.labelStyle, props.contentStyle],
  () => {
    if (!stopObservation.value) {
      stopObservation.value = true
    }
    refreshDefaultSlots()
  },
  {
    deep: true
  }
)
// 监听 defaultSlotsRef DOM 节点数量变化,重新渲染 Descriptions
observer.value = useMutationObserver(
  defaultSlotsRef,
  (MutationRecord: MutationRecord[]) => {
    if (!stopObservation.value) {
      stopObservation.value = true
      const mutation = MutationRecord.some((mutation: any) => mutation.type === 'childList')
      if (mutation) {
        refreshDefaultSlots()
      }
    }
  },
  { childList: true, attributes: true, subtree: true }
)
onMounted(() => {
  getGroupItems()
})
async function refreshDefaultSlots() {
  defaultSlots.value = !defaultSlots.value
  await nextTick()
  getGroupItems()
}
// 计算当前 group 中所有 span 之和
function getTotalSpan(group: any): number {
  return group.reduce((accumulator: number, currentValue: any) => accumulator + currentValue.span, 0)
}
// 根据不同 cloumn 处理 DescriptionsItems 节点
async function getGroupItems() {
  children.value = Array.from(defaultSlotsRef.value.children).filter((element: any) => {
    return element.className === (props.bordered ? 'descriptions-item-bordered' : 'descriptions-item')
  })
  if (groupItems.value.length) {
    groupItems.value.splice(0) // 清空列表
    await nextTick()
  }
  if (children.value && children.value.length) {
    const len = children.value.length
    let group: any[] = []
    for (let n = 0; n < len; n++) {
      const item = {
        span: Math.min(children.value[n].dataset.span ?? 1, responsiveColumn.value),
        element: children.value[n]
      }
      if (getTotalSpan(group) < responsiveColumn.value) {
        // 已有 items 的 totalSpan < column
        item.span = Math.min(item.span, responsiveColumn.value - getTotalSpan(group))
        group.push(item)
      } else {
        groupItems.value.push(group)
        group = [item]
      }
    }
    // 当使用水平列表且未设置 span 时等效于 span: 1,但最后一行的最后一项,会包含该行剩余的所有列数
    if (!props.vertical && !children.value[len - 1].dataset.span && getTotalSpan(group) < responsiveColumn.value) {
      const groupLen = group.length
      group[groupLen - 1].span = group[groupLen - 1].span + responsiveColumn.value - getTotalSpan(group)
    }
    groupItems.value.push(group)
    await nextTick()
    updateDescriptions()
  } else {
    stopObservation.value = false
  }
}
async function updateDescriptions() {
  if (props.bordered) {
    // 带边框列表
    groupItems.value.forEach((items: any, index: number) => {
      // 每一行 tr
      items.forEach((item: any) => {
        const itemChildren: any[] = Array.from(item.element.children)
        // 创建节点副本,否则原节点将先被移除,后插入到新位置,影响后续响应式布局计算
        const th = itemChildren[0]
        // 动态添加节点样式
        setStyle(th, props.labelStyle)
        const td = itemChildren[1]
        // 动态添加节点样式
        setStyle(td, props.contentStyle)
        // 插入节点到指定位置
        if (props.vertical) {
          // 垂直列表
          th.colSpan = item.span
          td.colSpan = item.span
          thVerticalBorderedRows.value[index].appendChild(th)
          tdVerticalBorderedRows.value[index].appendChild(td)
        } else {
          th.colSpan = 1
          td.colSpan = item.span * 2 - 1
          trBorderedRows.value[index].appendChild(th)
          trBorderedRows.value[index].appendChild(td)
        }
      })
    })
  } else {
    ;(children.value as any[]).forEach((element: any, index: number) => {
      const elementChildren: any[] = Array.from(element.children)
      const label = elementChildren[0]
      // 动态添加节点样式
      setStyle(label, props.labelStyle)
      const content = elementChildren[1]
      // 动态添加节点样式
      setStyle(content, props.contentStyle)
      // 插入节点到指定位置
      if (props.vertical) {
        // 垂直列表
        thVerticalCols.value[index].appendChild(element.firstChild)
        tdVerticalCols.value[index].appendChild(element.lastChild)
      } else {
        tdCols.value[index].appendChild(element)
      }
    })
  }
  await nextTick()
  stopObservation.value = false
}
// 为元素添加内联样式
function setStyle(element: any, styles: any) {
  if (JSON.stringify(styles) !== '{}') {
    Object.keys(styles).forEach((key: string) => {
      if (!element.style[key]) {
        element.style[key] = styles[key]
      }
    })
  }
}
</script>
<template>
  <div class="m-descriptions" :class="`descriptions-${size}`">
    <div class="m-descriptions-header" v-if="showHeader">
      <div class="descriptions-title">
        <slot name="title">{
  { title }}</slot>
      </div>
      <div class="descriptions-extra">
        <slot name="extra">{
  { extra }}</slot>
      </div>
    </div>
    <div v-if="!vertical" class="m-descriptions-view" :class="{ 'descriptions-bordered': bordered }">
      <table>
        <tbody v-if="!bordered">
          <tr v-for="(items, row) in groupItems" :key="row">
            <td
              ref="tdCols"
              class="descriptions-item-td"
              :colspan="item.span"
              v-for="(item, col) in items"
              :key="col"
            ></td>
          </tr>
        </tbody>
        <tbody v-else>
          <tr ref="trBorderedRows" class="descriptions-bordered-tr" v-for="row of groupItems.length" :key="row"></tr>
        </tbody>
      </table>
    </div>
    <div v-else class="m-descriptions-view" :class="{ 'descriptions-bordered': bordered }">
      <table>
        <tbody v-if="!bordered">
          <template v-for="(items, row) in groupItems" :key="row">
            <tr>
              <th class="descriptions-item-th" :colspan="item.span" v-for="(item, col) in items" :key="col">
                <div ref="thVerticalCols" class="descriptions-item"></div>
              </th>
            </tr>
            <tr>
              <td class="descriptions-item-td" :colspan="item.span" v-for="(item, col) in items" :key="col">
                <div ref="tdVerticalCols" class="descriptions-item"></div>
              </td>
            </tr>
          </template>
        </tbody>
        <tbody v-else>
          <template v-for="row in groupItems.length" :key="row">
            <tr ref="thVerticalBorderedRows" class="descriptions-bordered-tr"></tr>
            <tr ref="tdVerticalBorderedRows" class="descriptions-bordered-tr"></tr>
          </template>
        </tbody>
      </table>
    </div>
    <div ref="defaultSlotsRef" v-show="false">
      <slot v-if="defaultSlots"></slot>
      <slot v-else></slot>
    </div>
  </div>
</template>
<style lang="less" scoped>
.m-descriptions {
  font-size: 14px;
  color: rgba(0, 0, 0, 0.88);
  line-height: 1.5714285714285714;
  .m-descriptions-header {
    display: flex;
    align-items: center;
    margin-bottom: 20px;
    .descriptions-title {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      flex: auto;
      font-weight: 600;
      font-size: 16px;
      color: rgba(0, 0, 0, 0.88);
      line-height: 1.5714285714285714;
    }
    .descriptions-extra {
      margin-inline-start: auto;
      color: rgba(0, 0, 0, 0.88);
      font-size: 14px;
    }
  }
  .m-descriptions-view {
    width: 100%;
    border-radius: 8px;
    table {
      width: 100%;
      table-layout: fixed;
      display: table; // 可选,只为兼容 vitepress 中 .vp-doc 的样式入侵,下同
      border-collapse: separate; // 可选
      margin: 0; // 可选
      tr {
        // 可选
        border: none;
        background: transparent;
      }
      .descriptions-item-th {
        padding: 0; // 可选
        border: none; // 可选
        padding-bottom: 16px;
        vertical-align: top;
        background: transparent; // 可选
      }
      .descriptions-item-td {
        padding: 0; // 可选
        border: none; // 可选
        padding-bottom: 16px;
        vertical-align: top;
      }
      .descriptions-item {
        display: flex;
      }
    }
  }
  .descriptions-bordered {
    border: 1px solid rgba(5, 5, 5, 0.06);
    table {
      table-layout: auto;
      border-collapse: collapse;
      display: table; // 可选
      margin: 0; // 可选
      .descriptions-bordered-tr {
        border-bottom: 1px solid rgba(5, 5, 5, 0.06);
        &:last-child {
          border-bottom: none;
        }
        :deep(.descriptions-label-th) {
          border: none; // 可选
          color: rgba(0, 0, 0, 0.88);
          font-weight: normal;
          font-size: 14px;
          line-height: 1.5714285714285714;
          text-align: start;
          background-color: rgba(0, 0, 0, 0.02);
          padding: 16px 24px;
          border-inline-end: 1px solid rgba(5, 5, 5, 0.06);
          &:last-child {
            // 消除 vertical 列表最后一个 th 的边框
            border-inline-end: none;
          }
        }
        :deep(.descriptions-content-td) {
          border: none; // 可选
          display: table-cell;
          flex: 1;
          padding: 16px 24px;
          border-inline-end: 1px solid rgba(5, 5, 5, 0.06);
          color: rgba(0, 0, 0, 0.88);
          font-size: 14px;
          line-height: 1.5714285714285714;
          word-break: break-word;
          overflow-wrap: break-word;
          &:last-child {
            border-inline-end: none;
          }
        }
      }
    }
  }
}
.descriptions-middle {
  .m-descriptions-view {
    .descriptions-item-td {
      padding-bottom: 12px !important;
    }
  }
  .descriptions-bordered {
    :deep(.descriptions-label-th) {
      padding: 12px 24px !important;
    }
    :deep(.descriptions-content-td) {
      padding: 12px 24px !important;
    }
  }
}
.descriptions-small {
  .m-descriptions-view {
    .descriptions-item-td {
      padding-bottom: 8px !important;
    }
  }
  .descriptions-bordered {
    :deep(.descriptions-label-th) {
      padding: 8px 16px !important;
    }
    :deep(.descriptions-content-td) {
      padding: 8px 16px !important;
    }
  }
}
</style>

创建 DescriptionsItem.vue 组件

<script setup lang="ts">
import type { CSSProperties } from 'vue'
interface Props {
  label?: string // 内容的描述标签 string | slot
  span?: number // 包含列的数量;当使用水平列表且未设置 span 时等效于 span: 1,但最后一行的最后一项,会包含该行剩余的所有列数
  labelStyle?: CSSProperties // 自定义标签样式,优先级高于 Description 的 labelStyle
  contentStyle?: CSSProperties // 自定义内容样式,优先级高于 Description 的 contentStyle
}
withDefaults(defineProps<Props>(), {
  label: undefined,
  span: undefined,
  labelStyle: () => ({}),
  contentStyle: () => ({})
})
</script>
<template>
  <div class="descriptions-item" :data-span="span">
    <span class="descriptions-label" :style="labelStyle">
      <slot name="label">{
  { label }}</slot>
    </span>
    <span class="descriptions-content" :style="contentStyle">
      <slot></slot>
    </span>
  </div>
  <div class="descriptions-item-bordered" :data-span="span">
    <th class="descriptions-label-th" :style="labelStyle">
      <slot name="label">{
  { label }}</slot>
    </th>
    <td class="descriptions-content-td" :style="contentStyle">
      <slot></slot>
    </td>
  </div>
</template>
<style lang="less" scoped>
.descriptions-item {
  display: flex;
  .descriptions-label {
    display: inline-flex;
    align-items: baseline;
    color: rgba(0, 0, 0, 0.88);
    font-weight: normal;
    font-size: 14px;
    line-height: 1.5714285714285714;
    text-align: start;
    &::after {
      content: ':';
      position: relative;
      top: -0.5px;
      margin-inline: 2px 8px;
    }
  }
  .descriptions-content {
    display: inline-flex;
    align-items: baseline;
    flex: 1;
    color: rgba(0, 0, 0, 0.88);
    font-size: 14px;
    line-height: 1.5714285714285714;
    word-break: break-word;
    overflow-wrap: break-word;
  }
}
</style>

在要使用的页面引入

其中引入使用了以下组件:

<script setup lang="ts">
import Descriptions from './Descriptions.vue'
import DescriptionsItem from './DescriptionsItem.vue'
import { ref, reactive } from 'vue'
const size = ref('default')
const options = ref([
  {
    label: 'default',
    value: 'default'
  },
  {
    label: 'middle',
    value: 'middle'
  },
  {
    label: 'small',
    value: 'small'
  }
])
const show = ref(true)
const onClick = () => {
  show.value = false
}
const state = reactive({
  title: 'User Info',
  extra: 'extra',
  bordered: false,
  vertical: false,
  size: 'default',
  column: {
    xs: 1,
    sm: 2,
    md: 3,
    lg: 3,
    xl: 3,
    xxl: 3
  },
  labelStyle: {
    fontSize: '14px',
    color: '#FF6900',
    fontWeight: 600
  },
  contentStyle: {
    fontSize: '14px',
    color: '#1677FF',
    fontWeight: 400
  }
})
</script>
<template>
  <div>
    <h1>{
  { $route.name }} {
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Descriptions title="User Info">
      <template #extra>
        <a href="#" @click="onClick">more</a>
      </template>
      <DescriptionsItem label="UserName">Zhou Maomao</DescriptionsItem>
      <DescriptionsItem label="Telephone">1810000000</DescriptionsItem>
      <DescriptionsItem label="Live">Hangzhou, Zhejiang</DescriptionsItem>
      <DescriptionsItem label="Remark">empty</DescriptionsItem>
      <DescriptionsItem label="Address"
        >No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China</DescriptionsItem
      >
    </Descriptions>
    <h2 class="mt30 mb10">带边框的</h2>
    <Descriptions title="User Info" bordered>
      <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
      <DescriptionsItem label="Billing Mode">Prepaid</DescriptionsItem>
      <DescriptionsItem label="Automatic Renewal">YES</DescriptionsItem>
      <DescriptionsItem label="Order time">2018-04-24 18:00:00</DescriptionsItem>
      <DescriptionsItem label="Usage Time" :span="2">2030-04-24 18:00:00</DescriptionsItem>
      <DescriptionsItem label="Status" :span="3">
        <Badge status="processing" text="Running" />
      </DescriptionsItem>
      <DescriptionsItem label="Negotiated Amount">$80.00</DescriptionsItem>
      <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
      <DescriptionsItem label="Official Receipts">$60.00</DescriptionsItem>
      <DescriptionsItem label="Config Info">
        Data disk type: MongoDB
        <br />
        Database version: 3.4
        <br />
        Package: dds.mongo.mid
        <br />
        Storage space: 10 GB
        <br />
        Replication factor: 3
        <br />
        Region: East China 1
        <br />
      </DescriptionsItem>
    </Descriptions>
    <h2 class="mt30 mb10">响应式描述列表</h2>
    <Descriptions title="Responsive Descriptions" bordered :column="{ xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 }">
      <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
      <DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
      <DescriptionsItem label="Time">18:00:00</DescriptionsItem>
      <DescriptionsItem label="Amount">$80.00</DescriptionsItem>
      <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
      <DescriptionsItem label="Official">$60.00</DescriptionsItem>
      <DescriptionsItem label="Config Info">
        Data disk type: MongoDB
        <br />
        Database version: 3.4
        <br />
        Package: dds.mongo.mid
        <br />
        Storage space: 10 GB
        <br />
        Replication factor: 3
        <br />
        Region: East China 1
      </DescriptionsItem>
    </Descriptions>
    <h2 class="mt30 mb10">垂直列表</h2>
    <Descriptions title="User Info" vertical>
      <DescriptionsItem label="UserName">Zhou Maomao</DescriptionsItem>
      <DescriptionsItem label="Telephone">1810000000</DescriptionsItem>
      <DescriptionsItem label="Live">Hangzhou, Zhejiang</DescriptionsItem>
      <DescriptionsItem label="Address" :span="2">
        No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
      </DescriptionsItem>
      <DescriptionsItem label="Remark">empty</DescriptionsItem>
    </Descriptions>
    <h2 class="mt30 mb10">带边框的垂直列表</h2>
    <Descriptions title="User Info" vertical bordered>
      <DescriptionsItem label="UserName">Zhou Maomao</DescriptionsItem>
      <DescriptionsItem label="Telephone">1810000000</DescriptionsItem>
      <DescriptionsItem label="Live">Hangzhou, Zhejiang</DescriptionsItem>
      <DescriptionsItem label="Address" :span="2">
        No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
      </DescriptionsItem>
      <DescriptionsItem label="Remark">empty</DescriptionsItem>
    </Descriptions>
    <h2 class="mt30 mb10">自定义尺寸</h2>
    <Flex vertical gap="middle">
      <Radio :options="options" v-model:value="size" button button-style="solid" />
      <Descriptions bordered title="Custom Size" :size="size">
        <template #extra>
          <Button type="primary">Edit</Button>
        </template>
        <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
        <DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
        <DescriptionsItem label="Time">18:00:00</DescriptionsItem>
        <DescriptionsItem label="Amount">$80.00</DescriptionsItem>
        <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
        <DescriptionsItem label="Official">$60.00</DescriptionsItem>
        <DescriptionsItem label="Config Info">
          Data disk type: MongoDB
          <br />
          Database version: 3.4
          <br />
          Package: dds.mongo.mid
          <br />
          Storage space: 10 GB
          <br />
          Replication factor: 3
          <br />
          Region: East China 1
          <br />
        </DescriptionsItem>
      </Descriptions>
      <Descriptions title="Custom Size" :size="size">
        <template #extra>
          <Button type="primary">Edit</Button>
        </template>
        <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
        <DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
        <DescriptionsItem label="Time">18:00:00</DescriptionsItem>
        <DescriptionsItem label="Amount">$80.00</DescriptionsItem>
        <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
        <DescriptionsItem label="Official">$60.00</DescriptionsItem>
      </Descriptions>
    </Flex>
    <h2 class="mt30 mb10">自定义内容 & 标签样式</h2>
    <Flex vertical gap="middle">
      <Descriptions
        bordered
        title="Custom Style"
        :labelStyle="{ fontWeight: 800, color: '#faad14' }"
        :contentStyle="{ fontWeight: 600, color: '#1677ff' }"
      >
        <template #extra>
          <Button type="primary">Edit</Button>
        </template>
        <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
        <DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
        <DescriptionsItem label="Time">18:00:00</DescriptionsItem>
        <DescriptionsItem label="Amount" :labelStyle="{ color: '#52c41a' }" :contentStyle="{ color: '#ff4d4f' }"
          >$80.00</DescriptionsItem
        >
        <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
        <DescriptionsItem label="Official">$60.00</DescriptionsItem>
        <DescriptionsItem label="Config Info">
          Data disk type: MongoDB
          <br />
          Database version: 3.4
          <br />
          Package: dds.mongo.mid
          <br />
          Storage space: 10 GB
          <br />
          Replication factor: 3
          <br />
          Region: East China 1
          <br />
        </DescriptionsItem>
      </Descriptions>
      <Descriptions
        title="Custom Style"
        :labelStyle="{ fontWeight: 800, color: '#faad14' }"
        :contentStyle="{ fontWeight: 600, color: '#1677ff' }"
      >
        <template #extra>
          <Button type="primary">Edit</Button>
        </template>
        <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
        <DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
        <DescriptionsItem label="Time">18:00:00</DescriptionsItem>
        <DescriptionsItem label="Amount" :labelStyle="{ color: '#52c41a' }" :contentStyle="{ color: '#ff4d4f' }"
          >$80.00</DescriptionsItem
        >
        <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
        <DescriptionsItem label="Official">$60.00</DescriptionsItem>
      </Descriptions>
    </Flex>
    <h2 class="mt30 mb10">描述列表配置器</h2>
    <Flex vertical>
      <Row :gutter="[24, 12]">
        <Col :span="6">
          <Flex gap="small" vertical>
            title:
            <Input v-model:value="state.title" placeholder="title" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            extra:
            <Input v-model:value="state.extra" placeholder="extra" />
          </Flex>
        </Col>
        <Col :span="6">
          <Space gap="small" vertical>
            bordered:
            <Switch v-model="state.bordered" />
          </Space>
        </Col>
        <Col :span="6">
          <Space gap="small" vertical>
            vertical:
            <Switch v-model="state.vertical" />
          </Space>
        </Col>
        <Col :span="6">
          <Space gap="small" vertical>
            size:
            <Radio :options="options" v-model:value="state.size" button button-style="solid" />
          </Space>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            Column xs:
            <InputNumber v-model:value="state.column.xs" :min="1" placeholder="xs" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            Column sm:
            <InputNumber v-model:value="state.column.sm" :min="1" :max="9" placeholder="sm" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            Column md:
            <InputNumber v-model:value="state.column.md" :min="1" :max="9" placeholder="md" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            Column lg:
            <InputNumber v-model:value="state.column.lg" :min="1" :max="9" placeholder="lg" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            Column xl:
            <InputNumber v-model:value="state.column.xl" :min="1" :max="9" placeholder="xl" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            Column xxl:
            <InputNumber v-model:value="state.column.xxl" :min="1" :max="9" placeholder="xxl" />
          </Flex>
        </Col>
        <Col :span="6"></Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            labelStyle fontSize:
            <Input v-model:value="state.labelStyle.fontSize" placeholder="labelStyle fontSize" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            labelStyle color:
            <Input v-model:value="state.labelStyle.color" placeholder="labelStyle color" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            labelStyle fontWeight:
            <InputNumber
              v-model:value="state.labelStyle.fontWeight"
              :step="100"
              :min="100"
              :max="1000"
              placeholder="labelStyle fontWeight"
            />
          </Flex>
        </Col>
        <Col :span="6"></Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            contentStyle fontSize:
            <Input v-model:value="state.contentStyle.fontSize" placeholder="contentStyle fontSize" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            contentStyle color:
            <Input v-model:value="state.contentStyle.color" placeholder="contentStyle color" />
          </Flex>
        </Col>
        <Col :span="6">
          <Flex gap="small" vertical>
            contentStyle fontWeight:
            <InputNumber
              v-model:value="state.contentStyle.fontWeight"
              :step="100"
              :min="100"
              :max="1000"
              placeholder="contentStyle fontWeight"
            />
          </Flex>
        </Col>
      </Row>
      <Descriptions
        :title="state.title"
        :extra="state.extra"
        :bordered="state.bordered"
        :vertical="state.vertical"
        :size="state.size"
        :column="{
          xs: state.column.xs,
          sm: state.column.sm,
          md: state.column.md,
          lg: state.column.lg,
          xl: state.column.xl,
          xxl: state.column.xxl
        }"
        :label-style="state.labelStyle"
        :content-style="state.contentStyle"
      >
        <DescriptionsItem label="Product">Cloud Database</DescriptionsItem>
        <DescriptionsItem label="Billing">Prepaid</DescriptionsItem>
        <DescriptionsItem label="Time">18:00:00</DescriptionsItem>
        <DescriptionsItem label="Amount">$80.00</DescriptionsItem>
        <DescriptionsItem label="Discount">$20.00</DescriptionsItem>
        <DescriptionsItem label="Official">$60.00</DescriptionsItem>
        <DescriptionsItem label="Status" :span="1">
          <Badge status="processing" text="Running" />
        </DescriptionsItem>
        <DescriptionsItem label="Usage Time" :span="2">2030-04-24 18:00:00</DescriptionsItem>
        <DescriptionsItem label="Address" :span="3"
          >No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China</DescriptionsItem
        >
      </Descriptions>
    </Flex>
  </div>
</template>
相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
21天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
125 64
|
21天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
101 60
|
21天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
28 8
|
20天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
21 1
|
20天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
32 1
|
21天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
1月前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
74 7
|
1月前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
98 3