Files
sundynix-pets/pets-fe/utils/api.js
T
Blizzard 9b30bfaaff feat: 计划按自然月管理,加「应用到每天」和下月未排提醒
## 为什么不是「从计划开始日起 30 天」
上一版是那样做的,截图一看就不对:计划是月中建的,日历显示
「7月26日 — 8月25日」,既不像本月也不像下月,用户想问「这个月还剩什么」
根本对不上。改成自然月,日历就是一眼能认的月历。

能翻到下月 —— 否则「做下月计划」这件事本身做不了。

## 校验从「Day 上限」改成「日期」
PlanTask.Day 是相对计划开始日的,月份边界是相对今天的,两者会错开。
原来卡 Day<=30,计划建了几天之后下月末就换算成 Day=40 多,
用户在日历上明明看得到那格、点进去却存不了。

现在的规则:不早于计划开始日(下界放到开始日而不是本月 1 号——模板生成的
历史节点可能在上个月,那些要允许原地改),不晚于下月月底。

## 应用到每天
一天建一条,不做「一条 + repeat 字段读时展开」:勾选状态是按天的,
展开方案还得再开一张表记「哪天打过勾」,而直接建 N 条天然就有这个能力。

范围止于**这一天所在那个月**的月底,不跨月——「应用到每天」说的是这个月每天,
跨过去就变成用户没要求的事了。只有新增能选,已有的那条改成每天等于凭空
复制 N 份(用户想要的是「以后每天」,不是「把这条撒开」)。

## 首页那个提醒
GET /pets/:id/plan/month-status 返回本月件数/剩余、下月件数、need_next。

need_next 只在**月末最后 7 天**且下月为空时为真:月初就催「排下个月」太早,
用户会当成噪音。角标做在图标上而不是弹 toast——每次进首页弹一下很烦,
而这件事不紧急、看见就行。

副标题有优先级:「8 月计划还没排」压过「本月都做完了」,因为前者有时效。
四种状态实跑验过,包括让位关系。

## 顺手
pet-switch 加 show-add 属性。计划页只需要在既有宠物之间切,建档入口在首页
和「我的」就够了,到处摆一个「添加」会让这条 chip 变得很长。

本月/下月的切换用新的 .link-tabs 而不是挂在 .head-links 上:首页的
「管理 / 全部完成」也用 head-links,那是两个并列动作,不该有一个是灰的。

## 验证(预生产库实跑)
  月边界     本月末通过、下月末通过、下下月拒(报出具体日期)
  应用到每天  今天→本月末 2 条,没跨月
  month-status  下月清空 → need_next 翻 true;补一条 → 翻回 false
  四种副标题  下月未排 / 本月还有 N 件 / 本月都做完了 / 还没有安排,含优先级
  测试数据全部还原

上一条 cd 打错目录,导致「日期窗口」那次改动没写进文件、只有批量那半截进去了,
所以中间出现过一次 planWindow undefined。这次一并改对。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:09:06 +08:00

151 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { request, uploadFile, setToken, setRefreshToken } = require('./request.js');
// 登录成功后统一落盘 access + refresh 两个令牌
function saveTokens(data) {
setToken(data.token);
setRefreshToken(data.refresh_token);
return data;
}
// wx.login 取 code
function wxLogin() {
return new Promise((resolve, reject) => {
wx.login({
success: (r) => (r.code ? resolve(r.code) : reject(new Error('wx.login 未返回 code'))),
fail: reject,
});
});
}
// 登录:优先微信 code2session,失败回退开发态 Mock 登录(后端 dev_login=true
async function login() {
try {
const code = await wxLogin();
// noAuthRetry:登录接口失败时也会返回 40100,不能再触发「续期→重新登录」,否则会自己套自己
const data = await request({ url: '/api/auth/wechat', method: 'POST', data: { code }, noAuthRetry: true });
return saveTokens(data);
} catch (e) {
console.warn('[api] 微信登录失败,回退 Mock 登录:', e && e.message);
const data = await request({
url: '/api/auth/login',
method: 'POST',
data: { nickname: '开发用户' },
noAuthRetry: true,
});
return saveTokens(data);
}
}
const api = {
login,
uploadFile,
// 用户
getProfile: () => request({ url: '/api/user/profile' }),
updateProfile: (body) => request({ url: '/api/user/profile', method: 'PUT', data: body }),
// 用户
userSummary: () => request({ url: '/api/user/summary' }),
// 记录类型(后台可配)。分组好的,前端直接渲染
recordTypes: () => request({ url: '/api/record-types' }),
updateDecoration: (body) => request({ url: '/api/user/decoration', method: 'PUT', data: body }),
// 宠物
getPets: () => request({ url: '/api/pets' }),
homeSummary: (id) => request({ url: `/api/pets/${id}/home-summary` }),
getPet: (id) => request({ url: `/api/pets/${id}` }),
createPet: (body) => request({ url: '/api/pets', method: 'POST', data: body }),
updatePet: (id, body) => request({ url: `/api/pets/${id}`, method: 'PUT', data: body }),
deletePet: (id) => request({ url: `/api/pets/${id}`, method: 'DELETE' }),
onboarding: (body) => request({ url: '/api/onboarding', method: 'POST', data: body }),
// 记录
getRecords: (id, { type, page, pageSize } = {}) => {
const qs = [];
if (type) qs.push(`type=${type}`);
if (page) qs.push(`page=${page}`);
if (pageSize) qs.push(`page_size=${pageSize}`);
return request({ url: `/api/pets/${id}/records${qs.length ? '?' + qs.join('&') : ''}` });
},
createRecord: (id, body) => request({ url: `/api/pets/${id}/records`, method: 'POST', data: body }),
deleteRecord: (recordId) => request({ url: `/api/records/${recordId}`, method: 'DELETE' }),
petInsights: (id) => request({ url: `/api/pets/${id}/insights` }),
weightTrend: (id) => request({ url: `/api/pets/${id}/records/weight-trend` }),
// 任务
getTasks: (id, date) => request({ url: `/api/pets/${id}/tasks${date ? `?date=${date}` : ''}` }),
createTask: (id, body) => request({ url: `/api/pets/${id}/tasks`, method: 'POST', data: body }),
updateTask: (taskId, body) => request({ url: `/api/tasks/${taskId}`, method: 'PUT', data: body }),
deleteTask: (taskId) => request({ url: `/api/tasks/${taskId}`, method: 'DELETE' }),
toggleTask: (taskId) => request({ url: `/api/tasks/${taskId}/toggle`, method: 'POST' }),
completeAllTasks: (id) => request({ url: `/api/pets/${id}/tasks/complete-all`, method: 'POST' }),
// 计划
getPlan: (id) => request({ url: `/api/pets/${id}/plan` }),
planCalendar: (id, month) =>
request({ url: `/api/pets/${id}/plan/calendar${month ? `?month=${month}` : ''}` }),
dayPlan: (id, date) => request({ url: `/api/pets/${id}/day-plan?date=${date}` }),
// AI 相关:用户端入口已全部下掉,后端 10 个路由和配额配置都还在。
// 想开回来把这几行解注释、再把入口接上就行,不用重写。
// createAIPlan: (id, input) => request({ url: `/api/pets/${id}/ai-plan`, method: 'POST', data: { input } }),
// applyAIPlan: (planId) => request({ url: `/api/ai-plan/${planId}/apply`, method: 'POST' }),
// 用户自己管理计划节点
planMonthStatus: (petId) => request({ url: `/api/pets/${petId}/plan/month-status` }),
addPlanTask: (petId, body) => request({ url: `/api/pets/${petId}/plan-tasks`, method: 'POST', data: body }),
updatePlanTask: (taskId, body) => request({ url: `/api/plan-tasks/${taskId}`, method: 'PUT', data: body }),
deletePlanTask: (taskId) => request({ url: `/api/plan-tasks/${taskId}`, method: 'DELETE' }),
togglePlanTask: (taskId) => request({ url: `/api/plan-tasks/${taskId}/toggle`, method: 'POST' }),
// 提醒
getReminders: (id) => request({ url: `/api/pets/${id}/reminders` }),
createReminder: (id, body) => request({ url: `/api/pets/${id}/reminders`, method: 'POST', data: body }),
updateReminder: (remId, body) => request({ url: `/api/reminders/${remId}`, method: 'PUT', data: body }),
deleteReminder: (remId) => request({ url: `/api/reminders/${remId}`, method: 'DELETE' }),
// 报告
weeklyReport: (id) => request({ url: `/api/pets/${id}/report/weekly` }),
getBill: (id, period) => request({ url: `/api/pets/${id}/bill?period=${period || 'month'}` }),
healthSummary: (id) => request({ url: `/api/pets/${id}/health-summary` }),
getPoster: (id) => request({ url: `/api/pets/${id}/poster` }),
// 社区
listPosts: (tab, page) =>
request({ url: `/api/posts?tab=${encodeURIComponent(tab || '')}&page=${page || 1}&page_size=10` }),
createPost: (body) => request({ url: '/api/posts', method: 'POST', data: body }),
likePost: (id) => request({ url: `/api/posts/${id}/like`, method: 'POST' }),
userCard: (userId) => request({ url: `/api/users/${userId}/profile` }),
userPosts: (userId, page) => request({ url: `/api/users/${userId}/posts?page=${page || 1}&page_size=10` }),
relations: (userId, kind, page) =>
request({ url: `/api/users/${userId}/relations?kind=${kind}&page=${page || 1}&page_size=20` }),
followUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'POST' }),
unfollowUser: (userId) => request({ url: `/api/users/${userId}/follow`, method: 'DELETE' }),
deletePost: (id) => request({ url: `/api/posts/${id}`, method: 'DELETE' }),
deleteComment: (id) => request({ url: `/api/comments/${id}`, method: 'DELETE' }),
listComments: (id) => request({ url: `/api/posts/${id}/comments` }),
createComment: (id, content, images, parentId) =>
request({
url: `/api/posts/${id}/comments`,
method: 'POST',
data: { content, images: images || [], parent_id: parentId || '' },
}),
listReplies: (commentId) => request({ url: `/api/comments/${commentId}/replies` }),
// 文章
listArticles: () => request({ url: '/api/articles' }),
submitFeedback: (body) => request({ url: '/api/feedback', method: 'POST', data: body }),
getArticle: (id) => request({ url: `/api/articles/${id}` }),
// Pro
getPro: () => request({ url: '/api/pro' }),
activatePro: () => request({ url: '/api/pro/activate', method: 'POST' }),
// AI
// aiMessages: (session, limit) =>
// request({ url: `/api/ai/messages?session=${session || ''}&limit=${limit || 30}` }),
// aiChat: (body) => request({ url: '/api/ai/chat', method: 'POST', data: body }),
// assessSymptom: (id, body) =>
// request({ url: `/api/pets/${id}/ai/assess-symptom`, method: 'POST', data: body }),
};
module.exports = api;