|
|
@@ -1,4 +1,5 @@
|
|
|
-use crate::api::{models::WaterRecord, ApiClient};
|
|
|
+use crate::api::{models::{CfcTask, WaterRecord}, ApiClient};
|
|
|
+use crate::app_state::AppState;
|
|
|
use crate::config::PetConfig;
|
|
|
use crate::state::{map_state, PetState, PetStatePayload};
|
|
|
use chrono::Timelike;
|
|
|
@@ -59,6 +60,31 @@ async fn poll_tasks(app: &AppHandle, api: &ApiClient) {
|
|
|
let state = map_state(Some(&tasks), None, false, false);
|
|
|
let payload = PetStatePayload { state, reason: Some("tasks".into()), data: None };
|
|
|
let _ = app.emit("pet://state", payload);
|
|
|
+
|
|
|
+ // 选出最紧急的待办任务作为「点提醒即完成」的目标
|
|
|
+ let urgent = pick_urgent_task(&tasks);
|
|
|
+ let new_id = urgent.as_ref().map(|t| t.id);
|
|
|
+ let changed = match app.try_state::<AppState>() {
|
|
|
+ Some(s) => {
|
|
|
+ let mut cur = s.pending_task.lock().unwrap();
|
|
|
+ let changed = *cur != new_id;
|
|
|
+ *cur = new_id;
|
|
|
+ changed
|
|
|
+ }
|
|
|
+ None => false,
|
|
|
+ };
|
|
|
+ if changed {
|
|
|
+ if let Some(t) = &urgent {
|
|
|
+ let title = t.title.clone().unwrap_or_else(|| "待办任务".into());
|
|
|
+ let _ = app.emit(
|
|
|
+ "pet://bubble",
|
|
|
+ serde_json::json!({
|
|
|
+ "text": format!("「{title}」快到点了,点我完成!"),
|
|
|
+ "durationMs": 15000
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
Err(_) => {
|
|
|
let _ = app.emit(
|
|
|
@@ -69,6 +95,23 @@ async fn poll_tasks(app: &AppHandle, api: &ApiClient) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/// 从今日任务中选出最紧急、可一键完成的待办任务(未完成且已到期/今日到期)
|
|
|
+fn pick_urgent_task(tasks: &[CfcTask]) -> Option<CfcTask> {
|
|
|
+ let today = chrono::Local::now().date_naive();
|
|
|
+ tasks
|
|
|
+ .iter()
|
|
|
+ .filter(|t| matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS")))
|
|
|
+ .filter(|t| {
|
|
|
+ t.deadline
|
|
|
+ .as_ref()
|
|
|
+ .and_then(|d| d.to_local_date())
|
|
|
+ .map(|due| due <= today)
|
|
|
+ .unwrap_or(false)
|
|
|
+ })
|
|
|
+ .min_by_key(|t| t.deadline.as_ref().and_then(|d| d.to_local_date()))
|
|
|
+ .cloned()
|
|
|
+}
|
|
|
+
|
|
|
async fn poll_health(app: &AppHandle, api: &ApiClient) {
|
|
|
match (api.energy_overview().await, api.water_list().await) {
|
|
|
(Ok(energy), Ok(water)) => {
|
|
|
@@ -158,4 +201,36 @@ mod tests {
|
|
|
let today_dt = format!("{today}T09:30:00");
|
|
|
assert!(water_done_today(&[water(None, Some(&today_dt))]));
|
|
|
}
|
|
|
+
|
|
|
+ fn task(id: i64, status: &str, due: Option<&str>) -> CfcTask {
|
|
|
+ CfcTask {
|
|
|
+ id,
|
|
|
+ title: None,
|
|
|
+ status: Some(status.to_string()),
|
|
|
+ deadline: due.map(|s| FlexDate::Iso(s.to_string())),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn pick_urgent_prefers_earliest_due_pending() {
|
|
|
+ let today = chrono::Local::now().format("%Y-%m-%d").to_string();
|
|
|
+ let tasks = vec![
|
|
|
+ task(1, "COMPLETED", Some("2000-01-01")),
|
|
|
+ task(2, "PENDING", Some(&today)),
|
|
|
+ task(3, "PENDING", Some("2000-01-01")),
|
|
|
+ ];
|
|
|
+ assert_eq!(pick_urgent_task(&tasks).unwrap().id, 3);
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn pick_urgent_none_when_not_yet_due() {
|
|
|
+ let tasks = vec![task(1, "PENDING", Some("2099-12-31"))];
|
|
|
+ assert!(pick_urgent_task(&tasks).is_none());
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn pick_urgent_none_when_no_deadline() {
|
|
|
+ let tasks = vec![task(1, "PENDING", None)];
|
|
|
+ assert!(pick_urgent_task(&tasks).is_none());
|
|
|
+ }
|
|
|
}
|