当前位置:   article > 正文

基于vue3+ts配置化table表格组件实现浅析_vue3 table组件

vue3 table组件

背景

同form,table编写会出现大量的table-column组件,并且没有另外处理分页组件及分页参数,出现大量的重复代码影响美观及代码维护,另外,大量的使用table+分页组件,会出现大量的重复逻辑,容易出现遗漏细节性的低级bug,各个页面分页管理其实基本一致,这里将分页组件及分页参数也封装在此组件内部,调用组件不在需要管理分页参数

此组件在element-uiant-design-vue项目中均可直接使用,实现原理 vue3+ts组件库同时兼容多种ui框架

效果图

最终实现的效果是这样滴!!!

概要实现逻辑

组件目录

"食用"例子

我们先看下上述效果图的配置化JOSN实例,最终我们将实现所有table表格+分页都能通过这样一些简单的JSON实现渲染,最后通过一个简单的调用即可渲染一个form表单,具体组件的入参如下:

  1. tableConfig:table相关配置,主要是支持element-uiant-design-vue table组件所有原生属性(eg:stripe、border等)以及自定义的一些table相关属性具体参看tableConfigFace接口定义,常用字段均有默认值,无特殊指定可以不传
  2. pagingConfig:分页相关配置,主要是支持element-uiant-design-vue pagination组件所有原生属性(eg:background、layout等)以及自定义的一些分页相关属性具体参看pagingConfigFace接口定义,常用字段均有默认值,无特殊指定可以不传
  3. thead:table列JSON对象配置,具体参考theadConfigFace,必传
  4. loadData:table数据获取函数配置,此函数必须返回一个promise且次promise必须返回一个满足resultInt接口定义的数据格式的对象,有数据渲染时,必传

调用

<BaseTable
  :thead="thead"
  :load-data="loadData2"
/>
<script lang="tsx" setup>
  // table列配置
  const thead = ref<theadConfigFace>([
    { type: 'index', fixed: 'left' },
    { prop: 'id', label: 'id', width: 100, align: 'left', fixed: 'left' },
    { prop: 'createTime', label: '创建时间', width: 100 },
    { prop: 'loanCount', label: '笔数', width: 80 },
    { prop: 'effectiveDays', label: '下载有效期(天)' },
    { prop: 'statusDesc', label: '状态' },
    {
      prop: 'infoData1',
      label: '数目1',
      width: 160,
      nativeProps:{
        'show-overflow-tooltip': true,
      }
    },
    {
      prop: 'info', label: '统计', children: [
        {
          prop: 'infoData22',
          label: '统计数目22',
          width: 160,
          children: [
            {
              prop: 'infoData221',
              label: '统计数目221',
              width: 160,
            },
            {
              prop: 'infoData222',
              label: '统计数目212',
              width: 160,
            },
          ],
        },
        {
          prop: 'infoData21',
          label: '统计数目21',
          width: 160,
          children: [
            {
              prop: 'infoData211',
              label: '统计数目211',
              width: 160,
            },
            {
              prop: 'infoData212',
              label: '统计数目212',
              width: 160,
              render: (scope: any) => (
                <div>
                  {scope.row.infoData211} render测试
                </div>
              ),
            },
          ],
        },
      ],
    },
  ])
  // 加载函数配置
  const loadData2:loadDataFace = async({ pageIndex, pageSize }: { pageIndex: number, pageSize: number }) => {
    return new Promise<resultInt>((resolve) => {
      setTimeout(() => {
        resolve({
          success: true,
          list: pageIndex === 1 ? [
            { id: 1, createTime: '2021-01', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 2, createTime: '2021-02', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 3, createTime: '2021-03', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 4, createTime: '2021-04', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 5, createTime: '2021-05', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 6, createTime: '2021-06', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 7, createTime: '2021-07', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 8, createTime: '2021-08', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 9, createTime: '2021-09', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 10, createTime: '2021-10', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
          ] : [
            { id: 11, createTime: '2021-11', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
            { id: 12, createTime: '2021-12', loanCount: 5, effectiveDays: 5, statusDesc: '成功', infoData1: 1, infoData21: 21, infoData22: 22, infoData211: 211, infoData212: 212, infoData221: 221, infoData222: 222 },
          ],
          total: 12,
        })
      }, 1500)
    })
  }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92

接口定义

先看配置JSON对象ts接口定义

/*
 * @Author: 陈宇环
 * @Date: 2023-01-03 10:56:12
 * @LastEditTime: 2023-04-25 14:13:53
 * @LastEditors: 陈宇环
 * @Description:
 */

// table配置参数
export interface tableConfigFace {
  border?: boolean,  // 是否需要边框
  stripe?: boolean, // 是否斑马纹
  ifInitLoadData?: boolean, // 是否初始调用getList方法
  rowSelection?: rowSelectionFace // 选择行配置
  rowKey?: string, // 行对应key值,选择行功能开启时必传
  
  // ant

  nativeProps?: {   // ui框架原生属性
    [key: string]: any
  }
}

// 分页配置参数
export interface pagingConfigFace {
  open?: boolean,  // 是否需要分页
  pageIndex?: number,  // 默认pageIndex
  pageSize?: number,   // 默认pageSize
  total?: number, // 默认total
  showTotal?: any, // ant 属性
  showSizeChanger?: boolean, // ant 属性
  layout?: string,
  pageIndexChange?: (val: number) => any
  pageSizeChange?: (val: number) => any
  nativeProps?: {   // ui框架原生属性
    [key: string]: any
  }
}

// table列配置
export type theadConfigFace = theadItemConfig[]

// table列配置项item
export interface theadItemConfig {
  prop?: string,  // key
  label?: string, // 中文名称
  type?: 'selection' | 'index' | 'expand'  // 类型
  width?: string | number, // 宽度
  minWidth?: string | number, // 最小宽度
  align?: 'left' | 'center' | 'right', // 列align布局
  fixed?: 'left' | 'right' | true, // 列是否固定在左侧或者右侧,true 表示固定在左侧
  render?: (scope: any) => any,  // 自定义渲染函数
  children?: theadItemConfig[],  // 多级头定义
  nativeProps?: {   // ui框架原生属性
    [key: string]: any
  }
}

// table多选配置项
export type rowSelectionFace = {
  type: 'checkout' | 'radio', // 多选或者单选
  onChange:(selection?: any[]) => any,  // 选择变化勾选变化事件
  selectable?: (row:any, index:number) => boolean // 当前行勾选是否禁用
}

// table数据获取函数返回值校验
export interface resultInt {
  success: boolean, // 接口返回状态
  list: any[], // table数据列表
  total: number // table数据总数
}

// table数据获取函数接口
export type loadDataFace = ({ pageIndex, pageSize }: { pageIndex: number, pageSize: number }) => Promise<resultInt>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

数据获取及分页参数管理

本组件将分页参数也全部封装到了组件内部,组件调用不需要处理分页相关参数,只需要传入数据获取函数loadData既可,需要刷新列表或者手动获取某页参数时可以通过BaseTableRef.value.getList({pageIndex: 1,pageSize:20})的方式进行操作,具体实现部分代码如下:

// @/components/BaseTable/index

const pageInfo = reactive({
  pageIndex: clonePagingConfig.pageIndex,
  pageSize: clonePagingConfig.pageSize,
  total: clonePagingConfig.total,
})
const list = ref([])
// 获取数据函数
const getList = async({ pageIndex = pageInfo.pageIndex, pageSize = pageInfo.pageSize } : { pageIndex?: number, pageSize?: number } = {}) => {
  try {
    loading.value = true
    // 使用内部的分页参数来调用外部传入loadData函数,来获取数据
    const result = await loadData.value({
      pageIndex,
      pageSize,
    })
    loading.value = false
    if (result.success) {
      list.value = result.list
      pageInfo.total = result.total
    }
    pageInfo.pageIndex = pageIndex
    pageInfo.pageSize = pageSize
  } catch (error) {
    console.log(error)
  }
}
// 暴露getList方法给父组件
expose({
  getList,
})
onMounted(function() {
  // 如果需要默认调用getList
  if (cloneTableConfig.ifInitLoadData) {
    getList()
  }
})
// 分页size变化
const handleSizeChange = (val: number) => {
  console.log(`${val} items per page`)
  pageInfo.pageIndex = 1
  pageInfo.pageSize = val
  clonePagingConfig.pageSizeChange && clonePagingConfig.pageSizeChange(val)
  getList()
}
// 当前页变化
const handleCurrentChange = (val: number) => {
  console.log(`current page: ${val}`)
  pageInfo.pageIndex = val
  clonePagingConfig.pageIndexChange && clonePagingConfig.pageIndexChange(val)
  getList()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

配置化实现及多级头处理

不在需要复制粘贴table-column组件,通过如下配置既可生成table列

const thead = ref<theadConfigFace>([
  { type: 'index', fixed: 'left' },
  { prop: 'branchCode', label: '分支编码', minWidth: 120 },
  { prop: 'branchName', label: '分支名称', minWidth: 120 },
  { 
    label: '所属区域',
  	children: [
    	{ prop: 'regionName', label: '所属区域名称', minWidth: 120 },
      { prop: 'regionCode', label: '所属区域编码', minWidth: 120 },
  	] 
  },
  { prop: 'regionSupervisorName', label: '区域主管', minWidth: 120 },
  { prop: 'accountantName', label: '分支核算会计', minWidth: 120 },
  { prop: 'updateUserName', label: '最后修改人', minWidth: 120 },
  { prop: 'updateTime', label: '最后修改时间', minWidth: 160 },
  {
    label: '操作',
    width: 200,
    fixed: 'right',
    render(scope: any) {
      return <div>
        <el-button type="primary" size="small" onClick={() => {
          opFn('edit', scope)
        }}>变更</el-button>
        <el-button type="primary" size="small" onClick={() => {
          opFn('changeRecord', scope)
        }}>变更记录</el-button>
      </div>
    },
  },
])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

多级头这里我们采用递归的方式,遍历递归thead数组及children属性递归建立table组件,部分代码如下:

// @/components/BaseTable/index

/*遍历thead生成table列*/
{thead.value.map((item: theadItemConfig, index: any) => {
  return (
    <BaseTableItem
      key={item.prop ? item.prop : '' + index}
      item-data={item}
      />
  )
})}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
// @/components/BaseTable/BaseTableItem

// 导入组件本身
import BaseTableItem from './BaseTableItem'

// 多级头处理
const childrenDom = itemData.value.children && itemData.value.children.length > 0
  ? itemData.value.children.map((item:any, index:any) => (
    <BaseTableItem
      key={item.prop ? item.prop : '' + index}
      item-data={item}
    ></BaseTableItem>
  ))
  : null

return () => {
  return (
    <dynamicTableColumn
      prop={itemData.value.prop}
      label={itemData.value.label}
      width={itemData.value.width}
      min-width={itemData.value.minWidth}
      align={itemData.value.align ? itemData.value.align : 'center'}
      fixed={itemData.value.fixed ? itemData.value.fixed : false}
      {...itemData.value.nativeProps}
      v-slots={{
        default: (scope: any) => {
          return <>
            {
              itemData.value.render ?
                (typeof itemData.value.render === 'function' ? itemData.value.render(scope) : itemData.value.render) :
                scope.row[itemData.value.prop]
            }
            {/* 多级头部 */}
            {childrenDom}
          </>
        },
      }}
    ></cTableColumn>
  )
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

上述代码中childrenDom用来处理多级头部,如果配置thead中存在children则代表存在多级头部,递归children既可

兼容原生ui属性及方法

tableConfig、pagingConfig已经thead的每一项分别兼容element原生el-table、pagination、el-table-column组件所有属性实现方式

实现方式也很简单,在tsx中通过扩展符展开既可

<dynamicTable
  v-loading={loading.value}
  height="100%"
  ref={tableDom}
  class={[styles.table]}
  data={list.value}
  columns={columns.value}
  data-source={list.value}
  style={{ maxWidth: '100%' }}
  row-key={cloneTableConfig.rowKey}

  pagination={false} // ant 特有属性,关闭table自带分页

  {...cloneTableConfig.nativeProps}
  onSelectionChange={(val: any) => handleSelectionChange(val)}
</dynamicTable>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

支持行首单选

此功能只有选用element-ui时才支持

element-ui 原生单选是点击行选择然后高亮,个人觉得不是很友好

行单选?如下图:

实现

<div class={[styles.BaseTable]}>
  <dynamicTable
    v-loading={loading.value}
    height="100%"
    ref={tableDom}
    class={[styles.table]}
    data={list.value}
    columns={columns.value}
    data-source={list.value}
    style={{ maxWidth: '100%' }}
    row-key={cloneTableConfig.rowKey}

    pagination={false} // ant 特有属性,关闭table自带分页

    {...cloneTableConfig.nativeProps}
    onSelectionChange={(val: any) => handleSelectionChange(val)}
  >
    {/* 只有el-ui走这段渲染逻辑,ant-Design-vue是通过columns直接生成的 */}
    {CustomDynamicComponent.language === CustomDynamicComponent.eleLanguage ? <>
      {/* 需要多选行选择按钮 */}
      {cloneTableConfig.rowSelection && cloneTableConfig.rowSelection.type === 'checkout' ? (
        <dynamicTableColumn type="selection" align="center" selectable={(row: any, index: number) => {
          return cloneTableConfig.rowSelection?.selectable ? cloneTableConfig.rowSelection?.selectable(row, index) : true
        }} />
      ) : null}
      {/* 需要单选行选择按钮 */}
      {cloneTableConfig.rowSelection && cloneTableConfig.rowSelection.type === 'radio' ? (
        <dynamicTableColumn
          label=""
          align="center"
          width="60"
          fixed
          v-slots={{
            default: (scope: any, column: any, index: number) => {
              return (
                <div style={{ textAlign: 'center' }}>
                  <dynamicRadio
                    disabled={cloneTableConfig.rowSelection?.selectable ? !cloneTableConfig.rowSelection?.selectable(scope.row, index) : false}
                    class={[styles.rowRadio]}
                    v-model={radio.value}
                    label={scope.row[cloneTableConfig.rowKey ? cloneTableConfig.rowKey : 'id']}
                    onChange={(val: any) => handleSelectionChange(val)}
                  ></dynamicRadio>
                </div>
              )
            },
          }}
        ></dynamicTableColumn>
      ) : null}
      {columns.value.map((item: theadItemConfig, index: any) => {
        return (
          // 递归组件
          <BaseTableItem
            key={item.prop ? item.prop : '' + index}
            item-data={item}
          ></BaseTableItem>
        )
      })}</> : null}
  </cTable>
  {
    clonePagingConfig.open && <div
      style={{
        display: 'flex',
        justifyContent: 'center',
        padding: '15px 0',
      }}
    >
      <dynamicPagination
        current-page={pageInfo.pageIndex}
        page-size={pageInfo.pageSize}
        layout={defaultPagingConfig.layout}
        total={pageInfo.total}
        background
        {...clonePagingConfig.nativeProps}
        onSizeChange={(val: any) => handleSizeChange(val)}
        onCurrentChange={(val: any) => handleCurrentChange(val)}
        
        // ant-ui相关属性
        current={pageInfo.pageIndex}
        onShowSizeChange={(current: number, size: number) => handleSizeChange(size)}
        onChange={(page:number) => handleCurrentChange(page)}
      />
    </div>
  }
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

多选:沿用ui框架原生 type="selection"属性来实现

单选通过:自定义radio组件来实现

坑点

props 传对象时,默认值会被整体覆盖问题

举例

props: {
  tableConfig: {
    type: Object as PropType<tableConfigFace>,
    default() {
      return {
        border: true,
        stripe: true,
        ifInitLoadData: true,
        rowKey: 'id',
      }
    },
  },
},
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

默认参数如上,当传参如下时:

<BaseTable
  ref="BaseTableRef"
  :thead="thead"
  :load-data="loadData2"
  :table-config="{
    ifInitLoadData: false
  }"
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

此时:组件内部props拿到的tableConfig会被整体替换成{ifInitLoadData: false},tableConfig默认值对象里的其他字段全部为空了

解决方式

props: {
  tableConfig: {
    type: Object as PropType<tableConfigFace>,
    default() {
      return {
        border: true,
        stripe: true,
        ifInitLoadData: true,
        rowKey: 'id',
      }
    },
  },
},
    
...
    
const defaultTableConfig: tableConfigFace = {
  border: true,
  stripe: true,
  ifInitLoadData: true,
  rowKey: 'id',
  rowSelection: {
    type: 'checkout',
    onChange: (selection: any) => {
      console.log(selection)
    },
  },
}
const cloneTableConfig: tableConfigFace = reactive<tableConfigFace>({
  ...defaultTableConfig,
  ...props.tableConfig,
})
watch(
  () => props.tableConfig,
  () => {
    Object.assign(cloneTableConfig, defaultTableConfig, props.tableConfig)
  },
  { immediate: true, deep: true },
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

源码及实现浅析

https://blog.csdn.net/junner443/article/details/131302051

作者:快落的小海疼

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/621646
推荐阅读
相关标签
  

闽ICP备14008679号