Refactor frontend workspaces and centralize API contracts
Organize domain APIs and types under common/apis, shared components under named folders, and private workspace logic alongside each page. Use Bearer sessions, explicit response envelopes, session-aware routing and streams, and configurable login captcha presentation for the deployed backend contract. Preserve news, economic calendar, crawler, market, chat, Skills, settings, and demo trading workflows with responsive layouts and explicit configuration saves. Validate with frontend lint, type checking, 22 test groups, production builds, and deployed desktop/mobile page checks.
This commit is contained in:
1 parent
77b33e5e77
commit
bc480fb806
102 files changed
+4716
-1707
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import type { AxiosRequestConfig } from "axios"
|
||||
import axios, { CanceledError } from "axios"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
import { router } from "@/router"
|
||||
|
||||
declare module "axios" {
|
||||
interface AxiosRequestConfig {
|
||||
anonymous?: boolean
|
||||
sessionVersion?: number
|
||||
notify?: boolean
|
||||
/** 明确声明未包装的 JSON,例如 OpenAPI;不自动探测响应格式。 */
|
||||
rawResponse?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiEnvelope<T> {
|
||||
code: number
|
||||
data: T
|
||||
message: string
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public code: number, message: string) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export function readEnvelope<T>(value: unknown): ApiEnvelope<T> {
|
||||
if (!value || typeof value !== "object" || !("code" in value) || typeof value.code !== "number"
|
||||
|| !("message" in value) || typeof value.message !== "string" || !("data" in value)) {
|
||||
throw new ApiError(502, "服务器响应格式无效")
|
||||
}
|
||||
return value as ApiEnvelope<T>
|
||||
}
|
||||
|
||||
/** 无 Vue 生命周期接管的轮询回调:请求层已提示,保留真实错误供排查。 */
|
||||
export function logRequestError(error: unknown) {
|
||||
if (!axios.isCancel(error)) console.error(error)
|
||||
}
|
||||
|
||||
export function rejectAuthentication(version: number, redirect = true) {
|
||||
const user = useUserStore()
|
||||
if (version !== user.sessionVersion)
|
||||
return
|
||||
user.setToken("")
|
||||
const route = router.currentRoute.value
|
||||
if (redirect && route.name !== "login")
|
||||
void router.replace({ name: "login", query: { redirect: route.fullPath } })
|
||||
}
|
||||
|
||||
const client = axios.create({ baseURL: "/api", timeout: 15000 })
|
||||
client.interceptors.request.use((config) => {
|
||||
const user = useUserStore()
|
||||
if (config.sessionVersion !== user.sessionVersion || (!config.anonymous && !user.token))
|
||||
throw new CanceledError("会话已切换")
|
||||
if (!config.anonymous && user.token)
|
||||
config.headers.set("Authorization", `Bearer ${user.token}`)
|
||||
return config
|
||||
})
|
||||
|
||||
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||
const version = useUserStore().sessionVersion
|
||||
config.sessionVersion = version
|
||||
try {
|
||||
const response = await client.request(config)
|
||||
if (version !== useUserStore().sessionVersion)
|
||||
throw new CanceledError("会话已切换")
|
||||
if (config.rawResponse) return response.data
|
||||
const body = readEnvelope<T>(response.data)
|
||||
if (body.code !== 0)
|
||||
throw new ApiError(body.code, body.message)
|
||||
return body.data
|
||||
} catch (error) {
|
||||
if (version !== useUserStore().sessionVersion)
|
||||
throw new CanceledError("会话已切换")
|
||||
if (axios.isCancel(error))
|
||||
throw error
|
||||
let reason = error
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
try {
|
||||
const body = readEnvelope(error.response.data)
|
||||
reason = new ApiError(error.response.status, body.message || `请求失败:HTTP ${error.response.status}`)
|
||||
} catch (invalid) {
|
||||
reason = new ApiError(error.response.status, invalid instanceof Error ? invalid.message : "服务器响应格式无效")
|
||||
}
|
||||
}
|
||||
if (reason instanceof ApiError && reason.code === 401 && !config.anonymous) {
|
||||
rejectAuthentication(version, config.url !== "/auth/me")
|
||||
} else if (config.notify !== false) {
|
||||
ElMessage.error(reason instanceof Error ? reason.message : "请求失败")
|
||||
}
|
||||
throw reason
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user