pano.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <template>
  2. <div class="pano-layout" v-loading="loading">
  3. <canvas ref="panoDomRef"></canvas>
  4. <div class="btns">
  5. <el-button
  6. size="large"
  7. type="primary"
  8. style="margin-right: 20px; width: 100px"
  9. @click="photo"
  10. >
  11. 屏幕拍照
  12. </el-button>
  13. <el-button
  14. size="large"
  15. style="margin-right: 20px; width: 100px"
  16. @click="copyGis"
  17. v-if="point && !noValidPoint(point)"
  18. >
  19. 复制经纬度
  20. </el-button>
  21. <el-button
  22. size="large"
  23. type="primary"
  24. style="width: 100px"
  25. @click="update = true"
  26. v-if="router.currentRoute.value.name === 'pano'"
  27. >
  28. 测点说明
  29. </el-button>
  30. </div>
  31. </div>
  32. <SingleInput
  33. v-if="point"
  34. :visible="update"
  35. isAllowEmpty
  36. @update:visible="update = false"
  37. :value="point.name || ''"
  38. :update-value="tex => updateScenePointName(point!, tex)"
  39. title="测点说明"
  40. placeholder="请填写测点说明"
  41. />
  42. </template>
  43. <script setup lang="ts">
  44. import SingleInput from "@/components/single-input.vue";
  45. import { router, setDocTitle } from "@/router";
  46. import { mergeFuns, round } from "@/util";
  47. import { computed, onMounted, onUnmounted, ref, watchEffect } from "vue";
  48. import { init } from "./env";
  49. import {
  50. updateScenePointName,
  51. getPointPano,
  52. ScenePoint,
  53. scenePoints,
  54. } from "@/store/scene";
  55. import { copyText, toDegrees, getTextBound } from "@/util";
  56. import { ElMessage } from "element-plus";
  57. import saveAs from "@/util/file-serve";
  58. import { DeviceType } from "@/store/device";
  59. import { initRelics, relics } from "@/store/relics";
  60. import { noValidPoint } from "../map/install";
  61. type Params = { pid?: string; relicsId?: string } | null;
  62. const params = computed(() => router.currentRoute.value.params as Params);
  63. const panoDomRef = ref<HTMLCanvasElement>();
  64. const destroyFns: (() => void)[] = [];
  65. const point = ref<ScenePoint>();
  66. watchEffect(() => {
  67. if (params.value?.pid) {
  68. const pid = Number(params.value!.pid);
  69. point.value = scenePoints.value.find((scene) => scene.id === pid);
  70. if (!point.value) {
  71. initRelics(Number(params.value.relicsId)).then(() => {
  72. point.value = scenePoints.value.find((scene) => scene.id === pid);
  73. if (!point.value) {
  74. router.replace({ name: "relics" });
  75. }
  76. });
  77. }
  78. }
  79. });
  80. const panoUrls = computed(() => {
  81. return (
  82. point.value && getPointPano(point.value, point.value.cameraType === DeviceType.CLUNT)
  83. );
  84. });
  85. const update = ref(false);
  86. const loading = ref(false);
  87. const getGis = () => {
  88. const pos = point.value!.pos as number[];
  89. return `经度: ${toDegrees(pos[0])}\n纬度: ${toDegrees(pos[1])}\n高程: ${round(
  90. pos[2],
  91. 4
  92. )}`;
  93. };
  94. const copyGis = async () => {
  95. await copyText(getGis());
  96. ElMessage.success("经纬度高程复制成功");
  97. };
  98. const canvas = document.createElement("canvas");
  99. // 水印添加函数
  100. const addWatermark = (imgURL: string, ration: number) => {
  101. const ctx = canvas.getContext("2d");
  102. const image = new Image();
  103. image.src = imgURL;
  104. return new Promise<string>((resolve, reject) => {
  105. image.onload = () => {
  106. canvas.width = image.width;
  107. canvas.height = image.height;
  108. ctx.drawImage(image, 0, 0, image.width, image.height);
  109. const font = `${ration * 20}px Arial`;
  110. const pos = point.value!.pos as number[];
  111. const lines = `经度: ${toDegrees(pos[0])}\n纬度: ${toDegrees(pos[1])}`.split("\n");
  112. const lineTopPadding = 5 * ration;
  113. const lineBounds = lines.map((line) =>
  114. getTextBound(line, font, [lineTopPadding, 0])
  115. );
  116. const bound = lineBounds.reduce(
  117. (t, { width, height }) => {
  118. t.width = Math.max(t.width, width);
  119. t.height += height;
  120. return t;
  121. },
  122. { width: 0, height: 0 }
  123. );
  124. const padding = 20 * ration;
  125. const margin = 80 * ration;
  126. const position = [
  127. image.width - margin - bound.width,
  128. image.height - margin - bound.height,
  129. ];
  130. ctx.rect(
  131. position[0] - padding,
  132. position[1] - padding,
  133. bound.width + 2 * padding,
  134. bound.height + 2 * padding
  135. );
  136. ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
  137. ctx.fill();
  138. ctx.font = font;
  139. ctx.textBaseline = "top";
  140. ctx.fillStyle = "#fff";
  141. let itemTop = 0;
  142. lines.forEach((line, ndx) => {
  143. ctx.fillText(line, position[0], position[1] + itemTop + lineTopPadding);
  144. itemTop += lineBounds[ndx].height;
  145. });
  146. resolve(canvas.toDataURL("image/jpg", 1));
  147. };
  148. image.onerror = reject;
  149. });
  150. };
  151. const photo = async () => {
  152. loading.value = true;
  153. await new Promise((resolve) => setTimeout(resolve, 300));
  154. const ration = 3;
  155. setSize(ration, 1920, 1080);
  156. let dataURL = panoDomRef.value.toDataURL("image/jpg", 1);
  157. if (!noValidPoint(point.value)) {
  158. dataURL = await addWatermark(dataURL, ration);
  159. }
  160. await saveAs(dataURL, `${relics.value?.name}.jpg`);
  161. ElMessage.success("图片导出成功");
  162. setSize(devicePixelRatio);
  163. loading.value = false;
  164. };
  165. let pano: ReturnType<typeof init>;
  166. const setSize = (ration: number, w?: number, h?: number) => {
  167. const canvas = panoDomRef.value!;
  168. canvas.width = (w || canvas.offsetWidth) * ration;
  169. canvas.height = (h || canvas.offsetHeight) * ration;
  170. pano.setSize([canvas.width, canvas.height]);
  171. };
  172. onMounted(() => {
  173. if (!panoDomRef.value) throw "没有canvas DOM";
  174. pano = init(panoDomRef.value);
  175. const resizeHandler = () => {
  176. setSize(devicePixelRatio);
  177. };
  178. resizeHandler();
  179. window.addEventListener("resize", resizeHandler);
  180. destroyFns.push(
  181. watchEffect(() => {
  182. if (panoUrls.value) {
  183. loading.value = true;
  184. pano.changeUrls(panoUrls.value).then(() => (loading.value = false));
  185. }
  186. }),
  187. pano.destory,
  188. () => {
  189. window.removeEventListener("resize", resizeHandler);
  190. }
  191. );
  192. });
  193. onUnmounted(() => mergeFuns(...destroyFns)());
  194. watchEffect(() => {
  195. if (router.currentRoute.value.name.toString().includes("pano") && point.value) {
  196. setDocTitle(point.value.index.toString() || relics.value.name);
  197. }
  198. });
  199. </script>
  200. <style scoped lang="scss">
  201. .pano-layout,
  202. canvas {
  203. width: 100%;
  204. height: 100%;
  205. }
  206. .pano-layout {
  207. position: relative;
  208. .btns {
  209. position: absolute;
  210. left: 50%;
  211. transform: translateX(-50%);
  212. bottom: 40px;
  213. z-index: 1;
  214. }
  215. }
  216. </style>