Files
opsxapi/apps/web/src/styles.css
T
logan 1bcd7be2ac AI 大模型接入 + 自动化测试(手动/AI/定时三种触发)
- 自动化测试核心(packages/core/src/testing.ts):
  - JSONPath 求值($.a.b[0].c)、断言评估(状态码/耗时/Header/Body × 10 种操作)
  - 变量提取(响应 → 当前环境,{{var}} 链式传值)、Markdown 报告导出
- 用例模型:场景用例 = 有序步骤(引用 HTTP 接口 + 断言 + 提取),失败后继续可选
- 执行器 testRunner + 定时调度器 scheduler(应用运行期间按间隔自动执行)
- TestingView:用例列表 / 步骤编排 / 断言与提取编辑器 / 报告(统计卡+断言明细+过滤+再次运行+导出)
- AI 助手:OpenAI 兼容客户端(DeepSeek/智谱/Kimi/OpenAI/Ollama 预设)
  - 设置 → AI 助手:供应商 CRUD + 测试连接 + 当前使用切换
  - AI 生成场景用例:读取集合接口清单 → 编排步骤/断言/提取 → 预览保存
- 入口:侧栏底部「自动化测试」+ ⋯ 菜单;文档新增 /docs/testing /docs/ai
- 新增 11 个引擎单测(总 48 前端用例全绿);本地 E2E:运行通过+断言通过+petId 提取验证
2026-09-18 01:57:57 +08:00

568 lines
19 KiB
CSS

@tailwind base;
@tailwind components;
@tailwind utilities;
/* ============ 主题变量 ============ */
:root,
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #0b1220;
--panel: #0f1726;
--panel-2: #131e33;
--panel-3: #182540;
--border: #1e2b45;
--border-strong: #2b3b5c;
--text: #dbe4f3;
--text-dim: #8b99b0;
--text-faint: #5c6a83;
--accent: #3b82f6;
--accent-strong: #60a5fa;
--accent-soft: rgba(59, 130, 246, 0.14);
--success: #10b981;
--warn: #f59e0b;
--danger: #f43f5e;
--input-bg: #0a1120;
--code-bg: #070c16;
--hover: rgba(148, 163, 184, 0.08);
--active: rgba(59, 130, 246, 0.12);
--scrollbar: #334155;
--m-GET: #34d399;
--m-POST: #fbbf24;
--m-PUT: #60a5fa;
--m-PATCH: #c084fc;
--m-DELETE: #fb7185;
--m-HEAD: #94a3b8;
--m-OPTIONS: #38bdf8;
}
:root[data-theme="light"] {
color-scheme: light;
--bg: #eef1f6;
--panel: #ffffff;
--panel-2: #f6f8fb;
--panel-3: #eef2f8;
--border: #e3e8f0;
--border-strong: #cbd5e1;
--text: #1c2434;
--text-dim: #5b6880;
--text-faint: #8a95a9;
--accent: #2563eb;
--accent-strong: #1d4ed8;
--accent-soft: rgba(37, 99, 235, 0.1);
--success: #059669;
--warn: #d97706;
--danger: #e11d48;
--input-bg: #ffffff;
--code-bg: #f5f7fa;
--hover: rgba(100, 116, 139, 0.08);
--active: rgba(37, 99, 235, 0.1);
--scrollbar: #c4cdda;
--m-GET: #059669;
--m-POST: #d97706;
--m-PUT: #2563eb;
--m-PATCH: #9333ea;
--m-DELETE: #e11d48;
--m-HEAD: #64748b;
--m-OPTIONS: #0284c7;
}
@layer base {
* {
scrollbar-color: var(--scrollbar) transparent;
scrollbar-width: thin;
}
body {
@apply m-0 font-sans antialiased;
background: var(--bg);
color: var(--text);
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
}
::selection {
background: var(--accent-soft);
}
}
@layer components {
/* ---------- 文本工具 ---------- */
.ops-dim { color: var(--text-dim); }
.ops-faint { color: var(--text-faint); }
.ops-accent { color: var(--accent-strong); }
.ops-success { color: var(--success); }
.ops-warn { color: var(--warn); }
.ops-danger { color: var(--danger); }
/* ---------- 面板 ---------- */
.ops-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 0.75rem;
}
.ops-panel { background: var(--panel); }
.ops-panel-2 { background: var(--panel-2); }
.ops-border { border-color: var(--border); }
.ops-divider { border-bottom: 1px solid var(--border); }
/* ---------- 输入 ---------- */
.ops-input {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 0.4rem 0.7rem;
font-size: 0.8125rem;
color: var(--text);
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.ops-input::placeholder { color: var(--text-faint); }
.ops-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.ops-input-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.ops-label {
display: block;
margin-bottom: 0.375rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-faint);
}
/* ---------- 按钮 ---------- */
.ops-btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition;
border: 1px solid transparent;
cursor: pointer;
}
.ops-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.ops-btn-primary { background: var(--accent); color: #fff; }
.ops-btn-primary:hover:not(:disabled) { background: var(--accent-strong); }
.ops-btn-success { background: var(--success); color: #fff; }
.ops-btn-success:hover:not(:disabled) { filter: brightness(1.1); }
.ops-btn-danger { background: var(--danger); color: #fff; }
.ops-btn-danger:hover:not(:disabled) { filter: brightness(1.1); }
.ops-btn-secondary {
background: var(--panel-2);
border-color: var(--border);
color: var(--text);
}
.ops-btn-secondary:hover:not(:disabled) { border-color: var(--border-strong); background: var(--hover); }
.ops-btn-ghost {
background: transparent;
color: var(--text-dim);
}
.ops-btn-ghost:hover:not(:disabled) { background: var(--hover); color: var(--text); }
/* ---------- chip / badge ---------- */
.ops-chip {
@apply rounded-full px-2.5 py-1 text-[11px] font-medium transition;
border: 1px solid var(--border);
background: var(--panel-2);
color: var(--text-dim);
}
.ops-chip:hover { border-color: var(--accent); color: var(--accent-strong); }
.ops-chip-active {
background: var(--accent-soft);
border-color: var(--accent);
color: var(--accent-strong);
}
/* ---------- Tab ---------- */
.ops-tab {
@apply px-3.5 py-2 text-xs font-medium transition;
color: var(--text-faint);
border-bottom: 2px solid transparent;
white-space: nowrap;
}
.ops-tab:hover { color: var(--text); }
.ops-tab-active {
color: var(--accent-strong);
border-bottom-color: var(--accent);
}
.ops-tab .ops-count {
@apply ml-1 rounded-full px-1.5 text-[10px];
background: var(--hover);
color: var(--text-dim);
}
/* ---------- 代码 ---------- */
.ops-code {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 0.75rem;
font-family: "JetBrains Mono", SFMono-Regular, Menlo, monospace;
font-size: 0.75rem;
line-height: 1.55;
color: var(--text);
outline: none;
}
.ops-code:focus { border-color: var(--accent); }
.ops-code-no-bg {
background: transparent;
border: none;
padding: 0;
}
/* ---------- 方法色 ---------- */
.ops-method-GET { color: var(--m-GET); }
.ops-method-POST { color: var(--m-POST); }
.ops-method-PUT { color: var(--m-PUT); }
.ops-method-PATCH { color: var(--m-PATCH); }
.ops-method-DELETE { color: var(--m-DELETE); }
.ops-method-HEAD { color: var(--m-HEAD); }
.ops-method-OPTIONS { color: var(--m-OPTIONS); }
.ops-method-badge {
@apply inline-block rounded px-1.5 py-0.5 text-[10px] font-bold tracking-wide;
background: var(--hover);
}
/* ---------- 树 ---------- */
.ops-tree-row {
@apply flex items-center gap-1.5 rounded-md px-2 py-1.5 text-[13px] cursor-pointer transition;
color: var(--text-dim);
}
.ops-tree-row:hover { background: var(--hover); color: var(--text); }
.ops-tree-row-active {
background: var(--active);
color: var(--accent-strong);
}
/* ---------- 弹窗 ---------- */
.ops-modal-mask {
@apply fixed inset-0 z-50 grid place-items-center p-6;
background: rgba(2, 6, 18, 0.55);
backdrop-filter: blur(2px);
}
.ops-modal {
@apply w-full max-w-lg rounded-2xl shadow-2xl flex flex-col overflow-hidden;
background: var(--panel);
border: 1px solid var(--border);
max-height: 85vh;
}
.ops-modal-header {
@apply flex items-center justify-between px-5 py-3.5;
border-bottom: 1px solid var(--border);
}
.ops-modal-title { @apply text-sm font-semibold; }
.ops-modal-body { @apply px-5 py-4 overflow-auto; }
.ops-modal-footer {
@apply flex items-center justify-end gap-2 px-5 py-3;
border-top: 1px solid var(--border);
background: var(--panel-2);
}
/* ---------- 右键菜单 ---------- */
.ops-menu {
@apply fixed z-[60] min-w-[180px] rounded-xl py-1.5 shadow-2xl;
background: var(--panel);
border: 1px solid var(--border);
}
.ops-menu-item {
@apply flex w-full items-center gap-2.5 px-3.5 py-1.5 text-left text-[13px] transition;
color: var(--text-dim);
}
.ops-menu-item:hover { background: var(--hover); color: var(--text); }
.ops-menu-item-danger:hover { color: var(--danger); }
.ops-menu-sep { border-top: 1px solid var(--border); margin: 0.375rem 0; }
/* ---------- 实时消息 ---------- */
.ops-msg {
@apply rounded-lg border px-2.5 py-1.5 font-mono text-[12px] leading-5 whitespace-pre-wrap break-words;
}
.ops-msg-in { border-color: color-mix(in srgb, var(--success) 30%, transparent); background: color-mix(in srgb, var(--success) 8%, transparent); }
.ops-msg-out { border-color: color-mix(in srgb, var(--accent) 30%, transparent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.ops-msg-sys { background: var(--hover); border-color: var(--border); color: var(--text-dim); }
.ops-msg-err { border-color: color-mix(in srgb, var(--danger) 40%, transparent); background: color-mix(in srgb, var(--danger) 8%, transparent); }
/* ---------- JSON 高亮 ---------- */
.ops-json-key { color: var(--accent-strong); }
.ops-json-str { color: var(--success); }
.ops-json-num { color: var(--m-POST); }
.ops-json-bool { color: var(--m-PATCH); }
.ops-json-null { color: var(--text-faint); }
/* ---------- 状态点 ---------- */
.ops-dot { @apply inline-block h-2 w-2 rounded-full; }
.ops-dot-open { background: var(--success); box-shadow: 0 0 6px var(--success); }
.ops-dot-connecting { background: var(--warn); animation: ops-pulse 1s infinite; }
.ops-dot-closed { background: var(--text-faint); }
.ops-dot-error { background: var(--danger); }
/* ---------- 滚动区 ---------- */
.ops-scroll { overflow: auto; }
/* ---------- 环境徽标 ---------- */
.env-badge {
display: inline-grid;
place-items: center;
border-radius: 6px;
font-weight: 700;
line-height: 1;
border: 1px solid transparent;
flex-shrink: 0;
user-select: none;
}
/* ---------- 顶栏环境切换器 ---------- */
.ops-envswitch {
@apply inline-flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs font-medium transition;
border: 1px solid var(--border);
background: var(--panel-2);
color: var(--text);
max-width: 240px;
}
.ops-envswitch:hover { border-color: var(--border-strong); }
/* ---------- 菜单项 flex(支持徽标行) ---------- */
.ops-menu-item-flex { display: flex; align-items: center; width: 100%; }
/* ---------- 环境管理弹窗 ---------- */
.env-dialog { display: grid; grid-template-columns: 196px minmax(0, 1fr); gap: 0 18px; min-height: 420px; margin: 0 -1.25rem -0.25rem; }
.env-dialog-side {
border-right: 1px solid var(--border);
padding-right: 14px;
display: flex; flex-direction: column; gap: 2px;
max-height: 60vh; overflow-y: auto;
}
.env-dialog-group { @apply mb-1.5 px-2 text-[11px] font-semibold uppercase tracking-[0.08em]; color: var(--text-faint); }
.env-dialog-item {
@apply flex items-center gap-2 rounded-lg px-2 py-1.5 text-[13px] transition;
color: var(--text-dim);
}
.env-dialog-item:hover { background: var(--hover); color: var(--text); }
.env-dialog-item.active { background: var(--active); color: var(--accent-strong); }
.env-dialog-current {
@apply rounded-full px-1.5 py-0.5 text-[10px] font-semibold;
background: color-mix(in srgb, var(--success) 14%, transparent);
color: var(--success);
}
.env-dialog-new {
@apply mt-2 flex items-center justify-center gap-1 rounded-lg border border-dashed px-2 py-2 text-xs transition;
border-color: var(--border);
color: var(--text-dim);
}
.env-dialog-new:hover { border-color: var(--accent); color: var(--accent-strong); }
.env-dialog-main { min-width: 0; display: flex; flex-direction: column; }
.env-dialog-vars { margin-top: 10px; }
/* ---------- 信息提示条 ---------- */
.ops-hint {
@apply mb-3 flex items-start gap-2 rounded-lg px-3 py-2 text-xs leading-5;
background: color-mix(in srgb, var(--accent) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
color: var(--text-dim);
}
.ops-hint svg { color: var(--accent-strong); margin-top: 2px; }
.ops-hint b { color: var(--accent-strong); font-family: inherit; }
/* ---------- 设置弹窗(左侧导航 + 分区) ---------- */
.set-dialog { display: grid; grid-template-columns: 188px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); gap: 0 16px; height: min(66vh, 620px); min-height: 420px; margin: 0 -1.25rem -0.25rem; }
.set-side {
border-right: 1px solid var(--border);
padding-right: 12px;
display: flex; flex-direction: column; gap: 2px;
}
.set-side-head {
@apply mb-2 px-2 text-[11px] font-semibold uppercase tracking-[0.08em];
color: var(--text-faint);
}
.set-side-item {
@apply flex items-center gap-2.5 rounded-lg px-2 py-2 transition;
color: var(--text-dim);
}
.set-side-item:hover { background: var(--hover); color: var(--text); }
.set-side-item.active { background: var(--active); color: var(--accent-strong); }
.set-side-icon {
@apply grid h-7 w-7 shrink-0 place-items-center rounded-lg;
background: var(--panel-2);
color: var(--text-dim);
}
.set-side-icon.active { color: var(--accent-strong); }
.set-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.set-main-head { padding-bottom: 10px; flex-shrink: 0; }
.set-main-body { flex: 1; min-height: 0; padding-top: 14px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding-right: 4px; }
.set-main-body > .set-group { flex-shrink: 0; }
.set-group {
@apply overflow-hidden rounded-xl;
border: 1px solid var(--border);
background: var(--panel-2);
}
.set-group-title {
@apply px-4 pt-3 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em];
color: var(--text-faint);
}
.set-row {
@apply flex items-center gap-4 px-4 py-3;
}
.set-row + .set-row { border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.set-row-control { flex-shrink: 0; display: flex; align-items: center; gap: 6px; }
/* 开关 */
.set-toggle {
@apply relative h-[22px] w-[38px] rounded-full transition;
background: var(--border);
}
.set-toggle:hover { opacity: 0.85; }
.set-toggle.on { background: var(--accent); }
.set-toggle-dot {
@apply absolute top-[3px] left-[3px] h-4 w-4 rounded-full bg-white transition;
}
.set-toggle.on .set-toggle-dot { transform: translateX(16px); }
/* Mock 规则表(随右侧内容区整体滚动,表头吸附) */
.set-group .ops-hint { margin: 10px 12px 0; }
.mock-rules { @apply mt-2; }
.mock-rule-head, .mock-rule-row {
display: grid;
grid-template-columns: 88px 84px minmax(110px, 1fr) minmax(130px, 1.2fr) minmax(96px, 0.9fr) 44px 30px;
gap: 6px;
align-items: center;
padding: 4px 12px;
}
.mock-rule-head {
@apply sticky top-0 z-[1] text-[11px] font-semibold;
color: var(--text-faint);
background: var(--panel-2);
border-bottom: 1px solid var(--border);
padding-top: 8px;
padding-bottom: 8px;
}
.mock-rule-row { padding-top: 5px; padding-bottom: 5px; }
.mock-rule-row + .mock-rule-row { border-top: 1px solid color-mix(in srgb, var(--border) 45%, transparent); }
.mock-rule-pattern { font-family: ui-monospace, monospace; }
.mock-rule-expr { font-family: ui-monospace, monospace; color: var(--accent-strong); }
/* Mock 速查表 */
.mock-cheatsheet { @apply mt-2 grid grid-cols-2 gap-1.5; }
.mock-cheat-item {
@apply flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[11px] transition;
background: var(--panel);
border: 1px solid var(--border);
color: var(--text-dim);
}
.mock-cheat-item:hover { border-color: var(--accent); color: var(--text); }
.mock-cheat-item code {
font-family: ui-monospace, monospace;
color: var(--accent-strong);
font-size: 11px;
flex-shrink: 0;
}
.mock-cheat-item .ops-faint { flex: 1; min-width: 0; @apply truncate; }
.mock-cheat-gen {
max-width: 46%;
@apply truncate font-mono text-[10px];
color: var(--text-faint);
}
.mock-preview-value {
@apply rounded-md px-2 py-1 font-mono text-[12px];
background: var(--panel-2);
border: 1px solid var(--border);
color: var(--accent-strong);
}
/* ---------- 自动化测试 ---------- */
.test-assert-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.test-assert-row .ops-input { max-width: 150px; }
.test-assert-locator { font-family: ui-monospace, monospace; }
.test-assert-expected { font-family: ui-monospace, monospace; min-width: 110px; }
.test-stat-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
.test-stat {
@apply rounded-xl px-3 py-2.5 text-center;
background: var(--panel-2);
border: 1px solid var(--border);
}
.test-stat.ok { border-color: color-mix(in srgb, var(--success) 45%, transparent); }
.test-stat.bad { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.test-stat-value { font-size: 20px; font-weight: 700; line-height: 1.3; }
.test-stat.ok .test-stat-value { color: var(--success); }
.test-stat.bad .test-stat-value { color: var(--danger); }
.test-stat-label { @apply mt-0.5 text-[11px]; color: var(--text-faint); }
.test-step-result {
@apply overflow-hidden rounded-xl;
background: var(--panel-2);
border: 1px solid var(--border);
}
.test-assert-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.test-assert-table th {
@apply px-2 py-1 text-left font-semibold;
color: var(--text-faint);
border-bottom: 1px solid var(--border);
}
.test-assert-table td {
@apply px-2 py-1 align-top;
border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
color: var(--text-dim);
max-width: 320px;
overflow-wrap: anywhere;
}
/* ---------- {{变量}} 补全浮层 ---------- */
.var-hint-wrap { position: relative; min-width: 0; flex: 1; display: inline-flex; }
.var-hint-panel {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 40;
min-width: 280px;
max-width: 420px;
@apply overflow-hidden rounded-xl py-1 shadow-2xl;
background: var(--panel);
border: 1px solid var(--border);
}
.var-hint-head {
@apply flex items-center gap-1.5 px-2.5 py-1.5 text-[11px];
color: var(--text-dim);
border-bottom: 1px solid var(--border);
}
.var-hint-head .ops-faint { margin-left: auto; }
.var-hint-item {
@apply flex w-full items-center gap-3 px-2.5 py-1.5 text-left font-mono text-[12px] transition;
color: var(--text-dim);
}
.var-hint-item:hover, .var-hint-item.sel { background: var(--active); color: var(--text); }
.var-hint-name { color: var(--accent-strong); flex-shrink: 0; }
.var-hint-value { @apply truncate; color: var(--text-dim); }
/* ---------- misc ---------- */
.ops-link { color: var(--accent-strong); text-decoration: none; }
.ops-link:hover { text-decoration: underline; }
.ops-selected-input {
outline: 1.5px solid var(--accent);
border-radius: 0.25rem;
}
}
@keyframes ops-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* 拖拽落点提示 */
.ops-drop-target {
outline: 1.5px dashed var(--accent);
outline-offset: -2px;
}