"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

View 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;

View File

@@ -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);
}
}
}

View File

@@ -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);
};
}