提示词:
你是一名资深前端工程师,需按照以下规范生成代码:
1. 技术栈:Vue3 + TypeScript + Setup 语法 + Tailwind CSS;
2. 代码规范:符合 ESLint 规则,包含完整注释,支持响应式;
3. 功能要求:实现 登录的 表单 组件;
4. 输出格式:先说明组件逻辑,再给出完整代码(模板+脚本+样式)
5. 我需要打包成标准的项目结构
登录表单组件标准化项目结构(Vue3 + TS + Vite + Tailwind CSS)
以下是符合工业级标准的 Vue3 项目结构,包含登录组件、完整的工程配置、依赖管理等,可直接克隆/搭建并运行。
一、整体项目结构
vue3-login-template/
├── public/ # 静态资源(不会被打包处理)
│ └── favicon.ico # 网站图标
├── src/ # 源码目录
│ ├── assets/ # 静态资源(会被打包处理)
│ │ ├── styles/ # 全局样式
│ │ │ ├── main.css # 全局基础样式
│ │ │ └── tailwind.css # Tailwind 入口样式
│ │ └── images/ # 图片资源(如登录页背景图)
│ ├── components/ # 公共组件
│ │ └── LoginForm/ # 登录表单组件(独立目录便于维护)
│ │ ├── index.vue # 登录组件核心代码
│ │ └── type.ts # 组件类型定义(拆分更清晰)
│ ├── views/ # 页面视图
│ │ └── Login/ # 登录页面
│ │ └── index.vue # 登录页面(集成LoginForm组件)
│ ├── router/ # 路由配置
│ │ └── index.ts # 路由定义
│ ├── utils/ # 工具函数
│ │ ├── validate.ts # 校验工具(抽离通用校验逻辑)
│ │ └── request.ts # 请求封装(Axios)
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件
│ └── env.d.ts # TS类型声明(Vite自动生成)
├── .eslintrc.cjs # ESLint配置
├── .prettierrc.cjs # Prettier配置
├── index.html # HTML入口
├── package.json # 依赖配置
├── tsconfig.json # TS配置
├── vite.config.ts # Vite配置
└── tailwind.config.js # Tailwind配置
二、核心文件代码实现
1. 项目初始化依赖(package.json)
json
{
"name": "vue3-login-template",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.6.8",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite": "^5.2.8",
"vue-tsc": "^2.0.11"
}
}
2. 入口文件(src/main.ts)
typescript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 全局样式
import './assets/styles/main.css'
import './assets/styles/tailwind.css'
// 创建Vue实例
const app = createApp(App)
// 挂载路由
app.use(router)
// 挂载到DOM
app.mount('#app')
3. 根组件(src/App.vue)
vue
<template>
<router-view />
</template>
<script setup lang="ts">
// 根组件仅作为路由出口
</script>
<style scoped>
#app {
@apply font-sans antialiased text-gray-900 bg-gray-50 min-h-screen;
}
</style>
4. 路由配置(src/router/index.ts)
typescript
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Login from '../views/Login/index.vue'
// 路由规则
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/login' // 默认跳转到登录页
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
title: '用户登录' // 页面标题
}
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫:设置页面标题
router.afterEach((to) => {
if (to.meta.title) {
document.title = to.meta.title as string
}
})
export default router
5. Tailwind 配置
(1)tailwind.config.js
javascript
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: '#3b82f6', // 自定义主题色(蓝色)
},
},
},
plugins: [],
}
(2)src/assets/styles/tailwind.css
css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Tailwind 基础样式扩展 */
@layer base {
body {
@apply bg-gray-50;
}
}
/* 自定义组件样式 */
@layer components {
.form-input {
@apply w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent;
}
.btn-primary {
@apply w-full py-2 px-4 bg-primary text-white font-medium rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-70 disabled:cursor-not-allowed;
}
}
(3)src/assets/styles/main.css
css
/* 全局重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 滚动条优化 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 3px;
}
::-webkit-scrollbar-track {
background-color: #f9fafb;
}
6. 工具函数
(1)src/utils/validate.ts(通用校验工具)
typescript
/**
* 校验手机号
* @param phone 手机号
* @returns 是否有效
*/
export const isPhoneValid = (phone: string): boolean => {
const reg = /^1[3-9]\d{9}$/
return reg.test(phone)
}
/**
* 校验邮箱
* @param email 邮箱
* @returns 是否有效
*/
export const isEmailValid = (email: string): boolean => {
const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return reg.test(reg)
}
/**
* 校验密码(6-16位,字母+数字组合)
* @param password 密码
* @returns 是否有效
*/
export const isPasswordValid = (password: string): boolean => {
const reg = /^(?=.*[a-zA-Z])(?=.*\d).{6,16}$/
return reg.test(password)
}
(2)src/utils/request.ts(Axios 请求封装)
typescript
import axios from 'axios'
// 创建Axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取接口前缀
timeout: 10000, // 超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 请求拦截器:添加token
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器:统一处理错误
request.interceptors.response.use(
(response) => {
const res = response.data
// 假设接口返回code=200为成功
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败') // 需安装element-plus
return Promise.reject(res)
}
return res
},
(error) => {
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default request
7. 登录组件拆分
(1)src/components/LoginForm/type.ts(类型定义)
typescript
/**
* 登录表单数据类型
*/
export interface LoginForm {
account: string // 账号(手机号/邮箱)
password: string // 密码
remember: boolean // 是否记住密码
}
/**
* 表单错误信息类型
*/
export interface FormErrors {
account?: string
password?: string
}
(2)src/components/LoginForm/index.vue(组件核心)
vue
<template>
<!-- 登录表单容器 -->
<div class="login-form-container w-full max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold text-center text-gray-800 mb-6">用户登录</h2>
<!-- 登录表单 -->
<form @submit.prevent="handleSubmit" class="space-y-4">
<!-- 账号输入框 -->
<div class="form-item">
<label for="account" class="block text-sm font-medium text-gray-700 mb-1">
账号(手机号/邮箱)
</label>
<input
id="account"
v-model="form.account"
type="text"
:class="[
'form-input',
formErrors.account ? 'border-red-500' : 'border-gray-300'
]"
placeholder="请输入手机号或邮箱"
@blur="validateField('account')"
/>
<p v-if="formErrors.account" class="mt-1 text-sm text-red-500">{{ formErrors.account }}</p>
</div>
<!-- 密码输入框 -->
<div class="form-item">
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
密码
</label>
<input
id="password"
v-model="form.password"
type="password"
:class="[
'form-input',
formErrors.password ? 'border-red-500' : 'border-gray-300'
]"
placeholder="请输入密码(6-16位)"
@blur="validateField('password')"
/>
<p v-if="formErrors.password" class="mt-1 text-sm text-red-500">{{ formErrors.password }}</p>
</div>
<!-- 记住密码 & 忘记密码 -->
<div class="flex items-center justify-between">
<label class="flex items-center">
<input
v-model="form.remember"
type="checkbox"
class="w-4 h-4 text-primary focus:ring-primary border-gray-300 rounded"
/>
<span class="ml-2 text-sm text-gray-700">记住密码</span>
</label>
<a href="#" class="text-sm text-primary hover:text-primary/80">忘记密码?</a>
</div>
<!-- 提交按钮 -->
<button
type="submit"
:disabled="isSubmitting"
class="btn-primary"
>
<span v-if="!isSubmitting">登录</span>
<span v-else class="flex items-center justify-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
登录中...
</span>
</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { LoginForm, FormErrors } from './type'
import { isPhoneValid, isEmailValid, isPasswordValid } from '../../utils/validate'
/**
* 响应式表单数据
*/
const form = reactive<LoginForm>({
account: '',
password: '',
remember: false
})
/**
* 表单错误信息
*/
const formErrors = reactive<FormErrors>({})
/**
* 提交状态标识
*/
const isSubmitting = ref(false)
/**
* 自定义提交事件 - 向父组件传递登录参数
*/
const emit = defineEmits<{
(e: 'submit', formData: LoginForm): void;
}>();
/**
* 校验单个字段
* @param field 要校验的字段名
*/
const validateField = (field: keyof LoginForm) => {
switch (field) {
case 'account':
if (!form.account) {
formErrors.account = '账号不能为空'
} else if (!isPhoneValid(form.account) && !isEmailValid(form.account)) {
formErrors.account = '请输入正确的手机号或邮箱'
} else {
delete formErrors.account
}
break
case 'password':
if (!form.password) {
formErrors.password = '密码不能为空'
} else if (!isPasswordValid(form.password)) {
formErrors.password = '密码需6-16位,且包含字母和数字'
} else {
delete formErrors.password
}
break
default:
break
}
}
/**
* 全量校验表单
* @returns 是否校验通过
*/
const validateForm = (): boolean => {
// 校验所有字段
validateField('account')
validateField('password')
// 判断是否有错误信息
return Object.keys(formErrors).length === 0
}
/**
* 处理表单提交
*/
const handleSubmit = async () => {
// 全量校验
if (!validateForm()) return
// 设置提交状态
isSubmitting.value = true
try {
// 向外触发提交事件,父组件可监听该事件处理登录逻辑
emit('submit', { ...form })
} catch (error) {
console.error('登录提交失败:', error)
} finally {
// 重置提交状态
isSubmitting.value = false
}
}
</script>
<style scoped>
/* 基础样式补充(Tailwind 未覆盖的细节) */
.login-form-container {
@apply mt-10;
}
.form-item {
@apply transition-all duration-200;
}
/* 适配移动端小屏 */
@media (max-width: 480px) {
.login-form-container {
@apply p-4 mx-2;
}
}
</style>
8. 登录页面(src/views/Login/index.vue)
vue
<template>
<div class="login-page min-h-screen flex items-center justify-center bg-gray-100">
<!-- 登录表单组件 -->
<LoginForm @submit="handleLogin" />
</div>
</template>
<script setup lang="ts">
import LoginForm from '../../components/LoginForm/index.vue'
import request from '../../utils/request'
/**
* 处理登录逻辑
* @param formData 登录表单数据
*/
const handleLogin = async (formData: any) => {
try {
console.log('登录参数:', formData)
// 对接登录接口
// const res = await request.post('/api/login', formData)
// if (res.success) {
// // 记住密码逻辑
// if (formData.remember) {
// localStorage.setItem('token', res.data.token)
// } else {
// sessionStorage.setItem('token', res.data.token)
// }
// // 跳转首页(需配置首页路由)
// // router.push('/home')
// ElMessage.success('登录成功')
// }
} catch (error) {
console.error('登录失败:', error)
}
}
</script>
<style scoped>
.login-page {
@apply px-4;
}
</style>
9. 配置文件
(1)vite.config.ts
typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
// 路径别名
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000, // 开发服务器端口
open: true, // 启动后自动打开浏览器
cors: true // 允许跨域
},
build: {
outDir: 'dist', // 打包输出目录
assetsDir: 'assets', // 静态资源目录
minify: 'terser', // 压缩方式
terserOptions: {
// 移除console和debugger
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
(2)tsconfig.json
json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* 路径别名 */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* 模块解析选项 */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* 类型检查选项 */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
(3).eslintrc.cjs
javascript
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser'
},
plugins: ['vue', '@typescript-eslint'],
rules: {
'vue/multi-word-component-names': 'off', // 关闭组件名多单词校验(LoginForm已符合)
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}
三、项目使用步骤
1. 初始化项目
bash
# 创建项目目录
mkdir vue3-login-template && cd vue3-login-template
# 初始化package.json(按提示回车)
npm init -y
# 安装依赖
npm install vue vue-router axios
npm install -D vite @vitejs/plugin-vue typescript vue-tsc @vue/eslint-config-typescript @vue/eslint-config-prettier eslint prettier tailwindcss postcss autoprefixer
2. 创建上述目录和文件
按上面的项目结构,创建所有文件并写入对应代码。
3. 初始化Tailwind CSS
bash
npx tailwindcss init -p
(生成的tailwind.config.js已在上面提供,可直接替换)
4. 运行项目
bash
# 开发环境
npm run dev
# 打包构建
npm run build
# 预览打包结果
npm run preview
四、项目特性说明
- 标准化结构:遵循Vue3官方推荐的项目结构,便于团队协作和后期扩展;
- 类型安全:全流程TS类型覆盖,减少运行时错误;
- 工程化配置:集成ESLint+Prettier代码规范、Tailwind CSS样式方案;
- 可扩展性 :
- 路由可直接添加首页、注册页等;
- 工具函数抽离通用逻辑,便于复用;
- 请求封装支持拦截器、环境变量,可直接对接后端接口;
- 生产级优化:打包时移除console/debugger,压缩代码,静态资源分类;
- 响应式适配:适配移动端/PC端,交互体验友好。
五、扩展建议
- 添加UI组件库:如Element Plus,替换手动写的按钮/提示框;
- 环境变量配置 :创建
.env.development/.env.production区分开发/生产环境接口地址; - 状态管理:如需全局状态(如用户信息),可集成Pinia;
- 单元测试:添加Vitest+@vue/test-utils编写组件测试用例;
- 部署配置:添加nginx.conf配置文件,适配生产环境部署。