NormalTable.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. <template>
  2. <n-data-table class="normal-table"
  3. v-if="columns.length > 0"
  4. :columns="columns as DataTableColumns"
  5. :data="tableData"
  6. :single-line="false"
  7. />
  8. </template>
  9. <script setup lang="ts">
  10. import { h, ref, onMounted } from 'vue-demi'
  11. import { withBase } from 'vitepress';
  12. import { NDataTable, DataTableColumn, DataTableColumns, DataTableColumnGroup, DataTableBaseColumn, DataTableSelectionColumn, DataTableExpandColumn } from 'naive-ui'
  13. interface ChildColumnConfig {
  14. title: string;
  15. key?: string;
  16. children?: ChildTableConfig;
  17. align?: 'left' | 'right' | 'center'; // 列文本对齐方式,默认left
  18. titleAlign?: 'left' | 'right' | 'center'; // 列标题对齐方式,默认同文本
  19. minWidth?: number | string;
  20. maxWidth?: number | string;
  21. width?: number | string;
  22. vertical?: boolean; // 控制标题文件的显示方向为纵向
  23. }
  24. interface ColumnConfig extends ChildColumnConfig {
  25. // 类型选择,不填写默认为普通列
  26. // selection: 选项列,用于勾选数据行
  27. // expand: 可展开列,用于展开行
  28. type?: 'selection' | 'expand';
  29. // selection参数
  30. selectionMultiple?: boolean; // 默认true,false-单选,true-多选
  31. }
  32. type ChildTableConfig = (string | ChildColumnConfig)[]
  33. type TableConfig = (string | ColumnConfig)[]
  34. // type TableRowData = Record<string, unknown>;
  35. type TableRowData = CellDataType;
  36. // type TableColumn = DataTableColumn<TableRowData>;
  37. // type TableColumns = DataTableColumns<TableRowData>;
  38. type TableBaseColumn = DataTableBaseColumn<TableRowData>;
  39. // type TableColumnGroup = DataTableColumnGroup<TableRowData>;
  40. type TableSelectionColumn = DataTableSelectionColumn<TableRowData>;
  41. type TableExpandColumn = DataTableExpandColumn<TableRowData>;
  42. // 规避naive-ui库bug,children类型定义错误
  43. interface DataTableColumnGroupEx<T = TableRowData> extends Omit<DataTableColumnGroup<T>, 'children'> {
  44. children: ChildTableColumns
  45. }
  46. type DataTableColumnEx<T = TableRowData> = Exclude<DataTableColumn<T>, DataTableColumnGroup<T>> | DataTableColumnGroupEx<T>;
  47. type TableColumnGroup = DataTableColumnGroupEx<TableRowData>;
  48. type TableColumn = DataTableColumnEx<TableRowData>;
  49. type TableColumns = Array<DataTableColumnEx<TableRowData>>;
  50. type ChildTableColumn = DataTableBaseColumn<TableRowData> | DataTableColumnGroupEx<TableRowData>;
  51. type ChildTableColumns = Array<(DataTableBaseColumn<TableRowData> | DataTableColumnGroupEx<TableRowData>)>;
  52. interface CellDataExt {
  53. text: string;
  54. colspan?: number | undefined; // 向右合并 N 个单元格
  55. rowspan?: number | undefined; // 向下合并 N 个单元格
  56. }
  57. type CellDataType = {
  58. [title: string]: string | CellDataExt,
  59. } & {
  60. // expand参数
  61. _expandable?: boolean;
  62. _expandString?: string;
  63. }
  64. interface Props {
  65. config?: TableConfig;
  66. data?: CellDataType[];
  67. jsonFile?: string;
  68. }
  69. const props = withDefaults(defineProps<Props>(), {
  70. config: () => [],
  71. data: () => [],
  72. jsonFile: () => ''
  73. })
  74. const dataPointer: {data: CellDataType[]} = {
  75. data: props.data
  76. }
  77. const columns = ref<TableColumns>([])
  78. const tableData = ref<any[]>([])
  79. const handleRowSpan = (rowData: TableRowData, rowIndex: number, key: string): number => {
  80. const data = dataPointer.data[rowIndex]
  81. if (!data) return 1
  82. const keyValue = data[key]
  83. if (!keyValue) return 1
  84. if (typeof keyValue === "string") return 1
  85. return keyValue.rowspan ?? 1
  86. }
  87. const handleColSpan = (rowData: TableRowData, rowIndex: number, key: string): number => {
  88. const data = dataPointer.data[rowIndex]
  89. if (!data) return 1
  90. const keyValue = data[key]
  91. if (!keyValue) return 1
  92. if (typeof keyValue === "string") return 1
  93. return keyValue.colspan ?? 1
  94. }
  95. function createStringColumn(column: string, parentKey?: string): TableBaseColumn {
  96. const key = parentKey ? parentKey + `.${column}` : column
  97. const columnObj: TableBaseColumn = {
  98. title: column,
  99. key,
  100. rowSpan: (rowData, rowIndex) => {
  101. return handleRowSpan(rowData, rowIndex, key)
  102. },
  103. colSpan: (rowData, rowIndex) => {
  104. return handleColSpan(rowData, rowIndex, key)
  105. }
  106. }
  107. return columnObj
  108. }
  109. // 标题纵向渲染
  110. const verticalTitle = (text: string) => () =>
  111. h('div', {
  112. style: {
  113. writingMode: 'vertical-rl',
  114. transform: 'rotate(180deg)',
  115. whiteSpace: 'nowrap',
  116. textAlign: 'center'
  117. }
  118. }, text)
  119. function createBaseColumn(column: ChildColumnConfig, parentKey?: string): TableBaseColumn | TableColumnGroup {
  120. const columnKey = column.key ? column.key : column.title
  121. const key = parentKey ? parentKey + `.${columnKey}` : columnKey
  122. const title = column.vertical ? verticalTitle(column.title) : column.title
  123. const columnObj: TableBaseColumn | TableColumnGroup = {
  124. title,
  125. key,
  126. align: column.align,
  127. titleAlign: column.titleAlign,
  128. minWidth: column.minWidth,
  129. maxWidth: column.maxWidth,
  130. width: column.width,
  131. ...(() => {
  132. if (column.children === undefined) {
  133. return {
  134. rowSpan: (rowData, rowIndex) => {
  135. return handleRowSpan(rowData, rowIndex, key)
  136. },
  137. colSpan: (rowData, rowIndex) => {
  138. return handleColSpan(rowData, rowIndex, key)
  139. }
  140. }
  141. } else {
  142. const children = traversalChildren(column.children, key)
  143. return children ? { children } : {}
  144. }
  145. })()
  146. }
  147. return columnObj
  148. }
  149. function createSelectionColumn(column: ColumnConfig): TableSelectionColumn {
  150. const columnObj: TableSelectionColumn = {
  151. type: 'selection',
  152. multiple: column.selectionMultiple
  153. }
  154. return columnObj
  155. }
  156. function createExpandColumn(column: ColumnConfig): TableExpandColumn {
  157. const columnObj: TableExpandColumn = {
  158. type: 'expand',
  159. expandable: (rowData: TableRowData) => {
  160. return rowData._expandable ? rowData._expandable : false
  161. },
  162. renderExpand: (rowData: TableRowData, rowIndex: number) => {
  163. if (rowData._expandable === undefined || rowData._expandable === false) return undefined
  164. return rowData._expandString
  165. }
  166. }
  167. return columnObj
  168. }
  169. function traversalChildren(child: ChildTableConfig | undefined, parentKey?: string): ChildTableColumns | undefined {
  170. if (child === undefined) return undefined
  171. return child.map(item => {
  172. let obj: TableColumn | undefined = undefined
  173. if (typeof item === 'string') {
  174. obj = { ...createStringColumn(item, parentKey) }
  175. } else {
  176. obj = { ...createBaseColumn(item, parentKey) }
  177. }
  178. return obj
  179. }).filter(item => item !== undefined)
  180. }
  181. function createColumns(config: TableConfig): TableColumns {
  182. return config.map(column => {
  183. let obj: TableColumn | undefined = undefined
  184. if (typeof column === 'string') {
  185. obj = { ...createStringColumn(column) }
  186. } else if (column.type === 'selection') {
  187. obj = { ...createSelectionColumn(column) }
  188. } else if (column.type === 'expand') {
  189. obj = { ...createExpandColumn(column) }
  190. } else {
  191. obj = { ...createBaseColumn(column) }
  192. }
  193. return obj
  194. }).filter(item => item !== undefined)
  195. }
  196. function createData(data: CellDataType[]) {
  197. return data.map((row, index) => {
  198. let obj = {
  199. key: index
  200. }
  201. for (const key in row) {
  202. const value = row[key as keyof typeof row];
  203. if (typeof value === "string") {
  204. obj[key] = value
  205. } else {
  206. obj[key] = value.text
  207. }
  208. }
  209. return obj
  210. })
  211. }
  212. // const columns = createColumns(props.config)
  213. // const tableData = createData(props.data)
  214. interface JsonData {
  215. config: TableConfig,
  216. data: CellDataType[]
  217. }
  218. const getJson = async (path: string): Promise<JsonData | undefined> => {
  219. const res = await fetch(withBase(path)).catch((err) => {
  220. console.log("table file err: ", err)
  221. })
  222. if (!res) return undefined
  223. const json = await res.json()
  224. return json as JsonData
  225. }
  226. let jsonDatePromise: Promise<JsonData | undefined> | undefined = undefined
  227. if (props.jsonFile !== "") {
  228. jsonDatePromise = getJson(props.jsonFile)
  229. }
  230. const loadingLocalData = () => {
  231. columns.value.length = 0
  232. tableData.value.length = 0
  233. columns.value.push(...createColumns(props.config))
  234. tableData.value.push(...createData(props.data))
  235. }
  236. onMounted(async () => {
  237. if (jsonDatePromise === undefined) {
  238. loadingLocalData()
  239. } else {
  240. jsonDatePromise.then((value) => {
  241. if (value === undefined) {
  242. loadingLocalData()
  243. return
  244. }
  245. // 尝试处理数据
  246. try {
  247. const jsonColumns = createColumns(value.config)
  248. const jsonTableData = createData(value.data)
  249. dataPointer.data = value.data
  250. columns.value.push(...jsonColumns)
  251. tableData.value.push(...jsonTableData)
  252. } catch(err) {
  253. console.error('json表格数据处理错误:', err)
  254. loadingLocalData()
  255. }
  256. }).catch((reason) => {
  257. console.error('获取json表格数据失败:', reason)
  258. loadingLocalData()
  259. })
  260. }
  261. })
  262. </script>
  263. <style scoped>
  264. /* 覆盖.vp-doc table */
  265. .normal-table :deep(table) {
  266. margin: 0 0 !important;
  267. }
  268. .normal-table :deep(td) {
  269. white-space: pre-wrap; /* 保留换行和空格 */
  270. }
  271. </style>