index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. <template>
  2. <RightFillPano>
  3. <ui-group title="初始画面" borderBottom>
  4. <ui-group-option>
  5. <div class="init-pic" :class="{ disabled: isEdit }">
  6. <img :src="getFileUrl(setting!.cover)" class="init-puc-cover" />
  7. <div class="init-pic-set" @click="enterSetPic">设置</div>
  8. </div>
  9. </ui-group-option>
  10. </ui-group>
  11. <ui-group title="指北针">
  12. <template #icon>
  13. <ui-icon
  14. ctrl
  15. :type="setting?.openCompass ? 'eye-s' : 'eye-n'"
  16. @click="changeBack(setting!.back, setting!.backType, !setting!.openCompass, setting!.mapOpen, setting!.mapType,
  17. {scale: setting!.scale!, rotate: setting?.rotate!})"
  18. />
  19. </template>
  20. </ui-group>
  21. <ui-group title="地图" v-if="caseProject!.tmProject?.latlng">
  22. <template #icon>
  23. <ui-icon
  24. ctrl
  25. :type="setting?.mapOpen ? 'eye-s' : 'eye-n'"
  26. @click="changeBack(setting!.back, setting!.backType, setting!.openCompass, !setting!.mapOpen, setting!.mapType, {scale: setting!.scale!, rotate: setting?.rotate!})"
  27. />
  28. </template>
  29. <ui-group-option v-if="setting?.mapOpen">
  30. <ui-input
  31. type="select"
  32. width="100%"
  33. :options="[
  34. { label: '卫星地图', value: 'satellite' },
  35. { label: '矢量地图', value: 'standard' },
  36. ]"
  37. :modelValue="setting!.mapType"
  38. @update:modelValue="(e: string )=> changeBack(setting!.back, setting!.backType, setting!.openCompass, setting!.mapOpen, e, {scale: setting!.scale!, rotate: setting?.rotate!})"
  39. />
  40. </ui-group-option>
  41. </ui-group>
  42. <ui-group title="视角范围">
  43. <ui-group-option class="ant-modal-root">
  44. <Slider
  45. :value="setting!.fov || 70"
  46. :min="40"
  47. :step="1"
  48. :max="100"
  49. @update:value="(val: any) => changeFov(val)"
  50. />
  51. </ui-group-option>
  52. </ui-group>
  53. <ui-group title="设置背景">
  54. <ui-group-option>
  55. <div class="back-layout">
  56. <div
  57. v-for="back in settingResources"
  58. :key="back.resource"
  59. class="back-item"
  60. :class="{ [back.backType]: true, active: setting!.back === back.resource }"
  61. @click="setting!.back !== back.resource && changeBack(back.resource, back.backType, setting!.openCompass, setting!.mapOpen, setting!.mapType, {scale: setting!.scale!, rotate: setting?.rotate!})"
  62. >
  63. <img
  64. :src="back.covre || back.resource"
  65. v-if="back.backType === 'img' || back.backType === 'bimg'"
  66. />
  67. <i
  68. class="iconfont"
  69. :class="back.resource"
  70. v-else-if="back.backType === 'icon'"
  71. />
  72. <span :style="{ background: back.resource }" v-else></span>
  73. <p class="back-item-desc">
  74. {{
  75. (settingResourceTypeDesc[back.backType] &&
  76. settingResourceTypeDesc[back.backType] + "-") + back.name
  77. }}
  78. </p>
  79. <ui-button
  80. v-if="!back.sys"
  81. type="primary"
  82. class="del"
  83. @click.stop="delBack(back)"
  84. >删除</ui-button
  85. >
  86. </div>
  87. <ui-input
  88. class="input"
  89. preview
  90. accept=".jpg, .jpeg, .png"
  91. @update:modelValue="iconUpload"
  92. type="file"
  93. >
  94. <template v-slot:replace>
  95. <div class="back-item icon">
  96. <i class="iconfont icon-add" />
  97. </div>
  98. </template>
  99. </ui-input>
  100. </div>
  101. </ui-group-option>
  102. </ui-group>
  103. <Teleport
  104. to="#layout-app"
  105. v-if="
  106. setting?.backType === SettingResourceType.bottomImage &&
  107. $router.currentRoute.value.name === RoutesName.setting
  108. "
  109. >
  110. <div class="slider-demo-block ant-modal-root">
  111. 缩放
  112. <Slider
  113. :value="setting!.scale || 1"
  114. vertical
  115. :min="0.1"
  116. :step="0.01"
  117. :max="3"
  118. @update:value="(val: any) => changeBack(setting!.back, setting!.backType, setting!.openCompass, setting!.mapOpen, setting!.mapType, {scale: val, rotate: setting?.rotate!})"
  119. />
  120. </div>
  121. <div class="slider-demo-block ant-modal-root" style="margin-right: 60px">
  122. 旋转
  123. <Slider
  124. :value="setting!.rotate || 0"
  125. vertical
  126. :min="0.1"
  127. :step="0.01"
  128. :max="360"
  129. @update:value="(val: any) => changeBack(setting!.back, setting!.backType, setting!.openCompass, setting!.mapOpen, setting!.mapType, {scale: setting!.scale!, rotate: val})"
  130. />
  131. </div>
  132. </Teleport>
  133. </RightFillPano>
  134. <div class="edit-add-type" v-if="addTemp">
  135. <div class="edit-hot-item">
  136. <h3 class="edit-title">
  137. 背景图
  138. <ui-icon type="close" ctrl @click.stop="addTemp = undefined" class="edit-close" />
  139. </h3>
  140. <ui-input
  141. require
  142. class="input"
  143. width="100%"
  144. placeholder="请输入背景图名称标注"
  145. type="text"
  146. v-model="addTemp.name"
  147. maxlength="15"
  148. />
  149. <ui-input
  150. require
  151. class="input"
  152. width="100%"
  153. placeholder="请输入背景图名称标注"
  154. type="select"
  155. :options="options"
  156. v-model="addTemp.backType"
  157. maxlength="15"
  158. />
  159. <div class="edit-hot">
  160. <a @click="addBack">
  161. <ui-icon type="nav-edit" />
  162. 确定
  163. </a>
  164. </div>
  165. </div>
  166. </div>
  167. </template>
  168. <script lang="ts" setup>
  169. import { RightFillPano } from "@/layout";
  170. import {
  171. enterEdit,
  172. enterOld,
  173. setting,
  174. isEdit,
  175. updataSetting,
  176. caseProject,
  177. } from "@/store";
  178. import { ref } from "vue";
  179. import { togetherCallback, getFileUrl, loadPack } from "@/utils";
  180. import { showRightPanoStack, showRightCtrlPanoStack } from "@/env";
  181. import { sdk, setBackdrop, setMap } from "@/sdk";
  182. import {
  183. delSettingResource,
  184. fetchSettingResources,
  185. settingResources,
  186. settingResourceTypeDesc,
  187. } from "@/api/setting-resource";
  188. import { uploadFile } from "@/api";
  189. import { SettingResource, addSettingResource } from "@/api/setting-resource";
  190. import { SettingResourceType } from "@/api/setting-resource";
  191. import { Dialog } from "bill/index";
  192. import { Slider } from "ant-design-vue";
  193. import { RoutesName } from "@/router";
  194. fetchSettingResources();
  195. const addBack = async () => {
  196. if (!addTemp.value!.name.trim()) {
  197. Dialog.alert("请输入名称");
  198. return;
  199. }
  200. await addSettingResource(addTemp.value!);
  201. addTemp.value = undefined;
  202. await fetchSettingResources();
  203. };
  204. const enterSetPic = () => {
  205. enterEdit(
  206. togetherCallback([
  207. showRightPanoStack.push(ref(false)),
  208. showRightCtrlPanoStack.push(ref(false)),
  209. ])
  210. );
  211. enterOld(async () => {
  212. const dataURL = await sdk.screenshot(300, 150);
  213. const res = await fetch(dataURL);
  214. const blob = await res.blob();
  215. setting.value = {
  216. ...setting.value!,
  217. cover: { url: dataURL, blob },
  218. pose: sdk.getPose(),
  219. };
  220. await updataSetting();
  221. });
  222. };
  223. const initBack = setting.value!.back;
  224. const initType = setting.value!.backType;
  225. const initOpenCompass = setting.value!.openCompass;
  226. const initopenMap = setting.value!.mapOpen;
  227. const initmapType = setting.value!.mapType;
  228. const initScale = setting.value!.scale;
  229. const initRotate = setting.value!.rotate;
  230. let isFirst = true;
  231. const changeBack = (
  232. back: string,
  233. type: SettingResourceType,
  234. openCompass: boolean,
  235. openMap: boolean,
  236. mapType: string,
  237. tb: { scale: number; rotate: number } = { scale: 1, rotate: 0 }
  238. ) => {
  239. if (type === SettingResourceType.map && !caseProject.value!.tmProject?.latlng) {
  240. Dialog.alert("当前案件没绑定经纬度,无法开启地图功能");
  241. return;
  242. }
  243. setting.value!.back = back;
  244. setting.value!.backType = type;
  245. setting.value!.openCompass = openCompass;
  246. setting.value!.mapOpen = openMap;
  247. setting.value!.mapType = mapType;
  248. setting.value!.scale = tb.scale;
  249. setting.value!.rotate = tb.rotate;
  250. setBackdrop(back, type, tb);
  251. setMap(openMap, mapType);
  252. (document.querySelector("#direction") as HTMLDivElement)!.style.display = openCompass
  253. ? "block"
  254. : "none";
  255. if (isFirst) {
  256. let isSave = false;
  257. isFirst = false;
  258. enterEdit(() => {
  259. if (!isSave) {
  260. setting.value!.back = initBack;
  261. setting.value!.backType = initType;
  262. setting.value!.openCompass = initOpenCompass;
  263. setting.value!.mapOpen = initopenMap;
  264. setting.value!.mapType = initmapType;
  265. setting.value!.scale = initScale;
  266. setting.value!.rotate = initRotate;
  267. setBackdrop(initBack, initType, { scale: initScale, rotate: initRotate });
  268. setMap(initopenMap, initmapType);
  269. (document.querySelector(
  270. "#direction"
  271. ) as HTMLDivElement)!.style.display = initOpenCompass ? "block" : "none";
  272. }
  273. isFirst = true;
  274. });
  275. enterOld(async () => {
  276. isSave = true;
  277. await loadPack(updataSetting());
  278. });
  279. }
  280. };
  281. const delBack = (() => {
  282. let isFirst = true;
  283. let oldResources: SettingResource[];
  284. let dels: SettingResource[] = [];
  285. return (back: SettingResource) => {
  286. if (setting.value?.back === back.resource) {
  287. changeBack(
  288. settingResources.value[0].resource,
  289. settingResources.value[0].backType,
  290. setting!.value.openCompass,
  291. setting!.value.mapOpen,
  292. setting!.value.mapType,
  293. { scale: setting!.value.scale!, rotate: setting!.value.rotate! }
  294. );
  295. }
  296. if (isFirst) {
  297. dels = [];
  298. oldResources = [...settingResources.value];
  299. }
  300. const ndx = settingResources.value.indexOf(back);
  301. if (~ndx) {
  302. settingResources.value.splice(ndx, 1);
  303. dels.push(back);
  304. }
  305. if (isFirst) {
  306. let isSave = false;
  307. isFirst = false;
  308. enterEdit(() => {
  309. if (!isSave) {
  310. settingResources.value = oldResources;
  311. }
  312. isFirst = true;
  313. });
  314. enterOld(async () => {
  315. isSave = true;
  316. await loadPack(Promise.all(dels.map(delSettingResource)));
  317. });
  318. }
  319. };
  320. })();
  321. const changeFov = (() => {
  322. let isFirst = true;
  323. let initFov: number;
  324. return (fov: number) => {
  325. if (isFirst) {
  326. initFov = setting.value!.fov;
  327. }
  328. setting.value!.fov = fov;
  329. if (isFirst) {
  330. let isSave = false;
  331. isFirst = false;
  332. enterEdit(() => {
  333. if (!isSave) {
  334. setting.value!.fov = initFov;
  335. }
  336. isFirst = true;
  337. });
  338. enterOld(async () => {
  339. isSave = true;
  340. await loadPack(updataSetting());
  341. });
  342. }
  343. };
  344. })();
  345. const options = [
  346. {
  347. value: SettingResourceType.envImage,
  348. label: settingResourceTypeDesc[SettingResourceType.envImage],
  349. },
  350. {
  351. value: SettingResourceType.bottomImage,
  352. label: settingResourceTypeDesc[SettingResourceType.bottomImage],
  353. },
  354. ];
  355. const addTemp = ref<Omit<SettingResource, "id">>();
  356. const iconUpload = async (data: any) => {
  357. addTemp.value = {
  358. resource: await uploadFile({ blob: data.file as any, url: "" }),
  359. name: "",
  360. backType: SettingResourceType.envImage,
  361. };
  362. };
  363. </script>
  364. <style scoped lang="scss">
  365. .init-pic {
  366. height: 150px;
  367. border-radius: 4px;
  368. overflow: hidden;
  369. position: relative;
  370. }
  371. .init-puc-cover {
  372. width: 100%;
  373. height: 100%;
  374. object-fit: cover;
  375. }
  376. .init-pic-set {
  377. position: absolute;
  378. bottom: 0;
  379. left: 0;
  380. right: 0;
  381. background-color: rgba(0, 0, 0, 0.5);
  382. font-size: 12px;
  383. color: #fff;
  384. line-height: 32px;
  385. z-index: 1;
  386. text-align: center;
  387. cursor: pointer;
  388. }
  389. .back-layout {
  390. display: grid;
  391. grid-template-columns: repeat(3, 1fr);
  392. gap: 20px;
  393. }
  394. .back-item {
  395. position: relative;
  396. > span,
  397. .iconfont,
  398. img {
  399. display: block;
  400. height: 88px;
  401. cursor: pointer;
  402. outline: 2px solid transparent;
  403. transition: all 0.3s;
  404. border-radius: 4px;
  405. width: 88px;
  406. object-fit: cover;
  407. }
  408. .del {
  409. position: absolute;
  410. top: 64px;
  411. height: 24px;
  412. left: 0;
  413. right: 0;
  414. }
  415. .iconfont {
  416. display: flex;
  417. align-items: center;
  418. justify-content: center;
  419. color: #525252;
  420. font-size: 32px;
  421. }
  422. img {
  423. object-fit: cover;
  424. }
  425. &.active {
  426. > span,
  427. .iconfont,
  428. img {
  429. outline-color: #00c8af;
  430. }
  431. }
  432. }
  433. .back-item-desc {
  434. font-size: 14px;
  435. color: #fff;
  436. margin-top: 10px;
  437. text-align: center;
  438. }
  439. .edit-add-type {
  440. position: fixed;
  441. inset: 0;
  442. background: rgba(0, 0, 0, 0.3);
  443. backdrop-filter: blur(4px);
  444. z-index: 2000;
  445. padding: 20px;
  446. overflow-y: auto;
  447. .edit-hot-item {
  448. margin: 100px auto 20px;
  449. width: 400px;
  450. padding: 20px;
  451. background: rgba(27, 27, 28, 0.8);
  452. box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
  453. border-radius: 4px;
  454. .input {
  455. margin-bottom: 10px;
  456. }
  457. }
  458. }
  459. .edit-hot {
  460. margin-top: 20px;
  461. text-align: right;
  462. span {
  463. font-size: 14px;
  464. color: rgba(255, 255, 255, 0.6);
  465. cursor: pointer;
  466. }
  467. }
  468. .edit-close {
  469. position: absolute;
  470. cursor: pointer;
  471. top: calc((100% - 18px) / 2);
  472. right: 0;
  473. transform: translateY(-50%);
  474. }
  475. .edit-title {
  476. padding-bottom: 18px;
  477. margin-bottom: 18px;
  478. position: relative;
  479. &::after {
  480. content: "";
  481. position: absolute;
  482. left: -20px;
  483. right: -20px;
  484. height: 1px;
  485. bottom: 0;
  486. background-color: rgba(255, 255, 255, 0.16);
  487. }
  488. }
  489. .slider-demo-block {
  490. position: absolute;
  491. right: calc(var(--editor-menu-right) + var(--editor-toolbox-width)) !important;
  492. top: 50%;
  493. transform: translateY(-50%);
  494. margin-right: 20px;
  495. z-index: 99;
  496. height: 300px;
  497. }
  498. </style>