lv il y a 2 semaines
Parent
commit
990ec1c332
6 fichiers modifiés avec 78 ajouts et 54 suppressions
  1. 1 1
      src-tauri/src/cdp.rs
  2. 12 15
      src-tauri/src/landing.rs
  3. 7 5
      src-tauri/src/lib.rs
  4. 51 29
      src/App.tsx
  5. 1 1
      src/lib/tasks.ts
  6. 6 3
      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")]
 

+ 12 - 15
src-tauri/src/landing.rs

@@ -6,9 +6,9 @@
 //!      → lib.rs 把控制权交给 [`spawn_polling`]
 //!   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)
+//!   5. 提取到 Site 后:把 stage 切到 Final,webview.navigate(href)
 //!   6. webview 加载完最终页 → on_page_load 再次触发,stage=Final
 //!      → lib.rs 把 stage 推进到 Ready,emit `task-page-ready`,再触发 auto capture
 //!
@@ -29,7 +29,7 @@ const EVT_TASK_PAGE_TIMEOUT: &str = "task-page-timeout";
 /// 轮询间隔;起步 300ms 已经足够覆盖大多数页面渲染节奏
 #[cfg(target_os = "windows")]
 const POLL_INTERVAL_MS: u64 = 300;
-/// 总超时;超过该时长仍未找到 Visit Site,emit timeout 让前端弹「重试/取消」
+/// 总超时;超过该时长仍未找到 Site,emit timeout 让前端弹「重试/取消」
 #[cfg(target_os = "windows")]
 const POLL_TIMEOUT_MS: u64 = 15_000;
 
@@ -107,12 +107,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;
                 }
@@ -131,7 +131,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 链接"),
             },
         );
     }
@@ -152,7 +152,7 @@ fn still_active(app: &AppHandle, task_id: &str) -> bool {
     id_ok && stage_ok
 }
 
-/// 单次 CDP Runtime.evaluate:合并 tags 与 Visit Site 查询,减少往返次数。
+/// 单次 CDP Runtime.evaluate:合并 tags 与 Site 查询,减少往返次数。
 #[cfg(target_os = "windows")]
 async fn query_page(app: &AppHandle) -> anyhow::Result<(Vec<String>, Option<String>)> {
     use serde_json::json;
@@ -165,14 +165,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 };
 })()
 "#;
@@ -216,7 +213,7 @@ async fn query_page(app: &AppHandle) -> anyhow::Result<(Vec<String>, Option<Stri
 // 非 Windows 降级实现
 // =====================================================================
 // 没有 CDP 可用,无法注入 JS 拿 DOM 节点。直接把 stage 推进到 Ready,
-// 这样按钮立即可用——代价是没有 tags、不会自动跳到 Visit Site URL,
+// 这样按钮立即可用——代价是没有 tags、不会自动跳到 Site URL,
 // 需要用户手动在 webview 里点链接进入最终页。后续接入 WKWebView 时补齐。
 
 #[cfg(not(target_os = "windows"))]

+ 7 - 5
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);
     }
@@ -183,7 +185,7 @@ fn set_window_size(
 ///
 /// 根据 page_stage 分流:
 ///   - Initial:当前是中间页 → spawn landing 轮询;
-///   - Final:当前是 Visit Site 最终页 → emit task-page-ready,1.5s 后自动截图;
+///   - Final:当前是 Site 最终页 → emit task-page-ready,1.5s 后自动截图;
 ///   - Ready:已就绪还在加载(重新刷新等),不做额外动作。
 ///
 /// about:blank / 无任务上下文(current_task_id=None)的加载直接忽略。
@@ -208,7 +210,7 @@ async fn handle_page_loaded(app: AppHandle, loaded_url: String) {
 
     match stage {
         PageStage::Initial => {
-            // landing 模块自己负责 emit tags / 触发 Visit Site 跳转 / 超时
+            // landing 模块自己负责 emit tags / 触发 Site 跳转 / 超时
             landing::spawn_polling(app, task_id, loaded_url);
         }
         PageStage::Final => {
@@ -282,7 +284,7 @@ pub fn run() {
             // 2) 在已经放大的窗口上挂 child webview,定位到工具栏下方
             //
             //    on_page_load 钩子分流到 [`handle_page_loaded`]:
-            //    - Initial 阶段:spawn landing 轮询(找 tags + Visit Site 链接)
+            //    - Initial 阶段:spawn landing 轮询(找 tags + Site 链接)
             //    - Final   阶段:标记 Ready + 1.5s 后自动截图
             //    - Ready   阶段:忽略(用户手动刷新 / 同 URL 重新加载)
             let initial_url: url::Url = INITIAL_URL.parse()?;

+ 51 - 29
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";
@@ -44,11 +44,13 @@ import {
 } from "./lib/recorder";
 import "./App.css";
 import {
+  BackwardOutlined,
   BankOutlined,
   CheckCircleOutlined,
   CloseOutlined,
   FileImageOutlined,
   FolderOpenOutlined,
+  LeftOutlined,
   LinkOutlined,
   LoadingOutlined,
   MinusOutlined,
@@ -56,6 +58,8 @@ import {
   PictureOutlined,
   PlayCircleFilled,
   PlusOutlined,
+  RedoOutlined,
+  StepBackwardOutlined,
   StopOutlined,
   VideoCameraOutlined,
 } from "@ant-design/icons";
@@ -86,6 +90,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 }
 ];
@@ -113,6 +118,7 @@ function App() {
   // 当前激活的任务 id(仅用于左栏视觉高亮)
   const [activeId, setActiveId] = useState<string | null>(null);
 
+
   // 待处理任务(status=0)的真实列表,来自 sqlite
   const [tasks, setTasks] = useState<Task[]>([]);
 
@@ -129,7 +135,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,弹窗让用户「重试 / 取消」
@@ -138,10 +144,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");
@@ -154,10 +157,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));
@@ -176,6 +179,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);
@@ -216,6 +222,7 @@ function App() {
     if (!t) {
       return;
     }
+    showStatus("success", "任务开始", "任务开始", false);
     setActiveId(t.id);
     // 新一轮 landing 流转开始:先把按钮门控锁住,等 Rust 端发 task-page-ready 再放开
     setPageReady(false);
@@ -275,13 +282,11 @@ function App() {
     };
   }, [showStatus, reloadTasks]);
 
-  /**
-   * 监听 landing 三个事件:
-   *   - task-tags-extracted:中间页抓到标签 → 写库 + reload
-   *   - task-page-ready:跳到最终 URL 完成 → 解锁按钮 + 状态栏提示
-   *   - task-page-timeout:未找到 Visit Site → ask() 弹「重试 / 取消」
-   * 全部按 activeIdRef 过滤,避免给已切走的旧任务弹无效提示。
-   */
+  const activeIdRef = useRef<string | null>(activeId);
+
+  activeIdRef.current = activeId;
+
+
   useEffect(() => {
     let unlistenTags: UnlistenFn | null = null;
     let unlistenReady: UnlistenFn | null = null;
@@ -295,8 +300,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));
@@ -308,7 +313,7 @@ function App() {
         // 只对当前激活任务生效,避免切走后还把按钮放开
         if (activeIdRef.current !== taskId) return;
         setPageReady(true);
-        showStatus("success", "页面就绪", "已加载到最终 URL,可截图 / 录制");
+        showStatus("success", "页面就绪", "已加载到最终 URL,可截图 / 录制", false);
       });
       const u3 = await listen<TaskPageTimeoutPayload>(
         EVT_TASK_PAGE_TIMEOUT,
@@ -317,7 +322,7 @@ function App() {
           if (activeIdRef.current !== taskId) return;
           // 弹原生 ask 对话框:确认 = 重试;取消 = 保持禁用
           const retry = await ask(`${reason}\n\n是否刷新重试?`, {
-            title: "未找到 Visit Site 链接",
+            title: "未找到 网站 链接",
             kind: "warning",
             okLabel: "重试",
             cancelLabel: "取消",
@@ -357,7 +362,7 @@ function App() {
     if (!activeId) return;
     try {
       await markTaskDone(activeId);
-      showStatus("success", "任务已完成", `任务 ${activeId} 已从列表移除`);
+      showStatus("success", "任务已完成", `任务 ${activeId} 已从列表移除`, false);
       setActiveId(null);
       const list = await reloadTasks();
       // 若已无任务,自动再拉起一次导入窗口(用户可继续粘贴)
@@ -393,7 +398,7 @@ function App() {
         showStatus(
           "success",
           auto ? "自动截图完成" : "截图完成",
-          `任务 ${taskId} → ${path}`,
+          `任务 ${taskId} → ${path}`, false
         );
         // 当前选中即此任务时刷新 assets,让预览图按钮立即变可点
         if (activeIdRef.current === taskId) {
@@ -440,7 +445,7 @@ function App() {
     (async () => {
       const u1 = await listen<RecordingFinishedPayload>(EVT_RECORDING_FINISHED, (e) => {
         const { taskId, path } = e.payload;
-        showStatus("success", "录制完成", `任务 ${taskId} → ${path}`);
+        showStatus("success", "录制完成", `任务 ${taskId} → ${path}`, false);
         if (activeIdRef.current === taskId) {
           void refreshAssets(taskId);
         }
@@ -679,26 +684,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
@@ -830,7 +849,6 @@ function App() {
           ) : (
             <>
               <InputNumber
-
                 min={200}
                 max={3840}
                 value={contentSize.w}
@@ -890,8 +908,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>

+ 1 - 1
src/lib/tasks.ts

@@ -48,7 +48,7 @@ export function genTaskId(): string {
 export async function listPendingTasks(): Promise<Task[]> {
   const db = await getDb();
   return await db.select<Task[]>(
-    'SELECT id, url, status, "desc", tags FROM tasks WHERE status = 0 ORDER BY rowid ASC',
+    'SELECT id, url, status, "desc", tags FROM tasks WHERE status = 0 ORDER BY rowid ASC LIMIT 20',
   );
 }
 

+ 6 - 3
src/types/ipc.ts

@@ -40,9 +40,9 @@ export const EVT_RECORD_SHORTCUT = "record-shortcut";
 export const EVT_TASKS_IMPORTED = "tasks-imported";
 /** landing 中间页提取到 tags 后由 Rust 发出,前端写库 */
 export const EVT_TASK_TAGS_EXTRACTED = "task-tags-extracted";
-/** Visit Site 跳转后,最终 url 加载完成,前端解锁按钮 */
+/** 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 */
@@ -82,7 +82,7 @@ export interface TaskTagsExtractedPayload {
 /** landing 最终页加载完成事件 payload */
 export interface TaskPageReadyPayload {
   taskId: string;
-  /** 最终 URL(跳转后的 Visit Site 链接) */
+  /** 最终 URL(跳转后的 Site 链接) */
   url: string;
 }
 
@@ -97,6 +97,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;