设置中心与 Mock 数据功能

- 顶栏:移除 tauri-native 传输徽章;⋯ 更多菜单重做(设置/Mock/环境/主题/文档/官网/重置)
- 设置弹窗重写:左侧导航(外观/网络/Mock 数据/数据管理/关于)+ 分组卡片 + 开关控件
- Mock 引擎(packages/core/src/mock.ts):Mock.js 常用语法子集 + 智能字段匹配
  - 61 条默认规则(通配符/精确/正则 × string/number/integer/boolean)
  - 发送链路接入:Query/JSON Body/urlencoded/form-data/GraphQL Variables
  - Body 工具栏:智能填充(生成可编辑 @表达式模板)+ Mock 预览
  - 设置页规则表 CRUD + 表达式速查表即时预览
- 文档新增「Mock 数据」页(/docs/mock),Manifest 顺序调整
- 新增 16 个 Mock 单测(总 37 前端用例全绿)
This commit is contained in:
logan committed 2026-09-18 01:35:54 +08:00
1 parent 6307a017a3
commit 9a019408ab
18 files changed
+1684 -104

No files matched your search

+80 -9
View File
@@ -1,10 +1,9 @@
/**
* 顶栏:logo / 新建 / 导入导出 / 环境 / 主题 / 设置。
* 顶栏:logo / 新建 / 导入导出 / 环境 / 主题 / 文档 / 设置 / 更多。
*/
import { detectTransport } from "@opsxapi/transport";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { useWorkspace } from "../workspaceStore";
import { ContextMenu, cx, type MenuItem } from "./common";
import { ContextMenu, type MenuItem } from "./common";
import { EnvBadge } from "./EnvBadge";
import { Icon, type IconName } from "./icons";
import { Logo } from "./Logo";
@@ -31,7 +30,7 @@ export function TopBar() {
const [newMenu, setNewMenu] = useState<{ x: number; y: number } | null>(null);
const [exportMenu, setExportMenu] = useState<{ x: number; y: number } | null>(null);
const transportKind = useMemo(() => detectTransport({ ginBaseUrl: "" }).kind, []);
const [moreMenu, setMoreMenu] = useState<{ x: number; y: number } | null>(null);
useEffect(() => {
document.title = "OpsxAPI · 接口工作台";
@@ -90,6 +89,63 @@ export function TopBar() {
},
];
const moreItems: MenuItem[] = [
{
label: "设置…",
icon: <Icon name="settings" size={14} />,
onClick: () => setDialog({ type: "settings" }),
},
{
label: "Mock 数据…",
icon: <Icon name="flask-conical" size={14} />,
onClick: () => setDialog({ type: "settings", tab: "mock" }),
},
{
label: "环境管理…",
icon: <Icon name="arrow-left-right" size={14} />,
onClick: () => setDialog({ type: "environments" }),
},
{
label: settings.theme === "dark" ? "切换到浅色主题" : "切换到深色主题",
icon: <Icon name={settings.theme === "dark" ? "sun" : "moon"} size={14} />,
onClick: () => setTheme(settings.theme === "dark" ? "light" : "dark"),
},
{ type: "sep" as const },
{
label: (
<span className="flex items-center gap-2">
用户文档
<Icon name="external-link" size={11} className="ops-faint" />
</span>
),
icon: <Icon name="book-open" size={14} />,
onClick: () => window.open("/docs", "_blank", "noopener,noreferrer"),
},
{
label: (
<span className="flex items-center gap-2">
官网与价格
<Icon name="external-link" size={11} className="ops-faint" />
</span>
),
icon: <Icon name="globe" size={14} />,
onClick: () => window.open("/home", "_blank", "noopener,noreferrer"),
},
{ type: "sep" as const },
{
label: "重置工作区…",
icon: <Icon name="trash" size={14} />,
danger: true,
onClick: () =>
setDialog({
type: "confirm",
title: "重置工作区",
message: "将恢复到初始示例数据,当前集合 / 环境 / 历史会被覆盖(建议先在「导出」中生成全量备份)。确定继续吗?",
action: () => useWorkspace.getState().resetWorkspace(),
}),
},
];
return (
<header
className="ops-panel ops-divider flex h-12 shrink-0 items-center justify-between gap-3 px-4"
@@ -179,14 +235,29 @@ export function TopBar() {
>
<Icon name="book-open" size={14} /> 文档
</a>
<button className="ops-btn ops-btn-ghost" onClick={() => setDialog({ type: "settings" })} title="设置">
<button
className="ops-btn ops-btn-ghost"
onClick={() => setDialog({ type: "settings" })}
title="设置"
>
<Icon name="settings" size={15} />
</button>
<button
className="ops-btn ops-btn-ghost"
title="更多"
onClick={(e) => {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setMoreMenu({ x: rect.right, y: rect.bottom + 4 });
}}
>
<Icon name="ellipsis" size={16} />
</button>
<span className={cx("ops-chip")} style={{ padding: "1px 8px", fontSize: 10 }} title="请求通道">
{transportKind}
</span>
</div>
<ContextMenu
menu={moreMenu ? { x: moreMenu.x, y: moreMenu.y, items: moreItems } : null}
onClose={() => setMoreMenu(null)}
/>
<ContextMenu
menu={newMenu ? { x: newMenu.x, y: newMenu.y, items: newItems } : null}
onClose={() => setNewMenu(null)}