// 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 { MediaAttachExt } from "../../../types/Attachment"; 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 } }); }, fetch() { wx.request({ url: `${config.url}/journal/moment/list`, method: "POST", header: { Key: wx.getStorageSync("key") }, success: async (resp: any) => { const list = resp.data.data; if (!list || list.length === 0) { return; } this.setData({ list: list.map((item: any) => { const ext = JSON.parse(item.ext) as MediaAttachExt; const thumbURL = `${config.url}/attachment/read/${item.mongoId}`; const sourceURL = `${config.url}/attachment/read/${ext.sourceMongoId}`; return { id: item.id, type: ext.isImage ? MediaItemType.IMAGE : MediaItemType.VIDEO, thumbURL, sourceURL, checked: false } as Item; }) }); } }); }, 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 new Promise((resolve, reject) => { wx.request({ url: `${config.url}/journal/moment/filter`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: md5Results.map(item => item.md5), success: async (resp: any) => { resolve(resp.data.data); }, fail: reject }); }); // 过滤文件 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(); return; } wx.showLoading({ title: "正在上传..", mask: true }) // 计算上传大小 const sizePromises = files.map(file => { return new Promise((sizeResolve, sizeReject) => { wx.getFileSystemManager().getFileInfo({ filePath: file.tempFilePath, success: (res) => sizeResolve(res.size), fail: (err) => sizeReject(err) }); }); }); Promise.all(sizePromises).then(fileSizes => { const totalSize = fileSizes.reduce((acc, size) => acc + size, 0); const uploadTasks: WechatMiniprogram.UploadTask[] = []; let uploadedSize = 0; let lastUploadedSize = 0; that.setData({ uploadTotal: IOSize.format(totalSize, 2, Unit.MB) }); // 计算上传速度 const speedUpdateInterval = setInterval(() => { const chunkSize = uploadedSize - lastUploadedSize; that.setData({ uploadSpeed: `${IOSize.format(chunkSize)} / s` }); lastUploadedSize = uploadedSize; }, 1000); // 上传文件 const uploadPromises = files.map(file => { return new Promise((uploadResolve, uploadReject) => { const task = wx.uploadFile({ url: `${config.url}/temp/file/upload`, filePath: file.tempFilePath, name: "file", success: (resp) => { const result = JSON.parse(resp.data); if (result && result.code === 20000) { // 更新进度 const progress = totalSize > 0 ? uploadedSize / totalSize : 1; that.setData({ uploadProgress: Math.round(progress * 10000) / 100 }); uploadResolve(result.data[0].id); } else { uploadReject(new Error(`文件上传失败: ${result?.message || "未知错误"}`)); } }, fail: (err) => uploadReject(new Error(`文件上传失败: ${err.errMsg}`)) }); // 监听上传进度事件 let prevProgress = 0; task.onProgressUpdate((res) => { const fileUploaded = (res.totalBytesExpectedToSend * res.progress) / 100; const delta = fileUploaded - prevProgress; uploadedSize += delta; // 保存当前进度 prevProgress = fileUploaded; // 更新进度条 that.setData({ uploaded: IOSize.formatWithoutUnit(uploadedSize, 2, Unit.MB), uploadProgress: Math.round((uploadedSize / totalSize) * 10000) / 100 }); }); uploadTasks.push(task); }); }); Promise.all(uploadPromises).then((tempFileIds) => { wx.showLoading({ title: "正在保存..", mask: true }) // 清除定时器 clearInterval(speedUpdateInterval); uploadTasks.forEach(task => task.offProgressUpdate()); that.setData({ uploadProgress: 100, uploadSpeed: "0 MB / s" }); // 上传完成转附件 wx.request({ url: `${config.url}/journal/moment/create`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: tempFileIds, success: async (resp: any) => { wx.showToast({ title: "上传成功", icon: "success" }); const list = resp.data.data; const added = list.map((item: any) => { const ext = JSON.parse(item.ext) as MediaAttachExt; const thumbURL = `${config.url}/attachment/read/${item.mongoId}`; const sourceURL = `${config.url}/attachment/read/${ext.sourceMongoId}`; return { id: item.id, type: ext.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(); wx.hideLoading(); }, fail: handleFail }); }).catch((e: Error) => { // 取消所有上传任务 uploadTasks.forEach(task => task.abort()); that.updateHasChecked(); handleFail(e); }); }).catch(handleFail); } }) }, 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: (res) => { if (res.code) { wx.request({ url: `${config.url}/journal/openid`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: { code: res.code }, success: (resp) => { const data = resp.data as any; if (data.code === 20000) { resolve(data.data); } else { reject(new Error("获取 openId 失败")); } }, fail: () => 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; } wx.request({ url: `${config.url}/journal/moment/archive`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: archiveData, success: async (resp: any) => { if (resp.data && resp.data.code === 20000) { 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(); } else { wx.showToast({ title: "归档失败", icon: "error" }); this.setData({ isArchiving: false }); } }, fail: 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: res => { if (res.confirm) { const selected = this.data.list.filter(item => item.checked); wx.request({ url: `${config.url}/journal/moment/delete`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: selected.map(item => item.id), success: async (resp: any) => { if (resp.data && resp.data.code === 20000) { wx.showToast({ title: "删除成功", icon: "success" }); const list = this.data.list.filter(item => !item.checked); this.setData({ list }); this.updateHasChecked(); } }, }); } } }) } })