api-proxy-mock/admin.html
2026-06-04 12:24:27 +08:00

1062 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>API Proxy Mock 配置管理</title>
<link
rel="stylesheet"
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
/>
<style>
body {
margin: 0;
background: #f5f7fa;
}
.container {
max-width: 1320px;
margin: 20px auto;
padding: 0 16px 24px;
}
.section-card {
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: 1320px;
margin: 0 auto;
padding: 14px 16px;
}
.top-nav-title {
font-size: 18px;
font-weight: 600;
color: #303133;
letter-spacing: 1px;
}
.small-text {
color: #909399;
font-size: 12px;
}
.table-pagination {
margin-top: 12px;
text-align: right;
}
.route-table .el-table__cell .cell {
white-space: nowrap;
}
</style>
</head>
<body>
<div id="app">
<div class="top-nav">
<div class="top-nav-inner">
<span class="top-nav-title">企业级数据Mock管理系统</span>
</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>
<div style="display:flex;gap:8px;align-items:center;">
<el-input size="small" v-model="routeSearch" placeholder="搜索接口名称或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
<el-button size="mini" type="primary" @click="openRouteDialogForCreate">新增路由</el-button>
</div>
</div>
<el-table :data="pagedRoutes" border class="route-table" style="width: 100%;">
<el-table-column label="接口名称" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.apiName || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="请求路径" min-width="220" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.route }}</span>
</template>
</el-table-column>
<el-table-column label="Mock 文件路径" min-width="240" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.filePath }}</span>
</template>
</el-table-column>
<el-table-column label="返回状态码" width="120" align="center">
<template slot-scope="scope">
<span>{{ scope.row.statusCode || 200 }}</span>
</template>
</el-table-column>
<el-table-column label="启用" width="90" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.enabled" @change="saveRoutes"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<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="filteredRoutes.length"
@current-change="handleRoutePageChange"
></el-pagination>
</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>
<div style="display:flex;gap:8px;align-items:center;">
<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>
</div>
</div>
<el-table :data="pagedMockFiles" border>
<el-table-column label="别名" min-width="180">
<template slot-scope="scope">
<span>{{ scope.row.alias || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="Mock 文件路径" min-width="320">
<template slot-scope="scope">
<span>{{ scope.row.filePath }}</span>
</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="filteredMockFiles.length"
@current-change="handleMockFilePageChange"
></el-pagination>
</el-card>
</el-tab-pane>
<el-tab-pane label="接口管理" name="api">
<el-card class="section-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<strong>接口列表</strong>
<div style="display:flex;gap:8px;align-items:center;">
<el-input size="small" v-model="apiSearch" placeholder="搜索接口名称或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
<el-button size="mini" type="primary" @click="openApiDialogForCreate">新增接口</el-button>
<el-button size="mini" type="success" @click="openBatchImportDialog">批量导入</el-button>
</div>
</div>
<el-table :data="pagedApiList" border style="width: 100%;">
<el-table-column label="接口名称" min-width="200">
<template slot-scope="scope">
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="请求路径" min-width="300">
<template slot-scope="scope">
<span>{{ scope.row.route }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template slot-scope="scope">
<el-button size="mini" type="primary" plain @click="openApiDialogForEdit(scope.$index)">修改</el-button>
<el-button size="mini" type="danger" @click="removeApiItem(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="table-pagination"
background
layout="total, prev, pager, next"
:current-page="apiPagination.currentPage"
:page-size="apiPagination.pageSize"
:total="filteredApiList.length"
@current-change="handleApiPageChange"
></el-pagination>
</el-card>
</el-tab-pane>
<el-tab-pane label="基础配置" name="basic">
<el-card class="section-card">
<el-form :model="form.config" label-width="180px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="Mock 开关">
<el-switch v-model="form.config.mockEnabled" @change="saveServerConfig"></el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认响应 Content-Type">
<el-select
v-model="form.config.defaultContentType"
filterable
allow-create
default-first-option
placeholder="请选择"
@change="saveServerConfig"
style="width: 100%;"
>
<el-option
v-for="item in contentTypeOptions"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="本地代理端口">
<el-input-number
v-model="form.config.proxyPort"
:min="1"
:max="65535"
@change="saveServerConfig"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标主机">
<el-input v-model="form.config.targetHost" @blur="saveServerConfig"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标端口">
<el-input-number
v-model="form.config.targetPort"
:min="1"
:max="65535"
@change="saveServerConfig"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标 HTTPS">
<el-switch v-model="form.config.targetHttps" @change="saveServerConfig"></el-switch>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</el-tab-pane>
</el-tabs>
<el-dialog
:title="routeDialog.mode === 'edit' ? '修改路由' : '新增路由'"
:visible.sync="routeDialog.visible"
width="760px"
>
<el-form :model="routeDialog.form" label-width="180px">
<el-form-item label="预置接口">
<el-select
v-model="routeDialog.form.selectedApiRoute"
filterable
clearable
placeholder="可选:从 mock/api-list.json 选择"
@change="onSelectApiRoute"
style="width: 100%;"
>
<el-option
v-for="item in apiList"
:key="item.route"
:label="item.name + ' (' + item.route + ')'"
:value="item.route"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="接口名称">
<el-input
v-model="routeDialog.form.apiName"
placeholder="例如:获取用户信息"
:disabled="isApiListLocked()"
></el-input>
</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.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
:value="item.filePath"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="返回状态码">
<el-select
v-model="routeDialog.form.statusCode"
filterable
allow-create
default-first-option
clearable
placeholder="先选常用状态码,也可直接输入"
style="width: 100%;"
>
<el-option
v-for="item in commonStatusCodes"
:key="item.value"
:label="item.label"
:value="item.value"
></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="别名">
<el-input
v-model="mockFileDialog.form.alias"
placeholder="可选:用于展示,支持任意文本"
></el-input>
</el-form-item>
<el-form-item label="Mock 文件路径">
<el-input
v-model="mockFileDialog.form.fileName"
placeholder="请输入文件名,如 test123.json"
:disabled="mockFileDialog.mode === 'edit'"
>
<template slot="prepend">mock/</template>
</el-input>
</el-form-item>
<el-form-item label="文件内容">
<el-input
type="textarea"
:rows="12"
v-model="mockFileDialog.form.content"
placeholder='{"code":0,"message":"ok"}'
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="mockFileDialog.visible = false">取消</el-button>
<el-button type="primary" @click="submitMockFileDialog">保存 Mock 文件</el-button>
</span>
</el-dialog>
<el-dialog
:title="apiDialog.mode === 'edit' ? '修改接口' : '新增接口'"
:visible.sync="apiDialog.visible"
width="500px"
>
<el-form :model="apiDialog.form" label-width="80px">
<el-form-item label="接口名称">
<el-input v-model="apiDialog.form.name" placeholder="如:获取用户信息"></el-input>
</el-form-item>
<el-form-item label="请求路径">
<el-input v-model="apiDialog.form.route" placeholder="如:/api2/user/info"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="apiDialog.visible = false">取消</el-button>
<el-button type="primary" @click="submitApiDialog">确定</el-button>
</span>
</el-dialog>
<el-dialog
title="批量导入接口"
:visible.sync="batchImportDialog.visible"
width="600px"
>
<el-form label-width="80px">
<el-form-item label="JSON 数据">
<el-input
type="textarea"
:rows="10"
v-model="batchImportDialog.input"
placeholder='[{"route":"/api/xxxx","name":"登录接口"}]'
></el-input>
</el-form-item>
</el-form>
<div style="color:#909399;font-size:12px;margin-top:-8px;padding-left:80px;">
格式:JSON 数组,每项包含 route(请求路径)和 name(接口名称)
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="batchImportDialog.visible = false">取消</el-button>
<el-button type="primary" @click="submitBatchImport">导入</el-button>
</span>
</el-dialog>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: "#app",
data: function () {
return {
activeMainTab: "routes",
apiList: [],
apiPagination: { currentPage: 1, pageSize: 10 },
apiDialog: {
visible: false,
mode: "create",
editingIndex: -1,
form: { name: "", route: "", originalRoute: "" },
},
batchImportDialog: {
visible: false,
input: "",
},
mockFiles: [],
contentTypeOptions: [
"application/json",
"application/x-www-form-urlencoded",
],
commonStatusCodes: [
{ value: 200, label: "200 OK" },
{ value: 201, label: "201 Created" },
{ value: 204, label: "204 No Content" },
{ value: 400, label: "400 Bad Request" },
{ value: 401, label: "401 Unauthorized" },
{ value: 403, label: "403 Forbidden" },
{ value: 404, label: "404 Not Found" },
{ value: 409, label: "409 Conflict" },
{ value: 422, label: "422 Unprocessable Entity" },
{ value: 429, label: "429 Too Many Requests" },
{ value: 500, label: "500 Internal Server Error" },
{ value: 501, label: "501 Not Implemented" },
{ value: 502, label: "502 Bad Gateway" },
{ value: 503, label: "503 Service Unavailable" },
],
routeSearch: "",
mockFileSearch: "",
apiSearch: "",
routePagination: {
currentPage: 1,
pageSize: 10,
},
mockFilePagination: {
currentPage: 1,
pageSize: 10,
},
form: {
config: {
mockEnabled: true,
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json",
proxyPort: 8877,
targetHost: "",
targetPort: 443,
targetHttps: true,
},
routes: [],
},
routeDialog: {
visible: false,
mode: "create",
editingIndex: -1,
form: {
apiName: "",
selectedApiRoute: "",
originalRoute: "",
route: "",
filePath: "",
statusCode: 200,
enabled: true,
},
},
mockFileDialog: {
visible: false,
mode: "create",
form: {
fileName: "",
alias: "",
content: "",
},
},
};
},
watch: {
routeSearch: function () { this.routePagination.currentPage = 1; },
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
apiSearch: function () { this.apiPagination.currentPage = 1; },
},
created: async function () {
await this.loadApiList();
await this.loadMockFiles();
await this.loadConfig();
},
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);
},
normalizeStatusCode: function (value) {
var numeric = Number(value);
if (Number.isInteger(numeric) && numeric >= 100 && numeric <= 599) {
return numeric;
}
return 200;
},
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, routeStatusesObj, routeEnabledMap, routeApiNameMap) {
var self = this;
return Object.keys(routesObj || {}).map(function (route) {
var storedName = (routeApiNameMap || {})[route] || "";
var matched = storedName ? null : self.findApiByRoute(route);
return {
route: route,
filePath: routesObj[route],
statusCode: self.normalizeStatusCode(
routeStatusesObj && routeStatusesObj[route],
),
apiName: storedName || (matched ? matched.name : ""),
selectedApiRoute: matched ? matched.route : "",
enabled: (routeEnabledMap || {})[route] !== false,
};
});
},
toRouteObject: function (routeArray) {
var obj = {};
(routeArray || []).forEach(function (item) {
var route = (item.route || "").trim();
var filePath = (item.filePath || "").trim();
if (route && filePath) {
obj[route] = filePath;
}
});
return obj;
},
toRouteStatusObject: function (routeArray) {
var self = this;
var obj = {};
(routeArray || []).forEach(function (item) {
var route = (item.route || "").trim();
if (!route) return;
obj[route] = self.normalizeStatusCode(item.statusCode);
});
return obj;
},
toRouteEnabledMap: function (routeArray) {
var obj = {};
(routeArray || []).forEach(function (item) {
var route = (item.route || "").trim();
if (route) {
obj[route] = item.enabled !== false;
}
});
return obj;
},
toRouteApiNameMap: function (routeArray) {
var obj = {};
(routeArray || []).forEach(function (item) {
var route = (item.route || "").trim();
if (route) {
obj[route] = item.apiName || "";
}
});
return obj;
},
removeRoute: async function (index) {
var actualIndex =
(this.routePagination.currentPage - 1) * this.routePagination.pageSize +
index;
this.form.routes.splice(actualIndex, 1);
await this.saveRoutes();
},
getDefaultRouteForm: function () {
return {
apiName: "",
selectedApiRoute: "",
originalRoute: "",
route: "",
filePath: "",
statusCode: 200,
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 || "",
route: item.route || "",
filePath: item.filePath || "mock/",
statusCode: this.normalizeStatusCode(item.statusCode),
enabled: item.enabled !== false,
};
this.routeDialog.visible = true;
},
getDefaultMockFileForm: function () {
return {
fileName: "",
alias: "",
content: "",
};
},
openMockFileDialogForCreate: function () {
this.mockFileDialog.mode = "create";
this.mockFileDialog.form = this.getDefaultMockFileForm();
this.mockFileDialog.visible = true;
},
openMockFileDialogForEdit: function (item) {
var full = item.filePath || "";
var name = full.indexOf("mock/") === 0 ? full.slice(5) : full;
this.mockFileDialog.mode = "edit";
this.mockFileDialog.form = {
fileName: name,
alias: String(item.alias || ""),
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 () {
var fileName = (this.mockFileDialog.form.fileName || "").trim();
if (!fileName) {
this.$message.error("文件名不能为空");
return;
}
if (fileName.indexOf("/") !== -1 || fileName.indexOf("\\") !== -1) {
this.$message.error("文件名不能包含路径分隔符");
return;
}
var filePath = "mock/" + fileName;
try {
var resp = await fetch("/__mock-files", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filePath: filePath,
alias: this.mockFileDialog.form.alias,
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 () {
try {
var resp = await fetch("/__config");
var data = await resp.json();
this.form.config = Object.assign({}, this.form.config, data.config || {});
this.form.routes = this.toRouteArray(
data.routes || {},
data.routeStatuses || {},
data.routeEnabledMap || {},
data.routeApiNameMap || {},
);
this.routePagination.currentPage = 1;
} catch (err) {
this.$message.error("加载配置失败: " + err.message);
}
},
saveRoutes: async function () {
var payload = {
routes: this.toRouteObject(this.form.routes),
routeStatuses: this.toRouteStatusObject(this.form.routes),
routeEnabledMap: this.toRouteEnabledMap(this.form.routes),
routeApiNameMap: this.toRouteApiNameMap(this.form.routes),
};
try {
var resp = await fetch("/__config", {
method: "POST",
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 || "保存失败");
}
} catch (err) {
this.$message.error("保存路由失败: " + err.message);
}
},
saveServerConfig: async function () {
try {
var resp = await fetch("/__config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ config: this.form.config }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "保存失败");
}
} catch (err) {
this.$message.error("保存配置失败: " + err.message);
}
},
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,
statusCode: this.normalizeStatusCode(this.routeDialog.form.statusCode),
});
try {
var resp = await fetch("/__routes", {
method: "POST",
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.routeDialog.mode === "edit"
? "路由修改成功"
: "接口配置文件创建成功",
);
this.routeDialog.visible = false;
await this.loadMockFiles();
await this.loadConfig();
} catch (err) {
this.$message.error("创建失败: " + err.message);
}
},
handleApiPageChange: function (page) {
this.apiPagination.currentPage = page;
},
openApiDialogForCreate: function () {
this.apiDialog.mode = "create";
this.apiDialog.editingIndex = -1;
this.apiDialog.form = { name: "", route: "", originalRoute: "" };
this.apiDialog.visible = true;
},
openApiDialogForEdit: function (index) {
var actualIndex =
(this.apiPagination.currentPage - 1) * this.apiPagination.pageSize + index;
var item = this.apiList[actualIndex];
this.apiDialog.mode = "edit";
this.apiDialog.editingIndex = actualIndex;
this.apiDialog.form = {
name: item.name,
route: item.route,
originalRoute: item.route,
};
this.apiDialog.visible = true;
},
submitApiDialog: async function () {
if (!this.apiDialog.form.name) {
this.$message.error("接口名称不能为空");
return;
}
if (!this.apiDialog.form.route || !this.apiDialog.form.route.startsWith("/")) {
this.$message.error("请求路径必须以 / 开头");
return;
}
try {
var resp = await fetch("/__api-list", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.apiDialog.form),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "操作失败");
}
this.$message.success(this.apiDialog.mode === "edit" ? "接口已修改" : "接口已添加");
this.apiDialog.visible = false;
await this.loadApiList();
} catch (err) {
this.$message.error("操作失败: " + err.message);
}
},
removeApiItem: async function (index) {
var actualIndex =
(this.apiPagination.currentPage - 1) * this.apiPagination.pageSize + index;
var item = this.apiList[actualIndex];
try {
await this.$confirm("确定删除接口「" + item.name + "」?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
var resp = await fetch("/__api-list", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ route: item.route }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "删除失败");
}
this.$message.success("接口已删除");
await this.loadApiList();
} catch (err) {
if (err !== "cancel") {
this.$message.error("删除失败: " + err.message);
}
}
},
openBatchImportDialog: function () {
this.batchImportDialog.input = "";
this.batchImportDialog.visible = true;
},
submitBatchImport: async function () {
var raw = (this.batchImportDialog.input || "").trim();
if (!raw) {
this.$message.error("请输入 JSON 数据");
return;
}
var list;
try {
list = JSON.parse(raw);
} catch (e) {
this.$message.error("JSON 格式不正确:" + e.message);
return;
}
if (!Array.isArray(list) || list.length === 0) {
this.$message.error("请输入非空 JSON 数组");
return;
}
var okCount = 0;
var failCount = 0;
for (var i = 0; i < list.length; i++) {
var item = list[i];
if (!item || !item.route || !item.name) {
failCount++;
continue;
}
try {
var resp = await fetch("/__api-list", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ route: item.route, name: item.name }),
});
var data = await resp.json();
if (resp.ok && data.success !== false) {
okCount++;
} else {
failCount++;
}
} catch (err) {
failCount++;
}
}
if (okCount > 0) {
this.$message.success("成功导入 " + okCount + " 条" + (failCount > 0 ? ",失败 " + failCount + " 条" : ""));
} else {
this.$message.error("导入失败,共 " + failCount + " 条");
}
this.batchImportDialog.visible = false;
await this.loadApiList();
},
},
computed: {
filteredRoutes: function () {
var q = (this.routeSearch || "").toLowerCase().trim();
var list = this.form.routes;
if (q) {
list = list.filter(function (item) {
return (
(item.route || "").toLowerCase().indexOf(q) !== -1 ||
(item.apiName || "").toLowerCase().indexOf(q) !== -1 ||
(item.filePath || "").toLowerCase().indexOf(q) !== -1
);
});
}
return list.slice().sort(function (a, b) {
return (b.enabled !== false ? 1 : 0) - (a.enabled !== false ? 1 : 0);
});
},
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
);
});
},
filteredApiList: function () {
var q = (this.apiSearch || "").toLowerCase().trim();
if (!q) return this.apiList;
return this.apiList.filter(function (item) {
return (
(item.route || "").toLowerCase().indexOf(q) !== -1 ||
(item.name || "").toLowerCase().indexOf(q) !== -1
);
});
},
pagedRoutes: function () {
return this.getPagedData(this.filteredRoutes, this.routePagination);
},
pagedMockFiles: function () {
return this.getPagedData(this.filteredMockFiles, this.mockFilePagination);
},
pagedApiList: function () {
return this.getPagedData(this.filteredApiList, this.apiPagination);
},
},
});
</script>
</body>
</html>