// 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 { JournalType } from "../../../types/Journal"; import { MediaAttachType, PreviewImageMetadata } from "../../../types/Attachment"; import IOSize, { Unit } from "../../../utils/IOSize"; import { JournalApi } from "../../../api/JournalApi"; interface JournalEditorData { /** 模式:create 或 edit */ mode: "create" | "edit"; /** 日记 ID(编辑模式) */ id?: number; /** 想法 */ idea: string; /** 日期 */ date: string; /** 时间 */ time: string; /** 类型 */ type: JournalType; /** 媒体列表(创建模式:新选择的媒体;编辑模式:现有附件) */ mediaList: (MediaItem | WechatMediaItem)[]; /** 新媒体列表(仅编辑模式使用) */ 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: { mode: "create", id: undefined, idea: "", date: "2025-06-28", time: "16:00", type: JournalType.NORMAL, mediaList: [], newMediaList: [], location: undefined, isSaving: false, isLoading: false, 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"] || false); let isAuthLocation = JSON.parse(wx.getStorageSync("isAuthLocation")); this.setData({ isAuthLocation }); if (!isAuthLocation) { wx.authorize({ scope: "scope.userLocation" }).then(() => { isAuthLocation = true; this.setData({ isAuthLocation }); }); } // 判断模式:有 ID 是编辑,无 ID 是创建 const id = options.id ? parseInt(options.id) : undefined; if (id) { // 编辑模式 this.setData({ mode: "edit", id, isLoading: true }); await this.loadJournalDetail(id); } else { // 创建模式 this.setData({ mode: "create", isLoading: false }); // 设置当前时间 const unixTime = new Date().getTime(); this.setData({ date: Time.toDate(unixTime), time: Time.toTime(unixTime) }); // 获取默认定位 this.getDefaultLocation(); } }, /** 获取默认定位(创建模式) */ getDefaultLocation() { 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 } }); } } }); }).catch(() => { // 忽略定位失败 }); }, /** 加载日记详情(编辑模式) */ async loadJournalDetail(id: number) { try { const journal = await JournalApi.getDetail(id); const items = journal.items || []; const thumbItems = items.filter((item) => item.attachType === MediaAttachType.THUMB); const mediaList: MediaItem[] = thumbItems.map((thumbItem) => { const metadata = (typeof thumbItem.metadata === "string" ? JSON.parse(thumbItem.metadata) : thumbItem.metadata) as PreviewImageMetadata; const thumbURL = `${config.url}/attachment/read/${thumbItem.mongoId}`; const sourceURL = `${config.url}/attachment/read/${metadata.sourceMongoId}`; const isVideo = metadata.sourceMimeType?.startsWith("video/"); return { type: 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() { try { const location = await wx.chooseLocation({}); this.setData({ location: { lat: location.latitude, lng: location.longitude, text: location.name } }); } catch (err) { // 用户取消选择 } }, /** 新增附件 */ 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; }); if (that.data.mode === "create") { // 创建模式:直接添加到 mediaList that.setData({ mediaList: [...that.data.mediaList, ...newMedia] }); } else { // 编辑模式:添加到 newMediaList that.setData({ newMediaList: [...that.data.newMediaList, ...newMedia] }); } wx.hideLoading(); } }); }, /** 清空媒体(仅创建模式) */ clearMedia() { wx.showModal({ title: "提示", content: "确认清空已选照片或视频吗?", confirmText: "清空", confirmColor: "#E64340", cancelText: "取消", success: res => { if (res.confirm) { this.setData({ mediaList: [] }); } } }); }, /** 预览附件 */ preview(e: WechatMiniprogram.BaseEvent) { const isNewMedia = e.currentTarget.dataset.newMedia; const index = e.currentTarget.dataset.index; if (this.data.mode === "create") { // 创建模式:只有 mediaList const sources = (this.data.mediaList as WechatMediaItem[]).map(item => ({ url: item.path, type: item.type.toLowerCase() })); const total = sources.length; const startIndex = Math.max(0, index - 25); const endIndex = Math.min(total, startIndex + 50); const newCurrentIndex = index - startIndex; wx.previewMedia({ current: newCurrentIndex, sources: sources.slice(startIndex, endIndex) as WechatMiniprogram.MediaSource[] }); } else { // 编辑模式:mediaList + newMediaList const sources = (this.data.mediaList as MediaItem[]).map(item => ({ url: item.sourceURL, type: item.type })); const newSources = this.data.newMediaList.map(item => ({ url: item.path, type: item.type })); 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 (this.data.mode === "create") { // 创建模式:从 mediaList 删除 const mediaList = [...this.data.mediaList]; mediaList.splice(index, 1); this.setData({ mediaList }); } else { // 编辑模式:根据标识删除 if (isNewMedia) { const newMediaList = [...this.data.newMediaList]; newMediaList.splice(index, 1); this.setData({ newMediaList }); } else { const mediaList = [...this.data.mediaList]; mediaList.splice(index, 1); this.setData({ mediaList }); } } }, /** 取消 */ cancel() { if (this.data.mode === "create") { wx.switchTab({ url: "/pages/main/journal/index" }); } else { 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(); }, /** 执行删除 */ async executeDelete() { try { await JournalApi.delete(this.data.id!); Events.emit("JOURNAL_REFRESH"); Events.emit("JOURNAL_LIST_REFRESH"); wx.showToast({ title: "删除成功", icon: "success" }); setTimeout(() => { wx.navigateBack(); }, 1000); } catch (error) { console.error("删除日记失败:", error); } }, /** 提交/保存 */ submit() { if (this.data.mode === "create") { this.createJournal(); } else { this.updateJournal(); } }, /** 创建日记 */ async createJournal() { const handleFail = () => { wx.showToast({ title: "上传失败", icon: "error" }); this.setData({ isSaving: false }); }; this.setData({ isSaving: true }); try { // 获取 openId const getOpenId = 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: () => reject(new Error("登录失败")) }); }); // 文件上传 const uploadFiles = this.uploadMediaFiles(this.data.mediaList as WechatMediaItem[]); // 并行执行获取 openId 和文件上传 const [openId, tempFileIds] = await Promise.all([getOpenId, uploadFiles]); await JournalApi.create({ idea: this.data.idea, type: this.data.type, lat: this.data.location?.lat, lng: this.data.location?.lng, location: this.data.location?.text, pusher: openId, createdAt: Date.parse(`${this.data.date} ${this.data.time}`), tempFileIds }); Events.emit("JOURNAL_REFRESH"); wx.showToast({ title: "提交成功", icon: "success" }); this.setData({ idea: "", mediaList: [], isSaving: false, uploaded: "0", uploadTotal: "0 MB", uploadProgress: 0 }); await Toolkit.sleep(1000); wx.switchTab({ url: "/pages/main/journal/index" }); } catch (error) { console.error("创建日记失败:", error); handleFail(); } }, /** 更新日记 */ async updateJournal() { const handleFail = () => { wx.showToast({ title: "保存失败", icon: "error" }); this.setData({ isSaving: false }); }; this.setData({ isSaving: true }); try { // 收集保留的附件 ID(缩略图 ID) const attachmentIds = (this.data.mediaList as MediaItem[]).map(item => item.attachmentId); // 上传新媒体文件 const tempFileIds = await this.uploadMediaFiles(this.data.newMediaList); await JournalApi.update({ 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(), attachmentIds, tempFileIds }); 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(); } catch (error) { console.error("更新日记失败:", error); handleFail(); } }, /** 上传媒体文件 */ uploadMediaFiles(mediaList: WechatMediaItem[]): Promise { return new Promise((resolve, reject) => { if (mediaList.length === 0) { resolve([]); return; } wx.showLoading({ title: "正在上传..", mask: true }); // 计算总大小 const sizePromises = mediaList.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 = mediaList.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); }); } });