// pages/main/moment/index.ts import config from "../../../config/index"; import Events from "../../../utils/Events"; import IOSize, { Unit } from "../../../utils/IOSize"; import Time from "../../../utils/Time"; import Toolkit from "../../../utils/Toolkit"; import { Location, MediaItemType } from "../../../types/UI"; import { PreviewImageMetadata } from "../../../types/Attachment"; import { MomentApi } from "../../../api/MomentApi"; import { JournalApi } from "../../../api/JournalApi"; import { Network } from "../../../utils/Network"; type Item = { id: number; type: MediaItemType; thumbURL: string; sourceURL: string; checked: boolean; } type MD5Result = { path: string; md5: string; } type ArchiveStep = "select-type" | "select-journal" | "form"; interface MomentData { list: Item[]; type: string; idea: string; date: string; time: string; location?: Location; qqMapSDK?: any; uploaded: string; hasChecked: boolean; isUploading: boolean; isArchiving: boolean; uploadSpeed: string; uploadTotal: string; uploadProgress: number; isAuthLocation: boolean; isVisibleArchivePopup: boolean; archiveStep: ArchiveStep; selectedJournalId: number | null; } Page({ data: { list: [], type: "NORMAL", idea: "", date: "2025-06-28", time: "16:00", location: undefined, uploaded: "0", hasChecked: false, isUploading: false, isArchiving: false, uploadSpeed: "0 MB / s", uploadTotal: "0 MB", uploadProgress: 0, isAuthLocation: false, isVisibleArchivePopup: false, archiveStep: "select-type", selectedJournalId: null }, async onLoad() { this.fetch(); // 授权定位 const setting = await wx.getSetting(); wx.setStorageSync("isAuthLocation", setting.authSetting["scope.userLocation"] || false); let isAuthLocation = JSON.parse(wx.getStorageSync("isAuthLocation")); this.setData({ isAuthLocation }); if (!isAuthLocation) { wx.authorize({ scope: "scope.userLocation" }).then(() => { isAuthLocation = true; this.setData({ isAuthLocation }); }); } const unixTime = new Date().getTime(); this.setData({ idea: this.data.idea, date: Time.toDate(unixTime), time: Time.toTime(unixTime) }); // 获取默认定位 wx.getLocation({ type: "gcj02" }).then(resp => { this.setData({ location: { lat: resp.latitude, lng: resp.longitude }, }); const argLoc = `location=${this.data.location!.lat},${this.data.location!.lng}`; const argKey = "key=WW5BZ-J4LCM-UIT6I-65MXY-Z5HDT-VRFFU"; wx.request({ url: `https://apis.map.qq.com/ws/geocoder/v1/?${argLoc}&${argKey}`, success: res => { if (res.statusCode === 200) { this.setData({ location: { lat: this.data.location!.lat, lng: this.data.location!.lng, text: (res.data as any).result?.formatted_addresses?.recommend } }); } } }); }); }, async chooseLocation() { const location = await wx.chooseLocation({}); this.setData({ location: { lat: location.latitude, lng: location.longitude, text: location.name } }); }, async fetch() { try { const list = await MomentApi.getList(); if (!list || list.length === 0) { return; } this.setData({ list: list.map((item: any) => { const metadata = (typeof item.metadata === "string" ? JSON.parse(item.metadata) : item.metadata) as PreviewImageMetadata; const thumbURL = `${config.url}/attachment/read/${item.mongoId}`; const sourceURL = `${config.url}/attachment/read/${metadata.sourceMongoId}`; return { id: item.id, type: metadata.isImage ? MediaItemType.IMAGE : MediaItemType.VIDEO, thumbURL, sourceURL, checked: false } as Item; }) }); } catch (error) { console.error("加载 moment 列表失败:", error); } }, updateHasChecked() { this.setData({ hasChecked: this.data.list.some(item => item.checked) }); }, onCheckChange(e: WechatMiniprogram.BaseEvent) { const index = e.currentTarget.dataset.index; const list = [...this.data.list]; list[index].checked = !list[index].checked; this.setData({ list }); this.updateHasChecked(); }, preview(e: WechatMiniprogram.BaseEvent) { const index = e.currentTarget.dataset.index; const total = this.data.list.length; const startIndex = Math.max(0, index - 25); const endIndex = Math.min(total, startIndex + 50); const newCurrentIndex = index - startIndex; const sources = this.data.list.slice(startIndex, endIndex).map((item: Item) => { return { url: item.sourceURL, type: item.type.toLowerCase() } }) as any; wx.previewMedia({ current: newCurrentIndex, sources }) }, uploadMedia() { const handleFail = (e?: any) => { wx.showToast({ title: "上传失败", icon: "error" }); wx.hideLoading(); this.setData({ isUploading: false, }); wx.reportEvent("wxdata_perf_monitor", { wxdata_perf_monitor_id: "MOMENT_UPLOAD", wxdata_perf_monitor_level: 9, wxdata_perf_error_code: 1, wxdata_perf_error_msg: e?.message }); }; const that = this; wx.chooseMedia({ mediaType: ["mix"], sourceType: ["album", "camera"], camera: "back", async success(res) { that.setData({ isUploading: true }); wx.showLoading({ title: "正在读取..", mask: true }) let files = res.tempFiles; // 计算 MD5 const md5Results: MD5Result[] = []; await Promise.all(files.map(async (file) => { const md5 = await new Promise((resolve, reject) => { wx.getFileSystemManager().getFileInfo({ filePath: file.tempFilePath, digestAlgorithm: "md5", success: res => resolve(res.digest), fail: () => reject(`读取失败: ${file.tempFilePath}`) }); }); md5Results.push({ path: file.tempFilePath, md5: md5, } as MD5Result); })); // 查重 const filterMD5Result: string[] = await MomentApi.filterByMD5( md5Results.map(item => item.md5) ); // 过滤文件 const filterPath = md5Results.filter(item => filterMD5Result.indexOf(item.md5) !== -1) .map(item => item.path); files = files.filter(file => filterPath.indexOf(file.tempFilePath) !== -1); if (files.length === 0) { wx.hideLoading(); that.setData({ isUploading: false }); return; } // 使用 Network.uploadFiles 上传文件 try { const tempFileIds = await Network.uploadFiles({ mediaList: files.map(file => ({ path: file.tempFilePath, size: file.size })), onProgress: (progress) => { that.setData({ uploaded: IOSize.formatWithoutUnit(progress.uploaded, 2, Unit.MB), uploadTotal: IOSize.format(progress.total, 2, Unit.MB), uploadSpeed: `${IOSize.format(progress.speed)} / s`, uploadProgress: progress.percent }); }, showLoading: true }); // 上传完成转附件 const list = await MomentApi.create(tempFileIds); wx.showToast({ title: "上传成功", icon: "success" }); const added = list.map((item: any) => { const metadata = (typeof item.metadata === "string" ? JSON.parse(item.metadata) : item.metadata) as PreviewImageMetadata; const thumbURL = `${config.url}/attachment/read/${item.mongoId}`; const sourceURL = `${config.url}/attachment/read/${metadata.sourceMongoId}`; return { id: item.id, type: metadata.isImage ? MediaItemType.IMAGE : MediaItemType.VIDEO, thumbURL, sourceURL, checked: false } as Item; }); // 前插列表 that.data.list.unshift(...added); that.setData({ list: that.data.list, isUploading: false, uploaded: "0", uploadTotal: "0 MB", uploadProgress: 0 }); that.updateHasChecked(); } catch (error) { handleFail(error); } } }) }, showArchivePopup() { this.setData({ isVisibleArchivePopup: true, archiveStep: "select-type", selectedJournalId: null }); }, onArchiveToNew() { this.setData({ archiveStep: "form", selectedJournalId: null }); }, onArchiveToExisting() { this.setData({ archiveStep: "select-journal", selectedJournalId: null }); }, onJournalListSelect(e: any) { const { id } = e.detail; this.setData({ selectedJournalId: id }); }, onJournalListConfirm() { if (this.data.selectedJournalId) { this.archiveChecked(); } }, onJournalListCancel() { this.setData({ archiveStep: "select-type", selectedJournalId: null }); }, onNewJournalBack() { this.setData({ archiveStep: "select-type" }); }, toggleArchivePopup() { this.setData({ isVisibleArchivePopup: !this.data.isVisibleArchivePopup }); }, onArchivePopupVisibleChange(e: any) { this.setData({ isVisibleArchivePopup: e.detail.visible }); }, onChangeArchiveType(e: any) { const { value } = e.detail; this.setData({ type: value }); }, async archiveChecked() { const handleFail = () => { wx.showToast({ title: "归档失败", icon: "error" }); wx.hideLoading(); this.setData({ isArchiving: false }); }; this.setData({ isArchiving: true }); wx.showLoading({ title: "正在归档..", mask: true }) const openId = await new Promise((resolve, reject) => { wx.login({ success: async (res) => { if (res.code) { try { const openId = await JournalApi.getOpenId(res.code); resolve(openId); } catch (error) { reject(new Error("获取 openId 失败")); } } else { reject(new Error("获取登录凭证失败")); } }, fail: handleFail }); }); const archiveData: any = { type: this.data.type, idea: this.data.idea, lat: this.data.location?.lat, lng: this.data.location?.lng, location: this.data.location?.text, pusher: openId, thumbIds: this.data.list.filter(item => item.checked).map(item => item.id) }; // 如果选择了已存在的记录,添加 id 参数 if (this.data.selectedJournalId) { archiveData.id = this.data.selectedJournalId; } try { await MomentApi.archive(archiveData); Events.emit("JOURNAL_REFRESH"); wx.showToast({ title: "归档成功", icon: "success" }); this.setData({ idea: "", list: [], hasChecked: false, isArchiving: false, selectedJournalId: undefined, isVisibleArchivePopup: false }); await Toolkit.sleep(1000); this.fetch(); } catch (error) { handleFail(); } }, allChecked() { this.data.list.forEach(item => item.checked = true); this.setData({ list: this.data.list }); this.updateHasChecked(); }, clearChecked() { wx.showModal({ title: "提示", content: "确认清空已选照片或视频吗?", confirmText: "清空已选", confirmColor: "#E64340", cancelText: "取消", success: res => { if (res.confirm) { this.data.list.forEach(item => { item.checked = false; }) this.setData({ list: this.data.list }); this.updateHasChecked(); } } }) }, deleteChecked() { wx.showModal({ title: "提示", content: "确认删除已选照片或视频吗?", confirmText: "删除已选", confirmColor: "#E64340", cancelText: "取消", success: async (res) => { if (res.confirm) { const selected = this.data.list.filter(item => item.checked); try { await MomentApi.delete(selected.map(item => item.id)); wx.showToast({ title: "删除成功", icon: "success" }); const list = this.data.list.filter(item => !item.checked); this.setData({ list }); this.updateHasChecked(); } catch (error) { console.error("删除 moment 失败:", error); wx.showToast({ title: "删除失败", icon: "error" }); } } } }) } })