init: initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
function camelName(key) { return key.replace(/_([a-z0-9])/g, function(_, c) { return c.toUpperCase(); }); }
|
||||
function snakeName(key) { return key.replace(/[A-Z]/g, function(c) { return '_' + c.toLowerCase(); }); }
|
||||
function mapKeys(value, name) {
|
||||
if (Array.isArray(value)) return value.map(function(v) { return mapKeys(v, name); });
|
||||
if (value && typeof value === 'object') { var out = {}; Object.keys(value).forEach(function(key) { out[name(key)] = mapKeys(value[key], name); }); return out; }
|
||||
return value;
|
||||
}
|
||||
module.exports = { camelName, toWire: function(v) { return mapKeys(v, camelName); }, fromWire: function(v) { return mapKeys(v, snakeName); } };
|
||||
@@ -0,0 +1,14 @@
|
||||
const { getImageUrl } = require('./request');
|
||||
function diaryEntry(item) {
|
||||
var images = item.images || [];
|
||||
if (typeof images === 'string') { try { images = JSON.parse(images); } catch (e) { images = []; } }
|
||||
if (!Array.isArray(images)) images = [];
|
||||
var date = String(item.created_at || '').slice(0, 10);
|
||||
return Object.assign({}, item, { title: item.title || '日常的一页', summary: String(item.content || '').replace(/\s+/g, ' ').slice(0, 100), date: date, dateLabel: date.replace(/-/g, '.'), cover: getImageUrl(images[0]), imageCount: images.length, author: item.creator_name || item.created_by_name || '' });
|
||||
}
|
||||
function albumEntry(item) { return Object.assign({}, item, { cover: getImageUrl(item.cover), title: item.title || '我们的相册' }); }
|
||||
function memoryEntry(item) {
|
||||
var date = String(item.event_date || '').slice(0, 10);
|
||||
return Object.assign({}, item, { date: date, dateMonth: date.slice(5, 7), dateDay: date.slice(8, 10) });
|
||||
}
|
||||
module.exports = { diaryEntry, albumEntry, memoryEntry };
|
||||
@@ -0,0 +1,16 @@
|
||||
// 只计算设备布局,不保存接口数据。
|
||||
function getLayout() {
|
||||
var info = {};
|
||||
try { info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); } catch (e) {}
|
||||
var status = info.statusBarHeight || 20;
|
||||
var capsule;
|
||||
try { capsule = wx.getMenuButtonBoundingClientRect(); } catch (e) {}
|
||||
var bar = capsule && capsule.height > 0 && capsule.top >= status
|
||||
? capsule.height + (capsule.top - status) * 2 : 44;
|
||||
var height = status + bar;
|
||||
var width = info.windowWidth || 375;
|
||||
var side = capsule && capsule.left > width / 2 ? width - capsule.left + 12 : 100;
|
||||
return { statusBarHeight: status, barHeight: bar, navHeight: height, navSide: side,
|
||||
layoutStyle: '--nav-height:' + height + 'px;--status-height:' + status + 'px;' };
|
||||
}
|
||||
module.exports = { getLayout };
|
||||
@@ -0,0 +1,7 @@
|
||||
// 底部导航只属于三个主页面,二级页面由顶部返回按钮导航。
|
||||
const paths = {home:'pages/index/index',records:'pages/discover/discover',profile:'pages/profile/profile'};
|
||||
function tabForPage(route) {
|
||||
const current = String(route || '').replace(/^\//, '').split('?')[0];
|
||||
return Object.keys(paths).find(function(tab) { return paths[tab] === current; }) || '';
|
||||
}
|
||||
module.exports = { tabForPage, paths };
|
||||
@@ -0,0 +1,477 @@
|
||||
const { camelName, toWire, fromWire } = require('./api-naming');
|
||||
/**
|
||||
* 网络请求工具 - 对接后端 API
|
||||
*/
|
||||
const API_BASE = 'https://past.sundynix.cn/api';
|
||||
const MEDIA_BASE = 'https://past.sundynix.cn/backend/';
|
||||
|
||||
/**
|
||||
* 将相对路径转为完整URL(供小程序端显示图片使用)
|
||||
* @param {string} path - 相对路径,如 "uploads/avatars/xxx.jpg"
|
||||
* @returns {string} 完整URL,如 "http://192.168.2.103/uploads/avatars/xxx.jpg"
|
||||
*/
|
||||
function getImageUrl(path) {
|
||||
if (!path) return '';
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||||
if (path.startsWith('/')) return MEDIA_BASE + path.substring(1);
|
||||
return MEDIA_BASE + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析上传响应,兼容各种格式,提取 error 和 code 字段
|
||||
* @param {*} raw - 原始响应数据
|
||||
* @returns {object|null} 解析后的对象,包含可能的 error 和 code 字段
|
||||
*/
|
||||
function parseUploadResponse(raw) {
|
||||
if (!raw) return null;
|
||||
if (typeof raw === 'object') return fromWire(raw);
|
||||
var str = String(raw).trim();
|
||||
// 去除 BOM 和不可见字符
|
||||
str = str.replace(/^\uFEFF/, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
||||
try {
|
||||
return fromWire(JSON.parse(str));
|
||||
} catch(e) {
|
||||
// 尝试提取 JSON 片段(Android 端可能返回带额外字符的响应)
|
||||
var match = str.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
try { return fromWire(JSON.parse(match[0])); } catch(e2) {}
|
||||
}
|
||||
// 最后尝试:查找 "success" 或 "path" 关键词,手动构建响应对象
|
||||
if (str.indexOf('success') > -1 || str.indexOf('path') > -1) {
|
||||
var pathMatch = str.match(/"path"\s*:\s*"([^"]+)"/);
|
||||
if (pathMatch) {
|
||||
return { success: true, path: pathMatch[1] };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化上传错误信息,包含错误码
|
||||
* @param {object} data - 解析后的响应数据
|
||||
* @returns {string} 格式化的错误信息
|
||||
*/
|
||||
function formatUploadError(data) {
|
||||
if (!data) return '上传失败';
|
||||
var msg = data.error || '上传失败';
|
||||
if (data.code) {
|
||||
msg += ' (错误码: ' + data.code + ')';
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
* @param {string} action - API action 名称
|
||||
* @param {object} data - 请求数据
|
||||
* @param {string} method - 请求方法 GET/POST
|
||||
*/
|
||||
function authApp() {
|
||||
return typeof getApp === 'function' ? getApp() : null;
|
||||
}
|
||||
async function waitForAuth() {
|
||||
var app = authApp();
|
||||
if (app && app.ensureAuth) await app.ensureAuth();
|
||||
}
|
||||
async function request(action, data = null, method = 'POST') {
|
||||
if (camelName(action) === 'wechatLogin') return sendRequest(action, data, method);
|
||||
await waitForAuth();
|
||||
var token = wx.getStorageSync('token');
|
||||
try {
|
||||
return await sendRequest(action, data, method);
|
||||
} catch (error) {
|
||||
var app = authApp();
|
||||
if (error._statusCode !== 401 || !app || !app.refreshAuth) throw error;
|
||||
await app.refreshAuth(token);
|
||||
// 每个业务请求最多重试一次,防止 401 导致无限登录。
|
||||
return sendRequest(action, data, method);
|
||||
}
|
||||
}
|
||||
function sendRequest(action, data = null, method = 'POST') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token');
|
||||
const header = { 'Content-Type': 'application/json' };
|
||||
if (token) {
|
||||
header['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
|
||||
let url = API_BASE + '/' + encodeURIComponent(camelName(action));
|
||||
let requestData = toWire(data);
|
||||
|
||||
if (method === 'GET' && data) {
|
||||
const params = [];
|
||||
for (let key in data) {
|
||||
if (data[key] !== null && data[key] !== undefined) {
|
||||
params.push(camelName(key) + '=' + encodeURIComponent(data[key]));
|
||||
}
|
||||
}
|
||||
if (params.length > 0) {
|
||||
url += '?' + params.join('&');
|
||||
}
|
||||
requestData = null;
|
||||
}
|
||||
|
||||
// 后端返回 no-store;请求地址仅保留业务参数。
|
||||
|
||||
wx.request({
|
||||
url: url,
|
||||
method: method,
|
||||
timeout: 15000,
|
||||
data: requestData,
|
||||
header: header,
|
||||
success: (res) => {
|
||||
try { wx.hideLoading(); } catch(e) {}
|
||||
if (res.statusCode >= 400 || (res.data && res.data.error)) {
|
||||
reject({ ...fromWire(res.data), _statusCode: res.statusCode });
|
||||
} else {
|
||||
resolve(fromWire(res.data));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
try { wx.hideLoading(); } catch(e) {}
|
||||
wx.showToast({ title: '网络请求失败', icon: 'none' });
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========== API 方法封装 ==========
|
||||
const api = {
|
||||
// 用户相关
|
||||
wechatLogin: (code, region) => request('wechatLogin', { code: code, region: region || '' }),
|
||||
checkAuth: () => request('checkAuth', null, 'GET'),
|
||||
logout: () => request('logout', null, 'POST'),
|
||||
updateProfile: (data) => request('updateProfile', data),
|
||||
updateLocation: (location) => request('updateLocation', { location: location }),
|
||||
updateIpLocation: (data) => request('updateIpLocation', data || {}, 'POST'),
|
||||
_uploadFileDirect: async function(url, filePath, name, formData) {
|
||||
await waitForAuth();
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token');
|
||||
var uploadUrl = url;
|
||||
var sep = uploadUrl.indexOf('?') > -1 ? '&' : '?';
|
||||
uploadUrl += sep + 'token=' + encodeURIComponent(token || '');
|
||||
var finalFormData = toWire(formData || {});
|
||||
finalFormData.token = token || '';
|
||||
// 从 URL 中提取 action 参数并添加到 formData(解决非开发者模式下 URL 参数被剥离的问题)
|
||||
var actionMatch = url.match(/\/api\/([^/?]+)|[?&]action=([^&]+)/);
|
||||
if (actionMatch) finalFormData.action = decodeURIComponent(actionMatch[1] || actionMatch[2]);
|
||||
var finalHeader = {};
|
||||
if (token) finalHeader['Authorization'] = 'Bearer ' + token;
|
||||
function doUpload(fp) {
|
||||
return new Promise((res, rej) => {
|
||||
wx.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: fp,
|
||||
name: name || 'file',
|
||||
header: finalHeader,
|
||||
formData: finalFormData,
|
||||
timeout: 120000,
|
||||
success: (r) => {
|
||||
var parsed = parseUploadResponse(r.data);
|
||||
if (parsed) res(parsed);
|
||||
else rej({ error: '解析响应失败', raw: r.data });
|
||||
},
|
||||
fail: (e) => rej({ error: '网络异常', errMsg: e.errMsg })
|
||||
});
|
||||
});
|
||||
}
|
||||
compressForUpload(filePath).then((compressedPath) => {
|
||||
doUpload(compressedPath).then(resolve).catch(() => {
|
||||
doUpload(filePath).then(resolve).catch(reject);
|
||||
});
|
||||
}).catch(() => {
|
||||
doUpload(filePath).then(resolve).catch(reject);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
uploadAvatar: async (filePath) => {
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
try {
|
||||
const data = await api._uploadFileDirect(
|
||||
API_BASE + '/uploadAvatar',
|
||||
filePath,
|
||||
'avatar',
|
||||
{}
|
||||
);
|
||||
wx.hideLoading();
|
||||
if (data && (data.path || data.success)) return data;
|
||||
wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
||||
throw data || { error: '上传失败' };
|
||||
} catch(e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
uploadPartnerAvatar: async (filePath, partnerId) => {
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
try {
|
||||
const data = await api._uploadFileDirect(
|
||||
API_BASE + '/uploadPartnerAvatar',
|
||||
filePath,
|
||||
'avatar',
|
||||
{ partner_id: partnerId }
|
||||
);
|
||||
wx.hideLoading();
|
||||
if (data && (data.path || data.success)) return data;
|
||||
wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
||||
throw data || { error: '上传失败' };
|
||||
} catch(e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
// 情侣相关
|
||||
coupleInfo: () => request('coupleInfo', null, 'GET'),
|
||||
updateCoupleInfo: (data) => request('updateCoupleInfo', data),
|
||||
invitePartner: (data) => request('invitePartner', data),
|
||||
unbindRequest: () => request('unbindRequest', null, 'POST'),
|
||||
unbindRespond: (data) => request('unbindRespond', data),
|
||||
unbindCancel: () => request('unbindCancel', null, 'POST'),
|
||||
unbindStatus: () => request('unbindStatus', null, 'GET'),
|
||||
myInvitations: () => request('myInvitations', null, 'GET'),
|
||||
respondInvitation: (data) => request('respondInvitation', data),
|
||||
acceptShareInvite: (data) => request('acceptShareInvite', data),
|
||||
updatePartnerProfile: (data) => request('updatePartnerProfile', data),
|
||||
|
||||
// 时间线
|
||||
timelineList: () => request('timelineList', null, 'GET'),
|
||||
timelineAdd: (data) => request('timelineAdd', data),
|
||||
timelineUpdate: (data) => request('timelineUpdate', data),
|
||||
timelineDelete: (data) => request('timelineDelete', data),
|
||||
timelineDetail: (id) => request('timelineDetail', { id: id }, 'GET'),
|
||||
|
||||
// 相册
|
||||
albumList: (data, method) => request('albumList', data, method || 'GET'),
|
||||
albumAdd: (data) => request('albumAdd', data),
|
||||
albumUpdate: (data) => request('albumUpdate', data),
|
||||
albumDelete: (data) => request('albumDelete', data),
|
||||
photoDelete: (data) => request('photoDelete', data),
|
||||
photoAdd: (data) => request('photoAdd', data),
|
||||
photos: (albumId, data, method) => request('photos', { album_id: albumId, ...data }, method || 'GET'),
|
||||
photoUpload: async (filePath, albumId) => {
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
try {
|
||||
await waitForAuth();
|
||||
// 直接使用 wx.uploadFile,兼容安卓端不发送自定义 header 的问题
|
||||
const token = wx.getStorageSync('token');
|
||||
const uploadUrl = API_BASE + '/photoUpload?token=' + encodeURIComponent(token || '');
|
||||
const baseFormData = { albumId: albumId, token: token || '', action: 'photoUpload' };
|
||||
const data = await new Promise((resolve, reject) => {
|
||||
// 先压缩图片
|
||||
compressForUpload(filePath).then((compressedPath) => {
|
||||
wx.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: compressedPath,
|
||||
name: 'photos',
|
||||
formData: baseFormData,
|
||||
timeout: 120000,
|
||||
success: (res) => {
|
||||
var parsed = parseUploadResponse(res.data);
|
||||
if (parsed) resolve(parsed);
|
||||
else reject({ error: '解析响应失败', raw: res.data });
|
||||
},
|
||||
fail: (err) => {
|
||||
// 压缩后上传失败,尝试原路径
|
||||
wx.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: filePath,
|
||||
name: 'photos',
|
||||
formData: baseFormData,
|
||||
timeout: 120000,
|
||||
success: (r) => {
|
||||
var parsed = parseUploadResponse(r.data);
|
||||
if (parsed) resolve(parsed);
|
||||
else reject({ error: '解析响应失败', raw: r.data });
|
||||
},
|
||||
fail: (e2) => reject({ error: '网络异常', errMsg: e2.errMsg })
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 压缩失败直接上传原路径
|
||||
wx.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: filePath,
|
||||
name: 'photos',
|
||||
formData: baseFormData,
|
||||
timeout: 120000,
|
||||
success: (r) => {
|
||||
var parsed = parseUploadResponse(r.data);
|
||||
if (parsed) resolve(parsed);
|
||||
else reject({ error: '解析响应失败', raw: r.data });
|
||||
},
|
||||
fail: (e2) => reject({ error: '网络异常', errMsg: e2.errMsg })
|
||||
});
|
||||
});
|
||||
});
|
||||
wx.hideLoading();
|
||||
if (data && (data.success || data.path || data.uploaded)) return data;
|
||||
wx.showToast({ title: formatUploadError(data), icon: 'none' });
|
||||
throw data || { error: '上传失败' };
|
||||
} catch(e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: formatUploadError(e), icon: 'none' });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
// 日记
|
||||
diaryList: (data) => request('diaryList', data || null, 'GET'),
|
||||
diaryAdd: (data) => request('diaryAdd', data),
|
||||
diaryUpdate: (data) => request('diaryUpdate', data),
|
||||
diaryDelete: (data) => request('diaryDelete', data),
|
||||
diaryDetail: (id) => request('diaryDetail', { id: id }, 'GET'),
|
||||
diaryCommentList: (data) => request('diaryCommentList', data, 'GET'),
|
||||
diaryCommentAdd: (data) => request('diaryCommentAdd', data),
|
||||
diaryCommentDelete: (data) => request('diaryCommentDelete', data),
|
||||
// 点赞/收藏功能已移除
|
||||
|
||||
// 一起听过的歌
|
||||
songsList: () => request('songsList', null, 'GET'),
|
||||
songsAdd: (data) => request('songsAdd', data),
|
||||
songsUpdate: (data) => request('songsUpdate', data),
|
||||
songsDelete: (data) => request('songsDelete', data),
|
||||
|
||||
// 打卡
|
||||
checkinList: () => request('checkinList', null, 'GET'),
|
||||
checkinAdd: (data) => request('checkinAdd', data),
|
||||
checkinDo: (data) => request('checkinDo', data),
|
||||
checkinStats: () => request('checkinStats', null, 'GET'),
|
||||
checkinDetail: (id) => request('checkinDetail', { id: id }, 'GET'),
|
||||
checkinDelete: (data) => request('checkinDelete', data),
|
||||
|
||||
// 背景图
|
||||
getBgConfig: () => request('getBgConfig', null, 'GET'),
|
||||
deleteBgImage: () => request('deleteBgImage', {}),
|
||||
|
||||
// 操作记录
|
||||
activityLogList: (data) => request('activityLogList', data, 'GET'),
|
||||
|
||||
// 订阅消息
|
||||
subscribeTemplate: (data) => request('subscribeTemplate', data),
|
||||
unsubscribeTemplate: (data) => request('unsubscribeTemplate', data),
|
||||
mySubscribeTemplates: () => request('mySubscribeTemplates', null, 'GET'),
|
||||
|
||||
// 宠物养成
|
||||
petInfo: () => request('petInfo', null, 'GET'),
|
||||
petAdopt: (data) => request('petAdopt', data),
|
||||
petDailyAction: (data) => request('petDailyAction', data),
|
||||
petAchievements: () => request('petAchievements', null, 'GET'),
|
||||
petRename: (data) => request('petRename', data),
|
||||
|
||||
// 姨姨
|
||||
// 权限和周期数据会随伴侣设置立即变化,始终请求最新结果。
|
||||
periodList: () => request('periodList', null, 'GET'),
|
||||
periodAdd: (data) => request('periodAdd', data),
|
||||
periodUpdate: (data) => request('periodUpdate', data),
|
||||
periodDelete: (data) => request('periodDelete', data),
|
||||
periodDailySave: (data) => request('periodDailySave', data),
|
||||
// 爱爱记录(支持一天多条)
|
||||
periodSexAdd: (data) => request('periodSexAdd', data),
|
||||
periodSexDelete: (data) => request('periodSexDelete', data),
|
||||
periodSexList: (data) => request('periodSexList', data, 'GET'),
|
||||
// 姨姨权限(对方可见)
|
||||
periodVisibleGet: () => request('periodVisibleGet', null, 'GET'),
|
||||
periodVisibleSet: (data) => request('periodVisibleSet', data),
|
||||
periodSourceSelect: (data) => request('periodSourceSelect', data),
|
||||
};
|
||||
|
||||
/**
|
||||
* Android 兼容上传:先压缩图片再上传
|
||||
* 解决 Android 端 wx.uploadFile 上传照片失败的问题
|
||||
* @param {string} filePath - 原始图片路径
|
||||
* @returns {Promise<string>} 压缩后的图片路径
|
||||
*/
|
||||
function compressForUpload(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
if (!filePath) { resolve(filePath); return; }
|
||||
// Android 端 chooseImage 返回的路径可能是 http://tmp/xxx 或 wxfile://xxx 格式
|
||||
// 统一尝试压缩,压缩失败也不影响上传
|
||||
wx.compressImage({
|
||||
src: filePath,
|
||||
quality: 80,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
fail: () => {
|
||||
// 压缩失败,使用原路径
|
||||
resolve(filePath);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 兼容的 wx.uploadFile 封装
|
||||
* 先压缩图片,再上传,确保 Android 端能正常上传
|
||||
*/
|
||||
async function uploadFileCompat(options) {
|
||||
await waitForAuth();
|
||||
const { url, filePath, name, formData, timeout, header } = options;
|
||||
const token = wx.getStorageSync('token');
|
||||
const finalHeader = header || {};
|
||||
if (token && !finalHeader['Authorization']) {
|
||||
finalHeader['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
const finalFormData = toWire(formData || {});
|
||||
if (token && !finalFormData.token) {
|
||||
finalFormData.token = token;
|
||||
}
|
||||
// 从 URL 中提取 action 参数并添加到 formData(解决非开发者模式下 URL 参数被剥离的问题)
|
||||
var actionMatch = url.match(/\/api\/([^/?]+)|[?&]action=([^&]+)/);
|
||||
if (actionMatch) finalFormData.action = decodeURIComponent(actionMatch[1] || actionMatch[2]);
|
||||
|
||||
// 将 token 追加到 URL 参数中,解决 Android 端 wx.uploadFile 不传自定义 header 的问题
|
||||
var uploadUrl = url;
|
||||
if (token) {
|
||||
var sep = uploadUrl.indexOf('?') > -1 ? '&' : '?';
|
||||
uploadUrl += sep + 'token=' + encodeURIComponent(token);
|
||||
}
|
||||
|
||||
function doUpload(fp) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: fp,
|
||||
name: name || 'file',
|
||||
timeout: timeout || 120000,
|
||||
header: finalHeader,
|
||||
formData: finalFormData,
|
||||
success: (res) => {
|
||||
var data = parseUploadResponse(res.data);
|
||||
if (data) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject({ error: '解析响应失败', raw: res.data });
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject({ error: '网络异常', errMsg: err.errMsg });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 先压缩图片,确保 Android 临时文件路径可读
|
||||
compressForUpload(filePath).then((compressedPath) => {
|
||||
doUpload(compressedPath).then(resolve).catch(() => {
|
||||
// 压缩后上传失败,尝试原路径
|
||||
doUpload(filePath).then(resolve).catch(reject);
|
||||
});
|
||||
}).catch(() => {
|
||||
// 压缩失败时尝试直接上传
|
||||
doUpload(filePath).then(resolve).catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { api, getImageUrl, API_BASE, uploadFileCompat, compressForUpload };
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 页面滚动位置管理工具
|
||||
* 保存和恢复底部导航栏页面的滚动位置,切换 Tab 后返回时保持浏览位置
|
||||
*/
|
||||
|
||||
const SCROLL_POS_KEY = 'tabScrollPositions';
|
||||
|
||||
/**
|
||||
* 保存指定页面的滚动位置
|
||||
* @param {string} pageKey - 页面标识(index/diary/profile)
|
||||
* @param {number} scrollTop - scroll-view 的滚动位置
|
||||
*/
|
||||
function saveScrollPosition(pageKey, scrollTop) {
|
||||
try {
|
||||
var positions = wx.getStorageSync(SCROLL_POS_KEY) || {};
|
||||
positions[pageKey] = {
|
||||
scrollTop: scrollTop,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
wx.setStorageSync(SCROLL_POS_KEY, positions);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定页面保存的滚动位置
|
||||
* @param {string} pageKey - 页面标识
|
||||
* @returns {number} 保存的 scrollTop,没有则返回 0
|
||||
*/
|
||||
function getScrollPosition(pageKey) {
|
||||
try {
|
||||
var positions = wx.getStorageSync(SCROLL_POS_KEY) || {};
|
||||
var pos = positions[pageKey];
|
||||
return pos ? (pos.scrollTop || 0) : 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveScrollPosition,
|
||||
getScrollPosition
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 微信订阅消息工具
|
||||
* 统一管理订阅消息的请求和状态
|
||||
*
|
||||
* 订阅状态以服务端接口为准
|
||||
*/
|
||||
const { api } = require('./request');
|
||||
|
||||
// 统一使用「每日记录提醒」模板,所有操作都通过这个模板通知
|
||||
const SUBSCRIBE_TEMPLATES = {
|
||||
notify: 'tOaqM_Z6pNSih2pVpaUmP86MgwnDT5ZlwOqd0lgmXP8',
|
||||
};
|
||||
|
||||
// 防重复标记:同一会话中已请求过订阅,不再重复弹窗
|
||||
let _requestedInSession = false;
|
||||
|
||||
/**
|
||||
* 请求用户订阅指定模板
|
||||
* @param {string|string[]} templateTypes - 模板类型或模板类型数组
|
||||
* @returns {Promise<object>} 订阅结果 { templateType: true/false }
|
||||
*/
|
||||
function requestSubscribe(templateTypes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Array.isArray(templateTypes)) {
|
||||
templateTypes = [templateTypes];
|
||||
}
|
||||
|
||||
// 获取模板ID列表
|
||||
const tmplIds = templateTypes
|
||||
.map(type => SUBSCRIBE_TEMPLATES[type])
|
||||
.filter(id => id && id !== 'TEMPLATE_ID_');
|
||||
|
||||
if (tmplIds.length === 0) {
|
||||
console.warn('[subscribe] 模板ID未配置,请在微信小程序管理后台申请模板后更新');
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
|
||||
wx.requestSubscribeMessage({
|
||||
tmplIds: tmplIds,
|
||||
success: (res) => {
|
||||
// 处理每个模板的授权结果
|
||||
const results = {};
|
||||
templateTypes.forEach((type) => {
|
||||
const tmplId = SUBSCRIBE_TEMPLATES[type];
|
||||
const status = res[tmplId];
|
||||
results[type] = status === 'accept';
|
||||
});
|
||||
|
||||
// 将授权结果同步到后端
|
||||
syncSubscribeResults(results);
|
||||
|
||||
resolve(results);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[subscribe] 请求订阅失败:', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将订阅结果同步到后端
|
||||
*/
|
||||
function syncSubscribeResults(results) {
|
||||
Object.keys(results).forEach((type) => {
|
||||
const accepted = results[type];
|
||||
const tmplId = SUBSCRIBE_TEMPLATES[type];
|
||||
if (!tmplId || tmplId === 'TEMPLATE_ID_') return;
|
||||
|
||||
if (accepted) {
|
||||
// 用户授权了,记录到后端,并同步本地标记
|
||||
api.subscribeTemplate({
|
||||
template_type: type,
|
||||
template_id: tmplId
|
||||
}).then(() => {
|
||||
wx.setStorageSync('subscribed_notify', true);
|
||||
}).catch((e) => {
|
||||
console.warn('[subscribe] 同步订阅状态到后端失败:', e);
|
||||
});
|
||||
}
|
||||
// 用户拒绝时不做任何操作
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作后静默请求订阅(无应用层弹窗)
|
||||
*
|
||||
* 优化策略:
|
||||
* 1. 同一会话中只弹窗一次(_requestedInSession 标记)
|
||||
* 2. 用户明确关闭通知(subscribed_notify=false)时才跳过
|
||||
* 3. 如果用户没有伴侣,不弹窗
|
||||
*
|
||||
* @param {string} scene - 触发场景(diary/photo/checkin/login)
|
||||
*/
|
||||
function requestSubscribeAfterAction(scene) {
|
||||
// 同一会话中已请求过,跳过
|
||||
if (_requestedInSession) return;
|
||||
|
||||
// 用户明确关闭了通知,不打扰
|
||||
if (wx.getStorageSync('subscribed_notify') === false) return;
|
||||
|
||||
// 检查是否有伴侣,没有伴侣不需要订阅
|
||||
const coupleId = wx.getStorageSync('coupleId');
|
||||
if (!coupleId) return;
|
||||
|
||||
// 标记已请求,防止重复弹窗
|
||||
_requestedInSession = true;
|
||||
|
||||
// 延迟一点请求,避免干扰用户当前操作。
|
||||
// 注意:微信订阅消息是一次性授权,每次操作后都应申请续订,
|
||||
// 用户勾选「总是保持以上选择」后这里会静默通过,不会反复弹窗。
|
||||
setTimeout(() => {
|
||||
requestSubscribe('notify').catch(() => {});
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户当前的订阅状态
|
||||
* 从后端获取
|
||||
* @param {boolean} forceRefresh - 保留旧调用参数
|
||||
* @returns {Promise<object>} 订阅状态 { template_type: true/false }
|
||||
*/
|
||||
async function getSubscribeStatus(forceRefresh = false) {
|
||||
try {
|
||||
const res = await api.mySubscribeTemplates();
|
||||
const status = {};
|
||||
Object.keys(SUBSCRIBE_TEMPLATES).forEach(type => {
|
||||
status[type] = !!res.templates[type];
|
||||
});
|
||||
return status;
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requestSubscribe,
|
||||
requestSubscribeAfterAction,
|
||||
getSubscribeStatus
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 左滑返回手势
|
||||
* @param {Function} callback 触发返回时执行的回调
|
||||
* @returns {{ touchStartX: number, onTouchStart: Function, onTouchEnd: Function }}
|
||||
*/
|
||||
const createSwipeBack = (callback) => {
|
||||
let touchStartX = 0
|
||||
return {
|
||||
onTouchStart: function(e) {
|
||||
var t = e.touches && e.touches[0];
|
||||
if (t) touchStartX = t.clientX;
|
||||
},
|
||||
onTouchEnd: function(e) {
|
||||
var t = e.changedTouches && e.changedTouches[0];
|
||||
if (!t) return;
|
||||
var endX = t.clientX;
|
||||
var dist = endX - touchStartX;
|
||||
if (touchStartX < 80 && dist > 60) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSwipeBack
|
||||
}
|
||||
Reference in New Issue
Block a user