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

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,67 @@
/**
* API usage reference for Vue template.
* Mirrors react-vite conventions while keeping Vue-friendly consumption.
*/
import { api, extractApiData, type ApiResponse } from '@/lib/api';
type ID = string | number;
export interface PaginatedResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
export interface ListParams {
page: number;
pageSize: number;
keyword?: string;
}
export interface User {
id: ID;
name: string;
email: string;
avatar?: string;
role: 'admin' | 'user';
createdAt: string;
}
export interface CreateUserParams {
name: string;
email: string;
password: string;
role?: 'admin' | 'user';
}
export interface UpdateUserParams {
name?: string;
email?: string;
avatar?: string;
}
export const userApi = {
async getList(params: ListParams): Promise<PaginatedResult<User>> {
const response = await api.get<ApiResponse<PaginatedResult<User>>>(
'/api/users',
{ params }
);
return extractApiData<PaginatedResult<User>>(response);
},
async getById(id: ID): Promise<User> {
const response = await api.get<ApiResponse<User>>(`/api/users/${id}`);
return extractApiData<User>(response);
},
async create(data: CreateUserParams): Promise<User> {
const response = await api.post<ApiResponse<User>>('/api/users', data);
return extractApiData<User>(response);
},
async update(id: ID, data: UpdateUserParams): Promise<User> {
const response = await api.put<ApiResponse<User>>(`/api/users/${id}`, data);
return extractApiData<User>(response);
},
async delete(id: ID): Promise<void> {
await api.delete(`/api/users/${id}`);
},
};

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
import Button from '@/components/ui/button.vue';
import Input from '@/components/ui/input.vue';
import Select from '@/components/ui/select.vue';
import Checkbox from '@/components/ui/checkbox.vue';
import Label from '@/components/ui/label.vue';
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(value => value === true, 'You must accept the terms'),
});
type UserFormValues = z.infer<typeof userFormSchema>;
const roleOptions = [
{ label: 'User', value: 'user' },
{ label: 'Admin', value: 'admin' },
];
const { defineField, errors, handleSubmit, resetForm, isSubmitting } =
useForm<UserFormValues>({
validationSchema: toTypedSchema(userFormSchema),
initialValues: {
name: '',
email: '',
role: undefined,
agreeTerms: false,
},
});
const [name] = defineField('name');
const [email] = defineField('email');
const [role] = defineField('role');
const [agreeTerms] = defineField('agreeTerms');
const submitLabel = computed(() =>
isSubmitting.value ? 'Submitting...' : 'Submit'
);
const onSubmit = handleSubmit(async values => {
console.log('Form values:', values);
resetForm();
});
</script>
<template>
<form
class="space-y-6 p-6 rounded-lg border border-gray-200 bg-white"
@submit.prevent="onSubmit"
>
<div class="space-y-2">
<Label for="name">Name</Label>
<Input id="name" v-model="name" placeholder="Your name" />
<p v-if="errors.name" class="text-sm font-medium text-red-500">
{{ errors.name }}
</p>
</div>
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
v-model="email"
type="email"
placeholder="you@example.com"
/>
<p class="text-sm text-gray-500">We will never share your email.</p>
<p v-if="errors.email" class="text-sm font-medium text-red-500">
{{ errors.email }}
</p>
</div>
<div class="space-y-2">
<Label for="role">Role</Label>
<Select
id="role"
v-model="role"
:options="roleOptions"
placeholder="Choose a role"
/>
<p v-if="errors.role" class="text-sm font-medium text-red-500">
{{ errors.role }}
</p>
</div>
<div class="flex flex-row items-start space-x-3 space-y-0">
<Checkbox id="agreeTerms" v-model="agreeTerms" class="mt-1" />
<div class="space-y-1 leading-none">
<Label for="agreeTerms">I agree to the terms and privacy policy</Label>
<p v-if="errors.agreeTerms" class="text-sm font-medium text-red-500">
{{ errors.agreeTerms }}
</p>
</div>
</div>
<Button type="submit" :disabled="isSubmitting">{{ submitLabel }}</Button>
</form>
</template>

View File

@@ -0,0 +1,147 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import Button from '@/components/ui/button.vue';
import Card from '@/components/ui/card.vue';
import Input from '@/components/ui/input.vue';
import { useApi } from '@/lib/services';
interface Product {
id: string;
name: string;
price: number;
category: string;
stock: number;
}
interface PaginatedResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
const page = ref(1);
const pageSize = ref(10);
const keyword = ref('');
const mockFetchProducts = async (): Promise<PaginatedResult<Product>> => {
await new Promise(resolve => setTimeout(resolve, 350));
const source: Product[] = [
{
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,
},
];
const filtered = source.filter(item =>
item.name.toLowerCase().includes(keyword.value.toLowerCase())
);
return {
list: filtered,
total: filtered.length,
page: page.value,
pageSize: pageSize.value,
};
};
const { data, loading, error, refetch } = useApi(mockFetchProducts, {
immediate: true,
});
watch([page, pageSize, keyword], () => {
void refetch();
});
const totalPages = computed(() => {
if (!data.value || data.value.total === 0) return 1;
return Math.ceil(data.value.total / pageSize.value);
});
function nextPage(): void {
if (page.value < totalPages.value) {
page.value += 1;
}
}
function prevPage(): void {
if (page.value > 1) {
page.value -= 1;
}
}
</script>
<template>
<section class="p-6 space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold">Products</h1>
<Button>Add product</Button>
</div>
<div class="flex gap-4">
<Input
v-model="keyword"
placeholder="Search products..."
class="max-w-sm"
/>
<Button variant="outline" @click="refetch">Refresh</Button>
</div>
<div v-if="error" class="p-4 text-center text-red-500">
Failed to load: {{ error.message }}
<Button class="ml-2" @click="refetch">Retry</Button>
</div>
<div v-else-if="loading" class="text-center py-8 text-gray-500">
Loading...
</div>
<div
v-else-if="data && data.list.length > 0"
class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"
>
<Card v-for="product in data.list" :key="product.id" class="p-4">
<h3 class="font-semibold text-lg">{{ product.name }}</h3>
<p class="text-gray-500 text-sm">{{ product.category }}</p>
<div class="flex justify-between items-center mt-4">
<span class="text-blue-600 font-bold">${{ product.price }}</span>
<span class="text-gray-400 text-sm">Stock: {{ product.stock }}</span>
</div>
<div class="flex gap-2 mt-4">
<Button size="sm" variant="outline">Edit</Button>
<Button size="sm" variant="destructive">Delete</Button>
</div>
</Card>
</div>
<div v-else class="text-center py-8 text-gray-500">No data</div>
<div v-if="data && data.total > pageSize" class="flex justify-center gap-2">
<Button variant="outline" :disabled="page <= 1" @click="prevPage"
>Previous</Button
>
<span class="px-4 py-2">Page {{ page }} / {{ totalPages }}</span>
<Button variant="outline" :disabled="page >= totalPages" @click="nextPage"
>Next</Button
>
</div>
</section>
</template>