整理代码
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
next/node_modules/
|
||||
next/dist/
|
||||
next/.env
|
||||
next/.env.local
|
||||
next/.env.*.local
|
||||
report/
|
||||
tmp/
|
||||
@@ -1,2 +0,0 @@
|
||||
# 仅供本地 Vite 代理读取,不会通过 VITE_ 前缀打包到浏览器。
|
||||
QHSE_API_TOKEN='AF17168B-87BD-4GLY-1111-F0A0A1158F9B'
|
||||
@@ -1,5 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
+5
-6
@@ -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。
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user