接入模型改成右侧抽屉:先挑供应商、再填连接、当场测连通(且只测不存)

上一版是把字段倒在地上:整页宽的内联卡、输入框拉到 1100px、动作钮掉出屏幕。
照模型接入面的通行做法重排 —— 520px 右侧抽屉,头固定 / 身滚动 / 底动作栏常驻:

  ① **供应商**:一排可点的牌子,挑中即预填它的默认端点(没有默认的标「端点自填」);
  ② **连接**:API 端点 + API Key(密码框带显示切换),下面就是「测试连接」;
  ③ **型号与用途**:模型(厂商侧型号名)+ 用途名(活动里声明的那个)+ 两枚开关。
  遮罩点击与 Esc 都关得掉 —— 抽屉盖住整屏,不给键盘出路是把人关在里面。

**默认端点从服务端取**(新 `GET /api/gov/llm/providers`):前端不自带一份 URL 表 ——
那种表迟早与 provider 档案漂移,而漂移那天没人会发现(端点错了只表现为「连不上」)。

**「测试连接」只测不存**(新 `POST /api/gov/llm/probe` + `llm.probeDraft`):
拿表单上正填的四元真发一次最小调用,一个字节不落库;密钥留空则取该实例已存的密文,
改端点、换型号不必重打钥匙。上一版是「先存再试」—— 人只想知道钥匙对不对,
结果端点被改了、实例被建了,点取消也收不回来。实测:改错端点后测试,
库里 instances 逐字节未变(测过 before/after 全等)。

顺带补 `eye-off` 图标。五闸绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-30 01:10:49 +08:00
co-authored by Claude Opus 5
parent 21aea48c6c
commit f9318b1ae9
12 changed files with 422 additions and 76 deletions
+49
View File
@@ -314,6 +314,55 @@
.g-fcard .probe { position: relative; z-index: 1; display: flex; align-items: flex-start; gap: 6px; padding: 8px 10px; border-radius: 9px; background: #f2f7f4; font: 500 11px/1.5 var(--sans); color: #0c8154; }
.g-fcard .probe.bad { background: #fdf3f3; color: #a83a3a; }
.g-fcard .probe svg { flex: none; margin-top: 2px; }
/* ── 抽屉:一次性的配置岔路(接入模型 / 改绑定)——————————————————
右侧 520px 定宽面板 + 遮罩;头固定、身滚动、底动作栏常驻。
配置不该把货架顶下去:做完就该回到原地,所以是抽屉不是页内表单。 */
.g-drawer { position: fixed; inset: 0; z-index: 60; display: flex; justify-content: flex-end; }
.g-drawer-veil { flex: 1; border: 0; padding: 0; background: rgba(19, 28, 38, .38); backdrop-filter: blur(1.5px); cursor: default; animation: g-fade .16s ease-out; }
.g-drawer-p { position: relative; display: flex; flex-direction: column; width: min(520px, 100vw); background: var(--card); border-left: 1px solid var(--cline); box-shadow: -18px 0 44px -24px rgba(19, 28, 38, .5); animation: g-slidein .2s cubic-bezier(.2, .8, .2, 1); }
@keyframes g-fade { from { opacity: 0; } }
@keyframes g-slidein { from { transform: translateX(24px); opacity: .6; } }
@media (prefers-reduced-motion: reduce) { .g-drawer-veil, .g-drawer-p { animation: none; } }
.g-drawer-p > header { flex: none; display: flex; align-items: flex-start; gap: 12px; padding: 18px 20px 15px; border-bottom: 1px solid var(--line-soft); }
.g-drawer-p > header b { display: block; font-size: 16px; font-weight: 700; color: var(--ink); }
.g-drawer-p > header span { display: block; margin-top: 4px; font-size: 11.5px; line-height: 1.6; color: var(--ink-4); }
.g-drawer-p > header .x { flex: none; width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
.g-drawer-p > header .x:hover { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
.g-drawer-b { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 20px 18px; }
.g-drawer-b section { padding: 16px 0 4px; }
.g-drawer-b section + section { border-top: 1px solid var(--line-soft); }
.g-drawer-b h4 { margin: 0 0 12px; font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-4); }
.g-drawer-p > footer { flex: none; display: flex; justify-content: flex-end; gap: 8px; padding: 13px 20px; border-top: 1px solid var(--line-soft); background: #fbfcfd; }
/* 供应商牌子:挑一个即预填它的默认端点 —— 接入面的第一步本来就是「谁家的」 */
.g-picktiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(146px, 1fr)); gap: 8px; }
.g-picktile { position: relative; display: flex; flex-direction: column; gap: 3px; padding: 10px 11px; border: 1px solid var(--cline); border-radius: 9px; background: #fff; text-align: left; cursor: pointer; transition: border-color var(--t-fast), background var(--t-fast); }
.g-picktile:hover { border-color: #aebdcd; }
.g-picktile.on { border-color: var(--brand); background: var(--brand-wash); }
.g-picktile .n { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.g-picktile .s { font: 500 10px/1.3 var(--mono); color: var(--ink-4); }
.g-picktile.on .s { color: var(--brand-deep); }
.g-picktile svg { position: absolute; right: 9px; top: 9px; color: var(--brand); }
/* 竖排字段:标签在上、输入居中、说明在下 —— 抽屉里一列到底,不做两栏 */
.g-fld { display: block; margin-bottom: 14px; }
.g-fld > span { display: block; margin-bottom: 5px; font-size: 12px; font-weight: 600; color: var(--ink-2, var(--ink)); }
.g-fld small { display: block; margin-top: 5px; font-size: 10.5px; line-height: 1.6; color: var(--ink-4); }
.g-inwrap { position: relative; display: block; }
.g-inwrap .g-input { width: 100%; padding-right: 34px; }
.g-inwrap .eye { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); width: 26px; height: 26px; display: grid; place-items: center; border: 0; border-radius: 6px; background: transparent; color: var(--ink-4); cursor: pointer; }
.g-inwrap .eye:hover { background: #f1f4f8; color: var(--ink); }
/* 测试连接:钮 + 读数同一行 —— 填完密钥当场知道通不通 */
.g-testrow { display: flex; align-items: center; gap: 10px; margin: 2px 0 6px; }
.g-testrow .r { min-width: 0; font-size: 11px; line-height: 1.5; color: var(--ink-4); }
.g-testrow .r.ok { color: #0c8154; font-weight: 600; }
.g-testrow .r.bad { color: #a83a3a; }
.g-hint { display: block; margin-top: 8px; font-size: 10.5px; line-height: 1.6; color: var(--ink-4); }
/* 开关行:两枚复选框并排 —— 表单里唯一的横排 */
.g-swrow { display: flex; align-items: center; gap: 18px; }
.g-swrow label { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--ink-2, var(--ink)); cursor: pointer; }