// pages/main/journal-map/index.ts import config from "../../../config/index"; import Time from "../../../utils/Time"; import { Journal, JournalPageType } from "../../../types/Journal"; import { MediaAttachExt, MediaAttachType } from "../../../types/Attachment"; import Toolkit from "../../../utils/Toolkit"; interface MapMarker { id: number; latitude: number; longitude: number; width: number; height: number; customCallout: { anchorY: number; anchorX: number; display: string; }; } interface JournalInfo { id: number; date: string; time: string; location?: string; idea?: string; items: Array<{ type: number; thumbURL: string; sourceURL: string; mongoId: string; }>; } interface LocationMarker { locationKey: string; // 位置键 "lat,lng" lat: number; lng: number; location?: string; journalIds: number[]; // 该位置的所有日记 ID count: number; // 日记数量 date: string; previewThumb?: string; // 预览缩略图 } interface SelectedLocationInfo { location?: string; journals: JournalInfo[]; } interface JournalMapData { centerLat: number; centerLng: number; scale: number; markers: MapMarker[]; locations: LocationMarker[]; // 位置标记列表 customCalloutMarkerIds: string[]; // 改为 string[] 以支持 locationKey includePoints: Array<{ latitude: number; longitude: number }>; // 缩放视野以包含所有点 selectedLocation: SelectedLocationInfo | null; // 选中的位置信息 showDetail: boolean; // 是否显示详情(控制 DOM 存在) detailVisible: boolean; // 详情是否可见(控制动画) isLoading: boolean; } Page({ data: { centerLat: 39.908823, centerLng: 116.397470, scale: 13, markers: [], locations: [], customCalloutMarkerIds: [], includePoints: [], selectedLocation: null, showDetail: false, detailVisible: false, isLoading: true, }, async onLoad() { await this.loadJournals(); }, /** 加载所有记录 */ async loadJournals() { this.setData({ isLoading: true }); try { const list: Journal[] = await new Promise((resolve, reject) => { wx.request({ url: `${config.url}/journal/list`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: { page: 0, size: 9007199254740992, type: JournalPageType.PREVIEW }, success: (resp: any) => { if (resp.data.code === 20000) { resolve(resp.data.data.list); } else { reject(new Error(resp.data.message || "加载失败")); } }, fail: reject }); }) || []; // 过滤有位置信息的记录,并按位置分组 const locationMap = new Map(); list.filter((journal: any) => journal.lat && journal.lng).forEach((journal: any) => { // 保留 6 位小数作为位置键,约等于 0.1 米精度 const lat = Number(journal.lat.toFixed(6)); const lng = Number(journal.lng.toFixed(6)); const locationKey = `${lat},${lng}`; if (!locationMap.has(locationKey)) { // 获取第一个有缩略图的日记 const thumbItem = journal.items.find((item: any) => item.attachType === "THUMB"); locationMap.set(locationKey, { locationKey, lat, lng, location: journal.location, journalIds: [], count: 0, date: Time.toPassedDate(journal.createdAt), previewThumb: thumbItem ? `${config.url}/attachment/read/${thumbItem.mongoId}` : undefined }); } const marker = locationMap.get(locationKey)!; marker.journalIds.push(journal.id); marker.count++; // 如果还没有预览图,尝试从当前日记获取 if (!marker.previewThumb) { const thumbItem = journal.items.find((item: any) => item.attachType === "THUMB"); if (thumbItem) { marker.previewThumb = `${config.url}/attachment/read/${thumbItem.mongoId}`; } } }); const locations = Array.from(locationMap.values()); if (locations.length === 0) { wx.showToast({ title: "暂无位置记录", icon: "none" }); this.setData({ isLoading: false }); return; } // 生成地图标记 const markers: MapMarker[] = locations.map((location, index) => ({ id: index, latitude: location.lat, longitude: location.lng, width: 24, height: 30, customCallout: { anchorY: -2, anchorX: 0, display: "ALWAYS" } })); // 所有标记的 locationKey 列表 const customCalloutMarkerIds = locations.map(l => l.locationKey); // 计算中心点(所有标记的平均位置) const centerLat = locations.reduce((sum, l) => sum + l.lat, 0) / locations.length; const centerLng = locations.reduce((sum, l) => sum + l.lng, 0) / locations.length; // 缩放视野以包含所有标记点 const includePoints = locations.map((l) => ({ latitude: l.lat, longitude: l.lng })); this.setData({ locations, markers, customCalloutMarkerIds, centerLat, centerLng, includePoints, isLoading: false }); } catch (err: any) { wx.showToast({ title: err.message || "加载失败", icon: "error" }); this.setData({ isLoading: false }); } }, /** 标记点击事件 */ onMarkerTap(e: any) { const markerId = e.detail.markerId || e.markerId; this.loadLocationDetail(markerId); }, /** 气泡点击事件 */ onCalloutTap(e: any) { const markerId = e.detail.markerId || e.markerId; this.loadLocationDetail(markerId); }, /** 加载位置详情(该位置的所有日记) */ async loadLocationDetail(markerId: number) { const location = this.data.locations[markerId]; if (!location) return; wx.showLoading({ title: "加载中...", mask: true }); try { // 根据 journalIds 加载日记详情 const list: Journal[] = await new Promise((resolve, reject) => { wx.request({ url: `${config.url}/journal/list/ids`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: location.journalIds, success: (resp: any) => { if (resp.data.code === 20000) { resolve(resp.data.data); } else { reject(new Error(resp.data.message || "加载失败")); } }, fail: reject }); }) || []; // 转换为 JournalInfo 格式 const journals: JournalInfo[] = list.map((journal: any) => { const date = new Date(journal.createdAt); return { id: journal.id, date: Time.toPassedDateTime(journal.createdAt), time: `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`, location: journal.location, idea: journal.idea, items: journal.items .filter((item: any) => item.attachType === MediaAttachType.THUMB) .map((item: any) => { const ext = JSON.parse(item.ext); return { type: ext.isVideo ? 1 : 0, thumbURL: `${config.url}/attachment/read/${item.mongoId}`, sourceURL: `${config.url}/attachment/read/${ext.sourceMongoId}`, mongoId: item.mongoId, }; }) }; }); // 先显示元素,再触发动画 this.setData({ selectedLocation: { location: location.location, journals }, showDetail: true }); wx.nextTick(() => { this.setData({ detailVisible: true }); }); wx.hideLoading(); } catch (err: any) { wx.hideLoading(); wx.showToast({ title: err.message || "加载失败", icon: "error" }); } }, /** 关闭详情 */ async closeDetail() { this.setData({ detailVisible: false }); await Toolkit.sleep(350); this.setData({ showDetail: false, selectedLocation: null }); }, });