lv il y a 2 semaines
Parent
commit
35624f6689
5 fichiers modifiés avec 106 ajouts et 79 suppressions
  1. 1 1
      src-tauri/src/cdp.rs
  2. 29 11
      src-tauri/src/landing.rs
  3. 8 2
      src-tauri/src/lib.rs
  4. 63 63
      src/App.tsx
  5. 5 2
      src/types/ipc.ts

+ 1 - 1
src-tauri/src/cdp.rs

@@ -3,7 +3,7 @@
 //! 通过 webview2_com 的 CallDevToolsProtocolMethod 调任意 CDP 命令,
 //! 异步回调用 oneshot 桥成 async/await。当前供:
 //!   - capture.rs:截图前的预热脚本 + Page.captureScreenshot
-//!   - landing.rs:中间页轮询 div.c-tags 与 Visit Site 链接
+//!   - landing.rs:中间页轮询 div.c-tags 与 Site 链接
 
 #![cfg(target_os = "windows")]
 

+ 29 - 11
src-tauri/src/landing.rs

@@ -8,7 +8,7 @@
 //!       脚本,但我们要找的节点 parse 阶段就在 DOM 里了,能省好几秒)
 //!   3. 本模块每 [`POLL_INTERVAL_MS`] 毫秒用 CDP Runtime.evaluate 同时查询:
 //!      - 标签:div.c-tags > ul > li > strong > a 全部 text
-//!      - Visit Site 链接:.menu-float__content > strong > a 中 text 含 "Visit Site" 的 href
+//!      - Site 链接:.menu-float__content > strong > a 中 text 含 "Site" 的 href
 //!   4. 提取到 tags 后:emit `task-tags-extracted`(仅首次,前端落库)
 //!   5. 提取到 Visit Site 后:把 stage 切到 Final,webview.navigate(href)
 //!   6. 最终页 PageLoadEvent::Finished → lib.rs handle_page_loaded 把 stage
@@ -129,12 +129,12 @@ async fn poll_loop(app: AppHandle, task_id: String, intermediate_url: String) {
                         Ok(parsed) => match crate::find_content_webview(&app) {
                             Ok(wv) => {
                                 if let Err(e) = wv.navigate(parsed) {
-                                    eprintln!("[landing] 跳转 Visit Site 失败: {e}");
+                                    eprintln!("[landing] 跳转 Site 失败: {e}");
                                 }
                             }
                             Err(e) => eprintln!("[landing] 找不到子 webview: {e}"),
                         },
-                        Err(e) => eprintln!("[landing] Visit Site href 解析失败 ({href}): {e}"),
+                        Err(e) => eprintln!("[landing] Site href 解析失败 ({href}): {e}"),
                     }
                     return;
                 }
@@ -153,7 +153,7 @@ async fn poll_loop(app: AppHandle, task_id: String, intermediate_url: String) {
             PageTimeout {
                 task_id,
                 url: intermediate_url,
-                reason: format!("等待 {POLL_TIMEOUT_MS} ms 未找到 Visit Site 链接"),
+                reason: format!("等待 {POLL_TIMEOUT_MS} ms 未找到 Site 链接"),
             },
         );
     }
@@ -185,14 +185,11 @@ async fn query_page(app: &AppHandle) -> anyhow::Result<(Vec<String>, Option<Stri
     .filter(s => s.length > 0);
 
   let visitHref = null;
-  const linkEls = document.querySelectorAll('.menu-float__content > strong > a');
-  for (const a of linkEls) {
-    const text = (a.textContent || '').trim();
-    if (text.includes('Visit Site')) {
-      visitHref = a.href;
-      break;
+  const linkEl = document.querySelector('ul.toolbar-bts > li > a.toolbar-bts__item');
+  
+    if (linkEl) {
+      visitHref = linkEl.href;
     }
-  }
   return { tags, visitHref };
 })()
 "#;
@@ -235,3 +232,24 @@ async fn query_page(app: &AppHandle) -> anyhow::Result<(Vec<String>, Option<Stri
 // 非 Windows 没有 CDP 路径,本模块在 macOS 上整体不参与编译(见文件顶部
 // `#![cfg(target_os = "windows")]`)。macOS 的「最终页就绪」由 lib.rs
 // handle_page_loaded 在 Finished 事件里直接 promote_to_ready 完成。
+// =====================================================================
+// 非 Windows 降级实现
+// =====================================================================
+// 没有 CDP 可用,无法注入 JS 拿 DOM 节点。直接把 stage 推进到 Ready,
+// 这样按钮立即可用——代价是没有 tags、不会自动跳到 Site URL,
+// 需要用户手动在 webview 里点链接进入最终页。后续接入 WKWebView 时补齐。
+
+#[cfg(not(target_os = "windows"))]
+async fn poll_loop(app: AppHandle, task_id: String, _intermediate_url: String) {
+    eprintln!("[landing] 非 Windows 平台暂跳过 landing 轮询,直接标记 Ready");
+    if let Some(state) = app.try_state::<AppState>() {
+        if let Ok(mut g) = state.page_stage.lock() {
+            *g = PageStage::Ready;
+        }
+    }
+    // 直接 emit ready,让前端按钮可用;url 字段留空(无法判定最终 URL)
+    let _ = app.emit(
+        "task-page-ready",
+        serde_json::json!({ "taskId": task_id, "url": "" }),
+    );
+}

+ 8 - 2
src-tauri/src/lib.rs

@@ -29,7 +29,7 @@ const INITIAL_URL: &str = "about:blank";
 ///
 /// 状态转换:
 ///   navigate_webview(task.url)  ──► Initial(等待中间页加载 + 轮询)
-///   landing 找到 Visit Site     ──► Final  (等待最终页加载)
+///   landing 找到 Site     ──► Final  (等待最终页加载)
 ///   最终页 on_page_load          ──► Ready  (按钮可用,自动截图触发)
 ///   再次 navigate_webview        ──► Initial(重置)
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -71,6 +71,7 @@ impl AppState {
 /// 任务的产物文件查询结果
 #[derive(Serialize)]
 struct TaskAssets {
+    task_id: String,
     /// 截图文件预期路径(无论是否存在)
     screenshot_path: String,
     /// 截图文件是否存在
@@ -96,6 +97,7 @@ fn query_task_assets(app: AppHandle, task_id: String) -> Result<TaskAssets, Stri
     let r_dir = paths::recordings_dir(&app)?;
 
     Ok(TaskAssets {
+        task_id,
         screenshot_exists: screenshot.exists(),
         screenshot_path: screenshot.to_string_lossy().to_string(),
         recording_exists: recording.exists(),
@@ -131,7 +133,7 @@ fn navigate_webview(
 
     // 先更新 state(即便后续 navigate 失败,state 也会被下一次正常调用覆盖)。
     // 同时把 page_stage 重置为 Initial:这次 navigate 永远指向「中间页」,
-    // 后续 landing 模块决定是否再 navigate 到 Visit Site URL。
+    // 后续 landing 模块决定是否再 navigate 到 Site URL。
     if let Ok(mut guard) = state.current_task_id.lock() {
         *guard = Some(task_id);
     }
@@ -209,6 +211,10 @@ fn set_window_size(
 /// 广告 / 第三方脚本加载完成,而 `div.c-tags`、`.menu-float__content` 这些节点在
 /// HTML parse 阶段就已经进 DOM 了。Started 触发时新文档刚提交,立即跑 CDP 轮询
 /// 就能在几百毫秒内捕获到目标节点,无需等几秒的尾资源。
+/// 根据 page_stage 分流:
+///   - Initial:当前是中间页 → spawn landing 轮询;
+///   - Final:当前是 Site 最终页 → emit task-page-ready,1.5s 后自动截图;
+///   - Ready:已就绪还在加载(重新刷新等),不做额外动作。
 ///
 /// 平台说明:landing 轮询仅 Windows 实现(CDP 路径)。macOS 这里 no-op,靠
 /// handle_page_loaded 的 Finished 分支把 Initial 当 Final 处理。

+ 63 - 63
src/App.tsx

@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
 import { invoke } from "@tauri-apps/api/core";
 import { listen, type UnlistenFn } from "@tauri-apps/api/event";
 import { getCurrentWindow } from "@tauri-apps/api/window";
@@ -49,6 +49,7 @@ import {
 } from "./lib/recorder";
 import "./App.css";
 import {
+  BackwardOutlined,
   BankOutlined,
   CheckCircleOutlined,
   CloseOutlined,
@@ -62,7 +63,6 @@ import {
   PictureOutlined,
   PlayCircleFilled,
   PlusOutlined,
-  RedoOutlined,
   StopOutlined,
   VideoCameraOutlined,
 } from "@ant-design/icons";
@@ -93,6 +93,7 @@ const STATUS_DIALOG_KIND: Record<StatusKind, "info" | "warning" | "error"> = {
 /** 工具栏右侧的尺寸预设(统一横屏:宽 × 高) */
 const SIZE_PRESETS = [
   { label: "1280×720", w: 1280, h: 720 },
+  { label: "720x1280", w: 720, h: 1280 },
   { label: "1920×1080", w: 1920, h: 1080 },
   { label: "1024×768", w: 1024, h: 768 }
 ];
@@ -126,6 +127,7 @@ function App() {
   // 当前激活的任务 id(仅用于左栏视觉高亮)
   const [activeId, setActiveId] = useState<string | null>(null);
 
+
   // 待处理任务(status=0)的真实列表,来自 sqlite
   const [tasks, setTasks] = useState<Task[]>([]);
 
@@ -142,7 +144,7 @@ function App() {
   const [assets, setAssets] = useState<TaskAssets | null>(null);
 
   /**
-   * landing 流转门控:true 表示已加载到最终 (Visit Site) URL,可截图 / 录制。
+   * landing 流转门控:true 表示已加载到最终 (Site) URL,可截图 / 录制。
    * - handleSelectTask 触发 navigate 时强制 false
    * - 监听 EVT_TASK_PAGE_READY 后变 true
    * - 监听 EVT_TASK_PAGE_TIMEOUT 时保持 false,弹窗让用户「重试 / 取消」
@@ -169,10 +171,7 @@ function App() {
 
   // activeId 的最新值快照,供 listen 闭包中读取(避免在每个 effect 上加 activeId 依赖
   // 导致 listen 频繁重订)
-  const activeIdRef = useRef<string | null>(activeId);
-  useEffect(() => {
-    activeIdRef.current = activeId;
-  }, [activeId]);
+
 
   // 底部状态栏文案与左侧色块;showStatus 同时弹原生对话框 + 写状态栏
   const [statusColor, setStatusColor] = useState("#888");
@@ -185,10 +184,10 @@ function App() {
    * 对话框为非阻塞触发(fire-and-forget),失败仅打日志兜底
    */
   const showStatus = useCallback(
-    (kind: StatusKind, title: string, description?: string) => {
+    (kind: StatusKind, title: string, description?: string, dlg?: boolean) => {
       setStatusColor(STATUS_COLOR[kind]);
       setStatusText(title);
-      void message(description ? `${title}\n${description}` : title, {
+      dlg !== false && void message(description ? `${title}\n${description}` : title, {
         title: "提示",
         kind: STATUS_DIALOG_KIND[kind],
       }).catch((e) => console.error("显示对话框失败:", e));
@@ -207,6 +206,9 @@ function App() {
     try {
       const list = await listPendingTasks();
       setTasks(list);
+      if (!activeId && list.length > 0) {
+        handleSelectTask({ key: list[0].id });
+      }
       return list;
     } catch (e) {
       console.error("加载任务列表失败:", e);
@@ -247,6 +249,7 @@ function App() {
     if (!t) {
       return;
     }
+    showStatus("success", "任务开始", "任务开始", false);
     setActiveId(t.id);
     // 新一轮 landing 流转开始:先把按钮门控锁住,等 Rust 端发 task-page-ready 再放开
     setPageReady(false);
@@ -307,13 +310,11 @@ function App() {
   }, [showStatus, reloadTasks]);
 
   /**
-   * 监听 landing 个事件:
+   * 监听 landing 个事件:
    *   - task-tags-extracted:中间页抓到标签 → 写库 + reload
-   *   - task-site-url-found:landing 命中 Visit Site 链接的瞬间 → 立即写 site_url + reload
-   *     (此时还没 navigate,更不用说 Finished;即便后续 redirect/加载失败,site_url 也已落库)
-   *   - task-page-ready:跳到最终 URL 加载完成 → 解锁按钮 + 状态栏提示
+   *   - task-page-ready:跳到最终 URL 完成 → 解锁按钮 + 状态栏提示
    *   - task-page-timeout:未找到 Visit Site → ask() 弹「重试 / 取消」
-   * 全部按 activeIdRef 过滤,避免给已切走的旧任务弹无效提示(写库不过滤,所有命中任务都要落库)
+   * 全部按 activeIdRef 过滤,避免给已切走的旧任务弹无效提示。
    */
   useEffect(() => {
     let unlistenTags: UnlistenFn | null = null;
@@ -329,8 +330,8 @@ function App() {
           const { taskId, tags } = e.payload;
           try {
             await updateTaskTags(taskId, tags);
-            await reloadTasks();
-            showStatus("success", "已提取标签", tags || "(空)");
+            // await reloadTasks();
+            showStatus("success", "已提取标签", tags || "(空)", false);
           } catch (err) {
             console.error("写入 tags 失败:", err);
             showStatus("error", "写入标签失败", String(err));
@@ -355,7 +356,7 @@ function App() {
         // 只对当前激活任务生效,避免切走后还把按钮放开
         if (activeIdRef.current !== taskId) return;
         setPageReady(true);
-        showStatus("success", "页面就绪", "已加载到最终 URL,可截图 / 录制");
+        showStatus("success", "页面就绪", "已加载到最终 URL,可截图 / 录制", false);
       });
       const u4 = await listen<TaskPageTimeoutPayload>(
         EVT_TASK_PAGE_TIMEOUT,
@@ -364,7 +365,7 @@ function App() {
           if (activeIdRef.current !== taskId) return;
           // 弹原生 ask 对话框:确认 = 重试;取消 = 保持禁用
           const retry = await ask(`${reason}\n\n是否刷新重试?`, {
-            title: "未找到 Visit Site 链接",
+            title: "未找到 网站 链接",
             kind: "warning",
             okLabel: "重试",
             cancelLabel: "取消",
@@ -407,7 +408,7 @@ function App() {
     if (!activeId) return;
     try {
       await markTaskDone(activeId);
-      showStatus("success", "任务已完成", `任务 ${activeId} 已从列表移除`);
+      showStatus("success", "任务已完成", `任务 ${activeId} 已从列表移除`, false);
       setActiveId(null);
       const list = await reloadTasks();
       // 若已无任务,自动再拉起一次导入窗口(用户可继续粘贴)
@@ -438,27 +439,18 @@ function App() {
     let disposed = false;
 
     (async () => {
-      const u1 = await listen<ScreenshotFinishedPayload>(
-        EVT_SCREENSHOT_FINISHED,
-        async (e) => {
-          const { taskId, path, auto } = e.payload;
-          showStatus(
-            "success",
-            auto ? "自动截图完成" : "截图完成",
-            `任务 ${taskId} → ${path}`,
-          );
-          // 当前选中即此任务时刷新 assets,让预览图按钮立即变可点
-          if (activeIdRef.current === taskId) {
-            void refreshAssets(taskId);
-          }
-          // 把截图 basename 落库;失败仅打日志,不影响用户后续操作
-          try {
-            await updateTaskPic(taskId, basename(path));
-          } catch (err) {
-            console.error("写入 pic 失败:", err);
-          }
-        },
-      );
+      const u1 = await listen<ScreenshotFinishedPayload>(EVT_SCREENSHOT_FINISHED, (e) => {
+        const { taskId, path, auto } = e.payload;
+        showStatus(
+          "success",
+          auto ? "自动截图完成" : "截图完成",
+          `任务 ${taskId} → ${path}`,
+        );
+        // 当前选中即此任务时刷新 assets,让预览图按钮立即变可点
+        if (activeIdRef.current === taskId) {
+          void refreshAssets(taskId);
+        }
+      });
       const u2 = await listen<ScreenshotFailedPayload>(EVT_SCREENSHOT_FAILED, (e) => {
         const { taskId, auto, error } = e.payload;
         showStatus(
@@ -497,22 +489,13 @@ function App() {
     let disposed = false;
 
     (async () => {
-      const u1 = await listen<RecordingFinishedPayload>(
-        EVT_RECORDING_FINISHED,
-        async (e) => {
-          const { taskId, path } = e.payload;
-          showStatus("success", "录制完成", `任务 ${taskId} → ${path}`);
-          if (activeIdRef.current === taskId) {
-            void refreshAssets(taskId);
-          }
-          // 把录制 mp4 basename 落库;失败仅打日志
-          try {
-            await updateTaskVideo(taskId, basename(path));
-          } catch (err) {
-            console.error("写入 video 失败:", err);
-          }
-        },
-      );
+      const u1 = await listen<RecordingFinishedPayload>(EVT_RECORDING_FINISHED, (e) => {
+        const { taskId, path } = e.payload;
+        showStatus("success", "录制完成", `任务 ${taskId} → ${path}`);
+        if (activeIdRef.current === taskId) {
+          void refreshAssets(taskId);
+        }
+      });
       const u2 = await listen<RecordingFailedPayload>(EVT_RECORDING_FAILED, (e) => {
         const { taskId, error } = e.payload;
         showStatus("error", "录制失败", `任务 ${taskId}:${error}`);
@@ -747,26 +730,40 @@ function App() {
         data-tauri-drag-region
       >
         <div className="flex h-full flex-row items-center" data-tauri-drag-region>
-          <Space size={8}>
+          <Space size={4}>
             <Button
               className="app-close"
               icon={<CloseOutlined />}
-              size="large"
+              // size="large"
               type="text"
               data-tauri-drag-region="no-drag"
               onClick={handleAppClose}
             />
-            <Button icon={<BankOutlined />} disabled size="large" type="text" />
             <Button
               className="app-minimize"
               icon={<MinusOutlined />}
-              size="large"
+              // size="large"
               type="text"
               data-tauri-drag-region="no-drag"
               onClick={handleAppMinimize}
             />
           </Space>
-          <div className="w-32" />
+          <div className="w-8" />
+          <Button
+            icon={<LeftOutlined />}
+            // size="large"
+            type="text"
+            data-tauri-drag-region="no-drag"
+            onClick={handleBack}
+          />
+          <Button
+            icon={<RedoOutlined />}
+            // size="large"
+            type="text"
+            data-tauri-drag-region="no-drag"
+            onClick={handleRefresh}
+          />
+
           <Tooltip
             title={
               !activeId
@@ -898,7 +895,6 @@ function App() {
           ) : (
             <>
               <InputNumber
-
                 min={200}
                 max={3840}
                 value={contentSize.w}
@@ -958,8 +954,12 @@ function App() {
         <Layout className="flex flex-row">
           <Content className="w-full m-0 p-1 flex-1" />
           <div className="w-full h-6 flex items-center px-2 text-gray-3">
+
+            <div className="flex-1">任务:{activeId} {isWorking && <LoadingOutlined />}</div>
+            <div className="flex-1 overflow-hidden">{assets?.site_url}</div>
+            <div className="flex-1 overflow-hidden">{assets?.tags}</div>
+            <div className="flex-1 text-right">{statusText}</div>
             <div className="bg-gray-6 rounded-full w-3 h-3 m-2" style={{ backgroundColor: statusColor }} />
-            <div className="flex-1">{statusText}</div>
           </div>
         </Layout>
       </Layout>

+ 5 - 2
src/types/ipc.ts

@@ -44,7 +44,7 @@ export const EVT_TASK_TAGS_EXTRACTED = "task-tags-extracted";
 export const EVT_TASK_SITE_URL_FOUND = "task-site-url-found";
 /** Visit Site 跳转后,最终 url 加载完成,前端解锁按钮 */
 export const EVT_TASK_PAGE_READY = "task-page-ready";
-/** landing 轮询超时(未找到 Visit Site 链接),前端弹「重试 / 取消」 */
+/** landing 轮询超时(未找到 Site 链接),前端弹「重试 / 取消」 */
 export const EVT_TASK_PAGE_TIMEOUT = "task-page-timeout";
 
 /** 批量导入完成事件 payload */
@@ -91,7 +91,7 @@ export interface TaskSiteUrlFoundPayload {
 /** landing 最终页加载完成事件 payload */
 export interface TaskPageReadyPayload {
   taskId: string;
-  /** 最终 URL(跳转后的 Visit Site 链接) */
+  /** 最终 URL(跳转后的 Site 链接) */
   url: string;
 }
 
@@ -106,6 +106,9 @@ export interface TaskPageTimeoutPayload {
 
 /** query_task_assets 命令的返回结构(字段名与 Rust serde 一致) */
 export interface TaskAssets {
+  task_id: string;
+  site_url?: string;
+  tags?: string;
   screenshot_path: string;
   screenshot_exists: boolean;
   recording_path: string;