feat: 增加mock配置
This commit is contained in:
parent
8d20792fc3
commit
7ae07a6fcb
571
admin.html
571
admin.html
@ -21,31 +21,163 @@
|
|||||||
.section-card {
|
.section-card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
.top-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1100;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.top-nav-inner {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.actions {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.small-text {
|
.small-text {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
.table-pagination {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="container">
|
<div id="app">
|
||||||
|
<div class="top-nav">
|
||||||
|
<div class="top-nav-inner">
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="saveConfig">保存配置到 config.json</el-button>
|
||||||
|
<el-button @click="reloadConfig">重载服务内存配置</el-button>
|
||||||
|
<el-button @click="loadConfig">刷新页面数据</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<el-tabs v-model="activeMainTab" class="section-card">
|
||||||
|
<el-tab-pane label="路由配置" name="routes">
|
||||||
|
<el-card class="section-card">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
|
<strong>路由配置(routes)</strong>
|
||||||
|
<el-button size="mini" type="primary" @click="openRouteDialogForCreate">新增路由</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="pagedRoutes" border>
|
||||||
|
<el-table-column label="接口名称" min-width="220">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.apiName || "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="请求路径" min-width="260">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.route }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Mock 文件路径" min-width="300">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.filePath }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="启用" width="90" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-switch v-model="scope.row.enabled"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="openRouteDialogForEdit(scope.$index)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="danger" @click="removeRoute(scope.$index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
class="table-pagination"
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:current-page="routePagination.currentPage"
|
||||||
|
:page-size="routePagination.pageSize"
|
||||||
|
:total="form.routes.length"
|
||||||
|
@current-change="handleRoutePageChange"
|
||||||
|
></el-pagination>
|
||||||
|
<div class="small-text" style="margin-top:8px;">
|
||||||
|
说明:关闭“启用”后会以 # 注释路由,不参与 mock 命中;切换后请点击“保存配置到 config.json”生效到文件。
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="Mock 数据配置" name="mocks">
|
||||||
|
<el-card class="section-card">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
|
<strong>Mock 数据配置</strong>
|
||||||
|
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="pagedMockFiles" border>
|
||||||
|
<el-table-column label="Mock 文件路径" min-width="320">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.filePath }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="内容预览" min-width="420">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ getPreviewText(scope.row.content) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="primary" plain @click="openMockFileDialogForEdit(scope.row)">修改</el-button>
|
||||||
|
<el-button size="mini" type="danger" @click="removeMockFile(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
class="table-pagination"
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:current-page="mockFilePagination.currentPage"
|
||||||
|
:page-size="mockFilePagination.pageSize"
|
||||||
|
:total="mockFiles.length"
|
||||||
|
@current-change="handleMockFilePageChange"
|
||||||
|
></el-pagination>
|
||||||
|
</el-card>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="基础配置" name="basic">
|
||||||
<el-card class="section-card">
|
<el-card class="section-card">
|
||||||
<div slot="header"><strong>基础配置</strong></div>
|
|
||||||
<el-form :model="form.config" label-width="180px">
|
<el-form :model="form.config" label-width="180px">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="Mock 开关">
|
<el-form-item label="Mock 开关">
|
||||||
<el-switch v-model="form.config.mockEnabled"></el-switch>
|
<el-switch v-model="form.config.mockEnabled"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="自动重载配置文件">
|
<el-form-item label="自动重载配置文件">
|
||||||
<el-switch v-model="form.config.reloadOnChange"></el-switch>
|
<el-switch v-model="form.config.reloadOnChange"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="默认响应 Content-Type">
|
<el-form-item label="默认响应 Content-Type">
|
||||||
<el-input v-model="form.config.defaultContentType"></el-input>
|
<el-input v-model="form.config.defaultContentType"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="本地代理端口">
|
<el-form-item label="本地代理端口">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="form.config.proxyPort"
|
v-model="form.config.proxyPort"
|
||||||
@ -53,9 +185,13 @@
|
|||||||
:max="65535"
|
:max="65535"
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="目标主机">
|
<el-form-item label="目标主机">
|
||||||
<el-input v-model="form.config.targetHost"></el-input>
|
<el-input v-model="form.config.targetHost"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="目标端口">
|
<el-form-item label="目标端口">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="form.config.targetPort"
|
v-model="form.config.targetPort"
|
||||||
@ -63,92 +199,110 @@
|
|||||||
:max="65535"
|
:max="65535"
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="目标 HTTPS">
|
<el-form-item label="目标 HTTPS">
|
||||||
<el-switch v-model="form.config.targetHttps"></el-switch>
|
<el-switch v-model="form.config.targetHttps"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
<el-card class="section-card">
|
<el-dialog
|
||||||
<div slot="header" style="display:flex;justify-content:space-between;align-items:center;">
|
:title="routeDialog.mode === 'edit' ? '修改路由' : '新增路由'"
|
||||||
<strong>路由配置(routes)</strong>
|
:visible.sync="routeDialog.visible"
|
||||||
<el-button size="mini" type="primary" @click="addRouteRow">新增一行</el-button>
|
width="760px"
|
||||||
</div>
|
>
|
||||||
<el-table :data="form.routes" border>
|
<el-form :model="routeDialog.form" label-width="180px">
|
||||||
<el-table-column label="请求路径" min-width="260">
|
<el-form-item label="预置接口">
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-input v-model="scope.row.route" placeholder="/api/example"></el-input>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="Mock 文件路径" min-width="300">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-input
|
|
||||||
v-model="scope.row.filePath"
|
|
||||||
placeholder="mock/example.json"
|
|
||||||
></el-input>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="120">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button size="mini" type="danger" @click="removeRoute(scope.$index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="small-text" style="margin-top:8px;">
|
|
||||||
说明:如果路由以 # 开头,会作为注释保留,不参与 mock 命中。
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="section-card">
|
|
||||||
<div slot="header"><strong>动态新增接口配置文件</strong></div>
|
|
||||||
<el-form :model="newRoute" label-width="180px">
|
|
||||||
<el-form-item label="内容模板">
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="newRoute.template"
|
v-model="routeDialog.form.selectedApiRoute"
|
||||||
placeholder="请选择模板"
|
filterable
|
||||||
@change="applyTemplate"
|
clearable
|
||||||
|
placeholder="可选:从 mock/api-list.json 选择"
|
||||||
|
@change="onSelectApiRoute"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
>
|
>
|
||||||
<el-option label="自定义内容" value="custom"></el-option>
|
<el-option
|
||||||
<el-option label="基础失败模板" value="basicError"></el-option>
|
v-for="item in apiList"
|
||||||
|
:key="item.route"
|
||||||
|
:label="item.name + ' (' + item.route + ')'"
|
||||||
|
:value="item.route"
|
||||||
|
></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="请求路径">
|
<el-form-item label="接口名称">
|
||||||
<el-input v-model="newRoute.route" placeholder="/api/new/mock"></el-input>
|
<el-input
|
||||||
|
v-model="routeDialog.form.apiName"
|
||||||
|
placeholder="例如:获取用户信息"
|
||||||
|
:disabled="isApiListLocked()"
|
||||||
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="请求路径">
|
||||||
|
<el-input
|
||||||
|
v-model="routeDialog.form.route"
|
||||||
|
placeholder="/api/new/mock"
|
||||||
|
:disabled="isApiListLocked()"
|
||||||
|
></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.filePath"
|
||||||
|
:value="item.filePath"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否启用 Mock">
|
||||||
|
<el-switch v-model="routeDialog.form.enabled"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="routeDialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitRouteDialog">
|
||||||
|
{{ routeDialog.mode === "edit" ? "保存修改" : "创建路由+文件" }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
:title="mockFileDialog.mode === 'edit' ? '修改 Mock 文件' : '新增 Mock 文件'"
|
||||||
|
:visible.sync="mockFileDialog.visible"
|
||||||
|
width="760px"
|
||||||
|
>
|
||||||
|
<el-form :model="mockFileDialog.form" label-width="180px">
|
||||||
<el-form-item label="Mock 文件路径">
|
<el-form-item label="Mock 文件路径">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="newRoute.filePath"
|
v-model="mockFileDialog.form.filePath"
|
||||||
placeholder="mock/new-api.json"
|
placeholder="mock/new-api.json"
|
||||||
:disabled="newRoute.template === 'basicError'"
|
:disabled="mockFileDialog.mode === 'edit'"
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="文件内容">
|
<el-form-item label="文件内容">
|
||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="12"
|
||||||
v-model="newRoute.fileContent"
|
v-model="mockFileDialog.form.content"
|
||||||
placeholder='{"code":0,"message":"ok"}'
|
placeholder='{"code":0,"message":"ok"}'
|
||||||
:disabled="newRoute.template === 'basicError'"
|
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="覆盖已存在文件">
|
|
||||||
<el-switch
|
|
||||||
v-model="newRoute.overwrite"
|
|
||||||
:disabled="newRoute.template === 'basicError'"
|
|
||||||
></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-button type="success" @click="createRouteAndFile">创建路由+文件</el-button>
|
<span slot="footer" class="dialog-footer">
|
||||||
</el-card>
|
<el-button @click="mockFileDialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitMockFileDialog">保存 Mock 文件</el-button>
|
||||||
<el-card>
|
</span>
|
||||||
<div class="actions">
|
</el-dialog>
|
||||||
<el-button type="primary" @click="saveConfig">保存配置到 config.json</el-button>
|
|
||||||
<el-button @click="reloadConfig">重载服务内存配置</el-button>
|
|
||||||
<el-button @click="loadConfig">刷新页面数据</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
@ -158,6 +312,17 @@
|
|||||||
el: "#app",
|
el: "#app",
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
activeMainTab: "routes",
|
||||||
|
apiList: [],
|
||||||
|
mockFiles: [],
|
||||||
|
routePagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
mockFilePagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
form: {
|
form: {
|
||||||
config: {
|
config: {
|
||||||
mockEnabled: true,
|
mockEnabled: true,
|
||||||
@ -171,22 +336,78 @@
|
|||||||
},
|
},
|
||||||
routes: [],
|
routes: [],
|
||||||
},
|
},
|
||||||
newRoute: {
|
routeDialog: {
|
||||||
template: "custom",
|
visible: false,
|
||||||
|
mode: "create",
|
||||||
|
editingIndex: -1,
|
||||||
|
form: {
|
||||||
|
apiName: "",
|
||||||
|
selectedApiRoute: "",
|
||||||
|
originalRoute: "",
|
||||||
|
originalRawRoute: "",
|
||||||
route: "",
|
route: "",
|
||||||
|
filePath: "",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockFileDialog: {
|
||||||
|
visible: false,
|
||||||
|
mode: "create",
|
||||||
|
form: {
|
||||||
filePath: "mock/",
|
filePath: "mock/",
|
||||||
fileContent: "",
|
content: "",
|
||||||
overwrite: false,
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created: function () {
|
created: async function () {
|
||||||
this.loadConfig();
|
await this.loadApiList();
|
||||||
|
await this.loadMockFiles();
|
||||||
|
await this.loadConfig();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getPreviewText: function (content) {
|
||||||
|
var text = String(content || "").replace(/\s+/g, " ").trim();
|
||||||
|
if (!text) return "(空文件)";
|
||||||
|
return text.length > 120 ? text.slice(0, 120) + "..." : text;
|
||||||
|
},
|
||||||
|
getPagedData: function (list, pagination) {
|
||||||
|
var source = Array.isArray(list) ? list : [];
|
||||||
|
var pageSize = pagination.pageSize || 10;
|
||||||
|
var currentPage = pagination.currentPage || 1;
|
||||||
|
var maxPage = Math.max(1, Math.ceil(source.length / pageSize));
|
||||||
|
if (currentPage > maxPage) {
|
||||||
|
currentPage = maxPage;
|
||||||
|
pagination.currentPage = currentPage;
|
||||||
|
}
|
||||||
|
var start = (currentPage - 1) * pageSize;
|
||||||
|
return source.slice(start, start + pageSize);
|
||||||
|
},
|
||||||
|
handleRoutePageChange: function (page) {
|
||||||
|
this.routePagination.currentPage = page;
|
||||||
|
},
|
||||||
|
handleMockFilePageChange: function (page) {
|
||||||
|
this.mockFilePagination.currentPage = page;
|
||||||
|
},
|
||||||
|
findApiByRoute: function (route) {
|
||||||
|
return (this.apiList || []).find(function (item) {
|
||||||
|
return item.route === route;
|
||||||
|
});
|
||||||
|
},
|
||||||
toRouteArray: function (routesObj) {
|
toRouteArray: function (routesObj) {
|
||||||
return Object.keys(routesObj || {}).map(function (route) {
|
var self = this;
|
||||||
return { route: route, filePath: routesObj[route] };
|
return Object.keys(routesObj || {}).map(function (rawRoute) {
|
||||||
|
var enabled = !rawRoute.startsWith("#");
|
||||||
|
var route = enabled ? rawRoute : rawRoute.replace(/^#+/, "");
|
||||||
|
var matched = self.findApiByRoute(route);
|
||||||
|
return {
|
||||||
|
rawRoute: rawRoute,
|
||||||
|
route: route,
|
||||||
|
filePath: routesObj[rawRoute],
|
||||||
|
apiName: matched ? matched.name : "",
|
||||||
|
selectedApiRoute: matched ? matched.route : "",
|
||||||
|
enabled: enabled,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toRouteObject: function (routeArray) {
|
toRouteObject: function (routeArray) {
|
||||||
@ -195,16 +416,162 @@
|
|||||||
var route = (item.route || "").trim();
|
var route = (item.route || "").trim();
|
||||||
var filePath = (item.filePath || "").trim();
|
var filePath = (item.filePath || "").trim();
|
||||||
if (route && filePath) {
|
if (route && filePath) {
|
||||||
obj[route] = filePath;
|
var routeKey = item.enabled === false ? "#" + route : route;
|
||||||
|
obj[routeKey] = filePath;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
addRouteRow: function () {
|
|
||||||
this.form.routes.push({ route: "", filePath: "" });
|
|
||||||
},
|
|
||||||
removeRoute: function (index) {
|
removeRoute: function (index) {
|
||||||
this.form.routes.splice(index, 1);
|
var actualIndex =
|
||||||
|
(this.routePagination.currentPage - 1) * this.routePagination.pageSize +
|
||||||
|
index;
|
||||||
|
this.form.routes.splice(actualIndex, 1);
|
||||||
|
},
|
||||||
|
getDefaultRouteForm: function () {
|
||||||
|
return {
|
||||||
|
apiName: "",
|
||||||
|
selectedApiRoute: "",
|
||||||
|
originalRoute: "",
|
||||||
|
originalRawRoute: "",
|
||||||
|
route: "",
|
||||||
|
filePath: "",
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isApiListLocked: function () {
|
||||||
|
return !!this.routeDialog.form.selectedApiRoute;
|
||||||
|
},
|
||||||
|
onSelectApiRoute: function (route) {
|
||||||
|
var selected = route ? this.findApiByRoute(route) : null;
|
||||||
|
if (!selected) {
|
||||||
|
this.routeDialog.form.selectedApiRoute = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.routeDialog.form.selectedApiRoute = selected.route;
|
||||||
|
this.routeDialog.form.route = selected.route;
|
||||||
|
this.routeDialog.form.apiName = selected.name;
|
||||||
|
},
|
||||||
|
openRouteDialogForCreate: function () {
|
||||||
|
this.routeDialog.mode = "create";
|
||||||
|
this.routeDialog.editingIndex = -1;
|
||||||
|
this.routeDialog.form = this.getDefaultRouteForm();
|
||||||
|
this.routeDialog.visible = true;
|
||||||
|
},
|
||||||
|
openRouteDialogForEdit: function (index) {
|
||||||
|
var actualIndex =
|
||||||
|
(this.routePagination.currentPage - 1) * this.routePagination.pageSize +
|
||||||
|
index;
|
||||||
|
var item =
|
||||||
|
this.form.routes[actualIndex] || { route: "", filePath: "mock/" };
|
||||||
|
this.routeDialog.mode = "edit";
|
||||||
|
this.routeDialog.editingIndex = actualIndex;
|
||||||
|
var matched = this.findApiByRoute(item.route || "");
|
||||||
|
this.routeDialog.form = {
|
||||||
|
apiName: matched ? matched.name : item.apiName || "",
|
||||||
|
selectedApiRoute: matched ? matched.route : "",
|
||||||
|
originalRoute: item.route || "",
|
||||||
|
originalRawRoute: item.rawRoute || item.route || "",
|
||||||
|
route: item.route || "",
|
||||||
|
filePath: item.filePath || "mock/",
|
||||||
|
enabled: item.enabled !== false,
|
||||||
|
};
|
||||||
|
this.routeDialog.visible = true;
|
||||||
|
},
|
||||||
|
getDefaultMockFileForm: function () {
|
||||||
|
return {
|
||||||
|
filePath: "mock/",
|
||||||
|
content: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
openMockFileDialogForCreate: function () {
|
||||||
|
this.mockFileDialog.mode = "create";
|
||||||
|
this.mockFileDialog.form = this.getDefaultMockFileForm();
|
||||||
|
this.mockFileDialog.visible = true;
|
||||||
|
},
|
||||||
|
openMockFileDialogForEdit: function (item) {
|
||||||
|
this.mockFileDialog.mode = "edit";
|
||||||
|
this.mockFileDialog.form = {
|
||||||
|
filePath: item.filePath || "mock/",
|
||||||
|
content: String(item.content || ""),
|
||||||
|
};
|
||||||
|
this.mockFileDialog.visible = true;
|
||||||
|
},
|
||||||
|
loadMockFiles: async function () {
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__mock-files");
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "加载 mock 文件失败");
|
||||||
|
}
|
||||||
|
this.mockFiles = Array.isArray(data.list) ? data.list : [];
|
||||||
|
this.mockFilePagination.currentPage = 1;
|
||||||
|
} catch (err) {
|
||||||
|
this.mockFiles = [];
|
||||||
|
this.$message.error("加载 mock 文件失败: " + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitMockFileDialog: async function () {
|
||||||
|
if (!this.mockFileDialog.form.filePath) {
|
||||||
|
this.$message.error("Mock 文件路径不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__mock-files", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
filePath: this.mockFileDialog.form.filePath,
|
||||||
|
content: this.mockFileDialog.form.content,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "保存 mock 文件失败");
|
||||||
|
}
|
||||||
|
this.$message.success("Mock 文件已保存");
|
||||||
|
this.mockFileDialog.visible = false;
|
||||||
|
await this.loadMockFiles();
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error("保存 mock 文件失败: " + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeMockFile: async function (item) {
|
||||||
|
try {
|
||||||
|
await this.$confirm(
|
||||||
|
"确认删除 " + item.filePath + " 吗?删除后引用该文件的路由需要重新选择。",
|
||||||
|
"提示",
|
||||||
|
{ type: "warning" },
|
||||||
|
);
|
||||||
|
var resp = await fetch("/__mock-files", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ filePath: item.filePath }),
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "删除 mock 文件失败");
|
||||||
|
}
|
||||||
|
this.$message.success("Mock 文件已删除");
|
||||||
|
await this.loadMockFiles();
|
||||||
|
} catch (err) {
|
||||||
|
if (err !== "cancel") {
|
||||||
|
this.$message.error("删除 mock 文件失败: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadApiList: async function () {
|
||||||
|
try {
|
||||||
|
var resp = await fetch("/__api-list");
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || "加载接口列表失败");
|
||||||
|
}
|
||||||
|
this.apiList = Array.isArray(data.list) ? data.list : [];
|
||||||
|
} catch (err) {
|
||||||
|
this.apiList = [];
|
||||||
|
this.$message.error("加载接口列表失败: " + err.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
loadConfig: async function () {
|
loadConfig: async function () {
|
||||||
try {
|
try {
|
||||||
@ -212,6 +579,7 @@
|
|||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
this.form.config = Object.assign({}, this.form.config, data.config || {});
|
this.form.config = Object.assign({}, this.form.config, data.config || {});
|
||||||
this.form.routes = this.toRouteArray(data.routes || {});
|
this.form.routes = this.toRouteArray(data.routes || {});
|
||||||
|
this.routePagination.currentPage = 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$message.error("加载配置失败: " + err.message);
|
this.$message.error("加载配置失败: " + err.message);
|
||||||
}
|
}
|
||||||
@ -249,42 +617,47 @@
|
|||||||
this.$message.error("重载失败: " + err.message);
|
this.$message.error("重载失败: " + err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createRouteAndFile: async function () {
|
submitRouteDialog: async function () {
|
||||||
|
if (!this.routeDialog.form.route) {
|
||||||
|
this.$message.error("请求路径不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.routeDialog.form.filePath) {
|
||||||
|
this.$message.error("请选择 Mock 文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var payload = Object.assign({}, this.routeDialog.form, {
|
||||||
|
useExistingFile: true,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
var resp = await fetch("/__routes", {
|
var resp = await fetch("/__routes", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(payload),
|
||||||
Object.assign({}, this.newRoute, {
|
|
||||||
useExistingFile: this.newRoute.template === "basicError",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (!resp.ok || data.success === false) {
|
if (!resp.ok || data.success === false) {
|
||||||
throw new Error(data.error || "创建失败");
|
throw new Error(data.error || "创建失败");
|
||||||
}
|
}
|
||||||
this.$message.success("接口配置文件创建成功");
|
this.$message.success(
|
||||||
this.newRoute.template = "custom";
|
this.routeDialog.mode === "edit"
|
||||||
this.newRoute.route = "";
|
? "路由修改成功"
|
||||||
this.newRoute.filePath = "mock/";
|
: "接口配置文件创建成功",
|
||||||
this.newRoute.fileContent = "";
|
);
|
||||||
this.newRoute.overwrite = false;
|
this.routeDialog.visible = false;
|
||||||
this.loadConfig();
|
await this.loadMockFiles();
|
||||||
|
await this.loadConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$message.error("创建失败: " + err.message);
|
this.$message.error("创建失败: " + err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyTemplate: function (template) {
|
},
|
||||||
if (template === "basicError") {
|
computed: {
|
||||||
this.newRoute.filePath = "mock/basic-error.json";
|
pagedRoutes: function () {
|
||||||
this.newRoute.fileContent =
|
return this.getPagedData(this.form.routes, this.routePagination);
|
||||||
'{"code": -1, "success": false, "msg":"失败"}';
|
},
|
||||||
this.newRoute.overwrite = false;
|
pagedMockFiles: function () {
|
||||||
return;
|
return this.getPagedData(this.mockFiles, this.mockFilePagination);
|
||||||
}
|
|
||||||
this.newRoute.filePath = "mock/";
|
|
||||||
this.newRoute.fileContent = "";
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
265
index.api.ts
265
index.api.ts
@ -6,6 +6,8 @@ import * as zlib from "zlib";
|
|||||||
|
|
||||||
// 配置文件路径
|
// 配置文件路径
|
||||||
const CONFIG_FILE = path.join(__dirname, "config.json");
|
const CONFIG_FILE = path.join(__dirname, "config.json");
|
||||||
|
const API_LIST_FILE = path.join(__dirname, "mock", "api-list.json");
|
||||||
|
const MOCK_DIR = path.join(__dirname, "mock");
|
||||||
|
|
||||||
// 定义类型
|
// 定义类型
|
||||||
interface RouteConfig {
|
interface RouteConfig {
|
||||||
@ -31,6 +33,16 @@ interface ConfigFile {
|
|||||||
config: AppConfig;
|
config: AppConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiListItem {
|
||||||
|
name: string;
|
||||||
|
route: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockFileItem {
|
||||||
|
filePath: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 存储当前的路由配置
|
// 存储当前的路由配置
|
||||||
let MOCK_ROUTES: RouteConfig = {};
|
let MOCK_ROUTES: RouteConfig = {};
|
||||||
let RAW_ROUTES: RouteConfig = {};
|
let RAW_ROUTES: RouteConfig = {};
|
||||||
@ -160,6 +172,93 @@ function saveConfigFile(nextConfig: ConfigFile): void {
|
|||||||
CONFIG = nextConfig.config || CONFIG;
|
CONFIG = nextConfig.config || CONFIG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadApiList(): ApiListItem[] {
|
||||||
|
if (!fs.existsSync(API_LIST_FILE)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const text = fs.readFileSync(API_LIST_FILE, "utf-8").trim();
|
||||||
|
if (!text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const seenRoutes = new Set<string>();
|
||||||
|
const list: ApiListItem[] = [];
|
||||||
|
for (const item of parsed) {
|
||||||
|
const route = String(item?.route || "").trim();
|
||||||
|
const name = String(item?.name || "").trim();
|
||||||
|
if (!route || !route.startsWith("/") || !name || seenRoutes.has(route)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenRoutes.add(route);
|
||||||
|
list.push({ name, route });
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ADMIN] 读取 api-list.json 失败:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMockFilePath(filePath: string): string {
|
||||||
|
const trimmed = filePath.trim().replace(/\\/g, "/");
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error("filePath is required");
|
||||||
|
}
|
||||||
|
const relative = trimmed.startsWith("mock/") ? trimmed : `mock/${trimmed}`;
|
||||||
|
if (path.isAbsolute(relative)) {
|
||||||
|
throw new Error("filePath must be a relative path");
|
||||||
|
}
|
||||||
|
return relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMockFullPath(relativePath: string): string {
|
||||||
|
const normalized = normalizeMockFilePath(relativePath);
|
||||||
|
const fullPath = path.resolve(__dirname, normalized);
|
||||||
|
const mockRoot = path.resolve(MOCK_DIR);
|
||||||
|
if (!fullPath.startsWith(mockRoot)) {
|
||||||
|
throw new Error("filePath is invalid");
|
||||||
|
}
|
||||||
|
if (path.basename(fullPath) === "api-list.json") {
|
||||||
|
throw new Error("api-list.json is read-only in this panel");
|
||||||
|
}
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkMockFiles(dir: string, baseDir: string, result: string[]): void {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkMockFiles(fullPath, baseDir, result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
if (entry.name === "api-list.json") continue;
|
||||||
|
const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/");
|
||||||
|
result.push(`mock/${relative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMockFiles(): MockFileItem[] {
|
||||||
|
if (!fs.existsSync(MOCK_DIR)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const files: string[] = [];
|
||||||
|
walkMockFiles(MOCK_DIR, MOCK_DIR, files);
|
||||||
|
files.sort((a, b) => a.localeCompare(b));
|
||||||
|
return files.map((filePath) => {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
content: fs.readFileSync(fullPath, "utf-8"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readBody(req: http.IncomingMessage): Promise<string> {
|
function readBody(req: http.IncomingMessage): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
@ -375,28 +474,48 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
|
|||||||
readBody(clientReq)
|
readBody(clientReq)
|
||||||
.then((bodyText) => {
|
.then((bodyText) => {
|
||||||
const body = bodyText ? JSON.parse(bodyText) : {};
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
const route = String(body.route || "").trim();
|
let route = String(body.route || "").trim();
|
||||||
const filePath = String(body.filePath || "").trim();
|
const filePath = String(body.filePath || "").trim();
|
||||||
const fileContent = String(body.fileContent || "");
|
const fileContent = String(body.fileContent || "");
|
||||||
const overwrite = body.overwrite === true;
|
const overwrite = body.overwrite === true;
|
||||||
const template = String(body.template || "").trim();
|
const template = String(body.template || "").trim();
|
||||||
|
const apiName = String(body.apiName || "").trim();
|
||||||
|
const selectedApiRoute = String(body.selectedApiRoute || "").trim();
|
||||||
|
const originalRoute = String(body.originalRoute || "").trim();
|
||||||
|
const originalRawRoute = String(body.originalRawRoute || "").trim();
|
||||||
|
const enabled = body.enabled !== false;
|
||||||
const useExistingFile =
|
const useExistingFile =
|
||||||
body.useExistingFile === true || template === "basicError";
|
body.useExistingFile === true || template === "basicError";
|
||||||
|
const apiList = loadApiList();
|
||||||
|
const selectedApi = selectedApiRoute
|
||||||
|
? apiList.find((item) => item.route === selectedApiRoute)
|
||||||
|
: undefined;
|
||||||
|
const originalApi = originalRoute
|
||||||
|
? apiList.find((item) => item.route === originalRoute)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// 来自 api-list 的接口路由不可在新增/修改时变更
|
||||||
|
if (selectedApi) {
|
||||||
|
if (route && route !== selectedApi.route) {
|
||||||
|
throw new Error("api-list route cannot be changed");
|
||||||
|
}
|
||||||
|
if (apiName && apiName !== selectedApi.name) {
|
||||||
|
throw new Error("api-list name cannot be changed");
|
||||||
|
}
|
||||||
|
route = selectedApi.route;
|
||||||
|
}
|
||||||
|
if (originalApi && route !== originalApi.route) {
|
||||||
|
throw new Error("api-list route cannot be changed");
|
||||||
|
}
|
||||||
|
if (originalApi && apiName && apiName !== originalApi.name) {
|
||||||
|
throw new Error("api-list name cannot be changed");
|
||||||
|
}
|
||||||
|
|
||||||
if (!route.startsWith("/")) {
|
if (!route.startsWith("/")) {
|
||||||
throw new Error("route must start with '/'");
|
throw new Error("route must start with '/'");
|
||||||
}
|
}
|
||||||
if (!filePath) {
|
const normalizedFilePath = normalizeMockFilePath(filePath);
|
||||||
throw new Error("filePath is required");
|
const fullPath = resolveMockFullPath(normalizedFilePath);
|
||||||
}
|
|
||||||
if (path.isAbsolute(filePath)) {
|
|
||||||
throw new Error("filePath must be a relative path");
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = path.join(__dirname, filePath);
|
|
||||||
if (!fullPath.startsWith(__dirname)) {
|
|
||||||
throw new Error("filePath is invalid");
|
|
||||||
}
|
|
||||||
if (useExistingFile) {
|
if (useExistingFile) {
|
||||||
if (!fs.existsSync(fullPath)) {
|
if (!fs.existsSync(fullPath)) {
|
||||||
throw new Error("mock file does not exist");
|
throw new Error("mock file does not exist");
|
||||||
@ -412,7 +531,21 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextConfig = buildCurrentConfigFile();
|
const nextConfig = buildCurrentConfigFile();
|
||||||
nextConfig.routes[route] = filePath;
|
const nextRouteKey = enabled ? route : `#${route}`;
|
||||||
|
const oldRouteCandidates = new Set<string>();
|
||||||
|
if (originalRawRoute) {
|
||||||
|
oldRouteCandidates.add(originalRawRoute);
|
||||||
|
}
|
||||||
|
if (originalRoute) {
|
||||||
|
oldRouteCandidates.add(originalRoute);
|
||||||
|
oldRouteCandidates.add(`#${originalRoute}`);
|
||||||
|
}
|
||||||
|
oldRouteCandidates.forEach((key) => {
|
||||||
|
if (key && key !== nextRouteKey) {
|
||||||
|
delete nextConfig.routes[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nextConfig.routes[nextRouteKey] = normalizedFilePath;
|
||||||
saveConfigFile(nextConfig);
|
saveConfigFile(nextConfig);
|
||||||
|
|
||||||
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
@ -421,7 +554,111 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
|
|||||||
success: true,
|
success: true,
|
||||||
message: "Route and mock file created successfully",
|
message: "Route and mock file created successfully",
|
||||||
route,
|
route,
|
||||||
filePath,
|
routeKey: nextRouteKey,
|
||||||
|
filePath: normalizedFilePath,
|
||||||
|
enabled,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientRes.writeHead(400, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__api-list") {
|
||||||
|
if (clientReq.method !== "GET") {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "GET",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["GET"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
list: loadApiList(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/__mock-files") {
|
||||||
|
if (
|
||||||
|
clientReq.method !== "GET" &&
|
||||||
|
clientReq.method !== "POST" &&
|
||||||
|
clientReq.method !== "DELETE"
|
||||||
|
) {
|
||||||
|
clientRes.writeHead(405, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Allow: "GET, POST, DELETE",
|
||||||
|
});
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Method Not Allowed",
|
||||||
|
allow: ["GET", "POST", "DELETE"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientReq.method === "GET") {
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
list: loadMockFiles(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBody(clientReq)
|
||||||
|
.then((bodyText) => {
|
||||||
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
||||||
|
const filePath = String(body.filePath || "").trim();
|
||||||
|
const fullPath = resolveMockFullPath(filePath);
|
||||||
|
const normalizedPath = normalizeMockFilePath(filePath);
|
||||||
|
|
||||||
|
if (clientReq.method === "POST") {
|
||||||
|
const content = String(body.content || "");
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, content, "utf-8");
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error("mock file does not exist");
|
||||||
|
}
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
clientRes.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
clientRes.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
filePath: normalizedPath,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
60
mock/api-list.json
Normal file
60
mock/api-list.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
[
|
||||||
|
{ "name": "登录/注册发送验证码", "route": "/api1/authentication/sms/send" },
|
||||||
|
{ "name": "绑定手机号发送验证码", "route": "/api1/authentication/bind/send" },
|
||||||
|
{ "name": "账号注销", "route": "/api1/account/revoke" },
|
||||||
|
{ "name": "绑定手机号", "route": "/api1/account/bind" },
|
||||||
|
{ "name": "认证授权刷新接口", "route": "/api1/oauth2/token" },
|
||||||
|
{ "name": "一键登录阿里云授权码", "route": "/api1/authentication/common" },
|
||||||
|
|
||||||
|
{ "name": "更新用户信息", "route": "/api2/user/update" },
|
||||||
|
{ "name": "获取用户信息", "route": "/api2/user" },
|
||||||
|
{ "name": "我的链接", "route": "/api2/link" },
|
||||||
|
{ "name": "获取字典类型", "route": "/api2/dict/types" },
|
||||||
|
{ "name": "获取字典项", "route": "/api2/dict/" },
|
||||||
|
{ "name": "获取设备列表", "route": "/api2/device/list" },
|
||||||
|
{ "name": "根据类型获取设备列表", "route": "/api2/device/type/list" },
|
||||||
|
{ "name": "查询历史设备列表", "route": "/api2/device/history/list" },
|
||||||
|
{ "name": "绑定设备", "route": "/api2/device/save" },
|
||||||
|
{ "name": "切换设备", "route": "/api2/device/switch" },
|
||||||
|
{ "name": "用机人主设备", "route": "/api2/device/master" },
|
||||||
|
{ "name": "解绑设备", "route": "/api2/device/unbind" },
|
||||||
|
{ "name": "获取设备信息", "route": "/api2/device/one" },
|
||||||
|
{ "name": "获取设备店铺", "route": "/api2/device/store" },
|
||||||
|
{ "name": "获取设备图片", "route": "/api2/device/bind/img" },
|
||||||
|
{ "name": "查询设备报告日期", "route": "/api2/device/report/date" },
|
||||||
|
{ "name": "查询是否有报告页", "route": "/api2/device/reports" },
|
||||||
|
|
||||||
|
{ "name": "查询使用教程列表", "route": "/api2/course/list" },
|
||||||
|
{ "name": "查询白脸教程列表", "route": "/api2/course/white/face/list" },
|
||||||
|
{ "name": "获取省份城市列表", "route": "/api2/sys/provinces" },
|
||||||
|
{ "name": "制氧机报告单天", "route": "/api2/oxygenerator/report/day" },
|
||||||
|
{ "name": "制氧机报告多天", "route": "/api2/oxygenerator/report/multi/day" },
|
||||||
|
{ "name": "血氧仪报告单天", "route": "/api2/oximeter/report/day" },
|
||||||
|
{ "name": "血氧仪报告多天", "route": "/api2/oximeter/report/multi/day" },
|
||||||
|
{ "name": "呼吸机报告单天", "route": "/api2/ventilator/report/day" },
|
||||||
|
{ "name": "呼吸机报告多天", "route": "/api2/ventilator/report/multi/day" },
|
||||||
|
{ "name": "血氧仪健康报告单天", "route": "/api2/oximeter/health/report/day" },
|
||||||
|
{ "name": "血氧仪健康报告多天", "route": "/api2/oximeter/health/report/multi/day" },
|
||||||
|
{ "name": "血氧仪报告详情", "route": "/api2/oximeter/report/detail" },
|
||||||
|
{ "name": "首页判断", "route": "/api2/home/flag" },
|
||||||
|
{ "name": "首页Banner", "route": "/api2/home/ad/list" },
|
||||||
|
{ "name": "首页卡片信息", "route": "/api2/home/card/info" },
|
||||||
|
{ "name": "修改首页卡片", "route": "/api2/home/card/update" },
|
||||||
|
{ "name": "协议列表", "route": "/api2/protocol/list" },
|
||||||
|
{ "name": "同意协议", "route": "/api2/protocol/agree" },
|
||||||
|
{ "name": "协议是否更新", "route": "/api2/protocol/has/update" },
|
||||||
|
|
||||||
|
{ "name": "扫码获取报告", "route": "/api2/qr/scan" },
|
||||||
|
{ "name": "查询QR报告详情", "route": "/api2/qr/one" },
|
||||||
|
{ "name": "查询QR报告列表", "route": "/api2/qr/page" },
|
||||||
|
{ "name": "获取报告数量", "route": "/api2/qr/count" },
|
||||||
|
|
||||||
|
{ "name": "检查健康自测人数", "route": "/api2/self/check/head/count" },
|
||||||
|
{ "name": "健康自测结果", "route": "/api2/self/check/save" },
|
||||||
|
|
||||||
|
{ "name": "科普分类列表", "route": "/api2/health/category/list" },
|
||||||
|
{ "name": "科普列表", "route": "/api2/health/service/list" },
|
||||||
|
{ "name": "科普详情", "route": "/api2/health/service/getById" },
|
||||||
|
|
||||||
|
{ "name": "deepSeek流式接口", "route": "/api2/deepSeek/stream" }
|
||||||
|
]
|
||||||
Loading…
x
Reference in New Issue
Block a user