json-api-mocker 2.3.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ch.md CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  - 🚀 通过 JSON 配置快速搭建
14
14
  - 🔄 支持 GET、POST、PUT、DELETE 方法
15
+ - 🖥️ 可视化管理后台,无需手写 JSON
15
16
  - 📝 自动数据持久化
16
17
  - 🔍 内置分页支持
17
18
  - 🛠 可自定义响应结构
@@ -121,7 +122,8 @@ npm run mock
121
122
  你会看到类似这样的输出:
122
123
  ```bash
123
124
  Mock 服务器已启动:
124
- - 地址:http://localhost:8080
125
+ - HTTP 地址:http://localhost:8080
126
+ - 管理后台:http://localhost:8080/admin
125
127
  - 基础路径:/api
126
128
  可用的接口:
127
129
  GET http://localhost:8080/api/users
@@ -129,6 +131,24 @@ Mock 服务器已启动:
129
131
  POST http://localhost:8080/api/upload/avatar
130
132
  ```
131
133
 
134
+ ## 🖥️ 可视化管理后台
135
+
136
+ 不想手写 JSON 配置文件?启动服务后,打开浏览器访问管理后台:
137
+
138
+ ```
139
+ http://localhost:8080/admin
140
+ ```
141
+
142
+ 管理后台提供以下功能:
143
+
144
+ - 📝 **可视化编辑**:通过表单配置路由、接口方法、Mock 字段,无需了解 JSON 格式
145
+ - 🎨 **字段构建器**:通过下拉菜单选择字段类型(随机姓名、年龄范围、邮箱、城市等),自动生成 Mock.js 模板
146
+ - 💻 **JSON 源码编辑**:切换到源码模式直接编辑 JSON,支持格式化和语法检查
147
+ - 👀 **接口预览**:查看所有可用接口,GET 接口可直接点击测试
148
+ - 💾 **一键保存**:修改后点击保存,配置自动写回 `data.json` 文件
149
+
150
+ > 💡 保存配置后,静态数据修改会立即生效;新增路由需要重启服务才能完全生效。
151
+
132
152
  ## 📖 配置指南
133
153
 
134
154
  详细配置请参考 [CONFIG.ch.md](./CONFIG.ch.md)。
package/README.md CHANGED
@@ -12,6 +12,7 @@ A lightweight and flexible mock server that uses JSON configuration to quickly c
12
12
 
13
13
  - 🚀 Quick setup with JSON configuration
14
14
  - 🔄 Support for GET, POST, PUT, DELETE methods
15
+ - 🖥️ Visual admin console, no JSON editing required
15
16
  - 📝 Automatic data persistence
16
17
  - 🔍 Built-in pagination support
17
18
  - 🛠 Customizable response schemas
@@ -120,15 +121,34 @@ Now your mock server is running at `http://localhost:8080`!
120
121
 
121
122
  You'll see output like this:
122
123
  ```bash
123
- Mock Server is running:
124
- - Address: http://localhost:8080
125
- - Base Path: /api
126
- Available APIs:
124
+ Mock 服务器已启动:
125
+ - HTTP 地址: http://localhost:8080
126
+ - 管理后台: http://localhost:8080/admin
127
+ - 基础路径: /api
128
+ 可用的接口:
127
129
  GET http://localhost:8080/api/users
128
130
  POST http://localhost:8080/api/users
129
131
  POST http://localhost:8080/api/upload/avatar
130
132
  ```
131
133
 
134
+ ## 🖥️ Visual Admin Console
135
+
136
+ Don't want to write JSON config by hand? After starting the server, open the admin console in your browser:
137
+
138
+ ```
139
+ http://localhost:8080/admin
140
+ ```
141
+
142
+ The admin console provides the following features:
143
+
144
+ - 📝 **Visual Editor**: Configure routes, methods, and Mock fields via forms — no JSON knowledge required
145
+ - 🎨 **Field Builder**: Select field types (random name, age range, email, city, etc.) from a dropdown to auto-generate Mock.js templates
146
+ - 💻 **JSON Source Editor**: Switch to source mode to edit JSON directly, with formatting and syntax checking
147
+ - 👀 **API Preview**: View all available APIs; GET endpoints can be tested with one click
148
+ - 💾 **One-click Save**: Save changes and they're automatically written back to `data.json`
149
+
150
+ > 💡 After saving, static data changes take effect immediately; newly added routes require a server restart.
151
+
132
152
  ## 📖 Configuration Guide
133
153
 
134
154
  For detailed configuration options, please refer to [CONFIG.md](./CONFIG.md).
package/dist/index.js CHANGED
@@ -17,8 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.MockServer = void 0;
21
- exports.startServer = startServer;
20
+ exports.MockServer = exports.startServer = void 0;
22
21
  const fs_1 = __importDefault(require("fs"));
23
22
  const path_1 = __importDefault(require("path"));
24
23
  const server_1 = require("./server");
@@ -35,6 +34,7 @@ function startServer(configPath = 'data.json') {
35
34
  process.exit(1);
36
35
  }
37
36
  }
37
+ exports.startServer = startServer;
38
38
  if (require.main === module) {
39
39
  startServer();
40
40
  }
@@ -0,0 +1,908 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>JSON API Mocker 控制台</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
11
+ background: #f0f2f5;
12
+ color: #1f2937;
13
+ font-size: 14px;
14
+ }
15
+ /* 顶部栏 */
16
+ .topbar {
17
+ background: #fff;
18
+ border-bottom: 1px solid #e5e7eb;
19
+ padding: 0 24px;
20
+ height: 60px;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ position: sticky;
25
+ top: 0;
26
+ z-index: 100;
27
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
28
+ }
29
+ .topbar-left { display: flex; align-items: center; gap: 12px; }
30
+ .logo { font-size: 18px; font-weight: 700; color: #2563eb; }
31
+ .logo-sub { color: #9ca3af; font-size: 13px; }
32
+ .topbar-right { display: flex; align-items: center; gap: 12px; }
33
+ .status-badge {
34
+ padding: 4px 10px;
35
+ border-radius: 12px;
36
+ font-size: 12px;
37
+ font-weight: 500;
38
+ }
39
+ .status-ok { background: #dcfce7; color: #16a34a; }
40
+ .status-saving { background: #fef3c7; color: #d97706; }
41
+ .status-error { background: #fee2e2; color: #dc2626; }
42
+ /* 按钮 */
43
+ .btn {
44
+ padding: 7px 16px;
45
+ border-radius: 6px;
46
+ border: none;
47
+ cursor: pointer;
48
+ font-size: 13px;
49
+ font-weight: 500;
50
+ transition: all 0.15s;
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 6px;
54
+ }
55
+ .btn-primary { background: #2563eb; color: #fff; }
56
+ .btn-primary:hover { background: #1d4ed8; }
57
+ .btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
58
+ .btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
59
+ .btn-secondary:hover { background: #e5e7eb; }
60
+ .btn-danger { background: #fee2e2; color: #dc2626; }
61
+ .btn-danger:hover { background: #fecaca; }
62
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
63
+ .btn-ghost { background: transparent; color: #6b7280; }
64
+ .btn-ghost:hover { background: #f3f4f6; }
65
+ /* Tab */
66
+ .tabs {
67
+ display: flex;
68
+ gap: 4px;
69
+ padding: 12px 24px 0;
70
+ background: #fff;
71
+ border-bottom: 1px solid #e5e7eb;
72
+ }
73
+ .tab {
74
+ padding: 8px 16px;
75
+ cursor: pointer;
76
+ border-bottom: 2px solid transparent;
77
+ color: #6b7280;
78
+ font-weight: 500;
79
+ font-size: 13px;
80
+ }
81
+ .tab.active { color: #2563eb; border-bottom-color: #2563eb; }
82
+ /* 容器 */
83
+ .container { max-width: 1000px; margin: 0 auto; padding: 24px; }
84
+ /* 卡片 */
85
+ .card {
86
+ background: #fff;
87
+ border-radius: 10px;
88
+ padding: 20px;
89
+ margin-bottom: 16px;
90
+ border: 1px solid #e5e7eb;
91
+ }
92
+ .card-header {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ margin-bottom: 16px;
97
+ }
98
+ .card-title { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
99
+ .card-icon { width: 8px; height: 8px; border-radius: 50%; background: #2563eb; }
100
+ /* 表单 */
101
+ .form-row { display: flex; gap: 16px; margin-bottom: 12px; flex-wrap: wrap; }
102
+ .form-group { display: flex; flex-direction: column; gap: 4px; }
103
+ .form-group.flex1 { flex: 1; min-width: 160px; }
104
+ label { font-size: 12px; color: #6b7280; font-weight: 500; }
105
+ input, select, textarea {
106
+ border: 1px solid #d1d5db;
107
+ border-radius: 6px;
108
+ padding: 7px 10px;
109
+ font-size: 13px;
110
+ font-family: inherit;
111
+ outline: none;
112
+ transition: border-color 0.15s;
113
+ }
114
+ input:focus, select:focus, textarea:focus { border-color: #2563eb; }
115
+ input[type="number"] { width: 90px; }
116
+ textarea { resize: vertical; font-family: "SFMono-Regular", Consolas, monospace; }
117
+ /* 开关 */
118
+ .switch { position: relative; display: inline-block; width: 40px; height: 22px; }
119
+ .switch input { opacity: 0; width: 0; height: 0; }
120
+ .slider {
121
+ position: absolute; cursor: pointer; inset: 0;
122
+ background: #d1d5db; border-radius: 22px; transition: 0.2s;
123
+ }
124
+ .slider:before {
125
+ content: ""; position: absolute; height: 16px; width: 16px;
126
+ left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.2s;
127
+ }
128
+ .switch input:checked + .slider { background: #2563eb; }
129
+ .switch input:checked + .slider:before { transform: translateX(18px); }
130
+ .switch-label { display: flex; align-items: center; gap: 8px; }
131
+ /* 路由卡片 */
132
+ .route-card { border-left: 3px solid #2563eb; }
133
+ .route-header {
134
+ display: flex; align-items: center; justify-content: space-between;
135
+ cursor: pointer;
136
+ }
137
+ .route-path {
138
+ font-family: "SFMono-Regular", Consolas, monospace;
139
+ font-size: 14px; font-weight: 600; color: #1f2937;
140
+ }
141
+ .method-tags { display: flex; gap: 4px; margin-top: 4px; }
142
+ .method-tag {
143
+ padding: 2px 8px; border-radius: 4px; font-size: 11px;
144
+ font-weight: 600; font-family: monospace;
145
+ }
146
+ .method-get { background: #dcfce7; color: #16a34a; }
147
+ .method-post { background: #dbeafe; color: #2563eb; }
148
+ .method-put { background: #fef3c7; color: #d97706; }
149
+ .method-delete { background: #fee2e2; color: #dc2626; }
150
+ .route-body { margin-top: 16px; }
151
+ .route-body.collapsed { display: none; }
152
+ /* 方法配置 */
153
+ .method-config {
154
+ background: #f9fafb; border-radius: 8px; padding: 16px;
155
+ margin-bottom: 12px; border: 1px solid #e5e7eb;
156
+ }
157
+ .method-config-header {
158
+ display: flex; align-items: center; justify-content: space-between;
159
+ margin-bottom: 12px;
160
+ }
161
+ .method-name {
162
+ font-family: monospace; font-weight: 700; font-size: 13px;
163
+ padding: 3px 8px; border-radius: 4px;
164
+ }
165
+ /* 字段构建器 */
166
+ .field-row {
167
+ display: flex; gap: 8px; align-items: center; margin-bottom: 8px;
168
+ padding: 8px; background: #fff; border-radius: 6px; border: 1px solid #e5e7eb;
169
+ }
170
+ .field-name { width: 120px; }
171
+ .field-type { width: 150px; }
172
+ .field-value { flex: 1; }
173
+ .field-remove {
174
+ width: 28px; height: 28px; border: none; background: transparent;
175
+ color: #9ca3af; cursor: pointer; border-radius: 4px; font-size: 16px;
176
+ }
177
+ .field-remove:hover { background: #fee2e2; color: #dc2626; }
178
+ .field-add {
179
+ padding: 6px 12px; border: 1px dashed #d1d5db; background: transparent;
180
+ border-radius: 6px; cursor: pointer; color: #6b7280; font-size: 12px;
181
+ }
182
+ .field-add:hover { border-color: #2563eb; color: #2563eb; }
183
+ /* JSON 编辑 */
184
+ .json-editor {
185
+ width: 100%; min-height: 500px; font-family: "SFMono-Regular", Consolas, monospace;
186
+ font-size: 13px; padding: 16px; border-radius: 8px; border: 1px solid #d1d5db;
187
+ line-height: 1.6; tab-size: 2;
188
+ }
189
+ .json-editor.error { border-color: #dc2626; }
190
+ .json-error { color: #dc2626; font-size: 12px; margin-top: 6px; }
191
+ /* 预览 */
192
+ .preview-list { display: flex; flex-direction: column; gap: 6px; }
193
+ .preview-item {
194
+ display: flex; align-items: center; gap: 8px; padding: 8px 12px;
195
+ background: #f9fafb; border-radius: 6px; font-family: monospace; font-size: 12px;
196
+ }
197
+ /* 提示 */
198
+ .empty-hint {
199
+ text-align: center; padding: 40px; color: #9ca3af; font-size: 14px;
200
+ }
201
+ .hint-text { color: #9ca3af; font-size: 12px; margin-top: 4px; }
202
+ .section-title {
203
+ font-size: 12px; font-weight: 600; color: #6b7280;
204
+ text-transform: uppercase; letter-spacing: 0.5px; margin: 12px 0 8px;
205
+ }
206
+ /* Toast */
207
+ .toast {
208
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
209
+ padding: 10px 20px; border-radius: 8px; color: #fff; font-size: 13px;
210
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 200;
211
+ animation: fadeIn 0.2s;
212
+ }
213
+ .toast-success { background: #16a34a; }
214
+ .toast-error { background: #dc2626; }
215
+ @keyframes fadeIn { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; } }
216
+ .collapse-arrow { transition: transform 0.2s; color: #9ca3af; }
217
+ .collapse-arrow.collapsed { transform: rotate(-90deg); }
218
+ .ws-event-card {
219
+ background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px;
220
+ padding: 12px; margin-bottom: 8px;
221
+ }
222
+ </style>
223
+ </head>
224
+ <body>
225
+ <div id="app">
226
+ <!-- 顶部栏 -->
227
+ <div class="topbar">
228
+ <div class="topbar-left">
229
+ <span class="logo">Mock Server</span>
230
+ <span class="logo-sub">控制台</span>
231
+ </div>
232
+ <div class="topbar-right">
233
+ <span class="status-badge" :class="statusClass">{{ statusText }}</span>
234
+ <button class="btn btn-secondary" @click="loadConfig" :disabled="saving">重新加载</button>
235
+ <button class="btn btn-primary" @click="saveConfig" :disabled="saving">保存配置</button>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- Tab 切换 -->
240
+ <div class="tabs">
241
+ <div class="tab" :class="{active: tab==='visual'}" @click="tab='visual'">可视化编辑</div>
242
+ <div class="tab" :class="{active: tab==='json'}" @click="tab='json'">JSON 源码</div>
243
+ <div class="tab" :class="{active: tab==='preview'}" @click="tab='preview'">接口预览</div>
244
+ </div>
245
+
246
+ <!-- 可视化编辑 -->
247
+ <div class="container" v-show="tab==='visual'">
248
+ <!-- 服务器配置 -->
249
+ <div class="card">
250
+ <div class="card-header">
251
+ <div class="card-title"><span class="card-icon"></span>服务器配置</div>
252
+ </div>
253
+ <div class="form-row">
254
+ <div class="form-group">
255
+ <label>端口号</label>
256
+ <input type="number" v-model.number="config.server.port">
257
+ </div>
258
+ <div class="form-group flex1">
259
+ <label>基础路径 (baseProxy)</label>
260
+ <input v-model="config.server.baseProxy" placeholder="/api">
261
+ </div>
262
+ </div>
263
+ </div>
264
+
265
+ <!-- 路由配置 -->
266
+ <div class="card" v-for="(route, ri) in config.routes" :key="ri">
267
+ <div class="card-header route-header" @click="toggleRoute(ri)">
268
+ <div>
269
+ <div class="route-path">{{ route.path }}</div>
270
+ <div class="method-tags">
271
+ <span v-for="m in Object.keys(route.methods)" :key="m"
272
+ class="method-tag" :class="'method-'+m">{{ m.toUpperCase() }}</span>
273
+ </div>
274
+ </div>
275
+ <div style="display:flex;gap:8px;align-items:center">
276
+ <span class="collapse-arrow" :class="{collapsed: !expandedRoutes[ri]}">▼</span>
277
+ <button class="btn btn-danger btn-sm" @click.stop="removeRoute(ri)">删除路由</button>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="route-body" :class="{collapsed: !expandedRoutes[ri]}">
282
+ <div class="form-group" style="margin-bottom:16px">
283
+ <label>路由路径</label>
284
+ <input v-model="route.path" placeholder="/users" style="max-width:300px">
285
+ </div>
286
+
287
+ <!-- 方法配置 -->
288
+ <div class="method-config" v-for="(mc, method) in route.methods" :key="method">
289
+ <div class="method-config-header">
290
+ <span class="method-name" :class="'method-'+method">{{ method.toUpperCase() }}</span>
291
+ <button class="btn btn-danger btn-sm" @click="removeMethod(route, method)">移除方法</button>
292
+ </div>
293
+
294
+ <div class="form-row">
295
+ <div class="form-group">
296
+ <label>响应类型</label>
297
+ <select v-model="mc.type">
298
+ <option value="array">array (数组)</option>
299
+ <option value="object">object (对象)</option>
300
+ </select>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Mock 配置 -->
305
+ <div class="section-title">Mock 数据生成</div>
306
+ <div class="switch-label" style="margin-bottom:8px">
307
+ <label class="switch">
308
+ <input type="checkbox" v-model="mc.mock.enabled">
309
+ <span class="slider"></span>
310
+ </label>
311
+ <span style="font-size:13px">启用 Mock.js 随机数据</span>
312
+ </div>
313
+
314
+ <div v-if="mc.mock.enabled">
315
+ <div class="form-row" v-if="mc.type==='array'">
316
+ <div class="form-group">
317
+ <label>生成条数</label>
318
+ <input type="number" v-model.number="mc.mock.total" min="1">
319
+ </div>
320
+ </div>
321
+
322
+ <!-- 字段构建器 -->
323
+ <div class="section-title">返回字段 <span class="hint-text">(选择字段类型,自动生成随机数据)</span></div>
324
+ <div v-for="(field, fi) in getFields(mc.mock.template)" :key="fi" class="field-row">
325
+ <input class="field-name" v-model="field.name" placeholder="字段名">
326
+ <select class="field-type" v-model="field.type" @change="onFieldTypeChange(field)">
327
+ <option value="increment">自增 ID</option>
328
+ <option value="name">随机姓名(英文)</option>
329
+ <option value="cname">随机姓名(中文)</option>
330
+ <option value="integer">随机整数(范围)</option>
331
+ <option value="float">随机小数(范围)</option>
332
+ <option value="email">随机邮箱</option>
333
+ <option value="city">随机城市(英文)</option>
334
+ <option value="county">随机城市(中文)</option>
335
+ <option value="datetime">随机日期时间</option>
336
+ <option value="date">随机日期</option>
337
+ <option value="image">随机图片</option>
338
+ <option value="url">随机 URL</option>
339
+ <option value="paragraph">随机段落</option>
340
+ <option value="sentence">随机句子</option>
341
+ <option value="title">随机标题</option>
342
+ <option value="boolean">随机布尔值</option>
343
+ <option value="pick">随机选择(多项)</option>
344
+ </div>
345
+ <input class="field-value" v-model="field.value" :placeholder="fieldPlaceholder(field)">
346
+ <button class="field-remove" @click="removeField(mc.mock.template, fi)">×</button>
347
+ </div>
348
+ <button class="field-add" @click="addField(mc.mock.template)">+ 添加字段</button>
349
+ </div>
350
+
351
+ <!-- 静态响应 (未启用 mock 时) -->
352
+ <div v-if="!mc.mock.enabled">
353
+ <div class="section-title">静态响应数据 (JSON)</div>
354
+ <textarea v-model="mc._responseText" rows="5"
355
+ placeholder='{"success": true}' style="width:100%;font-family:monospace"></textarea>
356
+ <div class="json-error" v-if="mc._responseError">{{ mc._responseError }}</div>
357
+ </div>
358
+
359
+ <!-- 分页配置 -->
360
+ <div class="section-title" v-if="mc.type==='array'">分页设置</div>
361
+ <div v-if="mc.type==='array'">
362
+ <div class="switch-label" style="margin-bottom:8px">
363
+ <label class="switch">
364
+ <input type="checkbox" v-model="mc.pagination.enabled">
365
+ <span class="slider"></span>
366
+ </label>
367
+ <span style="font-size:13px">启用分页</span>
368
+ </div>
369
+ <div class="form-row" v-if="mc.pagination.enabled">
370
+ <div class="form-group">
371
+ <label>每页条数</label>
372
+ <input type="number" v-model.number="mc.pagination.pageSize">
373
+ </div>
374
+ <div class="form-group">
375
+ <label>数据总数</label>
376
+ <input type="number" v-model.number="mc.pagination.totalCount">
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+
382
+ <!-- 添加方法 -->
383
+ <div style="margin-top:12px">
384
+ <select v-model="newMethod[ri]" style="width:auto">
385
+ <option value="">+ 添加方法...</option>
386
+ <option value="get" v-if="!route.methods.get">GET</option>
387
+ <option value="post" v-if="!route.methods.post">POST</option>
388
+ <option value="put" v-if="!route.methods.put">PUT</option>
389
+ <option value="delete" v-if="!route.methods.delete">DELETE</option>
390
+ </select>
391
+ <button class="btn btn-secondary btn-sm" v-if="newMethod[ri]" @click="addMethod(route, ri)">添加</button>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <button class="btn btn-secondary" @click="addRoute" style="width:100%;padding:10px">+ 添加路由</button>
397
+
398
+ <!-- WebSocket 配置 -->
399
+ <div class="card" style="margin-top:16px">
400
+ <div class="card-header">
401
+ <div class="card-title"><span class="card-icon"></span>WebSocket 实时通信</div>
402
+ <div class="switch-label">
403
+ <label class="switch">
404
+ <input type="checkbox" v-model="config.websocket.enabled">
405
+ <span class="slider"></span>
406
+ </label>
407
+ <span style="font-size:13px">启用</span>
408
+ </div>
409
+ </div>
410
+ <div v-if="config.websocket.enabled">
411
+ <div class="form-row">
412
+ <div class="form-group flex1">
413
+ <label>WebSocket 路径</label>
414
+ <input v-model="config.websocket.path" placeholder="/ws">
415
+ </div>
416
+ </div>
417
+ <div class="section-title">事件列表</div>
418
+ <div class="ws-event-card" v-for="(ev, ename) in config.websocket.events" :key="ename">
419
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
420
+ <input v-model="ev._eventName" placeholder="事件名" style="font-weight:600;width:200px" @change="renameEvent(config.websocket.events, ename, ev._eventName)">
421
+ <button class="btn btn-danger btn-sm" @click="removeEvent(config.websocket.events, ename)">删除</button>
422
+ </div>
423
+ <div class="switch-label" style="margin-bottom:8px">
424
+ <label class="switch">
425
+ <input type="checkbox" v-model="ev.mock.enabled">
426
+ <span class="slider"></span>
427
+ </label>
428
+ <span style="font-size:13px">启用 Mock</span>
429
+ </div>
430
+ <div class="form-row" v-if="ev.mock.enabled">
431
+ <div class="form-group">
432
+ <label>自动推送间隔 (ms, 0=不自动推送)</label>
433
+ <input type="number" v-model.number="ev.mock.interval">
434
+ </div>
435
+ </div>
436
+ <div v-if="ev.mock.enabled">
437
+ <div class="section-title">返回字段</div>
438
+ <div v-for="(field, fi) in getFields(ev.mock.template)" :key="fi" class="field-row">
439
+ <input class="field-name" v-model="field.name" placeholder="字段名">
440
+ <select class="field-type" v-model="field.type" @change="onFieldTypeChange(field)">
441
+ <option value="increment">自增 ID</option>
442
+ <option value="name">随机姓名(英文)</option>
443
+ <option value="cname">随机姓名(中文)</option>
444
+ <option value="integer">随机整数(范围)</option>
445
+ <option value="float">随机小数(范围)</option>
446
+ <option value="email">随机邮箱</option>
447
+ <option value="city">随机城市(英文)</option>
448
+ <option value="county">随机城市(中文)</option>
449
+ <option value="datetime">随机日期时间</option>
450
+ <option value="date">随机日期</option>
451
+ <option value="image">随机图片</option>
452
+ <option value="paragraph">随机段落</option>
453
+ <option value="sentence">随机句子</option>
454
+ <option value="boolean">随机布尔值</option>
455
+ <option value="pick">随机选择(多项)</option>
456
+ </select>
457
+ <input class="field-value" v-model="field.value" :placeholder="fieldPlaceholder(field)">
458
+ <button class="field-remove" @click="removeField(ev.mock.template, fi)">×</button>
459
+ </div>
460
+ <button class="field-add" @click="addField(ev.mock.template)">+ 添加字段</button>
461
+ </div>
462
+ </div>
463
+ <button class="btn btn-secondary btn-sm" @click="addEvent(config.websocket.events)">+ 添加事件</button>
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <!-- JSON 源码 -->
469
+ <div class="container" v-show="tab==='json'">
470
+ <div class="card">
471
+ <div class="card-header">
472
+ <div class="card-title"><span class="card-icon"></span>直接编辑 JSON 源码</div>
473
+ <div style="display:flex;gap:8px">
474
+ <button class="btn btn-secondary btn-sm" @click="formatJson">格式化</button>
475
+ <button class="btn btn-secondary btn-sm" @click="syncToJson">从可视化同步</button>
476
+ </div>
477
+ </div>
478
+ <textarea class="json-editor" :class="{error: jsonError}" v-model="jsonText" @input="syncFromJson"></textarea>
479
+ <div class="json-error" v-if="jsonError">{{ jsonError }}</div>
480
+ </div>
481
+ </div>
482
+
483
+ <!-- 接口预览 -->
484
+ <div class="container" v-show="tab==='preview'">
485
+ <div class="card">
486
+ <div class="card-header">
487
+ <div class="card-title"><span class="card-icon"></span>可用接口列表</div>
488
+ </div>
489
+ <div class="preview-list" v-if="config.routes.length">
490
+ <div class="preview-item" v-for="r in previewRoutes" :key="r.method+r.path">
491
+ <span class="method-tag" :class="'method-'+r.method">{{ r.method.toUpperCase() }}</span>
492
+ <span>{{ config.server.baseProxy }}{{ r.path }}</span>
493
+ <a :href="'http://'+host+config.server.baseProxy+r.path" target="_blank"
494
+ v-if="r.method==='get'" style="margin-left:auto;color:#2563eb;text-decoration:none;font-size:12px">测试 →</a>
495
+ </div>
496
+ </div>
497
+ <div class="empty-hint" v-else>暂无接口,请在"可视化编辑"中添加路由</div>
498
+ </div>
499
+ <div class="card" v-if="config.websocket && config.websocket.enabled">
500
+ <div class="card-header">
501
+ <div class="card-title"><span class="card-icon"></span>WebSocket 接口</div>
502
+ </div>
503
+ <div class="preview-item">
504
+ <span class="method-tag" style="background:#ede9fe;color:#7c3aed">WS</span>
505
+ <span>ws://{{ host }}{{ config.websocket.path }}</span>
506
+ </div>
507
+ </div>
508
+ </div>
509
+
510
+ <!-- Toast -->
511
+ <div class="toast" :class="'toast-'+toast.type" v-if="toast.show">{{ toast.msg }}</div>
512
+ </div>
513
+
514
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
515
+ <script>
516
+ const { createApp, ref, reactive, computed, onMounted } = Vue;
517
+
518
+ // 字段类型 -> Mock.js 表达式生成器
519
+ const FIELD_SPECS = {
520
+ increment: { placeholder: '起始值,如 1', needValue: true },
521
+ name: { placeholder: '无需填写', needValue: false },
522
+ cname: { placeholder: '无需填写', needValue: false },
523
+ integer: { placeholder: '范围,如 18-60', needValue: true },
524
+ float: { placeholder: '范围,如 1-100 | 1-2', needValue: true },
525
+ email: { placeholder: '无需填写', needValue: false },
526
+ city: { placeholder: '无需填写', needValue: false },
527
+ county: { placeholder: '无需填写', needValue: false },
528
+ datetime: { placeholder: '格式,如 yyyy-MM-dd (可选)', needValue: false },
529
+ date: { placeholder: '无需填写', needValue: false },
530
+ image: { placeholder: '尺寸,如 200x200', needValue: true },
531
+ url: { placeholder: '无需填写', needValue: false },
532
+ paragraph: { placeholder: '无需填写', needValue: false },
533
+ sentence: { placeholder: '无需填写', needValue: false },
534
+ title: { placeholder: '无需填写', needValue: false },
535
+ boolean: { placeholder: '无需填写', needValue: false },
536
+ pick: { placeholder: '选项,用逗号分隔,如 男,女', needValue: true },
537
+ };
538
+
539
+ createApp({
540
+ setup() {
541
+ const config = reactive({
542
+ server: { port: 8080, baseProxy: '/api' },
543
+ routes: [],
544
+ websocket: { enabled: false, path: '/ws', events: {} }
545
+ });
546
+ const tab = ref('visual');
547
+ const saving = ref(false);
548
+ const status = ref('ok'); // ok | saving | error
549
+ const expandedRoutes = reactive({});
550
+ const newMethod = reactive({});
551
+ const jsonText = ref('');
552
+ const jsonError = ref('');
553
+ const toast = reactive({ show: false, msg: '', type: 'success' });
554
+ const host = ref(location.host);
555
+
556
+ const statusClass = computed(() => ({
557
+ ok: 'status-ok', saving: 'status-saving', error: 'status-error'
558
+ }[status.value]));
559
+ const statusText = computed(() => ({
560
+ ok: '已加载', saving: '保存中...', error: '保存失败'
561
+ }[status.value]));
562
+
563
+ const previewRoutes = computed(() => {
564
+ const list = [];
565
+ config.routes.forEach(r => {
566
+ Object.keys(r.methods || {}).forEach(m => list.push({ method: m, path: r.path }));
567
+ });
568
+ return list;
569
+ });
570
+
571
+ function showToast(msg, type = 'success') {
572
+ toast.msg = msg; toast.type = type; toast.show = true;
573
+ setTimeout(() => toast.show = false, 2500);
574
+ }
575
+
576
+ // 加载配置
577
+ async function loadConfig() {
578
+ try {
579
+ const res = await fetch('/admin/api/config');
580
+ const data = await res.json();
581
+ Object.assign(config, normalizeConfig(data));
582
+ syncToJson();
583
+ status.value = 'ok';
584
+ showToast('配置已加载');
585
+ } catch (e) {
586
+ status.value = 'error';
587
+ showToast('加载失败: ' + e.message, 'error');
588
+ }
589
+ }
590
+
591
+ // 规范化配置,补充默认值和辅助字段
592
+ function normalizeConfig(data) {
593
+ if (!data.server) data.server = { port: 8080, baseProxy: '/api' };
594
+ if (!data.routes) data.routes = [];
595
+ if (!data.websocket) data.websocket = { enabled: false, path: '/ws', events: {} };
596
+ if (!data.websocket.events) data.websocket.events = {};
597
+ if (!data.websocket.path) data.websocket.path = '/ws';
598
+
599
+ data.routes.forEach((route, ri) => {
600
+ expandedRoutes[ri] = expandedRoutes[ri] !== undefined ? expandedRoutes[ri] : true;
601
+ if (!route.methods) route.methods = {};
602
+ Object.entries(route.methods).forEach(([_, mc]) => {
603
+ if (!mc.type) mc.type = 'array';
604
+ if (!mc.mock) mc.mock = { enabled: false, template: {} };
605
+ if (!mc.mock.template) mc.mock.template = {};
606
+ if (mc.mock.total === undefined) mc.mock.total = 10;
607
+ if (!mc.pagination) mc.pagination = { enabled: false, pageSize: 10, totalCount: 100 };
608
+ // response 文本
609
+ mc._responseText = mc.response !== undefined ? JSON.stringify(mc.response, null, 2) : '';
610
+ mc._responseError = '';
611
+ });
612
+ });
613
+
614
+ // WebSocket events 补充 _eventName
615
+ Object.entries(data.websocket.events || {}).forEach(([ename, ev]) => {
616
+ ev._eventName = ename;
617
+ if (!ev.mock) ev.mock = { enabled: false, template: {} };
618
+ if (!ev.mock.template) ev.mock.template = {};
619
+ });
620
+ return data;
621
+ }
622
+
623
+ // 保存配置
624
+ async function saveConfig() {
625
+ // 先同步可视化 -> JSON
626
+ syncToJson();
627
+ if (jsonError.value) {
628
+ showToast('JSON 格式错误,请检查', 'error');
629
+ tab.value = 'json';
630
+ return;
631
+ }
632
+ saving.value = true;
633
+ status.value = 'saving';
634
+ try {
635
+ const res = await fetch('/admin/api/config', {
636
+ method: 'PUT',
637
+ headers: { 'Content-Type': 'application/json' },
638
+ body: jsonText.value
639
+ });
640
+ const result = await res.json();
641
+ if (result.success) {
642
+ status.value = 'ok';
643
+ showToast('保存成功!部分改动(新增路由)需重启服务生效');
644
+ } else {
645
+ throw new Error(result.error || '保存失败');
646
+ }
647
+ } catch (e) {
648
+ status.value = 'error';
649
+ showToast('保存失败: ' + e.message, 'error');
650
+ } finally {
651
+ saving.value = false;
652
+ }
653
+ }
654
+
655
+ // ===== 路由操作 =====
656
+ function addRoute() {
657
+ const newRoute = {
658
+ path: '/new-route',
659
+ methods: {
660
+ get: {
661
+ type: 'array',
662
+ mock: { enabled: true, total: 10, template: {} },
663
+ pagination: { enabled: false, pageSize: 10, totalCount: 100 },
664
+ _responseText: '', _responseError: ''
665
+ }
666
+ }
667
+ };
668
+ config.routes.push(newRoute);
669
+ expandedRoutes[config.routes.length - 1] = true;
670
+ }
671
+ function removeRoute(ri) {
672
+ if (!confirm('确定删除此路由?')) return;
673
+ config.routes.splice(ri, 1);
674
+ }
675
+ function toggleRoute(ri) {
676
+ expandedRoutes[ri] = !expandedRoutes[ri];
677
+ }
678
+
679
+ // ===== 方法操作 =====
680
+ function addMethod(route, ri) {
681
+ const m = newMethod[ri];
682
+ if (!m || route.methods[m]) return;
683
+ route.methods[m] = {
684
+ type: m === 'get' ? 'array' : 'object',
685
+ mock: { enabled: false, total: 10, template: {} },
686
+ pagination: { enabled: false, pageSize: 10, totalCount: 100 },
687
+ _responseText: '', _responseError: ''
688
+ };
689
+ newMethod[ri] = '';
690
+ }
691
+ function removeMethod(route, method) {
692
+ if (!confirm('确定移除 ' + method.toUpperCase() + ' 方法?')) return;
693
+ delete route.methods[method];
694
+ }
695
+
696
+ // ===== 字段构建器 =====
697
+ // 把 Mock.js template 转成可编辑的字段数组(挂在 __fields 上,缓存)
698
+ function getFields(template) {
699
+ if (!template.__fields) {
700
+ template.__fields = Object.entries(template)
701
+ .filter(([k]) => k !== '__fields')
702
+ .map(([key, val]) => parseMockField(key, val));
703
+ }
704
+ return template.__fields;
705
+ }
706
+
707
+ function parseMockField(key, val) {
708
+ // 解析 Mock.js 的 key|rule: value 格式
709
+ const pipeIdx = key.indexOf('|');
710
+ let name = key, rule = '', type = 'custom', value = '';
711
+ if (pipeIdx >= 0) {
712
+ name = key.substring(0, pipeIdx);
713
+ rule = key.substring(pipeIdx + 1);
714
+ }
715
+ if (rule === '+1') { type = 'increment'; value = String(val); }
716
+ else if (rule.match(/^\d+-\d+$/)) {
717
+ type = typeof val === 'number' && !Number.isInteger(val) ? 'float' : 'integer';
718
+ value = rule;
719
+ }
720
+ else if (typeof val === 'string' && val.startsWith('@')) {
721
+ const fn = val.substring(1).split('(')[0];
722
+ const argMatch = val.match(/\((.*)\)/);
723
+ const arg = argMatch ? argMatch[1].replace(/['"]/g, '') : '';
724
+ const map = { name:'name', cname:'cname', email:'email', city:'city',
725
+ county:'county', datetime:'datetime', date:'date', image:'image',
726
+ url:'url', paragraph:'paragraph', sentence:'sentence', title:'title' };
727
+ type = map[fn] || 'custom';
728
+ value = arg;
729
+ if (type === 'custom') value = val;
730
+ }
731
+ else if (typeof val === 'boolean') { type = 'boolean'; value = ''; }
732
+ else if (Array.isArray(val) && rule === '1') { type = 'pick'; value = val.join(','); }
733
+ else { type = 'custom'; value = typeof val === 'object' ? JSON.stringify(val) : String(val); }
734
+ return { name, type, value, _key: key };
735
+ }
736
+
737
+ function buildMockKey(field) {
738
+ const { name, type, value } = field;
739
+ switch (type) {
740
+ case 'increment': return { key: name + '|+1', val: Number(value) || 1 };
741
+ case 'integer': {
742
+ const v = value || '1-100';
743
+ return { key: name + '|' + v, val: 1 };
744
+ }
745
+ case 'float': {
746
+ const v = value || '1-100';
747
+ return { key: name + '|' + v, val: 1 };
748
+ }
749
+ case 'name': return { key: name, val: '@name' };
750
+ case 'cname': return { key: name, val: '@cname' };
751
+ case 'email': return { key: name, val: '@email' };
752
+ case 'city': return { key: name, val: '@city' };
753
+ case 'county': return { key: name, val: '@county(true)' };
754
+ case 'datetime': return { key: name, val: value ? '@datetime("' + value + '")' : '@datetime' };
755
+ case 'date': return { key: name, val: '@date' };
756
+ case 'image': return { key: name, val: "@image('" + (value || '200x200') + "')" };
757
+ case 'url': return { key: name, val: '@url' };
758
+ case 'paragraph': return { key: name, val: '@paragraph' };
759
+ case 'sentence': return { key: name, val: '@sentence' };
760
+ case 'title': return { key: name, val: '@title' };
761
+ case 'boolean': return { key: name + '|1', val: true };
762
+ case 'pick': {
763
+ const items = (value || 'a,b').split(',').map(s => s.trim()).filter(Boolean);
764
+ return { key: name + '|1', val: items };
765
+ }
766
+ default: return { key: name, val: value };
767
+ }
768
+ }
769
+
770
+ function onFieldTypeChange(field) {
771
+ // 类型改变时,重置 value
772
+ const spec = FIELD_SPECS[field.type];
773
+ if (!spec.needValue) field.value = '';
774
+ }
775
+
776
+ function fieldPlaceholder(field) {
777
+ return (FIELD_SPECS[field.type] || { placeholder: '' }).placeholder;
778
+ }
779
+
780
+ function addField(template) {
781
+ if (!template.__fields) template.__fields = [];
782
+ template.__fields.push({ name: 'field' + (template.__fields.length + 1), type: 'name', value: '' });
783
+ }
784
+
785
+ function removeField(template, fi) {
786
+ template.__fields.splice(fi, 1);
787
+ }
788
+
789
+ // ===== WebSocket 事件操作 =====
790
+ function addEvent(events) {
791
+ let name = 'event-' + (Object.keys(events).length + 1);
792
+ while (events[name]) name += '-1';
793
+ events[name] = { _eventName: name, mock: { enabled: true, template: {}, interval: 0 } };
794
+ }
795
+ function removeEvent(events, ename) {
796
+ if (!confirm('确定删除事件 ' + ename + '?')) return;
797
+ delete events[ename];
798
+ }
799
+ function renameEvent(events, oldName, newName) {
800
+ if (!newName || oldName === newName) return;
801
+ if (events[newName]) { showToast('事件名已存在', 'error'); return; }
802
+ const ev = events[oldName];
803
+ delete events[oldName];
804
+ events[newName] = ev;
805
+ }
806
+
807
+ // ===== JSON 同步 =====
808
+ function buildCleanConfig() {
809
+ const out = {
810
+ server: { port: config.server.port, baseProxy: config.server.baseProxy },
811
+ routes: config.routes.map(route => {
812
+ const methods = {};
813
+ Object.entries(route.methods).forEach(([m, mc]) => {
814
+ // 解析 response 文本
815
+ let response = mc.response;
816
+ if (mc._responseText !== undefined && mc._responseText !== '') {
817
+ try { response = JSON.parse(mc._responseText); } catch (e) { /* 忽略,前端已提示 */ }
818
+ }
819
+ const methodCfg = { type: mc.type };
820
+ if (mc.mock && mc.mock.enabled) {
821
+ // 从字段构建器生成 template
822
+ const template = {};
823
+ (mc.mock.template.__fields || []).forEach(f => {
824
+ const r = buildMockKey(f);
825
+ template[r.key] = r.val;
826
+ });
827
+ methodCfg.mock = { enabled: true, template };
828
+ if (mc.type === 'array') methodCfg.mock.total = mc.mock.total;
829
+ } else {
830
+ methodCfg.response = response !== undefined ? response : null;
831
+ }
832
+ if (mc.type === 'array' && mc.pagination && mc.pagination.enabled) {
833
+ methodCfg.pagination = {
834
+ enabled: true,
835
+ pageSize: mc.pagination.pageSize,
836
+ totalCount: mc.pagination.totalCount
837
+ };
838
+ }
839
+ methods[m] = methodCfg;
840
+ });
841
+ return { path: route.path, methods };
842
+ })
843
+ };
844
+ if (config.websocket && config.websocket.enabled) {
845
+ const events = {};
846
+ Object.entries(config.websocket.events || {}).forEach(([_, ev]) => {
847
+ const template = {};
848
+ (ev.mock.template.__fields || []).forEach(f => {
849
+ const r = buildMockKey(f);
850
+ template[r.key] = r.val;
851
+ });
852
+ events[ev._eventName] = {
853
+ mock: { enabled: ev.mock.enabled, template, interval: ev.mock.interval || undefined }
854
+ };
855
+ });
856
+ out.websocket = { enabled: true, path: config.websocket.path, events };
857
+ }
858
+ return out;
859
+ }
860
+
861
+ function syncToJson() {
862
+ try {
863
+ jsonText.value = JSON.stringify(buildCleanConfig(), null, 2);
864
+ jsonError.value = '';
865
+ } catch (e) {
866
+ jsonError.value = e.message;
867
+ }
868
+ }
869
+
870
+ function syncFromJson() {
871
+ try {
872
+ const parsed = JSON.parse(jsonText.value);
873
+ jsonError.value = '';
874
+ // 不立即覆盖可视化(避免输入中途破坏),用户点"重新加载"可应用
875
+ } catch (e) {
876
+ jsonError.value = 'JSON 语法错误: ' + e.message;
877
+ }
878
+ }
879
+
880
+ function formatJson() {
881
+ try {
882
+ jsonText.value = JSON.stringify(JSON.parse(jsonText.value), null, 2);
883
+ jsonError.value = '';
884
+ showToast('已格式化');
885
+ } catch (e) {
886
+ jsonError.value = 'JSON 语法错误: ' + e.message;
887
+ showToast('格式化失败', 'error');
888
+ }
889
+ }
890
+
891
+ onMounted(() => { loadConfig(); });
892
+
893
+ return {
894
+ config, tab, saving, status, statusClass, statusText,
895
+ expandedRoutes, newMethod, jsonText, jsonError, toast, host,
896
+ previewRoutes,
897
+ loadConfig, saveConfig,
898
+ addRoute, removeRoute, toggleRoute,
899
+ addMethod, removeMethod,
900
+ getFields, addField, removeField, onFieldTypeChange, fieldPlaceholder,
901
+ addEvent, removeEvent, renameEvent,
902
+ syncToJson, syncFromJson, formatJson,
903
+ };
904
+ }
905
+ }).mount('#app');
906
+ </script>
907
+ </body>
908
+ </html>
package/dist/server.d.ts CHANGED
@@ -12,6 +12,8 @@ export declare class MockServer {
12
12
  private logRequest;
13
13
  private generateMockData;
14
14
  private handleRequest;
15
+ private setupAdminRoutes;
16
+ private getAdminHtml;
15
17
  private setupRoutes;
16
18
  private createRoute;
17
19
  private findRouteConfig;
package/dist/server.js CHANGED
@@ -7,6 +7,8 @@ exports.MockServer = void 0;
7
7
  const mockjs_1 = __importDefault(require("mockjs"));
8
8
  const express_1 = __importDefault(require("express"));
9
9
  const cors_1 = __importDefault(require("cors"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
10
12
  const ws_1 = require("ws");
11
13
  const http_1 = __importDefault(require("http"));
12
14
  class MockServer {
@@ -34,6 +36,7 @@ class MockServer {
34
36
  this.configPath = configPath;
35
37
  this.server = http_1.default.createServer(this.app);
36
38
  this.setupMiddleware();
39
+ this.setupAdminRoutes();
37
40
  this.setupRoutes();
38
41
  if (config.websocket?.enabled) {
39
42
  this.setupWebSocket();
@@ -84,6 +87,57 @@ class MockServer {
84
87
  }
85
88
  };
86
89
  }
90
+ setupAdminRoutes() {
91
+ // 管理后台页面
92
+ this.app.get('/admin', (req, res) => {
93
+ const html = this.getAdminHtml();
94
+ if (html) {
95
+ res.type('html').send(html);
96
+ }
97
+ else {
98
+ res.status(404).send('Admin page not found');
99
+ }
100
+ });
101
+ // 获取当前配置
102
+ this.app.get('/admin/api/config', (req, res) => {
103
+ res.json(this.config);
104
+ });
105
+ // 保存配置
106
+ this.app.put('/admin/api/config', (req, res) => {
107
+ try {
108
+ const newConfig = req.body;
109
+ // 简单校验
110
+ if (!newConfig.server || !newConfig.routes) {
111
+ res.status(400).json({ success: false, error: '配置格式不正确:缺少 server 或 routes' });
112
+ return;
113
+ }
114
+ // 写入文件
115
+ const fullPath = path_1.default.resolve(process.cwd(), this.configPath);
116
+ fs_1.default.writeFileSync(fullPath, JSON.stringify(newConfig, null, 2), 'utf-8');
117
+ // 更新内存中的配置
118
+ this.config = newConfig;
119
+ console.log(`配置已保存到: ${fullPath}`);
120
+ res.json({ success: true, message: '配置已保存,部分改动(如新增路由)需重启服务生效' });
121
+ }
122
+ catch (error) {
123
+ console.error('保存配置失败:', error);
124
+ res.status(500).json({ success: false, error: error.message });
125
+ }
126
+ });
127
+ }
128
+ getAdminHtml() {
129
+ const candidates = [
130
+ path_1.default.join(__dirname, 'public', 'admin.html'),
131
+ path_1.default.join(__dirname, '..', 'public', 'admin.html'),
132
+ path_1.default.join(process.cwd(), 'public', 'admin.html'),
133
+ ];
134
+ for (const p of candidates) {
135
+ if (fs_1.default.existsSync(p)) {
136
+ return fs_1.default.readFileSync(p, 'utf-8');
137
+ }
138
+ }
139
+ return null;
140
+ }
87
141
  setupRoutes() {
88
142
  this.config.routes.forEach((route) => {
89
143
  Object.entries(route.methods).forEach(([method, methodConfig]) => {
@@ -178,6 +232,7 @@ class MockServer {
178
232
  this.server.listen(this.config.server.port, () => {
179
233
  console.log(`Mock 服务器已启动:`);
180
234
  console.log(`- HTTP 地址: http://localhost:${this.config.server.port}`);
235
+ console.log(`- 管理后台: http://localhost:${this.config.server.port}/admin`);
181
236
  if (this.config.websocket?.enabled) {
182
237
  console.log(`- WebSocket 地址: ws://localhost:${this.config.server.port}${this.config.websocket.path}`);
183
238
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "json-api-mocker",
3
- "version": "2.3.1",
3
+ "version": "3.1.0",
4
4
  "description": "A mock server based on JSON configuration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "bin": {
8
- "json-api-mocker": "./dist/cli.js"
8
+ "json-api-mocker": "dist/cli.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "dev": "nodemon",
19
- "build": "tsc",
19
+ "build": "tsc && node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.cpSync('public','dist/public',{recursive:true})\"",
20
20
  "start": "node dist/index.js",
21
21
  "test": "jest",
22
22
  "test:watch": "jest --watch",