Browse Source

feat(desktop-pet): JS<->Rust 桥接、设置窗口与命令实现

liaoxg 1 ngày trước cách đây
mục cha
commit
e4e72e8cf6

+ 84 - 8
cfc-desktop-pet/src-tauri/src/commands.rs

@@ -1,10 +1,27 @@
+use crate::app_state::AppState;
+use crate::poller;
+use crate::settings::SettingsStore;
+use crate::state::{PetState, PetStatePayload};
+use crate::token::TokenStore;
+use tauri::{AppHandle, Emitter, Manager, State};
+
 #[tauri::command]
-pub async fn on_interact(_app: tauri::AppHandle, _action: String, _data: Option<String>) -> Result<(), String> {
-    Ok(())
+pub async fn on_interact(app: AppHandle, action: String, _data: Option<String>) -> Result<(), String> {
+    match action.as_str() {
+        "click" => complete_water(&app).await,
+        "pet" => {
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "嘻嘻~", "durationMs": 2000}));
+            Ok(())
+        }
+        _ => Ok(()),
+    }
 }
 
 #[tauri::command]
-pub async fn drag_start(_app: tauri::AppHandle) -> Result<(), String> {
+pub async fn drag_start(app: AppHandle) -> Result<(), String> {
+    if let Some(win) = app.get_webview_window("main") {
+        win.start_dragging().map_err(|e| e.to_string())?;
+    }
     Ok(())
 }
 
@@ -14,15 +31,74 @@ pub async fn drag_end() -> Result<(), String> {
 }
 
 #[tauri::command]
-pub fn get_settings(_app: tauri::AppHandle) -> Result<serde_json::Value, String> {
-    Ok(serde_json::json!({}))
+pub fn get_settings(
+    _state: State<AppState>,
+    settings: State<SettingsStore>,
+) -> Result<serde_json::Value, String> {
+    let cfg = settings.load();
+    let has_token = TokenStore::new().load().map(|t| t.is_some()).unwrap_or(false);
+    Ok(serde_json::json!({
+        "baseUrl": cfg.base_url,
+        "hasToken": has_token
+    }))
 }
 
 #[tauri::command]
 pub fn save_settings(
-    _app: tauri::AppHandle,
-    _base_url: String,
-    _token: Option<String>,
+    app: AppHandle,
+    state: State<AppState>,
+    settings: State<SettingsStore>,
+    base_url: String,
+    token: Option<String>,
 ) -> Result<(), String> {
+    let mut cfg = settings.load();
+    cfg.base_url = base_url.trim().to_string();
+    settings.save(&cfg)?;
+
+    if let Some(t) = token {
+        if !t.trim().is_empty() {
+            TokenStore::new().save(t.trim())?;
+        }
+    }
+
+    // 更新运行时客户端并重启轮询(对齐现 ShowSettings 的 Stop/Start)
+    state.api.set_base_url(cfg.base_url.clone());
+    if let Some(t) = TokenStore::new().load()? {
+        state.api.update_token(t);
+    }
+    restart_polling(&app, &state);
+
+    if let Some(win) = app.get_webview_window("settings") {
+        let _ = win.hide();
+    }
     Ok(())
 }
+
+fn restart_polling(app: &AppHandle, state: &State<AppState>) {
+    if let Some(h) = state.poll_handle.lock().unwrap().take() {
+        h.abort();
+    }
+    let handle = poller::spawn(app.clone(), state.api.clone());
+    *state.poll_handle.lock().unwrap() = Some(handle);
+}
+
+async fn complete_water(app: &AppHandle) -> Result<(), String> {
+    let state = app.state::<AppState>();
+    let api = state.api.clone();
+    match api.complete_water().await {
+        Ok(_) => {
+            let payload = PetStatePayload {
+                state: PetState::Happy,
+                reason: Some("drink_complete".into()),
+                data: None,
+            };
+            let _ = app.emit("pet://state", payload);
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "喝水打卡成功!太棒了~", "durationMs": 4000}));
+            Ok(())
+        }
+        Err(_) => {
+            let _ = app.emit("pet://bubble", serde_json::json!({"text": "打卡失败,请稍后再试", "durationMs": 4000}));
+            Err("checkin failed".into())
+        }
+    }
+}

+ 34 - 0
cfc-desktop-pet/src/bridge.js

@@ -0,0 +1,34 @@
+(function () {
+  'use strict';
+
+  var invoke = window.__TAURI__.core.invoke;
+  var listen = window.__TAURI__.event.listen;
+
+  // ── Rust → JS ──────────────────────────────
+  listen('pet://state', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.notifyState(e.payload);
+  });
+  listen('pet://bubble', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.showBubble(e.payload.text, e.payload.durationMs);
+  });
+  listen('pet://config', function (e) {
+    if (window.cfcPetAPI) window.cfcPetAPI.loadConfig(e.payload);
+  });
+
+  // ── JS → Rust(替换 chrome.webview.hostObjects.sync.cfcHost)──
+  window.cfcHost = {
+    onInteract: function (action, dataJson) {
+      try {
+        invoke('on_interact', { action: action, data: dataJson });
+      } catch (err) {
+        console.log('[interaction]', action, dataJson);
+      }
+    },
+    onDragStart: function () {
+      try { invoke('drag_start'); } catch (e) { /* noop */ }
+    },
+    onDragEnd: function () {
+      try { invoke('drag_end'); } catch (e) { /* noop */ }
+    }
+  };
+})();

+ 1 - 0
cfc-desktop-pet/src/index.html

@@ -25,6 +25,7 @@
   </div>
 </div>
 <script src="pet-engine.js"></script>
+<script src="bridge.js"></script>
 <script src="interactions.js"></script>
 </body>
 </html>

+ 3 - 3
cfc-desktop-pet/src/interactions.js

@@ -37,15 +37,15 @@
 
   function notifyInteract(action, data) {
     try {
-      chrome.webview.hostObjects.sync.cfcHost.onInteract(action, JSON.stringify(data));
+      window.cfcHost.onInteract(action, JSON.stringify(data));
     } catch (err) { console.log('[interaction]', action, data); }
   }
 
   function notifyDragStart() {
-    try { chrome.webview.hostObjects.sync.cfcHost.onDragStart(); } catch (e) { /* noop */ }
+    try { window.cfcHost.onDragStart(); } catch (e) { /* noop */ }
   }
 
   function notifyDragEnd() {
-    try { chrome.webview.hostObjects.sync.cfcHost.onDragEnd(); } catch (e) { /* noop */ }
+    try { window.cfcHost.onDragEnd(); } catch (e) { /* noop */ }
   }
 })();

+ 51 - 0
cfc-desktop-pet/src/settings.html

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8" />
+<title>设置</title>
+<style>
+  body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; margin: 16px; }
+  label { display: block; margin-bottom: 4px; font-size: 13px; color: #555; }
+  input { width: 100%; box-sizing: border-box; padding: 6px 8px; margin-bottom: 12px;
+          border: 1px solid #ccc; border-radius: 6px; font-size: 13px; }
+  button { width: 100%; padding: 8px; border: none; border-radius: 6px;
+           background: #4A9BD7; color: #fff; font-size: 14px; cursor: pointer; }
+  #status { margin-top: 10px; font-size: 12px; color: #999; }
+</style>
+</head>
+<body>
+  <label for="base-url">后端地址</label>
+  <input id="base-url" placeholder="http://localhost:9082" />
+  <label for="token">令牌(留空则不修改)</label>
+  <input id="token" type="password" placeholder="JWT Token" />
+  <button id="save">保存</button>
+  <div id="status"></div>
+  <script>
+    (function () {
+      'use strict';
+      var invoke = window.__TAURI__.core.invoke;
+      var baseUrlEl = document.getElementById('base-url');
+      var tokenEl = document.getElementById('token');
+      var statusEl = document.getElementById('status');
+
+      invoke('get_settings').then(function (s) {
+        baseUrlEl.value = s.baseUrl || 'http://localhost:9082';
+        statusEl.textContent = s.hasToken ? '已保存令牌' : '未设置令牌';
+      }).catch(function () {});
+
+      document.getElementById('save').addEventListener('click', function () {
+        var baseUrl = baseUrlEl.value.trim();
+        var token = tokenEl.value.trim();
+        invoke('save_settings', { baseUrl: baseUrl, token: token ? token : null })
+          .then(function () {
+            statusEl.textContent = '已保存,轮询已重启';
+            setTimeout(function () { statusEl.textContent = ''; }, 2500);
+          })
+          .catch(function (e) {
+            statusEl.textContent = '保存失败:' + e;
+          });
+      });
+    })();
+  </script>
+</body>
+</html>