feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities

This commit is contained in:
skytek_xinliang
2026-03-26 11:24:37 +08:00
parent 507afcc99c
commit 069141794e
116 changed files with 15247 additions and 107 deletions
+125
View File
@@ -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 })
}