LightMap.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <template>
  2. <div
  3. class="light-map"
  4. :class="{
  5. 'full': full ,
  6. 'small': small
  7. }"
  8. @click="emit('mapClick')"
  9. >
  10. <slot />
  11. <map
  12. id="prevMap"
  13. map-id="prevMap"
  14. class="light-map-map"
  15. :style="{
  16. pointerEvents: small ? 'none' : undefined
  17. }"
  18. :show-location="true"
  19. :enable-poi="!small"
  20. :scale="12"
  21. :longitude="lonlat?.longitude"
  22. :latitude="lonlat?.latitude"
  23. @markertap="onMarkerTap"
  24. />
  25. <!-- 状态显示 -->
  26. <FlexCol v-if="mapLoader.error.value"
  27. gap="gap.md"
  28. position="absolute"
  29. inset="0"
  30. center
  31. >
  32. <FlexCol center gap="gap.md" padding="padding.md" backgroundColor="white" radius="radius.md">
  33. <Icon name="warning" size="44" color="red" />
  34. <Text textAlign="center" :text="mapLoader.error.value" />
  35. </FlexCol>
  36. </FlexCol>
  37. <FlexCol v-else-if="mapLoader.status.value === 'loading'"
  38. gap="gap.md"
  39. position="absolute"
  40. inset="0"
  41. center
  42. >
  43. <ActivityIndicator :size="64" />
  44. </FlexCol>
  45. <!-- 导航栏 -->
  46. <FlexCol
  47. position="absolute"
  48. :inset="{ l: 0, b: 0,r: 0 }"
  49. :zIndex="20"
  50. :padding="full ? [0, 10] : 20"
  51. >
  52. <FlexRow align="center" justify="space-between">
  53. <view v-if="small"></view>
  54. <template v-else>
  55. <BoxMid direction="row" wrap gap="gap.md" align="center" :padding="15">
  56. <NButton
  57. :text="city + ' ▼'"
  58. color="transparent"
  59. type="custom"
  60. :radius="30"
  61. :padding="30"
  62. :textProps="{
  63. maxWidth: 150,
  64. lines: 1
  65. }"
  66. @click="emit('changeCity')"
  67. />
  68. <SimpleDropDownPicker
  69. class="light-map-region-picker"
  70. backgroundColor="transparent"
  71. :modelValue="selectedRegion"
  72. @update:modelValue="onSelectedRegion"
  73. :columns="regionLoader.content.value"
  74. />
  75. <SimpleDropDownPicker
  76. class="light-map-region-picker"
  77. backgroundColor="transparent"
  78. v-model:modelValue="lightState"
  79. :columns="[
  80. {
  81. id: 1,
  82. name: '点亮状态',
  83. },
  84. {
  85. id: 2,
  86. name: '已点亮',
  87. },
  88. {
  89. id: 3,
  90. name: '未点亮',
  91. },
  92. ]"
  93. />
  94. <FrameButton
  95. icon="search"
  96. size="small"
  97. @click="showSearch"
  98. />
  99. <FrameButton
  100. icon="navigation"
  101. size="small"
  102. @click="emit('getCurrentLonlat')"
  103. />
  104. <Width />
  105. </BoxMid>
  106. </template>
  107. </FlexRow>
  108. <StatusBarSpace v-if="full" />
  109. </FlexCol>
  110. <!-- 上下渐变遮罩 -->
  111. <BackgroundBox
  112. v-if="full"
  113. position="fixed"
  114. :inset="{ l: 0, t: 0, r: 0 }"
  115. :height="500"
  116. :zIndex="19"
  117. color2="transparent"
  118. color1="white"
  119. />
  120. <BackgroundBox
  121. v-if="full"
  122. position="fixed"
  123. :inset="{ l: 0, b: 0, r: 0 }"
  124. :zIndex="19"
  125. :height="300"
  126. color1="transparent"
  127. color2="white"
  128. />
  129. </div>
  130. </template>
  131. <script setup lang="ts">
  132. import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
  133. import { assertNotNull, waitTimeOut } from '@imengyu/imengyu-utils';
  134. import { toast } from '@/components/dialog/CommonRoot';
  135. import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
  136. import LightVillageApi, { type VillageMapItem } from '@/api/light/LightVillageApi';
  137. import AppCofig from '@/common/config/AppCofig';
  138. import SimpleDropDownPicker from '@/common/components/SimpleDropDownPicker.vue';
  139. import NButton from '@/components/basic/Button.vue';
  140. import FlexCol from '@/components/layout/FlexCol.vue';
  141. import Text from '@/components/basic/Text.vue';
  142. import Icon from '@/components/basic/Icon.vue';
  143. import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
  144. import Dialog from '@/components/dialog/Dialog.vue';
  145. import Field from '@/components/form/Field.vue';
  146. import FlexRow from '@/components/layout/FlexRow.vue';
  147. import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
  148. import RegionApi from '@/api/map/RegionApi';
  149. import type { MapMarker } from '@/types/Map';
  150. import { hideLoading, loading } from '@/components/utils/DialogAction';
  151. import { showError } from '@/common/composeabe/ErrorDisplay';
  152. import FrameButton from '@/common/components/FrameButton.vue';
  153. import BoxMid from '@/common/components/box/BoxMid.vue';
  154. import Width from '@/components/layout/space/Width.vue';
  155. import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
  156. import { navTo } from '@/components/utils/PageAction';
  157. const instance = getCurrentInstance();
  158. const mapCtx = uni.createMapContext('prevMap', instance);
  159. const selectedRegion = ref<number>();
  160. const villageData = new Map<number, VillageMapItem>();
  161. const ready = ref(false);
  162. const hasResItems = ref(false);
  163. const emit = defineEmits([
  164. 'getCurrentLonlat',
  165. 'update:isLightMode',
  166. 'update:lonlat',
  167. 'update:district',
  168. 'selectVillage',
  169. 'regionChanged',
  170. 'lightVillage',
  171. 'createVillage',
  172. 'changeCity',
  173. 'mapClick',
  174. ]);
  175. const props = withDefaults(defineProps<{
  176. lonlat?: { longitude: number, latitude: number } | undefined;
  177. city?: string;
  178. cityCode?: number;
  179. district?: string;
  180. isLightMode?: boolean;
  181. small?: boolean;
  182. full?: boolean;
  183. }>(), {
  184. });
  185. const lightState = ref(1);
  186. const showLighted = computed(() => lightState.value === 2 || lightState.value === 1);
  187. const showUnLighted = computed(() => lightState.value === 3 || lightState.value === 1);
  188. const regionLoader = useSimpleDataLoader(async () => {
  189. if (!props.cityCode)
  190. return [];
  191. return ((await RegionApi.getChildList(props.cityCode))
  192. .filter(p => p.areaCode !== props.cityCode && p.name !== '市辖区')
  193. .map(p => ({
  194. ...p,
  195. name: p.name,
  196. id: p.areaCode,
  197. })));
  198. }, false);
  199. const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
  200. mapCtx.removeMarkers({
  201. markerIds: Array.from(villageData.keys()),
  202. })
  203. villageData.clear();
  204. if (!selectedRegion.value)
  205. return [];
  206. await waitTimeOut(100);
  207. let res = (await LightVillageApi.getMapVillage({
  208. areaCode: selectedRegion.value
  209. }));
  210. //如果小地图则只显示200个
  211. if (props.small && res.length > 200)
  212. res.splice(200, res.length - 200);
  213. //如果为空则尝试获取子级
  214. hasResItems.value = res.length > 0;
  215. const list = res
  216. .filter(p => {
  217. if (p.isLight && showLighted.value)
  218. return true;
  219. if (!p.isLight && showUnLighted.value)
  220. return true;
  221. return false;
  222. })
  223. .map((p, i) => {
  224. const id = p.villageId ?? i;
  225. villageData.set(id, p);
  226. const maker : MapMarker = {
  227. id,
  228. title: p.name,
  229. longitude: Number(p.longitude),
  230. latitude: Number(p.latitude),
  231. width: 30,
  232. height: 30,
  233. iconPath: '',
  234. joinCluster: true,
  235. callout: {
  236. display: props.small ? 'BYCLICK' : 'ALWAYS',
  237. content: p.name,
  238. color: '#000000',
  239. fontSize: 12,
  240. padding: 5,
  241. bgColor: '#ffffff',
  242. borderRadius: 5,
  243. },
  244. }
  245. if (p.isLight && p.villageId) {
  246. if (p.lightValue >= 1) {
  247. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light3.png`;
  248. } else if (p.lightValue >= 0.5) {
  249. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light2.png`;
  250. } else if (p.lightValue >= 0.25) {
  251. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light1.png`;
  252. } else {
  253. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light1.png`;
  254. }
  255. const size = Math.floor(30 +(p.lightValue) * 40);
  256. maker.width = size;
  257. maker.height = Math.floor(size * 1.05);
  258. } else {
  259. maker.width = 25;
  260. maker.height = 27;
  261. maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/LightUnLight.png`;
  262. }
  263. return maker as MapMarker;
  264. })
  265. .filter(p =>
  266. p.longitude && p.latitude
  267. && !isNaN(p.longitude) && !isNaN(p.latitude)
  268. && p.longitude > -180 && p.longitude < 180
  269. && p.latitude > -90 && p.latitude < 90
  270. );
  271. asyncAddMarkers(list);
  272. if (res.length == 1) {
  273. mapCtx.moveToLocation({
  274. latitude: Number(list[0].latitude),
  275. longitude: Number(list[0].longitude),
  276. });
  277. } else if (res.length > 1) {
  278. setTimeout(() => {
  279. mapCtx.includePoints({
  280. points: list.map(p => {
  281. if (!p.longitude || !p.latitude) {
  282. p.longitude = AppCofig.defaultLonLat[0];
  283. p.latitude = AppCofig.defaultLonLat[1];
  284. }
  285. return {
  286. latitude: p.latitude,
  287. longitude: p.longitude,
  288. }
  289. }),
  290. padding: [20, 20, 20, 20],
  291. });
  292. }, 200);
  293. } else {
  294. try {
  295. const currentRegion = regionLoader.content.value?.find(p => p.id == selectedRegion.value);
  296. if (currentRegion) {
  297. mapCtx.moveToLocation({
  298. latitude: Number(currentRegion.latitude),
  299. longitude: Number(currentRegion.longitude),
  300. });
  301. }
  302. } catch (error) {
  303. console.error(error);
  304. }
  305. }
  306. ready.value = true;
  307. return list;
  308. }, false, false);
  309. const BATCH_SIZE = 25;
  310. async function asyncAddMarkers(list: MapMarker[]) {
  311. if (list.length <= BATCH_SIZE) {
  312. mapCtx.addMarkers({
  313. clear: true,
  314. markers: list,
  315. });
  316. return;
  317. }
  318. for (let i = 0; i < list.length; i += BATCH_SIZE) {
  319. const batch = list.slice(i, i + BATCH_SIZE);
  320. mapCtx.addMarkers({
  321. clear: i === 0,
  322. markers: batch,
  323. });
  324. if (i + BATCH_SIZE < list.length) {
  325. await waitTimeOut(300);
  326. }
  327. }
  328. }
  329. async function onMarkerTap(e: any) {
  330. try {
  331. loading();
  332. const villageMapInfo = villageData.get(e.markerId);
  333. assertNotNull(villageMapInfo, 'markerId错误');
  334. if (!villageMapInfo.villageId) {
  335. emit('createVillage', villageMapInfo);
  336. return;
  337. }
  338. const village = await LightVillageApi.getVillageDetails(villageMapInfo.villageId);
  339. assertNotNull(village, '获取村社详情失败');
  340. if (props.isLightMode) {
  341. emit('update:isLightMode', false);
  342. emit('lightVillage', village);
  343. return;
  344. }
  345. emit('selectVillage', village);
  346. } catch (error) {
  347. showError(error);
  348. } finally {
  349. hideLoading();
  350. }
  351. }
  352. function onSelectedRegion(regionId: number) {
  353. selectedRegion.value = regionId;
  354. mapLoader.reload();
  355. emit('update:district', regionLoader.content.value?.find(p => p.id === regionId)?.name);
  356. emit('regionChanged', regionId);
  357. }
  358. function setCurrentRegion(regionAreaCode: number) {
  359. if (regionAreaCode === selectedRegion.value)
  360. return;
  361. selectedRegion.value = regionAreaCode;
  362. emit('regionChanged', selectedRegion.value);
  363. mapLoader.reload();
  364. }
  365. function loadFirstRegion() {
  366. //加载第一个地区(排除市辖区)
  367. let code : number | undefined = undefined;
  368. if (props.district)
  369. code = regionLoader.content.value?.find((p) => p.name === props.district)?.areaCode;
  370. if (!code)
  371. code = regionLoader.content.value?.find((p) => p.id !== props.cityCode)?.areaCode;
  372. if (!code)
  373. code = regionLoader.content.value?.[0]?.areaCode;
  374. if (code)
  375. setCurrentRegion(code);
  376. console.log('加载区域数据', code, regionLoader.content.value, props.district);
  377. }
  378. function showSearch() {
  379. navTo('/pages/home/search/index');
  380. }
  381. watch(() => props.cityCode, async () => {
  382. await regionLoader.reload();
  383. loadFirstRegion();
  384. }, { immediate: true });
  385. watch(showLighted, () => mapLoader.reload());
  386. watch(showUnLighted, () => mapLoader.reload());
  387. onMounted(async () => {
  388. mapCtx.initMarkerCluster({
  389. enableDefaultStyle: false,
  390. zoomOnClick: true,
  391. gridSize: props.small ? 25 : 50,
  392. });
  393. mapCtx.on('markerClusterCreate', (e: { clusters: any[] }) => {
  394. const customClusters = e.clusters.map((cluster) => {
  395. const { center, clusterId, markerIds } = cluster;
  396. return {
  397. ...center,
  398. width: 0,
  399. height: 0,
  400. clusterId,
  401. label: {
  402. content: markerIds.length.toString(), // 聚合点的数量
  403. fontSize: 16,
  404. color: '#fff',
  405. width: 30,
  406. height: 30,
  407. bgColor: '#8bb346', // 背景颜色
  408. borderRadius: 25,
  409. textAlign: 'center',
  410. anchorX: -10,
  411. anchorY: -35,
  412. },
  413. };
  414. });
  415. mapCtx.addMarkers({
  416. markers: customClusters,
  417. clear: false,
  418. });
  419. });
  420. });
  421. defineExpose({
  422. showSearch,
  423. reload: async () => {
  424. await regionLoader.reload();
  425. loadFirstRegion();
  426. },
  427. });
  428. </script>
  429. <style lang="scss">
  430. .light-map {
  431. position: relative;
  432. width: 100%;
  433. height: 600rpx;
  434. border-radius: 30rpx;
  435. overflow: hidden;
  436. border: 1px solid #d45652;
  437. &.full {
  438. height: 100vh;
  439. border-radius: 0;
  440. .light-map-map {
  441. height: 100vh;
  442. }
  443. }
  444. &.small {
  445. height: 400rpx;
  446. border-radius: 20rpx;
  447. .light-map-map {
  448. height: 400rpx;
  449. }
  450. }
  451. .light-map-map {
  452. width: 100%;
  453. height: 600rpx;
  454. }
  455. .light-map-address {
  456. position: absolute;
  457. bottom: 20rpx;
  458. right: 20rpx;
  459. z-index: 100;
  460. }
  461. }
  462. </style>