diff --git a/dbm-ui/frontend/src/components/editable-table/Column.vue b/dbm-ui/frontend/src/components/editable-table/Column.vue
new file mode 100644
index 0000000000..c275ae8eeb
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/Column.vue
@@ -0,0 +1,461 @@
+
+
+
+ |
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/Index.vue b/dbm-ui/frontend/src/components/editable-table/Index.vue
new file mode 100644
index 0000000000..29d2e2ecca
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/Index.vue
@@ -0,0 +1,409 @@
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/Row.vue b/dbm-ui/frontend/src/components/editable-table/Row.vue
new file mode 100644
index 0000000000..6f22c1d74c
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/Row.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/component/RenderColGroup.vue b/dbm-ui/frontend/src/components/editable-table/component/RenderColGroup.vue
new file mode 100644
index 0000000000..0bf83fbce9
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/RenderColGroup.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/component/render-body/Index.vue b/dbm-ui/frontend/src/components/editable-table/component/render-body/Index.vue
new file mode 100644
index 0000000000..7d265f1541
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/render-body/Index.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/component/render-body/render-cell.ts b/dbm-ui/frontend/src/components/editable-table/component/render-body/render-cell.ts
new file mode 100644
index 0000000000..099aab1f6c
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/render-body/render-cell.ts
@@ -0,0 +1,29 @@
+import { defineComponent, h } from 'vue';
+
+import type { IContext as IColumnContext } from '../../Column.vue';
+
+export default defineComponent({
+ name: 'RenderColumnCell',
+ props: {
+ column: {
+ type: Object as () => IColumnContext,
+ required: true,
+ },
+ },
+ setup(props) {
+ return () =>
+ h(
+ 'td',
+ {
+ class: 'bk-editable-table-body-column',
+ },
+ h(
+ 'div',
+ {
+ class: 'bk-editable-table-cell',
+ },
+ props.column.slots.default(),
+ ),
+ );
+ },
+});
diff --git a/dbm-ui/frontend/src/components/editable-table/component/render-header/Index.vue b/dbm-ui/frontend/src/components/editable-table/component/render-header/Index.vue
new file mode 100644
index 0000000000..d667c2bee9
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/render-header/Index.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/component/render-header/render-th.ts b/dbm-ui/frontend/src/components/editable-table/component/render-header/render-th.ts
new file mode 100644
index 0000000000..73f8fdc10e
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/render-header/render-th.ts
@@ -0,0 +1,81 @@
+import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
+
+import type { IContext as IColumnContext } from '../../Column.vue';
+
+export default defineComponent({
+ name: 'RenderColumnHead',
+ props: {
+ column: {
+ type: Object as () => IColumnContext,
+ required: true,
+ },
+ },
+ setup(props) {
+ return () => {
+ const childNode = [
+ withDirectives(
+ h(
+ 'div',
+ {
+ class: {
+ 'bk-editable-table-th-text': true,
+ 'bk-editable-table-th-text-description': Boolean(props.column.props.description),
+ },
+ },
+ props.column.slots.head ? props.column.slots.head() : props.column.props.label || '',
+ ),
+ [
+ [
+ resolveDirective('bk-tooltips'),
+ {
+ content: props.column.props.description || '',
+ disabled: !props.column.props.description,
+ },
+ ],
+ ],
+ ),
+ ];
+
+ if (!props.column.slots.head && props.column.slots.headPrepend) {
+ childNode.unshift(
+ h(
+ 'div',
+ {
+ class: 'bk-editable-table-th-prepend',
+ },
+ props.column.slots.headPrepend(),
+ ),
+ );
+ }
+
+ if (!props.column.slots.head && props.column.slots.headAppend) {
+ childNode.push(
+ h(
+ 'div',
+ {
+ class: 'bk-editable-table-th-append',
+ },
+ props.column.slots.headAppend(),
+ ),
+ );
+ }
+ return h(
+ 'th',
+ {
+ class: {
+ 'bk-editable-table-header-column': true,
+ 'is-required': props.column.props.required,
+ },
+ 'data-name': props.column.key,
+ },
+ h(
+ 'div',
+ {
+ class: 'bk-editable-table-label-cell',
+ },
+ childNode,
+ ),
+ );
+ };
+ },
+});
diff --git a/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/Index.vue b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/Index.vue
new file mode 100644
index 0000000000..16ce4af95f
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/Index.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-box-state.ts b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-box-state.ts
new file mode 100644
index 0000000000..5a69f2a790
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-box-state.ts
@@ -0,0 +1,79 @@
+/*
+ * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
+ *
+ * Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at https://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+ * the specific language governing permissions and limitations under the License.
+ */
+
+import { type ComponentInternalInstance, getCurrentInstance, reactive } from 'vue';
+
+import type { IContext } from '../index.vue';
+
+export default function () {
+ const { proxy } = getCurrentInstance() as ComponentInternalInstance & { proxy: IContext };
+
+ const state = reactive({
+ contentScrollHeight: 0,
+ contentScrollWidth: 0,
+ isRenderVerticalScroll: false,
+ isRenderHorizontalScrollbar: false,
+ styles: {
+ width: '',
+ height: '',
+ },
+ });
+
+ const initState = () => {
+ if (proxy.$refs.scrollBox && proxy.$refs.scrollContent) {
+ const { scrollHeight, scrollWidth } = proxy.$refs.scrollContent as Element;
+ state.contentScrollHeight = scrollHeight;
+ state.contentScrollWidth = scrollWidth;
+
+ const { width: boxWidth, height: boxHeight } = proxy.$refs.scrollBox.getBoundingClientRect();
+ // 内容区高度大于容器高度显示垂直滚动条
+ state.isRenderVerticalScroll = Math.ceil(state.contentScrollHeight) > Math.ceil(boxHeight);
+ // 内容区宽度大于容器宽度显示水平滚动条
+ state.isRenderHorizontalScrollbar = Math.ceil(state.contentScrollWidth) > Math.ceil(boxWidth);
+ const styles = {
+ width: '100%',
+ height: '100%',
+ maxHeight: '',
+ maxWidth: '',
+ };
+ // 计算滚动容器的展示宽高
+ const {
+ height: scrollBoxStyleHeight,
+ maxHeight: scrollBoxStyleMaxHeight,
+ width: scrollBoxStyleWidth,
+ maxWidth: scrollBoxStyleMaxWidth,
+ } = proxy.$refs.scrollBox.style;
+ if (state.isRenderVerticalScroll) {
+ if (scrollBoxStyleHeight) {
+ styles.height = scrollBoxStyleHeight;
+ } else if (scrollBoxStyleMaxHeight) {
+ styles.maxHeight = scrollBoxStyleMaxHeight;
+ }
+ }
+ if (state.isRenderHorizontalScrollbar) {
+ if (scrollBoxStyleWidth) {
+ styles.width = scrollBoxStyleWidth;
+ } else if (scrollBoxStyleMaxWidth) {
+ styles.maxWidth = scrollBoxStyleMaxWidth;
+ }
+ }
+
+ state.styles = Object.freeze(styles);
+ }
+ };
+
+ return {
+ state,
+ initState,
+ };
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-content.ts b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-content.ts
new file mode 100644
index 0000000000..e896f4731a
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-content.ts
@@ -0,0 +1,36 @@
+/*
+ * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
+ *
+ * Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at https://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+ * the specific language governing permissions and limitations under the License.
+ */
+
+import { ref } from 'vue';
+
+export default function () {
+ const isContentScroll = ref(false);
+ /**
+ * @desc 鼠标在内容区状态
+ */
+ const mouseenter = () => {
+ isContentScroll.value = true;
+ };
+ /**
+ * @desc 鼠标离开内容区状态
+ */
+ const mouseleave = () => {
+ isContentScroll.value = false;
+ };
+
+ return {
+ isContentScroll,
+ mouseenter,
+ mouseleave,
+ };
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-horizotal.ts b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-horizotal.ts
new file mode 100644
index 0000000000..c753d046b2
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-horizotal.ts
@@ -0,0 +1,36 @@
+/*
+ * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
+ *
+ * Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at https://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+ * the specific language governing permissions and limitations under the License.
+ */
+
+import { ref } from 'vue';
+
+export default function () {
+ const isHorizontalScroll = ref(false);
+ /**
+ * @desc 鼠标在水平滚动条区域
+ */
+ const mouseenter = () => {
+ isHorizontalScroll.value = true;
+ };
+ /**
+ * @desc 鼠标离开水平滚动条区域
+ */
+ const mouseleave = () => {
+ isHorizontalScroll.value = false;
+ };
+
+ return {
+ isHorizontalScroll,
+ mouseenter,
+ mouseleave,
+ };
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-vertical.ts b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-vertical.ts
new file mode 100644
index 0000000000..8c3a59160a
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/component/scroll-faker/hooks/use-vertical.ts
@@ -0,0 +1,36 @@
+/*
+ * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
+ *
+ * Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at https://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
+ * the specific language governing permissions and limitations under the License.
+ */
+
+import { ref } from 'vue';
+
+export default function () {
+ const isVerticalScroll = ref(false);
+ /**
+ * @desc 鼠标在垂直滚动条区域
+ */
+ const mouseenter = () => {
+ isVerticalScroll.value = true;
+ };
+ /**
+ * @desc 鼠标离开垂直滚动条区域
+ */
+ const mouseleave = () => {
+ isVerticalScroll.value = false;
+ };
+
+ return {
+ isVerticalScroll,
+ mouseenter,
+ mouseleave,
+ };
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/constants.ts b/dbm-ui/frontend/src/components/editable-table/constants.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/DatePicker.vue b/dbm-ui/frontend/src/components/editable-table/edit/DatePicker.vue
new file mode 100644
index 0000000000..a7fc6f07ed
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/DatePicker.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/Input.vue b/dbm-ui/frontend/src/components/editable-table/edit/Input.vue
new file mode 100644
index 0000000000..a530bb0a3e
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/Input.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/Select.vue b/dbm-ui/frontend/src/components/editable-table/edit/Select.vue
new file mode 100644
index 0000000000..d6ee9b6a5e
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/Select.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/TagInput.vue b/dbm-ui/frontend/src/components/editable-table/edit/TagInput.vue
new file mode 100644
index 0000000000..3f1d6ee34c
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/TagInput.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/Text.vue b/dbm-ui/frontend/src/components/editable-table/edit/Text.vue
new file mode 100644
index 0000000000..31b85e4bfc
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/Text.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+ {{ modelValue }}
+
+
+
+ {{ placeholder }}
+
+
+
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/Textarea.vue b/dbm-ui/frontend/src/components/editable-table/edit/Textarea.vue
new file mode 100644
index 0000000000..d8aaf10a66
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/Textarea.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/edit/TimePicker.vue b/dbm-ui/frontend/src/components/editable-table/edit/TimePicker.vue
new file mode 100644
index 0000000000..472b801390
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/edit/TimePicker.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/dbm-ui/frontend/src/components/editable-table/hooks/use-column-tips.ts b/dbm-ui/frontend/src/components/editable-table/hooks/use-column-tips.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/dbm-ui/frontend/src/components/editable-table/hooks/use-resize.ts b/dbm-ui/frontend/src/components/editable-table/hooks/use-resize.ts
new file mode 100644
index 0000000000..f0967d53df
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/hooks/use-resize.ts
@@ -0,0 +1,194 @@
+import _ from 'lodash';
+import { onBeforeUnmount, onMounted, type Ref, ref, watch } from 'vue';
+
+import type { IContext as IColumnContext } from '../Column.vue';
+
+export default function (
+ tableRef: Ref,
+ tableColumnResizeRef: Ref,
+ columnList: Ref,
+) {
+ let dragable = false;
+
+ const columnSizeConfig = ref<
+ Record<
+ string,
+ {
+ width: number;
+ minWidth: number;
+ maxWidth: number;
+ renderWidth: number;
+ }
+ >
+ >({});
+
+ const dragging = ref(false);
+ const dragState = ref({
+ startMouseLeft: 0,
+ startLeft: 0,
+ startColumnLeft: 0,
+ tableLeft: 0,
+ });
+
+ watch(
+ columnList,
+ () => {
+ const columnSizeConfigCache = { ...columnSizeConfig.value };
+ columnSizeConfig.value = columnList.value.reduce((result, columnConfig) => {
+ const width = columnConfig.props.width || 0;
+ const minWidth =
+ Number(columnConfig.props.minWidth) > 60 ? Number(columnConfig.props.minWidth) : Number.MIN_VALUE;
+ const maxWidth =
+ Number(columnConfig.props.maxWidth) > 60 ? Number(columnConfig.props.maxWidth) : Number.MAX_VALUE;
+
+ const renderWidth = Math.min(Math.max(width, minWidth), maxWidth);
+
+ Object.assign(result, {
+ [columnConfig.key]: columnSizeConfigCache[columnConfig.key]
+ ? columnSizeConfigCache[columnConfig.key]
+ : {
+ width,
+ minWidth,
+ maxWidth,
+ renderWidth,
+ },
+ });
+
+ return result;
+ }, {});
+ console.log('columnSizeConfig.value = ', columnSizeConfig.value);
+ },
+ {
+ immediate: true,
+ },
+ );
+
+ const handleMouseDown = (event: MouseEvent) => {
+ if (!dragable) {
+ return;
+ }
+ dragging.value = true;
+
+ const tableEl = tableRef.value;
+ const tableLeft = tableEl!.getBoundingClientRect().left;
+ const columnEl = event.target as HTMLElement;
+ const columnRect = columnEl!.getBoundingClientRect();
+
+ const columnKey = columnEl.dataset.name as string;
+
+ const minLeft = columnRect.left - tableLeft + 30;
+
+ dragState.value = {
+ startMouseLeft: event.clientX,
+ startLeft: columnRect.right - tableLeft,
+ startColumnLeft: columnRect.left - tableLeft,
+ tableLeft,
+ };
+ const resizeProxy = tableColumnResizeRef.value as HTMLElement;
+ resizeProxy.style.display = 'block';
+ resizeProxy.style.left = `${dragState.value.startLeft}px`;
+
+ document.onselectstart = function () {
+ return false;
+ };
+ document.ondragstart = function () {
+ return false;
+ };
+
+ const handleMouseMove = (event: MouseEvent) => {
+ const deltaLeft = event.clientX - dragState.value.startMouseLeft;
+ const proxyLeft = dragState.value.startLeft + deltaLeft;
+ resizeProxy.style.display = 'block';
+ resizeProxy.style.left = `${Math.max(minLeft, proxyLeft)}px`;
+ };
+
+ const handleMouseUp = () => {
+ if (dragging.value) {
+ const containerWidth = tableEl!.getBoundingClientRect().width;
+ const containerScrollWidth = tableEl!.scrollWidth;
+ const isScrolling = containerScrollWidth > containerWidth;
+ const { startColumnLeft } = dragState.value;
+ const finalLeft = Number.parseInt(resizeProxy.style.left, 10);
+ const latestColumnWidth = Math.ceil(Math.max(finalLeft - startColumnLeft, 60));
+
+ const nextSiblingEl = columnEl!.nextElementSibling as HTMLElement;
+
+ if (nextSiblingEl!.classList.contains('table-column-resize')) {
+ return;
+ }
+ // 没有出现滚动条时缩小当前列的宽度同时会放大后面一列的宽度
+ if (!isScrolling && columnRect.width > latestColumnWidth) {
+ const latestWidth = columnRect.width - latestColumnWidth + nextSiblingEl!.getBoundingClientRect().width;
+ console.log('latestWidth = ', latestWidth);
+ }
+
+ resizeProxy.style.display = 'none';
+ document.body.style.cursor = '';
+ dragging.value = false;
+
+ const realWidth = Math.max(
+ columnSizeConfig.value[columnKey].minWidth as number,
+ Math.min(latestColumnWidth, columnSizeConfig.value[columnKey].maxWidth as number),
+ );
+ columnSizeConfig.value[columnKey].renderWidth = realWidth;
+ }
+
+ dragable = false;
+
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.onselectstart = null;
+ document.ondragstart = null;
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ };
+
+ const handleMouseMove = (event: MouseEvent) => {
+ const target = (event.target as HTMLElement).closest('th');
+
+ if (!target) {
+ return;
+ }
+
+ const rect = target!.getBoundingClientRect();
+
+ const bodyStyle = document.body.style;
+ if (rect.width > 12 && rect.right - event.pageX < 16) {
+ bodyStyle.cursor = 'col-resize';
+ bodyStyle.userSelect = 'none';
+ dragable = true;
+ } else if (!dragging.value) {
+ bodyStyle.cursor = '';
+ bodyStyle.userSelect = '';
+ dragable = false;
+ }
+ };
+
+ const handleOuterMousemove = _.throttle((event: Event) => {
+ let i = event.composedPath().length - 1;
+ while (i >= 0) {
+ const target = event.composedPath()[i] as HTMLElement;
+ if (target.classList && target.classList.contains('bk-editable-table')) {
+ return;
+ }
+ i = i - 1;
+ }
+ document.body.style.cursor = '';
+ }, 500);
+
+ onMounted(() => {
+ document.addEventListener('mousemove', handleOuterMousemove);
+ });
+
+ onBeforeUnmount(() => {
+ document.removeEventListener('mousemove', handleOuterMousemove);
+ });
+
+ return {
+ columnSizeConfig,
+ handleMouseDown,
+ handleMouseMove,
+ };
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/hooks/use-scroll.ts b/dbm-ui/frontend/src/components/editable-table/hooks/use-scroll.ts
new file mode 100644
index 0000000000..c9030713a2
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/hooks/use-scroll.ts
@@ -0,0 +1,57 @@
+import _ from 'lodash';
+import { onBeforeUnmount, onMounted, type Ref, shallowRef } from 'vue';
+
+export default function (tableContentRef: Ref) {
+ const leftFixedStyles = shallowRef({});
+ const rightFixedStyles = shallowRef({});
+
+ const handleHorizontalScroll = _.throttle(() => {
+ const tableEl = tableContentRef.value as HTMLElement;
+ const { scrollLeft } = tableEl;
+ const tableContentWidth = tableEl.getBoundingClientRect().width;
+ const tableWidth = tableEl.querySelector('table')!.getBoundingClientRect().width;
+ if (scrollLeft === 0) {
+ leftFixedStyles.value = {
+ display: 'none',
+ };
+ } else {
+ const fixedColumns = tableEl.querySelectorAll('th.is-column-fixed-left');
+ const fixedWidth = Array.from(fixedColumns).reduce(
+ (result, itemEl) => result + itemEl.getBoundingClientRect().width,
+ 0,
+ );
+ leftFixedStyles.value = {
+ width: `${fixedWidth}px`,
+ };
+ }
+
+ if (tableContentWidth + scrollLeft >= tableWidth) {
+ rightFixedStyles.value = {
+ display: 'none',
+ };
+ } else {
+ const fixedRightColumns = tableEl.querySelectorAll('th.is-column-fixed-right');
+ const fixeRightdWidth = Array.from(fixedRightColumns).reduce(
+ (result, itemEl) => result + itemEl.getBoundingClientRect().width,
+ 0,
+ );
+ rightFixedStyles.value = {
+ width: `${fixeRightdWidth}px`,
+ };
+ }
+ }, 30);
+
+ onMounted(() => {
+ const tableEl = tableContentRef.value as HTMLElement;
+ tableEl.addEventListener('scroll', handleHorizontalScroll);
+ onBeforeUnmount(() => {
+ tableEl.removeEventListener('scroll', handleHorizontalScroll);
+ });
+ });
+
+ return {
+ leftFixedStyles,
+ rightFixedStyles,
+ initalScroll: handleHorizontalScroll,
+ };
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/types.ts b/dbm-ui/frontend/src/components/editable-table/types.ts
new file mode 100644
index 0000000000..627b8f6f9d
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/types.ts
@@ -0,0 +1,11 @@
+export interface IRule {
+ required?: boolean;
+ email?: boolean;
+ min?: number;
+ max?: number;
+ maxlength?: number;
+ pattern?: RegExp;
+ validator?: (value: any) => Promise | boolean | string;
+ message: (() => string) | string;
+ trigger: string;
+}
diff --git a/dbm-ui/frontend/src/components/editable-table/useColumn.ts b/dbm-ui/frontend/src/components/editable-table/useColumn.ts
new file mode 100644
index 0000000000..075c4cb9b3
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/useColumn.ts
@@ -0,0 +1,22 @@
+import { inject } from 'vue';
+
+import { EditableTableColumnKey } from './Column.vue';
+import { type IRule } from './types';
+
+export default (
+ options = {} as {
+ rules?: IRule[];
+ },
+) => {
+ const columnContext = inject(EditableTableColumnKey);
+
+ // if (!columnContext) {
+ // throw new Error('not found EditColumn');
+ // }
+
+ if (columnContext && options.rules) {
+ columnContext.registerRules(options.rules);
+ }
+
+ return columnContext;
+};
diff --git a/dbm-ui/frontend/src/components/editable-table/validator.ts b/dbm-ui/frontend/src/components/editable-table/validator.ts
new file mode 100644
index 0000000000..c9912af514
--- /dev/null
+++ b/dbm-ui/frontend/src/components/editable-table/validator.ts
@@ -0,0 +1,20 @@
+import isDate from 'lodash/isDate';
+import isEmpty from 'lodash/isEmpty';
+
+export default {
+ required: (value: any): boolean => {
+ if (typeof value === 'number' || typeof value === 'boolean' || isDate(value)) {
+ return true;
+ }
+ return !isEmpty(value);
+ },
+ min: (value: number, min: number): boolean => value >= min,
+ max: (value: number, max: number): boolean => max >= value,
+ email: (value: string): boolean => /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,4}$/.test(value),
+ maxlength: (value: string, maxlength: number): boolean => value.length <= maxlength,
+ pattern: (value: string, pattern: RegExp): boolean => {
+ const result = pattern.test(value);
+ pattern.lastIndex = 0; // eslint-disable-line no-param-reassign
+ return result;
+ },
+};