|
@@ -0,0 +1,447 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ class="handsontable-container"
|
|
|
+ :style="borderStyle"
|
|
|
+ v-loading="loading"
|
|
|
+ element-loading-background="rgba(245, 245, 245, 0.5)"
|
|
|
+ >
|
|
|
+ <div class="table-wrapper" ref="handsontableRef"></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+import { computed, defineComponent, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue';
|
|
|
+import Handsontable from '@sc/handsontable';
|
|
|
+import { TreeNode } from '@sc/tree';
|
|
|
+import debounce from 'lodash/debounce';
|
|
|
+import { off } from '@/utils/frontend/dom';
|
|
|
+import useClickPosition from '@/components/handsontable/composables/useClickPosition';
|
|
|
+import useInstance from './composables/useInstance';
|
|
|
+import useTableSettings, { getColumnsSettings, getTableData, overwriteReadOnly } from './composables/useTableSettings';
|
|
|
+import './renderer/registerRenderers';
|
|
|
+// import './cell-types/registerCellTypes';
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: 'Handsontable',
|
|
|
+ props: {
|
|
|
+ // 表格的数据
|
|
|
+ data: {
|
|
|
+ type: Array,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ // 表格的设置选项
|
|
|
+ settings: {
|
|
|
+ type: Object,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ // 是否是树结构,即每行数据是否是 `TreeNode`
|
|
|
+ tree: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ // 边框, none 表示没有边框
|
|
|
+ border: {
|
|
|
+ type: String,
|
|
|
+ default: 'top,right,bottom,left',
|
|
|
+ },
|
|
|
+ // 整个表格是否只读
|
|
|
+ readOnly: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ // 是否处于 loading 状态
|
|
|
+ loading: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ // 模板引用
|
|
|
+ const handsontableRef = ref<HTMLElement | null>(null);
|
|
|
+
|
|
|
+ const hotProps = props as Hot.IHandsontableProps;
|
|
|
+ const tableSettings = useTableSettings(hotProps);
|
|
|
+
|
|
|
+ let rawData = props.data as any;
|
|
|
+
|
|
|
+ // Handsontable 实例
|
|
|
+ let instance: Handsontable | null;
|
|
|
+
|
|
|
+ type RowHeaders = boolean | ((visualRow: number) => number);
|
|
|
+ const getRowHeader = (): RowHeaders => {
|
|
|
+ if (tableSettings.rowHeaders !== undefined) return tableSettings.rowHeaders as any;
|
|
|
+ return props.tree
|
|
|
+ ? (visualRow: number) => {
|
|
|
+ if (instance) {
|
|
|
+ const rowSourceData = instance.getSourceDataAtRow(instance.toPhysicalRow(visualRow)) as TreeNode;
|
|
|
+ return rowSourceData.getCtx().row() + 1;
|
|
|
+ }
|
|
|
+ return visualRow;
|
|
|
+ }
|
|
|
+ : true;
|
|
|
+ };
|
|
|
+ // 重新渲染,供外界调用
|
|
|
+ const Render = () => instance && instance.render();
|
|
|
+
|
|
|
+ const onResize = debounce(() => Render(), 100);
|
|
|
+
|
|
|
+ // 监听 mousedownPosition 事件的处理器
|
|
|
+ let clickPositionHandler: any;
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ const wrapperDOM = handsontableRef.value as HTMLElement;
|
|
|
+ // handsontable 实例
|
|
|
+ instance = useInstance(wrapperDOM, tableSettings);
|
|
|
+
|
|
|
+ // 获取鼠标点击的区域相关代码
|
|
|
+ clickPositionHandler = useClickPosition(wrapperDOM, tableSettings);
|
|
|
+
|
|
|
+ // 设置行号
|
|
|
+ (() => {
|
|
|
+ const rowHeaders = getRowHeader();
|
|
|
+ instance.updateSettings({ rowHeaders }, false);
|
|
|
+ })();
|
|
|
+
|
|
|
+ // 改变窗口大小监听事件
|
|
|
+ window.addEventListener('resize', onResize);
|
|
|
+ });
|
|
|
+
|
|
|
+ const { readOnly, tree } = toRefs(props);
|
|
|
+
|
|
|
+ // 重新加载数据,供外界调用
|
|
|
+ const Load = (data?: any) => {
|
|
|
+ if (data) rawData = data;
|
|
|
+ instance && instance.loadData(getTableData(tree.value, rawData));
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取 instance,供外界调用
|
|
|
+ const GetInstance = () => instance;
|
|
|
+
|
|
|
+ // 更新 Settings,供外界调用(如增加或减少列)
|
|
|
+ const Update = (newTableSettings: Hot.ISettings) => {
|
|
|
+ const { columnsMeta } = newTableSettings;
|
|
|
+ const columnsData = getColumnsSettings(columnsMeta);
|
|
|
+ let newSettings;
|
|
|
+ if (columnsData) {
|
|
|
+ newSettings = { ...columnsData, ...newTableSettings };
|
|
|
+ delete newSettings.columnsMeta;
|
|
|
+ } else {
|
|
|
+ newSettings = newTableSettings;
|
|
|
+ }
|
|
|
+
|
|
|
+ overwriteReadOnly(newSettings, readOnly.value);
|
|
|
+
|
|
|
+ if (newSettings.rowHeaders === undefined)
|
|
|
+ // 设置行号
|
|
|
+ newSettings.rowHeaders = getRowHeader();
|
|
|
+ instance && instance.updateSettings(newSettings, false);
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(readOnly, () => {
|
|
|
+ Update(props.settings);
|
|
|
+ });
|
|
|
+ watch(tree, () => {
|
|
|
+ const newSettings = props.settings;
|
|
|
+ const { columnsMeta } = newSettings;
|
|
|
+ columnsMeta &&
|
|
|
+ columnsMeta.forEach((columnMeta: any) => {
|
|
|
+ if (columnMeta.renderer === 'wc.switcherRenderer') {
|
|
|
+ delete columnMeta.renderer;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ Update(newSettings);
|
|
|
+ Load();
|
|
|
+ });
|
|
|
+ // 组件 unmount 之前销毁 handsontable 实例
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ if (instance) {
|
|
|
+ window.removeEventListener('resize', onResize);
|
|
|
+
|
|
|
+ // 移除所有的监听器
|
|
|
+ // Handsontable.hooks.destroy && Handsontable.hooks.destroy(instance);
|
|
|
+ instance.destroy();
|
|
|
+ instance = null;
|
|
|
+ }
|
|
|
+ if (clickPositionHandler) off(document.body, 'mouseup', clickPositionHandler);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 边框样式
|
|
|
+ const borderStyle = computed(() => {
|
|
|
+ if (props.border === 'none') {
|
|
|
+ return {
|
|
|
+ border: 'none',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const borders = props.border.split(',');
|
|
|
+ const style = {} as any;
|
|
|
+ borders.forEach(border => {
|
|
|
+ style[`border-${border}`] = '1px solid #d4d4d4';
|
|
|
+ });
|
|
|
+ return style;
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ handsontableRef,
|
|
|
+ borderStyle,
|
|
|
+ Render,
|
|
|
+ Update,
|
|
|
+ Load,
|
|
|
+ GetInstance,
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.handsontable-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ @include scrollbar-handsontable;
|
|
|
+}
|
|
|
+
|
|
|
+.table-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ ::v-deep > .ht_master {
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .ht_clone_top_left_corner,
|
|
|
+ ::v-deep > .ht_clone_top {
|
|
|
+ .htCore {
|
|
|
+ .real-th {
|
|
|
+ position: relative;
|
|
|
+ overflow: visible;
|
|
|
+
|
|
|
+ &.ht__highlight {
|
|
|
+ .relative {
|
|
|
+ background-color: #dad8d6 !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .relative {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ padding: 0;
|
|
|
+ width: 100%;
|
|
|
+ background-color: $smoke;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #dfdfdf !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .ht_clone_bottom {
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .ht_clone_left {
|
|
|
+ // overflow: visible !important;
|
|
|
+
|
|
|
+ .wtHolder {
|
|
|
+ width: 100% !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .htCore {
|
|
|
+ transition: box-shadow 0.3s ease;
|
|
|
+
|
|
|
+ &.shadow {
|
|
|
+ box-shadow: 4px 0 4px -3px rgba(0, 0, 0, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ tbody {
|
|
|
+ // 这段代码会导致小屏幕上缩放分辨率时,表格行错位的问题
|
|
|
+ /*th {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }*/
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .ht_clone_top_left_corner {
|
|
|
+ .htCore {
|
|
|
+ thead {
|
|
|
+ tr {
|
|
|
+ th {
|
|
|
+ border-color: $gainsboro;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .handsontableInputHolder {
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .ht_master,
|
|
|
+ ::v-deep > .ht_clone_top,
|
|
|
+ ::v-deep > .ht_clone_bottom,
|
|
|
+ ::v-deep > .ht_clone_left,
|
|
|
+ ::v-deep > .ht_clone_top_left_corner,
|
|
|
+ ::v-deep > .handsontableInputHolder {
|
|
|
+ .htCore {
|
|
|
+ td,
|
|
|
+ th {
|
|
|
+ // 当前选中行的样式
|
|
|
+ &.current-row {
|
|
|
+ background-color: $light-sky-blue !important;
|
|
|
+ }
|
|
|
+ &.show-highlight {
|
|
|
+ background-color: #c1e1fc !important;
|
|
|
+ }
|
|
|
+ // 当前选中单元格的样式
|
|
|
+ &.current {
|
|
|
+ &.highlight {
|
|
|
+ background-color: #c1e1fc !important;
|
|
|
+
|
|
|
+ .custom-button {
|
|
|
+ display: inline !important;
|
|
|
+ vertical-align: middle;
|
|
|
+ margin: -1px 0 0 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ border-color: $gainsboro !important;
|
|
|
+ background-color: $smoke !important;
|
|
|
+ color: $black;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #dfdfdf !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.ht__highlight {
|
|
|
+ background-color: #dad8d6 !important;
|
|
|
+
|
|
|
+ .colHeader,
|
|
|
+ .rowHeader {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #2577c2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ tbody {
|
|
|
+ tr {
|
|
|
+ &.hover {
|
|
|
+ td {
|
|
|
+ background-color: $ghost-grey;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ td {
|
|
|
+ transition: background-color 0.15s;
|
|
|
+ border-color: $gainsboro;
|
|
|
+ color: $black;
|
|
|
+
|
|
|
+ &.bg-danger {
|
|
|
+ background-color: #fbe3e3;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.font-danger {
|
|
|
+ color: $danger;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.wcSingleLine {
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.htMiddle {
|
|
|
+ vertical-align: middle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .htBorders {
|
|
|
+ .wtBorder.corner {
|
|
|
+ cursor: cell;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 没有左边框
|
|
|
+ ::v-deep > .ht_clone_left {
|
|
|
+ .htCore {
|
|
|
+ th {
|
|
|
+ border-left: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep > .ht_clone_top_left_corner {
|
|
|
+ .htCore {
|
|
|
+ thead {
|
|
|
+ th:first-child {
|
|
|
+ border-left: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 没有上边框
|
|
|
+ ::v-deep > .ht_master,
|
|
|
+ ::v-deep > .ht_clone_top,
|
|
|
+ ::v-deep > .ht_clone_bottom,
|
|
|
+ ::v-deep > .ht_clone_left,
|
|
|
+ ::v-deep > .ht_clone_top_left_corner,
|
|
|
+ ::v-deep > .handsontableInputHolder {
|
|
|
+ .htCore {
|
|
|
+ thead {
|
|
|
+ tr {
|
|
|
+ th {
|
|
|
+ border-top: none !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /*tbody {
|
|
|
+ tr {
|
|
|
+ td {
|
|
|
+ border-left: none !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }*/
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep .red {
|
|
|
+ color: red !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::v-deep .green {
|
|
|
+ color: green !important;
|
|
|
+ }
|
|
|
+ ::v-deep .stateCheckBox::after {
|
|
|
+ content: "";
|
|
|
+ display: block;
|
|
|
+ width: 7px;
|
|
|
+ height: 7px;
|
|
|
+ background-color: #007bff;
|
|
|
+ transform: translate(3px, 3px);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调整列大小的手柄
|
|
|
+ ::v-deep .manualColumnResizer {
|
|
|
+ cursor: ew-resize;
|
|
|
+ transform: translateX(3px);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调整行大小的手柄
|
|
|
+ ::v-deep .manualRowResizer {
|
|
|
+ cursor: ns-resize;
|
|
|
+ transform: translateY(3px);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|