- Vue 3 + TypeScript + Element Plus 前端界面 - Pinia 状态管理 - Vue Router 4 路由管理 - Axios HTTP 客户端 - MSW (Mock Service Worker) 开发环境模拟 - 账户管理界面 (列表、详情、三科目余额展示) - 交易管理界面 (列表、详情) - 对账管理界面 (三账校验) - 完善的 API 客户端封装 - Docker 容器化配置 - Nginx 配置用于生产环境
189 lines
5.2 KiB
JavaScript
189 lines
5.2 KiB
JavaScript
import {
|
|
InterceptorError,
|
|
RequestController
|
|
} from "./chunk-JXGB54LE.mjs";
|
|
|
|
// src/utils/emitAsync.ts
|
|
async function emitAsync(emitter, eventName, ...data) {
|
|
const listeners = emitter.listeners(eventName);
|
|
if (listeners.length === 0) {
|
|
return;
|
|
}
|
|
for (const listener of listeners) {
|
|
await listener.apply(emitter, data);
|
|
}
|
|
}
|
|
|
|
// src/utils/isObject.ts
|
|
function isObject(value, loose = false) {
|
|
return loose ? Object.prototype.toString.call(value).startsWith("[object ") : Object.prototype.toString.call(value) === "[object Object]";
|
|
}
|
|
|
|
// src/utils/isPropertyAccessible.ts
|
|
function isPropertyAccessible(obj, key) {
|
|
try {
|
|
obj[key];
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// src/utils/responseUtils.ts
|
|
function createServerErrorResponse(body) {
|
|
return new Response(
|
|
JSON.stringify(
|
|
body instanceof Error ? {
|
|
name: body.name,
|
|
message: body.message,
|
|
stack: body.stack
|
|
} : body
|
|
),
|
|
{
|
|
status: 500,
|
|
statusText: "Unhandled Exception",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
}
|
|
}
|
|
);
|
|
}
|
|
function isResponseError(response) {
|
|
return response != null && response instanceof Response && isPropertyAccessible(response, "type") && response.type === "error";
|
|
}
|
|
function isResponseLike(value) {
|
|
return isObject(value, true) && isPropertyAccessible(value, "status") && isPropertyAccessible(value, "statusText") && isPropertyAccessible(value, "bodyUsed");
|
|
}
|
|
|
|
// src/utils/handleRequest.ts
|
|
import { DeferredPromise } from "@open-draft/deferred-promise";
|
|
import { until } from "@open-draft/until";
|
|
|
|
// src/utils/isNodeLikeError.ts
|
|
function isNodeLikeError(error) {
|
|
if (error == null) {
|
|
return false;
|
|
}
|
|
if (!(error instanceof Error)) {
|
|
return false;
|
|
}
|
|
return "code" in error && "errno" in error;
|
|
}
|
|
|
|
// src/utils/handleRequest.ts
|
|
async function handleRequest(options) {
|
|
const handleResponse = async (response) => {
|
|
if (response instanceof Error) {
|
|
await options.controller.errorWith(response);
|
|
return true;
|
|
}
|
|
if (isResponseError(response)) {
|
|
await options.controller.respondWith(response);
|
|
return true;
|
|
}
|
|
if (isResponseLike(response)) {
|
|
await options.controller.respondWith(response);
|
|
return true;
|
|
}
|
|
if (isObject(response)) {
|
|
await options.controller.errorWith(response);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
const handleResponseError = async (error) => {
|
|
if (error instanceof InterceptorError) {
|
|
throw result.error;
|
|
}
|
|
if (isNodeLikeError(error)) {
|
|
await options.controller.errorWith(error);
|
|
return true;
|
|
}
|
|
if (error instanceof Response) {
|
|
return await handleResponse(error);
|
|
}
|
|
return false;
|
|
};
|
|
const requestAbortPromise = new DeferredPromise();
|
|
if (options.request.signal) {
|
|
if (options.request.signal.aborted) {
|
|
await options.controller.errorWith(options.request.signal.reason);
|
|
return;
|
|
}
|
|
options.request.signal.addEventListener(
|
|
"abort",
|
|
() => {
|
|
requestAbortPromise.reject(options.request.signal.reason);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
const result = await until(async () => {
|
|
const requestListenersPromise = emitAsync(options.emitter, "request", {
|
|
requestId: options.requestId,
|
|
request: options.request,
|
|
controller: options.controller
|
|
});
|
|
await Promise.race([
|
|
// Short-circuit the request handling promise if the request gets aborted.
|
|
requestAbortPromise,
|
|
requestListenersPromise,
|
|
options.controller.handled
|
|
]);
|
|
});
|
|
if (requestAbortPromise.state === "rejected") {
|
|
await options.controller.errorWith(requestAbortPromise.rejectionReason);
|
|
return;
|
|
}
|
|
if (result.error) {
|
|
if (await handleResponseError(result.error)) {
|
|
return;
|
|
}
|
|
if (options.emitter.listenerCount("unhandledException") > 0) {
|
|
const unhandledExceptionController = new RequestController(
|
|
options.request,
|
|
{
|
|
/**
|
|
* @note Intentionally empty passthrough handle.
|
|
* This controller is created within another controller and we only need
|
|
* to know if `unhandledException` listeners handled the request.
|
|
*/
|
|
passthrough() {
|
|
},
|
|
async respondWith(response) {
|
|
await handleResponse(response);
|
|
},
|
|
async errorWith(reason) {
|
|
await options.controller.errorWith(reason);
|
|
}
|
|
}
|
|
);
|
|
await emitAsync(options.emitter, "unhandledException", {
|
|
error: result.error,
|
|
request: options.request,
|
|
requestId: options.requestId,
|
|
controller: unhandledExceptionController
|
|
});
|
|
if (unhandledExceptionController.readyState !== RequestController.PENDING) {
|
|
return;
|
|
}
|
|
}
|
|
await options.controller.respondWith(
|
|
createServerErrorResponse(result.error)
|
|
);
|
|
return;
|
|
}
|
|
if (options.controller.readyState === RequestController.PENDING) {
|
|
return await options.controller.passthrough();
|
|
}
|
|
return options.controller.handled;
|
|
}
|
|
|
|
export {
|
|
isPropertyAccessible,
|
|
emitAsync,
|
|
isObject,
|
|
isResponseError,
|
|
handleRequest
|
|
};
|
|
//# sourceMappingURL=chunk-R6T7CL5E.mjs.map
|