待办清单项目10:使用Vue3和Vite重构前端
为什么要重构前端代码
- 前端代码写得太啰嗦,像讲故事绕弯子
- 前端代码各部分粘在一起,像一团乱麻
- 前端代码改起来很头疼,像修补破旧衣服
- 前端代码难以重复使用,像一次性筷子
- 前端代码难以搬到其他地方用,像搬家时的大件家具
- 前端代码难以部署上线,像组装复杂的玩具
Vue3简介
Vue3是什么?Vue3是一个前端框架,就像是一个乐高积木套装,帮你轻松搭建漂亮的网页。
Vue3的创造者是尤雨溪,他就像是中国的"前端钢铁侠",为全世界的程序员创造了这个好用的工具。
官方网站:https://cn.vuejs.org/
Vue3有哪些厉害的地方?
-
组合式API - 就像厨房里的模块化橱柜,你可以根据需要组合不同的功能,而不是所有东西都塞在一个大抽屉里
-
速度更快 - 就像换了一台更快的电脑,网页加载和响应的速度明显提升
-
工具更好用 - 就像升级了工具箱,有了更好的扳手、螺丝刀,开发起来更顺手
-
兼容性更好 - 就像一把万能钥匙,能适应更多的锁(浏览器和设备)
Vite简介
Vite(发音为"veet",法语中表示"快"的意思)是一个前端开发工具,就像一个超级快的"厨师",能够迅速准备好你的Vue项目。开发者同样是尤雨溪。
Vite有什么特点?
-
超级快的启动速度
- 传统工具可能需要等待几十秒才能启动,而Vite只需不到1秒
- 你修改代码后,页面几乎立即更新,不需要等待
-
简单易用
- 不需要复杂的配置,安装后就能直接使用
- 命令简单:
npm create vite@latest 项目名 -- --template vue
-
开发体验好
- 改了代码不用刷新页面,修改会自动显示
- 报错提示清晰,容易找到问题
Vite和Vue的关系
Vite和Vue就像好朋友一样,它们由同一个人(尤雨溪)创造:
- Vite最初是为Vue设计的
- 虽然现在也支持React等其他框架,但和Vue配合最默契
- Vue官方推荐使用Vite开发Vue项目
- 使用Vite开发Vue项目的好处
- 启动快:不用等很久就能开始写代码
- 热更新快:改完代码立刻看到效果
- 打包快:完成项目后,生成最终文件也很快
简单来说,Vite就是让你开发Vue项目时体验更流畅、更愉快的工具!
重构前端项目
1.在E盘创建一个名为Vue3Projects的目录,然后进入该目录,然后在空白的位置单击鼠标右键,选择【在终端中打开】
2.在cmd中输入命令npm create vite
,然后回车,初次运行这个命令会提示我们要安装create-vite
软件包,我们输入y,然后回车确认,接下来我们把项目的名字Project Name
设置成todoListFront
,然后回车,把包的名字Package Name
也设置成todoListFront
,然后回车,最后,通过键盘箭头上下键选择Vue
这个前端框架,回车。
3.最后我们还是通过上下按键选中JavaScript,然后回车确认
4.如果出现下面的提示信息,说明项目创建成功
5.接下来我们进入到刚才创建的todoListFront
目录中,可以发现vite
已经帮我们创建了一堆程序文件
6.在该文件夹中按住键盘的Shift键,然后单击鼠标右键,点击【通过Code打开】,进去以后点击信任作者
7.点击终端菜单里面的【新建终端】,然后在下面的终端窗口输入npm install
,然后回车运行,该命令用于安装基础软件包,然后再运行npm install vue-router
命令来安装路由软件包。
8.删除src
目录里面的style.css
文件
9.点击main.js
,把第2行代码删除
11.再把components
目录里面的HelloWord.vue
删掉
12.打开App.vue,把代码改成下面这样:
<script setup>
</script>
<template>
<div>
</div>
</template>
<style scoped>
</style>
13.复制一下App.vue
,然后在components
文件夹上粘贴4份,分别改名成Header.vue
, Login.vue
, Regist.vue
和ShowTodoList.vue
,分别是网页头部模板页,登录页,注册页和显示待办事项页
14.在src
文件夹中新建一个router
路由文件夹,然后在router
文件夹中新建一个router.js
文件
15.然后在router.js
文件中插入下面的代码:
import { createRouter, createWebHashHistory } from 'vue-router' // 从vue-router中导入createRouter和createWebHashHistory函数
import Header from '../components/Header.vue' // 导入Header组件
import Login from '../components/Login.vue' // 导入Login组件
import Regist from '../components/Regist.vue' // 导入Regist组件
import ShowTodoList from '../components/ShowTodoList.vue' // 导入ShowTodoList组件
// 创建路由实例
const router = createRouter({
history: createWebHashHistory(), // 使用hash模式的路由历史记录管理
routes: [ // 定义路由配置
{ // 首页路由配置
path: '/', // 路径为根路径
name: 'Header', // 路由名称为'Header'
component: ShowTodoList // 对应的组件为ShowTodoList
},
{ // 登录页面路由配置
path: '/login', // 路径为/login
name: 'Login', // 路由名称为'Login'
component: Login // 对应的组件为Login
},
{ // 注册页面路由配置
path: '/regist', // 路径为/regist
name: 'Regist', // 路由名称为'Regist'
component: Regist // 对应的组件为Regist
},
{ // 显示待办事项列表页面路由配置
path: '/todolist', // 路径为/todolist
name: 'ShowTodoList', // 路由名称为'ShowTodoList'
component: ShowTodoList // 对应的组件为ShowTodoList
}
]
})
export default router // 导出路由实例
16.点开main.js
文件,然后把代码改成下面这样
// 从 'vue' 库中导入 createApp 函数,该函数用于创建 Vue 应用实例
import { createApp } from 'vue'
// 导入根组件 App.vue,这是 Vue 应用的入口组件
import App from './App.vue'
// 导入路由配置,路由用于定义不同的 URL 对应不同的组件
import router from './router/router'
// 使用 createApp 函数创建一个 Vue 应用实例,并将根组件 App 作为参数传递
const app = createApp(App)
// 将路由配置应用到 Vue 实例上,使得应用可以使用路由功能
app.use(router)
// 将创建的 Vue 实例挂载到 id 为 'app' 的 DOM 元素上,这一步是将应用渲染到页面中
app.mount('#app')
17.再点开App.vue,把代码改成下面这样
/* 定义组件行为 */
<script setup>
// 使用组合式 API 的 <script setup> 块
// 从 ./components/ 目录下导入 Header.vue 组件,并将其命名为 Header
import Header from './components/Header.vue';
</script>
<!-- 定义组件的结构和外观 -->
<template>
<div>
<!-- 使用之前导入的 Header 组件 -->
<Header></Header>
<!-- 使用 Vue Router 提供的 router-view 组件,用于渲染与当前路由匹配的组件 -->
<router-view></router-view>
</div>
</template>
/* 定义组件的样式,并且添加了 scoped 属性,使得样式只应用于当前组件 */
<style scoped>
</style>
18.打开components
目录下的Header.vue
,我们来设计网页头部,请用AI帮忙设计,或者粘贴下面的代码:
<script setup>
import { ref, computed } from 'vue';
// 使用ref定义响应式变量isLoggedIn来模拟用户登录状态
const isLoggedIn = ref(false);
// 使用ref定义响应式变量username来存储用户名
const username = ref('');
// 定义退出登录函数logout,该函数会将isLoggedIn设置为false,清空username,并在控制台输出退出登录成功的消息
const logout = () => {
isLoggedIn.value = false;
username.value = '';
console.log('退出登录成功');
};
</script>
<template>
<header class="header">
<div class="logo">
<!-- 标题 -->
<h1>待办事项管理系统</h1>
<!-- 副标题 -->
<p>欢迎使用我们的待办事项管理系统</p>
</div>
<div class="actions">
<!-- 链接到待办事项页面的按钮 -->
<router-link to="/todolist" class="btn">待办事项</router-link>
<!-- 如果用户未登录,则显示登录和注册按钮 -->
<template v-if="!isLoggedIn">
<router-link to="/login" class="btn btn-primary">登录</router-link>
<router-link to="/regist" class="btn btn-outline">注册</router-link>
</template>
<!-- 如果用户已登录,则显示用户名和退出登录按钮 -->
<template v-else>
<span class="username">{{ username }}</span>
<button class="btn btn-danger" @click="logout">退出登录</button>
</template>
</div>
</header>
</template>
<style scoped>
.header {
display: flex;
justify-content: space-between; /* 使子元素在主轴上两端对齐 */
align-items: center; /* 使子元素在交叉轴上居中对齐 */
padding: 1rem 2rem; /* 设置内边距 */
background-color: #f8f9fa; /* 设置背景颜色 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
}
.logo h1 {
margin: 0; /* 去除默认的外边距 */
font-size: 1.5rem; /* 设置字体大小 */
color: #333; /* 设置字体颜色 */
}
.logo p {
margin: 0.25rem 0 0; /* 设置外边距 */
font-size: 0.9rem; /* 设置字体大小 */
color: #666; /* 设置字体颜色 */
}
.actions {
display: flex; /* 使用Flex布局 */
gap: 0.75rem; /* 设置子元素之间的间距 */
align-items: center; /* 使子元素在交叉轴上居中对齐 */
}
.username {
font-weight: 500; /* 设置字体粗细 */
margin-right: 0.5rem; /* 设置右边距 */
color: #333; /* 设置字体颜色 */
}
.btn {
text-decoration: none; /* 去除下划线 */
padding: 0.5rem 1rem; /* 设置内边距 */
border: none; /* 去除边框 */
border-radius: 4px; /* 设置圆角 */
cursor: pointer; /* 设置鼠标悬停时的光标样式 */
font-size: 0.9rem; /* 设置字体大小 */
transition: all 0.2s; /* 设置过渡效果 */
}
.btn:hover {
opacity: 0.9; /* 设置鼠标悬停时的透明度 */
}
.btn-primary {
background-color: #4361ee; /* 设置按钮背景颜色 */
color: white; /* 设置按钮字体颜色 */
}
.btn-outline {
background-color: transparent; /* 设置按钮背景颜色为透明 */
border: 1px solid #4361ee; /* 设置按钮边框 */
color: #4361ee; /* 设置按钮字体颜色 */
}
.btn-danger {
background-color: #ef476f; /* 设置按钮背景颜色 */
color: white; /* 设置按钮字体颜色 */
}
</style>
19.在终端中运行命令npm run dev
,然后安装Ctrl键,点击下面的网址
20.应该可以在浏览器中看到我们设计的网页头部
21.接下来设计登录页,把Login.vue
的代码改成下面这样:
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const username = ref('');
const password = ref('');
const handleLogin = () => {
// 这里添加登录逻辑
console.log('登录信息:', {
username: username.value,
password: password.value
});
// 登录成功后跳转到待办事项页面
// router.push('/todolist');
};
</script>
<template>
<div class="login-wrapper">
<div class="login-card">
<div class="login-header">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>
<h2>用户登录</h2>
<p>欢迎回来!请登录您的账户。</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="input-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="username"
placeholder="请输入用户名"
required
/>
</div>
<div class="input-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
placeholder="请输入密码"
required
/>
</div>
<button type="submit" class="btn-login">登 录</button>
</form>
<div class="login-footer">
<router-link to="/regist">还没有账号?立即注册</router-link>
</div>
</div>
</div>
</template>
<style scoped>
.login-wrapper {
/* min-height: 100vh; */ /* 移除 min-height */
flex-grow: 1; /* 让 wrapper 在 main-content 内填充可用空间 */
display: flex;
justify-content: center;
align-items: center;
/* background-color: #fff; */ /* 背景色由 main-content 或 body 控制,或者按需保留 */
padding: 2rem; /* 保留内边距 */
}
.login-card {
background: white;
padding: 2.5rem 3rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 420px;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.login-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
}
.login-header {
margin-bottom: 2rem;
}
.login-header svg {
color: #4361ee;
margin-bottom: 0.5rem;
}
.login-header h2 {
font-size: 1.8rem;
color: #333;
margin-bottom: 0.5rem;
}
.login-header p {
color: #777;
font-size: 0.95rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem; /* 增加输入框间距 */
}
.input-group {
text-align: left;
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
font-size: 0.9rem;
}
.input-group input {
width: 100%;
padding: 0.8rem 1rem; /* 增加内边距 */
border: 1px solid #ddd;
border-radius: 6px; /* 更圆润的边角 */
font-size: 1rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
box-sizing: border-box; /* 确保 padding 不会撑大元素 */
}
.input-group input:focus {
outline: none;
border-color: #4361ee;
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1); /* 添加聚焦光晕效果 */
}
.btn-login {
padding: 0.9rem; /* 增加按钮内边距 */
background: linear-gradient(90deg, #4361ee 0%, #3a53c0 100%); /* 渐变背景 */
color: white;
border: none;
border-radius: 6px;
font-size: 1.1rem; /* 增大字体 */
font-weight: 600; /* 加粗字体 */
cursor: pointer;
transition: all 0.3s ease;
margin-top: 1rem; /* 与上方输入框的间距 */
letter-spacing: 1px; /* 增加字间距 */
}
.btn-login:hover {
background: linear-gradient(90deg, #3a53c0 0%, #4361ee 100%);
box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);
transform: translateY(-2px);
}
.login-footer {
margin-top: 1.5rem;
font-size: 0.9rem;
}
.login-footer a {
color: #4361ee;
text-decoration: none;
transition: color 0.3s ease;
}
.login-footer a:hover {
color: #3451db;
text-decoration: underline;
}
</style>
22.然后设计注册页,把Regist.vue
文件的代码改成下面这样:
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const username = ref('');
const password = ref('');
const confirmPassword = ref('');
const usernameError = ref(''); // 用户名错误信息
const passwordError = ref(''); // 密码格式错误信息
const confirmPasswordError = ref(''); // 确认密码不匹配错误信息
// --- 验证函数 ---
const validateUsername = (value) => {
if (!value) return '用户名不能为空';
if (value.length < 3 || value.length > 20) return '用户名长度必须在 3 到 20 个字符之间';
if (!/^[a-zA-Z0-9_]+$/.test(value)) return '用户名只能包含字母、数字和下划线';
return ''; // 无错误
};
const validatePassword = (value) => {
if (!value) return '密码不能为空';
if (value.length < 6) return '密码长度至少为 6 个字符';
// 可以根据需要添加更复杂的密码规则
return ''; // 无错误
};
// --- 结束:验证函数 ---
const handleRegister = () => {
// --- 执行验证 ---
usernameError.value = validateUsername(username.value);
passwordError.value = validatePassword(password.value);
confirmPasswordError.value = ''; // 先清空确认密码错误
// 检查两次输入的密码是否一致 (仅在密码格式有效时检查)
if (!passwordError.value && password.value !== confirmPassword.value) {
confirmPasswordError.value = '两次输入的密码不一致';
}
// 如果有任何错误,则阻止提交
if (usernameError.value || passwordError.value || confirmPasswordError.value) {
return;
}
// --- 结束:执行验证 ---
// 清除所有错误信息
usernameError.value = '';
passwordError.value = '';
confirmPasswordError.value = '';
// 这里添加实际的注册逻辑,例如调用 API
console.log('注册信息:', {
username: username.value,
password: password.value // 注意:实际应用中不应明文传输密码
});
alert('注册成功(模拟)!'); // 模拟提示
// 注册成功后可以跳转到登录页面
// router.push('/login');
};
// --- 输入框失焦时触发验证 ---
const triggerUsernameValidation = () => {
usernameError.value = validateUsername(username.value);
};
const triggerPasswordValidation = () => {
passwordError.value = validatePassword(password.value);
// 当密码框失焦时,如果确认密码框已有内容,也检查一致性
if (confirmPassword.value && password.value !== confirmPassword.value) {
confirmPasswordError.value = '两次输入的密码不一致';
} else if (confirmPassword.value) {
confirmPasswordError.value = ''; // 如果一致了,清除错误
}
};
const triggerConfirmPasswordValidation = () => {
if (password.value !== confirmPassword.value) {
confirmPasswordError.value = '两次输入的密码不一致';
} else {
confirmPasswordError.value = '';
}
};
</script>
<template>
<div class="register-wrapper">
<div class="register-card">
<div class="register-header">
<!-- 使用不同的 SVG 图标 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-plus"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
<h2>创建账户</h2>
<p>加入我们,开始管理您的待办事项!</p>
</div>
<form @submit.prevent="handleRegister" class="register-form">
<!-- 用户名输入组 -->
<div class="input-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="username"
placeholder="3-20位, 字母/数字/_"
required
@blur="triggerUsernameValidation"
:class="{ 'input-error': usernameError }"
aria-describedby="username-error"
/>
<p v-if="usernameError" id="username-error" class="error-message">{{ usernameError }}</p>
</div>
<!-- 密码输入组 -->
<div class="input-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
placeholder="至少6位"
required
@blur="triggerPasswordValidation"
:class="{ 'input-error': passwordError }"
aria-describedby="password-error"
/>
<p v-if="passwordError" id="password-error" class="error-message">{{ passwordError }}</p>
</div>
<!-- 确认密码输入组 -->
<div class="input-group">
<label for="confirmPassword">确认密码</label>
<input
type="password"
id="confirmPassword"
v-model="confirmPassword"
placeholder="请再次输入密码"
required
@blur="triggerConfirmPasswordValidation"
:class="{ 'input-error': confirmPasswordError }"
aria-describedby="confirm-password-error"
/>
<p v-if="confirmPasswordError" id="confirm-password-error" class="error-message">{{ confirmPasswordError }}</p>
</div>
<!-- 注册按钮 -->
<button type="submit" class="btn-register">注 册</button>
</form>
<!-- 页脚链接 -->
<div class="register-footer">
<router-link to="/login">已有账号?返回登录</router-link>
</div>
</div>
</div>
</template>
<style scoped>
/* 整体布局和卡片样式 (与 Login.vue 类似) */
.register-wrapper {
/* min-height: 100vh; */ /* 移除 min-height */
flex-grow: 1; /* 让 wrapper 在 main-content 内填充可用空间 */
display: flex;
justify-content: center;
align-items: center;
/* background-color: #fff; */ /* 背景色由 main-content 或 body 控制,或者按需保留 */
padding: 2rem; /* 保留内边距 */
}
.register-card {
background: white;
padding: 2.5rem 3rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 420px;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.register-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
}
/* 头部样式 */
.register-header {
margin-bottom: 2rem;
}
.register-header svg {
color: #4361ee; /* 主题色 */
margin-bottom: 0.5rem;
}
.register-header h2 {
font-size: 1.8rem;
color: #333;
margin-bottom: 0.5rem;
}
.register-header p {
color: #777;
font-size: 0.95rem;
}
/* 表单样式 */
.register-form {
display: flex;
flex-direction: column;
gap: 0.8rem; /* 调整输入框组之间的基础间距 */
}
/* 输入框组样式 */
.input-group {
text-align: left;
margin-bottom: 0.7rem; /* 为错误信息预留空间 */
position: relative; /* 使得错误信息可以定位 */
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
font-size: 0.9rem;
}
.input-group input {
width: 100%;
padding: 0.8rem 1rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
box-sizing: border-box;
}
.input-group input:focus {
outline: none;
border-color: #4361ee;
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
}
/* 错误状态的输入框 */
.input-group input.input-error {
border-color: #ef476f; /* 红色边框 */
}
.input-group input.input-error:focus {
border-color: #ef476f;
box-shadow: 0 0 0 3px rgba(239, 71, 111, 0.1); /* 红色光晕 */
}
/* 错误信息样式 */
.error-message {
color: #ef476f;
font-size: 0.8rem;
margin-top: 0.3rem;
min-height: 1.1rem; /* 占据固定高度防止跳动 */
/* position: absolute; */ /* 可以取消绝对定位,让其自然流动 */
/* bottom: -1.2rem; */
/* left: 0; */
width: 100%; /* 确保宽度 */
text-align: left;
}
/* 注册按钮样式 (与 Login.vue 类似,但类名不同) */
.btn-register {
padding: 0.9rem;
background: linear-gradient(90deg, #4361ee 0%, #3a53c0 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 1rem; /* 与上方元素的间距 */
letter-spacing: 1px;
}
.btn-register:hover {
background: linear-gradient(90deg, #3a53c0 0%, #4361ee 100%);
box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);
transform: translateY(-2px);
}
/* 页脚链接样式 (与 Login.vue 类似) */
.register-footer {
margin-top: 1.5rem;
font-size: 0.9rem;
}
.register-footer a {
color: #4361ee;
text-decoration: none;
transition: color 0.3s ease;
}
.register-footer a:hover {
color: #3451db;
text-decoration: underline;
}
</style>
23.设计待办事项列表页,打开ShowTodoList.vue
文件,把代码改成这样
<script setup>
import { ref } from 'vue';
// 使用 ref 创建响应式的待办事项列表,初始包含一些示例数据
const todos = ref([
{ id: 1, text: '学习 Vue 3', completed: false },
{ id: 2, text: '完成项目报告', completed: true },
{ id: 3, text: '去超市购物', completed: false }
]);
// 用于绑定新待办事项输入框的 ref
const newTodoText = ref('');
// 用于生成唯一 ID 的计数器 (简单实现)
let nextTodoId = ref(todos.value.length + 1);
// 添加新的待办事项
const addTodo = () => {
const text = newTodoText.value.trim();
if (text) {
todos.value.push({
id: nextTodoId.value++,
text: text,
completed: false
});
newTodoText.value = ''; // 清空输入框
}
};
// 切换待办事项的完成状态
const toggleComplete = (id) => {
const todo = todos.value.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
};
// 删除待办事项
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id);
};
</script>
<template>
<div class="todolist-container">
<h2>我的待办事项</h2>
<!-- 添加新待办事项的表单 -->
<form @submit.prevent="addTodo" class="add-todo-form">
<input
type="text"
v-model="newTodoText"
placeholder="添加新的待办事项..."
class="todo-input"
/>
<button type="submit" class="btn-add">添加</button>
</form>
<!-- 待办事项列表 -->
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
:class="{ completed: todo.completed }"
class="todo-item"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleComplete(todo.id)"
class="todo-checkbox"
/>
<span class="todo-text">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" class="btn-remove">删除</button>
</li>
</ul>
<p v-if="todos.length === 0" class="empty-message">当前没有待办事项。</p>
</div>
</template>
<style scoped>
.todolist-container {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
color: #333;
margin-bottom: 1.5rem;
}
.add-todo-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.todo-input {
flex-grow: 1;
padding: 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.todo-input:focus {
outline: none;
border-color: #4361ee; /* 主题色 */
}
.btn-add {
padding: 0.75rem 1.5rem;
background-color: #4361ee; /* 主题色 */
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn-add:hover {
background-color: #3a53c0; /* 主题色加深 */
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 0.8rem 0.5rem;
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item:hover {
background-color: #f9f9f9;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #aaa;
}
.todo-checkbox {
margin-right: 0.75rem;
cursor: pointer;
/* 放大复选框,使其更容易点击 */
width: 18px;
height: 18px;
}
.todo-text {
flex-grow: 1;
color: #444;
word-break: break-all; /* 防止长文本溢出 */
}
.btn-remove {
background-color: #ef476f; /* 危险色 */
color: white;
border: none;
border-radius: 4px;
padding: 0.3rem 0.7rem;
font-size: 0.8rem;
cursor: pointer;
margin-left: 0.5rem;
opacity: 0; /* 默认隐藏 */
transition: opacity 0.2s ease, background-color 0.3s ease;
}
.todo-item:hover .btn-remove {
opacity: 1; /* 悬停时显示删除按钮 */
}
.btn-remove:hover {
background-color: #d6345b; /* 危险色加深 */
}
.empty-message {
text-align: center;
color: #888;
margin-top: 2rem;
font-style: italic;
}
</style>
24.如果前端服务器没有运行,在终端输入npm run dev 运行,然后访问下面的网址,如果已经运行服务器了,可以Ctrl+C停止服务器,然后再运行。
25.打开浏览器后,可以看到简介美观的前端。
前后端互联测试
1.打开注册组件,在找到validateUsername
函数,在if
之后,return
之前,插入下面的代码
// 创建一个新的XMLHttpRequest对象,用于在浏览器和服务器之间进行异步通信
var xhr = new XMLHttpRequest();
// 设置xhr对象的状态改变时的回调函数
// 该函数会在xhr对象的状态(readyState)改变时被调用
xhr.onreadystatechange = function() {
// 当readyState为4(请求已完成且响应已就绪)且status为200(HTTP状态码200表示请求成功)
// 进行以下操作
if (xhr.readyState === 4 && xhr.status === 200) {
// 解析服务器返回的JSON数据
var result = JSON.parse(xhr.responseText);
if (result.code != 200) {
//把用户名错误的提示信息设置成用户名已经存在
usernameError.value = "用户名已存在";
}
}
};
// 访问后台的检测用户名控件
xhr.open("GET", "http://localhost:8080/TodoList/user/checkUsername?username=" + value, true);
// 发送请求
xhr.send();
2.打开IDEA软件,在filter包里面创建一个Java类,取名为CROSFilter
CROSFilter
中的代码如下:
package com.youngshu.todolist.filter;
import com.youngshu.todolist.pojo.SysUser;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
//允许CROS
@WebFilter(urlPatterns = {"/*"})
public class CROSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 允许来自Vue前端的请求
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
httpResponse.setHeader("Access-Control-Max-Age", "3600");
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化方法
}
@Override
public void destroy() {
// 销毁方法
}
}
4.在IDEA中重启服务器,在VSCODE中重启前端项目,试试注册页是否可以显示用户名已经存在。
评论 (0)
暂无评论,来发表第一条评论吧!