Handsontable.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <template>
  2. <div
  3. class="handsontable-container"
  4. :style="borderStyle"
  5. v-loading="loading"
  6. element-loading-background="rgba(245, 245, 245, 0.5)"
  7. >
  8. <div class="table-wrapper" ref="handsontableRef"></div>
  9. </div>
  10. </template>
  11. <script lang="ts">
  12. import {
  13. computed,
  14. defineComponent,
  15. onBeforeUnmount,
  16. onMounted,
  17. ref,
  18. toRefs,
  19. watch,
  20. } from "vue";
  21. import Handsontable from "@sc/handsontable";
  22. import { TreeNode } from "@sc/tree";
  23. import debounce from "lodash/debounce";
  24. import { off } from "@/utils/frontend/dom";
  25. import useClickPosition from "./composables/useClickPosition";
  26. import useInstance from "./composables/useInstance";
  27. import useTableSettings, {
  28. getColumnsSettings,
  29. getTableData,
  30. overwriteReadOnly,
  31. } from "./composables/useTableSettings";
  32. import "./renderer/registerRenderers";
  33. import { Hot } from "@/types/components/hot";
  34. // import './cell-types/registerCellTypes';
  35. export default defineComponent({
  36. name: "Handsontable",
  37. props: {
  38. // 表格的数据
  39. data: {
  40. type: Array,
  41. required: true,
  42. },
  43. // 表格的设置选项
  44. settings: {
  45. type: Object,
  46. required: true,
  47. },
  48. // 是否是树结构,即每行数据是否是 `TreeNode`
  49. tree: {
  50. type: Boolean,
  51. default: false,
  52. },
  53. // 边框, none 表示没有边框
  54. border: {
  55. type: String,
  56. default: "top,right,bottom,left",
  57. },
  58. // 整个表格是否只读
  59. readOnly: {
  60. type: Boolean,
  61. default: false,
  62. },
  63. // 是否处于 loading 状态
  64. loading: {
  65. type: Boolean,
  66. default: false,
  67. },
  68. },
  69. setup(props) {
  70. // 模板引用
  71. const handsontableRef = ref<HTMLElement | null>(null);
  72. const hotProps = props as Hot.IHandsontableProps;
  73. const tableSettings = useTableSettings(hotProps);
  74. let rawData = props.data as any;
  75. // Handsontable 实例
  76. let instance: Handsontable | null;
  77. type RowHeaders = boolean | ((visualRow: number) => number);
  78. const getRowHeader = (): RowHeaders => {
  79. if (tableSettings.rowHeaders !== undefined)
  80. return tableSettings.rowHeaders as any;
  81. return props.tree
  82. ? (visualRow: number) => {
  83. if (instance) {
  84. const rowSourceData = instance.getSourceDataAtRow(
  85. instance.toPhysicalRow(visualRow)
  86. ) as TreeNode;
  87. return rowSourceData.getCtx().row() + 1;
  88. }
  89. return visualRow;
  90. }
  91. : true;
  92. };
  93. // 重新渲染,供外界调用
  94. const Render = () => instance && instance.render();
  95. const onResize = debounce(() => Render(), 100);
  96. // 监听 mousedownPosition 事件的处理器
  97. let clickPositionHandler: any;
  98. onMounted(() => {
  99. const wrapperDOM = handsontableRef.value as HTMLElement;
  100. // handsontable 实例
  101. instance = useInstance(wrapperDOM, tableSettings);
  102. // 获取鼠标点击的区域相关代码
  103. clickPositionHandler = useClickPosition(wrapperDOM, tableSettings);
  104. // 设置行号
  105. (() => {
  106. const rowHeaders = getRowHeader();
  107. instance.updateSettings({ rowHeaders }, false);
  108. })();
  109. // 改变窗口大小监听事件
  110. window.addEventListener("resize", onResize);
  111. });
  112. const { readOnly, tree } = toRefs(props);
  113. // 重新加载数据,供外界调用
  114. const Load = (data?: any) => {
  115. if (data) rawData = data;
  116. instance && instance.loadData(getTableData(tree.value, rawData));
  117. };
  118. // 获取 instance,供外界调用
  119. const GetInstance = () => instance;
  120. // 更新 Settings,供外界调用(如增加或减少列)
  121. const Update = (newTableSettings: Hot.ISettings) => {
  122. const { columnsMeta } = newTableSettings;
  123. const columnsData = getColumnsSettings(columnsMeta);
  124. let newSettings;
  125. if (columnsData) {
  126. newSettings = { ...columnsData, ...newTableSettings };
  127. delete newSettings.columnsMeta;
  128. } else {
  129. newSettings = newTableSettings;
  130. }
  131. overwriteReadOnly(newSettings, readOnly.value);
  132. if (newSettings.rowHeaders === undefined)
  133. // 设置行号
  134. newSettings.rowHeaders = getRowHeader();
  135. instance && instance.updateSettings(newSettings, false);
  136. };
  137. watch(readOnly, () => {
  138. Update(props.settings);
  139. });
  140. watch(tree, () => {
  141. const newSettings = props.settings;
  142. const { columnsMeta } = newSettings;
  143. columnsMeta &&
  144. columnsMeta.forEach((columnMeta: any) => {
  145. if (columnMeta.renderer === "wc.switcherRenderer") {
  146. delete columnMeta.renderer;
  147. }
  148. });
  149. Update(newSettings);
  150. Load();
  151. });
  152. // 组件 unmount 之前销毁 handsontable 实例
  153. onBeforeUnmount(() => {
  154. if (instance) {
  155. window.removeEventListener("resize", onResize);
  156. // 移除所有的监听器
  157. // Handsontable.hooks.destroy && Handsontable.hooks.destroy(instance);
  158. instance.destroy();
  159. instance = null;
  160. }
  161. if (clickPositionHandler)
  162. off(document.body, "mouseup", clickPositionHandler);
  163. });
  164. // 边框样式
  165. const borderStyle = computed(() => {
  166. if (props.border === "none") {
  167. return {
  168. border: "none",
  169. };
  170. }
  171. const borders = props.border.split(",");
  172. const style = {} as any;
  173. borders.forEach((border) => {
  174. style[`border-${border}`] = "1px solid #d4d4d4";
  175. });
  176. return style;
  177. });
  178. return {
  179. handsontableRef,
  180. borderStyle,
  181. Render,
  182. Update,
  183. Load,
  184. GetInstance,
  185. };
  186. },
  187. });
  188. </script>
  189. <style lang="scss" scoped>
  190. .handsontable-container {
  191. position: relative;
  192. width: 100%;
  193. height: 100%;
  194. @include scrollbar-handsontable;
  195. }
  196. .table-wrapper {
  197. width: 100%;
  198. height: 100%;
  199. overflow: hidden;
  200. /* > :deep(.ht_master) {
  201. } */
  202. > :deep(.ht_clone_top_left_corner),
  203. > :deep(.ht_clone_top) {
  204. .htCore {
  205. .real-th {
  206. position: relative;
  207. overflow: visible;
  208. &.ht__highlight {
  209. .relative {
  210. background-color: #dad8d6 !important;
  211. }
  212. }
  213. .relative {
  214. position: absolute;
  215. bottom: 0;
  216. left: 0;
  217. padding: 0;
  218. width: 100%;
  219. background-color: $smoke;
  220. &:hover {
  221. background-color: #dfdfdf !important;
  222. }
  223. }
  224. }
  225. }
  226. }
  227. /* > :deep(.ht_clone_bottom) {
  228. } */
  229. > :deep(.ht_clone_left) {
  230. // overflow: visible !important;
  231. .wtHolder {
  232. width: 100% !important;
  233. }
  234. .htCore {
  235. transition: box-shadow 0.3s ease;
  236. &.shadow {
  237. box-shadow: 4px 0 4px -3px rgba(0, 0, 0, 0.2);
  238. }
  239. /* tbody {
  240. // 这段代码会导致小屏幕上缩放分辨率时,表格行错位的问题
  241. th {
  242. display: flex;
  243. align-items: center;
  244. justify-content: center;
  245. }
  246. } */
  247. }
  248. }
  249. > :deep(.ht_clone_top_left_corner) {
  250. .htCore {
  251. thead {
  252. tr {
  253. th {
  254. border-color: $gainsboro;
  255. }
  256. }
  257. }
  258. }
  259. }
  260. /* :deep( > .handsontableInputHolder){
  261. }
  262. */
  263. > :deep(.ht_master),
  264. > :deep(.ht_clone_top),
  265. > :deep(.ht_clone_bottom),
  266. > :deep(.ht_clone_left),
  267. > :deep(.ht_clone_top_left_corner),
  268. > :deep(.handsontableInputHolder) {
  269. .htCore {
  270. td,
  271. th {
  272. // 当前选中行的样式
  273. &.current-row {
  274. background-color: $light-sky-blue !important;
  275. }
  276. &.show-highlight {
  277. background-color: #c1e1fc !important;
  278. }
  279. // 当前选中单元格的样式
  280. &.current {
  281. &.highlight {
  282. background-color: #c1e1fc !important;
  283. .custom-button {
  284. display: inline !important;
  285. vertical-align: middle;
  286. margin: -1px 0 0 2px;
  287. }
  288. }
  289. }
  290. }
  291. th {
  292. border-color: $gainsboro !important;
  293. background-color: $smoke !important;
  294. color: $black;
  295. cursor: pointer;
  296. &:hover {
  297. background-color: #dfdfdf !important;
  298. }
  299. &.ht__highlight {
  300. background-color: #dad8d6 !important;
  301. .colHeader,
  302. .rowHeader {
  303. font-weight: bold;
  304. color: #2577c2;
  305. }
  306. }
  307. }
  308. tbody {
  309. tr {
  310. &.hover {
  311. td {
  312. background-color: $ghost-grey;
  313. }
  314. }
  315. td {
  316. transition: background-color 0.15s;
  317. border-color: $gainsboro;
  318. color: $black;
  319. &.bg-danger {
  320. background-color: #fbe3e3;
  321. }
  322. &.font-danger {
  323. color: $danger;
  324. }
  325. &.wcSingleLine {
  326. white-space: nowrap;
  327. }
  328. &.htMiddle {
  329. vertical-align: middle;
  330. }
  331. }
  332. }
  333. }
  334. }
  335. .htBorders {
  336. .wtBorder.corner {
  337. cursor: cell;
  338. }
  339. }
  340. }
  341. // 没有左边框
  342. > :deep(.ht_clone_left) {
  343. .htCore {
  344. th {
  345. border-left: none;
  346. }
  347. }
  348. }
  349. > :deep(.ht_clone_top_left_corner) {
  350. .htCore {
  351. thead {
  352. th:first-child {
  353. border-left: none;
  354. }
  355. }
  356. }
  357. }
  358. // 没有上边框
  359. > :deep(.ht_master),
  360. > :deep(.ht_clone_top),
  361. > :deep(.ht_clone_bottom),
  362. > :deep(.ht_clone_left),
  363. > :deep(.ht_clone_top_left_corner),
  364. > :deep(.handsontableInputHolder) {
  365. .htCore {
  366. thead {
  367. tr {
  368. th {
  369. border-top: none !important;
  370. }
  371. }
  372. }
  373. /*tbody {
  374. tr {
  375. td {
  376. border-left: none !important;
  377. }
  378. }
  379. }*/
  380. }
  381. }
  382. :deep(.red) {
  383. color: red !important;
  384. }
  385. :deep(.green) {
  386. color: green !important;
  387. }
  388. :deep(.stateCheckBox::after) {
  389. content: "";
  390. display: block;
  391. width: 7px;
  392. height: 7px;
  393. background-color: #007bff;
  394. transform: translate(3px, 3px);
  395. }
  396. // 调整列大小的手柄
  397. :deep(.manualColumnResizer) {
  398. cursor: ew-resize;
  399. transform: translateX(3px);
  400. }
  401. // 调整行大小的手柄
  402. :deep(.manualRowResizer) {
  403. cursor: ns-resize;
  404. transform: translateY(3px);
  405. }
  406. }
  407. </style>