feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import type { ApiError } from '@/types/api'
|
||||
import { isAxiosError } from 'axios'
|
||||
|
||||
function isRecord (value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function firstString (value: unknown): string | undefined {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
return trimmed ? trimmed : undefined
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const found = firstString(item)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
const message = firstString(value.message)
|
||||
if (message) return message
|
||||
|
||||
// 常見錯誤格式:{ error: '...' } / { error: { message: '...' } }
|
||||
const errorValue = value.error
|
||||
const errorMessage = firstString(errorValue)
|
||||
if (errorMessage) return errorMessage
|
||||
|
||||
// RFC 7807 (problem+json): { title, detail }
|
||||
const detail = firstString(value.detail)
|
||||
if (detail) return detail
|
||||
const title = firstString(value.title)
|
||||
if (title) return title
|
||||
|
||||
// 有些後端用 msg
|
||||
const msg = firstString(value.msg)
|
||||
if (msg) return msg
|
||||
|
||||
// errors: Record<string, string[]>
|
||||
const errors = value.errors
|
||||
if (isRecord(errors)) {
|
||||
for (const key of Object.keys(errors)) {
|
||||
const found = firstString(errors[key])
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function extractErrorMessage (data: unknown): string | undefined {
|
||||
return firstString(data)
|
||||
}
|
||||
|
||||
// 統一錯誤格式
|
||||
//
|
||||
// 設計重點:
|
||||
// - 將 AxiosError 與非預期錯誤統一轉成 ApiRequestError
|
||||
// - Store 只需要處理 message/code/status,不需理解 Axios 結構
|
||||
// - 取消請求(AbortController)會轉成 CanceledRequestError
|
||||
export class ApiRequestError extends Error {
|
||||
code?: number
|
||||
status?: number
|
||||
errors?: ApiError['errors']
|
||||
raw?: unknown
|
||||
|
||||
constructor(params: {
|
||||
message: string
|
||||
code?: number
|
||||
status?: number
|
||||
errors?: ApiError['errors']
|
||||
raw?: unknown
|
||||
}) {
|
||||
super(params.message)
|
||||
this.name = 'ApiRequestError'
|
||||
this.code = params.code
|
||||
this.status = params.status
|
||||
this.errors = params.errors
|
||||
this.raw = params.raw
|
||||
}
|
||||
}
|
||||
|
||||
export class CanceledRequestError extends ApiRequestError {
|
||||
constructor() {
|
||||
super({ message: '請求已取消' })
|
||||
this.name = 'CanceledRequestError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isRequestCanceled (error: unknown): boolean {
|
||||
if (isAxiosError(error)) {
|
||||
return error.code === 'ERR_CANCELED'
|
||||
}
|
||||
return error instanceof DOMException && error.name === 'AbortError'
|
||||
}
|
||||
|
||||
export function normalizeError (error: unknown): ApiRequestError {
|
||||
if (error instanceof ApiRequestError) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (isRequestCanceled(error)) {
|
||||
return new CanceledRequestError()
|
||||
}
|
||||
|
||||
if (isAxiosError(error)) {
|
||||
const status = error.response?.status
|
||||
const data = error.response?.data as unknown
|
||||
const message = extractErrorMessage(data) || error.message || '請求失敗'
|
||||
const apiError = isRecord(data) ? (data as Partial<ApiError>) : undefined
|
||||
const code = apiError?.code ?? status
|
||||
return new ApiRequestError({
|
||||
message,
|
||||
code,
|
||||
status,
|
||||
errors: apiError?.errors,
|
||||
raw: error
|
||||
})
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return new ApiRequestError({ message: error.message, raw: error })
|
||||
}
|
||||
|
||||
return new ApiRequestError({ message: '未知錯誤', raw: error })
|
||||
}
|
||||
Reference in New Issue
Block a user