feat 增加mock数据别名
This commit is contained in:
parent
47cd3b338e
commit
7469ded0de
19
admin.html
19
admin.html
@ -137,6 +137,11 @@
|
|||||||
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="pagedMockFiles" border>
|
<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">
|
<el-table-column label="Mock 文件路径" min-width="320">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.filePath }}</span>
|
<span>{{ scope.row.filePath }}</span>
|
||||||
@ -266,7 +271,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in mockFiles"
|
v-for="item in mockFiles"
|
||||||
:key="item.filePath"
|
:key="item.filePath"
|
||||||
:label="item.filePath"
|
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
|
||||||
:value="item.filePath"
|
:value="item.filePath"
|
||||||
></el-option>
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
@ -307,10 +312,16 @@
|
|||||||
width="760px"
|
width="760px"
|
||||||
>
|
>
|
||||||
<el-form :model="mockFileDialog.form" label-width="180px">
|
<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-form-item label="Mock 文件路径">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="mockFileDialog.form.filePath"
|
v-model="mockFileDialog.form.filePath"
|
||||||
placeholder="mock/new-api.json"
|
placeholder="mock/test123.json(路径仅英文和数字)"
|
||||||
:disabled="mockFileDialog.mode === 'edit'"
|
:disabled="mockFileDialog.mode === 'edit'"
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -398,6 +409,7 @@
|
|||||||
mode: "create",
|
mode: "create",
|
||||||
form: {
|
form: {
|
||||||
filePath: "mock/",
|
filePath: "mock/",
|
||||||
|
alias: "",
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -548,6 +560,7 @@
|
|||||||
getDefaultMockFileForm: function () {
|
getDefaultMockFileForm: function () {
|
||||||
return {
|
return {
|
||||||
filePath: "mock/",
|
filePath: "mock/",
|
||||||
|
alias: "",
|
||||||
content: "",
|
content: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -560,6 +573,7 @@
|
|||||||
this.mockFileDialog.mode = "edit";
|
this.mockFileDialog.mode = "edit";
|
||||||
this.mockFileDialog.form = {
|
this.mockFileDialog.form = {
|
||||||
filePath: item.filePath || "mock/",
|
filePath: item.filePath || "mock/",
|
||||||
|
alias: String(item.alias || ""),
|
||||||
content: String(item.content || ""),
|
content: String(item.content || ""),
|
||||||
};
|
};
|
||||||
this.mockFileDialog.visible = true;
|
this.mockFileDialog.visible = true;
|
||||||
@ -589,6 +603,7 @@
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filePath: this.mockFileDialog.form.filePath,
|
filePath: this.mockFileDialog.form.filePath,
|
||||||
|
alias: this.mockFileDialog.form.alias,
|
||||||
content: this.mockFileDialog.form.content,
|
content: this.mockFileDialog.form.content,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"routes": {
|
"routes": {
|
||||||
"/api1/account/bind": "mock/basic-error.json",
|
"#/api1/account/bind": "mock/basic-error.json",
|
||||||
"/api2/admin/banner/list": "mock/banner.txt",
|
"/api2/admin/banner/list": "mock/banner.txt",
|
||||||
"/api2/home/ad/list": "mock/basic-error.json"
|
"/api2/home/ad/list": "mock/basic-error.json"
|
||||||
},
|
},
|
||||||
"routeStatuses": {
|
"routeStatuses": {
|
||||||
"/api1/account/bind": 500,
|
"#/api1/account/bind": 500,
|
||||||
"/api2/admin/banner/list": 200,
|
"/api2/admin/banner/list": 200,
|
||||||
"/api2/home/ad/list": 200
|
"/api2/home/ad/list": 200
|
||||||
},
|
},
|
||||||
|
|||||||
53
index.api.ts
53
index.api.ts
@ -49,6 +49,7 @@ interface ApiListItem {
|
|||||||
|
|
||||||
interface MockFileItem {
|
interface MockFileItem {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
alias: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ interface RouteRow {
|
|||||||
|
|
||||||
interface MockFileRow {
|
interface MockFileRow {
|
||||||
file_path: string;
|
file_path: string;
|
||||||
|
alias: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储当前的路由配置
|
// 存储当前的路由配置
|
||||||
@ -186,9 +188,15 @@ async function initDatabase(): Promise<void> {
|
|||||||
`);
|
`);
|
||||||
await dbRun(`
|
await dbRun(`
|
||||||
CREATE TABLE IF NOT EXISTS mock_files (
|
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 列表中的系统文件
|
// 清理不该出现在 mock 列表中的系统文件
|
||||||
await dbRun(
|
await dbRun(
|
||||||
"DELETE FROM mock_files WHERE file_path = ? OR file_path LIKE ?",
|
"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 }));
|
return rows.map((row) => ({ route: row.route, name: row.name }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertMockFilePathToDb(filePath: string): Promise<void> {
|
async function upsertMockFilePathToDb(filePath: string, alias?: string): Promise<void> {
|
||||||
await dbRun("INSERT OR IGNORE INTO mock_files(file_path) VALUES(?)", [filePath]);
|
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> {
|
async function removeMockFilePathFromDb(filePath: string): Promise<void> {
|
||||||
await dbRun("DELETE FROM mock_files WHERE file_path = ?", [filePath]);
|
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>(
|
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)) {
|
if (path.isAbsolute(relative)) {
|
||||||
throw new Error("filePath must be a relative path");
|
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;
|
return relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,8 +478,9 @@ function walkMockFiles(dir: string, baseDir: string, result: string[]): void {
|
|||||||
async function loadMockFiles(): Promise<MockFileItem[]> {
|
async function loadMockFiles(): Promise<MockFileItem[]> {
|
||||||
let files = await loadMockFilePathsFromDb();
|
let files = await loadMockFilePathsFromDb();
|
||||||
files = files.filter(
|
files = files.filter(
|
||||||
(filePath) =>
|
(item) =>
|
||||||
filePath !== "mock/api-list.json" && !filePath.endsWith(".sqlite3"),
|
item.file_path !== "mock/api-list.json" &&
|
||||||
|
!item.file_path.endsWith(".sqlite3"),
|
||||||
);
|
);
|
||||||
if (files.length === 0 && fs.existsSync(MOCK_DIR)) {
|
if (files.length === 0 && fs.existsSync(MOCK_DIR)) {
|
||||||
const fsFiles: string[] = [];
|
const fsFiles: string[] = [];
|
||||||
@ -462,15 +489,17 @@ async function loadMockFiles(): Promise<MockFileItem[]> {
|
|||||||
for (const filePath of fsFiles) {
|
for (const filePath of fsFiles) {
|
||||||
await upsertMockFilePathToDb(filePath);
|
await upsertMockFilePathToDb(filePath);
|
||||||
}
|
}
|
||||||
files = fsFiles;
|
files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: MockFileItem[] = [];
|
const result: MockFileItem[] = [];
|
||||||
for (const filePath of files) {
|
for (const item of files) {
|
||||||
|
const filePath = item.file_path;
|
||||||
const fullPath = path.join(__dirname, filePath);
|
const fullPath = path.join(__dirname, filePath);
|
||||||
if (!fs.existsSync(fullPath)) continue;
|
if (!fs.existsSync(fullPath)) continue;
|
||||||
result.push({
|
result.push({
|
||||||
filePath,
|
filePath,
|
||||||
|
alias: String(item.alias || ""),
|
||||||
content: fs.readFileSync(fullPath, "utf-8"),
|
content: fs.readFileSync(fullPath, "utf-8"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -878,6 +907,7 @@ const proxyServer = http.createServer(async (clientReq, clientRes) => {
|
|||||||
.then(async (bodyText) => {
|
.then(async (bodyText) => {
|
||||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
const filePath = String(body.filePath || "").trim();
|
const filePath = String(body.filePath || "").trim();
|
||||||
|
const alias = String(body.alias || "");
|
||||||
const fullPath = resolveMockFullPath(filePath);
|
const fullPath = resolveMockFullPath(filePath);
|
||||||
const normalizedPath = normalizeMockFilePath(filePath);
|
const normalizedPath = normalizeMockFilePath(filePath);
|
||||||
|
|
||||||
@ -885,12 +915,13 @@ const proxyServer = http.createServer(async (clientReq, clientRes) => {
|
|||||||
const content = String(body.content || "");
|
const content = String(body.content || "");
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
fs.writeFileSync(fullPath, content, "utf-8");
|
fs.writeFileSync(fullPath, content, "utf-8");
|
||||||
await upsertMockFilePathToDb(normalizedPath);
|
await upsertMockFilePathToDb(normalizedPath, alias);
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
clientRes.end(
|
clientRes.end(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
filePath: normalizedPath,
|
filePath: normalizedPath,
|
||||||
|
alias,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user