桌面 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:
1 parent
6452026fd6
commit
2bb4ef7a95
299 files changed
+6059
-3322
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user