Wave 2.1: 返工黑屏/安装进度/官方配置三层/文档深度/真机列表
老板实测 5 条修复:DiagTab Hooks 黑屏、安装三阶段进度、配置按官方字段三层分组、FR-09 中文文档、detectCliAll 接入列表页。
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Check, Eye, EyeOff, Lock } from "lucide-react";
|
||||
import { readConfig, writeConfig } from "../ipc";
|
||||
import type { ConfigFieldState, ConfigFormState, WriteResult } from "../ipc/types";
|
||||
import { Check, ChevronDown, CircleAlert, ExternalLink, Eye, EyeOff, Lock, ShieldCheck } from "lucide-react";
|
||||
import { readConfig, verifyConfig, writeConfig } from "../ipc";
|
||||
import type {
|
||||
ConfigFieldState,
|
||||
ConfigFormState,
|
||||
ConfigVerifyResult,
|
||||
WriteResult,
|
||||
} from "../ipc/types";
|
||||
import { MonoChip } from "./MonoChip";
|
||||
|
||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条) */
|
||||
/** 官方授权方式中文名 */
|
||||
function authModeLabel(mode: string): string {
|
||||
switch (mode) {
|
||||
case "browser_oauth":
|
||||
return "账号授权(浏览器)";
|
||||
case "device_code":
|
||||
return "设备码";
|
||||
case "api_key":
|
||||
return "API Key";
|
||||
case "local_tui":
|
||||
return "本机终端授权";
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/** 配置表单(视觉规范 §3.4:单列 ≤640px、敏感字段密码框 + 密钥库说明、吸底保存条)
|
||||
* Wave 2.1:字段按官方配置方法渲染 + 保存后「生效检查」 */
|
||||
export function ConfigForm({ id }: { id: string }) {
|
||||
const [state, setState] = useState<ConfigFormState | null>(null);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
@@ -13,7 +35,9 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [result, setResult] = useState<WriteResult | null>(null);
|
||||
const [verify, setVerify] = useState<ConfigVerifyResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -52,11 +76,29 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
if (!state) return null;
|
||||
|
||||
const hasFields = state.fields.length > 0;
|
||||
const authFields = state.fields.filter((f) => f.group === "auth");
|
||||
const commonFields = state.fields.filter((f) => f.group === "common");
|
||||
const advancedFields = state.fields.filter((f) => f.group === "advanced");
|
||||
|
||||
function renderField(field: ConfigFieldState) {
|
||||
return (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
value={values[field.id] ?? ""}
|
||||
saved={sensitiveSaved[field.id] ?? false}
|
||||
revealed={!!revealed[field.id]}
|
||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
setSaving(true);
|
||||
setSaved(false);
|
||||
setResult(null);
|
||||
setVerify(null);
|
||||
setError(null);
|
||||
const patch: Record<string, string> = {};
|
||||
for (const f of state!.fields) {
|
||||
@@ -78,6 +120,13 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
// 刷新回显
|
||||
const s = await readConfig(id);
|
||||
setState(s);
|
||||
// 生效检查:写回的配置能否被 CLI 接受(配置文件解析 + CLI 版本探测)
|
||||
try {
|
||||
const v = await verifyConfig(id);
|
||||
setVerify(v);
|
||||
} catch {
|
||||
setVerify(null);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
@@ -89,17 +138,59 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
<div className="config-form">
|
||||
{!hasFields && <p className="panel-empty">该 CLI 未声明可配置的中文表单字段。</p>}
|
||||
|
||||
{state.fields.map((field) => (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
value={values[field.id] ?? ""}
|
||||
saved={sensitiveSaved[field.id] ?? false}
|
||||
revealed={!!revealed[field.id]}
|
||||
onToggleReveal={() => setRevealed((r) => ({ ...r, [field.id]: !r[field.id] }))}
|
||||
onChange={(v) => setValues((x) => ({ ...x, [field.id]: v }))}
|
||||
/>
|
||||
))}
|
||||
{/* 第一层:授权 / 登录(官方授权方式引导 + API Key 类字段) */}
|
||||
{(state.auth_modes.length > 0 || authFields.length > 0) && (
|
||||
<section className="config-section">
|
||||
<h4 className="config-section-title">授权 / 登录</h4>
|
||||
{state.auth_modes.length > 0 && (
|
||||
<ul className="auth-modes">
|
||||
{state.auth_modes.map((m) => (
|
||||
<li key={m.mode} className="auth-mode">
|
||||
<span className="auth-mode-name">{authModeLabel(m.mode)}</span>
|
||||
{m.notes_zh && <span className="auth-mode-note">{m.notes_zh}</span>}
|
||||
{m.command.length > 0 && (
|
||||
<span className="auth-mode-cmd">
|
||||
<MonoChip>{m.command.join(" ")}</MonoChip>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{authFields.map(renderField)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 第二层:常用配置 */}
|
||||
{commonFields.length > 0 && (
|
||||
<section className="config-section">
|
||||
<h4 className="config-section-title">常用配置</h4>
|
||||
{commonFields.map(renderField)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 第三层:高级配置(默认折叠) */}
|
||||
{advancedFields.length > 0 && (
|
||||
<section className="config-section">
|
||||
<button
|
||||
type="button"
|
||||
className="config-advanced-toggle"
|
||||
onClick={() => setAdvancedOpen((v) => !v)}
|
||||
aria-expanded={advancedOpen}
|
||||
>
|
||||
<span className="config-section-title">高级配置</span>
|
||||
<span className="advanced-badge">高级</span>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className={advancedOpen ? "advanced-chevron open" : "advanced-chevron"}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<p className="advanced-hint">以下为官方支持但少数用户使用的高级项,一般无需修改。</p>
|
||||
{advancedOpen && <div className="advanced-fields">{advancedFields.map(renderField)}</div>}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{state.files.length > 0 && (
|
||||
<div className="config-file-info">
|
||||
@@ -121,6 +212,16 @@ export function ConfigForm({ id }: { id: string }) {
|
||||
<Check size={14} strokeWidth={1.5} aria-hidden="true" /> 已保存
|
||||
</span>
|
||||
)}
|
||||
{verify && (
|
||||
<span className={verify.ok ? "verify-ok" : "verify-fail"}>
|
||||
{verify.ok ? (
|
||||
<ShieldCheck size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
) : (
|
||||
<CircleAlert size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
)}
|
||||
{verify.message_zh}
|
||||
</span>
|
||||
)}
|
||||
{result?.backup_path && (
|
||||
<span className="save-detail">
|
||||
已备份原文件 · {result.backup_path.split(/[\\/]/).pop()}
|
||||
@@ -158,7 +259,8 @@ function Field({
|
||||
onToggleReveal: () => void;
|
||||
}) {
|
||||
const isPassword = field.sensitive;
|
||||
const inputType = isPassword ? (revealed ? "text" : "password") : field.field_type === "url" ? "text" : "text";
|
||||
const isEnum = field.field_type === "enum";
|
||||
const inputType = isPassword ? (revealed ? "text" : "password") : "text";
|
||||
return (
|
||||
<div className="field">
|
||||
<label className="field-label" htmlFor={`field-${field.id}`}>
|
||||
@@ -171,15 +273,31 @@ function Field({
|
||||
<Lock size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
id={`field-${field.id}`}
|
||||
type={inputType}
|
||||
value={value}
|
||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
{isEnum ? (
|
||||
<select
|
||||
id={`field-${field.id}`}
|
||||
className="field-select"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">(使用 CLI 默认)</option>
|
||||
{field.options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label_zh}({o.value})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
id={`field-${field.id}`}
|
||||
type={inputType}
|
||||
value={value}
|
||||
placeholder={isPassword ? (saved ? "已保存 · 留空则不变" : "输入 API Key") : ""}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -196,9 +314,16 @@ function Field({
|
||||
{field.help_zh}
|
||||
{isPassword && (
|
||||
<span className="field-keyring-note">
|
||||
{saved ? " · 已加密保存于系统密钥库" : " · 保存后写入系统密钥库,绝不明文落盘"}
|
||||
{saved
|
||||
? ` · 已加密保存于系统密钥库${field.env_key ? `(${field.env_key})` : ""}`
|
||||
: ` · 保存后写入系统密钥库,绝不明文落盘${field.env_key ? `(${field.env_key})` : ""}`}
|
||||
</span>
|
||||
)}
|
||||
{field.docs_url && (
|
||||
<a className="field-docs-link" href={field.docs_url} target="_blank" rel="noreferrer">
|
||||
官方文档 <ExternalLink size={11} strokeWidth={1.5} aria-hidden="true" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user