فهرست منبع

feat: handsontable 组件树结构及测试

qinlaiqiao 3 سال پیش
والد
کامیت
780fae7b4d

+ 12 - 11
src/components/handsontable/Handsontable.vue

@@ -15,10 +15,11 @@ 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 useClickPosition from './composables/useClickPosition';
 import useInstance from './composables/useInstance';
 import useTableSettings, { getColumnsSettings, getTableData, overwriteReadOnly } from './composables/useTableSettings';
 import './renderer/registerRenderers';
+import { Hot } from '@/types/components';
 // import './cell-types/registerCellTypes';
 
 export default defineComponent({
@@ -206,8 +207,8 @@ export default defineComponent({
   height: 100%;
   overflow: hidden;
 
-  ::v-deep > .ht_master {
-  }
+  /* ::v-deep > .ht_master {
+  } */
 
   ::v-deep > .ht_clone_top_left_corner,
   ::v-deep > .ht_clone_top {
@@ -238,8 +239,8 @@ export default defineComponent({
     }
   }
 
-  ::v-deep > .ht_clone_bottom {
-  }
+  /* ::v-deep > .ht_clone_bottom {
+  } */
 
   ::v-deep > .ht_clone_left {
     // overflow: visible !important;
@@ -255,14 +256,14 @@ export default defineComponent({
         box-shadow: 4px 0 4px -3px rgba(0, 0, 0, 0.2);
       }
 
-      tbody {
+      /* tbody {
         // 这段代码会导致小屏幕上缩放分辨率时,表格行错位的问题
-        /*th {
+        th {
           display: flex;
           align-items: center;
           justify-content: center;
-        }*/
-      }
+        }
+      } */
     }
   }
 
@@ -278,9 +279,9 @@ export default defineComponent({
     }
   }
 
-  ::v-deep > .handsontableInputHolder {
+  /*  ::v-deep > .handsontableInputHolder {
   }
-
+ */
   ::v-deep > .ht_master,
   ::v-deep > .ht_clone_top,
   ::v-deep > .ht_clone_bottom,

+ 1 - 0
src/components/handsontable/composables/useClickPosition.ts

@@ -1,3 +1,4 @@
+import { Hot } from '@/types/components';
 import { on, query } from '@/utils/frontend/dom';
 
 // 获取鼠标点击的区域相关代码

+ 0 - 46
src/components/handsontable/composables/useFixedLeftScroll.ts

@@ -1,46 +0,0 @@
-import debounce from 'lodash/debounce';
-
-// 水平滚动添加阴影
-export default function useFixedLeftScroll(tableWrapper: HTMLElement): { registerFixedLeftScroll: () => void } {
-  // handsontable 主体部分元素
-  let $holder: HTMLElement | null;
-
-  // fixed Left 的核心部分元素
-  let $leftCore: HTMLElement | null;
-
-  // 滚动监听器防抖
-  const debouncedScroll = debounce(
-    () => {
-      if (!$holder || !$leftCore) return;
-      if ($holder.scrollLeft <= 0) {
-        $leftCore.classList.remove('shadow');
-      } else {
-        $leftCore.classList.add('shadow');
-      }
-    },
-    100,
-    { maxWait: 100 }
-  );
-
-  // 注册滚动监听器,用于添加fixed columns阴影
-  const registerFixedLeftScroll = () => {
-    // handsontable wrapper
-    const $wrapper = tableWrapper;
-    // handsontable 主体部分的内容的 holder
-    $holder = $wrapper.querySelector('.ht_master .wtHolder');
-    // fixed Left 部分的核心部分
-    $leftCore = $wrapper.querySelector('.ht_clone_left .htCore');
-    $holder && $holder.addEventListener('scroll', debouncedScroll);
-  };
-
-  // 注销滚动监听器
-  // const unregisterScroll = () => {
-  //   if ($holder) {
-  //     console.log('被注销');
-  //     $holder.removeEventListener('scroll', debouncedScroll);
-  //   }
-  // };
-  return {
-    registerFixedLeftScroll,
-  };
-}

+ 0 - 57
src/components/handsontable/composables/useHoverChangeRowBg.ts

@@ -1,57 +0,0 @@
-interface AdditionalInfoHTMLElement extends HTMLTableRowElement {
-  index: number;
-  correspondingList: HTMLCollectionOf<HTMLTableRowElement> | null;
-}
-
-export default function useHoverChangeRowBg(tableWrapper: HTMLElement) {
-  let $leftTrList: HTMLCollectionOf<HTMLTableRowElement> | null;
-  let $masterTrList: HTMLCollectionOf<HTMLTableRowElement> | null;
-
-  const mouseOverHandler = (e: Event) => {
-    const target = e.currentTarget as AdditionalInfoHTMLElement;
-    target.correspondingList &&
-      target.correspondingList[target.index] &&
-      target.correspondingList[target.index].classList.add('hover');
-    target && target.classList.add('hover');
-  };
-  const mouseOutHandler = (e: Event) => {
-    const target = e.currentTarget as AdditionalInfoHTMLElement;
-    target.correspondingList &&
-      target.correspondingList[target.index] &&
-      target.correspondingList[target.index].classList.remove('hover');
-    target && target.classList.remove('hover');
-  };
-
-  const registerHover = () => {
-    // handsontable wrapper
-    const $wrapper = tableWrapper;
-    const $leftTBody = $wrapper.querySelector('.ht_clone_left .htCore tbody');
-    $leftTrList = $leftTBody && $leftTBody.getElementsByTagName('tr');
-
-    const $masterTBody = $wrapper.querySelector('.ht_master .htCore tbody');
-    $masterTrList = $masterTBody && $masterTBody.getElementsByTagName('tr');
-
-    if ($masterTrList) {
-      for (let i = 0; i < $masterTrList.length; i++) {
-        const $tr = $masterTrList[i] as AdditionalInfoHTMLElement;
-        $tr.index = i;
-        $tr.correspondingList = $leftTrList;
-        $tr.onmouseover = mouseOverHandler;
-        $tr.onmouseout = mouseOutHandler;
-      }
-    }
-    if ($leftTrList) {
-      for (let i = 0; i < $leftTrList.length; i++) {
-        const $tr = $leftTrList[i] as AdditionalInfoHTMLElement;
-        $tr.index = i;
-        $tr.correspondingList = $masterTrList;
-        $tr.onmouseover = mouseOverHandler;
-        $tr.onmouseout = mouseOutHandler;
-      }
-    }
-  };
-
-  return {
-    registerHover,
-  };
-}

+ 2 - 7
src/components/handsontable/composables/useInstance.ts

@@ -1,14 +1,9 @@
 import Handsontable from '@sc/handsontable';
-// import useFixedLeftScroll from './useFixedLeftScroll';
-// import useHoverChangeRowBg from './useHoverChangeRowBg';
 import useVerticalScroll from './useVerticalScroll';
 import useGroupingHeaders from './useGroupingHeaders';
 
 export default function useInstance(tableWrapper: HTMLElement, settings: any) {
-  // 注册左侧水平固定滚动监听器
-  // const { registerFixedLeftScroll } = useFixedLeftScroll(tableWrapper);
-  // 注册 tr hover监听器(hover时改变行背景)
-  // const { registerHover } = useHoverChangeRowBg(tableWrapper);
+  
   // 注册垂直滚动监听器(用于大数据量快速滚动导致错位的问题:停止后,手动触发滚动1px)
   const { registerVerticalScroll } = useVerticalScroll(tableWrapper);
   if (settings.cellGrid) {
@@ -30,7 +25,7 @@ export default function useInstance(tableWrapper: HTMLElement, settings: any) {
     // 视口之外预渲染 1 行 此处设为 0 会导致树结构展开收起失效
     viewportRowRenderingOffset: 30,
     // 视口之外预渲染 1 列 此处设为 0 会导致树结构展开收起失效
-    viewportColumnRenderingOffset: 1,
+    viewportColumnRenderingOffset: 2,
 
     selectionMode: 'range',
     /*  rowHeights: 23, */

+ 1 - 0
src/components/handsontable/composables/useTableSettings.ts

@@ -1,3 +1,4 @@
+import { Hot } from '@/types/components';
 import { TreeNode } from '@sc/tree';
 import cloneDeep from 'lodash/cloneDeep';
 

+ 1 - 0
src/components/handsontable/renderer/createRenderer.ts

@@ -1,5 +1,6 @@
 import Handsontable, { GridSettings } from '@sc/handsontable';
 import { addClass, append, empty, style } from '@/utils/frontend/dom';
+import { Hot } from '@/types/components';
 
 export default function createRenderer(renderer: Hot.IRenderer) {
   // eslint-disable-next-line func-names

+ 2 - 1
src/components/handsontable/renderer/index.ts

@@ -12,10 +12,11 @@ import shareProjectRenderer from './renderers/shareProjectRenderer';
 // import toUsersRenderer from './renderers/toUsersRenderer';
 import indentRender from './renderers/indentRender';
 import multipleLineRenderer from './renderers/multipleLineRenderer';
+import { Hot } from '@/types/components';
 
 const rendererRegisterList: Hot.RendererItem[] = [
   // renderer 函数
-  ['wc.switcherRenderer', switcherRenderer as Hot.IRenderer],
+  ['wc.switcherRenderer', switcherRenderer],
   ['wc.recoverRenderer', recoverRenderer],
   ['wc.deleteRenderer', deleteRenderer],
   ['wc.deleteRenderer.priceFile', deleteRendererPriceFile],

+ 1 - 0
src/components/handsontable/renderer/renderers/buttonRenderer.ts

@@ -1,3 +1,4 @@
+import { Hot } from '@/types/components';
 import { on, parse, query, style } from '@/utils/frontend/dom';
 
 type Callback = (...args: any) => void;

+ 0 - 33
src/components/handsontable/renderer/renderers/selectRender.ts

@@ -1,33 +0,0 @@
-import { empty } from '@/utils/frontend/dom';
-import { cellTypes, GridSettings } from '@sc/handsontable';
-import { find } from 'lodash';
-
-export default function selectRender(
-  instance: Handsontable,
-  TD: HTMLElement,
-  row: number,
-  col: number,
-  prop: string | number,
-  value: any,
-  cellProperties: GridSettings
-) {
-  // 调用dropdown绑定相关事件
-  cellTypes.dropdown.renderer(instance, TD, row, col, prop, '', cellProperties);
-  const { source } = cellProperties;
-  const option = find(source, { value });
-  if (option) value = option.key;
-
-  const container = document.createElement('div');
-  container.className = 'wcDropdown';
-  const textDiv = document.createElement('div');
-  textDiv.className = 'wcDropdownText';
-  textDiv.appendChild(document.createTextNode(value || ''));
-  const ARROW = TD.firstChild;
-  if (cellProperties.readOnly !== true) container.appendChild(ARROW as HTMLElement);
-
-  container.appendChild(textDiv);
-  empty(TD);
-  TD.appendChild(container);
-
-  return container;
-}

+ 18 - 0
src/composables/useHotRef.ts

@@ -0,0 +1,18 @@
+import { Hot } from '@/types/components';
+import { ref, Ref } from 'vue';
+
+// handsontable 组件的模板引用相关代码
+export default function useHotRef() {
+  // 用于定义模板引用
+  const handsontableRef = () => {
+    return ref<Hot.IHandsontableComponent | null>(null);
+  };
+  // 断言非 null
+  const notNull = (hotRef: Ref<Hot.IHandsontableComponent | null>) => {
+    return hotRef.value as Hot.IHandsontableComponent;
+  };
+  return {
+    getHotRef: handsontableRef,
+    notNull,
+  };
+}

+ 76 - 0
src/constants/tmp/table-columns-meta.ts

@@ -0,0 +1,76 @@
+/* subject 中 revise 的 columns meta */
+const subjectRevise = [
+  { title: '项目编码', data: 'code', renderer: 'wc.switcherRenderer', readOnly: true, width: 170 },
+  { title: '项目名称', data: 'name', width: 200 },
+  { title: '计量单位', data: 'unit', width: 60 },
+  { title: '工程量', data: 'count', type: 'numeric', width: 70 },
+  {
+    title: '综合单价(初始)',
+    data: 'initPrice',
+    type: 'numeric',
+    numericFormat: { pattern: '0,0.00' },
+    width: 100,
+  },
+  {
+    title: '综合合价(初始)',
+    data: 'initAllPrice',
+    type: 'numeric',
+    numericFormat: { pattern: '0,0.00' },
+    width: 100,
+  },
+  {
+    title: '综合单价(目标)',
+    data: 'targetPrice',
+    type: 'numeric',
+    numericFormat: { pattern: '0,0.00' },
+    width: 100,
+  },
+  {
+    title: '综合合价(目标)',
+    data: 'targetAllPrice',
+    type: 'numeric',
+    numericFormat: { pattern: '0,0.00' },
+    width: 100,
+  },
+  {
+    title: '综合单价(调整)',
+    data: 'adjustPrice',
+    type: 'numeric',
+    numericFormat: { pattern: '0,0.00' },
+    width: 100,
+  },
+  {
+    title: '综合合价(调整)',
+    data: 'adjustAllPrice',
+    type: 'numeric',
+    numericFormat: { pattern: '0,0.00' },
+    width: 100,
+  },
+  {
+    title: '人工',
+    data: 'labour',
+    type: 'numeric',
+    width: 80,
+  },
+  {
+    title: '材料',
+    data: 'material',
+    type: 'numeric',
+    width: 80,
+  },
+  {
+    title: '机械',
+    data: 'mechanics',
+    type: 'numeric',
+    width: 80,
+  },
+  {
+    title: '主材',
+    data: 'mainMaterial',
+    type: 'numeric',
+    width: 80,
+  },
+  { title: '设备', data: 'device', type: 'numeric', width: 80 },
+  { title: '子目工程量调整系数', data: 'coe', type: 'numeric', width: 80 },
+];
+export default subjectRevise;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
src/constants/tmp/table-data.json


+ 201 - 13
src/styles/_main.scss

@@ -7,7 +7,7 @@ html {
     width: 100%;
     height: 100%;
     font-family: "微软雅黑", "Microsoft YaHei", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial,
-    sans-serif;
+      sans-serif;
     -webkit-font-smoothing: antialiased; /*chrome、safari*/
     -moz-osx-font-smoothing: grayscale; /*firefox*/
     -webkit-text-size-adjust: none;
@@ -26,22 +26,210 @@ html {
   scrollbar-track-color: transparent;
 }
 
-::-webkit-scrollbar {
-  width: 8px;
-  height: 8px;
+@include scrollbar-normal;
+
+/* handsontable 树画线 */
+.ht-switcher-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+
+  &:hover {
+    .tail-icon-wrap {
+      opacity: 1 !important;
+    }
+  }
+
+  .ht-switcher-wrap {
+    display: flex;
+    height: 100%;
+    align-items: center;
+
+    .head-icon-wrap,
+    .text-icon-wrap,
+    .text-behind-icon-wrap {
+      display: inline-flex;
+      align-items: center;
+      padding-right: 2px !important;
+
+      > .iconfont {
+        line-height: 15px;
+        font-size: 16px;
+        width: 15px;
+        height: 15px;
+        color: $dark-blue;
+      }
+    }
+
+    > .text {
+      display: inline-flex;
+      align-items: center;
+      margin-left: 5px;
+      font-size: 0;
+
+      > .text-icon-wrap,
+      > .text-behind-icon-wrap {
+        padding: 0;
+
+        > .iconfont {
+          margin-right: 1px;
+        }
+      }
+
+      > .text-behind-icon-wrap {
+        margin-left: 3px;
+      }
+
+      > .content {
+        font-size: 14px;
+      }
+    }
+
+    > .tail-icon-wrap {
+      transition: opacity 0.1s;
+      /* opacity: 0; */
+      padding-left: 6px;
+      position: absolute;
+      right: 0;
+      color: $dark-blue;
+
+      > .iconfont {
+        /* cursor: pointer; */
+        font-size: 16px;
+        margin-right: 3px;
+        padding: 3px;
+        /* background-color: $white; */
+        border-radius: 50%;
+
+        &:hover {
+          color: #0062cc;
+        }
+      }
+    }
+
+    // 线
+    > .tree-line-wrap {
+      display: inline-flex;
+      height: 100%;
+
+      > .tree-line {
+        width: 1px;
+        height: 100%;
+        background-color: $light-grey;
+        margin: 0 8px 0 6px;
+      }
+    }
+
+    // 切换按钮 icon
+    > .toggle-icon-wrap {
+      position: relative;
+      width: 16px;
+      height: 100%;
+
+      > .toggle-icon {
+        display: inline-block;
+        position: absolute;
+        left: 0;
+        top: 50%;
+        transform: translateY(-50%);
+
+        &.ht-toggle-icon-expand,
+        &.ht-toggle-icon-shrink {
+          width: 12px;
+          height: 12px;
+          border: 1px solid $black;
+          cursor: pointer;
+          color: $black;
+          background-color: $white;
+        }
+
+        &.ht-toggle-icon-expand {
+          &::before {
+            position: absolute;
+            left: 0;
+            top: -1.5px;
+            content: "+";
+            font-size: 13px;
+            line-height: 12px;
+          }
+        }
+
+        &.ht-toggle-icon-shrink {
+          &::before {
+            position: absolute;
+            left: 2px;
+            top: 5px;
+            content: " ";
+            width: 6px;
+            height: 1px;
+            background-color: $black;
+          }
+        }
+      }
+
+      > .toggle-icon-top-line,
+      > .toggle-icon-right-line,
+      > .toggle-icon-bottom-line {
+        position: absolute;
+        background-color: $black;
+      }
+
+      > .toggle-icon-top-line {
+        top: 0;
+        left: 6px;
+        width: 1px;
+        height: 50%;
+        background-color: $light-grey;
+      }
+
+      > .toggle-icon-right-line {
+        right: 0;
+        top: 50%;
+        margin-top: -0.5px;
+        width: 10px;
+        height: 1px;
+        background-color: $light-grey;
+      }
+
+      > .toggle-icon-bottom-line {
+        bottom: 0;
+        left: 6px;
+        width: 1px;
+        height: 50%;
+        background-color: $light-grey;
+      }
+    }
+  }
 }
 
-::-webkit-scrollbar-track {
-  background-color: transparent;
+/* handsontable indent renderer*/
+.ht-indent-renderer {
+  display: flex;
+  align-items: center;
+  height: 100%;
 }
 
-::-webkit-scrollbar-thumb {
-  min-width: 50px;
-  min-height: 50px;
-  background-color: #c1c1c1;
-  border-radius: 2px;
+.handsontable.listbox td {
+  border-color: transparent !important;
 }
 
-::-webkit-scrollbar-thumb:hover {
-  background-color: rgba(0, 0, 0, 0.46);
+//自定义下拉框样式
+.wcDropdown {
+  display: flex;
+  flex-direction: row-reverse;
+
+  .wcDropdownText {
+    overflow: hidden;
+    flex: 1;
+  }
+
+  .htAutocompleteArrow {
+    display: none;
+  }
+}
+
+.current {
+  .htAutocompleteArrow {
+    display: block;
+  }
 }

+ 31 - 2
src/styles/_mixin.scss

@@ -1,8 +1,8 @@
 // 函数示例
-@function px2rem($px) {
+/* @function px2rem($px) {
   $rem: 14px;
   @return ($px/$rem) + rem;
-}
+} */
 
 @mixin dropdown-menu($width: auto, $min-width: auto, $submenu-min-width: auto) {
   z-index: 2050; /* 没有这行则 抽屉无法显示右键菜单 */
@@ -246,6 +246,33 @@
   }
 }
 
+// 普通的滚动条
+@mixin scrollbar-normal {
+  * {
+    scrollbar-width: thin;
+    scrollbar-track-color: transparent;
+  }
+  ::-webkit-scrollbar {
+    width: 8px;
+    height: 8px;
+  }
+
+  ::-webkit-scrollbar-track {
+    background-color: transparent;
+  }
+
+  ::-webkit-scrollbar-thumb {
+    min-width: 50px;
+    min-height: 50px;
+    background-color: #c1c1c1;
+    border-radius: 2px;
+  }
+
+  ::-webkit-scrollbar-thumb:hover {
+    background-color: rgba(0, 0, 0, 0.46);
+  }
+}
+
 // handsontable 滚动条
 @mixin scrollbar-handsontable {
   ::-webkit-scrollbar {
@@ -274,6 +301,8 @@
   ::-webkit-scrollbar-thumb {
     min-width: 50px;
     min-height: 50px;
+    max-width: 80px;
+    max-height: 80px;
     background-color: $light-grey;
     border-radius: 1px;
   }

+ 13 - 0
src/types/components.d.ts

@@ -1,3 +1,5 @@
+import Handsontable, { GridSettings } from '@sc/handsontable';
+
 declare namespace ContextMenu {
     interface IContextMenuComponent {
         // 隐藏
@@ -49,6 +51,17 @@ type Renderer = (
 ) => HTMLElement;
 
 declare namespace Hot {
+    interface IHandsontableComponent {
+        // 重新渲染(如容器宽高发生改变),供外界调用
+        Render: () => void;
+        // 更新 Settings,供外界调用(如增加或减少列)
+        Update: (settings: any) => void;
+        // 重新加载数据(如引用的数据发生改变),供外界调用
+        Load: (data?: any) => void;
+        // 获取 instance,供外界调用
+        GetInstance: () => Handsontable;
+    }
+
     interface ICoords {
         startRow: number;
         startCol: number;

+ 0 - 167
src/utils/frontend/http.ts

@@ -1,167 +0,0 @@
-// eslint-disable-next-line import/no-cycle
-import compilationStore from '@/store/modules/compilation';
-import userStore from '@/store/modules/user';
-import { clearTab } from '@/store/clear';
-/**
- * axios封装
- * 请求拦截、响应拦截、错误统一处理
- */
-import { IResult, LoginType } from '@sc/types';
-import axios from 'axios';
-import { get as getCookie } from 'js-cookie';
-import router from '@/router';
-import { getShareLinkFromHref, isShareHref } from './share';
-// eslint-disable-next-line import/no-cycle
-import enterpriseStore from '@/store/modules/enterprise';
-
-/**
- * 请求失败后的错误统一处理
- * @param {Number} errno 请求失败的错误码
- * @param message 错误信息
- */
-const handleError = async (errno: number, message: string) => {
-  // 状态码判断
-  switch (errno) {
-    // 401: 未登录状态,跳转登录页
-    case 1009:
-      await router.replace('/login');
-      break;
-    // 没有费用定额权限
-    case 3005:
-      if (userStore.loginType === LoginType.ENTERPRISE) {
-        clearTab();
-        await router.replace('/workbench');
-      }
-      break;
-    // 分享链接失效
-    case 4005:
-      if (isShareHref()) window.location.reload();
-      break;
-    default:
-      console.log(message);
-  }
-};
-
-// 创建axios实例
-const instance = axios.create({ timeout: 1000 * 12, baseURL: process.env.VUE_APP_BASE_URL });
-// 设置post请求头
-// 暂时可不用
-// instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
-/**
- * 请求拦截器
- * 每次请求前,如果存在token则在请求头中携带token
- */
-instance.interceptors.request.use(
-  config => {
-    // 登录流程控制中,根据本地是否存在token判断用户的登录情况
-    // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
-    // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
-    // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。
-
-    // 分享链接,需要设置请求头,后端才可根据请求头进行校验等
-    const shareLink = getShareLinkFromHref();
-    if (shareLink) {
-      config.headers['Share-Link'] = shareLink;
-      // 分享链接的情况,请求头需要携带费用定额ID,因为现在后端大多数请求、获取数据库表依赖费用定额ID
-      // 注意:后续可考虑将后端所有方法,不依赖session里的compilationID、userID,全通过token的方式,这样更好维护,不过代码改动量比较大
-      if (compilationStore.compilationInfo.ID) {
-        config.headers.Compilation = compilationStore.compilationInfo.ID;
-      }
-    }
-
-    const csrfToken = getCookie('csrfToken');
-    // Cookies.get('csrfToken');
-    if (csrfToken) config.headers.post['x-csrf-token'] = csrfToken;
-
-    // 退出登录的时候将本地清空
-    if (config.url?.startsWith('/user/logout')) {
-      userStore.commitReset();
-      enterpriseStore.commitReset();
-    }
-    return config;
-  },
-  error => Promise.reject(error)
-);
-
-// 响应拦截器
-instance.interceptors.response.use(
-  // 请求成功
-  res => (res.status === 200 ? Promise.resolve(res) : Promise.reject(res)),
-  // 请求失败
-  async error => {
-    if (error.message && error.message.includes('timeout')) {
-      return Promise.reject(new Error('请求超时,请稍后再试'));
-    }
-    const { response } = error;
-    const {
-      data: { errno, message },
-    } = response;
-    if (response) {
-      await handleError(errno, message);
-      return Promise.reject(response);
-    }
-    return Promise.reject(error);
-  }
-);
-
-/**
- * get方法,对应get请求
- * @param {String} url [请求的url地址]
- * @param {Object} params [请求时携带的参数]
- * @param isPlain
- */
-export function get<T = any>(url: string, params?: any, isPlain = false): Promise<T> {
-  return new Promise((resolve, reject) => {
-    instance
-      .get(url, {
-        params,
-      })
-      .then(res => {
-        const result: IResult<T> = res.data;
-        if (result.errno > 0) {
-          reject(result);
-        } else {
-          // 有些外部的请求返回的数据接口跟项目中的接口格式是不一样的,这个时候设置isPlain为true,直接返回响应数据
-          resolve(isPlain ? (result as unknown as T) : result.data);
-        }
-      })
-      .catch(err => {
-        if (err instanceof Error) {
-          reject(err);
-        } else {
-          reject(err.data);
-        }
-      });
-  });
-}
-
-/**
- * post方法,对应post请求
- * @param {String} url [请求的url地址]
- * @param {Object} params [请求时携带的参数]
- * @param isPlain
- */
-export function post<T = any>(url: string, params?: any, isPlain = false): Promise<T> {
-  return new Promise((resolve, reject) => {
-    instance
-      .post(url, params)
-      .then(res => {
-        const result: IResult<T> = res.data;
-        if (result.errno > 0) {
-          reject(result);
-        } else {
-          // 有些外部的请求返回的数据接口跟项目中的接口格式是不一样的,这个时候设置isPlain为true,直接返回响应数据
-          resolve(isPlain ? (result as any as T) : result.data);
-        }
-      })
-      .catch(err => {
-        if (err instanceof Error) {
-          reject(err);
-        } else {
-          reject(err.data);
-        }
-      });
-  });
-}
-
-// export default instance;

+ 56 - 0
src/utils/frontend/tree.ts

@@ -0,0 +1,56 @@
+/* eslint-disable import/prefer-default-export */
+import { Tree, TreeNode } from '@sc/tree';
+
+// 展开所有的树节点
+export function expandAllTreeNode(treeData: TreeNode[]): void {
+  treeData.forEach(item => {
+    item.getCtx().expanded = true;
+  });
+}
+
+// ElTree节点结构
+export interface IElTreeNode {
+  id: string;
+  label: string;
+  icon?: string;
+  children?: IElTreeNode[];
+}
+
+// 获取其及其子项所有ELTree节点
+const getElTreeData = (node: TreeNode): IElTreeNode => {
+  const data: IElTreeNode = {
+    id: node.ID,
+    label: node.name,
+  };
+  const children = node.getCtx().children();
+  if (children.length > 0) {
+    const subNodes = [];
+    for (const c of children) {
+      subNodes.push(getElTreeData(c));
+    }
+    data.children = subNodes;
+  }
+  return data;
+};
+
+// 将@sc/tree里的树结构数据 转换 为el-tree里需要的树结构
+export const toElTreeData = (tree: Tree): IElTreeNode[] => {
+  const elTreeData: IElTreeNode[] = [];
+  const roots = tree.getRoots();
+  if (roots.length > 0) {
+    roots.forEach(root => {
+      elTreeData.push(getElTreeData(root));
+    });
+  }
+  return elTreeData;
+};
+
+const setMap = (map: { [ID: string]: boolean }, node: TreeNode) => {
+  if (node.getCtx().expanded === true) map[node.ID] = true;
+  return map;
+};
+
+// 获取树展开收起的映射表
+export const getExpandMap = (nodes: TreeNode[]) => {
+  return nodes.reduce(setMap, {});
+};

+ 12 - 12
src/utils/frontend/vueHelper.ts

@@ -1,13 +1,13 @@
 import { App, Component } from 'vue';
-import { IGlobalPopup } from '@/components/popup/types';
+// import { IGlobalPopup } from '@/components/popup/types';
 
-type GlobalPopupMethod = (component?: Component, options?: any) => IGlobalPopup;
+// type GlobalPopupMethod = (component?: Component, options?: any) => IGlobalPopup;
 type GlobalPopoverMethod = (component?: Component, options?: any) => void;
-export interface IGlobalProperties {
-  $popup: GlobalPopupMethod;
-  $popover: GlobalPopoverMethod;
-  [func: string]: any;
-}
+// export interface IGlobalProperties {
+//   $popup: GlobalPopupMethod;
+//   $popover: GlobalPopoverMethod;
+//   [func: string]: any;
+// }
 
 let curApp: App;
 
@@ -15,16 +15,16 @@ export function setGlobalApp(app: App): void {
   curApp = app;
 }
 
-export function getGlobal(): IGlobalProperties {
+export function getGlobal()/* : IGlobalProperties */ {
   if (!curApp) {
     throw new Error('没有正确注册全局app');
   }
-  return curApp.config.globalProperties as IGlobalProperties;
+  return curApp.config.globalProperties/*  as IGlobalProperties */;
 }
 
-export function createPopup<T = any>(component?: Component, options?: T): IGlobalPopup {
-  return getGlobal().$popup(component, options);
-}
+// export function createPopup<T = any>(component?: Component, options?: T): IGlobalPopup {
+//   return getGlobal().$popup(component, options);
+// }
 export function createPopover<T = any>(component?: Component, options?: T): void {
   return getGlobal().$popover(component, options);
 }

+ 59 - 14
src/views/project/summary/components/cost-table/CostTable.vue

@@ -1,26 +1,71 @@
 <script setup lang="ts">
-import { onMounted, reactive, ref } from "vue";
-const tableData = [
-  ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'],
-  ['2017', 10, 11, 12, 13, 15, 16],
-  ['2018', 10, 11, 12, 13, 15, 16],
-  ['2019', 10, 11, 12, 13, 15, 16],
-  ['2020', 10, 11, 12, 13, 15, 16],
-  ['2021', 10, 11, 12, 13, 15, 16]
-]
+import { onMounted, ref } from "vue";
+import { Tree, TreeNode } from '@sc/tree';
+import { expandAllTreeNode } from '@/utils/frontend/tree';
+import useHotRef from '@/composables/useHotRef';
+import rawData from '@/constants/tmp/table-data.json'
+import tblColMeta from '@/constants/tmp/table-columns-meta'
+
+const { getHotRef, notNull } = useHotRef();
+const hotRef = getHotRef();
+
+///////
+const tree = new Tree([]);
+const tableTreeData: TreeNode[] = tree.data;
+const loading = ref(true);
+// 模拟异步获取表格数据
+setTimeout(() => {
+  const hot = hotRef.value as Hot.IHandsontableComponent;
+  tree.insert(rawData);
+  // tableTreeData.push(...new Tree(rawData).data);
+  expandAllTreeNode(tableTreeData);
+
+  loading.value = false;
+  hot.Load();
+}, 1000);
 
 const tableSettings = {
-  colHeaders: true,
-}
+  columnsMeta: tblColMeta,
+  manualRowMove: true,
+  // fixedColumnsLeft: 2,
+  groupingHeaders: [
+    [
+      { label: '项目编码', rowspan: 2 },
+      { label: '项目名称', rowspan: 2 },
+      { label: '计量单位', rowspan: 2 },
+      { label: '工程量', rowspan: 2 },
+      { label: '初始报价', colspan: 2 },
+      { label: '目标造价', colspan: 2 },
+      { label: '调整后报价', colspan: 2 },
+      { label: '消耗量调整系数', colspan: 5 },
+      { label: '子目工程量调整系数', rowspan: 2 },
+    ],
+    [
+      '综合单价',
+      '综合合价',
+      '综合单价',
+      '综合合价',
+      '综合单价',
+      '综合合价',
+      '人工',
+      '材料',
+      '机械',
+      '主材',
+      '设备',
+    ],
+  ],
+};
+
 </script>
 
 <template>
   <section class="cost-table">
     <handsontable
-      :data="tableData"
+      :data="tableTreeData"
       :settings="tableSettings"
-      class="cost-table"
-      ref="costHotRef"
+      tree
+      ref="hotRef"
+      :loading="loading"
     />
   </section>
 </template>

+ 1 - 1
src/views/project/summary/components/cost-table/style.scss

@@ -1,5 +1,5 @@
 .cost-table {
-  @apply h-full;
+  @apply h-full w-full;
   background: #fff;
   // border: 1px solid #e8eaec;
 }