feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import { API_BASE_URL } from '../utils/constants';
|
||||
|
||||
interface ApiResponse<T = any> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private baseURL: string;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const apiKey = localStorage.getItem('kb_api_key');
|
||||
const activeTenantId = localStorage.getItem('kb_active_tenant_id');
|
||||
const token = localStorage.getItem('authToken') || localStorage.getItem('token');
|
||||
const language = localStorage.getItem('userLanguage') || 'zh';
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-user-language': language,
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
if (apiKey.startsWith('kb_')) {
|
||||
headers['x-api-key'] = apiKey;
|
||||
} else {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
} else if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (activeTenantId && activeTenantId !== 'undefined' && activeTenantId !== 'null') {
|
||||
headers['x-tenant-id'] = activeTenantId;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
|
||||
const text = await response.text();
|
||||
let data: any;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message || text || 'Request failed');
|
||||
}
|
||||
|
||||
return { data: data as T, status: response.status };
|
||||
}
|
||||
|
||||
// 新しい API 呼び出し方法、{ data, status } を返す
|
||||
async get<T = any>(url: string): Promise<ApiResponse<T>> {
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'PUT',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async patch<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'PATCH',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
async delete<T = any>(url: string): Promise<ApiResponse<T>> {
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
// New methods for special formats
|
||||
async getBlob(url: string): Promise<Blob> {
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
|
||||
return await response.blob();
|
||||
}
|
||||
|
||||
async postMultipart<T = any>(url: string, formData: FormData): Promise<ApiResponse<T>> {
|
||||
const headers = this.getAuthHeaders();
|
||||
// Remove Content-Type to let the browser set it with the correct boundary
|
||||
delete headers['Content-Type'];
|
||||
|
||||
const response = await fetch(`${this.baseURL}${url}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return this.handleResponse<T>(response);
|
||||
}
|
||||
|
||||
// Legacy compatibility method — returns raw Response for streaming and other special cases
|
||||
async request(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
const authHeaders = this.getAuthHeaders();
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
// Merge auth headers into request headers
|
||||
Object.entries(authHeaders).forEach(([key, value]) => {
|
||||
// Don't override if already set
|
||||
if (headers.has(key)) return;
|
||||
|
||||
// Don't set Content-Type if body is FormData (let browser set it with boundary)
|
||||
if (key === 'Content-Type' && options.body instanceof FormData) return;
|
||||
|
||||
headers.set(key, value);
|
||||
});
|
||||
|
||||
let url = path;
|
||||
if (!path.startsWith('http')) {
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||
url = `${this.baseURL}${cleanPath}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('kb_api_key');
|
||||
localStorage.removeItem('authToken');
|
||||
window.location.href = '/login';
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private handleUnauthorized() {
|
||||
console.warn('[ApiClient] 401 Unauthorized detected. Cleaning up and redirecting to login...');
|
||||
localStorage.removeItem('kb_api_key');
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('kb_active_tenant_id');
|
||||
// Only redirect if we are not already on the login page
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient(API_BASE_URL);
|
||||
Reference in New Issue
Block a user