Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba252c8576 | |||
| 1115f4532c |
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="%BASE_URL%favicon.png" />
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="%BASE_URL%favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import logoUrl from "./assets/logo-universal.png";
|
||||
import ShowcaseVisual from "./ShowcaseVisual";
|
||||
|
||||
const slides = [
|
||||
{
|
||||
@@ -136,6 +137,9 @@ export default function App() {
|
||||
onPointerCancel={() => {
|
||||
dragStartX.current = null;
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
dragStartX.current = null;
|
||||
}}
|
||||
>
|
||||
<div className="window-chrome" aria-hidden="true">
|
||||
<span />
|
||||
@@ -144,20 +148,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
<div className="preview-stage">
|
||||
<div className="capture-frame">
|
||||
<span className="corner top-left" />
|
||||
<span className="corner top-right" />
|
||||
<span className="corner bottom-left" />
|
||||
<span className="corner bottom-right" />
|
||||
<div className="preview-card floating-card card-one">
|
||||
<span>Upload</span>
|
||||
<strong>snapgo/2026/capture.png</strong>
|
||||
</div>
|
||||
<div className="preview-card floating-card card-two">
|
||||
<span>Copied</span>
|
||||
<strong>https://cdn.example.com/...</strong>
|
||||
</div>
|
||||
</div>
|
||||
<ShowcaseVisual key={active.eyebrow} activeIndex={activeSlide} />
|
||||
</div>
|
||||
|
||||
<div className="slide-panel" key={active.title}>
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
import { useState, type PointerEvent } from "react";
|
||||
|
||||
type ShowcaseVisualProps = {
|
||||
activeIndex: number;
|
||||
};
|
||||
|
||||
type CaptureTool = "rect" | "arrow" | "pen" | "mosaic";
|
||||
type OcrLanguage = "zh" | "en" | "auto";
|
||||
type UploadDestination = {
|
||||
id: "obs" | "ssh" | "ftp";
|
||||
label: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
const destinations: UploadDestination[] = [
|
||||
{ id: "obs", label: "OBS", detail: "Bucket" },
|
||||
{ id: "ssh", label: "SSH", detail: "Server" },
|
||||
{ id: "ftp", label: "FTP", detail: "Remote" },
|
||||
];
|
||||
|
||||
const uploadRoutes: Record<UploadDestination["id"], string> = {
|
||||
obs: "M 50 30 C 59 30 61 17 71 17",
|
||||
ssh: "M 50 30 C 58 30 63 30 71 30",
|
||||
ftp: "M 50 30 C 59 30 61 43 71 43",
|
||||
};
|
||||
|
||||
const ocrModes: Array<{
|
||||
id: OcrLanguage;
|
||||
label: string;
|
||||
name: string;
|
||||
lines: string[];
|
||||
characters: number;
|
||||
confidence: string;
|
||||
}> = [
|
||||
{
|
||||
id: "zh",
|
||||
label: "中",
|
||||
name: "中文",
|
||||
lines: ["让每一次截图直接进入下一步。"],
|
||||
characters: 15,
|
||||
confidence: "99.5%",
|
||||
},
|
||||
{
|
||||
id: "en",
|
||||
label: "EN",
|
||||
name: "English",
|
||||
lines: ["Capture, understand, share."],
|
||||
characters: 27,
|
||||
confidence: "99.1%",
|
||||
},
|
||||
{
|
||||
id: "auto",
|
||||
label: "AUTO",
|
||||
name: "中英混排",
|
||||
lines: ["Capture, understand, share.", "让每一次截图直接进入下一步。"],
|
||||
characters: 42,
|
||||
confidence: "99.2%",
|
||||
},
|
||||
];
|
||||
|
||||
const aiModes = [
|
||||
{
|
||||
id: "vision",
|
||||
label: "识图",
|
||||
result: "检测到系统权限弹窗,截图权限尚未开启。",
|
||||
score: "96%",
|
||||
},
|
||||
{
|
||||
id: "summary",
|
||||
label: "总结",
|
||||
result: "当前页面包含 3 个设置项,建议先开启屏幕录制权限。",
|
||||
score: "93%",
|
||||
},
|
||||
{
|
||||
id: "question",
|
||||
label: "问答",
|
||||
result: "前往系统设置中的隐私与安全性即可完成授权。",
|
||||
score: "98%",
|
||||
},
|
||||
];
|
||||
|
||||
const captureTools: Array<{ id: CaptureTool; label: string }> = [
|
||||
{ id: "rect", label: "矩形" },
|
||||
{ id: "arrow", label: "箭头" },
|
||||
{ id: "pen", label: "画笔" },
|
||||
{ id: "mosaic", label: "马赛克" },
|
||||
];
|
||||
|
||||
function stopSceneDrag(event: PointerEvent<HTMLElement>) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function UploadScene() {
|
||||
const [selected, setSelected] = useState<UploadDestination>(destinations[0]);
|
||||
const routePath = uploadRoutes[selected.id];
|
||||
|
||||
return (
|
||||
<div className={`visual-scene upload-scene target-${selected.id}`}>
|
||||
<div className="upload-file-card">
|
||||
<div className="upload-file-preview" aria-hidden="true">
|
||||
<div className="screenshot-window-bar">
|
||||
<span><i /><i /><i /></span>
|
||||
<b className="screenshot-address-bar" />
|
||||
</div>
|
||||
<div className="screenshot-window-body">
|
||||
<div className="screenshot-sidebar"><i /><i /><i /><i /></div>
|
||||
<div className="screenshot-main">
|
||||
<span className="screenshot-heading" />
|
||||
<span className="screenshot-row row-long" />
|
||||
<span className="screenshot-row row-medium" />
|
||||
<div className="screenshot-card-grid">
|
||||
<i /><i /><i />
|
||||
</div>
|
||||
<div className="screenshot-copy-lines">
|
||||
<i /><i /><i />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="upload-capture-frame">
|
||||
<i className="upload-capture-corner corner-nw" />
|
||||
<i className="upload-capture-corner corner-ne" />
|
||||
<i className="upload-capture-corner corner-sw" />
|
||||
<i className="upload-capture-corner corner-se" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="upload-file-copy">
|
||||
<span>SCREENSHOT-1428.PNG</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
className="upload-route"
|
||||
viewBox="0 0 100 60"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
key={`route-${selected.id}`}
|
||||
className="upload-route-line"
|
||||
d={routePath}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
<path
|
||||
key={`shimmer-${selected.id}`}
|
||||
className="upload-route-shimmer"
|
||||
d={routePath}
|
||||
pathLength="100"
|
||||
strokeDasharray="16 84"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span key={`packet-${selected.id}`} className={`upload-packet packet-${selected.id}`} aria-hidden="true">
|
||||
<svg viewBox="0 0 20 20">
|
||||
<path d="M 5 10 H 14 M 10.5 6.5 L 14 10 L 10.5 13.5" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<div className="upload-destinations" role="group" aria-label="上传目标">
|
||||
{destinations.map((destination) => (
|
||||
<button
|
||||
key={destination.id}
|
||||
type="button"
|
||||
className={destination.id === selected.id ? "is-selected" : ""}
|
||||
aria-pressed={destination.id === selected.id}
|
||||
aria-label={`选择 ${destination.label} 上传目标`}
|
||||
onPointerDown={stopSceneDrag}
|
||||
onClick={() => setSelected(destination)}
|
||||
>
|
||||
<span className="destination-status" />
|
||||
<strong>{destination.label}</strong>
|
||||
<small>{destination.detail}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="upload-result scene-float-delayed" aria-live="polite">
|
||||
<span className="result-check" aria-hidden="true">✓</span>
|
||||
<div>
|
||||
<small>LINK COPIED</small>
|
||||
<strong>cdn.snapgo.dev/{selected.id}/...</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IdentificationScene() {
|
||||
const [mode, setMode] = useState(aiModes[0]);
|
||||
|
||||
return (
|
||||
<div className="visual-scene identification-scene">
|
||||
<div className="ai-source-card">
|
||||
<div className="mini-window-bar" aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<div className="ai-source-content">
|
||||
<span className="source-sidebar" />
|
||||
<div className="source-settings">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
<div className="source-dialog">
|
||||
<span className="dialog-icon">!</span>
|
||||
<div>
|
||||
<strong>Permission required</strong>
|
||||
<small>Screen Recording</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="ai-scan-line" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="ai-core" aria-hidden="true">
|
||||
<span>AI</span>
|
||||
<i className="ai-orbit orbit-one" />
|
||||
<i className="ai-orbit orbit-two" />
|
||||
</div>
|
||||
|
||||
<div className="ai-result-card scene-float-soft" aria-live="polite">
|
||||
<div className="ai-result-heading">
|
||||
<span className="ai-spark" aria-hidden="true">✦</span>
|
||||
<strong>SnapGo Vision</strong>
|
||||
<i />
|
||||
</div>
|
||||
<p>{mode.result}</p>
|
||||
<div className="confidence-row">
|
||||
<span><i /></span>
|
||||
<small>{mode.score} confidence</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ai-mode-switch" role="group" aria-label="AI 处理模式">
|
||||
{aiModes.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={item.id === mode.id ? "is-selected" : ""}
|
||||
aria-pressed={item.id === mode.id}
|
||||
onPointerDown={stopSceneDrag}
|
||||
onClick={() => setMode(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OcrScene() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [language, setLanguage] = useState<OcrLanguage>("auto");
|
||||
const activeMode = ocrModes.find((mode) => mode.id === language) ?? ocrModes[2];
|
||||
|
||||
function selectLanguage(mode: OcrLanguage) {
|
||||
setLanguage(mode);
|
||||
setCopied(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="visual-scene ocr-scene">
|
||||
<div className="ocr-document">
|
||||
<div className="document-header">
|
||||
<span />
|
||||
<div><i /><i /><i /></div>
|
||||
</div>
|
||||
<div className="document-copy">
|
||||
<strong>Release notes</strong>
|
||||
<span className="text-line line-long" />
|
||||
<span className="text-line line-medium" />
|
||||
<span className="text-line line-highlight"><i>Capture, understand, share.</i></span>
|
||||
<span className="text-line line-long" />
|
||||
<span className="text-line line-short" />
|
||||
<span className="text-line line-highlight secondary"><i>让每一次截图直接进入下一步。</i></span>
|
||||
</div>
|
||||
<span className="ocr-scan-bar" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="ocr-selection-card scene-float-delayed">
|
||||
<div className="selection-heading">
|
||||
<span>OCR</span>
|
||||
<small>{activeMode.name}</small>
|
||||
</div>
|
||||
<p>
|
||||
{activeMode.lines.map((line) => <span key={line}>{line}</span>)}
|
||||
</p>
|
||||
<div className="selection-meta">
|
||||
<span>{activeMode.characters} characters</span>
|
||||
<strong>{activeMode.confidence}</strong>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={copied ? "is-copied" : ""}
|
||||
aria-pressed={copied}
|
||||
onPointerDown={stopSceneDrag}
|
||||
onClick={() => setCopied((current) => !current)}
|
||||
>
|
||||
<span aria-hidden="true">{copied ? "✓" : "⌘C"}</span>
|
||||
{copied ? "已复制" : "复制文本"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ocr-language-pills" role="group" aria-label="OCR 识别语言">
|
||||
{ocrModes.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
className={mode.id === language ? "is-selected" : ""}
|
||||
aria-pressed={mode.id === language}
|
||||
onPointerDown={stopSceneDrag}
|
||||
onClick={() => selectLanguage(mode.id)}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolGlyph({ tool }: { tool: CaptureTool }) {
|
||||
if (tool === "rect") {
|
||||
return <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="4" y="4" width="12" height="12" rx="2" /></svg>;
|
||||
}
|
||||
|
||||
if (tool === "arrow") {
|
||||
return <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 15 15 4M9 4h6v6" /></svg>;
|
||||
}
|
||||
|
||||
if (tool === "pen") {
|
||||
return <svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 15 2.2-4.7L13.8 3.7l2.5 2.5-6.6 6.6L5 15Z" /><path d="m11.8 5.7 2.5 2.5" /></svg>;
|
||||
}
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true">
|
||||
<rect x="3" y="3" width="5" height="5" />
|
||||
<rect x="12" y="3" width="5" height="5" />
|
||||
<rect x="3" y="12" width="5" height="5" />
|
||||
<rect x="12" y="12" width="5" height="5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AnnotationPreview({ tool }: { tool: CaptureTool }) {
|
||||
if (tool === "arrow") {
|
||||
return (
|
||||
<svg className="annotation-arrow" viewBox="0 0 180 110" aria-hidden="true">
|
||||
<path d="M28 84C66 78 91 56 139 31" />
|
||||
<path d="m121 27 20 3-8 18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (tool === "pen") {
|
||||
return (
|
||||
<svg className="annotation-pen" viewBox="0 0 180 110" aria-hidden="true">
|
||||
<path d="M24 76c20-42 36 25 57-6s33-33 48-4c9 18 21 13 29-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (tool === "mosaic") {
|
||||
return <div className="annotation-mosaic" aria-hidden="true">{Array.from({ length: 20 }, (_, index) => <span key={index} />)}</div>;
|
||||
}
|
||||
|
||||
return <div className="annotation-rect" aria-hidden="true"><span /><span /><span /><span /></div>;
|
||||
}
|
||||
|
||||
function CaptureScene() {
|
||||
const [tool, setTool] = useState<CaptureTool>("rect");
|
||||
|
||||
return (
|
||||
<div className="visual-scene capture-scene">
|
||||
<div className="capture-canvas">
|
||||
<div className="canvas-app-bar">
|
||||
<span />
|
||||
<strong>Project overview</strong>
|
||||
<i />
|
||||
</div>
|
||||
<div className="canvas-layout" aria-hidden="true">
|
||||
<span className="canvas-sidebar" />
|
||||
<div className="canvas-content">
|
||||
<i className="canvas-heading" />
|
||||
<i className="canvas-line" />
|
||||
<i className="canvas-line short" />
|
||||
<div className="canvas-chart"><span /><span /><span /><span /><span /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="capture-selection">
|
||||
<span className="selection-size">1280 × 720</span>
|
||||
<i className="selection-handle handle-nw" />
|
||||
<i className="selection-handle handle-ne" />
|
||||
<i className="selection-handle handle-sw" />
|
||||
<i className="selection-handle handle-se" />
|
||||
<AnnotationPreview tool={tool} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="capture-toolbar" role="toolbar" aria-label="截图标注工具">
|
||||
{captureTools.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={item.id === tool ? "is-selected" : ""}
|
||||
aria-label={item.label}
|
||||
aria-pressed={item.id === tool}
|
||||
title={item.label}
|
||||
onPointerDown={stopSceneDrag}
|
||||
onClick={() => setTool(item.id)}
|
||||
>
|
||||
<ToolGlyph tool={item.id} />
|
||||
</button>
|
||||
))}
|
||||
<span className="toolbar-divider" />
|
||||
<span className="toolbar-color" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const scenes = [UploadScene, IdentificationScene, OcrScene, CaptureScene];
|
||||
|
||||
export default function ShowcaseVisual({ activeIndex }: ShowcaseVisualProps) {
|
||||
const Scene = scenes[activeIndex] ?? UploadScene;
|
||||
|
||||
return (
|
||||
<div className="scene-parallax" data-testid="showcase-visual">
|
||||
<Scene />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1828
-102
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user