feat 增加mock数据别名

This commit is contained in:
lenmotion 2026-04-24 10:25:30 +08:00
parent 47cd3b338e
commit 7469ded0de
3 changed files with 61 additions and 15 deletions

View File

@ -137,6 +137,11 @@
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
</div>
<el-table :data="pagedMockFiles" border>
<el-table-column label="别名" min-width="180">
<template slot-scope="scope">
<span>{{ scope.row.alias || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="Mock 文件路径" min-width="320">
<template slot-scope="scope">
<span>{{ scope.row.filePath }}</span>
@ -266,7 +271,7 @@
<el-option
v-for="item in mockFiles"
:key="item.filePath"
:label="item.filePath"
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
:value="item.filePath"
></el-option>
</el-select>
@ -307,10 +312,16 @@
width="760px"
>
<el-form :model="mockFileDialog.form" label-width="180px">
<el-form-item label="别名">
<el-input
v-model="mockFileDialog.form.alias"
placeholder="可选:用于展示,支持任意文本"
></el-input>
</el-form-item>
<el-form-item label="Mock 文件路径">
<el-input
v-model="mockFileDialog.form.filePath"
placeholder="mock/new-api.json"
placeholder="mock/test123.json路径仅英文和数字"
:disabled="mockFileDialog.mode === 'edit'"
></el-input>
</el-form-item>
@ -398,6 +409,7 @@
mode: "create",
form: {
filePath: "mock/",
alias: "",
content: "",
},
},
@ -548,6 +560,7 @@
getDefaultMockFileForm: function () {
return {
filePath: "mock/",
alias: "",
content: "",
};
},
@ -560,6 +573,7 @@
this.mockFileDialog.mode = "edit";
this.mockFileDialog.form = {
filePath: item.filePath || "mock/",
alias: String(item.alias || ""),
content: String(item.content || ""),
};
this.mockFileDialog.visible = true;
@ -589,6 +603,7 @@
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filePath: this.mockFileDialog.form.filePath,
alias: this.mockFileDialog.form.alias,
content: this.mockFileDialog.form.content,
}),
});

View File

@ -1,11 +1,11 @@
{
"routes": {
"/api1/account/bind": "mock/basic-error.json",
"#/api1/account/bind": "mock/basic-error.json",
"/api2/admin/banner/list": "mock/banner.txt",
"/api2/home/ad/list": "mock/basic-error.json"
},
"routeStatuses": {
"/api1/account/bind": 500,
"#/api1/account/bind": 500,
"/api2/admin/banner/list": 200,
"/api2/home/ad/list": 200
},

View File

@ -49,6 +49,7 @@ interface ApiListItem {
interface MockFileItem {
filePath: string;
alias: string;
content: string;
}
@ -60,6 +61,7 @@ interface RouteRow {
interface MockFileRow {
file_path: string;
alias: string;
}
// 存储当前的路由配置
@ -186,9 +188,15 @@ async function initDatabase(): Promise<void> {
`);
await dbRun(`
CREATE TABLE IF NOT EXISTS mock_files (
file_path TEXT PRIMARY KEY
file_path TEXT PRIMARY KEY,
alias TEXT NOT NULL DEFAULT ''
)
`);
await dbRun("ALTER TABLE mock_files ADD COLUMN alias TEXT NOT NULL DEFAULT ''").catch(
() => {
// ignore when column already exists
},
);
// 清理不该出现在 mock 列表中的系统文件
await dbRun(
"DELETE FROM mock_files WHERE file_path = ? OR file_path LIKE ?",
@ -246,19 +254,31 @@ async function loadApiListFromDb(): Promise<ApiListItem[]> {
return rows.map((row) => ({ route: row.route, name: row.name }));
}
async function upsertMockFilePathToDb(filePath: string): Promise<void> {
await dbRun("INSERT OR IGNORE INTO mock_files(file_path) VALUES(?)", [filePath]);
async function upsertMockFilePathToDb(filePath: string, alias?: string): Promise<void> {
if (typeof alias === "string") {
await dbRun(
"INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
[filePath, alias],
);
return;
}
await dbRun("INSERT OR IGNORE INTO mock_files(file_path, alias) VALUES(?, '')", [
filePath,
]);
}
async function removeMockFilePathFromDb(filePath: string): Promise<void> {
await dbRun("DELETE FROM mock_files WHERE file_path = ?", [filePath]);
}
async function loadMockFilePathsFromDb(): Promise<string[]> {
async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> {
const rows = await dbAll<MockFileRow>(
"SELECT file_path FROM mock_files ORDER BY file_path ASC",
"SELECT file_path, alias FROM mock_files ORDER BY file_path ASC",
);
return rows.map((row) => row.file_path);
return rows.map((row) => ({
file_path: row.file_path,
alias: String(row.alias || ""),
}));
}
// 加载配置文件
@ -417,6 +437,12 @@ function normalizeMockFilePath(filePath: string): string {
if (path.isAbsolute(relative)) {
throw new Error("filePath must be a relative path");
}
const innerPath = relative.replace(/^mock\//, "");
if (!/^[A-Za-z0-9/.]+$/.test(innerPath)) {
throw new Error(
"filePath only allows English letters, numbers, '/', and '.'",
);
}
return relative;
}
@ -452,8 +478,9 @@ function walkMockFiles(dir: string, baseDir: string, result: string[]): void {
async function loadMockFiles(): Promise<MockFileItem[]> {
let files = await loadMockFilePathsFromDb();
files = files.filter(
(filePath) =>
filePath !== "mock/api-list.json" && !filePath.endsWith(".sqlite3"),
(item) =>
item.file_path !== "mock/api-list.json" &&
!item.file_path.endsWith(".sqlite3"),
);
if (files.length === 0 && fs.existsSync(MOCK_DIR)) {
const fsFiles: string[] = [];
@ -462,15 +489,17 @@ async function loadMockFiles(): Promise<MockFileItem[]> {
for (const filePath of fsFiles) {
await upsertMockFilePathToDb(filePath);
}
files = fsFiles;
files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" }));
}
const result: MockFileItem[] = [];
for (const filePath of files) {
for (const item of files) {
const filePath = item.file_path;
const fullPath = path.join(__dirname, filePath);
if (!fs.existsSync(fullPath)) continue;
result.push({
filePath,
alias: String(item.alias || ""),
content: fs.readFileSync(fullPath, "utf-8"),
});
}
@ -878,6 +907,7 @@ const proxyServer = http.createServer(async (clientReq, clientRes) => {
.then(async (bodyText) => {
const body = bodyText ? JSON.parse(bodyText) : {};
const filePath = String(body.filePath || "").trim();
const alias = String(body.alias || "");
const fullPath = resolveMockFullPath(filePath);
const normalizedPath = normalizeMockFilePath(filePath);
@ -885,12 +915,13 @@ const proxyServer = http.createServer(async (clientReq, clientRes) => {
const content = String(body.content || "");
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, "utf-8");
await upsertMockFilePathToDb(normalizedPath);
await upsertMockFilePathToDb(normalizedPath, alias);
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(
JSON.stringify({
success: true,
filePath: normalizedPath,
alias,
}),
);
return;