api-proxy-mock/admin.html

1382 lines
55 KiB
HTML
Raw Permalink 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">
<a href="javascript:void(0)" style="color:#409EFF;cursor:pointer;" @click="openMockFileFromRoute(scope.row)">{{ scope.row.filePath }}</a>
</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="数据配置" 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-select size="small" v-model="mockGroupFilter" clearable placeholder="按分组筛选" style="width:160px;">
<el-option label="全部" :value="null"></el-option>
<el-option label="未分组" :value="-1"></el-option>
<el-option v-for="g in mockGroups" :key="g.id" :label="g.name" :value="g.id"></el-option>
</el-select>
<el-input size="small" v-model="mockFileSearch" placeholder="搜索别名或路径" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
<el-button size="mini" type="primary" @click="openMockFileDialogForCreate">新增 Mock 文件</el-button>
<el-button size="mini" @click="openMockGroupDialog">分组管理</el-button>
</div>
</div>
<el-table :data="pagedMockFiles" border>
<el-table-column label="分组" width="140">
<template slot-scope="scope">
<span>{{ getGroupName(scope.row.groupId) }}</span>
</template>
</el-table-column>
<el-table-column label="别名" min-width="180">
<template slot-scope="scope">
<span>{{ scope.row.alias || "-" }}</span>
</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="240">
<template slot-scope="scope">
<el-button size="mini" type="primary" plain @click="openMockFileDialogForEdit(scope.row)">修改</el-button>
<el-button size="mini" type="success" plain @click="openMockFileDialogForCopy(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>
<div style="color:#E6A23C;font-size:12px;margin-top:4px;">
<i class="el-icon-warning"></i> 修改端口后需重启应用才能生效
</div>
</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 文件">
<div style="display:flex;gap:8px;">
<el-select
v-model="routeDialog.form.groupFilter"
clearable
placeholder="选择分组"
style="width: 180px;"
@change="onRouteDialogGroupChange"
>
<el-option label="全部" :value="null"></el-option>
<el-option label="无分组" :value="-1"></el-option>
<el-option
v-for="g in mockGroups"
:key="g.id"
:label="g.name"
:value="g.id"
></el-option>
</el-select>
<el-select
v-model="routeDialog.form.filePath"
filterable
placeholder="请选择 mock 文件"
style="flex: 1;"
>
<el-option
v-for="item in routeDialogMockFiles"
:key="item.filePath"
:label="item.alias ? item.alias + ' (' + item.filePath + ')' : item.filePath"
:value="item.filePath"
></el-option>
</el-select>
</div>
</el-form-item>
<el-form-item label="返回状态码">
<el-select
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 文件' : mockFileDialog.mode === 'copy' ? '复制 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="分组">
<el-select v-model="mockFileDialog.form.groupId" clearable placeholder="选择分组" style="width: 100%;">
<el-option label="无分组" :value="null"></el-option>
<el-option v-for="g in mockGroups" :key="g.id" :label="g.name" :value="g.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="Mock 文件路径">
<el-input
v-model="mockFileDialog.form.fileName"
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>
<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>
<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: [],
mockGroups: [],
mockGroupFilter: null,
mockGroupDialog: {
visible: false,
mode: "create",
editingId: null,
form: { name: "" },
},
deleteGroupDialog: {
visible: false,
groupId: null,
groupName: "",
fileCount: 0,
action: "",
targetGroupId: null,
},
contentTypeOptions: [
"application/json",
"application/x-www-form-urlencoded",
],
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,
groupFilter: null,
},
},
mockFileDialog: {
visible: false,
mode: "create",
form: {
fileName: "",
alias: "",
content: "",
},
},
};
},
watch: {
routeSearch: function () { this.routePagination.currentPage = 1; },
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
mockGroupFilter: function () { this.mockFilePagination.currentPage = 1; },
apiSearch: function () { this.apiPagination.currentPage = 1; },
},
created: async function () {
await this.loadApiList();
await this.loadMockFiles();
await this.loadMockGroups();
await this.loadConfig();
},
methods: {
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 pagedIndex =
(this.routePagination.currentPage - 1) * this.routePagination.pageSize +
index;
var target = this.filteredRoutes[pagedIndex];
if (!target) return;
var realIndex = this.form.routes.indexOf(target);
if (realIndex === -1) return;
this.form.routes.splice(realIndex, 1);
await this.saveRoutes();
},
getDefaultRouteForm: function () {
return {
apiName: "",
selectedApiRoute: "",
originalRoute: "",
route: "",
filePath: "",
statusCode: 200,
enabled: true,
groupFilter: null,
};
},
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;
},
onRouteDialogGroupChange: function () {
// 切换分组时清空已选文件
this.routeDialog.form.filePath = "";
},
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.filteredRoutes[actualIndex] || { route: "", filePath: "mock/" };
this.routeDialog.mode = "edit";
this.routeDialog.editingIndex = actualIndex;
var matched = this.findApiByRoute(item.route || "");
var mockFile = this.mockFiles.find(function (f) { return f.filePath === item.filePath; });
var groupFilter = mockFile && mockFile.groupId ? mockFile.groupId : null;
this.routeDialog.form = {
apiName: matched ? matched.name : item.apiName || "",
selectedApiRoute: matched ? matched.route : "",
originalRoute: item.route || "",
route: item.route || "",
filePath: item.filePath || "mock/",
statusCode: this.normalizeStatusCode(item.statusCode),
enabled: item.enabled !== false,
groupFilter: groupFilter,
};
this.routeDialog.visible = true;
},
getDefaultMockFileForm: function () {
return {
fileName: "",
alias: "",
content: "",
groupId: null,
};
},
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 || ""),
groupId: item.groupId || null,
};
this.mockFileDialog.visible = true;
},
openMockFileFromRoute: function (routeRow) {
var filePath = routeRow.filePath || "";
var mockFile = this.mockFiles.find(function (f) { return f.filePath === filePath; });
if (!mockFile) {
this.$message.warning("未找到对应的 Mock 文件:" + filePath);
return;
}
this.openMockFileDialogForEdit(mockFile);
},
openMockFileDialogForCopy: function (item) {
this.mockFileDialog.mode = "copy";
this.mockFileDialog.form = {
fileName: "",
alias: String(item.alias || "") + "-副本",
content: String(item.content || ""),
groupId: item.groupId || null,
};
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;
}
if (!/\.[^./\\]+$/.test(fileName)) {
this.$message.error("文件名必须包含后缀,如 .json、.txt 等");
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,
groupId: this.mockFileDialog.form.groupId,
}),
});
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();
},
getGroupName: function (groupId) {
if (!groupId) return "未分组";
var group = this.mockGroups.find(function (g) { return g.id === groupId; });
return group ? group.name : "未分组";
},
getGroupFileCount: function (groupId) {
return this.mockFiles.filter(function (f) { return f.groupId === groupId; }).length;
},
loadMockGroups: async function () {
try {
var resp = await fetch("/__mock-groups");
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "加载分组失败");
}
this.mockGroups = Array.isArray(data.list) ? data.list : [];
} catch (err) {
this.mockGroups = [];
this.$message.error("加载分组失败: " + err.message);
}
},
openMockGroupDialog: function () {
this.mockGroupDialog.editingId = null;
this.mockGroupDialog.form.name = "";
this.mockGroupDialog.visible = true;
},
addMockGroup: async function () {
var self = this;
this.$prompt("请输入分组名称", "新增分组", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPattern: /\S+/,
inputErrorMessage: "分组名称不能为空",
}).then(async function (res) {
var name = (res.value || "").trim();
try {
var resp = await fetch("/__mock-groups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "新增分组失败");
}
self.$message.success("分组已创建");
await self.loadMockGroups();
} catch (err) {
self.$message.error("新增分组失败: " + err.message);
}
}).catch(function () {});
},
startEditMockGroup: function (group) {
this.mockGroupDialog.editingId = group.id;
this.mockGroupDialog.form.name = group.name;
},
saveMockGroupEdit: async function (group) {
var newName = (this.mockGroupDialog.form.name || "").trim();
this.mockGroupDialog.editingId = null;
if (!newName || newName === group.name) return;
try {
var resp = await fetch("/__mock-groups", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: group.id, name: newName }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "修改分组失败");
}
this.$message.success("分组已修改");
await this.loadMockGroups();
} catch (err) {
this.$message.error("修改分组失败: " + err.message);
}
},
confirmDeleteGroup: async function (group) {
var fileCount = this.getGroupFileCount(group.id);
if (fileCount === 0) {
// 无文件,直接删除
try {
var resp = await fetch("/__mock-groups", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: group.id }),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "删除分组失败");
}
this.$message.success("分组已删除");
await this.loadMockGroups();
} catch (err) {
this.$message.error("删除分组失败: " + err.message);
}
return;
}
// 有文件,弹出确认对话框
this.deleteGroupDialog.groupId = group.id;
this.deleteGroupDialog.groupName = group.name;
this.deleteGroupDialog.fileCount = fileCount;
this.deleteGroupDialog.action = "";
this.deleteGroupDialog.targetGroupId = null;
this.deleteGroupDialog.visible = true;
},
submitDeleteGroup: async function () {
var action = this.deleteGroupDialog.action;
if (!action) {
this.$message.error("请选择处理方式");
return;
}
if (action === "reassign" && !this.deleteGroupDialog.targetGroupId) {
this.$message.error("请选择目标分组");
return;
}
var payload = {
id: this.deleteGroupDialog.groupId,
action: action === "unassign" ? "reassign" : action,
};
if (action === "reassign") {
payload.targetGroupId = this.deleteGroupDialog.targetGroupId;
} else if (action === "unassign") {
payload.targetGroupId = null;
}
try {
var resp = await fetch("/__mock-groups", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "删除分组失败");
}
this.$message.success("分组已删除");
this.deleteGroupDialog.visible = false;
await this.loadMockGroups();
await this.loadMockFiles();
} catch (err) {
this.$message.error("删除分组失败: " + err.message);
}
},
},
computed: {
filteredRoutes: function () {
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();
var groupFilter = this.mockGroupFilter;
var list = this.mockFiles;
if (groupFilter !== null && groupFilter !== undefined) {
if (groupFilter === -1) {
list = list.filter(function (item) { return !item.groupId; });
} else {
list = list.filter(function (item) { return item.groupId === groupFilter; });
}
}
if (q) {
list = list.filter(function (item) {
return (
(item.filePath || "").toLowerCase().indexOf(q) !== -1 ||
(item.alias || "").toLowerCase().indexOf(q) !== -1
);
});
}
return list;
},
filteredApiList: function () {
var q = (this.apiSearch || "").toLowerCase().trim();
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);
},
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>
</body>
</html>