diff --git a/miniprogram/app.json b/miniprogram/app.json index 1f505d4..af3d278 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -4,6 +4,7 @@ "pages/main/journal/index", "pages/main/journal-creater/index", "pages/main/journal-search/index", + "pages/main/journal-editor/index", "pages/main/portfolio/index", "pages/main/travel/index", "pages/main/about/index", diff --git a/miniprogram/components/journal-list/index.ts b/miniprogram/components/journal-list/index.ts index f440c07..2b4bef1 100644 --- a/miniprogram/components/journal-list/index.ts +++ b/miniprogram/components/journal-list/index.ts @@ -94,6 +94,7 @@ Component({ } }, methods: { + /** 重置搜索页面 */ resetPage() { const likeExample = this.data.searchValue ? { idea: this.data.searchValue, @@ -116,6 +117,7 @@ Component({ isFinished: false }); }, + /** 获取数据 */ fetch() { if (this.data.isFetching || this.data.isFinished) { return; @@ -159,19 +161,16 @@ Component({ } }); }, + /** 输入搜索 */ onSearchChange(e: WechatMiniprogram.CustomEvent) { const value = e.detail.value.trim(); this.setData({ searchValue: value }); - // 如果是清空操作,不使用防抖(clear 事件会处理) - if (value === "" && this.debouncedSearch) { - this.debouncedSearch.cancel(); - return; - } - // 使用防抖自动搜索 + // 使用防抖自动搜索(包括清空的情况) if (this.debouncedSearch) { this.debouncedSearch(value); } }, + /** 提交搜索 */ onSearchSubmit(e: WechatMiniprogram.CustomEvent) { const value = e.detail.value.trim(); // 立即搜索,取消防抖 @@ -180,6 +179,7 @@ Component({ } this.resetAndSearch(value); }, + /** 清空搜索 */ onSearchClear() { // 取消防抖,立即搜索 if (this.debouncedSearch) { @@ -187,6 +187,11 @@ Component({ } this.resetAndSearch(""); }, + /** 保留搜索关键字重新搜索 */ + reSearch() { + this.resetAndSearch(this.data.searchValue); + }, + /** 重置配置重新搜索 */ resetAndSearch(keyword: string) { const likeExample = keyword ? { idea: keyword, diff --git a/miniprogram/pages/main/about/index.wxml b/miniprogram/pages/main/about/index.wxml index 82ece9c..3888661 100644 --- a/miniprogram/pages/main/about/index.wxml +++ b/miniprogram/pages/main/about/index.wxml @@ -26,7 +26,7 @@ 版本: - 1.4.0 + 1.5.0 {{copyright}} diff --git a/miniprogram/pages/main/journal-editor/index.json b/miniprogram/pages/main/journal-editor/index.json new file mode 100644 index 0000000..13fcbea --- /dev/null +++ b/miniprogram/pages/main/journal-editor/index.json @@ -0,0 +1,10 @@ +{ + "component": true, + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon", + "t-button": "tdesign-miniprogram/button/button", + "t-navbar": "tdesign-miniprogram/navbar/navbar", + "t-dialog": "tdesign-miniprogram/dialog/dialog", + "t-input": "tdesign-miniprogram/input/input" + } +} diff --git a/miniprogram/pages/main/journal-editor/index.less b/miniprogram/pages/main/journal-editor/index.less new file mode 100644 index 0000000..b5ad364 --- /dev/null +++ b/miniprogram/pages/main/journal-editor/index.less @@ -0,0 +1,134 @@ +/* pages/main/journal-editor/index.wxss */ +.container { + + .content { + width: calc(100% - 64px); + padding: 0 32px 32px 32px; + display: flex; + align-items: center; + flex-direction: column; + + .loading { + padding: 64px 0; + text-align: center; + color: var(--theme-text-secondary); + } + + .label { + color: var(--theme-text-secondary); + } + + .section { + width: 100%; + margin-top: 1.5rem; + + &.time { + display: flex; + + .picker { + margin-right: .25rem; + } + } + + &.media { + + .gallery { + gap: 10rpx; + display: grid; + grid-template-columns: repeat(3, 1fr); + + .item { + width: 200rpx; + height: 200rpx; + position: relative; + overflow: hidden; + background: var(--theme-bg-card); + box-shadow: 1px 1px 6px var(--theme-shadow-light); + border-radius: 2rpx; + + &.add { + color: var(--theme-wx); + margin: 0; + font-size: 80rpx; + } + + .thumbnail { + height: 200rpx; + display: block; + } + + .video-container { + position: relative; + + + .play-icon { + top: 50%; + left: 50%; + color: rgba(255, 255, 255, .8); + z-index: 2; + position: absolute; + font-size: 128rpx; + transform: translate(-50%, -50%); + text-shadow: 4rpx 4rpx 0 rgba(0, 0, 0, .5); + pointer-events: none; + } + } + + + .delete { + top: 10rpx; + right: 10rpx; + color: rgba(0, 0, 0, .7); + z-index: 3; + position: absolute; + font-size: 45rpx; + } + + .new-badge { + top: 10rpx; + left: 10rpx; + color: var(--theme-wx); + z-index: 3; + display: flex; + position: absolute; + font-size: 45rpx; + text-shadow: 4rpx 4rpx 0 rgba(0, 0, 0, .5); + } + } + } + } + } + + .progress { + width: 100%; + margin-top: 1rem; + } + + .ctrl { + width: 100%; + display: flex; + margin-top: 1rem; + align-items: center; + + .delete { + width: 200rpx; + } + + .save { + flex: 1; + margin-left: 12rpx; + } + } + } +} + +.delete-dialog { + padding: 16rpx 0; + + .tips { + color: var(--theme-text-secondary); + font-size: 28rpx; + line-height: 1.5; + margin-bottom: 24rpx; + } +} diff --git a/miniprogram/pages/main/journal-editor/index.ts b/miniprogram/pages/main/journal-editor/index.ts new file mode 100644 index 0000000..dd4dcef --- /dev/null +++ b/miniprogram/pages/main/journal-editor/index.ts @@ -0,0 +1,382 @@ +// 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 } from "../../../types/Journal"; +import { MediaAttachExt, MediaAttachType } from "../../../types/Attachment"; + +interface JournalEditorData { + id?: number; + idea: string; + date: string; + time: string; + mediaList: MediaItem[]; + newMediaList: WechatMediaItem[]; + location?: Location; + isAuthLocation: boolean; + isLoading: boolean; + saveText: string; + isSaving: boolean; + saveProgress: number; + mediaItemTypeEnum: any; + deleteDialogVisible: boolean; + deleteConfirmText: string; +} + +Page({ + data: { + id: undefined, + idea: "", + date: "2025-06-28", + time: "16:00", + mediaList: [], + newMediaList: [], + location: undefined, + saveText: "保存", + isSaving: false, + saveProgress: 0, + isLoading: true, + 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), + 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); + } + }, + /** 选择位置 */ + 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 currentIndex = isNewMedia ? this.data.mediaList.length + index : index; + wx.previewMedia({ + current: currentIndex, + sources: allSources 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: 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" }); + this.setData({ + saveText: "保存", + isSaving: false + }); + }; + + this.setData({ + saveText: "正在保存..", + isSaving: true + }); + // 收集保留的附件 ID(缩略图 ID) + const attachmentIds = this.data.mediaList.map(item => item.attachmentId); + // 上传新媒体文件 + const uploadFiles = new Promise((resolve, reject) => { + const total = this.data.newMediaList.length; + let completed = 0; + if (total === 0) { + resolve([]); + return; + } + this.setData({ + saveProgress: 0, + }); + // 上传临时文件 + const uploadPromises = this.data.newMediaList.map((item) => { + return new Promise((uploadResolve, uploadReject) => { + 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) { + completed++; + // 更新进度 + this.setData({ + saveProgress: (completed / total), + }); + uploadResolve(result.data[0].id); + } else { + uploadReject(new Error(`文件上传失败: ${result?.message || '未知错误'}`)); + } + }, + fail: (err) => uploadReject(new Error(`文件上传失败: ${err.errMsg}`)) + }); + }); + }); + // 并行执行所有文件上传 + Promise.all(uploadPromises).then((tempFileIds) => { + this.setData({ + saveProgress: 1, + }); + resolve(tempFileIds); + }).catch(reject); + }); + // 提交保存 + uploadFiles.then((tempFileIds) => { + wx.request({ + url: `${config.url}/journal/update`, + method: "POST", + header: { + Key: wx.getStorageSync("key") + }, + data: { + id: this.data.id, + idea: this.data.idea, + 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({ + saveText: "保存", + isSaving: false, + }); + await Toolkit.sleep(1000); + wx.navigateBack(); + } else { + handleFail(); + } + }, + fail: handleFail + }); + }).catch(handleFail); + } +}); diff --git a/miniprogram/pages/main/journal-editor/index.wxml b/miniprogram/pages/main/journal-editor/index.wxml new file mode 100644 index 0000000..2d40e55 --- /dev/null +++ b/miniprogram/pages/main/journal-editor/index.wxml @@ -0,0 +1,166 @@ + + + 取消 + + + + + 加载中... + + + +