新增对象存储/账单/保留IP页面,成本今天视图与样式收敛
CI / test (push) Successful in 42s

This commit is contained in:
2026-07-21 12:11:22 +08:00
parent d813225ac4
commit ac457282ce
91 changed files with 6672 additions and 652 deletions
+38 -5
View File
@@ -4,10 +4,13 @@ const BASE = '/api/v1'
export class ApiError extends Error {
status: number
/** 后端透传的 OCI 错误码(如 OrganizationExclusionError),供调用方做能力适配 */
ociCode?: string
constructor(status: number, message: string) {
constructor(status: number, message: string, ociCode?: string) {
super(message)
this.status = status
this.ociCode = ociCode
}
}
@@ -31,8 +34,15 @@ function buildUrl(path: string, query?: RequestOptions['query']): string {
async function parseError(resp: Response): Promise<never> {
let message = `请求失败(${resp.status})`
let ociCode: string | undefined
try {
const body = (await resp.json()) as { error?: string; hint?: string; errors?: unknown[] }
const body = (await resp.json()) as {
error?: string
hint?: string
errors?: unknown[]
ociCode?: string
}
ociCode = body.ociCode
if (body.error) {
message = body.hint ? `${body.hint}|${body.error}` : body.error
} else {
@@ -49,7 +59,7 @@ async function parseError(resp: Response): Promise<never> {
if (resp.status === 429 && !resp.url.includes('/auth/login') && location.pathname !== '/blocked') {
location.assign('/blocked')
}
throw new ApiError(resp.status, message)
throw new ApiError(resp.status, message, ociCode)
}
/** 统一请求封装:注入 JWT、401 登出、错误转 ApiError */
@@ -63,8 +73,31 @@ export async function request<T>(path: string, opts: RequestOptions = {}): Promi
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
})
if (!resp.ok) await parseError(resp)
if (resp.status === 204) return undefined as T
return (await resp.json()) as T
// 202/204 等成功响应可能无 body,直接 resp.json() 会抛 Unexpected end of JSON input
const text = await resp.text()
return (text ? JSON.parse(text) : undefined) as T
}
/** 原始请求:注入 JWT、错误同 request 转 ApiError,返回原始 Response(二进制内容读写用) */
export async function rawFetch(
path: string,
opts: {
method?: string
query?: RequestOptions['query']
headers?: Record<string, string>
body?: BodyInit
} = {},
): Promise<Response> {
const auth = useAuthStore()
const headers: Record<string, string> = { ...opts.headers }
if (auth.token) headers.Authorization = `Bearer ${auth.token}`
const resp = await fetch(buildUrl(path, opts.query), {
method: opts.method ?? 'GET',
headers,
body: opts.body,
})
if (!resp.ok) await parseError(resp)
return resp
}
export const mockOn = import.meta.env.VITE_MOCK === '1'