桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)

- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
This commit is contained in:
logan committed 2026-10-16 03:03:36 +08:00
1 parent 6452026fd6
commit 2bb4ef7a95
299 files changed
+6059 -3322

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
/**
* 带环境变量自动补全的输入框:
* 输入 "{{" 后弹出当前环境变量浮层,↑↓ 选择、Enter/Tab 插入 {{变量名}}。
*/
import { useWorkspace } from "../workspaceStore";
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
import { EnvBadge } from "./EnvBadge";
type Candidate = { name: string; value: string; description?: string };
export function VarHintInput({
value,
onChange,
placeholder,
className,
onKeyDown,
autoFocus,
spellCheck = false,
ariaLabel,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
className?: string;
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
autoFocus?: boolean;
spellCheck?: boolean;
ariaLabel?: string;
}) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [query, setQuery] = useState<{ prefix: string; caret: number } | null>(null);
const [active, setActive] = useState(0);
const candidates = useMemo<Candidate[]>(() => {
const state = useWorkspace.getState();
const env = state.environments.find((e) => e.id === state.activeEnvironmentId);
return (env?.variables ?? [])
.filter((v) => v.enabled && v.key.trim())
.map((v) => ({ name: v.key.trim(), value: v.value, description: v.description }));
}, [query]); // query 变化时重查(环境可能被改)
const filtered = useMemo(() => {
if (!query) return [];
const q = query.prefix.toLowerCase();
const list = candidates.filter((c) => c.name.toLowerCase().includes(q));
return list.slice(0, 8);
}, [candidates, query]);
// 光标前未闭合的 {{ 即触发
const detect = (v: string, caret: number) => {
const before = v.slice(0, caret);
const idx = before.lastIndexOf("{{");
if (idx < 0) return null;
const frag = before.slice(idx + 2);
if (frag.includes("}") || frag.includes("{") || frag.includes(" ") || frag.length > 40) return null;
return { prefix: frag, caret };
};
const sync = () => {
const el = inputRef.current;
if (!el) return;
const q = detect(el.value, el.selectionStart ?? 0);
setQuery(q);
setActive(0);
};
const insert = (name: string) => {
const el = inputRef.current;
if (!el || !query) return;
const idx = el.value.slice(0, query.caret).lastIndexOf("{{");
if (idx < 0) return;
const next = `${el.value.slice(0, idx)}{{${name}}}` + el.value.slice(query.caret);
onChange(next);
setQuery(null);
const caretTarget = idx + name.length + 4;
requestAnimationFrame(() => {
el.focus();
el.setSelectionRange(caretTarget, caretTarget);
});
};
// 过滤结果为空时收起
useEffect(() => {
if (query && filtered.length === 0) setQuery(null);
}, [filtered.length, query]);
// 外部清空时重置
useLayoutEffect(() => {
if (!value) setQuery(null);
}, [value]);
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (query && filtered.length > 0) {
if (e.key === "ArrowDown") {
e.preventDefault();
e.stopPropagation();
setActive((a) => Math.min(a + 1, filtered.length - 1));
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
e.stopPropagation();
setActive((a) => Math.max(a - 1, 0));
return;
}
if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
e.stopPropagation();
insert(filtered[active].name);
return;
}
if (e.key === "Escape") {
e.stopPropagation();
setQuery(null);
return;
}
}
onKeyDown?.(e);
};
const envName = useWorkspace(
(s) => s.environments.find((e) => e.id === s.activeEnvironmentId)?.name ?? "",
);
const envId = useWorkspace((s) => s.activeEnvironmentId);
return (
<div className="var-hint-wrap">
<input
ref={inputRef}
className={className}
placeholder={placeholder}
value={value}
aria-label={ariaLabel}
spellCheck={spellCheck}
autoFocus={autoFocus}
onChange={(e) => {
const el = e.currentTarget;
onChange(el.value);
// 同步阶段探测(input 事件时 DOM 已更新,避免 React 并发提交的竞态)
setQuery(detect(el.value, el.selectionStart ?? el.value.length));
setActive(0);
}}
onKeyUp={sync}
onClick={sync}
onBlur={() => setTimeout(() => setQuery(null), 120)}
onKeyDown={handleKeyDown}
/>
{query && filtered.length > 0 && (
<div className="var-hint-panel" onMouseDown={(e) => e.preventDefault()}>
<div className="var-hint-head">
{envId ? (
<>
<EnvBadge envId={envId} name={envName} size={16} />
<span>{envName || "当前环境"}</span>
</>
) : (
<span>未选择环境</span>
)}
<span className="ops-faint">{"输入 {{变量名}} 引用 · Enter 插入"}</span>
</div>
{filtered.map((c, i) => (
<button
key={c.name}
className={i === active ? "var-hint-item sel" : "var-hint-item"}
onMouseEnter={() => setActive(i)}
onMouseDown={(e) => {
e.preventDefault();
insert(c.name);
}}
>
<span className="var-hint-name">{"{{"}{c.name}{"}}"}</span>
<span className="var-hint-value">{c.value || "(空)"}</span>
</button>
))}
</div>
)}
</div>
);
}