返回 DeepSeek-Reasonix
BrowserResponsiveControls.tsx
根目录 / desktop / frontend / src / components / BrowserResponsiveControls.tsx
1 import type { BrowserTabView } from "../lib/browserHost";
2 import { useBrowserPanelStore } from "../lib/browserPanelStore";
3 import { useI18n } from "../lib/i18n";
4 import { useState } from "react";
5 import { browserElementDrafts } from "../lib/browserElementDrafts";
6
7 export function BrowserResponsiveControls({ tab }: { tab: BrowserTabView }) {
8 const chinese = useI18n().locale !== "en";
9 const viewport = tab.viewport;
10 const [picking, setPicking] = useState(false);
11 const pick = async () => {
12 const { host, notify } = useBrowserPanelStore.getState();
13 const target = browserElementDrafts.getState().target;
14 if (!target || !host?.pickElement) { notify(chinese ? "当前任务暂不能接收元素附件" : "The current task cannot receive an element attachment yet"); return; }
15 if (tab.taskId !== "user" && (tab.taskId !== target.taskId || tab.sessionId && tab.sessionId !== target.sessionId)) { notify(chinese ? "请切回此页面所属的任务" : "Return to the task that owns this page"); return; }
16 setPicking(true);
17 try {
18 const selection = await host.pickElement(tab.id);
19 if (selection) browserElementDrafts.getState().add(target.taskId, target.sessionId, selection);
20 } catch (error) { notify(String(error)); }
21 finally { setPicking(false); }
22 };
23 const set = (next: NonNullable<BrowserTabView["viewport"]> | null) => {
24 const { host, notify } = useBrowserPanelStore.getState();
25 void host?.setViewport?.(tab.id, next).catch(error => notify(String(error)));
26 };
27 return <details className="browser-responsive">
28 <summary>{chinese ? "响应式" : "Responsive"}{viewport ? ` · ${viewport.width}×${viewport.height}` : ""}</summary>
29 <div className="browser-responsive__controls">
30 {tab.restorePreview && <button type="button" className="btn btn--small" onClick={() => { const { host, notify } = useBrowserPanelStore.getState(); void host?.restorePreview?.(tab.id).catch(error => notify(String(error))); }}>{chinese ? "重新授权并打开预览" : "Reauthorize and reopen preview"}</button>}
31 <button type="button" className="btn btn--small" disabled={picking} onClick={() => void pick()}>{picking ? (chinese ? "点击元素 · Esc 退出" : "Click an element · Esc to cancel") : (chinese ? "选择元素加入聊天" : "Select element for chat")}</button>
32 <select aria-label={chinese ? "视口预设" : "Viewport preset"} value={viewport ? `${viewport.width}x${viewport.height}` : "natural"} onChange={event => {
33 if (event.target.value === "natural") set(null);
34 else if (event.target.value !== "custom") { const [width, height] = event.target.value.split("x").map(Number); set({ width, height, scale: viewport?.scale ?? "fit" }); }
35 }}>
36 <option value="natural">{chinese ? "自然尺寸" : "Natural"}</option>
37 <option value="393x852">{chinese ? "手机" : "Phone"} 393×852</option>
38 <option value="768x1024">{chinese ? "平板" : "Tablet"} 768×1024</option>
39 <option value="1280x720">{chinese ? "桌面" : "Desktop"} 1280×720</option>
40 {viewport && !["393x852", "768x1024", "1280x720"].includes(`${viewport.width}x${viewport.height}`) && <option value={`${viewport.width}x${viewport.height}`}>{chinese ? "自定义" : "Custom"}</option>}
41 </select>
42 <form key={`${tab.id}:${viewport?.width}:${viewport?.height}`} onSubmit={event => {
43 event.preventDefault(); const data = new FormData(event.currentTarget);
44 set({ width: Number(data.get("width")), height: Number(data.get("height")), scale: viewport?.scale ?? "fit" });
45 }}>
46 <input name="width" aria-label={chinese ? "CSS 宽度" : "CSS width"} type="number" min="320" max="3840" defaultValue={viewport?.width ?? 1280} required /> ×
47 <input name="height" aria-label={chinese ? "CSS 高度" : "CSS height"} type="number" min="320" max="2160" defaultValue={viewport?.height ?? 720} required />
48 <button type="submit" className="btn btn--small">{chinese ? "应用" : "Apply"}</button>
49 </form>
50 {viewport && <>
51 <button type="button" className="btn btn--small" disabled={viewport.width > 2160} onClick={() => set({ ...viewport, width: viewport.height, height: viewport.width })}>{chinese ? "旋转" : "Rotate"}</button>
52 <select aria-label={chinese ? "显示比例" : "Display scale"} value={viewport.scale} onChange={event => set({ ...viewport, scale: event.target.value === "fit" ? "fit" : Number(event.target.value) })}>
53 <option value="fit">Fit</option>{[0.5, 0.75, 1, 1.25, 1.5, 2].map(scale => <option key={scale} value={scale}>{scale * 100}%</option>)}
54 </select>
55 </>}
56 </div>
57 </details>;
58 }
59
59 lines Plain Text