-
路由配置(routes)
-
新增一行
+
+
+
+
+ 保存配置到 config.json
+ 重载服务内存配置
+ 刷新页面数据
+
-
+
+
+
+
+
+
+ 路由配置(routes)
+ 新增路由
+
+
+
+
+ {{ scope.row.apiName || "-" }}
+
+
-
+ {{ scope.row.route }}
-
+ {{ scope.row.filePath }}
-
+
+
+
+
+
+
+
+ 修改
+
删除
+
- 说明:如果路由以 # 开头,会作为注释保留,不参与 mock 命中。
+ 说明:关闭“启用”后会以 # 注释路由,不参与 mock 命中;切换后请点击“保存配置到 config.json”生效到文件。
-
+
+
-
- 动态新增接口配置文件
-
-
+
+
+
+ Mock 数据配置
+ 新增 Mock 文件
+
+
+
+
+ {{ scope.row.filePath }}
+
+
+
+
+ {{ getPreviewText(scope.row.content) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- 创建路由+文件
-
-
-
-
- 保存配置到 config.json
- 重载服务内存配置
- 刷新页面数据
-
-
+
+
+
@@ -158,6 +312,17 @@
el: "#app",
data: function () {
return {
+ activeMainTab: "routes",
+ apiList: [],
+ mockFiles: [],
+ routePagination: {
+ currentPage: 1,
+ pageSize: 10,
+ },
+ mockFilePagination: {
+ currentPage: 1,
+ pageSize: 10,
+ },
form: {
config: {
mockEnabled: true,
@@ -171,22 +336,78 @@
},
routes: [],
},
- newRoute: {
- template: "custom",
- route: "",
- filePath: "mock/",
- fileContent: "",
- overwrite: false,
+ routeDialog: {
+ visible: false,
+ mode: "create",
+ editingIndex: -1,
+ form: {
+ apiName: "",
+ selectedApiRoute: "",
+ originalRoute: "",
+ originalRawRoute: "",
+ route: "",
+ filePath: "",
+ enabled: true,
+ },
+ },
+ mockFileDialog: {
+ visible: false,
+ mode: "create",
+ form: {
+ filePath: "mock/",
+ content: "",
+ },
},
};
},
- created: function () {
- this.loadConfig();
+ created: async function () {
+ await this.loadApiList();
+ await this.loadMockFiles();
+ await this.loadConfig();
},
methods: {
+ getPreviewText: function (content) {
+ var text = String(content || "").replace(/\s+/g, " ").trim();
+ if (!text) return "(空文件)";
+ return text.length > 120 ? text.slice(0, 120) + "..." : text;
+ },
+ getPagedData: function (list, pagination) {
+ var source = Array.isArray(list) ? list : [];
+ var pageSize = pagination.pageSize || 10;
+ var currentPage = pagination.currentPage || 1;
+ var maxPage = Math.max(1, Math.ceil(source.length / pageSize));
+ if (currentPage > maxPage) {
+ currentPage = maxPage;
+ pagination.currentPage = currentPage;
+ }
+ var start = (currentPage - 1) * pageSize;
+ return source.slice(start, start + pageSize);
+ },
+ 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) {
- return Object.keys(routesObj || {}).map(function (route) {
- return { route: route, filePath: routesObj[route] };
+ var self = this;
+ return Object.keys(routesObj || {}).map(function (rawRoute) {
+ var enabled = !rawRoute.startsWith("#");
+ var route = enabled ? rawRoute : rawRoute.replace(/^#+/, "");
+ var matched = self.findApiByRoute(route);
+ return {
+ rawRoute: rawRoute,
+ route: route,
+ filePath: routesObj[rawRoute],
+ apiName: matched ? matched.name : "",
+ selectedApiRoute: matched ? matched.route : "",
+ enabled: enabled,
+ };
});
},
toRouteObject: function (routeArray) {
@@ -195,16 +416,162 @@
var route = (item.route || "").trim();
var filePath = (item.filePath || "").trim();
if (route && filePath) {
- obj[route] = filePath;
+ var routeKey = item.enabled === false ? "#" + route : route;
+ obj[routeKey] = filePath;
}
});
return obj;
},
- addRouteRow: function () {
- this.form.routes.push({ route: "", filePath: "" });
- },
removeRoute: function (index) {
- this.form.routes.splice(index, 1);
+ var actualIndex =
+ (this.routePagination.currentPage - 1) * this.routePagination.pageSize +
+ index;
+ this.form.routes.splice(actualIndex, 1);
+ },
+ getDefaultRouteForm: function () {
+ return {
+ apiName: "",
+ selectedApiRoute: "",
+ originalRoute: "",
+ originalRawRoute: "",
+ route: "",
+ filePath: "",
+ enabled: true,
+ };
+ },
+ isApiListLocked: function () {
+ return !!this.routeDialog.form.selectedApiRoute;
+ },
+ onSelectApiRoute: function (route) {
+ var selected = route ? this.findApiByRoute(route) : null;
+ if (!selected) {
+ this.routeDialog.form.selectedApiRoute = "";
+ return;
+ }
+ this.routeDialog.form.selectedApiRoute = selected.route;
+ this.routeDialog.form.route = selected.route;
+ this.routeDialog.form.apiName = selected.name;
+ },
+ openRouteDialogForCreate: function () {
+ this.routeDialog.mode = "create";
+ this.routeDialog.editingIndex = -1;
+ this.routeDialog.form = this.getDefaultRouteForm();
+ this.routeDialog.visible = true;
+ },
+ openRouteDialogForEdit: function (index) {
+ var actualIndex =
+ (this.routePagination.currentPage - 1) * this.routePagination.pageSize +
+ index;
+ var item =
+ this.form.routes[actualIndex] || { route: "", filePath: "mock/" };
+ this.routeDialog.mode = "edit";
+ this.routeDialog.editingIndex = actualIndex;
+ var matched = this.findApiByRoute(item.route || "");
+ this.routeDialog.form = {
+ apiName: matched ? matched.name : item.apiName || "",
+ selectedApiRoute: matched ? matched.route : "",
+ originalRoute: item.route || "",
+ originalRawRoute: item.rawRoute || item.route || "",
+ route: item.route || "",
+ filePath: item.filePath || "mock/",
+ enabled: item.enabled !== false,
+ };
+ this.routeDialog.visible = true;
+ },
+ getDefaultMockFileForm: function () {
+ return {
+ filePath: "mock/",
+ content: "",
+ };
+ },
+ openMockFileDialogForCreate: function () {
+ this.mockFileDialog.mode = "create";
+ this.mockFileDialog.form = this.getDefaultMockFileForm();
+ this.mockFileDialog.visible = true;
+ },
+ openMockFileDialogForEdit: function (item) {
+ this.mockFileDialog.mode = "edit";
+ this.mockFileDialog.form = {
+ filePath: item.filePath || "mock/",
+ content: String(item.content || ""),
+ };
+ this.mockFileDialog.visible = true;
+ },
+ loadMockFiles: async function () {
+ try {
+ var resp = await fetch("/__mock-files");
+ var data = await resp.json();
+ if (!resp.ok || data.success === false) {
+ throw new Error(data.error || "加载 mock 文件失败");
+ }
+ this.mockFiles = Array.isArray(data.list) ? data.list : [];
+ this.mockFilePagination.currentPage = 1;
+ } catch (err) {
+ this.mockFiles = [];
+ this.$message.error("加载 mock 文件失败: " + err.message);
+ }
+ },
+ submitMockFileDialog: async function () {
+ if (!this.mockFileDialog.form.filePath) {
+ this.$message.error("Mock 文件路径不能为空");
+ return;
+ }
+ try {
+ var resp = await fetch("/__mock-files", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ filePath: this.mockFileDialog.form.filePath,
+ content: this.mockFileDialog.form.content,
+ }),
+ });
+ var data = await resp.json();
+ if (!resp.ok || data.success === false) {
+ throw new Error(data.error || "保存 mock 文件失败");
+ }
+ this.$message.success("Mock 文件已保存");
+ this.mockFileDialog.visible = false;
+ await this.loadMockFiles();
+ } catch (err) {
+ this.$message.error("保存 mock 文件失败: " + err.message);
+ }
+ },
+ removeMockFile: async function (item) {
+ try {
+ await this.$confirm(
+ "确认删除 " + item.filePath + " 吗?删除后引用该文件的路由需要重新选择。",
+ "提示",
+ { type: "warning" },
+ );
+ var resp = await fetch("/__mock-files", {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ filePath: item.filePath }),
+ });
+ var data = await resp.json();
+ if (!resp.ok || data.success === false) {
+ throw new Error(data.error || "删除 mock 文件失败");
+ }
+ this.$message.success("Mock 文件已删除");
+ await this.loadMockFiles();
+ } catch (err) {
+ if (err !== "cancel") {
+ this.$message.error("删除 mock 文件失败: " + err.message);
+ }
+ }
+ },
+ loadApiList: async function () {
+ try {
+ var resp = await fetch("/__api-list");
+ var data = await resp.json();
+ if (!resp.ok || data.success === false) {
+ throw new Error(data.error || "加载接口列表失败");
+ }
+ this.apiList = Array.isArray(data.list) ? data.list : [];
+ } catch (err) {
+ this.apiList = [];
+ this.$message.error("加载接口列表失败: " + err.message);
+ }
},
loadConfig: async function () {
try {
@@ -212,6 +579,7 @@
var data = await resp.json();
this.form.config = Object.assign({}, this.form.config, data.config || {});
this.form.routes = this.toRouteArray(data.routes || {});
+ this.routePagination.currentPage = 1;
} catch (err) {
this.$message.error("加载配置失败: " + err.message);
}
@@ -249,42 +617,47 @@
this.$message.error("重载失败: " + err.message);
}
},
- createRouteAndFile: async function () {
+ submitRouteDialog: async function () {
+ if (!this.routeDialog.form.route) {
+ this.$message.error("请求路径不能为空");
+ return;
+ }
+ if (!this.routeDialog.form.filePath) {
+ this.$message.error("请选择 Mock 文件");
+ return;
+ }
+ var payload = Object.assign({}, this.routeDialog.form, {
+ useExistingFile: true,
+ });
try {
var resp = await fetch("/__routes", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify(
- Object.assign({}, this.newRoute, {
- useExistingFile: this.newRoute.template === "basicError",
- }),
- ),
+ body: JSON.stringify(payload),
});
var data = await resp.json();
if (!resp.ok || data.success === false) {
throw new Error(data.error || "创建失败");
}
- this.$message.success("接口配置文件创建成功");
- this.newRoute.template = "custom";
- this.newRoute.route = "";
- this.newRoute.filePath = "mock/";
- this.newRoute.fileContent = "";
- this.newRoute.overwrite = false;
- this.loadConfig();
+ this.$message.success(
+ this.routeDialog.mode === "edit"
+ ? "路由修改成功"
+ : "接口配置文件创建成功",
+ );
+ this.routeDialog.visible = false;
+ await this.loadMockFiles();
+ await this.loadConfig();
} catch (err) {
this.$message.error("创建失败: " + err.message);
}
},
- applyTemplate: function (template) {
- if (template === "basicError") {
- this.newRoute.filePath = "mock/basic-error.json";
- this.newRoute.fileContent =
- '{"code": -1, "success": false, "msg":"失败"}';
- this.newRoute.overwrite = false;
- return;
- }
- this.newRoute.filePath = "mock/";
- this.newRoute.fileContent = "";
+ },
+ computed: {
+ pagedRoutes: function () {
+ return this.getPagedData(this.form.routes, this.routePagination);
+ },
+ pagedMockFiles: function () {
+ return this.getPagedData(this.mockFiles, this.mockFilePagination);
},
},
});
diff --git a/index.api.ts b/index.api.ts
index 539815d..9530dc1 100644
--- a/index.api.ts
+++ b/index.api.ts
@@ -6,6 +6,8 @@ import * as zlib from "zlib";
// 配置文件路径
const CONFIG_FILE = path.join(__dirname, "config.json");
+const API_LIST_FILE = path.join(__dirname, "mock", "api-list.json");
+const MOCK_DIR = path.join(__dirname, "mock");
// 定义类型
interface RouteConfig {
@@ -31,6 +33,16 @@ interface ConfigFile {
config: AppConfig;
}
+interface ApiListItem {
+ name: string;
+ route: string;
+}
+
+interface MockFileItem {
+ filePath: string;
+ content: string;
+}
+
// 存储当前的路由配置
let MOCK_ROUTES: RouteConfig = {};
let RAW_ROUTES: RouteConfig = {};
@@ -160,6 +172,93 @@ function saveConfigFile(nextConfig: ConfigFile): void {
CONFIG = nextConfig.config || CONFIG;
}
+function loadApiList(): ApiListItem[] {
+ if (!fs.existsSync(API_LIST_FILE)) {
+ return [];
+ }
+ try {
+ const text = fs.readFileSync(API_LIST_FILE, "utf-8").trim();
+ if (!text) {
+ return [];
+ }
+ const parsed = JSON.parse(text);
+ if (!Array.isArray(parsed)) {
+ return [];
+ }
+ const seenRoutes = new Set
();
+ const list: ApiListItem[] = [];
+ for (const item of parsed) {
+ const route = String(item?.route || "").trim();
+ const name = String(item?.name || "").trim();
+ if (!route || !route.startsWith("/") || !name || seenRoutes.has(route)) {
+ continue;
+ }
+ seenRoutes.add(route);
+ list.push({ name, route });
+ }
+ return list;
+ } catch (error) {
+ console.warn("[ADMIN] 读取 api-list.json 失败:", error);
+ return [];
+ }
+}
+
+function normalizeMockFilePath(filePath: string): string {
+ const trimmed = filePath.trim().replace(/\\/g, "/");
+ if (!trimmed) {
+ throw new Error("filePath is required");
+ }
+ const relative = trimmed.startsWith("mock/") ? trimmed : `mock/${trimmed}`;
+ if (path.isAbsolute(relative)) {
+ throw new Error("filePath must be a relative path");
+ }
+ return relative;
+}
+
+function resolveMockFullPath(relativePath: string): string {
+ const normalized = normalizeMockFilePath(relativePath);
+ const fullPath = path.resolve(__dirname, normalized);
+ const mockRoot = path.resolve(MOCK_DIR);
+ if (!fullPath.startsWith(mockRoot)) {
+ throw new Error("filePath is invalid");
+ }
+ if (path.basename(fullPath) === "api-list.json") {
+ throw new Error("api-list.json is read-only in this panel");
+ }
+ return fullPath;
+}
+
+function walkMockFiles(dir: string, baseDir: string, result: string[]): void {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walkMockFiles(fullPath, baseDir, result);
+ continue;
+ }
+ if (!entry.isFile()) continue;
+ if (entry.name === "api-list.json") continue;
+ const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/");
+ result.push(`mock/${relative}`);
+ }
+}
+
+function loadMockFiles(): MockFileItem[] {
+ if (!fs.existsSync(MOCK_DIR)) {
+ return [];
+ }
+ const files: string[] = [];
+ walkMockFiles(MOCK_DIR, MOCK_DIR, files);
+ files.sort((a, b) => a.localeCompare(b));
+ return files.map((filePath) => {
+ const fullPath = path.join(__dirname, filePath);
+ return {
+ filePath,
+ content: fs.readFileSync(fullPath, "utf-8"),
+ };
+ });
+}
+
function readBody(req: http.IncomingMessage): Promise {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
@@ -375,28 +474,48 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
readBody(clientReq)
.then((bodyText) => {
const body = bodyText ? JSON.parse(bodyText) : {};
- const route = String(body.route || "").trim();
+ let route = String(body.route || "").trim();
const filePath = String(body.filePath || "").trim();
const fileContent = String(body.fileContent || "");
const overwrite = body.overwrite === true;
const template = String(body.template || "").trim();
+ const apiName = String(body.apiName || "").trim();
+ const selectedApiRoute = String(body.selectedApiRoute || "").trim();
+ const originalRoute = String(body.originalRoute || "").trim();
+ const originalRawRoute = String(body.originalRawRoute || "").trim();
+ const enabled = body.enabled !== false;
const useExistingFile =
body.useExistingFile === true || template === "basicError";
+ const apiList = loadApiList();
+ const selectedApi = selectedApiRoute
+ ? apiList.find((item) => item.route === selectedApiRoute)
+ : undefined;
+ const originalApi = originalRoute
+ ? apiList.find((item) => item.route === originalRoute)
+ : undefined;
+
+ // 来自 api-list 的接口路由不可在新增/修改时变更
+ if (selectedApi) {
+ if (route && route !== selectedApi.route) {
+ throw new Error("api-list route cannot be changed");
+ }
+ if (apiName && apiName !== selectedApi.name) {
+ throw new Error("api-list name cannot be changed");
+ }
+ route = selectedApi.route;
+ }
+ if (originalApi && route !== originalApi.route) {
+ throw new Error("api-list route cannot be changed");
+ }
+ if (originalApi && apiName && apiName !== originalApi.name) {
+ throw new Error("api-list name cannot be changed");
+ }
if (!route.startsWith("/")) {
throw new Error("route must start with '/'");
}
- if (!filePath) {
- throw new Error("filePath is required");
- }
- if (path.isAbsolute(filePath)) {
- throw new Error("filePath must be a relative path");
- }
-
- const fullPath = path.join(__dirname, filePath);
- if (!fullPath.startsWith(__dirname)) {
- throw new Error("filePath is invalid");
- }
+ const normalizedFilePath = normalizeMockFilePath(filePath);
+ const fullPath = resolveMockFullPath(normalizedFilePath);
if (useExistingFile) {
if (!fs.existsSync(fullPath)) {
throw new Error("mock file does not exist");
@@ -412,7 +531,21 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
}
const nextConfig = buildCurrentConfigFile();
- nextConfig.routes[route] = filePath;
+ const nextRouteKey = enabled ? route : `#${route}`;
+ const oldRouteCandidates = new Set();
+ if (originalRawRoute) {
+ oldRouteCandidates.add(originalRawRoute);
+ }
+ if (originalRoute) {
+ oldRouteCandidates.add(originalRoute);
+ oldRouteCandidates.add(`#${originalRoute}`);
+ }
+ oldRouteCandidates.forEach((key) => {
+ if (key && key !== nextRouteKey) {
+ delete nextConfig.routes[key];
+ }
+ });
+ nextConfig.routes[nextRouteKey] = normalizedFilePath;
saveConfigFile(nextConfig);
clientRes.writeHead(200, { "Content-Type": "application/json" });
@@ -421,7 +554,111 @@ const proxyServer = http.createServer((clientReq, clientRes) => {
success: true,
message: "Route and mock file created successfully",
route,
- filePath,
+ routeKey: nextRouteKey,
+ filePath: normalizedFilePath,
+ enabled,
+ }),
+ );
+ })
+ .catch((error) => {
+ clientRes.writeHead(400, { "Content-Type": "application/json" });
+ clientRes.end(
+ JSON.stringify({
+ success: false,
+ error: (error as Error).message,
+ }),
+ );
+ });
+ return;
+ }
+
+ if (requestPath === "/__api-list") {
+ if (clientReq.method !== "GET") {
+ clientRes.writeHead(405, {
+ "Content-Type": "application/json",
+ Allow: "GET",
+ });
+ clientRes.end(
+ JSON.stringify({
+ success: false,
+ error: "Method Not Allowed",
+ allow: ["GET"],
+ }),
+ );
+ return;
+ }
+ clientRes.writeHead(200, { "Content-Type": "application/json" });
+ clientRes.end(
+ JSON.stringify({
+ success: true,
+ list: loadApiList(),
+ }),
+ );
+ return;
+ }
+
+ if (requestPath === "/__mock-files") {
+ if (
+ clientReq.method !== "GET" &&
+ clientReq.method !== "POST" &&
+ clientReq.method !== "DELETE"
+ ) {
+ clientRes.writeHead(405, {
+ "Content-Type": "application/json",
+ Allow: "GET, POST, DELETE",
+ });
+ clientRes.end(
+ JSON.stringify({
+ success: false,
+ error: "Method Not Allowed",
+ allow: ["GET", "POST", "DELETE"],
+ }),
+ );
+ return;
+ }
+
+ if (clientReq.method === "GET") {
+ clientRes.writeHead(200, { "Content-Type": "application/json" });
+ clientRes.end(
+ JSON.stringify({
+ success: true,
+ list: loadMockFiles(),
+ }),
+ );
+ return;
+ }
+
+ readBody(clientReq)
+ .then((bodyText) => {
+ const body = bodyText ? JSON.parse(bodyText) : {};
+ const filePath = String(body.filePath || "").trim();
+ const fullPath = resolveMockFullPath(filePath);
+ const normalizedPath = normalizeMockFilePath(filePath);
+
+ if (clientReq.method === "POST") {
+ const content = String(body.content || "");
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
+ fs.writeFileSync(fullPath, content, "utf-8");
+ clientRes.writeHead(200, { "Content-Type": "application/json" });
+ clientRes.end(
+ JSON.stringify({
+ success: true,
+ filePath: normalizedPath,
+ }),
+ );
+ return;
+ }
+
+ // DELETE
+ if (!fs.existsSync(fullPath)) {
+ throw new Error("mock file does not exist");
+ }
+ fs.unlinkSync(fullPath);
+ clientRes.writeHead(200, { "Content-Type": "application/json" });
+ clientRes.end(
+ JSON.stringify({
+ success: true,
+ filePath: normalizedPath,
}),
);
})
diff --git a/mock/api-list.json b/mock/api-list.json
new file mode 100644
index 0000000..5581cdb
--- /dev/null
+++ b/mock/api-list.json
@@ -0,0 +1,60 @@
+[
+ { "name": "登录/注册发送验证码", "route": "/api1/authentication/sms/send" },
+ { "name": "绑定手机号发送验证码", "route": "/api1/authentication/bind/send" },
+ { "name": "账号注销", "route": "/api1/account/revoke" },
+ { "name": "绑定手机号", "route": "/api1/account/bind" },
+ { "name": "认证授权刷新接口", "route": "/api1/oauth2/token" },
+ { "name": "一键登录阿里云授权码", "route": "/api1/authentication/common" },
+
+ { "name": "更新用户信息", "route": "/api2/user/update" },
+ { "name": "获取用户信息", "route": "/api2/user" },
+ { "name": "我的链接", "route": "/api2/link" },
+ { "name": "获取字典类型", "route": "/api2/dict/types" },
+ { "name": "获取字典项", "route": "/api2/dict/" },
+ { "name": "获取设备列表", "route": "/api2/device/list" },
+ { "name": "根据类型获取设备列表", "route": "/api2/device/type/list" },
+ { "name": "查询历史设备列表", "route": "/api2/device/history/list" },
+ { "name": "绑定设备", "route": "/api2/device/save" },
+ { "name": "切换设备", "route": "/api2/device/switch" },
+ { "name": "用机人主设备", "route": "/api2/device/master" },
+ { "name": "解绑设备", "route": "/api2/device/unbind" },
+ { "name": "获取设备信息", "route": "/api2/device/one" },
+ { "name": "获取设备店铺", "route": "/api2/device/store" },
+ { "name": "获取设备图片", "route": "/api2/device/bind/img" },
+ { "name": "查询设备报告日期", "route": "/api2/device/report/date" },
+ { "name": "查询是否有报告页", "route": "/api2/device/reports" },
+
+ { "name": "查询使用教程列表", "route": "/api2/course/list" },
+ { "name": "查询白脸教程列表", "route": "/api2/course/white/face/list" },
+ { "name": "获取省份城市列表", "route": "/api2/sys/provinces" },
+ { "name": "制氧机报告单天", "route": "/api2/oxygenerator/report/day" },
+ { "name": "制氧机报告多天", "route": "/api2/oxygenerator/report/multi/day" },
+ { "name": "血氧仪报告单天", "route": "/api2/oximeter/report/day" },
+ { "name": "血氧仪报告多天", "route": "/api2/oximeter/report/multi/day" },
+ { "name": "呼吸机报告单天", "route": "/api2/ventilator/report/day" },
+ { "name": "呼吸机报告多天", "route": "/api2/ventilator/report/multi/day" },
+ { "name": "血氧仪健康报告单天", "route": "/api2/oximeter/health/report/day" },
+ { "name": "血氧仪健康报告多天", "route": "/api2/oximeter/health/report/multi/day" },
+ { "name": "血氧仪报告详情", "route": "/api2/oximeter/report/detail" },
+ { "name": "首页判断", "route": "/api2/home/flag" },
+ { "name": "首页Banner", "route": "/api2/home/ad/list" },
+ { "name": "首页卡片信息", "route": "/api2/home/card/info" },
+ { "name": "修改首页卡片", "route": "/api2/home/card/update" },
+ { "name": "协议列表", "route": "/api2/protocol/list" },
+ { "name": "同意协议", "route": "/api2/protocol/agree" },
+ { "name": "协议是否更新", "route": "/api2/protocol/has/update" },
+
+ { "name": "扫码获取报告", "route": "/api2/qr/scan" },
+ { "name": "查询QR报告详情", "route": "/api2/qr/one" },
+ { "name": "查询QR报告列表", "route": "/api2/qr/page" },
+ { "name": "获取报告数量", "route": "/api2/qr/count" },
+
+ { "name": "检查健康自测人数", "route": "/api2/self/check/head/count" },
+ { "name": "健康自测结果", "route": "/api2/self/check/save" },
+
+ { "name": "科普分类列表", "route": "/api2/health/category/list" },
+ { "name": "科普列表", "route": "/api2/health/service/list" },
+ { "name": "科普详情", "route": "/api2/health/service/getById" },
+
+ { "name": "deepSeek流式接口", "route": "/api2/deepSeek/stream" }
+]