1
0

help.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { appConstant } from "@/app";
  2. import {
  3. SceneTypeDesc,
  4. SceneTypeDomain,
  5. SceneTypePaths,
  6. } from "@/constant/scene";
  7. import { alert } from "@/helper/message";
  8. import { getCaseSceneList, getSyncSceneInfo } from "@/store/case";
  9. import { CaseTagging } from "@/store/caseTagging";
  10. import { ModelScene, QuoteScene, Scene, SceneType } from "@/store/scene";
  11. import { transformSWToken, user } from "@/store/user";
  12. import { base64ToBlob, drawImage } from "@/util";
  13. import { ref, watchEffect } from "vue";
  14. export type MenuItem = {
  15. key: string;
  16. label: string;
  17. onClick: (caseId: number) => void;
  18. };
  19. export const getFuseCodeLink = (caseId: number, query?: boolean) => {
  20. const params: { token?: string; caseId: string; app: string } = {
  21. caseId: caseId.toString(),
  22. app: appConstant.deptId.toString(),
  23. };
  24. if (!query) {
  25. params.token = user.value.token;
  26. }
  27. const url = new URL(
  28. SceneTypePaths[SceneType.SWMX][0],
  29. SceneTypeDomain[SceneType.SWMX]
  30. );
  31. for (const [name, val] of Object.entries(params)) {
  32. url.searchParams.append(name, val || "");
  33. }
  34. return url.href;
  35. };
  36. export const getSWKKSyncLink = async (caseId: number) => {
  37. const scenes = await getCaseSceneList(caseId);
  38. const supportTypes = [SceneType.SWKK, SceneType.SWKJ];
  39. const kkScenes = scenes.filter((scene) =>
  40. supportTypes.includes(scene.type)
  41. ) as QuoteScene[];
  42. let msg: string | null = null;
  43. if (!scenes.length) {
  44. msg = "当前案件下无场景,请先添加场景。";
  45. } else if (!kkScenes.length) {
  46. msg = `带看仅支持${supportTypes
  47. .map((type) => SceneTypeDesc[type])
  48. .join("、")}类型场景,请添加此类型场景。`;
  49. }
  50. if (msg) {
  51. alert(msg);
  52. throw msg;
  53. }
  54. const url = new URL(
  55. SceneTypePaths[SceneType.SWKK][2],
  56. SceneTypeDomain[SceneType.SWKK]
  57. );
  58. const roomId = await getSyncSceneInfo(caseId);
  59. const params = {
  60. vruserId: user.value.info.userName,
  61. // platform: "fd",
  62. roomId,
  63. // domain: location.href,
  64. // fromMiniApp: "0",
  65. role: "leader",
  66. avatar: user.value.info.avatar,
  67. redirect: encodeURIComponent(location.href),
  68. name: user.value.info.userName,
  69. // isTour: "0",
  70. m: kkScenes[0].num,
  71. };
  72. for (const [name, val] of Object.entries(params)) {
  73. url.searchParams.append(name, val || "");
  74. }
  75. return url;
  76. };
  77. export const checkScenesOpen = async (caseId: number, url: URL | string) => {
  78. const scenes = await getCaseSceneList(caseId);
  79. if (!scenes.length) {
  80. alert("当前案件下无场景,请先添加场景。");
  81. } else {
  82. window.open(url);
  83. }
  84. };
  85. export const getQuery = (caseId: number, share: boolean = false) =>
  86. `${getFuseCodeLink(caseId, true)}${share ? "&share=1" : ""}#show/summary`;
  87. // 查看
  88. export const gotoQuery = (caseId: number) => {
  89. checkScenesOpen(caseId, getQuery(caseId, false));
  90. };
  91. export enum FuseImageType {
  92. FUSE,
  93. KANKAN,
  94. LASER,
  95. }
  96. export const getSceneIframe = (iframe: HTMLIFrameElement) => {
  97. const iframeElement = iframe.contentWindow?.document.documentElement;
  98. if (!iframeElement) {
  99. return null;
  100. }
  101. const extIframe = iframeElement.querySelector(
  102. ".external"
  103. ) as HTMLIFrameElement;
  104. return extIframe || iframe;
  105. };
  106. export const getIframeSceneType = async (iframe: HTMLIFrameElement) => {
  107. const targetIframe = getSceneIframe(iframe);
  108. if (!targetIframe) {
  109. return null;
  110. }
  111. const targetWindow: any = targetIframe.contentWindow;
  112. const fuseCnavas = targetWindow.document.querySelector(
  113. ".scene-canvas > canvas"
  114. ) as HTMLElement;
  115. if (fuseCnavas) {
  116. return { type: FuseImageType.FUSE, sdk: targetWindow.sdk };
  117. }
  118. const isLaser = targetWindow.document.querySelector(".laser-layer");
  119. return {
  120. type: isLaser ? FuseImageType.LASER : FuseImageType.KANKAN,
  121. sdk: await targetWindow.__sdk,
  122. };
  123. };
  124. // 处理融合iframe页面
  125. export const fuseIframeHandler = (iframe: HTMLIFrameElement) => {
  126. const currentType = ref<FuseImageType>();
  127. const interval = setInterval(async () => {
  128. const typeMap = await getIframeSceneType(iframe);
  129. currentType.value = typeMap?.type;
  130. }, 100);
  131. const stopWatch = watchEffect((onCleanup) => {
  132. if (currentType.value === FuseImageType.LASER) {
  133. const $iframe = getSceneIframe(iframe)!;
  134. const target = $iframe.contentWindow!.document.head;
  135. const $style = document.createElement("style");
  136. $style.type = "text/css";
  137. var textNode = document.createTextNode(`
  138. .mode-tab > .model-mode-tab.strengthen {
  139. display: none
  140. }
  141. `);
  142. $style.appendChild(textNode);
  143. target.appendChild($style);
  144. }
  145. });
  146. return () => {
  147. clearInterval(interval);
  148. stopWatch();
  149. };
  150. };
  151. // 获取fuse场景截图
  152. export type FuseImageRet = { type: FuseImageType; blob: Blob | null };
  153. export const getFuseImage = async (
  154. iframe: HTMLIFrameElement,
  155. width: number = 500,
  156. height: number = width
  157. ) => {
  158. const typeMap = await getIframeSceneType(iframe);
  159. let blob: Blob | null = null;
  160. switch (typeMap?.type) {
  161. case FuseImageType.FUSE:
  162. const dataURL = await typeMap.sdk.screenshot(width, height);
  163. const res = await fetch(dataURL);
  164. blob = await res.blob();
  165. break;
  166. case FuseImageType.KANKAN:
  167. const result = await typeMap.sdk.Camera.screenshot(
  168. [{ width: width, height: width, name: "2k" }],
  169. false
  170. );
  171. blob = base64ToBlob(result[0].data);
  172. break;
  173. case FuseImageType.LASER:
  174. blob = await new Promise<Blob | null>((resolve) => {
  175. typeMap.sdk.scene
  176. .screenshot(width, height)
  177. .done((data: { dataUrl: string }) =>
  178. resolve(base64ToBlob(data.dataUrl))
  179. );
  180. });
  181. break;
  182. }
  183. return { type: typeMap?.type, blob };
  184. };
  185. // 处理fuse融合一起的图,将热点加入图片内
  186. export const fuseImageJoinHot = async (
  187. iframe: HTMLIFrameElement,
  188. rawBlob: Blob,
  189. showTags: CaseTagging[],
  190. width = 500
  191. ) => {
  192. // fuse场景需要特别处理
  193. const img = new Image();
  194. img.src = URL.createObjectURL(rawBlob);
  195. await new Promise((resolve) => (img.onload = resolve));
  196. const $canvas = document.createElement("canvas");
  197. $canvas.width = img.width;
  198. $canvas.height = img.height;
  199. const ctx = $canvas.getContext("2d")!;
  200. ctx.drawImage(img, 0, 0, img.width, img.height);
  201. const contentDoc = iframe.contentWindow!.document;
  202. const hotItems = Array.from(
  203. contentDoc.querySelectorAll(".hot-item")
  204. ) as HTMLDivElement[];
  205. hotItems.forEach((hot) => {
  206. const hotTitle = (hot.querySelector(".tip") as HTMLDivElement).innerText;
  207. const index = showTags.findIndex(
  208. (tag) => tag.tagTitle.trim() === hotTitle.trim()
  209. );
  210. if (index !== -1) {
  211. const bound = hot.getBoundingClientRect();
  212. const size = (img.width / width) * 32;
  213. const left = bound.left + size / 2;
  214. const top = bound.top + size / 2;
  215. ctx.save();
  216. ctx.translate(left, top);
  217. ctx.beginPath();
  218. ctx.arc(0, 0, size / 2, 0, 2 * Math.PI);
  219. ctx.strokeStyle = "#000";
  220. ctx.fillStyle = "#fff";
  221. ctx.stroke();
  222. ctx.fill();
  223. ctx.beginPath();
  224. ctx.fillStyle = "#000";
  225. ctx.textAlign = "center";
  226. ctx.textBaseline = "middle";
  227. ctx.font = `normal ${size / 2}px serif`;
  228. ctx.fillText((index + 1).toString(), 0, 0);
  229. ctx.restore();
  230. }
  231. });
  232. const $ccanvas = document.createElement("canvas");
  233. $ccanvas.width = width;
  234. $ccanvas.height = width;
  235. const cctx = $ccanvas.getContext("2d")!;
  236. drawImage(
  237. cctx,
  238. $ccanvas.width,
  239. $ccanvas.height,
  240. $canvas,
  241. img.width,
  242. img.height,
  243. 0,
  244. 0
  245. );
  246. const blob = await new Promise<Blob | null>((resolve) =>
  247. $ccanvas.toBlob(resolve, "png")
  248. );
  249. return blob;
  250. };
  251. export enum OpenType {
  252. query,
  253. edit,
  254. }
  255. export const openSceneUrl = async (scene: Scene, type: OpenType) => {
  256. const pathname = SceneTypePaths[scene.type][type];
  257. const url = new URL(pathname || "", window.location.href);
  258. if (scene.type === SceneType.SWMX) {
  259. url.searchParams.append(
  260. "modelId",
  261. (scene as ModelScene).modelId.toString()
  262. );
  263. url.hash = "#sign-model";
  264. url.searchParams.append("share", "1");
  265. } else {
  266. url.searchParams.append("m", (scene as QuoteScene).num);
  267. if (type === OpenType.edit) {
  268. url.searchParams.append(
  269. "token",
  270. await transformSWToken(scene as QuoteScene)
  271. );
  272. }
  273. }
  274. window.open(url);
  275. };