LightMap.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <template>
  2. <div
  3. class="light-map"
  4. :class="{
  5. 'full': full ,
  6. 'small': small
  7. }"
  8. >
  9. <slot />
  10. <map
  11. id="prevMap"
  12. map-id="prevMap"
  13. class="light-map-map"
  14. :enable-poi="false"
  15. :show-location="true"
  16. :scale="12"
  17. :longitude="lonlat?.longitude"
  18. :latitude="lonlat?.latitude"
  19. @markertap="onMarkerTap"
  20. />
  21. <FlexCol v-if="isEmptyRegion"
  22. gap="gap.xl"
  23. position="absolute"
  24. inset="0"
  25. padding="padding.md"
  26. center
  27. >
  28. <FlexCol center gap="gap.xl" padding="padding.md" backgroundColor="white" radius="radius.md">
  29. <Text fontConfig="importantTitle" textAlign="center">您选择的地区还未开通亮乡源数据,可联系客服开通</Text>
  30. <button open-type="contact" class="remove-button-style">
  31. <FlexCol padding="space.md" radius="radius.lg" center backgroundColor="button">
  32. <Text fontConfig="primaryTitle">联系客服</Text>
  33. </FlexCol>
  34. </button>
  35. </FlexCol>
  36. </FlexCol>
  37. <FlexCol v-if="mapLoader.error.value"
  38. gap="gap.md"
  39. position="absolute"
  40. inset="0"
  41. center
  42. >
  43. <FlexCol center gap="gap.md" padding="padding.md" backgroundColor="white" radius="radius.md">
  44. <Icon name="warning" size="44" color="red" />
  45. <Text textAlign="center" :text="mapLoader.error.value" />
  46. </FlexCol>
  47. </FlexCol>
  48. <FlexCol v-else-if="mapLoader.status.value === 'loading'"
  49. gap="gap.md"
  50. position="absolute"
  51. inset="0"
  52. center
  53. >
  54. <ActivityIndicator :size="64" />
  55. </FlexCol>
  56. <NButton
  57. :innerStyle="{
  58. position: 'absolute',
  59. bottom: '20rpx',
  60. left: '20rpx',
  61. zIndex: 100,
  62. backgroundColor: '#ffffff',
  63. }"
  64. icon="search"
  65. @click="showSearch"
  66. />
  67. <SimpleDropDownPicker
  68. v-if="!isEmptyRegion"
  69. class="light-map-region-picker"
  70. :modelValue="selectedRegion"
  71. @update:modelValue="onSelectedRegion"
  72. :columns="regionLoader.content.value"
  73. />
  74. <NButton
  75. :innerStyle="{
  76. position: 'absolute',
  77. bottom: '20rpx',
  78. right: '20rpx',
  79. zIndex: 100,
  80. backgroundColor: '#ffffff',
  81. }"
  82. text="定位"
  83. icon="navigation"
  84. @click="emit('getCurrentLonlat')"
  85. />
  86. <Dialog
  87. v-model:show="searchDialogShow"
  88. title="搜索村社"
  89. showCancel
  90. :maskClosable="false"
  91. :onConfirm="searchConfirm"
  92. >
  93. <template #content>
  94. <Field v-model="searchKeyword" placeholder="请输入村社名称进行搜索" />
  95. </template>
  96. </Dialog>
  97. </div>
  98. </template>
  99. <script setup lang="ts">
  100. import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
  101. import { navTo } from '@/components/utils/PageAction';
  102. import { waitTimeOut } from '@imengyu/imengyu-utils';
  103. import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
  104. import LightVillageApi, { VillageListItem } from '@/api/light/LightVillageApi';
  105. import AppCofig, { isDev } from '@/common/config/AppCofig';
  106. import SimpleDropDownPicker from '@/common/components/SimpleDropDownPicker.vue';
  107. import NButton from '@/components/basic/Button.vue';
  108. import FlexCol from '@/components/layout/FlexCol.vue';
  109. import Text from '@/components/basic/Text.vue';
  110. import CommonContent from '@/api/CommonContent';
  111. import Icon from '@/components/basic/Icon.vue';
  112. import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
  113. import type { MapMarker } from '@/types/Map';
  114. import MapApi from '@/api/map/MapApi';
  115. import Dialog from '@/components/dialog/Dialog.vue';
  116. import Field from '@/components/form/Field.vue';
  117. import { toast } from '@/components/dialog/CommonRoot';
  118. const instance = getCurrentInstance();
  119. const mapCtx = uni.createMapContext('prevMap', instance);
  120. const selectedRegion = ref<number>();
  121. const nextNeedAutoFocus = ref(false);
  122. const villageData = new Map<number, VillageListItem>();
  123. const ready = ref(false);
  124. const hasResItems = ref(false);
  125. const searchDialogShow = ref(false);
  126. const searchKeyword = ref('');
  127. const emit = defineEmits([
  128. 'getCurrentLonlat',
  129. 'update:isLightMode',
  130. 'update:lonlat',
  131. 'selectVillage',
  132. 'regionChanged',
  133. 'lightVillage',
  134. ]);
  135. const props = defineProps<{
  136. lonlat?: { longitude: number, latitude: number } | undefined;
  137. city?: string;
  138. isLightMode?: boolean;
  139. small?: boolean;
  140. full?: boolean;
  141. }>();
  142. const regionLoader = useSimpleDataLoader(async () => {
  143. if (!props.city)
  144. return [];
  145. return (await CommonContent.searchRegion(props.city)).map(p => ({
  146. id: p.id,
  147. name: p.title,
  148. }));
  149. }, false);
  150. const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
  151. mapCtx.removeMarkers({
  152. markerIds: Array.from(villageData.keys()),
  153. })
  154. villageData.clear();
  155. if (!selectedRegion.value)
  156. return [];
  157. await waitTimeOut(200);
  158. const res = (await LightVillageApi.getVillageList({
  159. region: selectedRegion.value,
  160. keyword: searchKeyword.value.trim() || undefined,
  161. page: 1,
  162. pageSize: 1000
  163. })).list;
  164. hasResItems.value = res.length > 0;
  165. const list = res.map((p, i) => {
  166. villageData.set(p.id, p);
  167. const maker : MapMarker = {
  168. id: p.id ?? i,
  169. title: p.name,
  170. longitude: Number(p.longitude),
  171. latitude: Number(p.latitude),
  172. width: 30,
  173. height: 30,
  174. iconPath: '',
  175. joinCluster: true,
  176. callout: {
  177. display: 'ALWAYS',
  178. content: p.name,
  179. color: '#000000',
  180. fontSize: 12,
  181. padding: 5,
  182. bgColor: '#ffffff',
  183. borderRadius: 5,
  184. },
  185. }
  186. if (p.isLight) {
  187. if (p.lightValue >= 1) {
  188. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light3.png`;
  189. } else if (p.lightValue >= 0.5) {
  190. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light2.png`;
  191. } else if (p.lightValue >= 0.25) {
  192. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light1.png`;
  193. } else {
  194. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light1.png`;
  195. }
  196. const size = Math.floor(35 +(p.lightValue) * 10);
  197. maker.width = size;
  198. maker.height = size;
  199. } else {
  200. maker.width = 35;
  201. maker.height = 35;
  202. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/LightUnLight.png`;
  203. }
  204. return maker as MapMarker;
  205. }).filter(p =>
  206. p.longitude && p.latitude
  207. && !isNaN(p.longitude) && !isNaN(p.latitude)
  208. && p.longitude > -180 && p.longitude < 180
  209. && p.latitude > -90 && p.latitude < 90
  210. );
  211. mapCtx.addMarkers({
  212. clear: true,
  213. markers: list,
  214. })
  215. if (nextNeedAutoFocus.value) {
  216. if (res.length > 0) {
  217. setTimeout(() => {
  218. mapCtx.includePoints({
  219. points: list.map(p => {
  220. if (!p.longitude || !p.latitude) {
  221. p.longitude = AppCofig.defaultLonLat[0];
  222. p.latitude = AppCofig.defaultLonLat[1];
  223. }
  224. return {
  225. latitude: p.latitude,
  226. longitude: p.longitude,
  227. }
  228. }),
  229. padding: [20, 20, 20, 20],
  230. });
  231. }, 200);
  232. } else {
  233. try {
  234. const currentRegionName = regionLoader.content.value?.find(p => p.id == selectedRegion.value)?.name;
  235. if (currentRegionName) {
  236. const res = (await MapApi.simpleGetRegion(currentRegionName)).requireData();
  237. mapCtx.moveToLocation({
  238. latitude: Number(res.center.split(',')[1]),
  239. longitude: Number(res.center.split(',')[0]),
  240. });
  241. }
  242. } catch (error) {
  243. console.error(error);
  244. }
  245. }
  246. }
  247. ready.value = true;
  248. return list;
  249. }, false, false);
  250. const isEmptyRegion = computed(() => {
  251. return !hasResItems.value && ready.value;
  252. });
  253. function onMarkerTap(e: any) {
  254. if (props.isLightMode) {
  255. emit('update:isLightMode', false);
  256. const village = villageData.get(e.markerId);
  257. if (village) {
  258. emit('lightVillage', village);
  259. return;
  260. }
  261. }
  262. const village = villageData.get(e.markerId);
  263. if (village) {
  264. emit('selectVillage', village);
  265. }
  266. }
  267. function onSelectedRegion(regionId: number) {
  268. selectedRegion.value = regionId;
  269. nextNeedAutoFocus.value = true;
  270. mapLoader.reload();
  271. emit('regionChanged', regionId);
  272. }
  273. function setCurrentRegion(regionName: string) {
  274. const region = regionLoader.content.value?.find(p => p.name == regionName)?.id ||
  275. regionLoader.content.value?.[0]?.id || undefined;
  276. if (region === selectedRegion.value)
  277. return;
  278. selectedRegion.value = region;
  279. emit('regionChanged', selectedRegion.value);
  280. mapLoader.reload();
  281. }
  282. function showSearch() {
  283. searchDialogShow.value = true;
  284. searchKeyword.value = '';
  285. }
  286. async function searchConfirm() {
  287. searchDialogShow.value = false;
  288. if (searchKeyword.value.trim() === '') {
  289. toast('请输入搜索关键词');
  290. return;
  291. }
  292. await mapLoader.reload();
  293. }
  294. watch(() => props.city, async (newVal) => {
  295. await regionLoader.reload();
  296. setCurrentRegion(newVal || '');
  297. }, { immediate: true });
  298. onMounted(async () => {
  299. mapCtx.initMarkerCluster({
  300. enableDefaultStyle: false,
  301. zoomOnClick: true,
  302. gridSize: 40,
  303. });
  304. mapCtx.on('markerClusterCreate', (e: { clusters: any[] }) => {
  305. const customClusters = e.clusters.map((cluster) => {
  306. const { center, clusterId, markerIds } = cluster;
  307. return {
  308. ...center,
  309. width: 0,
  310. height: 0,
  311. clusterId,
  312. label: {
  313. content: markerIds.length.toString(), // 聚合点的数量
  314. fontSize: 16,
  315. color: '#fff',
  316. width: 30,
  317. height: 30,
  318. bgColor: '#8bb346', // 背景颜色
  319. borderRadius: 25,
  320. textAlign: 'center',
  321. anchorX: -10,
  322. anchorY: -35,
  323. },
  324. };
  325. });
  326. mapCtx.addMarkers({
  327. markers: customClusters,
  328. clear: false,
  329. });
  330. });
  331. });
  332. </script>
  333. <style lang="scss">
  334. .light-map {
  335. position: relative;
  336. width: 100%;
  337. height: 600rpx;
  338. border-radius: 30rpx;
  339. overflow: hidden;
  340. border: 1px solid #d45652;
  341. &.full {
  342. height: 100vh;
  343. border-radius: 0;
  344. .light-map-map {
  345. height: 100vh;
  346. }
  347. }
  348. &.small {
  349. height: 500rpx;
  350. border-radius: 20rpx;
  351. .light-map-map {
  352. height: 500rpx;
  353. }
  354. }
  355. .light-map-map {
  356. width: 100%;
  357. height: 600rpx;
  358. }
  359. .light-map-region-picker {
  360. position: absolute;
  361. bottom: 20rpx;
  362. left: 50%;
  363. transform: translateX(-50%);
  364. z-index: 100;
  365. }
  366. .light-map-address {
  367. position: absolute;
  368. bottom: 20rpx;
  369. right: 20rpx;
  370. z-index: 100;
  371. }
  372. }
  373. </style>