mock数据分组管理
This commit is contained in:
parent
b1e4d2d7e7
commit
31095c967e
333
admin.html
333
admin.html
@ -123,16 +123,27 @@
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Mock 数据配置" name="mocks">
|
||||
<el-tab-pane label="数据配置" name="mocks">
|
||||
<el-card class="section-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<strong>Mock 数据配置</strong>
|
||||
<strong>数据配置(mock)</strong>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<el-select size="small" v-model="mockGroupFilter" clearable placeholder="按分组筛选" style="width:160px;">
|
||||
<el-option label="全部" :value="null"></el-option>
|
||||
<el-option label="未分组" :value="-1"></el-option>
|
||||
<el-option v-for="g in mockGroups" :key="g.id" :label="g.name" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
<el-input size="small" v-model="mockFileSearch" placeholder="搜索别名或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
|
||||
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
||||
<el-button size="mini" @click="openMockGroupDialog">分组管理</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="pagedMockFiles" border>
|
||||
<el-table-column label="分组" width="140">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ getGroupName(scope.row.groupId) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="别名" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.alias || "-" }}</span>
|
||||
@ -313,19 +324,37 @@
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Mock 文件">
|
||||
<el-select
|
||||
v-model="routeDialog.form.filePath"
|
||||
filterable
|
||||
placeholder="请选择 mock 文件"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in mockFiles"
|
||||
:key="item.filePath"
|
||||
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
|
||||
:value="item.filePath"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<el-select
|
||||
v-model="routeDialog.form.groupFilter"
|
||||
clearable
|
||||
placeholder="选择分组"
|
||||
style="width: 180px;"
|
||||
@change="onRouteDialogGroupChange"
|
||||
>
|
||||
<el-option label="全部" :value="null"></el-option>
|
||||
<el-option label="无分组" :value="-1"></el-option>
|
||||
<el-option
|
||||
v-for="g in mockGroups"
|
||||
:key="g.id"
|
||||
:label="g.name"
|
||||
:value="g.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="routeDialog.form.filePath"
|
||||
filterable
|
||||
placeholder="请选择 mock 文件"
|
||||
style="flex: 1;"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in routeDialogMockFiles"
|
||||
:key="item.filePath"
|
||||
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
|
||||
:value="item.filePath"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="返回状态码">
|
||||
<el-select
|
||||
@ -369,6 +398,12 @@
|
||||
placeholder="可选:用于展示,支持任意文本"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组">
|
||||
<el-select v-model="mockFileDialog.form.groupId" clearable placeholder="选择分组" style="width: 100%;">
|
||||
<el-option label="无分组" :value="null"></el-option>
|
||||
<el-option v-for="g in mockGroups" :key="g.id" :label="g.name" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Mock 文件路径">
|
||||
<el-input
|
||||
v-model="mockFileDialog.form.fileName"
|
||||
@ -435,6 +470,67 @@
|
||||
<el-button type="primary" @click="submitBatchImport">导入</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="分组管理"
|
||||
:visible.sync="mockGroupDialog.visible"
|
||||
width="600px"
|
||||
>
|
||||
<div style="margin-bottom:12px;">
|
||||
<el-button size="mini" type="primary" @click="addMockGroup">新增分组</el-button>
|
||||
</div>
|
||||
<el-table :data="mockGroups" border size="small" style="width: 100%;">
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column label="分组名称" min-width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="mockGroupDialog.editingId === scope.row.id"
|
||||
v-model="mockGroupDialog.form.name"
|
||||
size="small"
|
||||
@blur="saveMockGroupEdit(scope.row)"
|
||||
@keyup.enter.native="saveMockGroupEdit(scope.row)"
|
||||
></el-input>
|
||||
<span v-else>{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件数" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ getGroupFileCount(scope.row.id) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" plain @click="startEditMockGroup(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="danger" @click="confirmDeleteGroup(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="删除分组确认"
|
||||
:visible.sync="deleteGroupDialog.visible"
|
||||
width="500px"
|
||||
>
|
||||
<div style="margin-bottom:16px;">
|
||||
<p>分组「{{ deleteGroupDialog.groupName }}」下有 <strong>{{ deleteGroupDialog.fileCount }}</strong> 个 mock 文件。</p>
|
||||
<p>请选择对这些文件的处理方式:</p>
|
||||
</div>
|
||||
<el-radio-group v-model="deleteGroupDialog.action" style="display:flex;flex-direction:column;gap:12px;">
|
||||
<el-radio label="reassign">转移到其他分组</el-radio>
|
||||
<el-radio label="unassign">取消分组(设为未分组)</el-radio>
|
||||
<el-radio label="delete">删除全部文件</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="deleteGroupDialog.action === 'reassign'" style="margin-top:12px;">
|
||||
<el-select v-model="deleteGroupDialog.targetGroupId" placeholder="选择目标分组" style="width:100%;">
|
||||
<el-option v-for="g in mockGroups.filter(function(g){return g.id !== deleteGroupDialog.groupId})" :key="g.id" :label="g.name" :value="g.id"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="deleteGroupDialog.visible = false">取消</el-button>
|
||||
<el-button type="danger" @click="submitDeleteGroup">确认删除</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -459,6 +555,22 @@
|
||||
input: "",
|
||||
},
|
||||
mockFiles: [],
|
||||
mockGroups: [],
|
||||
mockGroupFilter: null,
|
||||
mockGroupDialog: {
|
||||
visible: false,
|
||||
mode: "create",
|
||||
editingId: null,
|
||||
form: { name: "" },
|
||||
},
|
||||
deleteGroupDialog: {
|
||||
visible: false,
|
||||
groupId: null,
|
||||
groupName: "",
|
||||
fileCount: 0,
|
||||
action: "",
|
||||
targetGroupId: null,
|
||||
},
|
||||
contentTypeOptions: [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
@ -515,6 +627,7 @@
|
||||
filePath: "",
|
||||
statusCode: 200,
|
||||
enabled: true,
|
||||
groupFilter: null,
|
||||
},
|
||||
},
|
||||
mockFileDialog: {
|
||||
@ -531,11 +644,13 @@
|
||||
watch: {
|
||||
routeSearch: function () { this.routePagination.currentPage = 1; },
|
||||
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
|
||||
mockGroupFilter: function () { this.mockFilePagination.currentPage = 1; },
|
||||
apiSearch: function () { this.apiPagination.currentPage = 1; },
|
||||
},
|
||||
created: async function () {
|
||||
await this.loadApiList();
|
||||
await this.loadMockFiles();
|
||||
await this.loadMockGroups();
|
||||
await this.loadConfig();
|
||||
},
|
||||
methods: {
|
||||
@ -648,6 +763,7 @@
|
||||
filePath: "",
|
||||
statusCode: 200,
|
||||
enabled: true,
|
||||
groupFilter: null,
|
||||
};
|
||||
},
|
||||
isApiListLocked: function () {
|
||||
@ -663,6 +779,10 @@
|
||||
this.routeDialog.form.route = selected.route;
|
||||
this.routeDialog.form.apiName = selected.name;
|
||||
},
|
||||
onRouteDialogGroupChange: function () {
|
||||
// 切换分组时清空已选文件
|
||||
this.routeDialog.form.filePath = "";
|
||||
},
|
||||
openRouteDialogForCreate: function () {
|
||||
this.routeDialog.mode = "create";
|
||||
this.routeDialog.editingIndex = -1;
|
||||
@ -678,6 +798,8 @@
|
||||
this.routeDialog.mode = "edit";
|
||||
this.routeDialog.editingIndex = actualIndex;
|
||||
var matched = this.findApiByRoute(item.route || "");
|
||||
var mockFile = this.mockFiles.find(function (f) { return f.filePath === item.filePath; });
|
||||
var groupFilter = mockFile && mockFile.groupId ? mockFile.groupId : null;
|
||||
this.routeDialog.form = {
|
||||
apiName: matched ? matched.name : item.apiName || "",
|
||||
selectedApiRoute: matched ? matched.route : "",
|
||||
@ -686,6 +808,7 @@
|
||||
filePath: item.filePath || "mock/",
|
||||
statusCode: this.normalizeStatusCode(item.statusCode),
|
||||
enabled: item.enabled !== false,
|
||||
groupFilter: groupFilter,
|
||||
};
|
||||
this.routeDialog.visible = true;
|
||||
},
|
||||
@ -694,6 +817,7 @@
|
||||
fileName: "",
|
||||
alias: "",
|
||||
content: "",
|
||||
groupId: null,
|
||||
};
|
||||
},
|
||||
openMockFileDialogForCreate: function () {
|
||||
@ -709,6 +833,7 @@
|
||||
fileName: name,
|
||||
alias: String(item.alias || ""),
|
||||
content: String(item.content || ""),
|
||||
groupId: item.groupId || null,
|
||||
};
|
||||
this.mockFileDialog.visible = true;
|
||||
},
|
||||
@ -745,6 +870,7 @@
|
||||
filePath: filePath,
|
||||
alias: this.mockFileDialog.form.alias,
|
||||
content: this.mockFileDialog.form.content,
|
||||
groupId: this.mockFileDialog.form.groupId,
|
||||
}),
|
||||
});
|
||||
var data = await resp.json();
|
||||
@ -1010,6 +1136,148 @@
|
||||
this.batchImportDialog.visible = false;
|
||||
await this.loadApiList();
|
||||
},
|
||||
getGroupName: function (groupId) {
|
||||
if (!groupId) return "未分组";
|
||||
var group = this.mockGroups.find(function (g) { return g.id === groupId; });
|
||||
return group ? group.name : "未分组";
|
||||
},
|
||||
getGroupFileCount: function (groupId) {
|
||||
return this.mockFiles.filter(function (f) { return f.groupId === groupId; }).length;
|
||||
},
|
||||
loadMockGroups: async function () {
|
||||
try {
|
||||
var resp = await fetch("/__mock-groups");
|
||||
var data = await resp.json();
|
||||
if (!resp.ok || data.success === false) {
|
||||
throw new Error(data.error || "加载分组失败");
|
||||
}
|
||||
this.mockGroups = Array.isArray(data.list) ? data.list : [];
|
||||
} catch (err) {
|
||||
this.mockGroups = [];
|
||||
this.$message.error("加载分组失败: " + err.message);
|
||||
}
|
||||
},
|
||||
openMockGroupDialog: function () {
|
||||
this.mockGroupDialog.editingId = null;
|
||||
this.mockGroupDialog.form.name = "";
|
||||
this.mockGroupDialog.visible = true;
|
||||
},
|
||||
addMockGroup: async function () {
|
||||
var self = this;
|
||||
this.$prompt("请输入分组名称", "新增分组", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: "分组名称不能为空",
|
||||
}).then(async function (res) {
|
||||
var name = (res.value || "").trim();
|
||||
try {
|
||||
var resp = await fetch("/__mock-groups", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: name }),
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (!resp.ok || data.success === false) {
|
||||
throw new Error(data.error || "新增分组失败");
|
||||
}
|
||||
self.$message.success("分组已创建");
|
||||
await self.loadMockGroups();
|
||||
} catch (err) {
|
||||
self.$message.error("新增分组失败: " + err.message);
|
||||
}
|
||||
}).catch(function () {});
|
||||
},
|
||||
startEditMockGroup: function (group) {
|
||||
this.mockGroupDialog.editingId = group.id;
|
||||
this.mockGroupDialog.form.name = group.name;
|
||||
},
|
||||
saveMockGroupEdit: async function (group) {
|
||||
var newName = (this.mockGroupDialog.form.name || "").trim();
|
||||
this.mockGroupDialog.editingId = null;
|
||||
if (!newName || newName === group.name) return;
|
||||
try {
|
||||
var resp = await fetch("/__mock-groups", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: group.id, name: newName }),
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (!resp.ok || data.success === false) {
|
||||
throw new Error(data.error || "修改分组失败");
|
||||
}
|
||||
this.$message.success("分组已修改");
|
||||
await this.loadMockGroups();
|
||||
} catch (err) {
|
||||
this.$message.error("修改分组失败: " + err.message);
|
||||
}
|
||||
},
|
||||
confirmDeleteGroup: async function (group) {
|
||||
var fileCount = this.getGroupFileCount(group.id);
|
||||
if (fileCount === 0) {
|
||||
// 无文件,直接删除
|
||||
try {
|
||||
var resp = await fetch("/__mock-groups", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: group.id }),
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (!resp.ok || data.success === false) {
|
||||
throw new Error(data.error || "删除分组失败");
|
||||
}
|
||||
this.$message.success("分组已删除");
|
||||
await this.loadMockGroups();
|
||||
} catch (err) {
|
||||
this.$message.error("删除分组失败: " + err.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 有文件,弹出确认对话框
|
||||
this.deleteGroupDialog.groupId = group.id;
|
||||
this.deleteGroupDialog.groupName = group.name;
|
||||
this.deleteGroupDialog.fileCount = fileCount;
|
||||
this.deleteGroupDialog.action = "";
|
||||
this.deleteGroupDialog.targetGroupId = null;
|
||||
this.deleteGroupDialog.visible = true;
|
||||
},
|
||||
submitDeleteGroup: async function () {
|
||||
var action = this.deleteGroupDialog.action;
|
||||
if (!action) {
|
||||
this.$message.error("请选择处理方式");
|
||||
return;
|
||||
}
|
||||
if (action === "reassign" && !this.deleteGroupDialog.targetGroupId) {
|
||||
this.$message.error("请选择目标分组");
|
||||
return;
|
||||
}
|
||||
var payload = {
|
||||
id: this.deleteGroupDialog.groupId,
|
||||
action: action === "unassign" ? "reassign" : action,
|
||||
};
|
||||
if (action === "reassign") {
|
||||
payload.targetGroupId = this.deleteGroupDialog.targetGroupId;
|
||||
} else if (action === "unassign") {
|
||||
payload.targetGroupId = null;
|
||||
}
|
||||
try {
|
||||
var resp = await fetch("/__mock-groups", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (!resp.ok || data.success === false) {
|
||||
throw new Error(data.error || "删除分组失败");
|
||||
}
|
||||
this.$message.success("分组已删除");
|
||||
this.deleteGroupDialog.visible = false;
|
||||
await this.loadMockGroups();
|
||||
await this.loadMockFiles();
|
||||
} catch (err) {
|
||||
this.$message.error("删除分组失败: " + err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredRoutes: function () {
|
||||
@ -1030,13 +1298,24 @@
|
||||
},
|
||||
filteredMockFiles: function () {
|
||||
var q = (this.mockFileSearch || "").toLowerCase().trim();
|
||||
if (!q) return this.mockFiles;
|
||||
return this.mockFiles.filter(function (item) {
|
||||
return (
|
||||
(item.filePath || "").toLowerCase().indexOf(q) !== -1 ||
|
||||
(item.alias || "").toLowerCase().indexOf(q) !== -1
|
||||
);
|
||||
});
|
||||
var groupFilter = this.mockGroupFilter;
|
||||
var list = this.mockFiles;
|
||||
if (groupFilter !== null && groupFilter !== undefined) {
|
||||
if (groupFilter === -1) {
|
||||
list = list.filter(function (item) { return !item.groupId; });
|
||||
} else {
|
||||
list = list.filter(function (item) { return item.groupId === groupFilter; });
|
||||
}
|
||||
}
|
||||
if (q) {
|
||||
list = list.filter(function (item) {
|
||||
return (
|
||||
(item.filePath || "").toLowerCase().indexOf(q) !== -1 ||
|
||||
(item.alias || "").toLowerCase().indexOf(q) !== -1
|
||||
);
|
||||
});
|
||||
}
|
||||
return list;
|
||||
},
|
||||
filteredApiList: function () {
|
||||
var q = (this.apiSearch || "").toLowerCase().trim();
|
||||
@ -1057,6 +1336,16 @@
|
||||
pagedApiList: function () {
|
||||
return this.getPagedData(this.filteredApiList, this.apiPagination);
|
||||
},
|
||||
routeDialogMockFiles: function () {
|
||||
var groupFilter = this.routeDialog.form.groupFilter;
|
||||
if (groupFilter === null || groupFilter === undefined) {
|
||||
return this.mockFiles;
|
||||
}
|
||||
if (groupFilter === -1) {
|
||||
return this.mockFiles.filter(function (f) { return !f.groupId; });
|
||||
}
|
||||
return this.mockFiles.filter(function (f) { return f.groupId === groupFilter; });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -9,6 +9,13 @@ import {
|
||||
insertApiItemToDb,
|
||||
updateApiItemInDb,
|
||||
deleteApiItemFromDb,
|
||||
loadMockGroups,
|
||||
insertMockGroup,
|
||||
updateMockGroup,
|
||||
deleteMockGroup,
|
||||
countMockFilesByGroup,
|
||||
updateMockFilesGroupId,
|
||||
deleteMockFilesByGroup,
|
||||
} from "./db";
|
||||
import {
|
||||
loadMockFiles,
|
||||
@ -393,9 +400,10 @@ async function handleMockFiles(
|
||||
|
||||
if (clientReq.method === "POST") {
|
||||
const content = String(body.content || "");
|
||||
const groupId = body.groupId != null ? Number(body.groupId) : null;
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, "utf-8");
|
||||
await upsertMockFilePathToDb(normalizedPath, alias);
|
||||
await upsertMockFilePathToDb(normalizedPath, alias, groupId);
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
@ -432,6 +440,113 @@ async function handleMockFiles(
|
||||
});
|
||||
}
|
||||
|
||||
async function handleMockGroups(
|
||||
clientReq: http.IncomingMessage,
|
||||
clientRes: http.ServerResponse,
|
||||
): Promise<void> {
|
||||
if (
|
||||
clientReq.method !== "GET" &&
|
||||
clientReq.method !== "POST" &&
|
||||
clientReq.method !== "PUT" &&
|
||||
clientReq.method !== "DELETE"
|
||||
) {
|
||||
clientRes.writeHead(405, {
|
||||
"Content-Type": "application/json",
|
||||
Allow: "GET, POST, PUT, DELETE",
|
||||
});
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Method Not Allowed",
|
||||
allow: ["GET, POST, PUT, DELETE"],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientReq.method === "GET") {
|
||||
const list = await loadMockGroups();
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(JSON.stringify({ success: true, list }));
|
||||
return;
|
||||
}
|
||||
|
||||
readBody(clientReq)
|
||||
.then(async (bodyText) => {
|
||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||
|
||||
if (clientReq.method === "POST") {
|
||||
const name = String(body.name || "").trim();
|
||||
if (!name) {
|
||||
throw new Error("分组名称不能为空");
|
||||
}
|
||||
const id = await insertMockGroup(name);
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(JSON.stringify({ success: true, id, name }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientReq.method === "PUT") {
|
||||
const id = Number(body.id);
|
||||
const name = String(body.name || "").trim();
|
||||
if (!id || !name) {
|
||||
throw new Error("id 和 name 不能为空");
|
||||
}
|
||||
await updateMockGroup(id, name);
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(JSON.stringify({ success: true, id, name }));
|
||||
return;
|
||||
}
|
||||
|
||||
// DELETE
|
||||
const id = Number(body.id);
|
||||
if (!id) {
|
||||
throw new Error("id 不能为空");
|
||||
}
|
||||
const fileCount = await countMockFilesByGroup(id);
|
||||
const action = String(body.action || "").trim();
|
||||
|
||||
if (fileCount > 0 && action === "reassign") {
|
||||
const targetGroupId = body.targetGroupId != null ? Number(body.targetGroupId) : null;
|
||||
await updateMockFilesGroupId(id, targetGroupId);
|
||||
} else if (fileCount > 0 && action === "delete") {
|
||||
const deletedFiles = await deleteMockFilesByGroup(id);
|
||||
// 删除磁盘上的文件
|
||||
for (const file of deletedFiles) {
|
||||
const fullPath = path.join(__dirname, "..", file.file_path);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
fs.unlinkSync(fullPath);
|
||||
}
|
||||
}
|
||||
} else if (fileCount > 0) {
|
||||
// 有文件但未指定 action,返回需要确认的信息
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
needConfirm: true,
|
||||
fileCount,
|
||||
message: `该分组下有 ${fileCount} 个 mock 文件,请指定操作`,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteMockGroup(id);
|
||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||
clientRes.end(JSON.stringify({ success: true, id }));
|
||||
})
|
||||
.catch((error) => {
|
||||
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||
clientRes.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAdminPage(
|
||||
clientReq: http.IncomingMessage,
|
||||
clientRes: http.ServerResponse,
|
||||
@ -476,6 +591,9 @@ export async function dispatchAdmin(
|
||||
case "/__mock-files":
|
||||
await handleMockFiles(clientReq, clientRes);
|
||||
return true;
|
||||
case "/__mock-groups":
|
||||
await handleMockGroups(clientReq, clientRes);
|
||||
return true;
|
||||
case "/__admin":
|
||||
await handleAdminPage(clientReq, clientRes);
|
||||
return true;
|
||||
|
||||
83
src/db.ts
83
src/db.ts
@ -7,6 +7,7 @@ import type {
|
||||
ApiListItem,
|
||||
AppConfig,
|
||||
MockFileRow,
|
||||
MockGroupRow,
|
||||
RouteApiNameMap,
|
||||
RouteConfig,
|
||||
RouteEnabledMap,
|
||||
@ -100,6 +101,12 @@ export async function initDatabase(): Promise<void> {
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS mock_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
)
|
||||
`);
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS mock_files (
|
||||
file_path TEXT PRIMARY KEY,
|
||||
@ -111,6 +118,11 @@ export async function initDatabase(): Promise<void> {
|
||||
).catch(() => {
|
||||
// ignore when column already exists
|
||||
});
|
||||
await dbRun(
|
||||
"ALTER TABLE mock_files ADD COLUMN group_id INTEGER",
|
||||
).catch(() => {
|
||||
// ignore when column already exists
|
||||
});
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS server_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
@ -211,8 +223,16 @@ export async function deleteApiItemFromDb(route: string): Promise<void> {
|
||||
export async function upsertMockFilePathToDb(
|
||||
filePath: string,
|
||||
alias?: string,
|
||||
groupId?: number | null,
|
||||
): Promise<void> {
|
||||
if (typeof alias === "string") {
|
||||
if (typeof groupId !== "undefined") {
|
||||
await dbRun(
|
||||
"INSERT INTO mock_files(file_path, alias, group_id) VALUES(?, ?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias, group_id = excluded.group_id",
|
||||
[filePath, alias, groupId ?? null],
|
||||
);
|
||||
return;
|
||||
}
|
||||
await dbRun(
|
||||
"INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
|
||||
[filePath, alias],
|
||||
@ -230,14 +250,75 @@ export async function removeMockFilePathFromDb(filePath: string): Promise<void>
|
||||
|
||||
export async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> {
|
||||
const rows = await dbAll<MockFileRow>(
|
||||
"SELECT file_path, alias FROM mock_files ORDER BY file_path ASC",
|
||||
"SELECT file_path, alias, group_id FROM mock_files ORDER BY file_path ASC",
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
file_path: row.file_path,
|
||||
alias: String(row.alias || ""),
|
||||
group_id: row.group_id ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
// ── mock_groups CRUD ──
|
||||
|
||||
export async function loadMockGroups(): Promise<MockGroupRow[]> {
|
||||
const rows = await dbAll<MockGroupRow>(
|
||||
"SELECT id, name FROM mock_groups ORDER BY id ASC",
|
||||
);
|
||||
return rows.map((row) => ({ id: row.id, name: row.name }));
|
||||
}
|
||||
|
||||
export async function insertMockGroup(name: string): Promise<number> {
|
||||
await dbRun("INSERT INTO mock_groups(name) VALUES(?)", [name]);
|
||||
const row = await dbAll<{ id: number }>(
|
||||
"SELECT id FROM mock_groups WHERE name = ?",
|
||||
[name],
|
||||
);
|
||||
return row[0]?.id ?? 0;
|
||||
}
|
||||
|
||||
export async function updateMockGroup(
|
||||
id: number,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
await dbRun("UPDATE mock_groups SET name = ? WHERE id = ?", [name, id]);
|
||||
}
|
||||
|
||||
export async function deleteMockGroup(id: number): Promise<void> {
|
||||
await dbRun("DELETE FROM mock_groups WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
export async function countMockFilesByGroup(
|
||||
groupId: number,
|
||||
): Promise<number> {
|
||||
const rows = await dbAll<{ cnt: number }>(
|
||||
"SELECT COUNT(*) AS cnt FROM mock_files WHERE group_id = ?",
|
||||
[groupId],
|
||||
);
|
||||
return rows[0]?.cnt ?? 0;
|
||||
}
|
||||
|
||||
export async function updateMockFilesGroupId(
|
||||
oldGroupId: number,
|
||||
newGroupId: number | null,
|
||||
): Promise<void> {
|
||||
await dbRun("UPDATE mock_files SET group_id = ? WHERE group_id = ?", [
|
||||
newGroupId,
|
||||
oldGroupId,
|
||||
]);
|
||||
}
|
||||
|
||||
export async function deleteMockFilesByGroup(
|
||||
groupId: number,
|
||||
): Promise<MockFileRow[]> {
|
||||
const rows = await dbAll<MockFileRow>(
|
||||
"SELECT file_path FROM mock_files WHERE group_id = ?",
|
||||
[groupId],
|
||||
);
|
||||
await dbRun("DELETE FROM mock_files WHERE group_id = ?", [groupId]);
|
||||
return rows.map((row) => ({ file_path: row.file_path, alias: "", group_id: null }));
|
||||
}
|
||||
|
||||
const DEFAULT_SERVER_CONFIG: AppConfig = {
|
||||
cacheConfig: true,
|
||||
reloadOnChange: true,
|
||||
|
||||
@ -63,7 +63,7 @@ export async function loadMockFiles(): Promise<MockFileItem[]> {
|
||||
for (const filePath of fsFiles) {
|
||||
await upsertMockFilePathToDb(filePath);
|
||||
}
|
||||
files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" }));
|
||||
files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "", group_id: null }));
|
||||
}
|
||||
|
||||
const result: MockFileItem[] = [];
|
||||
@ -75,6 +75,7 @@ export async function loadMockFiles(): Promise<MockFileItem[]> {
|
||||
filePath,
|
||||
alias: String(item.alias || ""),
|
||||
content: fs.readFileSync(fullPath, "utf-8"),
|
||||
groupId: item.group_id ?? null,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
||||
@ -35,6 +35,7 @@ export interface MockFileItem {
|
||||
filePath: string;
|
||||
alias: string;
|
||||
content: string;
|
||||
groupId: number | null;
|
||||
}
|
||||
|
||||
export interface RouteRow {
|
||||
@ -53,7 +54,13 @@ export interface RouteApiNameMap {
|
||||
[route: string]: string;
|
||||
}
|
||||
|
||||
export interface MockGroupRow {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MockFileRow {
|
||||
file_path: string;
|
||||
alias: string;
|
||||
group_id: number | null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user