From 3aee62c2a19d1c710ab3580b2dc78032b71b7123 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 12 Jan 2025 19:44:14 +0800 Subject: [PATCH 01/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC):=20=E6=B7=BB=E5=8A=A0=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vitepress/config.js | 1 + docs/components/data/data-table.md | 39 ++++++++++++++++++++ packages/index.ts | 5 ++- src/App.vue | 29 +++++++++++---- src/ui/data-table/ShadcnDataTable.vue | 16 ++++++++ src/ui/data-table/components/TableBody.vue | 22 +++++++++++ src/ui/data-table/components/TableHeader.vue | 19 ++++++++++ src/ui/data-table/index.ts | 1 + src/ui/data-table/types.ts | 15 ++++++++ 9 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 docs/components/data/data-table.md create mode 100644 src/ui/data-table/ShadcnDataTable.vue create mode 100644 src/ui/data-table/components/TableBody.vue create mode 100644 src/ui/data-table/components/TableHeader.vue create mode 100644 src/ui/data-table/index.ts create mode 100644 src/ui/data-table/types.ts diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 6d0eb30a..a522c717 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -282,6 +282,7 @@ export default { {text: '数据构建 (Data Builder)', link: 'data/data-builder', icon: '/components/data/data-builder.svg', version: '2024.5.0'}, {text: '流程 (Workflow)', link: 'data/workflow', icon: '/components/data/workflow.svg', version: '2024.5.1'}, {text: '数据过滤 (Data Filter)', link: 'data/data-filter', icon: '/components/data/data-filter.svg', version: '2024.5.2'}, + {text: '数据表格 (Data Table)', link: 'data/data-table', icon: '/components/data/data-table.svg', version: '2025.1.1'}, ] return { diff --git a/docs/components/data/data-table.md b/docs/components/data/data-table.md new file mode 100644 index 00000000..cf00f448 --- /dev/null +++ b/docs/components/data/data-table.md @@ -0,0 +1,39 @@ +--- +title: 数据表格 (Data Table) +--- + +# 介绍 + +
+ +本文档主要用于描述 `ShadcnDataTable` 组件的一些功能和用法。 + +## 用法 + +::: raw + + + + + + +::: + + \ No newline at end of file diff --git a/packages/index.ts b/packages/index.ts index 228f82e7..c7bf38c6 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -99,6 +99,7 @@ import { ShadcnContribution } from '@/ui/contribution' import { ShadcnMention } from '@/ui/mention' import { ShadcnImage, ShadcnImageGroup, ShadcnImageViewer } from '@/ui/image' import { ShadcnCarousel } from '@/ui/carousel' +import { ShadcnDataTable } from '@/ui/data-table' let components = [ ShadcnButton, @@ -194,7 +195,8 @@ let components = [ ShadcnContribution, ShadcnMention, ShadcnImage, ShadcnImageGroup, ShadcnImageViewer, - ShadcnCarousel + ShadcnCarousel, + ShadcnDataTable ] interface InstallOptions @@ -326,6 +328,7 @@ export { ShadcnContribution } from '@/ui/contribution' export { ShadcnMention } from '@/ui/mention' export { ShadcnImage, ShadcnImageGroup, ShadcnImageViewer } from '@/ui/image' export { ShadcnCarousel } from '@/ui/carousel' +export { ShadcnDataTable } from '@/ui/data-table' // Export functions export { fnToString, fnToFunction } from '@/utils/formatter' diff --git a/src/App.vue b/src/App.vue index 9cefa2ca..b1446058 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,14 +1,27 @@ \ No newline at end of file diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue new file mode 100644 index 00000000..f2cc360c --- /dev/null +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/src/ui/data-table/components/TableBody.vue b/src/ui/data-table/components/TableBody.vue new file mode 100644 index 00000000..efb14091 --- /dev/null +++ b/src/ui/data-table/components/TableBody.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/src/ui/data-table/components/TableHeader.vue b/src/ui/data-table/components/TableHeader.vue new file mode 100644 index 00000000..72a77690 --- /dev/null +++ b/src/ui/data-table/components/TableHeader.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/src/ui/data-table/index.ts b/src/ui/data-table/index.ts new file mode 100644 index 00000000..456e98f1 --- /dev/null +++ b/src/ui/data-table/index.ts @@ -0,0 +1 @@ +export { default as ShadcnDataTable } from './ShadcnDataTable.vue' \ No newline at end of file diff --git a/src/ui/data-table/types.ts b/src/ui/data-table/types.ts new file mode 100644 index 00000000..71f60a3e --- /dev/null +++ b/src/ui/data-table/types.ts @@ -0,0 +1,15 @@ +export interface ColumnProps +{ + key: string + label: string +} + +export interface DataTableProps +{ + columns: ColumnProps[] + data: Record[] +} + +export type DataTableEmits = { + (e: 'on-change'): void +} \ No newline at end of file From 5b83db7c7025e50039492b55b333f6ba8e44b603 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 12 Jan 2025 20:04:09 +0800 Subject: [PATCH 02/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC):=20=E6=B7=BB=E5=8A=A0=E5=B0=BA=E5=AF=B8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/data/data-table.md | 84 +++++++++++++++++++- src/App.vue | 31 +++++--- src/ui/data-table/ShadcnDataTable.vue | 13 +-- src/ui/data-table/components/TableBody.vue | 17 ++-- src/ui/data-table/components/TableHeader.vue | 12 ++- src/ui/data-table/size.ts | 10 +++ src/ui/data-table/types.ts | 3 + 7 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 src/ui/data-table/size.ts diff --git a/docs/components/data/data-table.md b/docs/components/data/data-table.md index cf00f448..0c6d881a 100644 --- a/docs/components/data/data-table.md +++ b/docs/components/data/data-table.md @@ -12,13 +12,95 @@ title: 数据表格 (Data Table) ::: raw - + ::: +::: details 查看代码 + +```vue + +``` + +::: + +## 尺寸 (size) + +::: raw + + +
+
+ 小尺寸 + + +
+
+ 默认尺寸 + + +
+
+ 大尺寸 + + +
+
+
+ +::: + +::: details 查看代码 + +```vue + +``` + +::: + +## 数据表格 (Data Table) 属性 + + + + \ No newline at end of file diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index f2cc360c..2bc9f97b 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -1,16 +1,17 @@ \ No newline at end of file +withDefaults(defineProps(), { + size: 'default' +}) \ No newline at end of file diff --git a/src/ui/data-table/components/TableBody.vue b/src/ui/data-table/components/TableBody.vue index efb14091..021671a8 100644 --- a/src/ui/data-table/components/TableBody.vue +++ b/src/ui/data-table/components/TableBody.vue @@ -2,10 +2,12 @@ + :class="[ 'border-b hover:bg-gray-50', + BaseSize[size] + ]"> + :class="[TablePaddingSize[size]]"> {{ row[col.key] }} @@ -13,10 +15,11 @@ \ No newline at end of file diff --git a/src/ui/data-table/components/TableHeader.vue b/src/ui/data-table/components/TableHeader.vue index 72a77690..f16edc9e 100644 --- a/src/ui/data-table/components/TableHeader.vue +++ b/src/ui/data-table/components/TableHeader.vue @@ -3,7 +3,9 @@ + :class="['text-left font-medium', + TablePaddingSize[size] + ]"> {{ col.label }} @@ -12,8 +14,12 @@ \ No newline at end of file diff --git a/src/ui/data-table/size.ts b/src/ui/data-table/size.ts new file mode 100644 index 00000000..6d882e3a --- /dev/null +++ b/src/ui/data-table/size.ts @@ -0,0 +1,10 @@ +import { BaseSize } from '@/ui/common/size.ts' + +export enum TablePaddingSize +{ + default = 'px-4 py-2 text-base', + small = 'px-2 py-1 text-sm', + large = 'px-6 py-3 text-lg' +} + +export type Size = keyof typeof BaseSize \ No newline at end of file diff --git a/src/ui/data-table/types.ts b/src/ui/data-table/types.ts index 71f60a3e..831f4ddf 100644 --- a/src/ui/data-table/types.ts +++ b/src/ui/data-table/types.ts @@ -1,3 +1,5 @@ +import { Size } from './size' + export interface ColumnProps { key: string @@ -8,6 +10,7 @@ export interface DataTableProps { columns: ColumnProps[] data: Record[] + size?: Size } export type DataTableEmits = { From c72d0de8f4266f4b7c7d5550cdd2648424d77796 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Sun, 12 Jan 2025 21:33:58 +0800 Subject: [PATCH 03/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC):=20=E6=B7=BB=E5=8A=A0=E5=88=97=E6=8E=92?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/data/data-table.md | 38 ++++++++++++ src/App.vue | 62 +++++++++++++------- src/ui/data-table/ShadcnDataTable.vue | 51 ++++++++++++++-- src/ui/data-table/components/TableHeader.vue | 38 +++++++++--- src/ui/data-table/hooks/useSort.ts | 49 ++++++++++++++++ src/ui/data-table/types.ts | 8 ++- 6 files changed, 211 insertions(+), 35 deletions(-) create mode 100644 src/ui/data-table/hooks/useSort.ts diff --git a/docs/components/data/data-table.md b/docs/components/data/data-table.md index 0c6d881a..bbb69c6f 100644 --- a/docs/components/data/data-table.md +++ b/docs/components/data/data-table.md @@ -90,6 +90,17 @@ title: 数据表格 (Data Table) ::: +## 排序 (sort) + +::: raw + + + + + + +::: + ## 数据表格 (Data Table) 属性 +
+ + + + +## 数据表格 (Data Table) 事件 + + + + \ No newline at end of file diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index 2bc9f97b..07736222 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -1,17 +1,58 @@ \ No newline at end of file +}) + +const emits = defineEmits() + +const { columns, toggleSort, getSortedColumns } = useSort(props.columns) + +const handleSortChange = (column: ColumnProps, event: MouseEvent) => { + toggleSort(column.key, event) + emits('on-sort', getSortedColumns()) +} + +const sortedData = computed(() => { + const sortColumns = getSortedColumns() + if (sortColumns.length === 0) { + return props.data + } + + return [...props.data].sort((a, b) => { + for (const column of sortColumns) { + const aValue = a[column.key] + const bValue = b[column.key] + const order = column.sort === 'asc' ? 1 : -1 + + if (aValue === bValue) { + continue + } + + if (typeof aValue === 'string') { + return aValue.localeCompare(bValue) * order + } + return (aValue - bValue) * order + } + return 0 + }) +}) + \ No newline at end of file diff --git a/src/ui/data-table/components/TableHeader.vue b/src/ui/data-table/components/TableHeader.vue index f16edc9e..6db7523e 100644 --- a/src/ui/data-table/components/TableHeader.vue +++ b/src/ui/data-table/components/TableHeader.vue @@ -1,12 +1,27 @@ @@ -55,4 +56,8 @@ const data = ref([ date: '2024-02-01' } ]) + +const handleCellClick = (playload: { rowIndex: number; col: string; row: any }) => { + console.log(`点击了第 ${ playload.rowIndex + 1 } 行,${ playload.col } 列`) +} \ No newline at end of file diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index 9885a0bd..2b05baaa 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -4,10 +4,14 @@ + @on-sort-change="handleSortChange"> + + + :size="size" + @on-cell-click="emits('on-cell-click', $event)"> +
diff --git a/src/ui/data-table/components/TableBody.vue b/src/ui/data-table/components/TableBody.vue index 38376cc6..d93f498b 100644 --- a/src/ui/data-table/components/TableBody.vue +++ b/src/ui/data-table/components/TableBody.vue @@ -1,7 +1,7 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/src/ui/data-table/components/TableHeader.vue b/src/ui/data-table/components/TableHeader.vue index f60f2c86..a1be3331 100644 --- a/src/ui/data-table/components/TableHeader.vue +++ b/src/ui/data-table/components/TableHeader.vue @@ -4,9 +4,10 @@
@@ -17,12 +18,19 @@ col.sort ? 'opacity-100' : 'opacity-0', col.sort === 'asc' && 'text-blue-500', col.sort === 'desc' && 'rotate-180 text-blue-500' - ]" + ]" icon="MoveUp" size="16">
+ + +
+
+
@@ -33,6 +41,7 @@ import type { ColumnProps } from '../types' import { Size, TablePaddingSize } from '@/ui/data-table/size.ts' import ShadcnIcon from '@/ui/icon' import { calcSize } from '@/utils/common.ts' +import { useResize } from '../hooks/useResize' withDefaults(defineProps<{ columns: ColumnProps[], @@ -48,4 +57,6 @@ const emits = defineEmits<{ const handleSort = (column: ColumnProps, event: MouseEvent) => { emits('on-sort-change', column, event) } + +const { handleMouseDown } = useResize() \ No newline at end of file diff --git a/src/ui/data-table/hooks/useResize.ts b/src/ui/data-table/hooks/useResize.ts new file mode 100644 index 00000000..aa9b82d1 --- /dev/null +++ b/src/ui/data-table/hooks/useResize.ts @@ -0,0 +1,63 @@ +import { onUnmounted, ref } from 'vue' +import type { ColumnProps } from '../types' + +export function useResize() +{ + const isResizing = ref(false) + const currentColumn = ref(null) + const startX = ref(0) + const startWidth = ref(0) + + const handleMouseDown = (e: MouseEvent, column: ColumnProps) => { + if (!column.resizable) { + return + } + + e.stopPropagation() + isResizing.value = true + currentColumn.value = column + startX.value = e.clientX + + const headerCell = (e.target as HTMLElement).closest('th') + if (headerCell) { + startWidth.value = headerCell.offsetWidth + } + + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing.value || !currentColumn.value) { + return + } + + e.preventDefault() + + const diffX = e.clientX - startX.value + const newWidth = Math.max(100, startWidth.value + diffX) + + if (currentColumn.value) { + currentColumn.value.width = `${ newWidth }px` + } + } + + const handleMouseUp = () => { + isResizing.value = false + currentColumn.value = null + + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + + onUnmounted(() => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + }) + + return { + isResizing, + currentColumn, + handleMouseDown + } +} \ No newline at end of file diff --git a/src/ui/data-table/types.ts b/src/ui/data-table/types.ts index b916596b..df4448d9 100644 --- a/src/ui/data-table/types.ts +++ b/src/ui/data-table/types.ts @@ -12,6 +12,7 @@ export interface ColumnProps width?: string // 宽度,支持输入数字和字符串会自动计算,如果是数字的情况时,单位为 px tooltip?: string align?: 'left' | 'center' | 'right' + resizable?: boolean } export interface DataTableProps From 118bad717f3d52a1c05f87f647b0b2d32de1ca2c Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Tue, 14 Jan 2025 09:28:35 +0800 Subject: [PATCH 09/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC):=20=E6=8B=86=E5=88=86=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E4=B8=BA=E5=8D=95=E7=8B=AC=E7=9A=84=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/data/data-table.md | 21 ++++ src/App.vue | 7 +- src/ui/data-table/ShadcnDataTable.vue | 9 +- src/ui/data-table/components/TableBody.vue | 112 ++++++------------- src/ui/data-table/components/TableHeader.vue | 59 +++++----- src/ui/data-table/hooks/useResize.ts | 34 +++++- src/ui/data-table/hooks/useTooltip.ts | 64 +++++++++++ src/ui/data-table/types.ts | 8 +- 8 files changed, 196 insertions(+), 118 deletions(-) create mode 100644 src/ui/data-table/hooks/useTooltip.ts diff --git a/docs/components/data/data-table.md b/docs/components/data/data-table.md index 88b4ac1b..9df48513 100644 --- a/docs/components/data/data-table.md +++ b/docs/components/data/data-table.md @@ -145,6 +145,17 @@ title: 数据表格 (Data Table) ::: +## 重置宽度 (resizable) + +::: raw + + + + + + +::: + ## 数据表格 (Data Table) 属性 @@ -179,6 +191,7 @@ title: 数据表格 (Data Table) :columns="[ ['on-sort', '当表格排序发生变化时触发', '排序列的列表'], ['on-cell-click', '当单元格被点击时触发', '行索引,列的 key 值,行数据'], + ['on-resizable', '当列宽度发生变化时触发', '当前列的信息,调整后列的宽度'], ]"> @@ -233,6 +246,14 @@ const alignColumns = ref([ { key: 'date', label: '发布日期', align: 'center' } ]) +const resizableColumns = ref([ + { key: 'title', label: '标题' }, + { key: 'author', label: '作者' }, + { key: 'description', label: '描述' }, + { key: 'category', label: '分类' }, + { key: 'date', label: '发布日期', resizable: true } +]) + const data = ref([ { title: '深入理解 Vue.js 响应式系统的原理与实现', diff --git a/src/App.vue b/src/App.vue index 9a48c804..660f0f8b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,7 +3,8 @@ + @on-cell-click="handleCellClick" + @on-resizable="handleResizable"> @@ -60,4 +61,8 @@ const data = ref([ const handleCellClick = (playload: { rowIndex: number; col: string; row: any }) => { console.log(`点击了第 ${ playload.rowIndex + 1 } 行,${ playload.col } 列`) } + +const handleResizable = (column: any, width: number) => { + console.log(`调整了 ${ column.label } 列的宽度为 ${ width }`) +} \ No newline at end of file diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index 49d65aeb..435e7763 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -1,10 +1,11 @@ diff --git a/src/ui/data-table/components/TableBody.vue b/src/ui/data-table/components/TableBody.vue index d93f498b..a0ebd990 100644 --- a/src/ui/data-table/components/TableBody.vue +++ b/src/ui/data-table/components/TableBody.vue @@ -1,41 +1,46 @@ \ No newline at end of file diff --git a/src/ui/data-table/components/TableHeader.vue b/src/ui/data-table/components/TableHeader.vue index a1be3331..c3d6322e 100644 --- a/src/ui/data-table/components/TableHeader.vue +++ b/src/ui/data-table/components/TableHeader.vue @@ -1,24 +1,27 @@ \ No newline at end of file diff --git a/src/ui/data-table/hooks/useResize.ts b/src/ui/data-table/hooks/useResize.ts index aa9b82d1..ba2d7637 100644 --- a/src/ui/data-table/hooks/useResize.ts +++ b/src/ui/data-table/hooks/useResize.ts @@ -1,12 +1,14 @@ import { onUnmounted, ref } from 'vue' import type { ColumnProps } from '../types' +import { calcSize } from '@/utils/common.ts' -export function useResize() +export function useResize(emit: (event: 'on-resizable', column: ColumnProps, width: number) => void) { const isResizing = ref(false) const currentColumn = ref(null) const startX = ref(0) const startWidth = ref(0) + const resizeElement = ref(null) const handleMouseDown = (e: MouseEvent, column: ColumnProps) => { if (!column.resizable) { @@ -18,9 +20,12 @@ export function useResize() currentColumn.value = column startX.value = e.clientX - const headerCell = (e.target as HTMLElement).closest('th') + // 获取当前拖拽的列元素 + const headerCell = (e.target as HTMLElement).closest('[data-resize-handle]')?.parentElement if (headerCell) { - startWidth.value = headerCell.offsetWidth + resizeElement.value = headerCell + // 修改这里:从 column.width 获取初始宽度,如果没有则使用默认值 + startWidth.value = column.width ? parseInt(column.width) : 150 } document.addEventListener('mousemove', handleMouseMove) @@ -28,23 +33,42 @@ export function useResize() } const handleMouseMove = (e: MouseEvent) => { - if (!isResizing.value || !currentColumn.value) { + if (!isResizing.value || !currentColumn.value || !resizeElement.value) { return } e.preventDefault() + // 计算鼠标移动的距离 const diffX = e.clientX - startX.value + + // 确保最小宽度 const newWidth = Math.max(100, startWidth.value + diffX) + // 直接设置列宽 if (currentColumn.value) { - currentColumn.value.width = `${ newWidth }px` + // 更新列的宽度属性 + currentColumn.value.width = `${ calcSize(newWidth) }` + + // 同时更新 DOM 元素的宽度 + if (resizeElement.value) { + resizeElement.value.style.width = `${ calcSize(newWidth) }` + } + + // 触发 on-resizable 事件 + emit('on-resizable', currentColumn.value, newWidth) } } const handleMouseUp = () => { + if (currentColumn.value) { + // 在松开鼠标时也触发一次事件,标记调整结束 + emit('on-resizable', currentColumn.value, parseInt(currentColumn.value.width || '150')) + } + isResizing.value = false currentColumn.value = null + resizeElement.value = null document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) diff --git a/src/ui/data-table/hooks/useTooltip.ts b/src/ui/data-table/hooks/useTooltip.ts new file mode 100644 index 00000000..eacfdbee --- /dev/null +++ b/src/ui/data-table/hooks/useTooltip.ts @@ -0,0 +1,64 @@ +import { onBeforeUnmount, onMounted, ref } from 'vue' +import { calcSize } from '@/utils/common.ts' + +export function useTooltip() +{ + let tooltipEl = ref(null) + + // 创建 tooltip 元素 + onMounted(() => { + tooltipEl.value = document.createElement('div') + tooltipEl.value.className = 'hidden fixed z-50 bg-gray-800 text-white text-sm rounded-lg py-2 px-3 max-w-xs whitespace-normal' + document.body.appendChild(tooltipEl.value) + }) + + // 清理 tooltip 元素 + onBeforeUnmount(() => { + tooltipEl.value?.remove() + }) + + // 显示 tooltip + const showTooltip = (event: MouseEvent, content: string) => { + if (!tooltipEl.value) { + return + } + + tooltipEl.value.textContent = content + tooltipEl.value.className = 'fixed z-50 bg-gray-800 text-white text-sm rounded-lg py-2 px-3 max-w-xs whitespace-normal' + + const tooltipRect = tooltipEl.value.getBoundingClientRect() + + // 计算位置,使 tooltip 在鼠标位置居中并默认显示在上方 + let left = event.clientX - (tooltipRect.width / 2) + let top = event.clientY - tooltipRect.height - 12 // 默认在鼠标上方,留出 12px 间距 + + // 检查右边界 + if (left + tooltipRect.width > window.innerWidth) { + left = window.innerWidth - tooltipRect.width - 8 + } + // 检查左边界 + if (left < 8) { + left = 8 + } + + // 如果上方空间不足,则显示在下方 + if (top < 8) { + top = event.clientY + 12 // 显示在鼠标下方 + } + + tooltipEl.value.style.left = `${ calcSize(left) }` + tooltipEl.value.style.top = `${ calcSize(top) }` + } + + // 隐藏 tooltip + const hideTooltip = () => { + if (tooltipEl.value) { + tooltipEl.value.className = 'hidden fixed z-50 bg-gray-800 text-white text-sm rounded-lg py-2 px-3 max-w-xs whitespace-normal' + } + } + + return { + showTooltip, + hideTooltip + } +} \ No newline at end of file diff --git a/src/ui/data-table/types.ts b/src/ui/data-table/types.ts index df4448d9..a10fbb8d 100644 --- a/src/ui/data-table/types.ts +++ b/src/ui/data-table/types.ts @@ -24,9 +24,15 @@ export interface DataTableProps export type DataTableEmits = { (e: 'on-sort', column: ColumnProps[]): void + (e: 'on-resizable', column: ColumnProps, width: number): void (e: 'on-cell-click', payload: { rowIndex: number; col: string; row: any }): void } -export type DataTableCellEmits = { +export type DataTableHeaderEmits = { + (e: 'on-sort', column: ColumnProps, event: MouseEvent): void + (e: 'on-resizable', column: ColumnProps, width: number): void +} + +export type DataTableBodyEmits = { (e: 'on-cell-click', payload: { rowIndex: number; col: string; row: any }): void } \ No newline at end of file From 85919d43086ba74a18adf810bfdfcd2e82f8586d Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Tue, 14 Jan 2025 09:42:55 +0800 Subject: [PATCH 10/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC):=20=E6=94=AF=E6=8C=81=E9=AB=98=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/data/data-table.md | 28 +++++++++++++++++++-------- src/App.vue | 6 ++++++ src/ui/data-table/ShadcnDataTable.vue | 6 ++++-- src/ui/data-table/types.ts | 1 + 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docs/components/data/data-table.md b/docs/components/data/data-table.md index 9df48513..528de1bb 100644 --- a/docs/components/data/data-table.md +++ b/docs/components/data/data-table.md @@ -90,11 +90,22 @@ title: 数据表格 (Data Table) ::: -## 排序 (sort) +## 高度 (height) ::: raw - + + + + + +::: + +## 列排序 (sort) + +::: raw + + @@ -123,33 +134,33 @@ title: 数据表格 (Data Table) ::: -## 显示提示 (tooltip) +## 列提示 (tooltip) ::: raw - + ::: -## 对齐方式 (align) +## 列对齐方式 (align) ::: raw - + ::: -## 重置宽度 (resizable) +## 列宽度调整 (resizable) ::: raw - + @@ -164,6 +175,7 @@ title: 数据表格 (Data Table) ['columns', '表格列数据', '\[\]', '\[\]', '-'], ['data', '表格数据', '\[\]', '\[\]', '-'], ['size', '表格尺寸', 'enum', 'default', 'default | small | large'], + ['height', '表格高度,支持输入数字和字符串会自动计算,如果是数字的情况时,单位为 px', 'string', 'auto', '-'], ]"> diff --git a/src/App.vue b/src/App.vue index 660f0f8b..ce2f9989 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,6 +3,7 @@ @@ -58,6 +59,11 @@ const data = ref([ } ]) +for (let i = 0; i < 1000; i++) { + const randomIndex = Math.floor(Math.random() * data.value.length) + data.value.push(data.value[randomIndex]) +} + const handleCellClick = (playload: { rowIndex: number; col: string; row: any }) => { console.log(`点击了第 ${ playload.rowIndex + 1 } 行,${ playload.col } 列`) } diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index 435e7763..4669521b 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index c063923e..011a3d7e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ @@ -25,9 +26,9 @@ setLocale('zh-CN') const columns = ref([ { key: 'index', label: '#', sortable: true, width: 50, align: 'center' }, - { key: 'title', label: '标题' }, + { key: 'title', label: '标题', editable: true }, { key: 'author', label: '作者', sortable: true, resizable: true }, - { key: 'description', label: '描述', width: 500, tooltip: true, resizable: true }, + { key: 'description', label: '描述', width: 500, tooltip: true, resizable: true, editable: true }, { key: 'category', label: '分类', sortable: true, align: 'center' }, { key: 'date', label: '发布日期', sortable: true } ]) diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index c4854e00..9dfa66b7 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -23,7 +23,8 @@ :selection-state="selectionState" :loading="loading" @on-cell-click="(payload) => emits('on-cell-click', payload as any)" - @on-row-select="(payload) => emits('on-row-select', payload as any)"> + @on-row-select="(payload) => emits('on-row-select', payload as any)" + @on-cell-edit="(payload) => emits('on-cell-edit', payload as any)"> \ No newline at end of file diff --git a/src/SelectEditor.vue b/src/SelectEditor.vue new file mode 100644 index 00000000..e4b22a45 --- /dev/null +++ b/src/SelectEditor.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/src/ui/data-table/components/TableBody.vue b/src/ui/data-table/components/TableBody.vue index 97b0b646..ee0f45dd 100644 --- a/src/ui/data-table/components/TableBody.vue +++ b/src/ui/data-table/components/TableBody.vue @@ -35,14 +35,21 @@ @@ -20,7 +22,7 @@ \ No newline at end of file diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 067f065b..f687c9e5 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -322,6 +322,8 @@ export default { dataTable: { text: { pageSize: 'Page size', + editCell: 'Edit cell', + editRow: 'Edit the entire line' } } } \ No newline at end of file diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 3dbeb18e..63a9ebcb 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -322,6 +322,8 @@ export default { dataTable: { text: { pageSize: '每页显示', + editCell: '编辑单元格', + editRow: '编辑整行' } } } \ No newline at end of file diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index 9dfa66b7..6cb8e061 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -22,9 +22,11 @@ :row-selection="rowSelection" :selection-state="selectionState" :loading="loading" + :context-menu="contextMenu" @on-cell-click="(payload) => emits('on-cell-click', payload as any)" @on-row-select="(payload) => emits('on-row-select', payload as any)" - @on-cell-edit="(payload) => emits('on-cell-edit', payload as any)"> + @on-cell-edit="(payload) => emits('on-cell-edit', payload as any)" + @on-row-edit="(payload) => emits('on-row-edit', payload as any)"> \ No newline at end of file diff --git a/src/ui/data-table/components/TableContextMenu.vue b/src/ui/data-table/components/TableContextMenu.vue new file mode 100644 index 00000000..f9ecb760 --- /dev/null +++ b/src/ui/data-table/components/TableContextMenu.vue @@ -0,0 +1,103 @@ + + + \ No newline at end of file diff --git a/src/ui/data-table/hooks/useContextMenu.ts b/src/ui/data-table/hooks/useContextMenu.ts new file mode 100644 index 00000000..a2db6ef7 --- /dev/null +++ b/src/ui/data-table/hooks/useContextMenu.ts @@ -0,0 +1,81 @@ +import { ref, Ref } from 'vue' +import { CellPayload, ColumnProps } from '../types' + +export interface ContextMenuPosition +{ + x: number + y: number +} + +export interface UseContextMenuReturn +{ + visible: Ref + position: Ref + actionsPosition: Ref + selectedValue: Ref + show: (event: MouseEvent, rowIndex: number, key: string, row: any, col: ColumnProps) => void + hide: (keepState?: boolean) => void + isVisible: () => boolean +} + +export const useContextMenu = (): UseContextMenuReturn => { + const visible = ref(false) + const position = ref({ x: 0, y: 0 }) + const actionsPosition = ref({ x: 0, y: 0 }) + const selectedCell = ref(null) + + const show = (event: MouseEvent, rowIndex: number, key: string, row: any, col: ColumnProps) => { + event.preventDefault() + + const x = event.clientX + const y = event.clientY + position.value = { x, y } + + const target = event.target as HTMLElement + const rect = target.getBoundingClientRect() + actionsPosition.value = { + x: rect.left, + y: rect.bottom + } + + selectedCell.value = { rowIndex, key, value: row[key], row, col } + visible.value = true + + const hideOnClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement + const contextMenu = document.querySelector('.context-menu') + + if (contextMenu && !contextMenu.contains(target)) { + hide(true) + document.removeEventListener('click', hideOnClickOutside) + } + } + + setTimeout(() => { + document.addEventListener('click', hideOnClickOutside) + }, 0) + } + + const hide = (keepState = false) => { + visible.value = false + + if (!keepState) { + position.value = { x: 0, y: 0 } + selectedCell.value = null + } + } + + const isVisible = (): boolean => { + return visible.value + } + + return { + visible, + position, + actionsPosition, + selectedValue: selectedCell, + show, + hide, + isVisible + } +} \ No newline at end of file diff --git a/src/ui/data-table/hooks/useEditable.ts b/src/ui/data-table/hooks/useEditable.ts index 94a2b8b4..44a85e7a 100644 --- a/src/ui/data-table/hooks/useEditable.ts +++ b/src/ui/data-table/hooks/useEditable.ts @@ -1,20 +1,36 @@ import { ref, Ref } from 'vue' -import { CellPayload } from '../types.ts' +import { CellPayload, ColumnProps, RowPayload } from '../types.ts' export interface UseEditableReturn { editingCell: Ref - startEditing: (rowIndex: number, key: string, value: any, row: any) => void - stopEditing: (value?: any) => CellPayload | null // 返回最后的编辑状态 + editingRowState: Ref + startEditing: (rowIndex: number, key: string, value: any, row: any, col: ColumnProps) => void + startRowEditing: (rowIndex: number, row: any) => void + stopEditing: (value?: any) => CellPayload | null + stopRowEditing: () => RowPayload | null updateValue: (value: any) => void + updateRowValue: (key: string, value: any) => void isEditing: (rowIndex: number, key: string) => boolean + isRowEditing: (rowIndex: number) => boolean } export const useEditable = (): UseEditableReturn => { const editingCell = ref(null) + const editingRowState = ref(null) - const startEditing = (rowIndex: number, key: string, value: any, row: any) => { - editingCell.value = { rowIndex, key, value, row } + const startEditing = (rowIndex: number, key: string, value: any, row: any, col: ColumnProps) => { + editingCell.value = { rowIndex, key, value, row, col } + editingRowState.value = null + } + + const startRowEditing = (rowIndex: number, row: any) => { + editingRowState.value = { + rowIndex, + row: { ...row }, // 创建原始行数据的深拷贝 + values: { ...row } // 创建用于编辑的数据副本 + } + editingCell.value = null } const stopEditing = (newValue?: any) => { @@ -30,24 +46,58 @@ export const useEditable = (): UseEditableReturn => { return lastEditState } + const stopRowEditing = () => { + if (!editingRowState.value) { + return null + } + + const { rowIndex, row, values } = editingRowState.value + + // 创建包含最新值的状态 + const finalState = { + rowIndex, + row: { ...row }, // 保留原始行数据 + values: { ...values } // 返回修改后的数据副本 + } + + // 清除编辑状态 + editingRowState.value = null + return finalState + } + const updateValue = (value: any) => { if (editingCell.value) { editingCell.value.value = value } } + const updateRowValue = (key: string, value: any) => { + if (editingRowState.value) { + editingRowState.value.values[key] = value + } + } + const isEditing = (rowIndex: number, key: string): boolean => { return ( - editingCell.value?.rowIndex === rowIndex && - editingCell.value?.key === key + (editingCell.value?.rowIndex === rowIndex && editingCell.value?.key === key) || + isRowEditing(rowIndex) ) } + const isRowEditing = (rowIndex: number): boolean => { + return editingRowState.value?.rowIndex === rowIndex + } + return { editingCell, + editingRowState, startEditing, + startRowEditing, stopEditing, + stopRowEditing, updateValue, - isEditing + updateRowValue, + isEditing, + isRowEditing } } \ No newline at end of file diff --git a/src/ui/data-table/index.ts b/src/ui/data-table/index.ts index 456e98f1..fa649650 100644 --- a/src/ui/data-table/index.ts +++ b/src/ui/data-table/index.ts @@ -1 +1,3 @@ -export { default as ShadcnDataTable } from './ShadcnDataTable.vue' \ No newline at end of file +export { default as ShadcnDataTable } from './ShadcnDataTable.vue' +export { default as TableCellInputEditor } from './components/TableCellInputEditor.vue' +export { default as TableCellSelectEditor } from './components/TableCellSelectEditor.vue' \ No newline at end of file diff --git a/src/ui/data-table/types.ts b/src/ui/data-table/types.ts index d7633df0..bc7bb083 100644 --- a/src/ui/data-table/types.ts +++ b/src/ui/data-table/types.ts @@ -6,7 +6,8 @@ export type RowSelectionMode = 'singleRow' | 'multipleRow' export type CellClickPayload = { rowIndex: number; col: string; row: any } | null export type RowSelectPayload = { rowIndex: number; row: any, selected: boolean, selectedRows: any[] } | null -export type CellPayload = { rowIndex: number; key: string; value: any; row: any } +export type CellPayload = { rowIndex: number; key: string; value: any; row: any, col: ColumnProps } +export type RowPayload = { rowIndex: number, row: any, values: Record } export enum TextAlign { @@ -50,6 +51,7 @@ export interface DataTableProps pagination?: PaginationProps rowSelection?: RowSelectionMode columnMove?: boolean // 是否允许移动列,移动后可以调整位置 + contextMenu?: boolean } export type DataTableHeaderEmits = { @@ -68,12 +70,14 @@ export type DataTableEmits = { (e: 'on-row-select', payload: RowSelectPayload): void (e: 'on-column-move', columns: ColumnProps[]): void (e: 'on-cell-edit', payload: CellPayload): void + (e: 'on-row-edit', payload: RowPayload): void } export type DataTableBodyEmits = { (e: 'on-cell-click', payload: CellClickPayload): void (e: 'on-row-select', payload: RowSelectPayload): void (e: 'on-cell-edit', payload: CellPayload): void + (e: 'on-row-edit', payload: RowPayload): void } export type DataTablePaginationEmits = { From 66c7c1cb40ef6c00f4ec929b3d6aba858f7077e9 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 15 Jan 2025 00:10:31 +0800 Subject: [PATCH 24/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC):=20=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=8F=B3=E9=94=AE=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/data/data-table.md | 1 + src/App.vue | 5 +++++ src/ui/data-table/ShadcnDataTable.vue | 9 +++++++++ src/ui/data-table/components/TableBody.vue | 10 +++++++++- src/ui/data-table/components/TableContextMenu.vue | 10 +++++++++- 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/components/data/data-table.md b/docs/components/data/data-table.md index 885bbe8a..93d0ff97 100644 --- a/docs/components/data/data-table.md +++ b/docs/components/data/data-table.md @@ -347,6 +347,7 @@ title: 数据表格 (Data Table) :columns="[ ['loading', '加载中的插槽', '-'], ['empty', '空数据的插槽', '-'], + ['contextMenu', '右键菜单的插槽', '{ visible, position, selectedValue, actionsPosition }
{菜单是否可见, 当前位置, 选中的值, 操作菜单的位置}'], ]"> diff --git a/src/App.vue b/src/App.vue index aaf1ff8f..c89f805e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,6 +15,11 @@ @on-column-move="console.log('列移动 ' + JSON.stringify($event))" @on-cell-edit="onCellEdit" @on-row-edit="onRowEdit"> + diff --git a/src/ui/data-table/ShadcnDataTable.vue b/src/ui/data-table/ShadcnDataTable.vue index 6cb8e061..12fba7ef 100644 --- a/src/ui/data-table/ShadcnDataTable.vue +++ b/src/ui/data-table/ShadcnDataTable.vue @@ -44,6 +44,15 @@ + + diff --git a/src/ui/data-table/components/TableBody.vue b/src/ui/data-table/components/TableBody.vue index a52e0e04..f7ecd897 100644 --- a/src/ui/data-table/components/TableBody.vue +++ b/src/ui/data-table/components/TableBody.vue @@ -83,7 +83,15 @@ v-show="contextMenuState.visible.value" :context-menu-state="contextMenuState" :editable-state="editableState" - @on-row-edit="(val) => handleSaveRowEdit(val)">> + @on-row-edit="(val) => handleSaveRowEdit(val)"> + diff --git a/src/ui/data-table/components/TableContextMenu.vue b/src/ui/data-table/components/TableContextMenu.vue index f9ecb760..e84466be 100644 --- a/src/ui/data-table/components/TableContextMenu.vue +++ b/src/ui/data-table/components/TableContextMenu.vue @@ -8,6 +8,13 @@ {{ t('dataTable.text.editRow') }} + + +
+ contextMenuState: any editableState: ReturnType }>() From b3c10797e3f2f1d91638c4a8e1ef77f34aaa3d27 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 15 Jan 2025 09:33:59 +0800 Subject: [PATCH 25/27] =?UTF-8?q?=E7=BB=84=E4=BB=B6(=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=A1=86):=20=E4=BF=AE=E5=A4=8D=E6=9C=AA=E8=AE=BE=E7=BD=AE=20n?= =?UTF-8?q?ame=20=E5=AD=97=E6=AE=B5=E5=AF=BC=E8=87=B4=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8F=B0=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/components/form/input.md | 23 +++++++++++++++- packages/index.ts | 4 +-- src/App.vue | 4 +++ .../components/TableCellInputEditor.vue | 2 +- .../components/TableContextMenu.vue | 17 ++++++------ src/ui/input/ShadcnInput.vue | 22 ++++------------ src/ui/input/index.ts | 4 +-- src/ui/input/types.ts | 26 +++++++++++++++++++ .../components/ShadcnWorkflowPanel.vue | 2 +- 9 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 src/ui/input/types.ts diff --git a/docs/components/form/input.md b/docs/components/form/input.md index 58cc6689..1b55012c 100644 --- a/docs/components/form/input.md +++ b/docs/components/form/input.md @@ -234,6 +234,21 @@ const input = ref('Hello View Shadcn UI') ::: +## 表单 (form) + +::: raw + + + + + + + 提交 + + + +::: + ## 输入框 (Input) 属性 \ No newline at end of file diff --git a/packages/index.ts b/packages/index.ts index ef05b3ac..4e8e8581 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -9,7 +9,7 @@ import ShadcnButton from '@/ui/button' import ShadcnButtonGroup from '@/ui/button/group' import ShadcnCard from '@/ui/card' import { ShadcnCopy } from '@/ui/copy' -import ShadcnInput from '@/ui/input' +import { ShadcnInput } from '@/ui/input' import ShadcnIcon from '@/ui/icon' import ShadcnModal from '@/ui/modal' import ShadcnTooltip from '@/ui/tooltip' @@ -239,7 +239,7 @@ export { default as ShadcnButtonGroup } from '@/ui/button/group' export { default as ShadcnCard } from '@/ui/card' export { ShadcnCopy } from '@/ui/copy' export { ShadcnCodeEditor } from '@/ui/code-editor' -export { default as ShadcnInput } from '@/ui/input' +export { ShadcnInput } from '@/ui/input' export { default as ShadcnIcon } from '@/ui/icon' export { default as ShadcnModal } from '@/ui/modal' export { default as ShadcnTooltip } from '@/ui/tooltip' diff --git a/src/App.vue b/src/App.vue index c89f805e..597b89a3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,7 @@