352 lines
10 KiB
JavaScript
352 lines
10 KiB
JavaScript
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 IMAGE_PLACEHOLDER_URL = 'https://picsum.photos/200/200';
|
|
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 IMAGE_PLACEHOLDER_URL;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
});
|