index.vue 16 KB

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