Java 课堂学习笔记
一、项目介绍
基于 Vue3 + Vue Router 4 开发微头条单页应用,实现登录校验、路由跳转、动态发布 / 点赞 / 删除、退出登录功能。
二、模块详情
1. 根组件 src/App.vue
介绍:项目根组件,提供路由出口,用于渲染路由匹配的页面。
<template>
<div class="login-container">
<div class="login-card">
<div class="brand">
<div class="brand-icon">📰</div>
<h1>微头条</h1>
<p>登录后,发现更多精彩</p>
</div>
<div class="form">
<div class="input-group">
<input
type="text"
v-model="username"
placeholder="用户名"
class="input"
/>
</div>
<div class="input-group">
<input
type="password"
v-model="password"
placeholder="密码"
class="input"
/>
</div>
<button class="btn-primary" @click="handleLogin">登录</button>
<button class="btn-secondary" @click="clearForm">清空</button>
<div class="links">
<a href="#">忘记密码?</a>
<span class="separator">·</span>
<a href="#">注册账号</a>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const username = ref('')
const password = ref('')
const router = useRouter()
const handleLogin = () => {
if (username.value === 'weibotiao' && password.value === '123456') {
localStorage.setItem('token', 'fake-token')
router.push('/home')
} else {
alert('用户名或密码错误')
}
}
const clearForm = () => {
username.value = ''
password.value = ''
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* 冷调柔白渐变,无暖色 */
background: linear-gradient(145deg, #f0f4fa 0%, #e9eef4 100%);
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, sans-serif;
padding: 1.5rem;
}
.login-card {
max-width: 440px;
width: 100%;
background: #ffffff;
border-radius: 2rem;
padding: 2.5rem 2rem;
box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.02);
transition: all 0.3s ease;
}
.login-card:hover {
box-shadow: 0 25px 40px -14px rgba(0, 0, 0, 0.08);
}
.brand {
text-align: center;
margin-bottom: 2rem;
}
.brand-icon {
font-size: 3.2rem;
margin-bottom: 0.6rem;
}
h1 {
font-size: 1.9rem;
font-weight: 600;
color: #1f2a3e; /* 深灰蓝,无棕 */
letter-spacing: -0.3px;
margin-bottom: 0.25rem;
}
.brand p {
color: #6c7a8e;
font-size: 0.9rem;
font-weight: 400;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-group {
width: 100%;
}
.input {
width: 100%;
padding: 0.9rem 1.2rem;
font-size: 0.95rem;
border: 1px solid #dfe4ea;
border-radius: 2rem;
background: #ffffff;
outline: none;
transition: all 0.2s;
font-family: inherit;
color: #1f2a3e;
}
.input:focus {
border-color: #8ba0bc;
box-shadow: 0 0 0 3px rgba(139, 160, 188, 0.15);
}
.input::placeholder {
color: #9aabbf;
font-weight: 400;
}
button {
width: 100%;
padding: 0.85rem;
border: none;
border-radius: 3rem;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.btn-primary {
background: #4b6e8a; /* 静谧蓝灰 */
color: white;
margin-top: 0.5rem;
box-shadow: 0 2px 8px rgba(75, 110, 138, 0.15);
}
.btn-primary:hover {
background: #3a5a74;
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(75, 110, 138, 0.2);
}
.btn-secondary {
background: #f1f5f9;
color: #3a5a74;
border: 1px solid #dfe4ea;
}
.btn-secondary:hover {
background: #e6edf4;
transform: translateY(-1px);
}
.links {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-top: 1.2rem;
font-size: 0.8rem;
}
.links a {
color: #7c8ba0;
text-decoration: none;
transition: color 0.2s;
font-weight: 450;
}
.links a:hover {
color: #4b6e8a;
text-decoration: none;
}
.separator {
color: #cbd6e3;
}
/* 移动端适配 */
@media (max-width: 480px) {
.login-card {
padding: 2rem 1.5rem;
}
h1 {
font-size: 1.7rem;
}
}
</style>
2.2 首页 src/views/Home.vue
<template>
<div class="home-container">
<div class="home-card">
<div class="header">
<h2>📰 微头条 · 首页</h2>
<button class="logout-btn" @click="logout">退出</button>
</div>
<div class="publish-area">
<input
v-model="newTitle"
placeholder="写一个吸引人的标题..."
class="publish-input"
@keyup.enter="addNews"
/>
<button class="publish-btn" @click="addNews">
✨ 发布微头条
</button>
</div>
<!-- 排序控件 -->
<div class="sort-bar">
<span class="sort-label">排序:</span>
<button
class="sort-btn"
:class="{ active: sortOrder === 'desc' }"
@click="sortOrder = 'desc'"
>
⏬ 最新在前
</button>
<button
class="sort-btn"
:class="{ active: sortOrder === 'asc' }"
@click="sortOrder = 'asc'"
>
⏫ 最早在前
</button>
</div>
<div class="news-list">
<div v-if="sortedNewsList.length === 0" class="empty-state">
<span class="empty-icon">📭</span>
<p>暂无新闻,去发布第一条吧~</p>
</div>
<div v-for="item in sortedNewsList" :key="item.id" class="news-item">
<h3>{{ item.title }}</h3>
<div class="meta">
<span class="time">🕒 {{ item.time }}</span>
</div>
<div class="content">{{ item.content }}</div>
<div class="actions">
<button class="delete-btn" @click="confirmDelete(item.id)">
🗑️ 删除
</button>
<button class="like-btn" @click="likeNews(item.id)">
👍 点赞 <span class="like-count" :class="{ 'liked-animation': likedId === item.id }">{{ item.likes }}</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 初始新闻数据(包含 timestamp 字段用于排序)
const newsList = ref([
{
id: 1,
title: '微头条1.0 正式发布',
time: '2025-06-01 10:30',
content: '🎉 全新微头条平台上线,记录生活每一刻。',
likes: 3,
timestamp: new Date(2025, 5, 1, 10, 30).getTime()
},
{
id: 2,
title: 'Vue3 学习心得',
time: '2025-06-02 14:15',
content: '组合式 API 真方便!分享给大家。',
likes: 7,
timestamp: new Date(2025, 5, 2, 14, 15).getTime()
}
])
let nextId = 3
const newTitle = ref('')
const likedId = ref(null)
const sortOrder = ref('desc') // 'desc': 最新在前, 'asc': 最早在前
// 计算属性:根据 sortOrder 排序后的列表
const sortedNewsList = computed(() => {
const list = [...newsList.value]
if (sortOrder.value === 'desc') {
return list.sort((a, b) => b.timestamp - a.timestamp)
} else {
return list.sort((a, b) => a.timestamp - b.timestamp)
}
})
const addNews = () => {
if (!newTitle.value.trim()) return
const now = new Date()
newsList.value.unshift({
id: nextId++,
title: newTitle.value,
time: now.toLocaleString(),
content: '新鲜出炉的微头条,快来互动吧~',
likes: 0,
timestamp: now.getTime()
})
newTitle.value = ''
}
const confirmDelete = (id) => {
if (confirm('确定要删除这条微头条吗?')) {
deleteNews(id)
}
}
const deleteNews = (id) => {
newsList.value = newsList.value.filter(item => item.id !== id)
}
const likeNews = (id) => {
const item = newsList.value.find(i => i.id === id)
if (item) {
item.likes++
likedId.value = id
setTimeout(() => { likedId.value = null }, 300)
}
}
const logout = () => {
localStorage.removeItem('token')
router.push('/login')
}
</script>
<style scoped>
/* 样式部分(与上一版相同,仅增加 sort-bar 样式) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.home-container {
min-height: 100vh;
background: linear-gradient(145deg, #f0f4fa 0%, #e9eef4 100%);
font-family: 'Inter', 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, sans-serif;
padding: 2rem 1.5rem;
display: flex;
justify-content: center;
align-items: flex-start;
}
.home-card {
max-width: 800px;
width: 100%;
background: rgba(255, 255, 255, 0.98);
border-radius: 2rem;
padding: 2rem 2rem 2.5rem;
box-shadow: 0 15px 40px -12px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.01);
transition: box-shadow 0.3s ease;
}
.home-card:hover {
box-shadow: 0 20px 45px -14px rgba(0, 0, 0, 0.08);
}
.header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1.5px solid #eef2f6;
}
.header h2 {
font-size: 1.6rem;
font-weight: 600;
color: #1f2a3e;
letter-spacing: -0.3px;
}
.logout-btn {
background: #f1f5f9;
color: #3a5a74;
border: 1px solid #dfe4ea;
padding: 0.4rem 1.2rem;
border-radius: 2rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.logout-btn:hover {
background: #e6edf4;
transform: translateY(-1px);
}
.publish-area {
display: flex;
gap: 0.8rem;
margin-bottom: 1.5rem;
}
.publish-input {
flex: 1;
padding: 0.85rem 1.2rem;
font-size: 0.95rem;
border: 1px solid #dfe4ea;
border-radius: 2rem;
background: #ffffff;
outline: none;
transition: all 0.2s;
font-family: inherit;
color: #1f2a3e;
}
.publish-input:focus {
border-color: #8ba0bc;
box-shadow: 0 0 0 3px rgba(139, 160, 188, 0.2);
}
.publish-input::placeholder {
color: #9aabbf;
font-weight: 400;
}
.publish-btn {
background: #4b6e8a;
color: white;
border: none;
padding: 0.85rem 1.5rem;
border-radius: 2rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.publish-btn:hover {
background: #3a5a74;
transform: translateY(-1px);
box-shadow: 0 6px 12px rgba(75, 110, 138, 0.15);
}
/* 排序栏样式 */
.sort-bar {
display: flex;
align-items: center;
gap: 0.8rem;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #eef2f6;
}
.sort-label {
font-size: 0.85rem;
color: #6c7a8e;
font-weight: 500;
}
.sort-btn {
background: #f1f5f9;
border: 1px solid #dfe4ea;
padding: 0.3rem 1rem;
border-radius: 2rem;
font-size: 0.8rem;
font-weight: 500;
color: #4a5b6e;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.sort-btn:hover {
background: #e6edf4;
transform: translateY(-1px);
}
.sort-btn.active {
background: #4b6e8a;
border-color: #4b6e8a;
color: white;
}
.news-list {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.empty-state {
text-align: center;
padding: 3rem 2rem;
background: #fafcff;
border-radius: 1.5rem;
border: 1px dashed #dce3ec;
color: #8ba0bc;
}
.empty-icon {
font-size: 3rem;
display: block;
margin-bottom: 0.5rem;
opacity: 0.6;
}
.empty-state p {
font-size: 0.9rem;
}
.news-item {
background: #ffffff;
border-radius: 1.5rem;
padding: 1.5rem;
border: 1px solid #eef2f6;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02);
}
.news-item:hover {
border-color: #dce3ec;
box-shadow: 0 12px 24px -16px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.news-item h3 {
font-size: 1.2rem;
font-weight: 600;
color: #1f2a3e;
margin-bottom: 0.5rem;
}
.meta {
margin-bottom: 0.75rem;
}
.time {
font-size: 0.7rem;
color: #9aabbf;
background: #f8fafd;
padding: 0.2rem 0.6rem;
border-radius: 1rem;
display: inline-block;
}
.content {
font-size: 0.9rem;
color: #4a5b6e;
line-height: 1.45;
margin-bottom: 1.2rem;
}
.actions {
display: flex;
gap: 1rem;
border-top: 1px solid #f0f4fa;
padding-top: 1rem;
}
.actions button {
background: none;
border: none;
font-size: 0.8rem;
font-weight: 500;
padding: 0.4rem 1rem;
border-radius: 2rem;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.delete-btn {
color: #a0afc3;
background: #f8fafd;
border: 1px solid #eef2f6;
}
.delete-btn:hover {
color: #e57373;
background: #fff5f5;
border-color: #ffcdd2;
}
.like-btn {
color: #4b6e8a;
background: #f1f5f9;
border: 1px solid #e2e8f0;
}
.like-btn:hover {
background: #e6edf4;
color: #3a5a74;
transform: translateY(-1px);
}
.like-count {
display: inline-block;
min-width: 1.8rem;
text-align: center;
font-weight: 600;
}
.liked-animation {
animation: likePop 0.3s cubic-bezier(0.34, 1.2, 0.64, 1);
}
@keyframes likePop {
0% { transform: scale(1); }
50% { transform: scale(1.4); color: #e87c6e; }
100% { transform: scale(1); }
}
@media (max-width: 640px) {
.home-container {
padding: 1rem;
}
.home-card {
padding: 1.5rem;
}
.header h2 {
font-size: 1.3rem;
}
.publish-area {
flex-direction: column;
}
.publish-btn {
width: 100%;
justify-content: center;
}
.sort-bar {
flex-wrap: wrap;
justify-content: center;
}
.news-item {
padding: 1rem;
}
.actions button {
padding: 0.3rem 0.8rem;
}
}
</style>
2.3 src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue' // 路径根据你的实际位置调整
import Home from '../views/Home.vue'
const routes = [
{ path: '/login', component: Login }, // 登录页路由
{ path: '/home', component: Home },
{ path: '/', redirect: '/login' } // 访问根路径自动跳转到登录页
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2.4 src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
2.5 src/App.vue
<template>
<router-view />
</template>
2.6 运行结果
登录页面

首页

浙公网安备 33010602011771号