diff --git a/.gitignore b/.gitignore index ce2b0ab..393697f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ node_modules .nuxt +.output +.vs [Bb]in [Oo]bj [Ll]og/ diff --git a/Service/Application.Web/Controllers/Login/LoginController.cs b/Service/Application.Web/Controllers/Login/LoginController.cs index cf18d1f..669bc83 100644 --- a/Service/Application.Web/Controllers/Login/LoginController.cs +++ b/Service/Application.Web/Controllers/Login/LoginController.cs @@ -22,5 +22,13 @@ namespace Application.Web.Controllers.Login { return PoAction.Ok(parms.name); } + + [HttpGet] + public async Task Test(string name) + { + return PoAction.Ok(name); + + } + } } diff --git a/Web/nuxt.config.ts b/Web/nuxt.config.ts index 4d0df40..8514f06 100644 --- a/Web/nuxt.config.ts +++ b/Web/nuxt.config.ts @@ -26,7 +26,8 @@ export default defineNuxtConfig({ 'stores', 'composables', 'extends', - 'services' + 'services', + "model" ] }, diff --git a/Web/src/app.vue b/Web/src/app.vue index 84a983e..540bae7 100644 --- a/Web/src/app.vue +++ b/Web/src/app.vue @@ -7,6 +7,7 @@ diff --git a/Web/src/composables/ApiService.ts b/Web/src/composables/ApiService.ts new file mode 100644 index 0000000..975a786 --- /dev/null +++ b/Web/src/composables/ApiService.ts @@ -0,0 +1,120 @@ +import { navigateTo } from "#app"; +import { RequestExtend } from "@/extends/RequestExtend"; +import { BaseConfig } from "@/config/BaseConfig"; +import type { IResultData } from "@/model/common/ResultData"; + +type HttpMethod = "get" | "post" | "put" | "delete" | "patch"; +type RequestParams = Record; + +export type HandledRedirectError = { + handled: true; + redirectTo: string; + message: string; +}; + +export class ApiService { + private static initialized = false; + + private static request = new RequestExtend({ + baseURL: BaseConfig.BaseUrl, + timeout: 60000 + }); + + private static isResultData(value: unknown): value is IResultData { + return typeof value === "object" && value !== null && "code" in value && "msg" in value; + } + + public static isHandledRedirectError(error: unknown): error is HandledRedirectError { + return typeof error === "object" && error !== null && "handled" in error && error.handled === true; + } + + private static redirectToLogin() { + if (typeof localStorage !== "undefined") { + localStorage.removeItem("token"); + localStorage.removeItem("userInfo"); + } + + if (typeof window !== "undefined") { + void navigateTo("/home", { replace: true }); + } + } + + private static ensureInitialized() { + if (this.initialized) { + return; + } + + RequestExtend.addRequestInterceptor({ + onFulfilled: (config) => { + const token = typeof localStorage !== "undefined" ? localStorage.getItem("token") : ""; + + if (token) { + config.headers = { + ...config.headers, + Authorization: `Bearer ${token}` + }; + } + + config.timeout = 60000; + return config; + } + }); + + RequestExtend.addResponseInterceptor({ + onFulfilled: (response) => { + if (!this.isResultData(response.data)) { + return response; + } + const result = response.data; + if (result.code === 401) { + console.log(result.data); + } else if (result.code === 40101) { + this.redirectToLogin(); + throw { + handled: true, + redirectTo: "/login/login", + message: result.msg || "登录已失效" + } satisfies HandledRedirectError; + } else if (result.code === 500) { + // 跳转错误页面 + } else if (result.code === 404) { + // 跳转不存在页面 + } + + return response; + }, + onRejected: (error) => { + if (error && typeof error === "object" && "status" in error) { + // console.log("接口错误:", error); + } + + return error; + } + }); + + this.initialized = true; + } + + public static async ApiRequest( + method: HttpMethod, + url: string, + params: RequestParams = {} + ): Promise> { + this.ensureInitialized(); + + switch (method) { + case "get": + return await this.request.get>(url, { params }); + case "post": + return await this.request.post>(url, params); + case "put": + return await this.request.put>(url, params); + case "delete": + return await this.request.delete>(url, { params }); + case "patch": + return await this.request.patch>(url, params); + default: + throw new Error(`不支持的请求方法: ${method}`); + } + } +} diff --git a/Web/src/config/BaseConfig.ts b/Web/src/config/BaseConfig.ts new file mode 100644 index 0000000..e51ac56 --- /dev/null +++ b/Web/src/config/BaseConfig.ts @@ -0,0 +1,6 @@ +/* +统一配置中心 +*/ +export class BaseConfig { +public static BaseUrl:string="https://localhost:7198"; +} \ No newline at end of file diff --git a/Web/src/extends/requestEXTEND.ts b/Web/src/extends/requestEXTEND.ts index 5fb0aef..7d8b7c1 100644 --- a/Web/src/extends/requestEXTEND.ts +++ b/Web/src/extends/requestEXTEND.ts @@ -1,86 +1,87 @@ -/** - * 网络请求工具类(支持实例化) - * 基于ofetch封装统一的请求逻辑,支持拦截器配置 - */ -export class RequestEXTEND { +type RequestConfig = { + url: string + method: string + headers: Record + timeout?: number + body?: BodyInit | null +} + +type ResponseWrapper = { + data: T + status: number + statusText: string + headers: Headers +} + +type RequestInterceptor = { + onFulfilled?: (config: RequestConfig) => RequestConfig | Promise + onRejected?: (error: unknown) => RequestConfig | Promise +} + +type ResponseInterceptor = { + onFulfilled?: ( + response: ResponseWrapper + ) => ResponseWrapper | Promise> + onRejected?: (error: unknown) => unknown | Promise +} + +export class RequestExtend { private baseURL: string private timeout: number private headers: Record - // 静态默认配置 private static defaultBaseURL = '' private static defaultTimeout = 30000 private static defaultHeaders: Record = { 'Content-Type': 'application/json' } - // 静态拦截器 - private static requestInterceptors: Array<{ - onFulfilled?: (config: any) => any - onRejected?: (error: any) => any - }> = [] + private static requestInterceptors: RequestInterceptor[] = [] + private static responseInterceptors: ResponseInterceptor[] = [] - private static responseInterceptors: Array<{ - onFulfilled?: (response: any) => any - onRejected?: (error: any) => any - }> = [] - - /** - * 构造函数:初始化请求配置 - * @param config 自定义请求配置(可选) - */ constructor(config?: { baseURL?: string timeout?: number headers?: Record }) { - this.baseURL = config?.baseURL || RequestEXTEND.defaultBaseURL - this.timeout = config?.timeout || RequestEXTEND.defaultTimeout + this.baseURL = config?.baseURL || RequestExtend.defaultBaseURL + this.timeout = config?.timeout || RequestExtend.defaultTimeout this.headers = { - ...RequestEXTEND.defaultHeaders, + ...RequestExtend.defaultHeaders, ...config?.headers } } - /** - * 设置静态默认配置 - */ static setDefaultConfig(config: { baseURL?: string timeout?: number headers?: Record }) { - if (config.baseURL) this.defaultBaseURL = config.baseURL - if (config.timeout) this.defaultTimeout = config.timeout - if (config.headers) this.defaultHeaders = { ...this.defaultHeaders, ...config.headers } + if (config.baseURL) { + this.defaultBaseURL = config.baseURL + } + + if (config.timeout) { + this.defaultTimeout = config.timeout + } + + if (config.headers) { + this.defaultHeaders = { ...this.defaultHeaders, ...config.headers } + } } - /** - * 添加请求拦截器(静态方法) - */ - static addRequestInterceptor(interceptor: { - onFulfilled?: (config: any) => any - onRejected?: (error: any) => any - }) { + static addRequestInterceptor(interceptor: RequestInterceptor) { this.requestInterceptors.push(interceptor) } - /** - * 添加响应拦截器(静态方法) - */ - static addResponseInterceptor(interceptor: { - onFulfilled?: (response: any) => any - onRejected?: (error: any) => any - }) { + static addResponseInterceptor(interceptor: ResponseInterceptor) { this.responseInterceptors.push(interceptor) } - /** - * 执行请求拦截器链 - */ - private async executeRequestInterceptors(config: any): Promise { + private async executeRequestInterceptors(config: RequestConfig): Promise { let result = config - for (const interceptor of RequestEXTEND.requestInterceptors) { + + for (const interceptor of RequestExtend.requestInterceptors) { try { if (interceptor.onFulfilled) { result = await interceptor.onFulfilled(result) @@ -93,81 +94,136 @@ export class RequestEXTEND { } } } + return result } - /** - * 执行响应拦截器链 - */ - private async executeResponseInterceptors(response: any): Promise { + private async executeResponseInterceptors( + response: ResponseWrapper + ): Promise> { let result = response - for (const interceptor of RequestEXTEND.responseInterceptors) { + + for (const interceptor of RequestExtend.responseInterceptors) { try { if (interceptor.onFulfilled) { result = await interceptor.onFulfilled(result) } } catch (error) { if (interceptor.onRejected) { - result = await interceptor.onRejected(error) - } else { - throw error + const interceptedError = await interceptor.onRejected(error) + throw interceptedError } + + throw error } } + return result } - /** - * 构建请求URL - */ - private buildURL(url: string, params?: Record): string { + private async executeResponseErrorInterceptors(error: unknown): Promise { + let result = error + + for (const interceptor of RequestExtend.responseInterceptors) { + if (!interceptor.onRejected) { + continue + } + + try { + result = await interceptor.onRejected(result) + } catch (interceptorError) { + result = interceptorError + } + } + + return result + } + + private buildURL(url: string, params?: Record): string { let fullURL = url - // 处理相对路径 + if (!url.startsWith('http://') && !url.startsWith('https://')) { fullURL = this.baseURL + url } - // 处理查询参数 + if (params && Object.keys(params).length > 0) { const searchParams = new URLSearchParams() + for (const key in params) { const value = params[key] if (value !== undefined && value !== null) { searchParams.append(key, String(value)) } } + const queryString = searchParams.toString() if (queryString) { fullURL += (fullURL.includes('?') ? '&' : '?') + queryString } } + return fullURL } - /** - * 通用请求方法 - */ - private async request( + private async parseResponseBody(response: Response): Promise { + const contentType = response.headers.get('content-type') || '' + + if (contentType.includes('application/json')) { + return response.json() + } + + if (contentType.includes('application/octet-stream')) { + return response.blob() + } + + const text = await response.text() + if (!text) { + return text + } + + try { + return JSON.parse(text) + } catch { + return text + } + } + + private getErrorMessage(result: unknown, fallback: string): string { + if (typeof result === 'string' && result.trim()) { + return result + } + + if (result && typeof result === 'object') { + const payload = result as { msg?: string; message?: string } + return payload.msg || payload.message || fallback + } + + return fallback + } + + private async request( method: string, url: string, options: { - params?: Record - data?: any + params?: Record + data?: unknown headers?: Record } = {} ): Promise { const { params, data, headers } = options - // 构建配置 - const config: any = { + const config: RequestConfig = { method: method.toUpperCase(), headers: { ...this.headers, ...headers }, - timeout: this.timeout + timeout: this.timeout, + url: params && method.toUpperCase() === 'GET' + ? this.buildURL(url, params) + : this.buildURL(url) } - // 添加Body(GET/HEAD请求不添加body) if (data && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') { if (data instanceof FormData) { config.body = data @@ -177,126 +233,108 @@ export class RequestEXTEND { } } - // 处理查询参数(GET请求) - if (params && method.toUpperCase() === 'GET') { - url = this.buildURL(url, params) - } else { - url = this.buildURL(url, undefined) - } - - // 执行请求拦截器 - config.url = url const interceptedConfig = await this.executeRequestInterceptors(config) try { - // 发起请求 const response = await fetch(interceptedConfig.url, { method: interceptedConfig.method, headers: interceptedConfig.headers, body: interceptedConfig.body, - signal: interceptedConfig.timeout ? AbortSignal.timeout(interceptedConfig.timeout) : undefined + signal: interceptedConfig.timeout + ? AbortSignal.timeout(interceptedConfig.timeout) + : undefined }) - // 处理响应 - let result - const contentType = response.headers.get('content-type') - if (contentType && contentType.includes('application/json')) { - result = await response.json() - } else { - result = await response.text() - } - - // 包装响应 - const wrappedResponse = { + const result = await this.parseResponseBody(response) + const wrappedResponse: ResponseWrapper = { data: result, status: response.status, statusText: response.statusText, headers: response.headers } - // 执行响应拦截器 - return await this.executeResponseInterceptors(wrappedResponse) - } catch (error: any) { - // 执行错误拦截器 - const errorResponse = { - message: error.message || '网络请求失败', - code: error.code || 'NETWORK_ERROR', - status: error.status || 0 + if (!response.ok) { + const handledError = await this.executeResponseErrorInterceptors({ + ...wrappedResponse, + message: this.getErrorMessage(result, response.statusText || 'Request failed') + }) + throw handledError } - throw errorResponse + + const interceptedResponse = await this.executeResponseInterceptors(wrappedResponse) + return interceptedResponse.data as T + } catch (error) { + if (error && typeof error === 'object' && ('status' in error || 'code' in error)) { + throw error + } + + const handledError = await this.executeResponseErrorInterceptors({ + message: error instanceof Error ? error.message : '网络请求失败', + code: + error && typeof error === 'object' && 'code' in error + ? (error as { code?: string }).code || 'NETWORK_ERROR' + : 'NETWORK_ERROR', + status: + error && typeof error === 'object' && 'status' in error + ? Number((error as { status?: number }).status || 0) + : 0 + }) + + throw handledError } } - /** - * GET请求 - */ - async get(url: string, options?: { - params?: Record + async get(url: string, options?: { + params?: Record headers?: Record }): Promise { return this.request('GET', url, options) } - /** - * POST请求 - */ - async post(url: string, data?: any, options?: { - params?: Record + async post(url: string, data?: unknown, options?: { + params?: Record headers?: Record }): Promise { return this.request('POST', url, { ...options, data }) } - /** - * PUT请求 - */ - async put(url: string, data?: any, options?: { - params?: Record + async put(url: string, data?: unknown, options?: { + params?: Record headers?: Record }): Promise { return this.request('PUT', url, { ...options, data }) } - /** - * DELETE请求 - */ - async delete(url: string, options?: { - params?: Record + async delete(url: string, options?: { + params?: Record headers?: Record }): Promise { return this.request('DELETE', url, options) } - /** - * PATCH请求 - */ - async patch(url: string, data?: any, options?: { - params?: Record + async patch(url: string, data?: unknown, options?: { + params?: Record headers?: Record }): Promise { return this.request('PATCH', url, { ...options, data }) } - /** - * 文件上传 - */ - async upload(url: string, file: File | FormData, options?: { - params?: Record + async upload(url: string, file: File | FormData, options?: { + params?: Record headers?: Record }): Promise { const formData = file instanceof FormData ? file : new FormData() + if (file instanceof File) { formData.append('file', file) } + return this.request('POST', url, { ...options, data: formData }) } - /** - * 文件下载 - */ async download(url: string, filename?: string): Promise { const response = await fetch(this.buildURL(url), { method: 'GET', @@ -318,39 +356,3 @@ export class RequestEXTEND { window.URL.revokeObjectURL(downloadURL) } } - -// 导出默认实例 -export const request = new RequestEXTEND() - -// 添加默认的Token拦截器(示例) -RequestEXTEND.addRequestInterceptor({ - onFulfilled: (config) => { - // 从localStorage获取Token - const token = typeof localStorage !== 'undefined' ? localStorage.getItem('token') : '' - if (token) { - config.headers = { - ...config.headers, - Authorization: `Bearer ${token}` - } - } - return config - } -}) - -// 添加默认的响应错误处理拦截器 -RequestEXTEND.addResponseInterceptor({ - onRejected: (error: any) => { - if (error.status === 401) { - // Token过期,清除登录状态 - if (typeof localStorage !== 'undefined') { - localStorage.removeItem('token') - localStorage.removeItem('userInfo') - } - // 跳转到登录页 - if (typeof window !== 'undefined') { - window.location.href = '/login' - } - } - return Promise.reject(error) - } -}) \ No newline at end of file diff --git a/Web/src/layouts/default.vue b/Web/src/layouts/default.vue index 8f51bbe..3879b71 100644 --- a/Web/src/layouts/default.vue +++ b/Web/src/layouts/default.vue @@ -1,8 +1,6 @@