// pages/main/journal-editor/index.ts import Events from "../../../utils/Events"; import Time from "../../../utils/Time"; import Toolkit from "../../../utils/Toolkit"; import config from "../../../config/index"; import { Location, MediaItem, MediaItemType, WechatMediaItem } from "../../../types/UI"; import { Journal, JournalType } from "../../../types/Journal"; import { MediaAttachExt, MediaAttachType } from "../../../types/Attachment"; import IOSize, { Unit } from "../../../utils/IOSize"; interface JournalEditorData { id?: number; idea: string; date: string; time: string; type: JournalType; mediaList: MediaItem[]; newMediaList: WechatMediaItem[]; location?: Location; isAuthLocation: boolean; isLoading: boolean; isSaving: boolean; uploaded: string; uploadTotal: string; uploadSpeed: string; uploadProgress: number; mediaItemTypeEnum: any; deleteDialogVisible: boolean; deleteConfirmText: string; } Page({ data: { id: undefined, idea: "", date: "2025-06-28", time: "16:00", type: JournalType.NORMAL, mediaList: [], newMediaList: [], location: undefined, isSaving: false, isLoading: true, uploaded: "0", uploadTotal: "0 MB", uploadSpeed: "0 MB / s", uploadProgress: 0, mediaItemTypeEnum: { ...MediaItemType }, isAuthLocation: false, deleteDialogVisible: false, deleteConfirmText: "" }, async onLoad(options: any) { // 授权定位 const setting = await wx.getSetting(); wx.setStorageSync("isAuthLocation", setting.authSetting["scope.userLocation"]); let isAuthLocation = JSON.parse(wx.getStorageSync("isAuthLocation")); this.setData({ isAuthLocation }); if (!isAuthLocation) { wx.authorize({ scope: "scope.userLocation" }).then(() => { isAuthLocation = true; this.setData({ isAuthLocation }); }); } // 获取日记 ID const id = options.id ? parseInt(options.id) : undefined; if (!id) { wx.showToast({ title: "缺少日志 ID", icon: "error" }); setTimeout(() => { wx.navigateBack(); }, 1500); return; } this.setData({ id }); await this.loadJournalDetail(id); }, /** 加载日记详情 */ async loadJournalDetail(id: number) { wx.showLoading({ title: "加载中...", mask: true }); try { const journal: Journal = await new Promise((resolve, reject) => { wx.request({ url: `${config.url}/journal/${id}`, method: "POST", header: { Key: wx.getStorageSync("key") }, success: (res: any) => { if (res.data.code === 20000) { resolve(res.data.data); } else { reject(new Error(res.data.message || "加载失败")); } }, fail: reject }); }); const items = journal.items || []; const thumbItems = items.filter((item) => item.attachType === MediaAttachType.THUMB); const mediaList: MediaItem[] = thumbItems.map((thumbItem) => { const ext = thumbItem.ext = JSON.parse(thumbItem.ext!.toString()) as MediaAttachExt; const thumbURL = `${config.url}/attachment/read/${thumbItem.mongoId}`; const sourceURL = `${config.url}/attachment/read/${ext.sourceMongoId}`; return { type: ext.isVideo ? MediaItemType.VIDEO : MediaItemType.IMAGE, thumbURL, sourceURL, size: thumbItem.size || 0, attachmentId: thumbItem.id } as MediaItem; }); this.setData({ idea: journal.idea || "", date: Time.toDate(journal.createdAt), time: Time.toTime(journal.createdAt), type: journal.type, location: journal.location ? { lat: journal.lat, lng: journal.lng, text: journal.location } : undefined, mediaList, isLoading: false }); wx.hideLoading(); } catch (err: any) { wx.hideLoading(); wx.showToast({ title: err.message || "加载失败", icon: "error" }); setTimeout(() => { wx.navigateBack(); }, 1500); } }, onChangeType(e: any) { const { value } = e.detail; this.setData({ type: value }); }, /** 选择位置 */ async chooseLocation() { const location = await wx.chooseLocation({}); this.setData({ location: { lat: location.latitude, lng: location.longitude, text: location.name } }); }, /** 新增附件 */ addMedia() { const that = this; wx.chooseMedia({ mediaType: ["mix"], sourceType: ["album", "camera"], camera: "back", success(res) { wx.showLoading({ title: "加载中..", mask: true }); const tempFiles = res.tempFiles; const newMedia = tempFiles.map(item => { return { type: (MediaItemType)[item.fileType.toUpperCase()], path: item.tempFilePath, thumbPath: item.thumbTempFilePath, size: item.size, duration: item.duration, raw: item } as WechatMediaItem; }); that.setData({ newMediaList: [...that.data.newMediaList, ...newMedia] }); wx.hideLoading(); } }); }, /** 预览附件 */ preview(e: WechatMiniprogram.BaseEvent) { const isNewMedia = e.currentTarget.dataset.newMedia; const index = e.currentTarget.dataset.index; const sources = this.data.mediaList.map(item => ({ url: item.sourceURL, type: MediaItemType[item.type].toLowerCase() })); const newSources = this.data.newMediaList.map(item => ({ url: item.path, type: MediaItemType[item.type].toLowerCase() })); const allSources = [...sources, ...newSources]; const itemIndex = isNewMedia ? this.data.mediaList.length + index : index; const total = allSources.length; const startIndex = Math.max(0, itemIndex - 25); const endIndex = Math.min(total, startIndex + 50); const newCurrentIndex = itemIndex - startIndex; wx.previewMedia({ current: newCurrentIndex, sources: allSources.slice(startIndex, endIndex) as WechatMiniprogram.MediaSource[] }); }, /** 删除附件 */ deleteMedia(e: WechatMiniprogram.BaseEvent) { const isNewMedia = e.currentTarget.dataset.newMedia; const index = e.currentTarget.dataset.index; if (isNewMedia) { const mediaList = [...this.data.mediaList]; mediaList.splice(index, 1); this.setData({ mediaList }); } else { const newMediaList = [...this.data.newMediaList]; newMediaList.splice(index, 1); this.setData({ newMediaList }); } }, /** 取消编辑 */ cancel() { wx.navigateBack(); }, /** 删除记录 */ deleteJournal() { this.setData({ deleteDialogVisible: true, deleteConfirmText: "" }); }, /** 取消删除 */ cancelDelete() { this.setData({ deleteDialogVisible: false, deleteConfirmText: "" }); }, /** 确认删除 */ confirmDelete() { const inputText = this.data.deleteConfirmText.trim(); if (inputText !== "确认删除") { wx.showToast({ title: "输入不匹配", icon: "error" }); return; } this.setData({ deleteDialogVisible: false }); this.executeDelete(); }, /** 执行删除 */ executeDelete() { wx.showLoading({ title: "删除中...", mask: true }); wx.request({ url: `${config.url}/journal/delete`, method: "POST", header: { Key: wx.getStorageSync("key"), "Content-Type": "application/json" }, data: { id: this.data.id }, success: (res: any) => { wx.hideLoading(); if (res.data.code === 20000 || res.statusCode === 200) { Events.emit("JOURNAL_REFRESH"); Events.emit("JOURNAL_LIST_REFRESH"); wx.showToast({ title: "删除成功", icon: "success" }); setTimeout(() => { wx.navigateBack(); }, 1000); } else { wx.showToast({ title: res.data.message || "删除失败", icon: "error" }); } }, fail: () => { wx.hideLoading(); wx.showToast({ title: "删除失败", icon: "error" }); } }); }, /** 保存 */ save() { const handleFail = () => { wx.showToast({ title: "保存失败", icon: "error" }); wx.hideLoading(); this.setData({ isSaving: false }); }; this.setData({ isSaving: true }); // 收集保留的附件 ID(缩略图 ID) const attachmentIds = this.data.mediaList.map(item => item.attachmentId); // 上传新媒体文件 const uploadFiles = new Promise((resolve, reject) => { if (this.data.newMediaList.length === 0) { resolve([]); return; } wx.showLoading({ title: "正在上传..", mask: true }); // 计算总大小 const sizePromises = this.data.newMediaList.map(item => { return new Promise((sizeResolve, sizeReject) => { wx.getFileSystemManager().getFileInfo({ filePath: item.path, 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; this.setData({ uploadTotal: IOSize.format(totalSize, 2, Unit.MB) }); // 计算上传速度 const speedUpdateInterval = setInterval(() => { const chunkSize = uploadedSize - lastUploadedSize; this.setData({ uploadSpeed: `${IOSize.format(chunkSize)} / s` }); lastUploadedSize = uploadedSize; }, 1000); // 上传文件 const uploadPromises = this.data.newMediaList.map(item => { return new Promise((uploadResolve, uploadReject) => { const task = wx.uploadFile({ url: `${config.url}/temp/file/upload`, filePath: item.path, name: "file", success: (resp) => { const result = JSON.parse(resp.data); if (result && result.code === 20000) { 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; // 更新进度条 this.setData({ uploaded: IOSize.formatWithoutUnit(uploadedSize, 2, Unit.MB), uploadProgress: Math.round((uploadedSize / totalSize) * 10000) / 100 }); }); uploadTasks.push(task); }); }); Promise.all(uploadPromises).then((tempFileIds) => { // 清除定时器 clearInterval(speedUpdateInterval); uploadTasks.forEach(task => task.offProgressUpdate()); this.setData({ uploadProgress: 100, uploadSpeed: "0 MB / s" }); resolve(tempFileIds); }).catch((e: Error) => { // 取消所有上传任务 uploadTasks.forEach(task => task.abort()); clearInterval(speedUpdateInterval); reject(e); }); }).catch(reject); }); // 提交保存 uploadFiles.then((tempFileIds) => { wx.showLoading({ title: "正在保存..", mask: true }); wx.request({ url: `${config.url}/journal/update`, method: "POST", header: { Key: wx.getStorageSync("key") }, data: { id: this.data.id, idea: this.data.idea, type: this.data.type, lat: this.data.location?.lat, lng: this.data.location?.lng, location: this.data.location?.text, createdAt: new Date(`${this.data.date}T${this.data.time}:00`).getTime(), // 保留的现有附件 ID attachmentIds, // 新上传的临时文件 ID tempFileIds }, success: async (resp: any) => { if (resp.data.code === 20000 || resp.statusCode === 200) { Events.emit("JOURNAL_REFRESH"); Events.emit("JOURNAL_LIST_REFRESH"); wx.showToast({ title: "保存成功", icon: "success" }); this.setData({ isSaving: false, uploaded: "0", uploadTotal: "0 MB", uploadProgress: 0 }); await Toolkit.sleep(1000); wx.navigateBack(); } else { handleFail(); } }, fail: handleFail }); }).catch(handleFail); } });