mock数据分组管理
This commit is contained in:
parent
b1e4d2d7e7
commit
31095c967e
333
admin.html
333
admin.html
@ -123,16 +123,27 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="Mock 数据配置" name="mocks">
|
<el-tab-pane label="数据配置" name="mocks">
|
||||||
<el-card class="section-card">
|
<el-card class="section-card">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
<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;">
|
<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-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" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
||||||
|
<el-button size="mini" @click="openMockGroupDialog">分组管理</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="pagedMockFiles" border>
|
<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">
|
<el-table-column label="别名" min-width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ scope.row.alias || "-" }}</span>
|
<span>{{ scope.row.alias || "-" }}</span>
|
||||||
@ -313,19 +324,37 @@
|
|||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Mock 文件">
|
<el-form-item label="Mock 文件">
|
||||||
<el-select
|
<div style="display:flex;gap:8px;">
|
||||||
v-model="routeDialog.form.filePath"
|
<el-select
|
||||||
filterable
|
v-model="routeDialog.form.groupFilter"
|
||||||
placeholder="请选择 mock 文件"
|
clearable
|
||||||
style="width: 100%;"
|
placeholder="选择分组"
|
||||||
>
|
style="width: 180px;"
|
||||||
<el-option
|
@change="onRouteDialogGroupChange"
|
||||||
v-for="item in mockFiles"
|
>
|
||||||
:key="item.filePath"
|
<el-option label="全部" :value="null"></el-option>
|
||||||
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
|
<el-option label="无分组" :value="-1"></el-option>
|
||||||
:value="item.filePath"
|
<el-option
|
||||||
></el-option>
|
v-for="g in mockGroups"
|
||||||
</el-select>
|
: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>
|
||||||
<el-form-item label="返回状态码">
|
<el-form-item label="返回状态码">
|
||||||
<el-select
|
<el-select
|
||||||
@ -369,6 +398,12 @@
|
|||||||
placeholder="可选:用于展示,支持任意文本"
|
placeholder="可选:用于展示,支持任意文本"
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</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-form-item label="Mock 文件路径">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="mockFileDialog.form.fileName"
|
v-model="mockFileDialog.form.fileName"
|
||||||
@ -435,6 +470,67 @@
|
|||||||
<el-button type="primary" @click="submitBatchImport">导入</el-button>
|
<el-button type="primary" @click="submitBatchImport">导入</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -459,6 +555,22 @@
|
|||||||
input: "",
|
input: "",
|
||||||
},
|
},
|
||||||
mockFiles: [],
|
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: [
|
contentTypeOptions: [
|
||||||
"application/json",
|
"application/json",
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
@ -515,6 +627,7 @@
|
|||||||
filePath: "",
|
filePath: "",
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
groupFilter: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mockFileDialog: {
|
mockFileDialog: {
|
||||||
@ -531,11 +644,13 @@
|
|||||||
watch: {
|
watch: {
|
||||||
routeSearch: function () { this.routePagination.currentPage = 1; },
|
routeSearch: function () { this.routePagination.currentPage = 1; },
|
||||||
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
|
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
|
||||||
|
mockGroupFilter: function () { this.mockFilePagination.currentPage = 1; },
|
||||||
apiSearch: function () { this.apiPagination.currentPage = 1; },
|
apiSearch: function () { this.apiPagination.currentPage = 1; },
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.loadApiList();
|
await this.loadApiList();
|
||||||
await this.loadMockFiles();
|
await this.loadMockFiles();
|
||||||
|
await this.loadMockGroups();
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -648,6 +763,7 @@
|
|||||||
filePath: "",
|
filePath: "",
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
groupFilter: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isApiListLocked: function () {
|
isApiListLocked: function () {
|
||||||
@ -663,6 +779,10 @@
|
|||||||
this.routeDialog.form.route = selected.route;
|
this.routeDialog.form.route = selected.route;
|
||||||
this.routeDialog.form.apiName = selected.name;
|
this.routeDialog.form.apiName = selected.name;
|
||||||
},
|
},
|
||||||
|
onRouteDialogGroupChange: function () {
|
||||||
|
// 切换分组时清空已选文件
|
||||||
|
this.routeDialog.form.filePath = "";
|
||||||
|
},
|
||||||
openRouteDialogForCreate: function () {
|
openRouteDialogForCreate: function () {
|
||||||
this.routeDialog.mode = "create";
|
this.routeDialog.mode = "create";
|
||||||
this.routeDialog.editingIndex = -1;
|
this.routeDialog.editingIndex = -1;
|
||||||
@ -678,6 +798,8 @@
|
|||||||
this.routeDialog.mode = "edit";
|
this.routeDialog.mode = "edit";
|
||||||
this.routeDialog.editingIndex = actualIndex;
|
this.routeDialog.editingIndex = actualIndex;
|
||||||
var matched = this.findApiByRoute(item.route || "");
|
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 = {
|
this.routeDialog.form = {
|
||||||
apiName: matched ? matched.name : item.apiName || "",
|
apiName: matched ? matched.name : item.apiName || "",
|
||||||
selectedApiRoute: matched ? matched.route : "",
|
selectedApiRoute: matched ? matched.route : "",
|
||||||
@ -686,6 +808,7 @@
|
|||||||
filePath: item.filePath || "mock/",
|
filePath: item.filePath || "mock/",
|
||||||
statusCode: this.normalizeStatusCode(item.statusCode),
|
statusCode: this.normalizeStatusCode(item.statusCode),
|
||||||
enabled: item.enabled !== false,
|
enabled: item.enabled !== false,
|
||||||
|
groupFilter: groupFilter,
|
||||||
};
|
};
|
||||||
this.routeDialog.visible = true;
|
this.routeDialog.visible = true;
|
||||||
},
|
},
|
||||||
@ -694,6 +817,7 @@
|
|||||||
fileName: "",
|
fileName: "",
|
||||||
alias: "",
|
alias: "",
|
||||||
content: "",
|
content: "",
|
||||||
|
groupId: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
openMockFileDialogForCreate: function () {
|
openMockFileDialogForCreate: function () {
|
||||||
@ -709,6 +833,7 @@
|
|||||||
fileName: name,
|
fileName: name,
|
||||||
alias: String(item.alias || ""),
|
alias: String(item.alias || ""),
|
||||||
content: String(item.content || ""),
|
content: String(item.content || ""),
|
||||||
|
groupId: item.groupId || null,
|
||||||
};
|
};
|
||||||
this.mockFileDialog.visible = true;
|
this.mockFileDialog.visible = true;
|
||||||
},
|
},
|
||||||
@ -745,6 +870,7 @@
|
|||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
alias: this.mockFileDialog.form.alias,
|
alias: this.mockFileDialog.form.alias,
|
||||||
content: this.mockFileDialog.form.content,
|
content: this.mockFileDialog.form.content,
|
||||||
|
groupId: this.mockFileDialog.form.groupId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
@ -1010,6 +1136,148 @@
|
|||||||
this.batchImportDialog.visible = false;
|
this.batchImportDialog.visible = false;
|
||||||
await this.loadApiList();
|
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: {
|
computed: {
|
||||||
filteredRoutes: function () {
|
filteredRoutes: function () {
|
||||||
@ -1030,13 +1298,24 @@
|
|||||||
},
|
},
|
||||||
filteredMockFiles: function () {
|
filteredMockFiles: function () {
|
||||||
var q = (this.mockFileSearch || "").toLowerCase().trim();
|
var q = (this.mockFileSearch || "").toLowerCase().trim();
|
||||||
if (!q) return this.mockFiles;
|
var groupFilter = this.mockGroupFilter;
|
||||||
return this.mockFiles.filter(function (item) {
|
var list = this.mockFiles;
|
||||||
return (
|
if (groupFilter !== null && groupFilter !== undefined) {
|
||||||
(item.filePath || "").toLowerCase().indexOf(q) !== -1 ||
|
if (groupFilter === -1) {
|
||||||
(item.alias || "").toLowerCase().indexOf(q) !== -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 () {
|
filteredApiList: function () {
|
||||||
var q = (this.apiSearch || "").toLowerCase().trim();
|
var q = (this.apiSearch || "").toLowerCase().trim();
|
||||||
@ -1057,6 +1336,16 @@
|
|||||||
pagedApiList: function () {
|
pagedApiList: function () {
|
||||||
return this.getPagedData(this.filteredApiList, this.apiPagination);
|
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>
|
</script>
|
||||||
|
|||||||
@ -9,6 +9,13 @@ import {
|
|||||||
insertApiItemToDb,
|
insertApiItemToDb,
|
||||||
updateApiItemInDb,
|
updateApiItemInDb,
|
||||||
deleteApiItemFromDb,
|
deleteApiItemFromDb,
|
||||||
|
loadMockGroups,
|
||||||
|
insertMockGroup,
|
||||||
|
updateMockGroup,
|
||||||
|
deleteMockGroup,
|
||||||
|
countMockFilesByGroup,
|
||||||
|
updateMockFilesGroupId,
|
||||||
|
deleteMockFilesByGroup,
|
||||||
} from "./db";
|
} from "./db";
|
||||||
import {
|
import {
|
||||||
loadMockFiles,
|
loadMockFiles,
|
||||||
@ -393,9 +400,10 @@ async function handleMockFiles(
|
|||||||
|
|
||||||
if (clientReq.method === "POST") {
|
if (clientReq.method === "POST") {
|
||||||
const content = String(body.content || "");
|
const content = String(body.content || "");
|
||||||
|
const groupId = body.groupId != null ? Number(body.groupId) : null;
|
||||||
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, alias);
|
await upsertMockFilePathToDb(normalizedPath, alias, groupId);
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
clientRes.end(
|
clientRes.end(
|
||||||
JSON.stringify({
|
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(
|
async function handleAdminPage(
|
||||||
clientReq: http.IncomingMessage,
|
clientReq: http.IncomingMessage,
|
||||||
clientRes: http.ServerResponse,
|
clientRes: http.ServerResponse,
|
||||||
@ -476,6 +591,9 @@ export async function dispatchAdmin(
|
|||||||
case "/__mock-files":
|
case "/__mock-files":
|
||||||
await handleMockFiles(clientReq, clientRes);
|
await handleMockFiles(clientReq, clientRes);
|
||||||
return true;
|
return true;
|
||||||
|
case "/__mock-groups":
|
||||||
|
await handleMockGroups(clientReq, clientRes);
|
||||||
|
return true;
|
||||||
case "/__admin":
|
case "/__admin":
|
||||||
await handleAdminPage(clientReq, clientRes);
|
await handleAdminPage(clientReq, clientRes);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
83
src/db.ts
83
src/db.ts
@ -7,6 +7,7 @@ import type {
|
|||||||
ApiListItem,
|
ApiListItem,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
MockFileRow,
|
MockFileRow,
|
||||||
|
MockGroupRow,
|
||||||
RouteApiNameMap,
|
RouteApiNameMap,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
RouteEnabledMap,
|
RouteEnabledMap,
|
||||||
@ -100,6 +101,12 @@ export async function initDatabase(): Promise<void> {
|
|||||||
name TEXT NOT NULL
|
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(`
|
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,
|
||||||
@ -111,6 +118,11 @@ export async function initDatabase(): Promise<void> {
|
|||||||
).catch(() => {
|
).catch(() => {
|
||||||
// ignore when column already exists
|
// ignore when column already exists
|
||||||
});
|
});
|
||||||
|
await dbRun(
|
||||||
|
"ALTER TABLE mock_files ADD COLUMN group_id INTEGER",
|
||||||
|
).catch(() => {
|
||||||
|
// ignore when column already exists
|
||||||
|
});
|
||||||
await dbRun(`
|
await dbRun(`
|
||||||
CREATE TABLE IF NOT EXISTS server_config (
|
CREATE TABLE IF NOT EXISTS server_config (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@ -211,8 +223,16 @@ export async function deleteApiItemFromDb(route: string): Promise<void> {
|
|||||||
export async function upsertMockFilePathToDb(
|
export async function upsertMockFilePathToDb(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
alias?: string,
|
alias?: string,
|
||||||
|
groupId?: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (typeof alias === "string") {
|
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(
|
await dbRun(
|
||||||
"INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
|
"INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias",
|
||||||
[filePath, alias],
|
[filePath, alias],
|
||||||
@ -230,14 +250,75 @@ export async function removeMockFilePathFromDb(filePath: string): Promise<void>
|
|||||||
|
|
||||||
export async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> {
|
export async function loadMockFilePathsFromDb(): Promise<MockFileRow[]> {
|
||||||
const rows = await dbAll<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) => ({
|
return rows.map((row) => ({
|
||||||
file_path: row.file_path,
|
file_path: row.file_path,
|
||||||
alias: String(row.alias || ""),
|
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 = {
|
const DEFAULT_SERVER_CONFIG: AppConfig = {
|
||||||
cacheConfig: true,
|
cacheConfig: true,
|
||||||
reloadOnChange: true,
|
reloadOnChange: true,
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export async function loadMockFiles(): Promise<MockFileItem[]> {
|
|||||||
for (const filePath of fsFiles) {
|
for (const filePath of fsFiles) {
|
||||||
await upsertMockFilePathToDb(filePath);
|
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[] = [];
|
const result: MockFileItem[] = [];
|
||||||
@ -75,6 +75,7 @@ export async function loadMockFiles(): Promise<MockFileItem[]> {
|
|||||||
filePath,
|
filePath,
|
||||||
alias: String(item.alias || ""),
|
alias: String(item.alias || ""),
|
||||||
content: fs.readFileSync(fullPath, "utf-8"),
|
content: fs.readFileSync(fullPath, "utf-8"),
|
||||||
|
groupId: item.group_id ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export interface MockFileItem {
|
|||||||
filePath: string;
|
filePath: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
groupId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteRow {
|
export interface RouteRow {
|
||||||
@ -53,7 +54,13 @@ export interface RouteApiNameMap {
|
|||||||
[route: string]: string;
|
[route: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MockGroupRow {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MockFileRow {
|
export interface MockFileRow {
|
||||||
file_path: string;
|
file_path: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
|
group_id: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user