feat: OpsxAPI v1.0 全协议接口工作台

- 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT
- 集合树/多标签/多环境变量/历史记录,localStorage 持久化
- 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、
  Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖)
- 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、
  全量备份;单请求生成 cURL/fetch/Python/Go 代码
- Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、
  WebSocket 隧道(浏览器自定义握手 Header)
- 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付)
- lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题
- 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全
This commit is contained in:
logan committed 2026-09-18 00:16:59 +08:00
commit dec66db5aa
182 files changed
+23377

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#080d14" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>OpsxAPI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@opsxapi/web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1 --port 5173",
"build": "tsc -b && vite build",
"preview": "vite preview --host 127.0.0.1 --port 5173"
},
"dependencies": {
"@opsxapi/core": "*",
"@opsxapi/transport": "*",
"@opsxapi/ui": "*",
"@tauri-apps/api": "^2.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.20.3",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2",
"vite": "^6.2.2"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2563EB"/>
<stop offset="1" stop-color="#06B6D4"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="url(#g)"/>
<g stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M17 25h26"/>
<path d="M37 19l6 6-6 6"/>
<path d="M47 39H21"/>
<path d="M27 33l-6 6 6 6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 564 B

+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { OpsxWorkspace } from "@opsxapi/ui";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<OpsxWorkspace />
</React.StrictMode>,
);
+417
View File
@@ -0,0 +1,417 @@
@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; }
/* ---------- {{变量}} 补全浮层 ---------- */
.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;
}
+15
View File
@@ -0,0 +1,15 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./index.html",
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
theme: {
extend: {
fontFamily: { mono: ["JetBrains Mono", "SFMono-Regular", "Menlo", "monospace"] },
},
},
plugins: [],
} satisfies Config;
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"noEmit": true,
"allowImportingTsExtensions": true,
"types": [
"vite/client"
]
},
"include": [
"src",
"../../packages/ui/src",
"../../packages/core/src",
"../../packages/transport/src"
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": true,
"composite": true,
"types": [
"node"
]
},
"include": [
"vite.config.ts"
]
}
+41
View File
@@ -0,0 +1,41 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@opsxapi/core": path.resolve(__dirname, "../../packages/core/src/index.ts"),
"@opsxapi/transport": path.resolve(__dirname, "../../packages/transport/src/index.ts"),
"@opsxapi/ui": path.resolve(__dirname, "../../packages/ui/src/index.ts"),
},
},
server: {
host: "127.0.0.1",
port: 5173,
strictPort: true,
proxy: {
"/v1": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
"/health": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
"/docs": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
"/home": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
"/site": {
target: "http://127.0.0.1:8787",
changeOrigin: true,
},
},
},
});