- 环境新增「前置 URL」字段;环境管理弹窗独立配置区 + 说明
- URL 输入改为一体胶囊:前缀块(环境徽标 + 域名,点击进环境管理)+ 分隔线 + 相对路径输入
- 路径以 / 开头(或空)自动拼接当前环境前置 URL;完整 https:// 地址不受影响
- 光标 / 全选只作用于路径,不再误编辑 {{baseUrl}} 文本
- 覆盖 HTTP / GraphQL / SSE 编辑器与发送链路(含自动化测试执行器、代码生成用完整地址)
- 存量数据一次性迁移:{{baseUrl}}/path → /path,baseUrl 变量 → 环境字段(启动 / 导入 / 备份恢复均生效)
- 顶栏切换环境即换前缀;docs 06-env 增前置 URL 章节
- 验证:48 前端用例全绿;本地 E2E(迁移/发送 200/环境切换前缀联动/胶囊几何对齐)+ 近景视觉评审通过
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
/**
|
||
* URL 输入(前置 URL 模式):
|
||
* 当前环境配置了前置 URL 且值为相对路径(/ 开头或空)时,
|
||
* 展示「前缀块 + 分隔线 + 路径输入」的一体胶囊 —— 借鉴 Apifox 前置 URL。
|
||
* 路径输入只编辑相对部分,光标 / 全选不会碰到服务地址。
|
||
*/
|
||
import type { KeyboardEvent } from "react";
|
||
import { useWorkspace } from "../workspaceStore";
|
||
import { cx } from "./common";
|
||
import { EnvBadge } from "./EnvBadge";
|
||
import { VarHintInput } from "./VarHintInput";
|
||
|
||
export function useEnvBaseUrl(): { baseUrl: string; envId: string | null; envName: string } {
|
||
const envId = useWorkspace((s) => s.activeEnvironmentId);
|
||
const env = useWorkspace((s) => (envId ? s.environments.find((e) => e.id === envId) : undefined));
|
||
return {
|
||
baseUrl: env?.baseUrl?.trim() ?? "",
|
||
envId: envId ?? null,
|
||
envName: env?.name ?? "",
|
||
};
|
||
}
|
||
|
||
/** 前置 URL 与相对路径拼接(供编辑器展示「实际地址」与代码生成复用) */
|
||
export function joinUrl(baseUrl: string, path: string): string {
|
||
const p = path.trim();
|
||
if (!baseUrl || (p !== "" && !p.startsWith("/"))) return path;
|
||
const b = baseUrl.replace(/\/+$/, "");
|
||
if (p === "") return `${b}/`;
|
||
return p.startsWith("/") ? `${b}${p}` : `${b}/${p}`;
|
||
}
|
||
|
||
export function UrlPathInput({
|
||
value,
|
||
onChange,
|
||
placeholder,
|
||
className,
|
||
onKeyDown,
|
||
ariaLabel,
|
||
}: {
|
||
value: string;
|
||
onChange: (v: string) => void;
|
||
placeholder?: string;
|
||
className?: string;
|
||
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
||
ariaLabel?: string;
|
||
}) {
|
||
const { baseUrl, envId, envName } = useEnvBaseUrl();
|
||
const setDialog = useWorkspace((s) => s.setDialog);
|
||
const usePrefix = !!baseUrl && (value === "" || value.trimStart().startsWith("/"));
|
||
|
||
if (!usePrefix) {
|
||
return (
|
||
<VarHintInput
|
||
className={className}
|
||
value={value}
|
||
onChange={onChange}
|
||
placeholder={placeholder}
|
||
onKeyDown={onKeyDown}
|
||
ariaLabel={ariaLabel}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// 展示 host(去协议),完整地址放 title
|
||
const display = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
||
|
||
return (
|
||
<div className={cx("url-pill", className)}>
|
||
<button
|
||
type="button"
|
||
className="url-pill-prefix"
|
||
title={`前置 URL(${envName || "当前环境"}):${baseUrl}\n点击管理环境`}
|
||
onClick={() => setDialog({ type: "environments" })}
|
||
>
|
||
{envId && <EnvBadge envId={envId} name={envName} size={14} />}
|
||
<span className="truncate">{display}</span>
|
||
</button>
|
||
<span className="url-pill-divider" />
|
||
<VarHintInput
|
||
className="url-pill-input"
|
||
value={value}
|
||
onChange={onChange}
|
||
placeholder={placeholder ?? "/path"}
|
||
onKeyDown={onKeyDown}
|
||
ariaLabel={ariaLabel}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|