整理代码

This commit is contained in:
2026-07-14 11:24:41 +08:00
parent 9d02c0a840
commit 7fac3c7f97
34 changed files with 53 additions and 38 deletions
+7
View File
@@ -0,0 +1,7 @@
next/node_modules/
next/dist/
next/.env
next/.env.local
next/.env.*.local
report/
tmp/
-2
View File
@@ -1,2 +0,0 @@
# 仅供本地 Vite 代理读取,不会通过 VITE_ 前缀打包到浏览器。
QHSE_API_TOKEN='AF17168B-87BD-4GLY-1111-F0A0A1158F9B'
-5
View File
@@ -1,5 +0,0 @@
node_modules/
dist/
.env
.env.local
.env.*.local
+5 -6
View File
@@ -102,15 +102,14 @@ next/
### 安全与部署边界
- 浏览器只访问同源 `/qhse_webapi/`,前端构建产物不包含接口 token。
- 本地 token 仅写入 `.env.local`,由 Vite 开发代理注入。
- 生产环境由网关反向代理 `https://qhse.cncecoa.com/qhse_webapi` 并注入 token。
- 浏览器只访问同源 `/qhse_webapi/`,由开发服务器或生产网关转发到正式接口。
- 接口 token 固定在 `config/dashboard.ts`,由 `services/http.ts` 写入每个请求的 `token` 请求头。
- token 会进入浏览器构建产物,只适用于当前受控内网部署环境。
- 生产环境仍需将 `/qhse_webapi/` 反向代理到 `https://qhse.cncecoa.com/qhse_webapi`,但无需在网关重复注入 token。
- 网关应配置上游超时、请求 ID 透传、访问日志和 5xx 告警。
## 本地开发
复制 `.env.example` 为 `.env.local`,填写仅供本地代理使用的 `QHSE_API_TOKEN`,然后执行:
```powershell
npm install
npm run dev
@@ -133,7 +132,7 @@ npm run typecheck
npm run build
```
产物位于 `dist/`,可部署到任意静态 Web 服务。生产环境需要由同源网关代理 `/qhse_webapi/`,并在服务端注入接口 token;token 不进入浏览器构建产物。
产物位于 `dist/`,可部署到静态 Web 服务。生产环境需要由同源网关代理 `/qhse_webapi/`;token 已由前端请求头携带。
建议网关同时配置 12 秒上游超时、请求 ID 透传、访问日志和 5xx 告警。应用采用哈希路由,无需 Web 服务器配置 SPA history fallback。
+4 -1
View File
@@ -1,9 +1,12 @@
/**
* 看板静态配置。
* 这里只存放接口相对路径、外部链接和枚举映射,认证令牌必须由开发代理或生产网关注入。
* 这里只存放接口相对路径、固定令牌、外部链接和枚举映射,业务模块统一从这里读取运行配置。
*/
export const API_BASE = "/qhse_webapi/api/DataGovernance";
/** 数据治理接口固定令牌,由统一请求服务写入请求头。 */
export const API_TOKEN = "AF17168B-87BD-4GLY-1111-F0A0A1158F9B";
export const API_ENDPOINTS = {
monitoring: `${API_BASE}/GetMetricMonitoringSituation`,
unitMonitoring: `${API_BASE}/GetUnitMetricMonitoringSituation`,
+33 -10
View File
@@ -3,12 +3,13 @@
* 负责查询参数编码、请求 ID、超时/取消和业务错误归一化,不承载具体业务字段转换。
*/
import type { ApiEnvelope } from "../types/dashboard";
import { API_TOKEN } from "../config/dashboard";
export class ApiError extends Error {
constructor(
message: string,
public readonly status = 0,
public readonly requestId = ""
public readonly requestId = "",
) {
super(message);
this.name = "ApiError";
@@ -18,7 +19,7 @@ export class ApiError extends Error {
export async function getJson<T>(
url: string,
params: Record<string, string | number | undefined>,
options: { timeout?: number; signal?: AbortSignal } = {}
options: { timeout?: number; signal?: AbortSignal } = {},
): Promise<ApiEnvelope<T>> {
const requestId = crypto.randomUUID();
const target = new URL(url, window.location.origin);
@@ -29,24 +30,46 @@ export async function getJson<T>(
// 外部 signal 与内部超时共用一个控制器,调用方取消和超时都走相同错误出口。
const timeoutController = new AbortController();
const timeoutId = window.setTimeout(() => timeoutController.abort(), options.timeout ?? 12000);
const timeoutId = window.setTimeout(
() => timeoutController.abort(),
options.timeout ?? 12000,
);
const abort = () => timeoutController.abort();
options.signal?.addEventListener("abort", abort, { once: true });
try {
const response = await fetch(target, {
headers: { Accept: "application/json", "X-Request-Id": requestId },
headers: {
Accept: "application/json",
token: API_TOKEN,
"X-Request-Id": requestId,
},
cache: "no-store",
signal: timeoutController.signal
signal: timeoutController.signal,
});
if (!response.ok) throw new ApiError(`接口请求失败:HTTP ${response.status}`, response.status, requestId);
const result = await response.json() as ApiEnvelope<T>;
if (Number(result.code) !== 1) throw new ApiError(result.message || "接口返回业务错误", response.status, requestId);
if (!response.ok)
throw new ApiError(
`接口请求失败:HTTP ${response.status}`,
response.status,
requestId,
);
const result = (await response.json()) as ApiEnvelope<T>;
if (Number(result.code) !== 1)
throw new ApiError(
result.message || "接口返回业务错误",
response.status,
requestId,
);
return result;
} catch (error) {
if (error instanceof ApiError) throw error;
if (timeoutController.signal.aborted) throw new ApiError("接口请求超时或已取消", 0, requestId);
throw new ApiError(error instanceof Error ? error.message : "接口请求失败", 0, requestId);
if (timeoutController.signal.aborted)
throw new ApiError("接口请求超时或已取消", 0, requestId);
throw new ApiError(
error instanceof Error ? error.message : "接口请求失败",
0,
requestId,
);
} finally {
window.clearTimeout(timeoutId);
options.signal?.removeEventListener("abort", abort);
+4 -14
View File
@@ -1,14 +1,11 @@
/**
* Vite 开发与构建配置。
* 开发环境将同源 /qhse_webapi 请求转发到正式接口域名,并从服务端环境变量注入 token。
* 开发环境将同源 /qhse_webapi 请求转发到正式接口域名,token 由浏览器请求统一携带。
*/
import { defineConfig, loadEnv } from "vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import type { ClientRequest } from "node:http";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "QHSE_");
return {
export default defineConfig({
plugins: [vue()],
base: "./",
server: {
@@ -19,15 +16,8 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
secure: true,
timeout: 15000,
proxyTimeout: 15000,
configure(proxy) {
// QHSE_ 变量仅存在于 Node 侧,不能使用会暴露到浏览器的 VITE_ 前缀。
(proxy as unknown as { on: (event: string, callback: (request: ClientRequest) => void) => void }).on("proxyReq", (proxyRequest) => {
if (env.QHSE_API_TOKEN) proxyRequest.setHeader("token", env.QHSE_API_TOKEN);
});
}
proxyTimeout: 15000
}
}
}
};
});