"添加前端模板和运行代码模块"
This commit is contained in:
132
qiming-xagi-frontend-templates/packages/vue3-vite/src/lib/api.ts
Normal file
132
qiming-xagi-frontend-templates/packages/vue3-vite/src/lib/api.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private instance: AxiosInstance;
|
||||
|
||||
constructor(baseURL = '') {
|
||||
this.instance = axios.create({
|
||||
baseURL,
|
||||
timeout: 60000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private setupInterceptors(): void {
|
||||
this.instance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => config,
|
||||
(error: AxiosError) => Promise.reject(error)
|
||||
);
|
||||
|
||||
this.instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response) {
|
||||
console.error('API Error:', error.response.status, error.message);
|
||||
} else if (error.request) {
|
||||
console.error('Network Error:', error.message);
|
||||
} else {
|
||||
console.error('Request Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async get<T = unknown>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.instance.get<T>(url, config);
|
||||
}
|
||||
|
||||
async post<T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.instance.post<T>(url, data, config);
|
||||
}
|
||||
|
||||
async put<T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.instance.put<T>(url, data, config);
|
||||
}
|
||||
|
||||
async delete<T = unknown>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.instance.delete<T>(url, config);
|
||||
}
|
||||
|
||||
async patch<T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<T>> {
|
||||
return this.instance.patch<T>(url, data, config);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
|
||||
export const api = {
|
||||
get: <T = unknown>(url: string, config?: AxiosRequestConfig) =>
|
||||
apiClient.get<T>(url, config),
|
||||
post: <T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig
|
||||
) => apiClient.post<T>(url, data, config),
|
||||
put: <T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig
|
||||
) => apiClient.put<T>(url, data, config),
|
||||
delete: <T = unknown>(url: string, config?: AxiosRequestConfig) =>
|
||||
apiClient.delete<T>(url, config),
|
||||
patch: <T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: AxiosRequestConfig
|
||||
) => apiClient.patch<T>(url, data, config),
|
||||
};
|
||||
|
||||
/**
|
||||
* Unwrap common backend envelope:
|
||||
* - `{ code, data, message }` -> `data`
|
||||
* - plain payload -> payload itself
|
||||
*/
|
||||
export function extractApiData<T = unknown>(response: AxiosResponse): T {
|
||||
const payload = response.data;
|
||||
if (payload && typeof payload === 'object' && 'data' in payload) {
|
||||
return (payload as { data: T }).data;
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
fetchEventSource,
|
||||
type EventSourceMessage,
|
||||
} from '@microsoft/fetch-event-source';
|
||||
import { onMounted, ref, type Ref, watch, type WatchSource } from 'vue';
|
||||
import { api, extractApiData, type ApiResponse } from './api';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ListParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface ListResult<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
async getUserInfo(): Promise<User> {
|
||||
const response = await api.get<ApiResponse<User>>('/user/info');
|
||||
return extractApiData<User>(response);
|
||||
},
|
||||
async updateUserInfo(data: Partial<User>): Promise<User> {
|
||||
const response = await api.put<ApiResponse<User>>('/user/info', data);
|
||||
return extractApiData<User>(response);
|
||||
},
|
||||
};
|
||||
|
||||
export const exampleApi = {
|
||||
async getList(params: ListParams): Promise<ListResult<unknown>> {
|
||||
const response = await api.get<ApiResponse<ListResult<unknown>>>(
|
||||
'/example/list',
|
||||
{ params }
|
||||
);
|
||||
return extractApiData<ListResult<unknown>>(response);
|
||||
},
|
||||
async create(data: unknown): Promise<unknown> {
|
||||
const response = await api.post<ApiResponse<unknown>>(
|
||||
'/example/create',
|
||||
data
|
||||
);
|
||||
return extractApiData(response);
|
||||
},
|
||||
async update(id: number, data: unknown): Promise<unknown> {
|
||||
const response = await api.put<ApiResponse<unknown>>(
|
||||
`/example/update/${id}`,
|
||||
data
|
||||
);
|
||||
return extractApiData(response);
|
||||
},
|
||||
async delete(id: number): Promise<void> {
|
||||
await api.delete(`/example/delete/${id}`);
|
||||
},
|
||||
async getDetail(id: number): Promise<unknown> {
|
||||
const response = await api.get<ApiResponse<unknown>>(
|
||||
`/example/detail/${id}`
|
||||
);
|
||||
return extractApiData(response);
|
||||
},
|
||||
};
|
||||
|
||||
interface UseApiOptions {
|
||||
immediate?: boolean;
|
||||
watchSources?: WatchSource[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue-friendly async state helper aligned with react-vite semantics:
|
||||
* - loading / error state
|
||||
* - explicit execute()
|
||||
* - refetch alias
|
||||
*/
|
||||
export function useApi<T>(
|
||||
apiCall: () => Promise<T>,
|
||||
options: UseApiOptions = {}
|
||||
): {
|
||||
data: Ref<T | null>;
|
||||
loading: Ref<boolean>;
|
||||
error: Ref<Error | null>;
|
||||
execute: () => Promise<void>;
|
||||
refetch: () => Promise<void>;
|
||||
} {
|
||||
const data = ref<T | null>(null) as Ref<T | null>;
|
||||
const loading = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
|
||||
const execute = async (): Promise<void> => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
data.value = await apiCall();
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error('Unknown error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (options.immediate ?? true) {
|
||||
onMounted(execute);
|
||||
}
|
||||
|
||||
if (options.watchSources && options.watchSources.length > 0) {
|
||||
watch(options.watchSources, () => {
|
||||
void execute();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
execute,
|
||||
refetch: execute,
|
||||
};
|
||||
}
|
||||
|
||||
export type StreamResponse<T = unknown> = T;
|
||||
|
||||
export async function streamRequest<T>(
|
||||
url: string,
|
||||
payload: unknown,
|
||||
onData: (response: StreamResponse<T>) => void,
|
||||
onError?: (error: Error) => void
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fetchEventSource(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
onmessage(msg: EventSourceMessage) {
|
||||
if (msg.event === 'FatalError') {
|
||||
throw new Error(msg.data);
|
||||
}
|
||||
try {
|
||||
onData(JSON.parse(msg.data) as StreamResponse<T>);
|
||||
} catch {
|
||||
console.warn('Failed to parse stream chunk:', msg.data);
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
if (onError) {
|
||||
onError(err as Error);
|
||||
}
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (onError) {
|
||||
onError(err as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Merge Tailwind classes and remove conflicting duplicates.
|
||||
* This keeps component class composition predictable across variants.
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
/** Format a date-like value using locale defaults. */
|
||||
export function formatDate(
|
||||
date: Date | string,
|
||||
options: Intl.DateTimeFormatOptions = {}
|
||||
): string {
|
||||
const dateValue = typeof date === 'string' ? new Date(date) : date;
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
...options,
|
||||
}).format(dateValue);
|
||||
}
|
||||
|
||||
/** Debounce a function call by wait milliseconds. */
|
||||
export function debounce<T extends (...args: never[]) => void>(
|
||||
fn: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
/** Throttle a function call to at most once per limit milliseconds. */
|
||||
export function throttle<T extends (...args: never[]) => void>(
|
||||
fn: T,
|
||||
limit: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle = false;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (inThrottle) {
|
||||
return;
|
||||
}
|
||||
|
||||
fn(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user