// pages/main/travel-location-editor/index.ts import { Network, WechatMediaItem } from "../../../utils/Network"; import { TravelLocationApi } from "../../../api/TravelLocationApi"; import { TravelLocationType, TravelLocationTypeLabel } from "../../../types/Travel"; import { MediaAttachType, PreviewImageMetadata } from "../../../types/Attachment"; import config from "../../../config/index"; import { MediaItem, MediaItemType } from "../../../types/UI"; interface TravelLocationEditorData { /** 模式:create 或 edit */ mode: "create" | "edit"; /** 出行地点 ID(编辑模式) */ id?: number; /** 关联的出行计划 ID */ travelId: number; /** 地点类型 */ type: TravelLocationType; /** 标题 */ title: string; /** 说明 */ description: string; /** 位置描述 */ location: string; /** 纬度 */ lat: number; /** 经度 */ lng: number; /** 费用 */ amount: number; /** 是否需要身份证 */ requireIdCard: boolean; /** 是否需要预约 */ requireAppointment: boolean; /** 评分 */ score: number; /** 评分是否未定 */ scoreUndecided: boolean; /** 重要程度 */ importance: number; /** 重要程度是否未定 */ importanceUndecided: boolean; /** 媒体列表(创建和编辑模式使用) */ mediaList: (MediaItem | WechatMediaItem)[]; /** 新媒体列表(编辑模式使用) */ newMediaList: WechatMediaItem[]; /** 是否正在加载(编辑模式) */ isLoading: boolean; /** 是否正在保存 */ isSaving: boolean; /** 是否正在上传 */ isUploading: boolean; /** 上传进度信息 */ uploadInfo: string; /** 地点类型选项 */ locationTypes: string[]; /** 地点类型值数组 */ locationTypeValues: TravelLocationType[]; /** 地点类型选中索引 */ locationTypeIndex: number; /** 删除对话框可见性 */ deleteDialogVisible: boolean; /** 删除确认文本 */ deleteConfirmText: string; /** 媒体类型枚举 */ mediaItemTypeEnum: any; } Page({ data: { mode: "create", id: undefined, travelId: 0, type: TravelLocationType.FOOD, title: "", description: "", location: "", lat: 0, lng: 0, amount: 0, requireIdCard: false, requireAppointment: false, score: 3, scoreUndecided: true, importance: 1, importanceUndecided: true, mediaList: [], newMediaList: [], isLoading: false, isSaving: false, isUploading: false, uploadInfo: "", mediaItemTypeEnum: { ...MediaItemType }, locationTypes: Object.values(TravelLocationTypeLabel), locationTypeValues: [ TravelLocationType.FOOD, TravelLocationType.HOTEL, TravelLocationType.TRANSPORT, TravelLocationType.ATTRACTION, TravelLocationType.SHOPPING, TravelLocationType.PLAY, TravelLocationType.LIFE ], locationTypeIndex: 0, deleteDialogVisible: false, deleteConfirmText: "" }, onLoad(options: any) { // 获取 travelId(必填) const travelId = options.travelId ? parseInt(options.travelId) : 0; if (!travelId) { wx.showToast({ title: "缺少出行计划 ID", icon: "error" }); setTimeout(() => { wx.navigateBack(); }, 1500); return; } // 判断模式:有 ID 是编辑,无 ID 是创建 const id = options.id ? parseInt(options.id) : undefined; if (id) { // 编辑模式 this.setData({ mode: "edit", id, travelId, isLoading: true }); this.loadLocationDetail(id); } else { // 创建模式 this.setData({ mode: "create", travelId, isLoading: false }); } }, /** 加载地点详情(编辑模式) */ async loadLocationDetail(id: number) { wx.showLoading({ title: "加载中...", mask: true }); try { const location = await TravelLocationApi.getDetail(id); // 计算地点类型索引 const type = location.type || TravelLocationType.ATTRACTION; const locationTypeIndex = this.data.locationTypeValues.findIndex( item => item === type ); const items = location.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}`; return { type: metadata.isVideo ? MediaItemType.VIDEO : MediaItemType.IMAGE, thumbURL, sourceURL, size: thumbItem.size || 0, attachmentId: thumbItem.id } as MediaItem; }); // 判断评分是否未定 const scoreUndecided = location.score === null || location.score === undefined; const score = location.score !== null && location.score !== undefined ? location.score : 3; // 判断重要程度是否未定 const importanceUndecided = location.importance === null || location.importance === undefined; const importance = location.importance !== null && location.importance !== undefined ? location.importance : 1; this.setData({ type, locationTypeIndex: locationTypeIndex >= 0 ? locationTypeIndex : 0, title: location.title || "", description: location.description || "", location: location.location || "", lat: location.lat || 0, lng: location.lng || 0, amount: location.amount || 0, requireIdCard: location.requireIdCard || false, requireAppointment: location.requireAppointment || false, score, scoreUndecided, importance, importanceUndecided, mediaList, isLoading: false }); } catch (error) { wx.showToast({ title: "加载失败", icon: "error" }); setTimeout(() => { wx.navigateBack(); }, 1500); } finally { wx.hideLoading(); } }, /** 改变地点类型 */ onChangeLocationType(e: any) { const index = e.detail.value; this.setData({ locationTypeIndex: index, type: this.data.locationTypeValues[index] }); }, /** 改变是否需要身份证 */ onChangeRequireIdCard(e: any) { this.setData({ requireIdCard: e.detail.value }); }, /** 改变是否需要预约 */ onChangeRequireAppointment(e: any) { this.setData({ requireAppointment: e.detail.value }); }, /** 改变评分 */ onChangeScore(e: any) { this.setData({ score: e.detail.value }); }, /** 改变重要程度 */ onChangeImportance(e: any) { this.setData({ importance: e.detail.value }); }, /** 清除评分 */ clearScore() { this.setData({ scoreUndecided: true }); }, /** 清除重要程度 */ clearImportance() { this.setData({ importanceUndecided: true }); }, /** 点击未定文字选择评分 */ selectScore() { this.setData({ scoreUndecided: false, score: 3 }); }, /** 点击未定文字选择重要程度 */ selectImportance() { this.setData({ importanceUndecided: false, importance: 1 }); }, /** 选择位置 */ chooseLocation() { wx.chooseLocation({ success: (res) => { const locationName = res.name || res.address; const updateData: any = { location: locationName, lat: res.latitude, lng: res.longitude }; // 如果标题为空,使用选择的地点名称作为标题 if (!this.data.title.trim()) { updateData.title = res.name || locationName; } this.setData(updateData); }, fail: (err) => { if (err.errMsg.includes("auth deny")) { wx.showToast({ title: "请授权位置权限", icon: "error" }); } } }); }, /** 添加媒体 */ 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") { that.setData({ mediaList: [...that.data.mediaList, ...newMedia] }); } else { that.setData({ newMediaList: [...that.data.newMediaList, ...newMedia] }); } wx.hideLoading(); } }); }, /** 删除媒体(创建模式) */ deleteMedia(e: any) { const { index } = e.currentTarget.dataset; const mediaList = this.data.mediaList; mediaList.splice(index, 1); this.setData({ mediaList }); }, /** 删除新媒体(编辑模式) */ deleteNewMedia(e: any) { const { index } = e.currentTarget.dataset; const newMediaList = this.data.newMediaList; newMediaList.splice(index, 1); this.setData({ newMediaList }); }, /** 删除已有媒体(编辑模式) */ deleteExistingMedia(e: any) { const { index } = e.currentTarget.dataset; const existingMedia = this.data.mediaList; existingMedia.splice(index, 1); this.setData({ existingMedia }); }, /** 预览媒体 */ 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.toLowerCase() })); const newSources = this.data.newMediaList.map(item => ({ url: item.path, type: 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[] }); } }, /** 取消 */ cancel() { wx.navigateBack(); }, /** 删除地点 */ deleteLocation() { 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() { if (!this.data.id) { return; } wx.showLoading({ title: "删除中...", mask: true }); try { await TravelLocationApi.delete(this.data.id); wx.showToast({ title: "删除成功", icon: "success" }); setTimeout(() => { wx.navigateBack(); }, 1500); } catch (error) { // 错误已由 Network 类处理 } finally { wx.hideLoading(); } }, /** 提交/保存 */ submit() { // 验证必填字段 if (!this.data.title.trim()) { wx.showToast({ title: "请输入标题", icon: "error" }); return; } if (!this.data.location || !this.data.lat || !this.data.lng) { wx.showToast({ title: "请选择位置", icon: "error" }); return; } if (this.data.mode === "create") { this.createLocation(); } else { this.updateLocation(); } }, /** 创建地点 */ async createLocation() { this.setData({ isSaving: true }); try { // 上传媒体文件 this.setData({ isUploading: true, uploadInfo: "正在上传 0%" }); const tempFileIds = await Network.uploadFiles({ mediaList: this.data.mediaList as WechatMediaItem[], onProgress: ({ percent }) => { this.setData({ uploadInfo: `正在上传 ${percent}%` }); } }); this.setData({ isUploading: false, uploadInfo: "" }); // 创建地点 await TravelLocationApi.create({ travelId: this.data.travelId, type: this.data.type, title: this.data.title.trim(), description: this.data.description.trim(), location: this.data.location, lat: this.data.lat, lng: this.data.lng, amount: this.data.amount, requireIdCard: this.data.requireIdCard, requireAppointment: this.data.requireAppointment, score: this.data.scoreUndecided ? null : this.data.score, importance: this.data.importanceUndecided ? null : this.data.importance, tempFileIds }); wx.showToast({ title: "创建成功", icon: "success" }); setTimeout(() => { wx.navigateBack(); }, 1000); } catch (error) { this.setData({ isSaving: false, isUploading: false, uploadInfo: "" }); } }, /** 更新地点 */ async updateLocation() { this.setData({ isSaving: true }); try { // 保留的附件 ID 列表 const attachmentIds = (this.data.mediaList as MediaItem[]).map(item => item.attachmentId); // 上传新媒体文件 this.setData({ isUploading: true, uploadInfo: "正在上传 0%" }); const tempFileIds = await Network.uploadFiles({ mediaList: this.data.newMediaList, onProgress: ({ percent }) => { this.setData({ uploadInfo: `正在上传 ${percent}%` }); } }); this.setData({ isUploading: false, uploadInfo: "" }); // 更新地点 await TravelLocationApi.update({ id: this.data.id!, travelId: this.data.travelId, type: this.data.type, title: this.data.title.trim(), description: this.data.description.trim(), location: this.data.location, lat: this.data.lat, lng: this.data.lng, amount: this.data.amount, requireIdCard: this.data.requireIdCard, requireAppointment: this.data.requireAppointment, score: this.data.scoreUndecided ? null : this.data.score, importance: this.data.importanceUndecided ? null : this.data.importance, attachmentIds, tempFileIds }); wx.showToast({ title: "保存成功", icon: "success" }); setTimeout(() => { wx.navigateBack(); }, 1000); } catch (error) { this.setData({ isSaving: false, isUploading: false, uploadInfo: "" }); } } });