2007 lines
82 KiB
HTML
2007 lines
82 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>企业级数据Mock管理系统 - by tony</title>
|
||
<link rel="stylesheet" href="/assets/element-ui/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;
|
||
}
|
||
.mock-content-textarea textarea {
|
||
font-family: Consolas, Monaco, "Courier New", monospace;
|
||
line-height: 1.5;
|
||
}
|
||
.el-dialog.is-fullscreen .el-dialog__body {
|
||
overflow-y: auto;
|
||
max-height: calc(100vh - 120px);
|
||
}
|
||
.basic-config-readonly {
|
||
pointer-events: none;
|
||
opacity: 0.65;
|
||
}
|
||
</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" icon="el-icon-refresh" :loading="routesRefreshing" @click="refreshRoutes">刷新</el-button>
|
||
<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)">{{ getMockFileAlias(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" icon="el-icon-refresh" :loading="mocksRefreshing" @click="refreshMocks">刷新</el-button>
|
||
<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>接口管理(api)</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" icon="el-icon-refresh" :loading="apiRefreshing" @click="refreshApiList">刷新</el-button>
|
||
<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">
|
||
<div style="display:flex;justify-content:flex-end;align-items:center;margin-bottom:12px;">
|
||
<el-button size="mini" icon="el-icon-refresh" :loading="configRefreshing" @click="refreshConfig">刷新</el-button>
|
||
</div>
|
||
<el-alert
|
||
v-if="!canEditBasicConfig"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
style="margin-bottom:16px;"
|
||
:title="'当前 IP(' + (clientIp || '未知') + ')不在管理员白名单内,无法修改系统配置'"
|
||
description="系统配置仅允许 admin_list.txt 中的 IP 访问与编辑。"
|
||
></el-alert>
|
||
<div :class="{ 'basic-config-readonly': !canEditBasicConfig }">
|
||
<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>
|
||
</div>
|
||
</el-card>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="个性化配置" name="ip">
|
||
<el-card class="section-card">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||
<div>
|
||
<strong>个性化配置</strong>
|
||
<div class="small-text" style="margin-top:4px;">
|
||
为当前 IP 配置独立目标环境(不含本地代理端口),覆盖全局系统配置。
|
||
普通用户仅可管理 IP 为 <strong>{{ clientIp || "未知" }}</strong> 的配置;管理员可管理全部。
|
||
<a href="/__client-ip" target="_blank" style="color:#409EFF;">查看当前识别 IP</a>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;align-items:center;">
|
||
<el-input size="small" v-model="ipRuleSearch" placeholder="搜索 IP 或备注" clearable style="width:220px;" prefix-icon="el-icon-search"></el-input>
|
||
<el-button size="mini" icon="el-icon-refresh" :loading="ipRulesRefreshing" @click="refreshIpRules">刷新</el-button>
|
||
<el-button
|
||
v-if="canCreateIpRule"
|
||
size="mini"
|
||
type="primary"
|
||
@click="openIpRuleDialogForCreate"
|
||
>{{ isAdmin ? "新增配置" : "配置我的环境" }}</el-button>
|
||
</div>
|
||
</div>
|
||
<el-table :data="pagedIpRules" border style="width: 100%;">
|
||
<el-table-column label="IP 地址" min-width="160">
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.ip }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="140" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.remark || "-" }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="策略" width="120" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag v-if="scope.row.blocked" type="danger" size="mini">拉黑</el-tag>
|
||
<el-tag v-else-if="scope.row.useCustomConfig" type="warning" size="mini">个性化</el-tag>
|
||
<el-tag v-else type="info" size="mini">默认</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="目标环境" min-width="260" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ getIpRuleTargetSummary(scope.row) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="Mock" width="90" align="center">
|
||
<template slot-scope="scope">
|
||
<span v-if="scope.row.blocked">-</span>
|
||
<span v-else-if="scope.row.useCustomConfig">{{ scope.row.config.mockEnabled === false ? "关闭" : "开启" }}</span>
|
||
<span v-else>全局</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="160">
|
||
<template slot-scope="scope">
|
||
<template v-if="canManageIpRule(scope.row)">
|
||
<el-button size="mini" type="primary" plain @click="openIpRuleDialogForEdit(scope.row)">修改</el-button>
|
||
<el-button size="mini" type="danger" @click="removeIpRule(scope.row)">删除</el-button>
|
||
</template>
|
||
<span v-else class="small-text">无权限</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-pagination
|
||
class="table-pagination"
|
||
background
|
||
layout="total, prev, pager, next"
|
||
:current-page="ipRulePagination.currentPage"
|
||
:page-size="ipRulePagination.pageSize"
|
||
:total="filteredIpRules.length"
|
||
@current-change="handleIpRulePageChange"
|
||
></el-pagination>
|
||
</el-card>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<el-dialog
|
||
:title="routeDialog.mode === 'edit' ? '修改路由' : '新增路由'"
|
||
:visible.sync="routeDialog.visible"
|
||
width="760px"
|
||
>
|
||
<el-form
|
||
ref="routeForm"
|
||
:model="routeDialog.form"
|
||
:rules="routeFormRules"
|
||
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="接口名称" prop="apiName">
|
||
<el-input
|
||
v-model="routeDialog.form.apiName"
|
||
placeholder="例如:获取用户信息"
|
||
:disabled="isApiListLocked()"
|
||
></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="请求路径" prop="route">
|
||
<el-input
|
||
v-model="routeDialog.form.route"
|
||
placeholder="/api/new/mock"
|
||
:disabled="isApiListLocked()"
|
||
></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="Mock 文件" prop="filePath">
|
||
<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.filePath"
|
||
:value="item.filePath"
|
||
></el-option>
|
||
</el-select>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="返回状态码" prop="statusCode">
|
||
<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" prop="enabled" required>
|
||
<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="mockFileDialog.fullscreen ? '100%' : '760px'"
|
||
:fullscreen="mockFileDialog.fullscreen"
|
||
:top="mockFileDialog.fullscreen ? '0' : '15vh'"
|
||
>
|
||
<el-form
|
||
ref="mockFileForm"
|
||
:model="mockFileDialog.form"
|
||
:rules="mockFileFormRules"
|
||
label-width="180px"
|
||
>
|
||
<el-form-item label="别名" prop="alias">
|
||
<el-input
|
||
v-model="mockFileDialog.form.alias"
|
||
placeholder="必填:用于展示,支持任意文本"
|
||
></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="分组" prop="groupId" required>
|
||
<el-select v-model="mockFileDialog.form.groupId" 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 文件路径" prop="fileName">
|
||
<el-input
|
||
v-model="mockFileDialog.form.fileName"
|
||
:placeholder="mockFileDialog.mode === 'edit' ? '' : '根据别名自动生成:别名_随机串.json'"
|
||
disabled
|
||
>
|
||
<template slot="prepend">mock/</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="文件内容" prop="content">
|
||
<div style="display:flex;justify-content:flex-end;margin-bottom:6px;">
|
||
<el-button size="mini" type="text" @click="mockFileDialog.fullscreen = !mockFileDialog.fullscreen">
|
||
<i :class="mockFileDialog.fullscreen ? 'el-icon-copy-document' : 'el-icon-full-screen'"></i>
|
||
{{ mockFileDialog.fullscreen ? '退出全屏' : '全屏编辑' }}
|
||
</el-button>
|
||
</div>
|
||
<el-input
|
||
type="textarea"
|
||
class="mock-content-textarea"
|
||
:autosize="mockFileDialog.fullscreen ? { minRows: 28, maxRows: 28 } : { minRows: 12, maxRows: 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>
|
||
|
||
<el-dialog
|
||
:title="ipRuleDialog.mode === 'edit' ? '修改个性化配置' : (isAdmin ? '新增个性化配置' : '配置我的环境')"
|
||
:visible.sync="ipRuleDialog.visible"
|
||
width="760px"
|
||
>
|
||
<el-form :model="ipRuleDialog.form" label-width="180px">
|
||
<el-form-item label="IP 地址">
|
||
<el-input
|
||
v-model="ipRuleDialog.form.ip"
|
||
:placeholder="isAdmin && ipRuleDialog.mode === 'create' ? '请输入 IP 地址' : '自动识别当前 IP'"
|
||
:disabled="!(isAdmin && ipRuleDialog.mode === 'create')"
|
||
></el-input>
|
||
<div class="small-text" style="margin-top:4px;">
|
||
{{ isAdmin && ipRuleDialog.mode === 'create'
|
||
? '管理员可填写任意有效 IP'
|
||
: (ipRuleDialog.mode === 'create' ? '自动填充当前 IP,不可修改' : 'IP 地址不可修改') }}
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="ipRuleDialog.form.remark" placeholder="如:测试组、开发组"></el-input>
|
||
</el-form-item>
|
||
<el-form-item v-if="isAdmin" label="拉黑访问">
|
||
<el-switch v-model="ipRuleDialog.form.blocked" @change="onIpRuleBlockedChange"></el-switch>
|
||
<div class="small-text" style="margin-top:4px;">开启后该 IP 的所有请求将被拒绝(403)</div>
|
||
</el-form-item>
|
||
<template v-if="!ipRuleDialog.form.blocked">
|
||
<el-form-item label="个性化配置">
|
||
<el-switch v-model="ipRuleDialog.form.useCustomConfig"></el-switch>
|
||
<div class="small-text" style="margin-top:4px;">开启后可为此 IP 单独配置目标环境,覆盖全局系统配置</div>
|
||
</el-form-item>
|
||
<template v-if="ipRuleDialog.form.useCustomConfig">
|
||
<el-divider content-position="left">独立环境配置</el-divider>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="Mock 开关">
|
||
<el-switch v-model="ipRuleDialog.form.config.mockEnabled"></el-switch>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="默认 Content-Type">
|
||
<el-select
|
||
v-model="ipRuleDialog.form.config.defaultContentType"
|
||
filterable
|
||
allow-create
|
||
default-first-option
|
||
placeholder="请选择"
|
||
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 v-model="ipRuleDialog.form.config.targetHost" placeholder="如 dev.example.com"></el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="目标端口">
|
||
<el-input-number
|
||
v-model="ipRuleDialog.form.config.targetPort"
|
||
:min="1"
|
||
:max="65535"
|
||
style="width: 100%;"
|
||
></el-input-number>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="目标 HTTPS">
|
||
<el-switch v-model="ipRuleDialog.form.config.targetHttps"></el-switch>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</template>
|
||
</template>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="ipRuleDialog.visible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitIpRuleDialog">保存</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/assets/vue.js"></script>
|
||
<script src="/assets/element-ui/index.js"></script>
|
||
<script>
|
||
new Vue({
|
||
el: "#app",
|
||
data: function () {
|
||
return {
|
||
activeMainTab: "routes",
|
||
routesRefreshing: false,
|
||
mocksRefreshing: false,
|
||
apiRefreshing: false,
|
||
configRefreshing: false,
|
||
canEditBasicConfig: false,
|
||
clientIp: "",
|
||
isAdmin: false,
|
||
ipRulesRefreshing: false,
|
||
ipRules: [],
|
||
ipRuleSearch: "",
|
||
ipRulePagination: { currentPage: 1, pageSize: 10 },
|
||
ipRuleDialog: {
|
||
visible: false,
|
||
mode: "create",
|
||
form: {
|
||
ip: "",
|
||
remark: "",
|
||
blocked: false,
|
||
useCustomConfig: true,
|
||
config: {
|
||
mockEnabled: true,
|
||
defaultContentType: "application/json",
|
||
targetHost: "",
|
||
targetPort: 443,
|
||
targetHttps: true,
|
||
},
|
||
},
|
||
},
|
||
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,
|
||
fullscreen: false,
|
||
mode: "create",
|
||
form: {
|
||
fileName: "",
|
||
alias: "",
|
||
content: "",
|
||
},
|
||
},
|
||
routeFormRules: {
|
||
apiName: [
|
||
{ required: true, message: "请输入接口名称", trigger: "blur" },
|
||
],
|
||
route: [
|
||
{ required: true, message: "请输入请求路径", trigger: "blur" },
|
||
{
|
||
validator: function (rule, value, callback) {
|
||
if (value && String(value).charAt(0) !== "/") {
|
||
callback(new Error("请求路径必须以 / 开头"));
|
||
} else {
|
||
callback();
|
||
}
|
||
},
|
||
trigger: "blur",
|
||
},
|
||
],
|
||
filePath: [
|
||
{ required: true, message: "请选择 Mock 文件", trigger: "change" },
|
||
],
|
||
statusCode: [
|
||
{
|
||
required: true,
|
||
validator: function (rule, value, callback) {
|
||
if (value === null || value === undefined || value === "") {
|
||
callback(new Error("请选择或输入返回状态码"));
|
||
} else {
|
||
callback();
|
||
}
|
||
},
|
||
trigger: "change",
|
||
},
|
||
],
|
||
enabled: [
|
||
{
|
||
required: true,
|
||
validator: function (rule, value, callback) {
|
||
if (typeof value !== "boolean") {
|
||
callback(new Error("请设置是否启用 Mock"));
|
||
} else {
|
||
callback();
|
||
}
|
||
},
|
||
trigger: "change",
|
||
},
|
||
],
|
||
},
|
||
mockFileFormRules: {
|
||
alias: [
|
||
{ required: true, message: "请输入别名", trigger: "blur" },
|
||
],
|
||
groupId: [
|
||
{
|
||
validator: function (rule, value, callback) {
|
||
// null 表示「无分组」,视为已选择;仅未赋值时失败
|
||
if (value === undefined || value === "") {
|
||
callback(new Error("请选择分组"));
|
||
} else {
|
||
callback();
|
||
}
|
||
},
|
||
trigger: "change",
|
||
},
|
||
],
|
||
fileName: [
|
||
{ required: true, message: "文件路径不能为空", trigger: "change" },
|
||
],
|
||
content: [
|
||
{ required: true, message: "请输入文件内容", trigger: "blur" },
|
||
],
|
||
},
|
||
};
|
||
},
|
||
watch: {
|
||
activeMainTab: function (tab) {
|
||
if (tab === "routes") { this.loadRoutes(); }
|
||
else if (tab === "mocks") { this.loadMockFiles(); this.loadMockGroups(); }
|
||
else if (tab === "api") { this.loadApiList(); }
|
||
else if (tab === "basic") { this.loadConfig(); }
|
||
else if (tab === "ip") { this.loadIpRules(); }
|
||
},
|
||
routeSearch: function () { this.routePagination.currentPage = 1; },
|
||
mockFileSearch: function () { this.mockFilePagination.currentPage = 1; },
|
||
mockGroupFilter: function () { this.mockFilePagination.currentPage = 1; },
|
||
"mockFileDialog.form.alias": function () {
|
||
if (this.mockFileDialog.mode === "create" || this.mockFileDialog.mode === "copy") {
|
||
this.mockFileDialog.form.fileName = this.generateMockFileName(
|
||
this.mockFileDialog.form.alias,
|
||
);
|
||
}
|
||
},
|
||
apiSearch: function () { this.apiPagination.currentPage = 1; },
|
||
ipRuleSearch: function () { this.ipRulePagination.currentPage = 1; },
|
||
},
|
||
created: async function () {
|
||
await this.loadApiList();
|
||
await this.loadMockFiles();
|
||
await this.loadMockGroups();
|
||
await this.loadConfig();
|
||
await this.loadRoutes();
|
||
},
|
||
methods: {
|
||
loadRoutes: async function () {
|
||
try {
|
||
var resp = await fetch("/__routes");
|
||
var data = await resp.json();
|
||
if (!resp.ok || data.success === false) {
|
||
throw new Error(data.error || "加载路由失败");
|
||
}
|
||
this.form.routes = this.toRouteArray(
|
||
data.routes || {},
|
||
data.routeStatuses || {},
|
||
data.routeEnabledMap || {},
|
||
data.routeApiNameMap || {},
|
||
);
|
||
this.routePagination.currentPage = 1;
|
||
} catch (err) {
|
||
this.$message.error("加载路由失败: " + err.message);
|
||
}
|
||
},
|
||
refreshRoutes: async function () {
|
||
this.routesRefreshing = true;
|
||
try {
|
||
await this.loadRoutes();
|
||
this.$message.success("路由配置已刷新");
|
||
} finally {
|
||
this.routesRefreshing = false;
|
||
}
|
||
},
|
||
refreshMocks: async function () {
|
||
this.mocksRefreshing = true;
|
||
try {
|
||
await this.loadMockFiles();
|
||
await this.loadMockGroups();
|
||
this.$message.success("Mock 数据已刷新");
|
||
} finally {
|
||
this.mocksRefreshing = false;
|
||
}
|
||
},
|
||
refreshApiList: async function () {
|
||
this.apiRefreshing = true;
|
||
try {
|
||
await this.loadApiList();
|
||
this.$message.success("接口列表已刷新");
|
||
} finally {
|
||
this.apiRefreshing = false;
|
||
}
|
||
},
|
||
refreshConfig: async function () {
|
||
this.configRefreshing = true;
|
||
try {
|
||
await this.loadConfig();
|
||
this.$message.success("系统配置已刷新");
|
||
} finally {
|
||
this.configRefreshing = false;
|
||
}
|
||
},
|
||
refreshIpRules: async function () {
|
||
this.ipRulesRefreshing = true;
|
||
try {
|
||
await this.loadIpRules();
|
||
this.$message.success("个性化配置已刷新");
|
||
} finally {
|
||
this.ipRulesRefreshing = false;
|
||
}
|
||
},
|
||
loadIpRules: async function () {
|
||
try {
|
||
var resp = await fetch("/__ip-rules");
|
||
var data = await resp.json();
|
||
if (!resp.ok || data.success === false) {
|
||
throw new Error(data.error || "加载 IP 规则失败");
|
||
}
|
||
this.ipRules = Array.isArray(data.list) ? data.list : [];
|
||
if (data.clientIp) {
|
||
this.clientIp = data.clientIp;
|
||
}
|
||
if (data.isAdmin === true) {
|
||
this.isAdmin = true;
|
||
} else if (data.isAdmin === false) {
|
||
this.isAdmin = false;
|
||
}
|
||
this.ipRulePagination.currentPage = 1;
|
||
} catch (err) {
|
||
this.ipRules = [];
|
||
this.$message.error("加载个性化配置失败: " + err.message);
|
||
}
|
||
},
|
||
canManageIpRule: function (rule) {
|
||
if (!rule) return false;
|
||
if (this.isAdmin) return true;
|
||
var ruleIp = (rule.ip || "").trim();
|
||
var mine = (this.clientIp || "").trim();
|
||
return ruleIp && mine && ruleIp === mine;
|
||
},
|
||
getDefaultIpRuleForm: function () {
|
||
var global = this.form.config || {};
|
||
return {
|
||
ip: this.clientIp || "",
|
||
remark: "",
|
||
blocked: false,
|
||
useCustomConfig: true,
|
||
config: {
|
||
mockEnabled: global.mockEnabled !== false,
|
||
defaultContentType: global.defaultContentType || "application/json",
|
||
targetHost: global.targetHost || "",
|
||
targetPort: global.targetPort || 443,
|
||
targetHttps: global.targetHttps !== false,
|
||
},
|
||
};
|
||
},
|
||
getIpRuleTargetSummary: function (rule) {
|
||
if (!rule) return "-";
|
||
if (rule.blocked) return "已拉黑,禁止访问";
|
||
if (!rule.useCustomConfig) return "使用全局配置";
|
||
var cfg = rule.config || {};
|
||
var host = cfg.targetHost || this.form.config.targetHost || "-";
|
||
var port = cfg.targetPort != null ? cfg.targetPort : this.form.config.targetPort;
|
||
var https = cfg.targetHttps !== undefined ? cfg.targetHttps !== false : this.form.config.targetHttps !== false;
|
||
var proto = https ? "https" : "http";
|
||
return proto + "://" + host + (port ? ":" + port : "");
|
||
},
|
||
onIpRuleBlockedChange: function (blocked) {
|
||
if (blocked) {
|
||
this.ipRuleDialog.form.useCustomConfig = false;
|
||
}
|
||
},
|
||
openIpRuleDialogForCreate: async function () {
|
||
if (!this.canCreateIpRule) {
|
||
this.$message.warning("您已有个性化配置,请直接修改");
|
||
return;
|
||
}
|
||
if (!this.clientIp) {
|
||
await this.loadIpRules();
|
||
}
|
||
this.ipRuleDialog.mode = "create";
|
||
this.ipRuleDialog.form = this.getDefaultIpRuleForm();
|
||
this.ipRuleDialog.visible = true;
|
||
},
|
||
openIpRuleDialogForEdit: function (rule) {
|
||
if (!this.canManageIpRule(rule)) {
|
||
this.$message.error("只能修改与当前 IP 匹配的个性化配置");
|
||
return;
|
||
}
|
||
var cfg = rule.config || {};
|
||
this.ipRuleDialog.mode = "edit";
|
||
this.ipRuleDialog.form = {
|
||
ip: rule.ip || "",
|
||
remark: rule.remark || "",
|
||
blocked: !!rule.blocked,
|
||
useCustomConfig: !!rule.useCustomConfig,
|
||
config: {
|
||
mockEnabled: cfg.mockEnabled !== false,
|
||
defaultContentType: cfg.defaultContentType || this.form.config.defaultContentType || "application/json",
|
||
targetHost: cfg.targetHost || "",
|
||
targetPort: cfg.targetPort != null ? cfg.targetPort : this.form.config.targetPort,
|
||
targetHttps: cfg.targetHttps !== undefined ? cfg.targetHttps !== false : this.form.config.targetHttps !== false,
|
||
},
|
||
};
|
||
this.ipRuleDialog.visible = true;
|
||
},
|
||
submitIpRuleDialog: async function () {
|
||
var form = this.ipRuleDialog.form;
|
||
var ip = (form.ip || "").trim();
|
||
if (!this.isAdmin) {
|
||
ip = (this.clientIp || "").trim();
|
||
form.blocked = false;
|
||
}
|
||
if (!ip) {
|
||
this.$message.error("IP 地址不能为空");
|
||
return;
|
||
}
|
||
if (this.isAdmin && !form.blocked && !form.useCustomConfig) {
|
||
this.$message.error("请至少启用「拉黑访问」或「个性化配置」");
|
||
return;
|
||
}
|
||
if (!this.isAdmin && !form.useCustomConfig) {
|
||
this.$message.error("请启用个性化配置");
|
||
return;
|
||
}
|
||
try {
|
||
var resp = await fetch("/__ip-rules", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
mode: this.ipRuleDialog.mode,
|
||
ip: ip,
|
||
remark: form.remark,
|
||
blocked: !!form.blocked,
|
||
useCustomConfig: !!form.useCustomConfig,
|
||
config: form.useCustomConfig ? form.config : {},
|
||
}),
|
||
});
|
||
var data = await resp.json();
|
||
if (!resp.ok || data.success === false) {
|
||
throw new Error(data.error || "保存 IP 规则失败");
|
||
}
|
||
this.$message.success("IP 规则已保存");
|
||
this.ipRuleDialog.visible = false;
|
||
await this.loadIpRules();
|
||
} catch (err) {
|
||
this.$message.error("保存 IP 规则失败: " + err.message);
|
||
}
|
||
},
|
||
removeIpRule: async function (rule) {
|
||
if (!this.canManageIpRule(rule)) {
|
||
this.$message.error("只能删除与当前 IP 匹配的个性化配置");
|
||
return;
|
||
}
|
||
try {
|
||
await this.$confirm("确定删除 IP「" + rule.ip + "」的规则吗?", "提示", {
|
||
type: "warning",
|
||
});
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
try {
|
||
var resp = await fetch("/__ip-rules", {
|
||
method: "DELETE",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ ip: rule.ip }),
|
||
});
|
||
var data = await resp.json();
|
||
if (!resp.ok || data.success === false) {
|
||
throw new Error(data.error || "删除 IP 规则失败");
|
||
}
|
||
this.$message.success("IP 规则已删除");
|
||
await this.loadIpRules();
|
||
} catch (err) {
|
||
this.$message.error("删除 IP 规则失败: " + err.message);
|
||
}
|
||
},
|
||
handleIpRulePageChange: function (page) {
|
||
this.ipRulePagination.currentPage = page;
|
||
},
|
||
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;
|
||
this.$nextTick(function () {
|
||
if (this.$refs.routeForm) this.$refs.routeForm.clearValidate();
|
||
});
|
||
},
|
||
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;
|
||
this.$nextTick(function () {
|
||
if (this.$refs.routeForm) this.$refs.routeForm.clearValidate();
|
||
});
|
||
},
|
||
sanitizeMockFileAlias: function (alias) {
|
||
var text = String(alias || "").trim();
|
||
text = text.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, "_");
|
||
text = text.replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
||
return text || "mock";
|
||
},
|
||
generateMockFileName: function (alias) {
|
||
var prefix = this.sanitizeMockFileAlias(alias);
|
||
var random = Math.random().toString(36).slice(2, 10);
|
||
return prefix + "_" + random + ".json";
|
||
},
|
||
getDefaultMockFileForm: function () {
|
||
return {
|
||
fileName: this.generateMockFileName(""),
|
||
alias: "",
|
||
content: "",
|
||
groupId: null,
|
||
};
|
||
},
|
||
openMockFileDialogForCreate: function () {
|
||
this.mockFileDialog.mode = "create";
|
||
this.mockFileDialog.fullscreen = false;
|
||
this.mockFileDialog.form = this.getDefaultMockFileForm();
|
||
this.mockFileDialog.visible = true;
|
||
this.$nextTick(function () {
|
||
if (this.$refs.mockFileForm) this.$refs.mockFileForm.clearValidate();
|
||
});
|
||
},
|
||
openMockFileDialogForEdit: function (item) {
|
||
var full = item.filePath || "";
|
||
var name = full.indexOf("mock/") === 0 ? full.slice(5) : full;
|
||
this.mockFileDialog.mode = "edit";
|
||
this.mockFileDialog.fullscreen = false;
|
||
this.mockFileDialog.form = {
|
||
fileName: name,
|
||
alias: String(item.alias || ""),
|
||
content: String(item.content || ""),
|
||
groupId: item.groupId || null,
|
||
};
|
||
this.mockFileDialog.visible = true;
|
||
this.$nextTick(function () {
|
||
if (this.$refs.mockFileForm) this.$refs.mockFileForm.clearValidate();
|
||
});
|
||
},
|
||
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);
|
||
},
|
||
getMockFileAlias: function (filePath) {
|
||
var path = filePath || "";
|
||
var mockFile = this.mockFiles.find(function (f) { return f.filePath === path; });
|
||
if (mockFile && mockFile.alias) {
|
||
return mockFile.alias;
|
||
}
|
||
return path || "-";
|
||
},
|
||
openMockFileDialogForCopy: function (item) {
|
||
var alias = String(item.alias || "") + "-副本";
|
||
this.mockFileDialog.mode = "copy";
|
||
this.mockFileDialog.fullscreen = false;
|
||
this.mockFileDialog.form = {
|
||
fileName: this.generateMockFileName(alias),
|
||
alias: alias,
|
||
content: String(item.content || ""),
|
||
groupId: item.groupId || null,
|
||
};
|
||
this.mockFileDialog.visible = true;
|
||
this.$nextTick(function () {
|
||
if (this.$refs.mockFileForm) this.$refs.mockFileForm.clearValidate();
|
||
});
|
||
},
|
||
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: function () {
|
||
var self = this;
|
||
if (this.mockFileDialog.mode === "create" || this.mockFileDialog.mode === "copy") {
|
||
this.mockFileDialog.form.fileName = this.generateMockFileName(
|
||
this.mockFileDialog.form.alias,
|
||
);
|
||
}
|
||
this.$refs.mockFileForm.validate(function (valid) {
|
||
if (!valid) return;
|
||
self.doSubmitMockFileDialog();
|
||
});
|
||
},
|
||
doSubmitMockFileDialog: 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.canEditBasicConfig = data.canEditBasicConfig === true;
|
||
this.clientIp = data.clientIp || "";
|
||
if (data.canEditBasicConfig === true) {
|
||
this.isAdmin = true;
|
||
}
|
||
} 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 () {
|
||
if (!this.canEditBasicConfig) {
|
||
return;
|
||
}
|
||
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: function () {
|
||
var self = this;
|
||
this.$refs.routeForm.validate(function (valid) {
|
||
if (!valid) return;
|
||
self.doSubmitRouteDialog();
|
||
});
|
||
},
|
||
doSubmitRouteDialog: async function () {
|
||
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.loadRoutes();
|
||
} 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
|
||
);
|
||
});
|
||
},
|
||
filteredIpRules: function () {
|
||
var q = (this.ipRuleSearch || "").toLowerCase().trim();
|
||
var list = this.ipRules || [];
|
||
if (!q) return list;
|
||
return list.filter(function (item) {
|
||
return (
|
||
(item.ip || "").toLowerCase().indexOf(q) !== -1 ||
|
||
(item.remark || "").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);
|
||
},
|
||
pagedIpRules: function () {
|
||
return this.getPagedData(this.filteredIpRules, this.ipRulePagination);
|
||
},
|
||
canCreateIpRule: function () {
|
||
if (this.isAdmin) return true;
|
||
return !(this.ipRules || []).some(function (rule) {
|
||
return rule && rule.ip;
|
||
});
|
||
},
|
||
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>
|