table.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. <template>
  2. <TempTable :data="tData" ref="tableRef" :id="data.id" />
  3. <PropertyUpdate
  4. :describes="describes"
  5. :data="data"
  6. :target="shape"
  7. @change="emit('updateShape', { ...data })"
  8. />
  9. <Operate
  10. :target="shape"
  11. :menus="operateMenus"
  12. @show="menuShowHandler"
  13. @hide="menuHideHandler"
  14. />
  15. <template v-for="(raw, rawNdx) in data.content">
  16. <template v-for="(_, colNdx) in raw">
  17. <TextDom
  18. v-if="tableRef?.texts[rawNdx] && tableRef?.texts[rawNdx][colNdx]?.getNode()"
  19. :shape="tableRef.texts[rawNdx][colNdx].getNode()!"
  20. @submit="(val) => submitInputHandler(val, rawNdx, colNdx)"
  21. @show="showInputHandler"
  22. @hide="quitInputHandler"
  23. />
  24. </template>
  25. </template>
  26. </template>
  27. <script lang="ts" setup>
  28. import TempTable from "./temp-table.vue";
  29. import TextDom from "../share/text-area.vue";
  30. import { TableData, getMouseStyle, defaultStyle, TableCollData } from "./index.ts";
  31. import { PropertyUpdate, Operate } from "../../propertys";
  32. import { useComponentStatus } from "@/core/hook/use-component.ts";
  33. import { Transform } from "konva/lib/Util";
  34. import { MathUtils } from "three";
  35. import {
  36. useCustomTransformer,
  37. useGetTransformerOperType,
  38. } from "@/core/hook/use-transformer.ts";
  39. import { copy, getResizeCorsur } from "@/utils/shared.ts";
  40. import { computed, ref, watch, watchEffect } from "vue";
  41. import { Group } from "konva/lib/Group";
  42. import { DC } from "@/deconstruction.js";
  43. import { Minus, Plus } from "@element-plus/icons-vue";
  44. import { Rect } from "konva/lib/shapes/Rect";
  45. import { setShapeTransform } from "@/utils/shape.ts";
  46. import { Text } from "konva/lib/shapes/Text";
  47. import {
  48. useMouseShapesStatus,
  49. useMouseShapeStatus,
  50. } from "@/core/hook/use-mouse-status.ts";
  51. import { useCursor, usePointerPos } from "@/core/hook/use-global-vars.ts";
  52. import { numEq, Pos } from "@/utils/math.ts";
  53. const props = defineProps<{ data: TableData }>();
  54. const emit = defineEmits<{
  55. (e: "updateShape", value: TableData): void;
  56. (e: "addShape", value: TableData): void;
  57. (e: "delShape"): void;
  58. }>();
  59. type TableRef = {
  60. shape: DC<Group>;
  61. texts: DC<Text>[][];
  62. getMouseIntersect: (
  63. pos?: Pos
  64. ) => {
  65. rawBorderNdx: number;
  66. colBorderNdx: number;
  67. rawNdx: number;
  68. colNdx: number;
  69. };
  70. };
  71. const tableRef = ref<TableRef>();
  72. const status = useMouseShapeStatus(computed(() => tableRef.value?.shape));
  73. let inter: Pick<
  74. ReturnType<TableRef["getMouseIntersect"]>,
  75. "rawBorderNdx" | "colBorderNdx"
  76. > | null = null;
  77. const pos = usePointerPos();
  78. const cursor = useCursor();
  79. const shapesStatus = useMouseShapesStatus();
  80. watch(
  81. () => status.value.hover && !status.value.press,
  82. (hover, _, onCleanup) => {
  83. if (!hover) return;
  84. onCleanup(
  85. watch(
  86. () => pos.value && tableRef.value?.getMouseIntersect(pos.value),
  87. (inter, _, onCleanup) => {
  88. const $shape = shape.value?.getNode();
  89. if ($shape && inter && (~inter.colBorderNdx || ~inter.rawBorderNdx)) {
  90. onCleanup(
  91. cursor.push(getResizeCorsur(!~inter.rawBorderNdx, $shape.rotation()))
  92. );
  93. }
  94. },
  95. { immediate: true, flush: "post" }
  96. )
  97. );
  98. },
  99. { flush: "post" }
  100. );
  101. watch(
  102. () => status.value.press,
  103. (press, _, onCleanup) => {
  104. if (!press) return;
  105. inter = tableRef.value?.getMouseIntersect() || null;
  106. const $shape = shape.value?.getNode();
  107. if ($shape && inter && (~inter.colBorderNdx || ~inter.rawBorderNdx)) {
  108. const pop = cursor.push(
  109. getResizeCorsur(!~inter.rawBorderNdx, shape.value?.getNode().rotation())
  110. );
  111. const isActive = shapesStatus.actives.includes($shape);
  112. if (isActive) {
  113. shapesStatus.actives = shapesStatus.actives.filter((s) => s !== $shape);
  114. }
  115. onCleanup(() => {
  116. inter = null;
  117. pop();
  118. // if (isActive) {
  119. // shapesStatus.actives = [...shapesStatus.actives, $shape];
  120. // }
  121. });
  122. }
  123. },
  124. { flush: "post" }
  125. );
  126. const getColMinSize = (col: TableCollData) => {
  127. const minw = (col.padding || 0) * 2 + (col.fontSize || 12) + 4;
  128. const minh = (col.padding || 0) * 2 + (col.fontSize || 12) + 4;
  129. return { w: minw, h: minh };
  130. };
  131. const getOperType = useGetTransformerOperType();
  132. const matToData = (data: TableData, mat: Transform, initData?: TableData) => {
  133. if (!initData) {
  134. initData = copy(data);
  135. }
  136. const dec = mat.decompose();
  137. if (!inter || (!~inter.colBorderNdx && !~inter.rawBorderNdx)) {
  138. const oldData = copy(data);
  139. data.height = dec.scaleY * initData.height;
  140. data.width = dec.scaleX * initData.width;
  141. let w = 0;
  142. let h = 0;
  143. data.content.forEach((raw, rndx) => {
  144. raw.forEach((col, cndx) => {
  145. const initCol = initData.content[rndx][cndx];
  146. const minSize = getColMinSize(initCol);
  147. col.width = Math.max(minSize.w, data.width * (initCol.width / initData.width));
  148. col.height = Math.max(
  149. minSize.h,
  150. data.height * (initCol.height / initData.height)
  151. );
  152. if (rndx === 0) {
  153. w += col.width;
  154. }
  155. if (cndx === 0) {
  156. h += col.height;
  157. }
  158. });
  159. });
  160. const eqW = numEq(w, data.width);
  161. const eqH = numEq(h, data.height);
  162. if (!eqW || !eqH) {
  163. const type = getOperType();
  164. if (type) {
  165. Object.assign(data, oldData);
  166. } else {
  167. data.width = w;
  168. data.height = h;
  169. const initDec = new Transform(initData.mat).decompose();
  170. data.mat = new Transform()
  171. .translate(eqW ? dec.x : initDec.x, eqH ? dec.y : initDec.y)
  172. .rotate(MathUtils.degToRad(dec.rotation)).m;
  173. }
  174. } else {
  175. data.mat = new Transform()
  176. .translate(dec.x, dec.y)
  177. .rotate(MathUtils.degToRad(dec.rotation)).m;
  178. }
  179. return data;
  180. }
  181. const initDec = new Transform(initData.mat).decompose();
  182. const move = new Transform().rotate(MathUtils.degToRad(-dec.rotation)).point({
  183. x: dec.x - initDec.x,
  184. y: dec.y - initDec.y,
  185. });
  186. if (~inter.rawBorderNdx) {
  187. const ndxRaw = inter.rawBorderNdx - 1;
  188. const ndx = ndxRaw === -1 ? 0 : ndxRaw;
  189. let offset = ndxRaw === -1 ? -move.y : move.y;
  190. const minSize = getColMinSize(data.content[ndx][0]);
  191. const h = Math.max(minSize.h, initData.content[ndx][0].height + offset);
  192. offset = h - initData.content[ndx][0].height;
  193. data.content[ndx].forEach(
  194. (col, colNdx) => (col.height = initData.content[ndx][colNdx].height + offset)
  195. );
  196. data.height = initData.height + offset;
  197. if (ndxRaw === -1) {
  198. const translate = new Transform()
  199. .rotate(MathUtils.degToRad(dec.rotation))
  200. .point({ x: 0, y: -offset });
  201. data.mat = new Transform()
  202. .translate(translate.x, translate.y)
  203. .multiply(new Transform(initData.mat)).m;
  204. }
  205. } else {
  206. const ndxRaw = inter.colBorderNdx - 1;
  207. const ndx = ndxRaw === -1 ? 0 : ndxRaw;
  208. let offset = ndxRaw === -1 ? -move.x : move.x;
  209. const minSize = getColMinSize(data.content[0][ndx]);
  210. const w = Math.max(minSize.w, initData.content[0][ndx].width + offset);
  211. offset = w - initData.content[0][ndx].width;
  212. data.content.forEach((row, rowNdx) => {
  213. row[ndx].width = initData.content[rowNdx][ndx].width + offset;
  214. });
  215. data.width = initData.width + offset;
  216. if (ndxRaw === -1) {
  217. const translate = new Transform()
  218. .rotate(MathUtils.degToRad(dec.rotation))
  219. .point({ x: -offset, y: 0 });
  220. data.mat = new Transform()
  221. .translate(translate.x, translate.y)
  222. .multiply(new Transform(initData.mat)).m;
  223. }
  224. }
  225. return data;
  226. };
  227. let repShape: Rect | null;
  228. const sync = (data: TableData) => {
  229. if (repShape) {
  230. repShape.width(data.width);
  231. repShape.height(data.height);
  232. const tf = new Transform(data.mat);
  233. setShapeTransform(repShape, tf);
  234. initData = copy(data);
  235. }
  236. };
  237. let initData: TableData;
  238. const { shape, tData, data, operateMenus, describes } = useComponentStatus<
  239. Group,
  240. TableData
  241. >({
  242. emit,
  243. props,
  244. getMouseStyle,
  245. defaultStyle,
  246. alignment: (data, mat) => {
  247. const tf = shape.value!.getNode().getTransform();
  248. mat.multiply(tf);
  249. matToData(data, mat);
  250. sync(data);
  251. },
  252. transformType: "custom",
  253. customTransform(callback, shape, data) {
  254. useCustomTransformer(shape, data, {
  255. openSnap: true,
  256. transformerConfig: { flipEnabled: false },
  257. getRepShape() {
  258. repShape = new Rect();
  259. sync(data.value);
  260. return {
  261. shape: repShape as any,
  262. };
  263. },
  264. beforeHandler(data, mat) {
  265. return matToData(copy(data), mat, initData);
  266. },
  267. handler(data, mat) {
  268. matToData(data, mat, initData);
  269. // sync(data);
  270. },
  271. callback(data) {
  272. callback();
  273. sync(data);
  274. },
  275. });
  276. },
  277. copyHandler(mat, data) {
  278. const tf = shape.value!.getNode().getTransform();
  279. mat.multiply(tf);
  280. return matToData({ ...data }, mat);
  281. },
  282. propertys: [
  283. "fill",
  284. "stroke",
  285. "fontColor",
  286. "strokeWidth",
  287. "fontSize",
  288. // "ref",
  289. "opacity",
  290. // "zIndex"
  291. "align",
  292. "fontStyle",
  293. ],
  294. });
  295. watchEffect((onCleanup) => {
  296. shape.value = tableRef.value?.shape;
  297. onCleanup(() => (shape.value = undefined));
  298. });
  299. watch(
  300. () => data.value.fontSize,
  301. () => {
  302. data.value.content.forEach((raw) => {
  303. raw.forEach((col) => {
  304. col.fontSize = data.value.fontSize;
  305. });
  306. });
  307. const $shape = shape.value!.getNode();
  308. data.value = matToData(data.value, $shape.getTransform());
  309. sync(data.value);
  310. $shape.fire("bound-change");
  311. },
  312. { flush: "sync" }
  313. );
  314. watchEffect(
  315. () => {
  316. data.value.content.forEach((raw) => {
  317. raw.forEach((col) => {
  318. col.fontColor = data.value.fontColor;
  319. col.fontStyle = data.value.fontStyle;
  320. col.align = data.value.align;
  321. });
  322. });
  323. },
  324. { flush: "sync" }
  325. );
  326. let addMenu: any;
  327. const menuShowHandler = () => {
  328. const config = tableRef.value!.getMouseIntersect();
  329. addMenu = [
  330. {
  331. icon: Plus,
  332. label: "插入行",
  333. handler: () => {
  334. const tempRaw = data.value.content[config.rawNdx];
  335. data.value.content.splice(
  336. config.rawNdx,
  337. 0,
  338. tempRaw.map((item) => ({ ...item, content: "" }))
  339. );
  340. data.value.height += tempRaw[0].height;
  341. sync(data.value);
  342. emit("updateShape", { ...data.value });
  343. },
  344. },
  345. {
  346. icon: Minus,
  347. label: "删除行",
  348. handler: () => {
  349. const tempRaw = data.value.content[config.rawNdx];
  350. data.value.content.splice(config.rawNdx, 1);
  351. data.value.height -= tempRaw[0].height;
  352. if (data.value.content.length === 0) {
  353. emit("delShape");
  354. } else {
  355. sync(data.value);
  356. emit("updateShape", data.value);
  357. }
  358. },
  359. },
  360. {
  361. icon: Plus,
  362. label: "插入列",
  363. handler: () => {
  364. const tempCol = data.value.content[0][config.colNdx];
  365. for (let i = 0; i < data.value.content.length; i++) {
  366. const raw = data.value.content[i];
  367. raw.splice(config.colNdx, 0, { ...tempCol, content: "" });
  368. }
  369. data.value.width += tempCol.width;
  370. sync(data.value);
  371. emit("updateShape", data.value);
  372. },
  373. },
  374. {
  375. icon: Minus,
  376. label: "删除列",
  377. handler: () => {
  378. const tempCol = data.value.content[0][config.colNdx];
  379. for (let i = 0; i < data.value.content.length; i++) {
  380. const raw = data.value.content[i];
  381. raw.splice(config.colNdx, 1);
  382. }
  383. data.value.width -= tempCol.width;
  384. if (data.value.content[0].length === 0) {
  385. emit("delShape");
  386. } else {
  387. sync(data.value);
  388. emit("updateShape", data.value);
  389. }
  390. },
  391. },
  392. ];
  393. operateMenus.unshift(...addMenu);
  394. };
  395. const menuHideHandler = () => {
  396. for (let i = 0; i < addMenu.length; i++) {
  397. const ndx = operateMenus.indexOf(addMenu[i]);
  398. if (ndx !== -1) {
  399. operateMenus.splice(ndx, 1);
  400. }
  401. }
  402. addMenu = [];
  403. };
  404. const showText = ref(false);
  405. const showInputHandler = () => {
  406. showText.value = true;
  407. const ndx = shapesStatus.actives.indexOf(shape.value!.getNode());
  408. if (~ndx) {
  409. shapesStatus.actives = shapesStatus.actives.filter(
  410. (v) => v !== shape.value!.getNode()
  411. );
  412. }
  413. };
  414. const quitInputHandler = () => {
  415. showText.value = false;
  416. };
  417. const submitInputHandler = (val: string, rawNdx: number, colNdx: number) => {
  418. quitInputHandler();
  419. data.value.content[rawNdx][colNdx].content = val;
  420. emit("updateShape", data.value);
  421. };
  422. </script>