const api = require('../../utils/api'); const auth = require('../../utils/auth'); const MAX_IMAGES = 4; const MAX_IMAGE_SIZE = 1024 * 1024; // 超过 1MB 才压缩 const COMPRESS_LEVELS = [80, 60, 45]; // 逐级降低质量,直到满足大小限制 const ALLOWED_IMAGE_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']; const TYPE_LABELS = { weekly: '周计划', monthly: '月计划', yearly: '年计划' }; function errDetail(err) { if (err && err.detail) return String(err.detail); if (err && err.error) return String(err.error); if (err && err.message) return String(err.message); return ''; } function getFileSize(filePath) { return new Promise((resolve) => { wx.getFileInfo({ filePath, success: (res) => resolve(Number(res.size) || 0), fail: () => resolve(0) }); }); } function hasImageExt(path) { if (!path) return false; const ext = (String(path).split('.').pop() || '').toLowerCase(); return !!ext && ALLOWED_IMAGE_EXT.indexOf(ext) !== -1; } Page({ data: { planType: 'weekly', typeLabel: '周计划', periodKey: '', groupName: '', loading: true, loadError: '', canSubmit: true, message: '', content: '', checked: false, record: null, attachments: [], attachmentThumbs: [], uploading: false, submitting: false, history: [] }, onLoad(options) { const planType = options.plan_type || 'weekly'; this.setData({ planType, typeLabel: TYPE_LABELS[planType] || '计划', periodKey: options.period_key ? decodeURIComponent(options.period_key) : '', groupName: options.group_name ? decodeURIComponent(options.group_name) : '' }); auth.ensureLogin().then((token) => { if (!token) { this.setData({ loading: false, loadError: '请先在「我的」页面绑定手机号' }); setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 600); return; } this.loadStatus(); }).catch(() => { this.setData({ loading: false, loadError: '登录失败,请重试' }); }); }, onRetry() { this.setData({ loading: true, loadError: '' }); auth.ensureLogin().then((token) => { if (!token) { this.setData({ loading: false, loadError: '请先在「我的」页面绑定手机号' }); setTimeout(() => wx.switchTab({ url: '/pages/profile/profile' }), 600); return; } this.loadStatus(); }).catch(() => { this.setData({ loading: false, loadError: '登录失败,请重试' }); }); }, loadStatus() { api.request({ url: '/api/v1/plan/status', data: { type: this.data.planType, period_key: this.data.periodKey } }).then((res) => { this.applyRecord(res); if (this.data.planType !== 'weekly') { this.loadHistory(); } }).catch((err) => { this.setData({ loading: false, loadError: errDetail(err) || '加载计划失败' }); }); }, applyRecord(res) { const record = res.record; const attachments = record ? (record.attachments || []) : []; this.setData({ loading: false, loadError: '', periodKey: res.period_key || this.data.periodKey, canSubmit: res.can_submit !== false, message: res.message || '', checked: !!res.checked, record, content: record ? (record.content || '') : '', attachments, attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) }); }, loadHistory() { api.request({ url: '/api/v1/plan/history', data: { type: this.data.planType, page: 1, size: 20 } }) .then((res) => { const history = (res.list || []).map((item) => ({ ...item, thumbSrcs: (item.attachments || []).map((p) => this.planFileUrl(p)) })); this.setData({ history }); }).catch(() => {}); }, onContentInput(e) { this.setData({ content: e.detail.value }); }, planFileUrl(path) { if (!path) return ''; if (/^https?:\/\//i.test(path)) return path; const parts = String(path).replace(/^attachments\//, '').split('/'); if (parts.length < 4) { return ''; } return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/'); }, chooseImages() { const remain = MAX_IMAGES - this.data.attachments.length; if (remain <= 0) { wx.showToast({ title: '最多上传4张图片', icon: 'none' }); return; } if (!this.data.canSubmit || !this.data.periodKey) { wx.showToast({ title: '当前不可上传', icon: 'none' }); return; } wx.chooseMedia({ count: remain, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => { const paths = (res.tempFiles || []).map((f) => f.tempFilePath); this.uploadImages(paths); } }); }, uploadImages(paths) { if (this.data.uploading || !paths.length) { return; } const planType = this.data.planType; const periodKey = this.data.periodKey; let attachments = this.data.attachments.slice(); this.setData({ uploading: true }); const run = (index) => { if (index >= paths.length) { this.setData({ uploading: false, attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) }); return; } const rawPath = paths[index]; const doUpload = (src) => { wx.uploadFile({ url: api.BASE_URL + '/api/v1/plan/upload', filePath: src, name: 'files', formData: { plan_type: planType, period_key: periodKey }, header: { Authorization: 'Bearer ' + auth.getToken() }, success: (res) => { let data = {}; try { data = JSON.parse(res.data); } catch (e) { data = {}; } if (data.success && Array.isArray(data.attachments)) { attachments = attachments.concat(data.attachments).slice(0, MAX_IMAGES); this.setData({ attachments, attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) }); } else { wx.showToast({ title: data.error || '上传失败', icon: 'none' }); } run(index + 1); }, fail: () => { wx.showToast({ title: '上传失败', icon: 'none' }); run(index + 1); } }); }; // 按大小限制压缩:小图直接上传,大图逐级降质量直到 ≤ MAX_IMAGE_SIZE const tryCompress = (src, level) => { getFileSize(src).then((size) => { if (!size || size <= MAX_IMAGE_SIZE || level >= COMPRESS_LEVELS.length) { doUpload(src); return; } wx.compressImage({ src, quality: COMPRESS_LEVELS[level], success: (res) => { const compressed = res.tempFilePath || src; if (!hasImageExt(compressed)) { // 压缩产物扩展名丢失时回退当前图,避免后端按扩展名校验失败 doUpload(src); return; } tryCompress(compressed, level + 1); }, fail: () => doUpload(src) }); }); }; tryCompress(rawPath, 0); }; run(0); }, removeImage(e) { if (this.data.uploading) { return; } const index = Number(e.currentTarget.dataset.index); const path = this.data.attachments[index]; if (!path) { return; } const parts = path.replace(/^attachments\//, '').split('/'); const dropLocal = () => { const attachments = this.data.attachments.slice(); attachments.splice(index, 1); this.setData({ attachments, attachmentThumbs: attachments.map((p) => this.planFileUrl(p)) }); }; if (parts.length < 4) { dropLocal(); return; } const sid = parts[0]; const planType = parts[1]; const period = parts[2]; const filename = parts[3]; api.request({ url: `/api/v1/plan/file/${sid}/${planType}/${period}/${encodeURIComponent(filename)}`, method: 'DELETE' }).then(dropLocal).catch(() => { wx.showToast({ title: '删除失败', icon: 'none' }); }); }, previewImage(e) { const index = Number(e.currentTarget.dataset.index); const urls = this.data.attachmentThumbs; const src = urls[index]; if (!src) { return; } wx.previewImage({ current: src, urls }); }, previewHistoryImage(e) { const historyId = e.currentTarget.dataset.id; const index = Number(e.currentTarget.dataset.index); const item = this.data.history.find((h) => String(h.id) === String(historyId)); if (!item) { return; } const urls = item.thumbSrcs || []; const src = urls[index]; if (!src) { return; } wx.previewImage({ current: src, urls }); }, submit() { if (this.data.submitting) { return; } if (!this.data.canSubmit) { wx.showToast({ title: this.data.message || '当前不可提交', icon: 'none' }); return; } const hasText = !!(this.data.content || '').trim(); const hasImages = !!this.data.attachments.length; if (!hasText && !hasImages) { wx.showToast({ title: '请填写内容或上传图片', icon: 'none' }); return; } const doPost = () => { this.setData({ submitting: true }); api.request({ url: '/api/v1/plan/checkin', method: 'POST', data: { plan_type: this.data.planType, period_key: this.data.periodKey, status: '已完成', content: this.data.content, attachments: this.data.attachments } }).then(() => { this.setData({ submitting: false }); wx.showToast({ title: '已保存', icon: 'success' }); wx.setStorageSync('plan_overview_dirty', true); setTimeout(() => wx.navigateBack(), 600); }).catch((err) => { this.setData({ submitting: false }); const message = errDetail(err) || '保存失败'; wx.showToast({ title: message, icon: 'none' }); }); }; if (this.data.checked || this.data.record) { wx.showModal({ title: '覆盖确认', content: '将覆盖本周期已提交的计划,确定保存?', success: (r) => { if (r.confirm) { doPost(); } } }); } else { doPost(); } } });