快乐的梦鱼 2 semanas atrás
pai
commit
338fa98845

+ 123 - 4
src/api/RequestModules.ts

@@ -6,9 +6,9 @@
 
 import ApiCofig from "@/common/config/ApiCofig";
 import { isDev } from "../common/config/AppCofig";
-import { appendGetUrlParams } from "@imengyu/imengyu-utils";
 import { BaseAppServerRequestModule } from "./BaseAppServerRequestModule";
-import type { DataModel } from "@imengyu/js-request-transform";
+import type { DataModel, NewDataModel } from "@imengyu/js-request-transform";
+import { appendGetUrlParams, defaultResponseDataHandlerCatch, RequestApiError, RequestApiResult, RequestCoreInstance, RequestOptions, RequestResponse, type RequestApiInfoStruct } from "@imengyu/imengyu-utils";
 
 
 /**
@@ -25,10 +25,129 @@ export class AppServerRequestModule<T extends DataModel> extends BaseAppServerRe
  */
 export class MapServerRequestModule<T extends DataModel> extends BaseAppServerRequestModule<T> {
   constructor() {
-    super('https://restapi.amap.com');
+    super('https://update-server1.imengyu.top');
+    //super('http://localhost:3012');
     this.config.requestInterceptor = (url, req) => {
-      url = appendGetUrlParams(url, 'key', ApiCofig.amapServerKey);
       return { newUrl: url, newReq: req };
     };
+    this.config.responseDataHandler = async function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel | undefined, instance: RequestCoreInstance<T>, apiInfo: RequestApiInfoStruct): Promise<RequestApiResult<T>> {
+      const method = req.method || 'GET';
+      try {
+        const json = await response.json();
+        if (response.ok) {
+          if (!json) {
+            throw new RequestApiError(
+              'businessError',
+              '后端未返回数据',
+              '',
+              response.status,
+              null,
+              null,
+              response.headers,
+              apiInfo
+            );
+          }
+          if (!json.success)
+            throw new RequestApiError(
+              'businessError',
+              json.message,
+              json.code.toString(),
+              json.code,
+              json,
+              json,
+              response.headers,
+              apiInfo
+            );
+          
+          return new RequestApiResult(
+            resultModelClass ?? instance.config.modelClassCreator,
+            json?.code || response.status,
+            json.message,
+            json.data,
+            json,
+            response.headers,
+            apiInfo
+          );
+        }
+        else {
+          throw json;
+        }
+
+      } catch (err) {
+        if (err instanceof RequestApiError) {
+          throw response;
+        }
+        //错误统一处理
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          defaultResponseDataHandlerCatch(method, req, response, null, err as any, apiInfo, response.url, reject, instance);
+        });
+      }
+    };
+  }
+}
+
+/**
+ * 腾讯地图服务请求模块
+ */
+export class TencentMapServerRequestModule<T extends DataModel> extends BaseAppServerRequestModule<T> {
+  constructor() {
+    super("https://apis.map.qq.com");
+    this.config.requestInterceptor = (url, req) => {
+      url = appendGetUrlParams(url, 'key', ApiCofig.mapKey);
+      return { newUrl: url, newReq: req };
+    };
+    this.config.responseDataHandler = async function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel | undefined, instance: RequestCoreInstance<T>, apiInfo: RequestApiInfoStruct): Promise<RequestApiResult<T>> {
+      const method = req.method || 'GET';
+      try {
+        const json = await response.json();
+        if (response.ok) {
+          if (!json) {
+            throw new RequestApiError(
+              'businessError',
+              '后端未返回数据',
+              '',
+              response.status,
+              null,
+              null,
+              response.headers,
+              apiInfo
+            );
+          }
+          if (json.status !== 0)
+            throw new RequestApiError(
+              'businessError',
+              json.message,
+              json.status.toString(),
+              json.status,
+              json,
+              json,
+              response.headers,
+              apiInfo
+            );
+          
+          return new RequestApiResult(
+            resultModelClass ?? instance.config.modelClassCreator,
+            json?.status || response.status,
+            json.message,
+            json.result,
+            json,
+            response.headers,
+            apiInfo
+          );
+        }
+        else {
+          throw json;
+        }
+
+      } catch (err) {
+        if (err instanceof RequestApiError) {
+          throw response;
+        }
+        //错误统一处理
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          defaultResponseDataHandlerCatch(method, req, response, null, err as any, apiInfo, response.url, reject, instance);
+        });
+      }
+    };
   }
 }

+ 50 - 31
src/api/light/LightVillageApi.ts

@@ -79,21 +79,17 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
     village_id?: number;
   }) {
     const res = await this.post<{
-      code: number;
-      msg: string;
-      time: string;
-      data: Array<{
-        id: number;
-        name: string;
-        mobile: string;
-        points: number;
-        level: number;
-        type_text?: string;
-        sex_text?: string;
-        status_text?: string;
-      }>;
-    }>('/village/volunteer/getRanklist', '志愿者排行榜', params);
-    return res.requireData().data;
+      id: number;
+      name: string;
+      mobile: string;
+      points: number;
+      level: number;
+      type_text?: string;
+      sex_text?: string;
+      status_text?: string;
+      image?: string;
+    }[]>('/village/volunteer/getRanklist', '志愿者排行榜', params);
+    return res.requireData();
   }
 
   /**
@@ -109,22 +105,18 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
     is_light?: number;
   }) {
     const res = await this.post<{
-      code: number;
-      msg: string;
-      time: string;
-      data: Array<{
-        id: number;
-        name: string;
-        points: number;
-        region: number;
-        status?: string;
-        is_light: number | string;
-        status_text?: string;
-        region_text?: string;
-        is_light_text?: string;
-      }>;
-    }>('/village/village/getRanklist', '村社排行榜', params);
-    return res.requireData().data;
+      id: number;
+      name: string;
+      points: number;
+      region: number;
+      status?: string;
+      is_light: number | string;
+      status_text?: string;
+      region_text?: string;
+      is_light_text?: string;
+      image?: string;
+    }[]>('/village/village/getRanklist', '村社排行榜', params);
+    return res.requireData();
   }
 
   async getVillageList(level?: number, region?: number, status?: number, page?: number, pageSize?: number) {
@@ -161,6 +153,33 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
     }>('/village/volunteer/getIpArea', '获取IP地址');
     return res.requireData();
   }
+
+  async getMessages(page: number, pageSize: number, keywords?: string) {
+    const res = await this.get<{
+      data: {
+        id: number;
+        user_id: number;
+        village_id: number;
+        title: string;
+        content: string | null;
+        images: string;
+        image: string;
+        msgid: string;
+        jump_url: string;
+        send_time: string;
+        like_count: number;
+        village_name: string | null;
+        username: string | null;
+        nick_name: string | null;
+      }[],
+      total: number,
+    }>('/village/collect/wechatContentList', '获取微信贴图列表', {
+      page,
+      pageSize,
+      keywords,
+    });
+    return res.requireData();
+  }
 }
 
 

+ 65 - 107
src/api/map/MapApi.ts

@@ -1,29 +1,5 @@
-import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { DataModel } from '@imengyu/js-request-transform';
 import { MapServerRequestModule } from '../RequestModules';
-import { type QueryParams } from '@imengyu/imengyu-utils';
-
-export class Poi extends DataModel<Poi> {
-  constructor() {
-    super(Poi, "Poi");
-    this.setNameMapperCase('Camel', 'Snake');
-    this._convertTable = {
-      location: { clientSide: 'splitCommaArray' },
-    };
-    this._convertKeyType = (key, direction) => {
-      if (key.endsWith('Time'))
-        return {
-          clientSide: 'date',
-          serverSide: 'string',
-        };
-      return undefined;
-    };
-  }
-  name = '';
-  location = [] as number[];
-  type = '';
-  address = '';
-  cityname = '';
-}
 
 export interface CityItem {
   first_letter: string;
@@ -38,64 +14,82 @@ export interface CityItem {
   children?: CityItem[];
 }
 
-export class MapApi extends MapServerRequestModule<DataModel> {
+/** 行政区划层级(与服务端 MapCityData.level 一致) */
+export type MapCityDataLevel = 'province' | 'city' | 'district';
 
+/**
+ * 省市区数据模型
+ */
+export class MapCityData extends DataModel<MapCityData> {
   constructor() {
-    super();
+    super(MapCityData, '省市区数据');
   }
 
+  adcode = '';
+  parentAdCode: string | null = null;
+  citycode = '';
+  name = '';
+  center = '';
+  polyline = '';
+  pinyin = '';
+  level = '' as MapCityDataLevel;
+}
+
+export class MapApi extends MapServerRequestModule<MapCityData> {
 
-  searchPoi(name: string, page = 1, size = 10, querys?: QueryParams) {
-    return this.get(`/v3/place/text?keywords=${encodeURIComponent(name)}&citylimit=true&offset=${size}&page=${page}`, `搜索POI ${name} 第${page}页`, {
-      ...querys,
-    })
-      .then(res => transformArrayDataModel<Poi>(Poi, res.data ?? [], ''))
+  /**
+   * 快速获取地区编码
+   * @param name 地区名称
+   * @param city 城市名称(可选,用于消歧)
+   */
+  simpleGetRegionCode(name: string, city?: string) {
+    return this.get<string>(
+      `/content/map/simpleGetRegionCode`,
+      '快速获取地区编码',
+      { name, city },
+    )
+      .then(res => res.requireData() as string)
       .catch(e => { throw e });
   }
-  regeo(lat: number, lng: number, querys?: QueryParams) {
-    return this.get(`/v3/geocode/regeo`, `查询经纬度(${lat}, ${lng})所属区县`, {
-      location: `${lng},${lat}`,
-      ...querys,
-    })
-      .then(res => (res.data as any).regeocode.addressComponent as {
-        country: string,
-        province: string,
-        city: string,
-        district: string,
-        adcode: string,
-        township: string,
-        citycode: string,
-        towncode: string,
-      })
-      .catch(e => { throw e });
+
+  /**
+   * 简单获取地区(扁平地区信息)
+   * @param name 地区名称
+   * @param city 城市名称(可选,用于消歧)
+   */
+  simpleGetRegion(name: string, city?: string) {
+    return this.get<MapCityData>(
+      `/content/map/simpleGetRegion`,
+      '简单获取地区',
+      { name, city },
+      MapCityData,
+    );
   }
-  regeoAddress(lat: number, lng: number, querys?: QueryParams) {
-    return this.get(`/v3/geocode/regeo`, `查询经纬度(${lat}, ${lng})地址`, {
-      location: `${lng},${lat}`,
-      ...querys,
-    })
-      .then(res => (res.data as any).regeocode.formattedAddress as string)
-      .catch(e => { throw e });
+
+  /**
+   * 按编码获取地区详情
+   * @param adcode 地区编码
+   */
+  getRegionInfo(adcode: string) {
+    return this.get<MapCityData>(
+      `/content/map/getRegionInfo`,
+      '按编码获取地区详情',
+      { adcode },
+    );
   }
+
   /**
-   * 简单获取地区编码
-   *
-   * 匹配方式:
-   * 1. 如果匹配到单个地区数据,则直接返回
-   * 2. 如果匹配到多个地区数据,则需要根据省份和城市名称来区分
-   * @param name 地区名称
-   * @param province 省份名称,可选,当出现同名地区时,需要省份名称来区分
-   * @param city 城市名称,可选,当出现同名地区时,需要城市名称来区分
+   * 获取子级地区列表
+   * @param adcode 地区编码(可选;不传则返回省级列表)
    */
-  simpleGetRegionCode(name: string, province?: string, city?: string) {
-    return this.get<string>(`https://update-server1.imengyu.top/content/map/simpleGetRegionCode`, `简单获取地区编码`, {
-      name,
-      province,
-      city,
-    })
-      .then(res => res.requireData() as string)
-      .catch(e => { throw e });
+  getRegionChildren(adcode?: string) {
+    return this.get<MapCityData[]>(
+      `/content/map/getRegionChildren`,
+      '获取子级地区列表',
+      { adcode },
+    );
   }
+
   loadCityData(full = false) : Promise<CityItem[]> {
     return new Promise((resolve, reject) => {
       uni.request({
@@ -114,42 +108,6 @@ export class MapApi extends MapServerRequestModule<DataModel> {
       })
     });
   }
-  loadCountryGeoJsonData() {
-    return new Promise((resolve, reject) => {
-      uni.request({
-        url: 'https://mn.wenlvti.net/app_static/xiangyuan/data/100000_full.json',
-        method: 'GET',
-        success(result) {
-          if (result.statusCode === 200) {
-            resolve(result.data);
-          } else {
-            reject(new Error(`请求失败,状态码:${result.statusCode}`));
-          }
-        },
-        fail(error) {
-          reject(error);
-        }
-      })
-    });
-  }
-  loadGeoJsonData(code: string) {
-    return new Promise((resolve, reject) => {
-      uni.request({
-        url: `https://mn.wenlvti.net/app_static/xiangyuan/data/${code}_full.json`,
-        method: 'GET',
-        success(result) {
-          if (result.statusCode === 200) {
-            resolve(result.data);
-          } else {
-            reject(new Error(`请求失败,状态码:${result.statusCode}`));
-          }
-        },
-        fail(error) {
-          reject(error);
-        }
-      })
-    });
-  }
 }
 
 export default new MapApi();

+ 95 - 0
src/api/map/TenMapApi.ts

@@ -0,0 +1,95 @@
+import type { DataModel } from "@imengyu/js-request-transform";
+import { TencentMapServerRequestModule } from "../RequestModules";
+
+export class TenMapApi extends TencentMapServerRequestModule<DataModel> {
+  constructor() {
+    super();
+  }
+
+  /**
+   * 逆地址解析(坐标位置描述)
+   * 
+   * 文档:https://lbs.qq.com/service/webService/webServiceGuide/address/Gcoder
+   * 
+   * 说明:
+   * 本接口提供由经纬度到文字地址及相关位置信息的转换能力,广泛应用于物流、出行、O2O、社交等场景。服务响应速度快、稳定,支撑亿级调用。
+   * 支持根据输入经纬度,获取:
+   * 1 . 经纬度所在省、市、区、乡镇、门牌号、行政区划代码,及周边参考位置信息,如道路及交叉口、河流、湖泊、桥等
+   * 2 . 通过知名地点、地标组合形成的易于理解的地址,如:北京市海淀区中钢国际广场(欧美汇购物中心北)。
+   * 3 . 商圈、附近知名的一级地标、代表当前位置的二级地标等。
+   * 4 . 周边POI(AOI)列表。
+   * 
+   * @param location 经纬度(GCJ02坐标系),格式:location=lat<纬度>,lng<经度>
+   * @param radius 半径,单位:米
+   * @param get_poi 是否获取周边POI(AOI)列表
+   * @param poi_options 周边POI(AOI)列表选项,格式:poi_options=type<POI类型>,radius<半径>,数量<数量>
+   * @returns 
+   */
+  async geocoder(location: string, radius?: number, get_poi?: boolean, poi_options?: string) {
+    return (await this.get<{
+      address: string;
+      formatted_addresses: {
+        recommend: string;
+        rough: string;
+        standard_address: string;
+      };
+      address_component: {
+        nation: string;
+        province: string;
+        city: string;
+        district: string;
+        street: string;
+        street_number: string;
+      };
+    }>('/ws/geocoder/v1/', '逆地址解析(坐标位置描述)', {
+      location,
+      radius,
+      get_poi: get_poi ? 1 : 0,
+      poi_options,
+    })).data;
+  }
+
+  /**
+   * 地址解析(地址转坐标)
+   *
+   * 文档:https://lbs.qq.com/service/webService/webServiceGuide/address/Geocoder
+   *
+   * 说明:
+   * 本接口提供由文字地址到经纬度的转换能力,并同时提供结构化的省市区地址信息。
+   * 为提升解析准确率,地址中请至少包含城市名称;地址请尽量完整、具体(包括省市区乡镇/街道门牌及详细地点信息)。
+   *
+   * @param address 要解析的输入地址(建议包含城市,尽量完整;若包含 # 等特殊字符需先 URL 编码)
+   * @param policy 解析策略:0 标准(默认,地址须包含城市);1 宽松(允许缺失城市,准确性可能受影响)
+   * @returns 解析结果,含坐标(GCJ02)、省市区等结构化信息及可信度、解析级别
+   */
+  async geocodeAddress(
+    address: string,
+    policy?: 0 | 1
+  ) {
+    return (await this.get<{
+      /** [废弃] 最终用于坐标解析的地址或地点名称,仅供参考 */
+      title: string;
+      /** 解析到的坐标(GCJ02 坐标系) */
+      location: { lat: number; lng: number };
+      /** 解析后的地址部件 */
+      address_components: {
+        province: string;
+        city: string;
+        district: string;
+        street: string;
+        street_number: string;
+      };
+      /** 行政区划信息 */
+      ad_info: { adcode: string };
+      /** 可信度 1–10,>=7 时解析结果较为准确 */
+      reliability: number;
+      /** 解析精度级别,一般 >=9 即可采用(定位到点) */
+      level?: number;
+    }>('/ws/geocoder/v1/', '地址解析(地址转坐标)', {
+      address,
+      ...(policy !== undefined && { policy }),
+    })).data;
+  }
+}
+
+export default new TenMapApi();

+ 3 - 0
src/common/components/parts/IndexCommonImageItem.vue

@@ -7,6 +7,7 @@
     :imageWidth="340"
     :imageRadius="15"
     backgroundColor="transparent"
+    @click="$emit('click')"
   >
     <template #footer> 
       <FlexRow justify="space-between" align="center" :padding="[10,0]" :margin="[10,0,0,0]">
@@ -39,4 +40,6 @@ const props = defineProps<{
   isLike?: boolean;
 }>()
 
+const emit = defineEmits([ 'click' ]);
+
 </script>

+ 1 - 1
src/common/config/ApiCofig.ts

@@ -9,7 +9,7 @@ export default {
   },
   mainBodyId: 1,
   platformId: 330,
-  amapServerKey: '8fd09264c33678141f609588c432df0e',
+  mapKey: 'LDXBZ-JIWWC-IXW2S-AUDZS-26VC2-GRBC4',
   bugReport: {
     server: 'https://update-server1.imengyu.top/bug-submit',
     appId: 1,

+ 54 - 110
src/pages/home/components/LightMap.vue

@@ -13,33 +13,58 @@
       class="light-map-map"
       :enable-poi="false"
       :scale="12"
-      :longitude="currentLonlat.longitude"
-      :latitude="currentLonlat.latitude"
+      :longitude="lonlat?.longitude"
+      :latitude="lonlat?.latitude"
       @markertap="onMarkerTap" 
     />
-    <FlexCol v-if="isEmptyRegion" gap="gap.md" position="absolute" inset="0" center backgroundColor="mask.default">
-      <Text>您选择的地区还未开通亮乡源数据,可联系客服开通</Text>
+    <FlexCol v-if="isEmptyRegion" 
+      gap="gap.xl" 
+      position="absolute" 
+      inset="0" 
+      padding="space.xl"
+      center 
+      backgroundColor="mask.default"
+    > 
+      <Text fontConfig="importantTitle" textAlign="center">您选择的地区还未开通亮乡源数据,可联系客服开通</Text>
       <button open-type="contact" class="remove-button-style">
-        <Button type="primary" text="联系客服"></Button>
+        <FlexCol padding="space.md" radius="radius.lg" center backgroundColor="white">
+          <Text fontConfig="primaryTitle">联系客服</Text>
+        </FlexCol>
       </button>
     </FlexCol>
     <SimpleDropDownPicker 
+      v-if="!isEmptyRegion"
       class="light-map-region-picker" 
       :modelValue="selectedRegion" 
       @update:modelValue="onSelectedRegion"
       :columns="regionLoader.content.value" 
     />
-    <Button 
+    <FlexCol
+      v-if="isDev"
+      :innerStyle="{
+        position: 'absolute',
+        bottom: '20rpx',
+        left: '20rpx',
+        zIndex: 100,
+        backgroundColor: '#ffffff',
+      }"
+    >
+      <Text :text="`lonlat:`" />
+      <Text :text="`${lonlat?.longitude}`" />
+      <Text :text="`${lonlat?.latitude}`" />
+    </FlexCol>
+    <NButton 
       :innerStyle="{
         position: 'absolute',
         bottom: '20rpx',
         right: '20rpx',
         zIndex: 100,
         backgroundColor: '#ffffff',
-      }" 
+      }"
+      text="定位"
       icon="navigation" 
-      @click="getCurrentLonlat" 
-    >定位</Button>
+      @click="emit('getCurrentLonlat')" 
+    />
   </div>
 </template>
 
@@ -47,14 +72,12 @@
 import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
 import { navTo } from '@/components/utils/PageAction';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
-import { useVillageStore } from '@/store/village';
 import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
 import LightVillageApi, { VillageListItem } from '@/api/light/LightVillageApi';
 import type { MapMarker } from '@/types/Map';
-import MapApi from '@/api/map/MapApi';
-import AppCofig from '@/common/config/AppCofig';
+import AppCofig, { isDev } from '@/common/config/AppCofig';
 import SimpleDropDownPicker from '@/common/components/SimpleDropDownPicker.vue';
-import Button from '@/components/basic/Button.vue';
+import NButton from '@/components/basic/Button.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Text from '@/components/basic/Text.vue';
 import CommonContent from '@/api/CommonContent';
@@ -63,31 +86,25 @@ const instance = getCurrentInstance();
 const mapCtx = uni.createMapContext('prevMap', instance);
 const selectedRegion = ref<number>();
 const nextNeedAutoFocus = ref(false);
-const currentAddress = ref<string>('');
-const currentLonlat = ref<{ longitude: number, latitude: number }>({ 
-  longitude: 0, 
-  latitude: 0,
-});
 const villageData = new Map<number, VillageListItem>();
 const emit = defineEmits([
-  'getedCurrentLonlat',
+  'getCurrentLonlat',
   'update:isLightMode', 
+  'update:lonlat',
   'selectVillage',
-  'changedCity',
+  'regionChanged',
 ]);
 
 const props = defineProps<{
-  startLonlat?: { longitude: number, latitude: number } | undefined;
+  lonlat?: { longitude: number, latitude: number } | undefined;
+  city?: string;
   isLightMode?: boolean;
-  limitCity?: string;
   small?: boolean;
   full?: boolean;
 }>();
 
-const villageStore = useVillageStore();
-
 const regionLoader = useSimpleDataLoader(async () => {
-  return (await CommonContent.searchRegion(props.limitCity)).map(p => ({
+  return (await CommonContent.searchRegion(props.city)).map(p => ({
     id: p.id,
     name: p.title,
   }));
@@ -196,46 +213,20 @@ function onSelectedRegion(regionId: number) {
   selectedRegion.value = regionId;
   nextNeedAutoFocus.value = true;
   mapLoader.reload();
+  emit('regionChanged', regionId);
 }
 function setCurrentRegion(regionName: string) {
-  selectedRegion.value = regionLoader.content.value?.find(p => p.name == regionName)?.id || undefined;
+  selectedRegion.value = regionLoader.content.value?.find(p => p.name == regionName)?.id || 
+    regionLoader.content.value?.[0]?.id || undefined;
+  emit('regionChanged', selectedRegion.value);
   mapLoader.reload();
 }
-function getCurrentLonlat() {
-  uni.getLocation({
-    type: 'wgs84',
-    success: async (res) => {
-      currentLonlat.value = {
-        longitude: res.longitude,
-        latitude: res.latitude,
-      };
-      villageStore.setCurrentLonlat(currentLonlat.value);
-      const address = await MapApi.regeo(res.latitude, res.longitude);
-      currentAddress.value = address.district;
-      villageStore.setCurrentRegion(address.district);
-      setCurrentRegion(address.district);
-      emit('getedCurrentLonlat', currentLonlat.value);
-      emit('changedCity', address.city);
-      mapCtx.moveToLocation({
-        latitude: currentLonlat.value.latitude,
-        longitude: currentLonlat.value.longitude,
-      });
-    },
-  });
-}
-watch(() => props.limitCity, (newVal) => {
-  regionLoader.reload();
-  mapLoader.reload();
-});
-watch(() => props.startLonlat, (newVal) => {
-  if (newVal) {
-    currentLonlat.value = newVal;
-    mapCtx.moveToLocation({
-      latitude: currentLonlat.value.latitude,
-      longitude: currentLonlat.value.longitude,
-    });
-  }
-}, { deep: true });
+
+watch(() => props.city, async (newVal) => {
+  await regionLoader.reload();
+  setCurrentRegion(newVal || '');
+  await mapLoader.reload();
+}, { immediate: true });
 
 onMounted(async () => {
   mapCtx.initMarkerCluster({
@@ -270,55 +261,8 @@ onMounted(async () => {
       clear: false,
     });
   });
-
-  if (props.startLonlat) {
-    currentLonlat.value = props.startLonlat;
-    mapCtx.moveToLocation({
-      latitude: currentLonlat.value.latitude,
-      longitude: currentLonlat.value.longitude,
-    });
-  }
-
   await regionLoader.reload();
-
-  if (!props.startLonlat) {
-
-    if (props.limitCity) {
-      const res = await MapApi.searchPoi(props.limitCity);
-      if (res.length > 0) {
-        currentLonlat.value = {
-          longitude: res[0].location[0],
-          latitude: res[0].location[1],
-        };
-        villageStore.setCurrentLonlat(currentLonlat.value);
-        regionLoader.reload();
-        mapLoader.reload();
-      }
-    } else {
-      const res = await LightVillageApi.getIpAddress();
-      currentLonlat.value = {
-        longitude: Number(res.point.x),
-        latitude: Number(res.point.y),
-      };
-      villageStore.setCurrentLonlat(currentLonlat.value);
-      currentAddress.value = res.address_detail.district;
-      setCurrentRegion(res.address_detail.district);
-      villageStore.setCurrentRegion(res.address_detail.district);
-      emit('changedCity', res.address_detail.city);
-    }
-    await waitTimeOut(100);
-    emit('getedCurrentLonlat', currentLonlat.value);
-    mapCtx.moveToLocation({
-      latitude: currentLonlat.value.latitude,
-      longitude: currentLonlat.value.longitude,
-    });
-  } else {
-    const address = await MapApi.regeo(currentLonlat.value.latitude, currentLonlat.value.longitude);
-    currentAddress.value = address.district;
-    setCurrentRegion(address.district);
-  }
 });
-
 </script>
 
 <style lang="scss">
@@ -338,10 +282,10 @@ onMounted(async () => {
     }
   }
   &.small {
-    height: 340rpx;
+    height: 500rpx;
     border-radius: 20rpx;
     .light-map-map {
-      height: 340rpx;
+      height: 500rpx;
     }
   }
 

+ 27 - 18
src/pages/home/components/VillageRankList.vue

@@ -1,4 +1,5 @@
 <template>
+  <Empty v-if="list.length === 0" description="暂无排名数据" />
   <FlexRow :gap="20">
     <FlexCol 
       v-for="(item) in list" 
@@ -46,25 +47,33 @@
 <script setup lang="ts">
 import Image from '@/components/basic/Image.vue';
 import Text from '@/components/basic/Text.vue';
+import Empty from '@/components/feedback/Empty.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 
-
-const list = [
-  {
-    image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest1.png',
-    title: '乡村1',
-    rank: 1,
-  },
-  {
-    image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest2.png',
-    title: '乡村2',
-    rank: 2,
-  },
-  {
-    image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest3.png',
-    title: '乡村3',
-    rank: 3,
-  },
-]
+withDefaults(defineProps<{
+  list?: {
+    image: string;
+    title: string;
+    rank: number;
+  }[];
+}>(), {
+  list: () => [
+    {
+      image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest1.png',
+      title: '乡村1',
+      rank: 1,
+    },
+    {
+      image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest2.png',
+      title: '乡村2',
+      rank: 2,
+    },
+    {
+      image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest3.png',
+      title: '乡村3',
+      rank: 3,
+    },
+  ],
+});
 </script>

+ 32 - 22
src/pages/home/components/VillageUserRankList.vue

@@ -1,4 +1,5 @@
 <template>
+  <Empty v-if="list.length === 0" description="暂无排名数据" />
   <FlexRow align="flex-end">
     <FlexCol 
       v-for="(item) in list" 
@@ -31,38 +32,47 @@
 </template>
 
 <script setup lang="ts">
-import Image from '@/components/basic/Image.vue';
 import Text from '@/components/basic/Text.vue';
 import Avatar from '@/components/display/Avatar.vue';
+import Empty from '@/components/feedback/Empty.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Height from '@/components/layout/space/Height.vue';
 
-
-const list = [
-  {
-    image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest2.png',
-    title: '用户2',
-    rank: 2,
-    score: 90,
-  },
-  {
-    image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest1.png',
-    title: '用户1',
-    rank: 1,
-    score: 100,
-  },
-  {
-    image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest3.png',
-    title: '用户3',
-    rank: 3,
-    score: 80,
-  },
-]
 const scoreColors = [
   '#cb8833',
   '#7788b9',
   '#cd7853',
 ]
 
+withDefaults(defineProps<{
+  list?: {
+    image: string;
+    title: string;
+    rank: number;
+    score: number;
+  }[];
+}>(), {
+  list: () => [
+    {
+      image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest2.png',
+      title: '用户2',
+      rank: 2,
+      score: 90,
+    },
+    {
+      image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest1.png',
+      title: '用户1',
+      rank: 1,
+      score: 100,
+    },
+    {
+      image: 'https://mncdn.wenlvti.net/app_static/minnan/images/test/ImageTest3.png',
+      title: '用户3',
+      rank: 3,
+      score: 80,
+    },
+  ],
+});
+
 </script>

+ 72 - 0
src/pages/home/composeabe/GetCurrentLocation.ts

@@ -0,0 +1,72 @@
+import LightVillageApi from "@/api/light/LightVillageApi";
+import MapApi from "@/api/map/MapApi";
+import TenMapApi from "@/api/map/TenMapApi";
+import { useVillageStore } from "@/store/village";
+import { ref } from "vue";
+
+export function useGetCurrentLocation(
+  options: {
+    onCityChanged?: (city: string) => void;
+  }
+) {
+
+  const { onCityChanged } = options;
+  
+  const villageStore = useVillageStore();
+  const currentLonlat = ref<{ longitude: number, latitude: number }>({ 
+    longitude: 0, 
+    latitude: 0,
+  });
+
+
+  async function getCurrentExactLocation() {
+    return new Promise((resolve, reject) => {
+      uni.getLocation({
+        type: 'wgs84',
+        success: async (res) => {
+          currentLonlat.value = {
+            longitude: res.longitude,
+            latitude: res.latitude,
+          };
+          villageStore.setCurrentLonlat(currentLonlat.value);
+          const address = (await TenMapApi.geocoder(`${res.latitude},${res.longitude}`));
+          villageStore.setCurrentRegion(address?.address_component?.city || '');
+          onCityChanged?.(address?.address_component?.city || '');
+          resolve(currentLonlat.value);
+        },
+        fail: (err) => {
+          reject(err);
+        },
+      });
+    });
+  }
+  async function getCurrentFuzzyLocation() {
+    const res = await LightVillageApi.getIpAddress();
+    currentLonlat.value = {
+      longitude: Number(res.point.x),
+      latitude: Number(res.point.y),
+    };
+    villageStore.setCurrentLonlat(currentLonlat.value);
+    villageStore.setCurrentRegion(res.address_detail.district);
+    onCityChanged?.(res.address_detail.city);
+  }
+
+  async function setCurrentLocationWithCity(city: string) {
+    const res = (await MapApi.simpleGetRegion(city)).requireData();
+    currentLonlat.value = {
+      longitude: Number(res.center.split(',')[0]),
+      latitude: Number(res.center.split(',')[1]),
+    };
+    console.log('currentLonlat.value', currentLonlat.value);
+    villageStore.setCurrentLonlat(currentLonlat.value);  
+    villageStore.setCurrentRegion(res.name);
+    onCityChanged?.(city);
+  }
+
+  return {
+    currentLonlat,
+    getCurrentExactLocation,
+    getCurrentFuzzyLocation,
+    setCurrentLocationWithCity,
+  };
+}

+ 94 - 31
src/pages/home/index.vue

@@ -41,10 +41,11 @@
 
     <LightMap
       small
-      :limitCity="currentCity"
-      :startLonlat="currentLonlat"
-      @changedCity="handleChangedCity"
+      :city="currentCity"
+      :lonlat="currentLocation.currentLonlat.value"
+      @getCurrentLonlat="currentLocation.getCurrentExactLocation"
       @selectVillage="goDetails"
+      @regionChanged="currentRegion=$event"
     >
       <NoticeBar 
         v-if="currentNoticeContent"
@@ -88,22 +89,22 @@
       <Button 
         icon="https://xy.wenlvti.net/app_static/images/home/IconLight.png" 
         radius="radius.lg" :padding="[10, 30]" 
-        @click="navTo('/pages/home/light/submit-map', { code: currentCityCode })"
+        @click="navTo('/pages/home/light/submit-map', { city: currentCity })"
       >
         点亮村社
       </Button>
     </FlexRow>
 
     <HomeTitle title="乡村排名" showMore />
-    <VillageRankList />
+    <VillageRankList :list="villageRankListLoader.content.value ?? []" />
 
     <HomeTitle title="志愿者排名" showMore :lightCount="3" />
-    <VillageUserRankList />
+    <VillageUserRankList :list="villageUserRankListLoader.content.value ?? []" />
 
     <HomeTitle title="精选记忆" showMore />
     <MasonryGrid>
       <MasonryGridItem
-        v-for="(item, i) in recommendLoader.content.value"
+        v-for="(item, i) in recommendLoader.list.value"
         :key="i"
         :width="340"
       >
@@ -114,6 +115,7 @@
           :userName="item.userName"
           :likes="item.likes"
           :isLike="item.isLike"
+          @click="goMessageDetails(item)"
         />
       </MasonryGridItem>
     </MasonryGrid>
@@ -173,6 +175,12 @@ import NoticeBar from '@/components/display/NoticeBar.vue';
 import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
 import type { VillageListItem } from '@/api/inhert/VillageApi';
 import FollowVillageApi from '@/api/light/FollowVillageApi';
+import { useGetCurrentLocation } from './composeabe/GetCurrentLocation';
+import { toast } from '@/components/utils/DialogAction';
+import LightVillageApi from '@/api/light/LightVillageApi';
+import CommonContent from '@/api/CommonContent';
+import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePageListLoader';
+import { ArrayUtils } from '@imengyu/imengyu-utils';
 
 const emit = defineEmits(['goVillage']);
 
@@ -181,29 +189,62 @@ const themeContext = useTheme();
 const searchKeywords = ref('');
 const showCityPopup = ref(false);
 const showMyFollowPopup = ref(false);
-const currentLonlat = ref<{ longitude: number, latitude: number }>();
 
-const { value: currentCity } = useStorageVar('currentCityName', '厦门市');
-const currentCityCode = ref('');
+const currentRegion = ref<number | null>(null);
+const { value: currentCity } = useStorageVar('currentCityName', '');
+const currentLocation = useGetCurrentLocation({
+  onCityChanged: (city) => {
+    currentCity.value = city;
+  },
+});
+const currentNoticeContent = ref('目前厦门市已被点亮一个社区,刚刚小亮贡献了10个光源,让湖里区点亮了一个社区');
 
-watch(currentCity, async (newVal) => {
-  currentCityCode.value = await MapApi.simpleGetRegionCode(newVal);
-}, { immediate: true });
+const villageRankListLoader = useSimpleDataLoader(async () => {
+  const res = await LightVillageApi.getVillageRankList({ region_id: currentRegion.value ?? undefined, num: 3 });
+  return res.map((item, i) => ({
+    image: item.image ?? '',
+    title: item.name,
+    rank: i + 1,
+    id: item.id,
+  }));
+});
+const villageUserRankListLoader = useSimpleDataLoader(async () => {
+  const res = (await LightVillageApi.getVolunteerRankList({ region_id: currentRegion.value ?? undefined, num: 3 }))
+    .map((item, i) => ({
+      id: item.id,
+      image: item.image ?? '',
+      title: item.name,
+      rank: i + 1,
+      score: item.points,
+    }));
 
-const currentNoticeContent = ref('目前厦门市已被点亮一个社区,刚刚小亮贡献了10个光源,让湖里区点亮了一个社区');
+  if (res.length >= 3) {
+    //移动第一名到中间
+    const first = res[0];
+    ArrayUtils.removeAt(res, 0);
+    ArrayUtils.insert(res, 1, first);
+  }
 
-const recommendLoader = useSimpleDataLoader(async () => {
-   const res = (await VillageInfoApi.getListForDiscover(
-    1, 20, 
-    '', 
-  ));
-  return res.list.concat(res.list,res.list, res.list, res.list).sort(() => Math.random() - 0.5).map((item) => ({
-    ...item,
-    isLike: Math.random() > 0.5,
-    likes: Math.floor(Math.random() * 1000),
-    userName: '用户' + Math.floor(Math.random() * 1000000),
-    badge: item.villageName || '',
-  }))
+  return res
+});
+const recommendLoader = useSimplePageListLoader(20, async (page, pageSize, params) => {
+  const res = await LightVillageApi.getMessages(page, pageSize, params?.keywords ?? '');
+  return {
+    list: res.data.map((item) => ({
+      image: item.image ?? '',
+      title: item.title,
+      desc: item.content ?? '',
+      userName: item.nick_name ?? '',
+      likes: item.like_count,
+      isLike: false,
+    })),
+    total: res.total,
+  };
+}, true);
+
+watch(currentRegion, async (newVal) => {
+  await villageRankListLoader.reload();
+  await villageUserRankListLoader.reload();
 });
 
 function goDetails(item: VillageListItem) {
@@ -211,14 +252,26 @@ function goDetails(item: VillageListItem) {
   villageStore.setCurrentVillage(item);
   emit('goVillage')
 }
+function goMessageDetails(item: any) {
+  uni.openOfficialAccountArticle({
+    url: item.jump_url,
+    fail: (err) => {
+      console.error(err);
+    },
+  })
+}
 async function handleChangedCity(city: string) {
   currentCity.value = city;
-  const res = await MapApi.searchPoi(city);
-  if (res.length > 0) {
-    currentLonlat.value = {
-      longitude: res[0].location[0],
-      latitude: res[0].location[1],
+  try {
+    const res = (await MapApi.simpleGetRegion(city)).requireData();
+    currentLocation.currentLonlat.value = {
+      longitude: Number(res.center.split(',')[0]),
+      latitude: Number(res.center.split(',')[1]),
     };
+    console.log('currentLocation.currentLonlat.value', currentLocation.currentLonlat.value);
+  } catch (error) {
+    console.error(error);
+    return;
   }
 }
 function handleSelectCity(city: CityItem) {
@@ -228,6 +281,16 @@ function handleSelectCity(city: CityItem) {
 }
 
 onMounted(async () => {
+  try {
+    if (currentCity.value) {
+      await currentLocation.setCurrentLocationWithCity(currentCity.value);
+    } else {
+      await currentLocation.getCurrentFuzzyLocation();
+    }
+  } catch (error) {
+    console.error(error);
+    toast('获取当前位置失败,现在显示的是');
+  }
   const res = await FollowVillageApi.getFollowVillageList({ page: 1, pageSize: 200 });
   villageStore.setMyFollowVillages(res.list);
   if (res.list.length > 0)

+ 9 - 5
src/pages/home/light/submit-map.vue

@@ -1,13 +1,19 @@
 <script setup lang="ts">
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useGetCurrentLocation } from '../composeabe/GetCurrentLocation';
 import AppCofig from '@/common/config/AppCofig';
 import LightMap from '../components/LightMap.vue';
 
 const { querys } = useLoadQuerys({
   latitude: AppCofig.defaultLonLat[1],
   longitude: AppCofig.defaultLonLat[0],
-  code: ''
+  city: ''
 }, async (querys) => {
+  currentLocation.setCurrentLocationWithCity(querys.city);
+});
+
+const currentLocation = useGetCurrentLocation({
+  onCityChanged: (city) => {},
 });
 
 </script>
@@ -15,10 +21,8 @@ const { querys } = useLoadQuerys({
 <template>
   <LightMap 
     full
-    :startLonlat="{ 
-      latitude: querys.latitude, 
-      longitude: querys.longitude 
-    }"
+    :lonlat="currentLocation.currentLonlat.value"
     :isLightMode="true" 
+    @getCurrentLonlat="currentLocation.getCurrentExactLocation"
   />
 </template>