ConfigEdit.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <template>
  2. <div class="page">
  3. <div class="page-head flex items-center">
  4. <div class="head-opt flex-1 flex justify-between items-center" style="margin-left: 0px">
  5. <div>
  6. <!-- <span>场景:</span> -->
  7. <!-- <el-select
  8. v-model="selectedCompany"
  9. class="m-2"
  10. placeholder="请选择相关公司"
  11. style="width: 216px"
  12. @change="changeCompany"
  13. >
  14. <el-option
  15. v-for="item in scenesInfos"
  16. :key="item.id"
  17. :label="item.name"
  18. :value="item.id"
  19. />
  20. </el-select> -->
  21. </div>
  22. <!-- <div v-if="selectedCompany" style="display: flex">
  23. <div style="display: flex">
  24. <div class="label-workshop" style="margin-left: 0px">选择标签:</div>
  25. <div>
  26. <el-radio-group
  27. v-model="label"
  28. :border="true"
  29. style="display: flex"
  30. @change="changeShop"
  31. >
  32. <el-radio-button
  33. v-for="item in labelList"
  34. :key="item.id"
  35. :label="item.id!"
  36. class="label-select"
  37. >{{ item.name }}</el-radio-button
  38. >
  39. </el-radio-group></div
  40. >
  41. </div>
  42. </div> -->
  43. <div class="top-bar">
  44. <div class="back-btn" @click="router.back">
  45. <img src="@/assets/rollback.png" />
  46. <span>返回</span>
  47. <div class="company-name">{{ companyName }}</div>
  48. </div>
  49. <!-- <el-button @click="toJson">tojson</el-button> -->
  50. <div class="operation-btns">
  51. <el-upload
  52. class="avatar-uploader"
  53. :action="actionUrl"
  54. :show-file-list="false"
  55. :on-success="handleAvatarSuccess"
  56. :before-upload="handleBeforeUpload"
  57. :with-credentials="true"
  58. name="file"
  59. :data="{ companyId, deleteFileName: bgImg }"
  60. :headers="getHeaders()"
  61. >
  62. <el-button :icon="Refresh"> 替换照片 </el-button>
  63. </el-upload>
  64. <el-button :icon="Refresh" @click="clearLayout"> 重置 </el-button>
  65. <el-button @click="handleSave" type="primary" :disabled="!companyId"> 保存 </el-button>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. <div class="paint-tool">
  71. <div class="camera-list">
  72. <div>
  73. <span class="label-text flex">车间列表:</span>
  74. <ElInput
  75. class="search-put"
  76. style="margin: 10px 0; width: 230px"
  77. placeholder="请输入搜索内容"
  78. v-model="searchKey"
  79. :suffix-icon="Search"
  80. />
  81. </div>
  82. <!-- <span v-if="filterShopList.length == 0" class="ml-1" style="color: #3f3f3f">
  83. 提示:请先选择相应公司和图片
  84. </span> -->
  85. <el-scrollbar style="position: relative; height: calc(100% - 90px)">
  86. <div
  87. v-for="item in filterShopList"
  88. :key="item.code"
  89. class="camera-item flex justify-start items-center"
  90. :class="{
  91. isAdded: isAddedShop(item.id),
  92. isActive: item.id === Number(activeShopId),
  93. }"
  94. @click="handleAddShop(item)"
  95. >
  96. <span class="camera-id">{{ item.name }}</span>
  97. </div>
  98. </el-scrollbar>
  99. </div>
  100. <div ref="drawContainer" id="drawContainer" class="draw-container">
  101. <div id="shopEditContainer" v-moveable:1 class="shop-edit-container">
  102. <MapContainer
  103. ref="mapContainerRef"
  104. :is-mobile-view="isMobileView"
  105. :module-code="labelMouduleCode"
  106. @on-open="openConfig"
  107. />
  108. </div>
  109. <el-upload
  110. v-if="!hasBg"
  111. class="upload-icon flex justify-center items-center"
  112. :action="actionUrl"
  113. :show-file-list="false"
  114. :before-upload="handleBeforeUpload"
  115. :on-success="handleAvatarSuccess"
  116. :with-credentials="true"
  117. name="file"
  118. :data="{ companyId }"
  119. :headers="getHeaders()"
  120. >
  121. <img src="~@/assets/images/img-upload.png" />
  122. </el-upload>
  123. </div>
  124. <div class="shop-tag-edit-area" v-if="hasBg">
  125. <ShopTagEditArea @transformer-change="transformChange" />
  126. </div>
  127. </div>
  128. <!-- <el-tooltip
  129. class="box-item position-tooltip"
  130. effect="dark"
  131. content="显示侧边栏"
  132. :offset="12"
  133. placement="left"
  134. >
  135. <div
  136. v-if="leftShow && activeShopId && !isMobileView && activeShopId != -1"
  137. class="circle-rectangle"
  138. :class="{ 'shape-shadow': shadow }"
  139. @mouseover="shadowAdd"
  140. @mouseout="shadowRemove"
  141. @click="dialogReopen"
  142. >
  143. <el-icon class="left-icon" size="16px"><ArrowLeftBold /></el-icon>
  144. <el-icon style="margin-top: 7px" size="16px"><ArrowLeftBold /></el-icon>
  145. </div>
  146. </el-tooltip> -->
  147. <!-- <ConfigDialog
  148. v-if="!isMobileView"
  149. ref="configDrawer"
  150. @on-close="onClose"
  151. @transformer-change="transformChange"
  152. class="drawer-position"
  153. /> -->
  154. <ConfigFinish
  155. :visible="visibleResult"
  156. :status="configStatus"
  157. @on-close="closeResult"
  158. class="feedback-position"
  159. />
  160. </div>
  161. </template>
  162. <script setup lang="ts">
  163. import ConfigDialog from './component/ConfigDrawer.vue';
  164. import ConfigFinish from './component/ConfigFinish.vue';
  165. import { storeToRefs } from 'pinia';
  166. import { ElMessage, ElInput, ElSwitch, ElMessageBox } from 'element-plus';
  167. import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
  168. import { getWorkshopListApi } from '@/api/scene/scene';
  169. import { computed } from 'vue';
  170. import { Search, Refresh, ArrowLeftBold } from '@element-plus/icons-vue';
  171. import usePageConfig from './usePageConfig';
  172. import MapContainer from './component/mapContainer/MapContainer.vue';
  173. import useMapEditor, { LabelPositionEnum } from './stores/useMapEditor';
  174. import { uploadCompanyLayout, updateCompanyLayout, getCompanyLayoutApi } from '@/api/scene/scene';
  175. import safeParse from '@/utils/safeParse';
  176. import { useRouter } from 'vue-router';
  177. import urlJoin from 'url-join';
  178. import { useGlobSetting } from '@/hooks/setting';
  179. import { getHeaders } from '@/utils/http/axios';
  180. import { CompanyLayoutType, LayoutType, ShopType } from '@/types/page-config/type';
  181. import ShopTagEditArea from './component/ShopTagEditArea.vue';
  182. const mapEditor = useMapEditor();
  183. const { bgImg, addedShops, activeShopId, showShops } = storeToRefs(mapEditor);
  184. const { addShop, addBg, calcLayout, resetMap, createMap, deleteShop } = mapEditor;
  185. const router = useRouter();
  186. const pageConfig = usePageConfig();
  187. const { scenesInfos, label, labelList, layoutId } = pageConfig;
  188. const { urlPrefix } = useGlobSetting();
  189. const drawContainer = ref<HTMLDivElement>();
  190. const mapContainerRef = ref();
  191. const searchKey = ref('');
  192. // 是否已有背景图
  193. const hasBg = ref(false);
  194. // 是否开启移动端视图
  195. const isMobileView = ref(false);
  196. const companyId = ref<number>();
  197. const companyName = ref<string>();
  198. const viewType = ref<number>();
  199. const shopList = ref<ShopType[]>([]); // 车间列表
  200. // // mapContainer宽高
  201. // const mapContainerStageSize = reactive({ height: 0, width: 777 });
  202. const handleBeforeUpload = (rawFile) => {
  203. // if (!selectedCompany.value || !label.value) {
  204. // ElMessage.error({
  205. // message: '请先选择公司和标签',
  206. // });
  207. // return false;
  208. // }
  209. if (
  210. rawFile.type !== 'image/jpg' &&
  211. rawFile.type !== 'image/jpeg' &&
  212. rawFile.type !== 'image/png'
  213. ) {
  214. ElMessage.error('请上传jpg、jpeg、png格式的图片!');
  215. return false;
  216. }
  217. };
  218. /** 判断相机是否已经添加 */
  219. const isAddedShop = (shopId: number) => {
  220. const index = addedShops.value.findIndex((item) => item.id === shopId);
  221. return index >= 0;
  222. };
  223. //左边的浮动按钮
  224. const leftShow = ref(true);
  225. const onClose = (val) => {
  226. leftShow.value = val;
  227. };
  228. const transformChange = () => {
  229. mapContainerRef.value.updateLayoutTransformer();
  230. };
  231. const actionUrl = computed(() => {
  232. // return urlJoin(urlPrefix, '/homepageConfig/updateCompanyPicture');
  233. return urlJoin(urlPrefix, '/admin/homepageConfig/uploadCompanyPicture');
  234. });
  235. watch(
  236. () => showShops,
  237. () => {
  238. transformChange();
  239. },
  240. { deep: true },
  241. );
  242. const shadow = ref(false);
  243. const shadowAdd = () => {
  244. shadow.value = true;
  245. };
  246. const shadowRemove = () => {
  247. shadow.value = false;
  248. };
  249. const configDrawer = ref();
  250. const dialogReopen = () => {
  251. configDrawer.value.openDialog();
  252. };
  253. const openConfig = () => {
  254. configDrawer.value.openDialog();
  255. };
  256. //总体的保存,将整个数据传过去
  257. const visibleResult = ref(false);
  258. const configStatus = ref(true);
  259. const closeResult = () => {
  260. visibleResult.value = false;
  261. };
  262. /** ------------- */
  263. const handleAvatarSuccess = (e) => {
  264. bgImg.value = e.data;
  265. hasBg.value = true;
  266. addBg();
  267. };
  268. const labelMouduleCode = computed(() => {
  269. return (
  270. scenesInfos.value
  271. .find((item) => item.id === companyId.value)
  272. ?.labelModuleList.find((item) => item.sceneLabel.id === label.value)?.sceneModule.code ||
  273. 'default'
  274. );
  275. });
  276. const changeShop = () => {
  277. hasBg.value = false;
  278. resetMap();
  279. getShopContent();
  280. };
  281. const getShopContent = () => {
  282. // getCompanyLayoutApi({ companyId: selectedCompany.value || 2 }).then((res) => {
  283. getWorkshopListApi({ companyId: companyId.value! }).then((res) => {
  284. shopList.value = res;
  285. });
  286. getCompanyLayoutApi({
  287. companyId: companyId.value!,
  288. }).then((res) => {
  289. if (!res) {
  290. layoutId.value = undefined;
  291. return;
  292. }
  293. layoutId.value = res.id;
  294. const layoutJSON = res.layout ? safeParse(res.layout) : null;
  295. if (!layoutJSON) {
  296. return;
  297. }
  298. hasBg.value = true;
  299. createMap(layoutJSON);
  300. });
  301. };
  302. // const changeCompany = () => {
  303. const clearLayout = () => {
  304. // label.value = undefined;
  305. ElMessageBox.confirm('是否重置当前设置?', '提示', {
  306. confirmButtonText: '确认',
  307. cancelButtonText: '取消',
  308. type: 'warning',
  309. }).then(() => {
  310. resetMap();
  311. hasBg.value = false;
  312. });
  313. };
  314. const handleKeyDown = (e) => {
  315. // 删除键
  316. if (e.keyCode === 46 || e.code === 'Delete') {
  317. deleteShop();
  318. configDrawer.value.closeDialog();
  319. }
  320. };
  321. const filterShopList = computed(() => {
  322. const k = searchKey.value.trim();
  323. if (!k) return shopList.value;
  324. return shopList.value?.filter((x) => x.name?.includes(k));
  325. });
  326. const setNodePosition = (positionX = 50, positionY = 50) => {
  327. if (addedShops.value.find((item) => item.x === positionX && item.y === positionY)) {
  328. return setNodePosition(positionX, positionY + 50);
  329. } else return { positionX, positionY };
  330. };
  331. const handleAddShop = (shop: ShopType) => {
  332. //如果已经存在车间,则禁止加入
  333. const existingCamera = addedShops.value.find((item) => item.id === shop.id);
  334. if (existingCamera) return;
  335. // activeShopId.value = shop.id;
  336. if (!hasBg.value) {
  337. ElMessage.warning({
  338. message: '请先添加背景图片',
  339. });
  340. return;
  341. }
  342. const nodePosition = setNodePosition();
  343. const shopNode = {
  344. ...shop,
  345. x: nodePosition.positionX,
  346. y: nodePosition.positionY,
  347. scaleX: 1,
  348. scaleY: 1,
  349. bgColor: 'rgba(58, 170, 209, 1)',
  350. fontSize: 14,
  351. fontColor: '#ffffff',
  352. posType: LabelPositionEnum.LEFT,
  353. };
  354. addShop(shopNode);
  355. // dialogReopen();
  356. };
  357. const handleSave = () => {
  358. const { json, scale } = mapContainerRef.value?.getLayout();
  359. console.log('layout json', JSON.parse(json));
  360. console.log('scale', scale);
  361. const layout = hasBg.value === false ? '' : calcLayout(json, scale);
  362. if (hasBg.value) console.log('Calc layout', JSON.parse(layout));
  363. const param = {
  364. layout,
  365. targetId: companyId.value || 2,
  366. labelId: label.value || 1,
  367. };
  368. if (!layoutId.value) {
  369. uploadCompanyLayout(param).then((res) => {
  370. layoutId.value = res;
  371. ElMessage.success('保存成功');
  372. });
  373. } else {
  374. updateCompanyLayout({ ...param, id: layoutId.value, viewType: viewType.value! }).then(
  375. (_res) => {
  376. ElMessage.success('更新成功');
  377. },
  378. );
  379. }
  380. };
  381. onBeforeUnmount(() => {
  382. window.removeEventListener('keydown', handleKeyDown);
  383. resetMap();
  384. });
  385. onMounted(() => {
  386. const routerParams = router.currentRoute.value.query;
  387. if (routerParams.companyId) {
  388. companyId.value = Number(routerParams.companyId);
  389. companyName.value = String(routerParams.companyName);
  390. viewType.value = Number(routerParams.viewType);
  391. }
  392. // if (routerParams.labelId) {
  393. // label.value = Number(routerParams.labelId);
  394. // }
  395. window.addEventListener('keydown', handleKeyDown);
  396. if (companyId.value) {
  397. getShopContent();
  398. }
  399. // console.log('clientHeight', document.getElementById('shopEditContainer')?.clientHeight);
  400. // console.log('clientWidth', document.getElementById('shopEditContainer')?.clientWidth);
  401. // mapContainerStageSize.height = document.getElementById('shopEditContainer')?.clientHeight!;
  402. // mapContainerStageSize.width = document.getElementById('shopEditContainer')?.clientWidth!;
  403. });
  404. </script>
  405. <style scoped lang="scss">
  406. .page {
  407. }
  408. .page-head {
  409. height: 54px;
  410. padding-left: 15px;
  411. background-color: #ffffff;
  412. }
  413. .head-opt {
  414. margin-left: 20px;
  415. padding-right: 15px;
  416. font-size: 14px;
  417. color: #3f3f3f;
  418. }
  419. .avatar-uploader {
  420. border-radius: 4px;
  421. margin-left: 30px;
  422. }
  423. .label-workshop {
  424. font-size: 14px;
  425. font-weight: 400;
  426. margin-left: 36px;
  427. margin-top: 6px;
  428. margin-right: 16px;
  429. }
  430. .upload-icon {
  431. position: absolute;
  432. top: 0;
  433. right: 0;
  434. left: 0;
  435. bottom: 0;
  436. margin: auto;
  437. }
  438. .paint-tool {
  439. position: relative;
  440. height: calc(100vh - 138px);
  441. margin-top: 2px;
  442. display: flex;
  443. justify-content: space-between;
  444. }
  445. .camera-list {
  446. width: 250px;
  447. padding: 0 10px;
  448. background-color: #ffffff;
  449. }
  450. .label-text {
  451. font-size: 14px;
  452. font-weight: 600;
  453. margin: 10px 0 5px 10px;
  454. }
  455. .camera-item {
  456. height: 32px;
  457. font-size: 14px;
  458. padding-left: 8px;
  459. font-weight: 400;
  460. color: #404040;
  461. line-height: 14px;
  462. cursor: pointer;
  463. &:hover {
  464. background-color: #e6f7ff;
  465. color: #1890ff;
  466. }
  467. }
  468. .isAdded {
  469. color: #1890ff;
  470. cursor: not-allowed;
  471. }
  472. .isActive {
  473. background-color: #e6f7ff;
  474. color: #1890ff;
  475. }
  476. .camera-item-disabled {
  477. color: #c6c6c6;
  478. }
  479. .camera-id {
  480. width: 110px;
  481. }
  482. .space-name {
  483. width: 120px;
  484. white-space: nowrap;
  485. overflow: hidden;
  486. text-overflow: ellipsis;
  487. }
  488. .draw-container {
  489. height: calc(100vh - 160px);
  490. position: relative;
  491. width: calc(100% - 600px);
  492. margin: 10px;
  493. overflow: hidden;
  494. border: 1px solid #1890ff;
  495. }
  496. .shop-edit-container {
  497. height: 100%;
  498. border: 1px solid #ff7118;
  499. }
  500. .drawer-position {
  501. position: absolute;
  502. right: 0px;
  503. // left: 1150px;
  504. top: 74px;
  505. z-index: 99;
  506. opacity: 1;
  507. background: #ffffff;
  508. }
  509. .dialog-btn {
  510. position: absolute;
  511. right: 0px;
  512. top: 66px;
  513. }
  514. .position-tooltip {
  515. margin-right: -10px;
  516. }
  517. .circle-rectangle {
  518. width: 40px;
  519. height: 30px;
  520. border-radius: 50% 0% 0% 50%;
  521. position: absolute;
  522. right: 0px;
  523. top: 66px;
  524. background-color: white;
  525. display: flex;
  526. }
  527. .shape-shadow {
  528. filter: drop-shadow(5px 5px 10px rgb(102, 100, 100));
  529. }
  530. .left-icon {
  531. margin-top: 7px;
  532. margin-left: 8px;
  533. margin-right: -7px;
  534. }
  535. .feedback-position {
  536. position: absolute;
  537. left: 760px;
  538. top: 250px;
  539. z-index: 9999;
  540. }
  541. .top-bar {
  542. width: 100%;
  543. display: flex;
  544. justify-content: space-between;
  545. align-items: center;
  546. .back-btn {
  547. display: flex;
  548. align-items: center;
  549. gap: 10px;
  550. cursor: pointer;
  551. img {
  552. width: 14px;
  553. height: 14px;
  554. }
  555. span {
  556. line-height: 14px;
  557. }
  558. .company-name {
  559. font-weight: 600;
  560. font-size: 14px;
  561. color: rgba(0, 0, 0, 0.85);
  562. }
  563. }
  564. .operation-btns {
  565. width: 350px;
  566. display: flex;
  567. align-items: center;
  568. .el-button {
  569. margin-left: 20px;
  570. }
  571. }
  572. }
  573. .shop-tag-edit-area {
  574. width: 300px;
  575. background-color: #ffffff;
  576. }
  577. :deep(.search-put .el-input__wrapper) {
  578. background-color: #f0f2f5;
  579. }
  580. :deep(.el-popper__arrow) {
  581. display: none;
  582. }
  583. :deep(.el-tree-node__content:hover) {
  584. background: #e6f4ff;
  585. }
  586. :deep(.el-button--primary) {
  587. --el-button-disabled-bg-color: #bfbfbf;
  588. }
  589. :deep(.el-popover) {
  590. width: unset !important;
  591. min-width: 110px;
  592. text-align: center;
  593. font-weight: 400;
  594. }
  595. ::v-deep.el-radio-button {
  596. margin-right: 8px;
  597. box-shadow: none;
  598. border-radius: 4px !important;
  599. border: 1px solid #d9d9d9 !important;
  600. }
  601. </style>
  602. ./MapBase/useCameraMap ./MapBase/CameraMapBak ./usePageConfig1