# 创建Vue3+TypeScript项目
npm init vite@latest my-vue3-ts-app -- --template vue-ts
# 进入项目目录
cd my-vue3-ts-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev
命令 | 描述 |
---|---|
| 启动开发服务器 |
| 构建生产环境版本 |
| 预览生产环境构建结果 |
| 代码检查 |
| 代码格式化 |
<!-- components/BaseButton.vue -->
<template>
<button
:class="[
'px-4 py-2 rounded transition-all duration-200',
variantClass,
sizeClass,
{ 'opacity-75 cursor-not-allowed': disabled },
]"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script lang="ts" setup>
import { computed, defineProps, defineEmits } from 'vue';
const props = defineProps({
variant: {
type: String,
default: 'primary',
validator: (value: string) => ['primary', 'secondary', 'success', 'danger', 'warning', 'info'].includes(value),
},
size: {
type: String,
default: 'medium',
validator: (value: string) => ['small', 'medium', 'large'].includes(value),
},
disabled: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['click']);
const variantClass = computed(() => {
const classes = {
primary: 'bg-primary text-white hover:bg-primary/90',
secondary: 'bg-secondary text-white hover:bg-secondary/90',
success: 'bg-success text-white hover:bg-success/90',
danger: 'bg-danger text-white hover:bg-danger/90',
warning: 'bg-warning text-white hover:bg-warning/90',
info: 'bg-info text-white hover:bg-info/90',
};
return classes[props.variant];
});
const sizeClass = computed(() => {
const classes = {
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
};
return classes[props.size];
});
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emits('click', event);
}
};
</script>
<!-- components/BaseInput.vue -->
<template>
<div class="form-group">
<label v-if="label" :for="id" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
</label>
<div class="relative">
<input
:id="id"
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:class="[
'w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50',
{
'border-gray-300': !error,
'border-red-300 focus:ring-red-500': error,
'bg-gray-100 cursor-not-allowed': disabled || readonly,
},
]"
@input="$emit('update:modelValue', $event.target.value)"
@blur="$emit('blur', $event)"
@focus="$emit('focus', $event)"
/>
<span v-if="prefix" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
{{ prefix }}
</span>
<span v-if="suffix" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500">
{{ suffix }}
</span>
</div>
<p v-if="error" class="mt-1 text-sm text-red-500">{{ error }}</p>
</div>
</template>
<script lang="ts" setup>
import { computed, defineProps, defineEmits } from 'vue';
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
label: {
type: String,
default: '',
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
error: {
type: String,
default: '',
},
prefix: {
type: String,
default: '',
},
suffix: {
type: String,
default: '',
},
id: {
type: String,
default: () => `input-${Math.random().toString(36).substring(2, 10)}`,
},
});
const emits = defineEmits(['update:modelValue', 'blur', 'focus']);
</script>
// composables/useFormValidation.ts
import { reactive, computed, ref } from 'vue';
export const useFormValidation = <T extends Record<string, any>>(
initialValues: T,
validationRules: {
[K in keyof T]?: {
required?: boolean;
pattern?: RegExp;
minLength?: number;
maxLength?: number;
custom?: (value: T[K]) => boolean;
message?: string;
}[];
}
) => {
const formData = reactive({ ...initialValues });
const errors = reactive<{ [K in keyof T]?: string }>({});
const validateField = (field: keyof T) => {
if (!validationRules[field]) return true;
const value = formData[field];
const rules = validationRules[field]!;
let isValid = true;
errors[field] = '';
for (const rule of rules) {
if (rule.required && !value) {
errors[field] = rule.message || '此字段为必填项';
isValid = false;
break;
}
if (rule.pattern && value && !rule.pattern.test(String(value))) {
errors[field] = rule.message || '格式不正确';
isValid = false;
break;
}
if (rule.minLength && value && String(value).length < rule.minLength) {
errors[field] = rule.message || `最少需要${rule.minLength}个字符`;
isValid = false;
break;
}
if (rule.maxLength && value && String(value).length > rule.maxLength) {
errors[field] = rule.message || `最多不能超过${rule.maxLength}个字符`;
isValid = false;
break;
}
if (rule.custom && !rule.custom(value)) {
errors[field] = rule.message || '验证失败';
isValid = false;
break;
}
}
return isValid;
};
const validateAll = () => {
let isValid = true;
for (const field in validationRules) {
if (!validateField(field as keyof T)) {
isValid = false;
}
}
return isValid;
};
const resetForm = () => {
for (const field in formData) {
formData[field as keyof T] = initialValues[field as keyof T];
errors[field as keyof T] = '';
}
};
const setFieldValue = (field: keyof T, value: T[field]) => {
formData[field] = value;
validateField(field);
};
const isFormValid = computed(() => {
for (const field in errors) {
if (errors[field as keyof T]) {
return false;
}
}
return true;
});
return {
formData,
errors,
validateField,
validateAll,
resetForm,
setFieldValue,
isFormValid,
};
};
// composables/useApi.ts
import { ref, computed, onMounted, watch } from 'vue';
import axios, { AxiosRequestConfig } from 'axios';
export const useApi = <T = any>(url: string, options: {
method?: 'get' | 'post' | 'put' | 'delete' | 'patch';
params?: Record<string, any>;
data?: Record<string, any>;
headers?: Record<string, any>;
autoFetch?: boolean;
watchParams?: any;
transformData?: (data: any) => T;
errorHandler?: (error: any) => string;
config?: AxiosRequestConfig;
} = {}) => {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const response = ref<any | null>(null);
const fetchData = async (params: Record<string, any> = {}) => {
loading.value = true;
error.value = null;
try {
const config: AxiosRequestConfig = {
method: options.method || 'get',
url,
params: options.method === 'get' ? { ...options.params, ...params } : options.params,
data: options.method !== 'get' ? { ...options.data, ...params } : options.data,
headers: options.headers || {},
...options.config,
};
const res = await axios(config);
response.value = res;
data.value = options.transformData ? options.transformData(res.data) : res.data;
} catch (err) {
error.value = options.errorHandler ? options.errorHandler(err) : (err as Error).message;
} finally {
loading.value = false;
}
};
if (options.autoFetch !== false) {
onMounted(fetchData);
}
if (options.watchParams) {
watch(options.watchParams, fetchData, { deep: true });
}
return {
data,
loading,
error,
response,
fetchData,
};
};
// stores/counter.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
title: 'Pinia Counter',
}),
getters: {
doubleCount: (state) => state.count * 2,
countPlusOne: (state) => state.count + 1,
},
actions: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
incrementBy(value: number) {
this.count += value;
},
reset() {
this.count = 0;
},
},
});
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import AboutView from '../views/AboutView.vue';
import ProductView from '../views/ProductView.vue';
import NotFoundView from '../views/NotFoundView.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: HomeView,
meta: {
title: '首页',
requiresAuth: false,
},
},
{
path: '/about',
name: 'About',
component: AboutView,
meta: {
title: '关于我们',
requiresAuth: false,
},
},
{
path: '/product/:id',
name: 'Product',
component: ProductView,
meta: {
title: '商品详情',
requiresAuth: false,
},
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFoundView,
meta: {
title: '页面不存在',
requiresAuth: false,
},
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
},
});
// 路由守卫
router.beforeEach((to, from, next) => {
document.title = to.meta.title || 'Vue App';
// 身份验证示例
const isAuthenticated = localStorage.getItem('token');
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'Login' });
} else {
next();
}
});
export default router;
<!-- views/LoginView.vue -->
<template>
<div class="container mx-auto px-4 py-12 max-w-md">
<div class="bg-white rounded-xl shadow-lg p-8">
<h2 class="text-2xl font-bold text-center mb-6">登录</h2>
<form @submit.prevent="handleLogin">
<BaseInput
v-model="formData.username"
label="用户名"
placeholder="请输入用户名"
:error="errors.username"
@blur="validateField('username')"
/>
<BaseInput
v-model="formData.password"
type="password"
label="密码"
placeholder="请输入密码"
:error="errors.password"
@blur="validateField('password')"
class="mt-4"
/>
<div class="mt-6">
<BaseButton variant="primary" size="large" block>
登录
</BaseButton>
</div>
</form>
<div class="mt-6 text-center">
<router-link to="/register" class="text-primary hover:underline">
注册账号
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useFormValidation } from '@/composables/useFormValidation';
import BaseInput from '@/components/BaseInput.vue';
import BaseButton from '@/components/BaseButton.vue';
const { formData, errors, validateField, validateAll } = useFormValidation(
{
username: '',
password: '',
},
{
username: [
{ required: true, message: '请输入用户名' },
{ minLength: 3, message: '用户名至少3个字符' },
],
password: [
{ required: true, message: '请输入密码' },
{ minLength: 6, message: '密码至少6个字符' },
],
}
);
const handleLogin = () => {
if (validateAll()) {
// 登录逻辑
console.log('登录成功:', formData);
}
};
</script>
<!-- views/ProductsView.vue -->
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">商品列表</h1>
<div v-if="loading" class="text-center py-12">
<i class="fa fa-spinner fa-spin text-2xl text-primary"></i>
<p class="mt-2">加载中...</p>
</div>
<div v-else-if="error" class="text-center py-12 text-red-500">
<i class="fa fa-exclamation-circle text-2xl"></i>
<p class="mt-2">{{ error }}</p>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="product in products" :key="product.id" class="bg-white rounded-lg shadow-md overflow-hidden transition-all duration-300 hover:shadow-lg">
<img :src="product.image" alt="product.name" class="w-full h-48 object-cover">
<div class="p-4">
<h3 class="font-bold text-lg mb-2">{{ product.name }}</h3>
<p class="text-gray-600 mb-4">{{ product.description }}</p>
<div class="flex justify-between items-center">
<span class="font-bold text-primary text-xl">{{ product.price }}</span>
<BaseButton variant="primary" size="small">
加入购物车
</BaseButton>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useApi } from '@/composables/useApi';
import BaseButton from '@/components/BaseButton.vue';
interface Product {
id: number;
name: string;
description: string;
price: number;
image: string;
}
const { data: products, loading, error, fetchData } = useApi<Product[]>('/api/products', {
method: 'get',
autoFetch: true,
});
</script>
通过以上组件封装和组合式函数开发方法,你可以:
这些方法与Vue3+TypeScript项目结合,可以帮助你高效构建高质量的前端应用。
Vue3,TypeScript, 项目开发,指南,组件封装,实操方法,前端开发,JavaScript, 响应式编程,组合式 API, 单文件组件,TypeScript 类型,组件通信,状态管理,工程化
资源地址:
https://pan.quark.cn/s/85ab8f4cc743
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。