gemercheung 1 gadu atpakaļ
vecāks
revīzija
60960fee62

+ 2 - 0
.env.development

@@ -24,3 +24,5 @@ VITE_GLOB_UPLOAD_URL=/upload
 VITE_GLOB_API_URL_PREFIX=
 
 VITE_GOOGLE_KEY=AIzaSyBGUvCR1bppO9pfuS0MUWzuftiZ127y4Os
+
+VITE_DOMAIN=https://test-jp.4dkankan.com/

+ 2 - 0
.env.production

@@ -37,3 +37,5 @@ VITE_LEGACY = false
 
 
 VITE_GOOGLE_KEY=AIzaSyBGUvCR1bppO9pfuS0MUWzuftiZ127y4Os
+
+VITE_DOMAIN=https://test-jp.4dkankan.com/

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "@iconify/iconify": "^2.1.0",
     "@logicflow/core": "^0.7.16",
     "@logicflow/extension": "^0.7.16",
+    "@types/google.maps": "^3.55.12",
     "@vue/runtime-core": "^3.2.26",
     "@vue/shared": "^3.2.26",
     "@vueuse/core": "^7.4.1",

+ 8 - 0
pnpm-lock.yaml

@@ -28,6 +28,9 @@ importers:
       '@logicflow/extension':
         specifier: ^0.7.16
         version: 0.7.16
+      '@types/google.maps':
+        specifier: ^3.55.12
+        version: 3.55.12
       '@vue/runtime-core':
         specifier: ^3.2.26
         version: 3.2.26
@@ -1311,6 +1314,9 @@ packages:
   '@types/glob@7.2.0':
     resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
 
+  '@types/google.maps@3.55.12':
+    resolution: {integrity: sha512-Q8MsLE+YYIrE1H8wdN69YHHAF8h7ApvF5MiMXh/zeCpP9Ut745mV9M0F4X4eobZ2WJe9k8tW2ryYjLa87IO2Sg==}
+
   '@types/graceful-fs@4.1.5':
     resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==}
 
@@ -7981,6 +7987,8 @@ snapshots:
       '@types/minimatch': 3.0.5
       '@types/node': 17.0.8
 
+  '@types/google.maps@3.55.12': {}
+
   '@types/graceful-fs@4.1.5':
     dependencies:
       '@types/node': 17.0.8

+ 15 - 0
src/api/mapOpt/list.ts

@@ -12,6 +12,8 @@ enum Api {
   getScene = '/service/manage_jp/project/getScene',
   addScene = '/service/manage_jp/project/addScene',
   delScene = '/service/manage_jp/project/delScene',
+  createshare = '/service/manage_jp/projectScene/createSceneShare',
+  shareGpsScene = '/manage_jp/projectScene/getSceneMap',
 }
 
 type listType = {};
@@ -119,3 +121,16 @@ export const DelSceneApi = (params: AddSceneParamType) =>
       ignoreCancelToken: true,
     },
   });
+//
+export const CreateShareMapApi = (type: number) =>
+  defHttp.get<{
+    ciphertext: string;
+    type: number;
+  }>({
+    url: Api.createshare + `/${type}`,
+    // data: { type },
+    headers: {
+      // @ts-ignore
+      ignoreCancelToken: true,
+    },
+  });

+ 5 - 4
src/locales/lang/json/zh-CN.json

@@ -182,6 +182,7 @@
   "layout.header.tooltipExitFull": "退出全屏",
   "layout.header.tooltipLock": "锁定屏幕",
   "layout.header.tooltipNotify": "消息通知",
+  "layout.mapOpt.title": "项目管理",
   "layout.multipleTab.close": "关闭标签页",
   "layout.multipleTab.closeAll": "关闭全部标签页",
   "layout.multipleTab.closeLeft": "关闭左侧标签页",
@@ -254,6 +255,7 @@
   "layout.setting.triggerClick": "点击",
   "layout.setting.triggerHover": "悬停",
   "modal.atLeastOne": "请至少选择一项",
+  "routes.dashboard.mapOpt": "地图",
   "routes.archive.patchArchive": "批量归档",
   "routes.archive.patchRestore": "批量复原",
   "routes.archive.payStatus0": "正常",
@@ -494,6 +496,7 @@
   "routes.scenes.selectCameraMFirst": "请输入公司管理员账号",
   "routes.scenes.shootUserName": "拍摄账号",
   "routes.scenes.showMap": "首页显示",
+  "routes.scenes.snCode": "SN码",
   "routes.scenes.sortOrder": "排序",
   "routes.scenes.unAssistant": "取消协作",
   "routes.scenes.unAssistantConfirm": "是否确定取消协作?",
@@ -503,7 +506,6 @@
   "routes.scenes.userName": "员工名称:",
   "routes.scenes.viewCount": "场景访问量",
   "routes.scenes.webSite": "场景链接",
-  "routes.scenes.snCode": "SN码",
   "routes.staff.createTime": "创建时间",
   "routes.staff.deptName": "所属公司",
   "routes.staff.mobile": "手机",
@@ -605,6 +607,5 @@
   "sys.login.signUpFormTitle": "注册",
   "sys.login.smsCode": "短信验证码",
   "sys.login.smsPlaceholder": "请输入验证码",
-  "sys.login.userName": "企业邮箱",
-  "layout.mapOpt.title": "项目管理"
-}
+  "sys.login.userName": "企业邮箱"
+}

+ 126 - 57
src/views/map/addProjectModal.vue

@@ -2,7 +2,8 @@
   <BasicModal
     v-bind="$attrs"
     @register="register"
-    title="新增项目"
+    :title="isEditMode ? '项目信息' : '新增项目'"
+    :minHeight="380"
     @ok="handleSubmit"
     @cancel="handleCancel"
   >
@@ -10,8 +11,8 @@
       <BasicForm @register="registerForm">
         <template #location>
           <div class="map-select">
-            <a-input v-model:value="location" disabled />
-            <a-button type="default" @click="$emit('open-map')">地图选择</a-button>
+            <a-input v-model:value="location" allow-clear disabled />
+            <a-button type="default" @click="handleOpenMap">地图搜索</a-button>
           </div>
         </template>
       </BasicForm>
@@ -24,48 +25,9 @@
   import { BasicModal, useModalInner } from '/@/components/Modal';
   import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
   import { useI18n } from '/@/hooks/web/useI18n';
-  import { AddOptOrUpdateApi } from '/@/api/mapOpt/list';
+  import { AddOptOrUpdateApi, AllGpsApi } from '/@/api/mapOpt/list';
 
   const { t } = useI18n();
-  const schemas: FormSchema[] = [
-    {
-      field: 'projectName',
-      label: '项目名称',
-      component: 'Input',
-      required: true,
-      colProps: {
-        span: 24,
-      },
-    },
-    {
-      field: 'projectSn',
-      label: '项目编号',
-      required: true,
-      component: 'Input',
-      colProps: {
-        span: 24,
-      },
-    },
-    {
-      field: 'location',
-      label: '位置',
-      component: 'Input',
-      required: false,
-      slot: 'location',
-      colProps: {
-        span: 24,
-      },
-    },
-    {
-      field: 'isShow',
-      label: '是否展示',
-      component: 'Switch',
-      defaultValue: true,
-      colProps: {
-        span: 24,
-      },
-    },
-  ];
 
   export default defineComponent({
     components: { BasicModal, BasicForm },
@@ -77,16 +39,105 @@
         }>,
       },
     },
-    emits: ['register', 'success', 'open-map', 'reload', 'cancel'],
+    emits: ['register', 'success', 'open-map', 'reload', 'cancel', 'update-gps'],
     setup(props, { emit }) {
       // const { createMessage } = useMessage();
-      const sceneNum = ref('');
+      const isEditMode = ref(false);
+      const locationEdit = ref('');
+
+      // const customEditSelect: FormSchema = {
+      //   field: 'sceneID',
+      //   label: '选择场景',
+      //   component: 'Input',
+      // };
+      const schemas: FormSchema[] = [
+        {
+          field: 'id',
+          label: 'Id',
+          component: 'Input',
+          ifShow: false,
+        },
+        {
+          field: 'projectName',
+          label: '项目名称',
+          component: 'Input',
+          required: true,
+          colProps: {
+            span: 24,
+          },
+        },
+        {
+          field: 'projectSn',
+          label: '项目编号',
+          required: true,
+          component: 'Input',
+          colProps: {
+            span: 24,
+          },
+        },
+        {
+          field: 'location',
+          label: '位置',
+          component: 'Input',
+          required: false,
+          slot: 'location',
+          colProps: {
+            span: 24,
+          },
+        },
+        {
+          field: 'gpsNum',
+          label: '选择场景',
+          component: 'ApiSelect',
+          componentProps: {
+            api: AllGpsApi,
+            resultField: 'list',
+            labelField: 'title',
+            valueField: 'num',
+            showSearch: true,
+            optionFilterProp: 'label',
+            immediate: true,
+            listHeight: 160,
+            allowClear: true,
+
+            onSelect: (_, item) => {
+              const { lat, lon } = item;
+              console.log('选择场景', _, lat, lon);
+              if (lat && lon) {
+                emit('update-gps', {
+                  lat: lat,
+                  lng: lon,
+                });
+              }
+            },
+            params: {
+              page: 1,
+              type: 1,
+              searchKey: '',
+              limit: 5000,
+            },
+          },
+          ifShow: ({ values }) => {
+            return values?.id;
+          },
+        },
+        {
+          field: 'isShow',
+          label: '是否展示',
+          component: 'Switch',
+          defaultValue: true,
+          colProps: {
+            span: 24,
+          },
+        },
+      ];
+
       const location = computed(() =>
         props.currentLatLng ? `${props.currentLatLng?.lat}, ${props.currentLatLng?.lng}` : '',
       );
       const googleKey = computed(() => import.meta.env.VITE_GOOGLE_KEY);
 
-      const [registerForm, { setFieldsValue, validate }] = useForm({
+      const [registerForm, { setFieldsValue, resetFields, validate, getFieldsValue }] = useForm({
         schemas: schemas,
         labelWidth: 100,
         showActionButtonGroup: false,
@@ -97,23 +148,28 @@
         // submitFunc: handleSubmit,
       });
       const [register, { closeModal }] = useModalInner((data) => {
+        resetFields();
         data && onDataReceive(data);
       });
 
       function onDataReceive(data) {
-        console.log('Data Received', data, data.num);
-
-        setFieldsValue({
+        const allData = {
           ...data,
-        });
-
-        sceneNum.value = data.num;
+          isShow: Boolean(data.isShow),
+        };
+        console.log('Data Received', allData);
+        setFieldsValue(allData);
+        isEditMode.value = true;
+        // locationEdit.value = `${data?.lat}, ${data?.lon}`;
+        // locationEV.value.lat = data?.lat;
+        // locationEV.value.lng = data?.lon;
       }
       const handleSubmit = async () => {
         const data = await validate();
-        if (props.currentLatLng?.lat || props.currentLatLng?.lng) {
-        }
+        const allValue = getFieldsValue();
         const pro = {
+          ...allValue,
+          id: allValue.id,
           isShow: Number(data.isShow),
           projectName: data.projectName,
           projectSn: data.projectSn,
@@ -121,16 +177,26 @@
           lon: props.currentLatLng?.lng,
           alt: 0,
         };
-        const res = await AddOptOrUpdateApi(pro);
-        console.log('data', res);
+        if (!isEditMode.value) {
+          delete pro.id;
+        }
+        console.log('save', pro);
+        await AddOptOrUpdateApi(pro);
         closeModal();
         emit('reload');
       };
 
       const handleCancel = async () => {
-        // console.log('handleCancel');
+        resetFields();
         emit('cancel');
       };
+      const handleOpenMap = () => {
+        if (props.currentLatLng?.lat && props.currentLatLng?.lng) {
+          emit('open-map', props.currentLatLng);
+        } else {
+          emit('open-map');
+        }
+      };
 
       return {
         t,
@@ -142,6 +208,9 @@
         location,
         handleCancel,
         googleKey,
+        locationEdit,
+        isEditMode,
+        handleOpenMap,
       };
     },
   });

+ 190 - 99
src/views/map/index.vue

@@ -3,7 +3,7 @@
     ref="mapRef"
     :center="center"
     :api-key="googleKey"
-    mapId="DEMO_MAP_ID"
+    mapId="japan_4dkankan"
     :map-type-control="true"
     :disable-default-ui="false"
     :language="lang"
@@ -20,53 +20,72 @@
     :loading-tip="t('common.loadingText')"
   >
     <MarkerCluster>
-      <AdvancedMarker
-        v-for="(location, i) in locations"
-        :key="i"
-        :options="{ position: location }"
-        @click="handleMarkerClick(location)"
-      >
-        <!-- <InfoWindow>
-          <div id="content">
-            <div id="siteNotice"></div>
-            <h1 id="firstHeading" class="firstHeading">{{ location?.num }}</h1>
-            <div id="bodyContent">
-              <p>
-                <a :href="location?.webSite"> {{ location?.webSite }}</a>
-              </p>
-            </div>
-          </div>
-        </InfoWindow> -->
-      </AdvancedMarker>
+      <template v-if="form.type === 0">
+        <CustomMarker
+          v-for="(location, i) in locations"
+          :key="i"
+          :options="{
+            position: { lat: location.lat, lng: location.lng },
+            title: location.title,
+            anchorPoint: 'TOP_CENTER',
+          }"
+          @click="handleMarkerClick(location)"
+        >
+          <div class="custom-marker">{{ location.title }}</div>
+        </CustomMarker>
+      </template>
+      <template v-else>
+        <AdvancedMarker
+          v-for="(location, i) in locations"
+          :key="i"
+          :options="{
+            position: { lat: location.lat, lng: location.lng },
+            title: location.title,
+          }"
+          @click="handleMarkerClick(location)"
+        />
+      </template>
     </MarkerCluster>
 
     <CustomControl position="TOP_LEFT">
       <div class="top_left_control">
         <div>
-          <!-- <a-input
-            type="text"
-            style="width: 220px"
-            v-model:value="form.searchValue"
-            size="large"
-            :placeholder="t('common.inputText')"
-          >
-            <template #prefix><SearchOutlined /></template>
-          </a-input> -->
-
-          <AutoComplete
-            v-model:value="form.searchValue"
-            :options="options"
+          <ApiSelect
+            :api="projectFetch"
             size="large"
-            allowClear
+            numberToString
+            resultField="list"
+            labelField="title"
+            valueField="id"
+            optionFilterProp="label"
+            v-model:value="form.projectValue"
+            immediate
+            show-search
+            allow-clear
+            v-if="form.type === 0"
+            @select="handleSelect"
             style="width: 300px"
-            :placeholder="t('common.inputText')"
+            :params="{
+              type: 0,
+              projectId: projectId,
+              searchKey: form.searchValue,
+            }"
+          />
+          <ApiSelect
+            :api="sceneFetch"
+            v-if="form.type === 1"
+            size="large"
+            resultField="list"
+            labelField="title"
+            valueField="num"
+            optionFilterProp="label"
+            immediate
+            show-search
+            allow-clear
             @select="handleSelect"
-            @search="onSearch"
-          >
-            <template #option="{ title, value: num }">
-              <span style="font-weight: bold" :value="num">{{ title }}</span>
-            </template>
-          </AutoComplete>
+            style="width: 300px"
+            :params="{ type: 1, searchKey: form.searchValue }"
+          />
         </div>
         <div>
           <Select
@@ -74,60 +93,90 @@
             v-model:value="form.type"
             style="width: 120px"
             size="large"
+            @select="handleTypeSelect"
             :placeholder="t('common.chooseText')"
           >
             <SelectOption :value="0">全部项目</SelectOption>
             <SelectOption :value="1">全部场景</SelectOption>
           </Select>
         </div>
-
-        <div>
-          <!-- <ApiTreeSelect size="large" style="width: 120px" /> -->
-        </div>
       </div>
     </CustomControl>
 
-    <CustomControl position="LEFT_CENTER">
-      <button class="custom-btn">👋</button>
+    <CustomControl position="BOTTOM_CENTER">
+      <div class="share-container">
+        <Popover trigger="click" v-model:open="shareOpen">
+          <template #content>
+            <div class="share-content">
+              <p>扫码分享</p>
+              <QrCode :value="qrCodeShareUrl" :width="250" :height="250" />
+              <a-button text @click="handleCopy">复制链接</a-button>
+            </div>
+          </template>
+          <!-- <a-button type="primary">Hover me</a-button> -->
+          <a-button class="custom-btn" @click="handleShare">分享</a-button>
+        </Popover>
+      </div>
     </CustomControl>
   </GoogleMap>
 </template>
 
 <script lang="ts" setup>
   // import { SearchOutlined } from '@ant-design/icons-vue';
-  import { ref, onMounted, watch, computed } from 'vue';
+  import { ref, onMounted, watch, computed, watchEffect } from 'vue';
   import {
     GoogleMap,
     AdvancedMarker,
     MarkerCluster,
+    CustomMarker,
     // InfoWindow,
     CustomControl,
   } from 'vue3-google-map';
   import { useRouteQuery } from '@vueuse/router';
-  // import { Loading } from '/@/components/Loading';
-  import { Select, SelectOption, AutoComplete } from 'ant-design-vue';
-  // import { ApiTreeSelect } from '/@/components/Form';
-  // import { all } from './test';
+  import { Select, SelectOption, Popover } from 'ant-design-vue';
+  import ApiSelect from '/@/components/Form/src/components/ApiSelect.vue';
+  import { useRouter } from 'vue-router';
   const loadingRef = ref(true);
   import { useI18n } from '/@/hooks/web/useI18n';
+  import { AllGpsApi, CreateShareMapApi } from '/@/api/mapOpt/list';
+  import { QrCode } from '/@/components/Qrcode/index';
+  import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
+  import { useMessage } from '/@/hooks/web/useMessage';
 
-  import { AllGpsApi } from '/@/api/mapOpt/list';
   const { t } = useI18n();
+  const router = useRouter();
   const googleKey = computed(() => import.meta.env.VITE_GOOGLE_KEY);
   const center = { lat: 35.717, lng: 139.731 };
-  const options = ref([]);
+  const { createMessage } = useMessage();
+  // const options = ref([]);
   // const pinOptions = { background: '#FBBC04' };
+  const shareOpen = ref(false);
+  const qrCodeShareUrl = ref('');
+  const { clipboardRef, isSuccessRef } = useCopyToClipboard(qrCodeShareUrl.value);
   const lang = useRouteQuery('lang', 'ja');
-  // console.log('lang', lang);
-
-  const form = ref({
+  const projectId = useRouteQuery('projectId');
+  const type = useRouteQuery('type', 0);
+  const form = ref<{
+    searchValue: null | string;
+    projectValue: null | string;
+    sceneValue: null | string;
+    type: number;
+  }>({
     searchValue: '',
-    type: 1,
-    orgId: undefined,
+    projectValue: '',
+    sceneValue: null,
+    type: 0,
   });
 
   const mapRef = ref();
-  const locations = ref([]);
+  const locations = ref<
+    {
+      lat: string;
+      lng: string;
+      title: string;
+      webSite: string;
+    }[]
+  >([]);
   // locations.value = locations.value.concat(all);
   console.log('total', locations.value.length);
 
@@ -137,7 +186,7 @@
       if (!ready) return;
       console.log('ready', ready, lo.value.length);
       loadingRef.value = false;
-      if (ready && lo.value.length > 2) {
+      if (ready && lo.value.length > 0) {
         // debugger;
         mapFitBounds(mapRef, locations.value);
       }
@@ -146,37 +195,25 @@
       deep: true,
     },
   );
-  const getGps = () => {
-    return AllGpsApi({ searchKey: form.value.searchValue, type: form.value.type });
-  };
+
   onMounted(async () => {
-    const allGps = await getGps();
-
-    console.log('allGps', allGps);
-    let data = allGps.map((item) => {
-      const mapper = {
-        lat: Number(item.lat),
-        lng: Number(item.lon),
-        webSite: item.webSite,
-        num: item.num,
-      };
-      return mapper;
+    watchEffect(() => {
+      console.log(projectId.value);
+      if (projectId.value) {
+        form.value.projectValue = String(projectId.value);
+      }
+      if (type.value) {
+        form.value.type = Number(type.value);
+      }
     });
-    locations.value = data;
   });
 
-  watch(
-    () => form,
-    async () => {
-      // const allGps = await getGps();
-      // // const opts = allGps.map
-      // options.value = allGps;
-      // console.log('allGps', allGps);
-    },
-    {
-      deep: true,
-    },
-  );
+  function reloadWithParams() {
+    console.log('reloadWithParams');
+    const routeData = router.resolve({ name: 'Map', query: { type: form.value.type } });
+    location.replace(routeData.href);
+    location.reload();
+  }
 
   function mapFitBounds(mapRef, markers) {
     let bounds;
@@ -198,23 +235,61 @@
   const handleMarkerClick = (data) => {
     // const { lat, lng } = event.latLng;
     console.log('handleMarkerClick', data);
-    window.open(data.webSite);
-    // infowindow.value = true;
-    // infowindowPosition.value.lat = lat();
-    // infowindowPosition.value.lng = lng();
-  };
-
-  const onSearch = async (item) => {
-    console.log('onSearch', item);
-    const allGps = await getGps();
-    // const opts = allGps.map
-    options.value = allGps;
-    // debugger;
+    data.webSite && window.open(data.webSite);
   };
 
   const handleSelect = (item: any) => {
     console.log('onSelect', item);
   };
+
+  const handleShare = async () => {
+    const res = await CreateShareMapApi(form.value.type);
+    console.log('handleShare', res);
+    const routeData = router.resolve({
+      name: 'Map',
+      query: { ciphertext: res.ciphertext, type: form.value.type },
+    });
+    const url = location.protocol + '//' + location.hostname + '/' + routeData.href;
+
+    qrCodeShareUrl.value = url;
+  };
+
+  const getMarkerData = (data) => {
+    return data.map((item) => {
+      const mapper = {} as any;
+      mapper.lat = Number(item.lat);
+      mapper.lng = Number(item.lon);
+      mapper.title = item.title;
+      mapper.webSite = item.webSite;
+      return mapper;
+    });
+  };
+
+  const projectFetch = async (params) => {
+    const res = await AllGpsApi(params);
+    const data = getMarkerData(res);
+    console.log('result', data.length);
+    locations.value = data;
+    return res;
+  };
+  const sceneFetch = async (params) => {
+    const res = await AllGpsApi(params);
+    const data = getMarkerData(res);
+    console.log('result', data.length);
+    locations.value = data;
+    return res;
+  };
+
+  const handleTypeSelect = () => {
+    reloadWithParams();
+  };
+
+  const handleCopy = () => {
+    clipboardRef.value = qrCodeShareUrl.value;
+    if (isSuccessRef.value) {
+      createMessage.success(t('routes.scenes.copyInfi.ok'));
+    }
+  };
 </script>
 <style lang="less">
   // @import './dark.less';
@@ -228,4 +303,20 @@
     flex-direction: row;
     gap: 25px;
   }
+  .share-container {
+    padding-bottom: 30px;
+  }
+  .custom-marker {
+    background: rgba(0, 0, 0, 0.6);
+    padding: 8px;
+    border-radius: 10px;
+    color: white;
+  }
+  .share-content {
+    min-width: 300px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+  }
 </style>

+ 32 - 9
src/views/map/list.vue

@@ -25,7 +25,7 @@
               // icon: 'mage:edit-fill',
               label: '项目信息',
               tooltip: '项目信息',
-              onClick: () => {},
+              onClick: () => handleOpenInfo(record),
             },
             {
               // color: 'warning',
@@ -39,7 +39,7 @@
               // icon: 'mage:edit-fill',
               label: '地图模式',
               tooltip: '地图模式',
-              onClick: () => {},
+              onClick: () => handleOpenMap(record),
             },
             {
               color: 'error',
@@ -58,11 +58,16 @@
     <AddProjectModal
       @register="registerAddProjectModal"
       @open-map="handleSelectMap"
+      @update-gps="(val) => (currentLatLng = val)"
       @reload="reload"
       @cancel="currentLatLng = undefined"
       :currentLatLng="currentLatLng"
     />
-    <MapSelectModal @register="registerMapSelectModal" @update="handleMapChose" />
+    <MapSelectModal
+      @register="registerMapSelectModal"
+      @update="handleMapUpdate"
+      :currentLatLng="currentLatLng"
+    />
     <SceneModal @register="registerSceneModal" />
   </div>
 </template>
@@ -82,6 +87,7 @@
   import AddProjectModal from './addProjectModal.vue';
   import MapSelectModal from './mapSelectModal.vue';
   import SceneModal from './sceneModal.vue';
+  import { useRouter } from 'vue-router';
 
   const localeStore = useLocaleStore();
 
@@ -103,6 +109,7 @@
       const [registerSceneModal, { openModal: openSceneModal }] = useModal();
       const { createMessage } = useMessage();
       const go = useGo();
+      const router = useRouter();
       const { t } = useI18n();
       const columns: BasicColumn[] = [
         {
@@ -178,16 +185,15 @@
       function handleCreate() {
         openAddProjectModal(true);
       }
-      function handleSelectMap() {
-        openMapModal(true);
+      function handleSelectMap(latLng: { lat: number; lng: number }) {
+        latLng ? openMapModal(true) : openMapModal(true, latLng);
       }
-      function handleMapChose(coord: { lat: number; lng: number }) {
-        console.log('handleMapChose', coord);
+      function handleMapUpdate(coord: { lat: number; lng: number }) {
+        console.log('handleMapUpdate', coord);
         currentLatLng.value = coord;
       }
       async function handleDelete(record: MapOptType) {
         await DelMapOptApi({ id: record.id });
-        // console.log('handleDelete', record, res);
         createMessage.success(t('common.optSuccess'));
         reload();
       }
@@ -201,6 +207,21 @@
         console.log('handleOpenScene', record);
         openSceneModal(true, record);
       }
+      function handleOpenMap(record: MapOptType) {
+        console.log('record', record);
+        const routeData = router.resolve({ name: 'Map', query: { projectId: record.id, type: 0 } });
+        window.open(routeData.href, '_blank');
+      }
+      function handleOpenInfo(record: MapOptType) {
+        console.log('record', record?.lat, record?.lon);
+        if (record?.lat && record?.lon) {
+          currentLatLng.value = {
+            lat: record.lat,
+            lng: record.lon,
+          };
+        }
+        openAddProjectModal(true, record);
+      }
 
       return {
         registerTable,
@@ -211,13 +232,15 @@
         handleCreate,
         registerMapSelectModal,
         handleSelectMap,
-        handleMapChose,
+        handleMapUpdate,
         currentLatLng,
         reload,
         handleDelete,
         handleUpdateSwitch,
         registerSceneModal,
         handleOpenScene,
+        handleOpenMap,
+        handleOpenInfo,
       };
     },
   });

+ 23 - 7
src/views/map/mapSelectModal.vue

@@ -101,10 +101,15 @@
   export default defineComponent({
     components: { BasicModal, GoogleMap, CustomControl },
     props: {
-      userData: { type: Object },
+      currentLatLng: {
+        type: Object as PropType<{
+          lat: number;
+          lng: number;
+        }>,
+      },
     },
     emits: ['register', 'success', 'update'],
-    setup(_, { emit }) {
+    setup(props, { emit }) {
       // const { createMessage } = useMessage();
       const sceneNum = ref('');
       const location = ref('');
@@ -130,8 +135,8 @@
         data && onDataReceive(data);
       });
       watch(
-        () => mapRef.value?.ready,
-        (ready) => {
+        () => [mapRef.value?.ready, props.currentLatLng],
+        ([ready, currentLatLng]) => {
           if (!ready) return;
           console.log('ready', ready);
 
@@ -147,7 +152,7 @@
             const input = document.getElementById('pac-input') as HTMLInputElement;
             const options = {
               bounds: defaultBounds,
-              componentRestrictions: { country: 'jp' },
+              componentRestrictions: { country: ['jp'] },
               fields: ['address_components', 'geometry', 'icon', 'name'],
               strictBounds: false,
             };
@@ -158,7 +163,7 @@
             console.log('map', map);
             autocomplete.bindTo('bounds', map);
             // @ts-ignore
-            searchMarkerRef.value = new google.maps.Marker({
+            searchMarkerRef.value = new google.maps.marker.AdvancedMarkerElement({
               map,
               // @ts-ignore
               anchorPoint: new google.maps.Point(0, -29),
@@ -189,6 +194,17 @@
               searchMarkerRef.value.setVisible(true);
             });
           }
+          if (ready && currentLatLng?.lat && currentLatLng?.lng) {
+            // debugger;
+            setTimeout(() => {
+              searchMarker.value.lat = Number(currentLatLng.lat);
+              searchMarker.value.lng = Number(currentLatLng.lng);
+              console.log('last-get', searchMarker.value);
+              searchMarkerRef.value.setPosition(searchMarker.value);
+              searchMarkerRef.value.setVisible(true);
+              map.setCenter(searchMarker.value);
+            }, 500);
+          }
         },
         {
           deep: true,
@@ -196,7 +212,7 @@
       );
 
       function onDataReceive(data) {
-        console.log('Data Received', data, data.num);
+        console.log('Data Received', data);
 
         setFieldsValue({
           ...data,

+ 322 - 0
src/views/map/share.vue

@@ -0,0 +1,322 @@
+<template>
+  <GoogleMap
+    ref="mapRef"
+    :center="center"
+    :api-key="googleKey"
+    mapId="japan_4dkankan"
+    :map-type-control="true"
+    :disable-default-ui="false"
+    :language="lang"
+    region="JP"
+    :zoom="7"
+    map-type-id="roadmap"
+    :mapTypeControlOptions="{
+      style: 1,
+      position: 3,
+    }"
+    style="width: 100vw; height: 100vh"
+    @click="handleMapClick"
+    v-loading="loadingRef"
+    :loading-tip="t('common.loadingText')"
+  >
+    <MarkerCluster>
+      <template v-if="form.type === 0">
+        <CustomMarker
+          v-for="(location, i) in locations"
+          :key="i"
+          :options="{
+            position: { lat: location.lat, lng: location.lng },
+            title: location.title,
+            anchorPoint: 'TOP_CENTER',
+          }"
+          @click="handleMarkerClick(location)"
+        >
+          <div class="custom-marker">{{ location.title }}</div>
+        </CustomMarker>
+      </template>
+      <template v-else>
+        <AdvancedMarker
+          v-for="(location, i) in locations"
+          :key="i"
+          :options="{
+            position: { lat: location.lat, lng: location.lng },
+            title: location.title,
+          }"
+          @click="handleMarkerClick(location)"
+        />
+      </template>
+    </MarkerCluster>
+
+    <CustomControl position="TOP_LEFT">
+      <div class="top_left_control">
+        <div>
+          <ApiSelect
+            :api="projectFetch"
+            size="large"
+            numberToString
+            resultField="list"
+            labelField="title"
+            valueField="id"
+            optionFilterProp="label"
+            v-model:value="form.projectValue"
+            immediate
+            show-search
+            allow-clear
+            v-if="form.type === 0"
+            @select="handleSelect"
+            style="width: 300px"
+            :params="{
+              type: 0,
+              projectId: projectId,
+              searchKey: form.searchValue,
+            }"
+          />
+          <ApiSelect
+            :api="sceneFetch"
+            v-if="form.type === 1"
+            size="large"
+            resultField="list"
+            labelField="title"
+            valueField="num"
+            optionFilterProp="label"
+            immediate
+            show-search
+            allow-clear
+            @select="handleSelect"
+            style="width: 300px"
+            :params="{ type: 1, searchKey: form.searchValue }"
+          />
+        </div>
+        <div>
+          <Select
+            ref="select"
+            v-model:value="form.type"
+            style="width: 120px"
+            size="large"
+            @select="handleTypeSelect"
+            :placeholder="t('common.chooseText')"
+          >
+            <SelectOption :value="0">全部项目</SelectOption>
+            <SelectOption :value="1">全部场景</SelectOption>
+          </Select>
+        </div>
+      </div>
+    </CustomControl>
+
+    <CustomControl position="BOTTOM_CENTER">
+      <div class="share-container">
+        <Popover trigger="click" v-model:open="shareOpen">
+          <template #content>
+            <div class="share-content">
+              <p>扫码分享</p>
+              <QrCode :value="qrCodeShareUrl" :width="250" :height="250" />
+              <a-button text @click="handleCopy">复制链接</a-button>
+            </div>
+          </template>
+          <!-- <a-button type="primary">Hover me</a-button> -->
+          <a-button class="custom-btn" @click="handleShare">分享</a-button>
+        </Popover>
+      </div>
+    </CustomControl>
+  </GoogleMap>
+</template>
+
+<script lang="ts" setup>
+  // import { SearchOutlined } from '@ant-design/icons-vue';
+  import { ref, onMounted, watch, computed, watchEffect } from 'vue';
+  import {
+    GoogleMap,
+    AdvancedMarker,
+    MarkerCluster,
+    CustomMarker,
+    // InfoWindow,
+    CustomControl,
+  } from 'vue3-google-map';
+  import { useRouteQuery } from '@vueuse/router';
+  import { Select, SelectOption, Popover } from 'ant-design-vue';
+  import ApiSelect from '/@/components/Form/src/components/ApiSelect.vue';
+  import { useRouter } from 'vue-router';
+  const loadingRef = ref(true);
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { AllGpsApi, CreateShareMapApi } from '/@/api/mapOpt/list';
+  import { QrCode } from '/@/components/Qrcode/index';
+  import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  const { t } = useI18n();
+  const router = useRouter();
+  const googleKey = computed(() => import.meta.env.VITE_GOOGLE_KEY);
+  const center = { lat: 35.717, lng: 139.731 };
+  const { createMessage } = useMessage();
+  // const options = ref([]);
+  // const pinOptions = { background: '#FBBC04' };
+  const shareOpen = ref(false);
+  const qrCodeShareUrl = ref('');
+  const { clipboardRef, isSuccessRef } = useCopyToClipboard(qrCodeShareUrl.value);
+  const lang = useRouteQuery('lang', 'ja');
+  const projectId = useRouteQuery('projectId');
+  const type = useRouteQuery('type', 0);
+  const form = ref<{
+    searchValue: null | string;
+    projectValue: null | string;
+    sceneValue: null | string;
+    type: number;
+  }>({
+    searchValue: '',
+    projectValue: '',
+    sceneValue: null,
+    type: 0,
+  });
+
+  const mapRef = ref();
+  const locations = ref<
+    {
+      lat: string;
+      lng: string;
+      title: string;
+      webSite: string;
+    }[]
+  >([]);
+  // locations.value = locations.value.concat(all);
+  console.log('total', locations.value.length);
+
+  watch(
+    () => [mapRef.value?.ready, locations],
+    ([ready, lo]) => {
+      if (!ready) return;
+      console.log('ready', ready, lo.value.length);
+      loadingRef.value = false;
+      if (ready && lo.value.length > 0) {
+        // debugger;
+        mapFitBounds(mapRef, locations.value);
+      }
+    },
+    {
+      deep: true,
+    },
+  );
+
+  onMounted(async () => {
+    watchEffect(() => {
+      console.log(projectId.value);
+      if (projectId.value) {
+        form.value.projectValue = String(projectId.value);
+      }
+      if (type.value) {
+        form.value.type = Number(type.value);
+      }
+    });
+  });
+
+  function reloadWithParams() {
+    console.log('reloadWithParams');
+    const routeData = router.resolve({ name: 'Map', query: { type: form.value.type } });
+    location.replace(routeData.href);
+    location.reload();
+  }
+
+  function mapFitBounds(mapRef, markers) {
+    let bounds;
+    const api = mapRef.value.api;
+    const map = mapRef.value.map;
+
+    bounds = new api.LatLngBounds();
+    for (let i = 0; i < markers.length; i++) {
+      bounds.extend(markers[i]);
+    }
+    map.fitBounds(bounds);
+    map.panToBounds(bounds);
+  }
+  const handleMapClick = (event) => {
+    const { lat, lng } = event.latLng;
+    console.log('event', `{ lat: ${lat()}, lng: ${lng()}}`);
+  };
+
+  const handleMarkerClick = (data) => {
+    // const { lat, lng } = event.latLng;
+    console.log('handleMarkerClick', data);
+    data.webSite && window.open(data.webSite);
+  };
+
+  const handleSelect = (item: any) => {
+    console.log('onSelect', item);
+  };
+
+  const handleShare = async () => {
+    const res = await CreateShareMapApi(form.value.type);
+    console.log('handleShare', res);
+    const routeData = router.resolve({
+      name: 'Map',
+      query: { ciphertext: res.ciphertext, type: form.value.type },
+    });
+    const url = location.protocol + '//' + location.hostname + '/' + routeData.href;
+
+    qrCodeShareUrl.value = url;
+  };
+
+  const getMarkerData = (data) => {
+    return data.map((item) => {
+      const mapper = {} as any;
+      mapper.lat = Number(item.lat);
+      mapper.lng = Number(item.lon);
+      mapper.title = item.title;
+      mapper.webSite = item.webSite;
+      return mapper;
+    });
+  };
+
+  const projectFetch = async (params) => {
+    const res = await AllGpsApi(params);
+    const data = getMarkerData(res);
+    console.log('result', data.length);
+    locations.value = data;
+    return res;
+  };
+  const sceneFetch = async (params) => {
+    const res = await AllGpsApi(params);
+    const data = getMarkerData(res);
+    console.log('result', data.length);
+    locations.value = data;
+    return res;
+  };
+
+  const handleTypeSelect = () => {
+    reloadWithParams();
+  };
+
+  const handleCopy = () => {
+    clipboardRef.value = qrCodeShareUrl.value;
+    if (isSuccessRef.value) {
+      createMessage.success(t('routes.scenes.copyInfi.ok'));
+    }
+  };
+</script>
+<style lang="less">
+  // @import './dark.less';
+  #map-container {
+    width: 100vw;
+    height: 100vh;
+  }
+  .top_left_control {
+    padding: 20px;
+    display: flex;
+    flex-direction: row;
+    gap: 25px;
+  }
+  .share-container {
+    padding-bottom: 30px;
+  }
+  .custom-marker {
+    background: rgba(0, 0, 0, 0.6);
+    padding: 8px;
+    border-radius: 10px;
+    color: white;
+  }
+  .share-content {
+    min-width: 300px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+  }
+</style>