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

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,126 @@
/**
* API usage reference — patterns for typed calls and `extractApiData`.
* Agents can mirror this structure when generating API layers.
*/
import { api, extractApiData } from '@/lib/api';
// ============================================
// 1. Shared types (align with your backend contract)
// ============================================
/** Typical wrapped API response */
interface ApiResponse<T = any> {
code: number;
data: T;
message: string;
}
/** Paginated list payload */
interface PaginatedResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
/** List query parameters */
interface ListParams {
page: number;
pageSize: number;
keyword?: string;
}
/** Identifier accepted by the API */
type ID = string | number;
// ============================================
// 2. Domain entities
// ============================================
interface User {
id: ID;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'user';
createdAt: string;
}
interface CreateUserParams {
name: string;
email: string;
password: string;
role?: 'admin' | 'user';
}
interface UpdateUserParams {
name?: string;
email?: string;
avatar?: string;
}
// ============================================
// 3. API functions
// ============================================
export const userApi = {
/** Paginated user list */
getList: async (params: ListParams): Promise<PaginatedResult<User>> => {
const response = await api.get<ApiResponse<PaginatedResult<User>>>(
'/api/users',
{ params }
);
return extractApiData(response);
},
/** Single user by id */
getById: async (id: ID): Promise<User> => {
const response = await api.get<ApiResponse<User>>(`/api/users/${id}`);
return extractApiData(response);
},
/** Create user */
create: async (data: CreateUserParams): Promise<User> => {
const response = await api.post<ApiResponse<User>>('/api/users', data);
return extractApiData(response);
},
/** Update user */
update: async (id: ID, data: UpdateUserParams): Promise<User> => {
const response = await api.put<ApiResponse<User>>(`/api/users/${id}`, data);
return extractApiData(response);
},
/** Delete user */
delete: async (id: ID): Promise<void> => {
await api.delete(`/api/users/${id}`);
},
};
// ============================================
// 4. Usage inside a component (commented)
// ============================================
/*
import { useApi } from '@/lib/services';
import { userApi } from '@/examples/api-example';
function UserListPage() {
const { data, loading, error, refetch } = useApi(
() => userApi.getList({ page: 1, pageSize: 10 }),
[]
);
if (loading) return <div>Loading…</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.list.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
*/

View File

@@ -0,0 +1,166 @@
/**
* Form reference — React Hook Form + Zod + shadcn-style Form primitives.
* Agents can copy patterns, not this file wholesale, into production code.
*/
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
// ============================================
// 1. Zod schema
// ============================================
const userFormSchema = z.object({
name: z
.string()
.min(2, 'Name must be at least 2 characters')
.max(50, 'Name must be at most 50 characters'),
email: z.string().email('Enter a valid email address'),
role: z.enum(['admin', 'user'], { required_error: 'Select a role' }),
agreeTerms: z
.boolean()
.refine(val => val === true, 'You must accept the terms'),
});
type UserFormValues = z.infer<typeof userFormSchema>;
// ============================================
// 2. Form component
// ============================================
interface UserFormProps {
defaultValues?: Partial<UserFormValues>;
onSubmit: (data: UserFormValues) => void | Promise<void>;
loading?: boolean;
}
export function UserFormExample({
defaultValues,
onSubmit,
loading,
}: UserFormProps) {
const form = useForm<UserFormValues>({
resolver: zodResolver(userFormSchema),
defaultValues: {
name: '',
email: '',
role: undefined,
agreeTerms: false,
...defaultValues,
},
});
const handleSubmit = async (data: UserFormValues) => {
try {
await onSubmit(data);
form.reset();
} catch (error) {
console.error('Form submit failed:', error);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6'>
{/* Text input */}
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder='Your name' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email */}
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type='email' placeholder='you@example.com' {...field} />
</FormControl>
<FormDescription>We will never share your email.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Select */}
<FormField
control={form.control}
name='role'
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Choose a role' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='user'>User</SelectItem>
<SelectItem value='admin'>Admin</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox */}
<FormField
control={form.control}
name='agreeTerms'
render={({ field }) => (
<FormItem className='flex flex-row items-start space-x-3 space-y-0'>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className='space-y-1 leading-none'>
<FormLabel>I agree to the terms and privacy policy</FormLabel>
</div>
</FormItem>
)}
/>
<Button type='submit' disabled={loading}>
{loading ? 'Submitting…' : 'Submit'}
</Button>
</form>
</Form>
);
}
export default UserFormExample;

View File

@@ -0,0 +1,205 @@
/**
* List page reference — patterns for data tables, search, and pagination.
* Copy ideas, not this file verbatim, into production code.
*/
import { useState } from 'react';
import { useApi } from '@/lib/services';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card } from '@/components/ui/card';
// ============================================
// 1. Shared types (adjust to your API contract)
// ============================================
/** Paginated list payload */
interface PaginatedResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
/** Query params for list endpoints */
interface ListParams {
page: number;
pageSize: number;
keyword?: string;
}
// ============================================
// 2. Domain model
// ============================================
interface Product {
id: string;
name: string;
price: number;
category: string;
stock: number;
}
// Mock fetch — swap for real `api.get` + `extractApiData`
const mockFetchProducts = async (
params: ListParams
): Promise<PaginatedResult<Product>> => {
await new Promise(resolve => setTimeout(resolve, 500));
return {
list: [
{
id: '1',
name: 'Product A',
price: 99.99,
category: 'Electronics',
stock: 100,
},
{
id: '2',
name: 'Product B',
price: 199.99,
category: 'Apparel',
stock: 50,
},
{
id: '3',
name: 'Product C',
price: 49.99,
category: 'Grocery',
stock: 200,
},
],
total: 3,
page: params.page,
pageSize: params.pageSize,
};
};
// ============================================
// 3. Example page component
// ============================================
export function ProductListExample() {
const [params, setParams] = useState<ListParams>({
page: 1,
pageSize: 10,
keyword: '',
});
const { data, loading, error, refetch } = useApi(
() => mockFetchProducts(params),
[params.page, params.pageSize, params.keyword]
);
const handleSearch = (keyword: string) => {
setParams(prev => ({ ...prev, keyword, page: 1 }));
};
const handlePageChange = (page: number) => {
setParams(prev => ({ ...prev, page }));
};
const handleDelete = async (id: string) => {
if (confirm('Delete this item?')) {
// await productApi.delete(id);
console.log('deleted:', id);
refetch();
}
};
if (error) {
return (
<div className='p-4 text-center text-red-500'>
Failed to load: {error.message}
<Button onClick={refetch} className='ml-2'>
Retry
</Button>
</div>
);
}
return (
<div className='p-6 space-y-6'>
<div className='flex justify-between items-center'>
<h1 className='text-2xl font-bold'>Products</h1>
<Button>Add product</Button>
</div>
<div className='flex gap-4'>
<Input
placeholder='Search products…'
value={params.keyword}
onChange={e => handleSearch(e.target.value)}
className='max-w-sm'
/>
<Button variant='outline' onClick={refetch}>
Refresh
</Button>
</div>
{loading && (
<div className='text-center py-8 text-gray-500'>Loading</div>
)}
{!loading && data && (
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-3'>
{data.list.map(product => (
<Card key={product.id} className='p-4'>
<h3 className='font-semibold text-lg'>{product.name}</h3>
<p className='text-gray-500 text-sm'>{product.category}</p>
<div className='flex justify-between items-center mt-4'>
<span className='text-blue-600 font-bold'>
¥{product.price}
</span>
<span className='text-gray-400 text-sm'>
Stock: {product.stock}
</span>
</div>
<div className='flex gap-2 mt-4'>
<Button size='sm' variant='outline'>
Edit
</Button>
<Button
size='sm'
variant='destructive'
onClick={() => handleDelete(product.id)}
>
Delete
</Button>
</div>
</Card>
))}
</div>
)}
{!loading && data?.list.length === 0 && (
<div className='text-center py-8 text-gray-500'>No data</div>
)}
{data && data.total > params.pageSize && (
<div className='flex justify-center gap-2'>
<Button
variant='outline'
disabled={params.page <= 1}
onClick={() => handlePageChange(params.page - 1)}
>
Previous
</Button>
<span className='px-4 py-2'>
Page {params.page} / {Math.ceil(data.total / params.pageSize)}
</span>
<Button
variant='outline'
disabled={params.page >= Math.ceil(data.total / params.pageSize)}
onClick={() => handlePageChange(params.page + 1)}
>
Next
</Button>
</div>
)}
</div>
);
}
export default ProductListExample;