第一步 新建模块

第二步 引入依赖

第三步 项目代码
UserController类
java
package com.linwu.controller;
import com.linwu.entity.User;
import com.linwu.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class UserController {
@Autowired
private UserRepository userRepository;
// 默认管理员
private final String adminUser = "tbb"; // ********************输入你自己的账户信息
private final String adminPass = "123456";
// 登录页面
@GetMapping("/")
public String showLoginPage() {
return "login";
}
// 登录逻辑
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
Model model) {
// 管理员登录
if (adminUser.equals(username) && adminPass.equals(password)) {
model.addAttribute("username", username); // ✅ 添加用户名到模型
return "success";
}
// 普通用户登录
User user = userRepository.findByUsername(username);
if (user != null && user.getPassword().equals(password)) {
model.addAttribute("username", user.getUsername()); // ✅ 添加用户名
return "success";
}
model.addAttribute("error", "用户名或密码错误!");
return "login";
}
// 注册页面
@GetMapping("/register")
public String showRegisterPage() {
return "register";
}
// 注册逻辑
@PostMapping("/register")
public String register(@RequestParam String username,
@RequestParam String password,
Model model) {
if (username.equals(adminUser)) {
model.addAttribute("error", "管理员账号不可注册!");
return "register";
}
if (userRepository.findByUsername(username) != null) {
model.addAttribute("error", "该用户名已存在!");
return "register";
}
User newUser = new User();
newUser.setUsername(username);
newUser.setPassword(password);
userRepository.save(newUser);
model.addAttribute("msg", "注册成功,请返回登录!");
return "register";
}
}
User类
java
package com.linwu.entity;
import jakarta.persistence.*; // JPA 注解,用于定义实体类和数据库映射
import lombok.Data; // Lombok 注解,自动生成 getter/setter/toString 等方法
/**
* 用户实体类,对应数据库中的 users 表
*/
@Data // 自动生成 getter、setter、toString、equals、hashCode 等方法
@Entity // 标记这是一个 JPA 实体类,Spring Boot 会将其映射到数据库表
@Table(name = "users") // 指定对应的数据库表名为 "users"
public class User {
@Id // 主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增策略,数据库自动生成主键
private Long id; // 用户ID,唯一标识
@Column(nullable = false, unique = true) // 映射到数据库列,不能为空且唯一
private String username; // 用户名
@Column(nullable = false) // 映射到数据库列,不能为空
private String password; // 用户密码
}
User类
java
package com.linwu.repository;
import com.linwu.entity.User; // 导入User实体类
import org.springframework.data.jpa.repository.JpaRepository; // 导入Spring Data JPA的Repository接口
/**
* 用户仓库接口,用于操作数据库中的 users 表
* JpaRepository 提供了常用的增删改查方法
* 泛型 <User, Long> 表示操作的实体类是 User,主键类型是 Long
*/
public interface UserRepository extends JpaRepository<User, Long> {
/**
* 根据用户名查找用户
* Spring Data JPA 会根据方法名自动生成对应的查询语句
*
* @param username 用户名
* @return User 对象,如果找不到返回 null
*/
User findByUsername(String username);
}
前端UserLogin.vue页面代码
html
<template>
<div class="login-container">
<div class="login-background">
<div class="cloud cloud-1"></div>
<div class="cloud cloud-2"></div>
<div class="cloud cloud-3"></div>
<div class="sun"></div>
<div class="bird bird-1"></div>
<div class="bird bird-2"></div>
</div>
<div class="login-card animate__animated animate__fadeInUp">
<div class="login-header">
<div class="logo-wrapper">
<div class="cartoon-avatar">
<div class="avatar-face">
<div class="eyes">
<div class="eye left-eye"></div>
<div class="eye right-eye"></div>
</div>
<div class="mouth"></div>
</div>
</div>
</div>
<h2>留言板</h2>
<p class="welcome-text">{{ isLogin ? '欢迎回到快乐天地' : '加入我们的快乐大家庭' }}</p>
</div>
<!-- 登录表单 -->
<el-form v-if="isLogin" ref="loginFormRef" :model="loginForm" :rules="loginRules" class="login-form"
@submit.prevent="handleLogin">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="请输入你的小名" size="large" clearable :prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入魔法密码" size="large" show-password
:prefix-icon="Lock" />
</el-form-item>
<el-form-item prop="role">
<el-select v-model="loginForm.role" placeholder="请选择你的身份" size="large" style="width: 100%">
<el-option label="普通小伙伴" value="0">
<span class="option-content">
<el-icon>
<User />
</el-icon>
<span>普通小伙伴</span>
</span>
</el-option>
<el-option label="管理员大大" value="1">
<span class="option-content">
<el-icon>
<Management />
</el-icon>
<span>管理员大大</span>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" native-type="submit" :loading="loading" style="width: 100%"
class="login-button">
{{ loading ? '进入中...' : '进入乐园' }}
</el-button>
</el-form-item>
</el-form>
<!-- 注册表单 -->
<el-form v-else ref="registerFormRef" :model="registerForm" :rules="registerRules" class="register-form"
@submit.prevent="handleRegister">
<el-form-item prop="username">
<el-input v-model="registerForm.username" placeholder="给自己起个小名" size="large" clearable prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="设置魔法密码" size="large" show-password
prefix-icon="Lock" />
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="再次确认密码" size="large"
show-password prefix-icon="Lock" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" native-type="submit" :loading="loading" style="width: 100%"
class="register-button">
{{ loading ? '注册中...' : '成为小伙伴' }}
</el-button>
</el-form-item>
</el-form>
<!-- 错误提示 -->
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon style="margin-top: 20px" closable
class="error-alert" />
<!-- 成功提示 -->
<el-alert v-if="successMessage" :title="successMessage" type="success" show-icon style="margin-top: 20px" closable
class="success-alert" />
<!-- 切换链接 -->
<div class="switch-form">
<span v-if="isLogin">
还没有加入我们?
<el-button type="text" @click="switchToRegister">立即加入</el-button>
</span>
<span v-else>
已经是小伙伴了?
<el-button type="text" @click="switchToLogin">立即进入</el-button>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import { User, Lock, Management, UserFilled, Message } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { loginUsingPost , registerUsingPost } from '../api/user'
interface LoginForm {
username: string
password: string
role: number | undefined
}
interface RegisterForm {
username: string
password: string
confirmPassword: string
role: string
}
const router = useRouter()
const loginFormRef = ref<FormInstance>()
const registerFormRef = ref<FormInstance>()
const isLogin = ref(true)
const loading = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
const loginForm = reactive<LoginForm>({
username: '',
password: '',
role: undefined,
})
const registerForm = reactive<RegisterForm>({
username: '',
password: '',
confirmPassword: '',
role: '1',
})
const loginRules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度应在 2 到 20 个字符之间', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 20, message: '长度应在 3 到 20 个字符之间', trigger: 'blur' }
],
role: [
{ required: true, message: '请选择用户类型', trigger: 'blur' }
]
})
const registerRules = reactive<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度应在 2 到 20 个字符之间', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 20, message: '长度应在 3 到 20 个字符之间', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
const switchToRegister = () => {
isLogin.value = false
errorMessage.value = ''
successMessage.value = ''
}
const switchToLogin = () => {
isLogin.value = true
errorMessage.value = ''
successMessage.value = ''
}
const handleLogin = async () => {
if (!validateLogin()) {
return
}
const res = await loginUsingPost(loginForm)
if (res.code === 0 && res.data) {
successMessage.value = res.message
switchToLogin()
ElMessage.success('登录成功')
router.push('/main')
} else {
ElMessage.error('登录失败')
}
}
const handleRegister = async () => {
if (!validateRegister()) {
return
}
const res = await registerUsingPost(registerForm)
if (res.code === 0 && res.data) {
successMessage.value = res.message
switchToRegister()
ElMessage.success('注册成功')
isLogin.value = true
} else {
ElMessage.error('注册失败')
}
}
const validateLogin = (): boolean => {
if (!loginForm.username || !loginForm.password) {
ElMessage.error('用户名和密码不能为空')
return false
}
return true
}
const validateRegister = (): boolean => {
if (!registerForm.username || !registerForm.password || !registerForm.confirmPassword) {
ElMessage.error('所有字段均不能为空')
return false
}
if (registerForm.password !== registerForm.confirmPassword) {
ElMessage.error('两次输入的密码不一致')
return false
}
return true
}
</script>
<style scoped>
@import url('https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css');
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
padding: 20px;
position: relative;
overflow: hidden;
}
.login-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.cloud {
position: absolute;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.cloud-1 {
width: 120px;
height: 40px;
top: 15%;
left: 10%;
animation: float 8s ease-in-out infinite;
}
.cloud-1::before {
content: '';
position: absolute;
width: 60px;
height: 60px;
top: -30px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.cloud-1::after {
content: '';
position: absolute;
width: 50px;
height: 50px;
top: -20px;
right: 15px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.cloud-2 {
width: 100px;
height: 35px;
top: 25%;
right: 15%;
animation: float 10s ease-in-out infinite;
}
.cloud-2::before {
content: '';
position: absolute;
width: 50px;
height: 50px;
top: -25px;
left: 5px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.cloud-2::after {
content: '';
position: absolute;
width: 40px;
height: 40px;
top: -15px;
right: 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.cloud-3 {
width: 140px;
height: 45px;
bottom: 20%;
left: 20%;
animation: float 12s ease-in-out infinite;
}
.cloud-3::before {
content: '';
position: absolute;
width: 70px;
height: 70px;
top: -35px;
left: 15px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.cloud-3::after {
content: '';
position: absolute;
width: 60px;
height: 60px;
top: -25px;
right: 20px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
}
.sun {
position: absolute;
width: 80px;
height: 80px;
top: 10%;
right: 10%;
background: #FFD166;
border-radius: 50%;
box-shadow: 0 0 40px #FFD166;
animation: rotate 20s linear infinite;
}
.sun::before,
.sun::after {
content: '';
position: absolute;
background: #FFD166;
border-radius: 50%;
}
.sun::before {
width: 20px;
height: 20px;
top: -30px;
left: 30px;
}
.sun::after {
width: 15px;
height: 15px;
bottom: -25px;
right: 25px;
}
.bird {
position: absolute;
width: 30px;
height: 15px;
background: #6A994E;
border-radius: 50% 50% 0 0;
animation: fly 20s linear infinite;
}
.bird::before,
.bird::after {
content: '';
position: absolute;
background: #6A994E;
border-radius: 50%;
}
.bird::before {
width: 10px;
height: 10px;
top: -5px;
left: 5px;
}
.bird::after {
width: 5px;
height: 5px;
top: -2px;
left: 15px;
}
.bird-1 {
top: 30%;
left: -50px;
animation-delay: 0s;
}
.bird-2 {
top: 40%;
left: -50px;
animation-delay: 5s;
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
100% {
transform: translateY(0px);
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes fly {
0% {
transform: translateX(0) translateY(0);
}
100% {
transform: translateX(2000px) translateY(100px);
}
}
.login-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(5px);
border-radius: 25px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 450px;
transition: transform 0.3s ease;
position: relative;
z-index: 1;
border: 3px solid #A7C957;
}
.login-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.logo-wrapper {
display: flex;
justify-content: center;
margin-bottom: 15px;
}
.cartoon-avatar {
width: 100px;
height: 100px;
position: relative;
margin: 0 auto;
}
.avatar-face {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
position: relative;
border: 4px solid #fff;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.eyes {
display: flex;
justify-content: space-around;
padding-top: 30px;
}
.eye {
width: 15px;
height: 15px;
background: #fff;
border-radius: 50%;
position: relative;
overflow: hidden;
}
.eye::before {
content: '';
position: absolute;
width: 7px;
height: 7px;
background: #333;
border-radius: 50%;
top: 4px;
left: 4px;
animation: blink 5s infinite;
}
.mouth {
width: 30px;
height: 15px;
background: #fff;
border-radius: 0 0 15px 15px;
margin: 15px auto 0;
position: relative;
}
.mouth::before {
content: '';
position: absolute;
width: 20px;
height: 10px;
background: #FF6B6B;
border-radius: 0 0 10px 10px;
bottom: 0;
left: 5px;
}
@keyframes blink {
0%, 45%, 55%, 100% {
height: 7px;
}
50% {
height: 2px;
}
}
.login-header h2 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
font-weight: 700;
font-family: 'Comic Sans MS', cursive, sans-serif;
}
.welcome-text {
color: #666;
font-size: 16px;
margin: 0;
font-family: 'Comic Sans MS', cursive, sans-serif;
}
.login-form :deep(.el-form-item),
.register-form :deep(.el-form-item) {
margin-bottom: 24px;
}
.login-form :deep(.el-input__wrapper),
.register-form :deep(.el-input__wrapper) {
border-radius: 50px;
background-color: #f0f8ff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 2px solid #d1e8ff;
}
.login-form :deep(.el-input__wrapper:hover),
.register-form :deep(.el-input__wrapper:hover) {
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
border-color: #a1c4fd;
}
.login-form :deep(.el-input__wrapper.is-focus),
.register-form :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 6px 15px rgba(102, 126, 234, 0.3);
border-color: #667eea;
}
.login-form :deep(.el-select .el-input__wrapper),
.register-form :deep(.el-select .el-input__wrapper) {
background-color: #f0f8ff;
}
.login-button,
.register-button {
margin-top: 10px;
border-radius: 50px;
font-weight: 700;
letter-spacing: 1px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
box-shadow: 0 6px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
font-family: 'Comic Sans MS', cursive, sans-serif;
font-size: 16px;
}
.login-button:hover,
.register-button:hover {
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.6);
transform: translateY(-3px);
}
.option-content {
display: flex;
align-items: center;
gap: 8px;
}
.switch-form {
text-align: center;
margin-top: 20px;
color: #666;
font-family: 'Comic Sans MS', cursive, sans-serif;
}
.switch-form .el-button {
font-weight: 700;
font-size: 16px;
color: #667eea;
}
.switch-form .el-button:hover {
color: #764ba2;
}
.error-alert :deep(.el-alert__content),
.success-alert :deep(.el-alert__content) {
width: 100%;
}
@media (max-width: 480px) {
.login-card {
padding: 30px 20px;
}
.login-header h2 {
font-size: 24px;
}
.login-form :deep(.el-form-item),
.register-form :deep(.el-form-item) {
margin-bottom: 20px;
}
.avatar-face {
width: 80px;
height: 80px;
}
.cartoon-avatar {
width: 80px;
height: 80px;
}
.eyes {
padding-top: 25px;
}
}
</style>
前端Main.vue
html
<template>
<div class="message-board-container">
<!-- 第一部分:搜索留言 -->
<el-card class="search-card" shadow="hover">
<div class="card-header">
<el-icon>
<Search />
</el-icon>
<span class="header-title">搜索留言</span>
</div>
<el-form :model="searchParams" class="search-form">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="用户名">
<el-input v-model="searchParams.username" placeholder="请输入用户名" clearable size="small">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
class="date-picker"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="内容">
<el-input v-model="searchParams.content" placeholder="请输入搜索内容" clearable size="small">
<template #prefix>
<el-icon><Document /></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-buttons">
<el-button type="primary" size="small" @click="handleSearch" class="action-button">
<el-icon>
<Search />
</el-icon>搜索
</el-button>
<el-button size="small" @click="resetSearch" class="action-button">
<el-icon>
<Refresh />
</el-icon>重置
</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 第二部分:添加留言 -->
<el-card class="add-message-card" shadow="hover">
<div class="card-header">
<el-icon>
<Edit />
</el-icon>
<span class="header-title">添加留言</span>
</div>
<el-form :model="MessagesAddDTO" label-width="0px">
<el-form-item>
<el-input
type="textarea"
v-model="MessagesAddDTO.content"
:rows="4"
placeholder="分享你的想法..."
maxlength="500"
show-word-limit
class="message-textarea"
>
</el-input>
</el-form-item>
<div class="submit-section">
<el-button type="primary" @click="submitMessage" size="small" class="submit-button">
<el-icon>
<Position />
</el-icon>
发布留言
</el-button>
</div>
</el-form>
</el-card>
<!-- 第三部分:留言展示 -->
<div class="messages-section">
<div class="section-header">
<h3>
<el-icon>
<ChatDotRound />
</el-icon>
留言列表
</h3>
<div class="section-info">
共 {{ dataList.length }} 条留言
</div>
</div>
<transition-group name="message-list" tag="div" class="messages-container">
<el-card v-for="message in dataList" :key="message.id" class="message-card" shadow="hover">
<div class="message-header">
<div class="user-info">
<el-avatar :size="40" class="user-avatar">{{ message.username.charAt(0).toUpperCase() }}</el-avatar>
<div class="user-details">
<div class="username">{{ message.username }}</div>
<div class="time">{{ dayjs(message.createdAt).format('YYYY-MM-DD HH:mm') }}</div>
</div>
</div>
<div class="message-actions-header">
<el-tag size="mini" type="info" class="message-id">#{{ message.id }}</el-tag>
<!-- 管理员操作按钮 -->
<div v-if="isAdmin === '1'" class="admin-actions">
<el-button type="text" size="mini" @click="editMessage(message)" class="action-button-text">
<el-icon>
<Edit />
</el-icon>编辑
</el-button>
<el-button type="text" size="mini" @click="deleteDialogVisible = true; currentDeleteMessageId = message.id" class="action-button-text">
<el-icon>
<Delete />
</el-icon>删除
</el-button>
</div>
</div>
</div>
<!-- 编辑留言表单 -->
<div v-if="editingMessageId === message.id" class="edit-message-form">
<el-input
type="textarea"
:rows="3"
v-model="editForm.content"
maxlength="500"
show-word-limit
class="edit-textarea"
>
</el-input>
<div class="edit-form-actions">
<el-button type="primary" size="mini" @click="saveEditMessage" class="save-button">保存</el-button>
<el-button size="mini" @click="cancelEdit" class="cancel-button">取消</el-button>
</div>
</div>
<!-- 显示留言内容(非编辑状态) -->
<div v-else class="message-content">
{{ message.content }}
</div>
<div class="message-actions">
<el-button type="text" @click="forwardMessage(message)" :class="{ 'forwarded': message.isForwarded }" class="action-button-text">
<el-icon>
<Share />
</el-icon>
转发
<span class="count" v-if="message.forwardCount > 0">{{ message.forwardCount }}</span>
</el-button>
<el-button type="text" @click="toggleComments(message)" class="action-button-text comment-toggle">
<el-icon>
<ChatLineSquare />
</el-icon>
评论
<span class="count" v-if="message.comments && message.comments.length > 0">{{ message.comments.length }}</span>
</el-button>
</div>
<!-- 评论区域 -->
<div class="comments-section" v-show="message.showComments">
<div class="comments-list">
<transition-group name="comment-list" tag="div">
<div
v-for="(comment, index) in message.comments"
:key="comment.id"
class="comment-item"
:class="{ 'highlight': index === message.comments.length - 1 && message.highlightNewComment }"
>
<div class="comment-header">
<el-avatar :size="24" class="comment-avatar">{{ comment.username.charAt(0).toUpperCase() }}</el-avatar>
<div class="comment-user-info">
<span class="comment-user">{{ comment.username }}</span>
<span class="comment-time">{{ dayjs(comment.createdAt).format('MM-DD HH:mm') }}</span>
</div>
</div>
<div class="comment-content">
{{ comment.content }}
</div>
</div>
</transition-group>
<div v-if="!message.comments || message.comments.length === 0" class="no-comments">
<el-icon>
<ChatLineRound />
</el-icon>
<p>暂无评论,快来抢沙发吧!</p>
</div>
</div>
<!-- 添加评论 -->
<div class="add-comment">
<el-input
v-model="message.newComment"
placeholder="写下你的评论..."
class="comment-input"
size="small"
@keyup.enter="addComment(message)"
>
<template #append>
<el-button type="primary" size="small" @click="addComment(message)" class="comment-button">发表</el-button>
</template>
</el-input>
</div>
</div>
</el-card>
</transition-group>
<!-- 空状态 -->
<div v-if="dataList && dataList.length === 0" class="empty-state">
<el-empty description="暂无匹配的留言" :image-size="100">
<el-button type="primary" size="small" @click="resetSearch" class="reset-button">
<el-icon>
<Refresh />
</el-icon>重置搜索
</el-button>
</el-empty>
</div>
</div>
<!-- 删除确认对话框 -->
<el-dialog v-model="deleteDialogVisible" title="确认删除" width="400px" custom-class="custom-dialog">
<div class="dialog-content">
<div class="dialog-icon">
<el-icon class="warning-icon">
<Warning />
</el-icon>
</div>
<p class="dialog-text">确定要删除这条留言吗?此操作不可恢复。</p>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="deleteDialogVisible = false" class="cancel-dialog-button">取 消</el-button>
<el-button type="danger" @click="confirmDelete" :loading="deleteLoading" class="confirm-dialog-button">确 定</el-button>
</span>
</template>
</el-dialog>
<!-- 转发对话框 -->
<el-dialog title="转发留言" v-model="forwardDialogVisible" width="500px" class="forward-dialog" custom-class="custom-dialog">
<el-form>
<el-form-item label="选择用户">
<el-select
v-model="selectedUserIds"
multiple
filterable
placeholder="请选择要转发的用户"
style="width: 100%"
class="user-select"
>
<el-option
v-for="user in UserListDTO"
:key="user.id"
:label="user.username"
:value="user.id"
class="user-option"
>
<el-avatar :size="20" class="option-avatar">{{ user.username.charAt(0).toUpperCase() }}</el-avatar>
<span>{{ user.username }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="附加消息">
<el-input
v-model="forwardMessageText"
type="textarea"
:rows="3"
placeholder="请输入附加消息(可选)"
class="forward-textarea"
>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="forwardDialogVisible = false" class="cancel-dialog-button">取 消</el-button>
<el-button type="primary" @click="confirmForward" :loading="forwardLoading" class="confirm-dialog-button">确 定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {
getMessages, addMessages, getUserPerson, updateMessage, deleteMessage, getUsersList
} from '../api/user';
import { reactive, Ref, ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import dayjs from 'dayjs';
import { User, Document } from '@element-plus/icons-vue';
interface MessageQueryDTO {
username?: string;
content?: string;
}
interface CommentItem {
id: string;
content: string;
userId: string;
username: string;
createdAt: string;
}
interface DataItem {
id: string;
content: string;
userId: string;
parentId?: string;
username: string;
comments: CommentItem[];
isForwarded?: boolean;
forwardCount?: number;
showComments?: boolean;
newComment?: string;
highlightNewComment?: boolean;
createdAt: string;
orignalMessageId?: string;
}
interface MessagesAddDTO {
content: string;
userId: string;
}
interface UserInfo {
id: string;
username: string;
role: string;
}
interface UserItem {
id: string;
username: string;
}
interface MessageUpdateDTO {
id?: string;
content?: string;
}
const MessageUpdateDTO = reactive<MessageUpdateDTO>({
id: '',
content: ''
});
const MessagesAddDTO = reactive<MessagesAddDTO>({
content: '',
userId: '',
});
const searchParams = reactive<MessageQueryDTO>({
username: '',
content: ''
});
const dateRange = ref<[Date, Date] | undefined>(undefined);
const dataList: Ref<DataItem[]> = ref([]);
const UserLoginInfo: Ref<UserInfo | null> = ref(null);
const deleteDialogVisible = ref(false);
const deleteLoading = ref(false);
const currentDeleteMessageId = ref<string | null>(null);
const editingMessageId = ref<string | null>(null);
const editForm = reactive({ content: '' });
const isAdmin = ref();
// 转发相关数据
const forwardDialogVisible = ref(false);
const selectedUserIds = ref<string[]>([]);
const forwardMessageText = ref('');
const currentForwardMessage = ref<DataItem | null>(null);
const userList = ref<UserItem[]>([]);
const forwardLoading = ref(false);
// 获取数据
async function fetchData(params?: MessageQueryDTO & { startTime?: string, endTime?: string }) {
try {
const queryParams = params || { ...searchParams };
// 处理时间范围参数
if (dateRange.value && dateRange.value.length === 2) {
queryParams.startTime = dateRange.value[0] + '';
queryParams.endTime = dateRange.value[1] + '';
}
const res = await getMessages(queryParams);
if (!res || !res.data) {
ElMessage.error('接口响应格式异常');
return;
}
if (res.code === 0 && res.data) {
// 初始化评论和其他属性
dataList.value = res.data.map(item => ({
...item,
comments: item.comments ? (Array.isArray(item.comments) ? item.comments : []) : [],
showComments: false,
newComment: '',
isForwarded: item.isForwarded || false,
forwardCount: item.forwardCount || 0
})) || [];
} else {
ElMessage.error('获取数据失败');
}
} catch (error) {
ElMessage.error('请求失败,请检查网络');
console.error('请求错误:', error);
}
}
const handleSearch = () => {
fetchData();
};
const resetSearch = () => {
searchParams.username = '';
searchParams.content = '';
dateRange.value = undefined;
fetchData();
};
const addComment = async (message: DataItem) => {
if (!message.newComment?.trim()) {
ElMessage.warning('请输入评论内容');
return;
}
// 创建新评论对象
const newComment: CommentItem = {
id: Date.now().toString(),
content: message.newComment,
userId: UserLoginInfo.value?.id || '',
username: UserLoginInfo.value?.username || '匿名用户',
createdAt: new Date().toISOString()
};
if (!message.comments) {
message.comments = [];
}
message.comments.push(newComment);
message.newComment = '';
message.highlightNewComment = true;
ElMessage.success('评论发表成功!');
};
const submitMessage = async () => {
if (!MessagesAddDTO.content.trim()) {
ElMessage.warning('请输入留言内容');
return;
}
MessagesAddDTO.userId = UserLoginInfo.value?.id || '';
try {
const res = await addMessages(MessagesAddDTO);
if (res.code === 0) {
ElMessage.success('留言发布成功!');
MessagesAddDTO.content = '';
fetchData();
} else {
ElMessage.error('留言发布失败');
}
} catch (error) {
ElMessage.error('请求失败,请检查网络');
}
};
const forwardMessage = async (message: DataItem) => {
// 检查用户是否已登录
if (!UserLoginInfo.value) {
ElMessage.warning('请先登录后再进行转发操作');
return;
}
// 设置当前转发的消息
currentForwardMessage.value = message;
// 获取用户列表
await getUsersList();
// 显示转发对话框
forwardDialogVisible.value = true;
};
// 确认转发
const confirmForward = async () => {
if (!currentForwardMessage.value) {
ElMessage.error('转发消息异常');
return;
}
if (selectedUserIds.value.length === 0) {
ElMessage.warning('请选择要转发的用户');
return;
}
forwardLoading.value = true;
try {
// 模拟转发API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟API成功响应
const res = { code: 0, message: '转发成功' };
if (res.code === 0) {
ElMessage.success('留言转发成功!');
// 更新本地状态
if (currentForwardMessage.value) {
currentForwardMessage.value.isForwarded = true;
currentForwardMessage.value.forwardCount =
(currentForwardMessage.value.forwardCount || 0) + selectedUserIds.value.length;
}
// 关闭对话框并重置数据
forwardDialogVisible.value = false;
selectedUserIds.value = [];
forwardMessageText.value = '';
currentForwardMessage.value = null;
} else {
ElMessage.error(res.message || '转发失败');
}
} catch (error) {
ElMessage.error('转发请求失败,请检查网络');
console.error('转发错误:', error);
} finally {
forwardLoading.value = false;
}
};
const toggleComments = (message: DataItem) => {
message.showComments = !message.showComments;
if (message.showComments) {
message.highlightNewComment = false;
}
};
const getUserInfo = async () => {
try {
const res = await getUserPerson();
if (res.code === 0 && res.data) {
UserLoginInfo.value = res.data;
isAdmin.value = res.data.role;
}
} catch (error) {
ElMessage.error('获取用户信息失败');
}
};
// 页面加载时获取数据
onMounted(() => {
getUserInfo();
fetchData();
getUser();
});
// 编辑相关函数
const editMessage = (message: DataItem) => {
editingMessageId.value = message.id;
editForm.content = message.content;
};
const saveEditMessage = async () => {
MessageUpdateDTO.id = editingMessageId.value
MessageUpdateDTO.content = editForm.content;
const res = await updateMessage(MessageUpdateDTO);
if (res.code === 0) {
ElMessage.success('留言编辑成功!');
editingMessageId.value = null;
fetchData();
} else {
ElMessage.error('留言编辑失败');
}
};
const cancelEdit = () => {
editingMessageId.value = null;
};
const MessageDeleteDTO = ref({
id: ''
});
const confirmDelete = async () => {
if (!currentDeleteMessageId.value) {
ElMessage.error('删除消息异常');
return;
}
deleteLoading.value = true;
MessageDeleteDTO.value.id = currentDeleteMessageId.value;
try {
const res = await deleteMessage(MessageDeleteDTO.value);
if (res.code === 0) {
ElMessage.success('留言删除成功!');
// 关闭对话框
deleteDialogVisible.value = false;
// 重置当前删除消息ID
currentDeleteMessageId.value = null;
// 重新获取数据
fetchData();
}
} catch (error) {
console.error('删除错误:', error);
ElMessage.error('删除失败,请重试');
} finally {
deleteLoading.value = false;
}
};
interface UserListDTO {
id?: string;
username?: string;
}
const UserListDTO = ref<UserListDTO[]>([]);
const getUser = async () => {
const res = await getUsersList();
UserListDTO.value = res.data;
};
</script>
<style scoped>
.message-board-container {
max-width: 1000px;
margin: 0 auto;
padding: 30px;
background: linear-gradient(135deg, #ffd1dc 0%, #c9e9ff 50%, #ffd700 100%);
min-height: 100vh;
box-sizing: border-box;
font-family: 'Comic Sans MS', '幼圆', cursive, sans-serif;
}
.card-header {
display: flex;
align-items: center;
padding: 20px;
background: linear-gradient(90deg, #ffb6c1 0%, #87cefa 100%);
border-radius: 20px 20px 0 0;
border-bottom: 2px dashed #ff69b4;
}
.card-header .el-icon {
margin-right: 10px;
color: #ff4500;
font-size: 24px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.header-title {
font-weight: 700;
font-size: 20px;
color: #8b008b;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.search-card,
.add-message-card {
margin-bottom: 25px;
border-radius: 20px;
border: 3px solid #ff69b4;
box-shadow: 0 8px 20px rgba(255, 105, 180, 0.3);
transition: all 0.3s ease;
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
color: #8b008b;
overflow: hidden;
}
.search-card:hover,
.add-message-card:hover {
box-shadow: 0 12px 28px rgba(255, 105, 180, 0.5);
transform: translateY(-5px) rotate(1deg);
}
.search-form {
padding: 20px;
}
.search-buttons {
text-align: right;
padding-top: 10px;
}
.message-textarea ::v-deep .el-textarea__inner {
border-radius: 15px;
padding: 18px;
font-size: 16px;
border: 2px dashed #ff69b4;
transition: all 0.3s ease;
background: linear-gradient(135deg, #fff8dc 0%, #ffe4e1 100%);
color: #8b008b;
min-height: 120px;
font-family: 'Comic Sans MS', '幼圆', cursive, sans-serif;
}
.message-textarea ::v-deep .el-textarea__inner:focus {
border-color: #ff4500;
box-shadow: 0 0 0 3px rgba(255, 69, 0, 0.3);
}
.message-textarea ::v-deep .el-textarea__inner::placeholder {
color: #da70d6;
}
.submit-section {
text-align: right;
margin-top: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding: 20px;
background: linear-gradient(90deg, #ffb6c1 0%, #87cefa 100%);
border-radius: 20px;
box-shadow: 0 6px 16px rgba(255, 105, 180, 0.3);
border: 2px dashed #ff69b4;
}
.section-header h3 {
margin: 0;
color: #8b008b;
font-size: 22px;
display: flex;
align-items: center;
}
.section-header h3 .el-icon {
margin-right: 10px;
color: #ff4500;
font-size: 26px;
}
.section-info {
font-size: 16px;
color: #8b008b;
font-weight: bold;
}
.messages-container {
display: flex;
flex-direction: column;
gap: 25px;
}
.message-card {
border-radius: 20px;
border: 3px solid #ff69b4;
box-shadow: 0 6px 20px rgba(255, 105, 180, 0.3);
transition: all 0.3s ease;
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
color: #8b008b;
overflow: hidden;
}
.message-card:hover {
box-shadow: 0 10px 28px rgba(255, 105, 180, 0.5);
transform: translateY(-5px) rotate(-1deg);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px 20px 0 20px;
}
.user-info {
display: flex;
align-items: center;
}
.user-avatar {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
color: white;
font-weight: bold;
margin-right: 15px;
border: 2px solid white;
box-shadow: 0 0 8px rgba(255, 105, 180, 0.7);
}
.user-details {
display: flex;
flex-direction: column;
}
.username {
font-weight: 700;
color: #8b008b;
font-size: 18px;
margin-bottom: 4px;
}
.time {
font-size: 14px;
color: #da70d6;
}
.message-actions-header {
display: flex;
align-items: center;
gap: 15px;
}
.message-id {
background-color: #ffb6c1;
border-color: #ff69b4;
color: #8b008b;
border-radius: 20px;
}
.admin-actions {
display: flex;
gap: 10px;
}
.action-button-text {
padding: 0;
font-size: 16px;
color: #da70d6;
transition: all 0.3s ease;
font-weight: bold;
}
.action-button-text:hover {
color: #ff4500;
background-color: transparent;
transform: scale(1.1);
}
.action-button-text .el-icon {
margin-right: 5px;
}
.edit-message-form {
margin: 20px;
padding: 20px;
background: linear-gradient(135deg, #fff8dc 0%, #e0ffff 100%);
border-radius: 15px;
border: 2px dashed #ff69b4;
}
.edit-textarea ::v-deep .el-textarea__inner {
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
border: 2px dashed #ff69b4;
color: #8b008b;
border-radius: 12px;
font-family: 'Comic Sans MS', '幼圆', cursive, sans-serif;
}
.edit-form-actions {
margin-top: 20px;
text-align: right;
}
.save-button {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
border: none;
margin-right: 10px;
border-radius: 20px;
font-weight: bold;
color: white;
}
.cancel-button {
background-color: #ffb6c1;
border: 2px solid #ff69b4;
color: #8b008b;
border-radius: 20px;
font-weight: bold;
}
.message-content {
margin: 0 20px 20px;
line-height: 1.8;
font-size: 17px;
color: #8b008b;
white-space: pre-wrap;
padding: 20px;
background: linear-gradient(135deg, #fff8dc 0%, #e0ffff 100%);
border-radius: 15px;
border: 2px dashed #ff69b4;
}
.message-actions {
display: flex;
padding: 0 20px 20px;
border-top: 2px dashed #ff69b4;
padding-top: 20px;
gap: 25px;
}
.message-actions .forwarded {
color: #ff4500;
}
.count {
margin-left: 5px;
font-size: 14px;
background-color: #ffb6c1;
color: #8b008b;
padding: 4px 10px;
border-radius: 20px;
font-weight: 700;
}
.comments-section {
padding: 0 20px 20px;
}
.comments-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 20px;
padding: 15px;
background: linear-gradient(135deg, #fff8dc 0%, #e0ffff 100%);
border-radius: 15px;
border: 2px dashed #ff69b4;
}
.comment-item {
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
background-color: #ffb6c1;
box-shadow: 0 3px 8px rgba(255, 105, 180, 0.3);
transition: all 0.3s ease;
}
.comment-item:hover {
background-color: #ffaebc;
transform: scale(1.02);
}
.comment-item.highlight {
background-color: #ff69b4;
border-left: 5px solid #ff4500;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.comment-avatar {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
color: white;
margin-right: 10px;
border: 2px solid white;
box-shadow: 0 0 6px rgba(255, 105, 180, 0.7);
}
.comment-user-info {
display: flex;
flex-direction: column;
}
.comment-user {
font-weight: 700;
color: #8b008b;
font-size: 16px;
margin-bottom: 2px;
}
.comment-time {
font-size: 13px;
color: #da70d6;
}
.comment-content {
font-size: 16px;
color: #8b008b;
line-height: 1.6;
padding-left: 34px;
}
.no-comments {
text-align: center;
padding: 30px 20px;
color: #da70d6;
font-size: 17px;
display: flex;
flex-direction: column;
align-items: center;
}
.no-comments .el-icon {
font-size: 40px;
margin-bottom: 15px;
color: #ff69b4;
}
.no-comments p {
margin: 0;
}
.add-comment ::v-deep .el-input-group__append {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
border-color: #ff69b4;
color: white;
transition: all 0.3s ease;
border-radius: 0 10px 10px 0;
font-weight: bold;
}
.add-comment ::v-deep .el-input-group__append:hover {
background: linear-gradient(135deg, #ff4500 0%, #ff6347 100%);
transform: scale(1.05);
}
.add-comment ::v-deep .el-input__inner {
border-radius: 10px 0 0 10px;
font-size: 16px;
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
border: 2px dashed #ff69b4;
color: #8b008b;
height: 45px;
font-family: 'Comic Sans MS', '幼圆', cursive, sans-serif;
}
.add-comment ::v-deep .el-input__inner::placeholder {
color: #da70d6;
}
.comment-button {
height: 45px;
font-weight: bold;
}
/* 对话框样式 */
.custom-dialog {
border-radius: 25px;
overflow: hidden;
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
border: 3px solid #ff69b4;
box-shadow: 0 15px 40px rgba(255, 105, 180, 0.5);
}
.custom-dialog ::v-deep .el-dialog__header {
background: linear-gradient(90deg, #ffb6c1 0%, #87cefa 100%);
color: #8b008b;
border-bottom: 2px dashed #ff69b4;
padding: 20px;
border-radius: 25px 25px 0 0;
}
.custom-dialog ::v-deep .el-dialog__title {
color: #8b008b;
font-size: 20px;
font-weight: 700;
}
.custom-dialog ::v-deep .el-dialog__headerbtn .el-dialog__close {
color: #da70d6;
font-size: 22px;
}
.custom-dialog ::v-deep .el-dialog__headerbtn .el-dialog__close:hover {
color: #ff4500;
}
.custom-dialog ::v-deep .el-dialog__body {
padding: 25px;
}
.dialog-content {
text-align: center;
padding: 20px 0;
}
.dialog-icon {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.warning-icon {
font-size: 60px;
color: #ff4500;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.dialog-text {
font-size: 18px;
color: #8b008b;
margin: 0;
line-height: 1.6;
font-weight: bold;
}
.dialog-footer {
text-align: right;
padding: 20px;
background: linear-gradient(90deg, #ffb6c1 0%, #87cefa 100%);
border-top: 2px dashed #ff69b4;
}
.cancel-dialog-button {
background-color: #ffb6c1;
border: 2px solid #ff69b4;
color: #8b008b;
margin-right: 12px;
padding: 10px 22px;
border-radius: 20px;
font-weight: bold;
}
.confirm-dialog-button {
background: linear-gradient(135deg, #ff4500 0%, #ff6347 100%);
border: none;
padding: 10px 22px;
border-radius: 20px;
font-weight: bold;
color: white;
}
/* 转发对话框样式 */
.forward-dialog .el-select {
width: 100%;
}
.user-select ::v-deep .el-select__tags {
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
}
.user-select ::v-deep .el-select__tags-text {
color: #8b008b;
font-weight: bold;
}
.user-select ::v-deep .el-tag {
background-color: #ffb6c1;
border-color: #ff69b4;
color: #8b008b;
border-radius: 20px;
}
.user-option ::v-deep .el-select-dropdown__item.selected {
color: #ff4500;
font-weight: 700;
background-color: #fff0f5;
}
.option-avatar {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
color: white;
margin-right: 10px;
border: 2px solid white;
box-shadow: 0 0 6px rgba(255, 105, 180, 0.7);
}
.forward-textarea ::v-deep .el-textarea__inner {
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
border: 2px dashed #ff69b4;
color: #8b008b;
border-radius: 12px;
font-family: 'Comic Sans MS', '幼圆', cursive, sans-serif;
}
.forward-textarea ::v-deep .el-textarea__inner:focus {
border-color: #ff4500;
box-shadow: 0 0 0 3px rgba(255, 69, 0, 0.3);
}
/* 表单标签 */
::v-deep .el-form-item__label {
color: #8b008b;
font-weight: 700;
padding-bottom: 8px;
font-size: 16px;
}
/* 输入框 */
::v-deep .el-input__wrapper {
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
box-shadow: 0 0 0 2px #ff69b4 inset;
border-radius: 12px;
}
::v-deep .el-input__wrapper.is-focus {
box-shadow: 0 0 0 3px #ff4500 inset;
}
::v-deep .el-input__inner {
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
color: #8b008b;
height: 45px;
font-family: 'Comic Sans MS', '幼圆', cursive, sans-serif;
font-weight: bold;
}
::v-deep .el-input__inner::placeholder {
color: #da70d6;
}
.date-picker ::v-deep .el-range-input {
background-color: transparent;
color: #8b008b;
font-weight: bold;
}
.date-picker ::v-deep .el-range-separator {
color: #da70d6;
font-weight: bold;
}
/* 按钮 */
.action-button {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
border: none;
padding: 12px 22px;
font-weight: 700;
margin-left: 10px;
transition: all 0.3s ease;
border-radius: 20px;
color: white;
font-size: 16px;
}
.action-button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 6px 15px rgba(255, 105, 180, 0.5);
}
.submit-button {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
border: none;
padding: 14px 28px;
font-weight: 700;
font-size: 17px;
transition: all 0.3s ease;
border-radius: 25px;
color: white;
}
.submit-button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 6px 18px rgba(255, 105, 180, 0.5);
}
.reset-button {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
border: none;
padding: 12px 22px;
font-weight: 700;
transition: all 0.3s ease;
border-radius: 20px;
color: white;
}
.reset-button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 6px 15px rgba(255, 105, 180, 0.5);
}
::v-deep .el-button {
background: linear-gradient(135deg, #ffb6c1 0%, #87cefa 100%);
border: 2px solid #ff69b4;
color: #8b008b;
border-radius: 12px;
transition: all 0.3s ease;
font-weight: bold;
}
::v-deep .el-button:hover {
background: linear-gradient(135deg, #ffaebc 0%, #7ac5cd 100%);
border-color: #ff4500;
color: #8b008b;
transform: translateY(-2px);
}
::v-deep .el-button--primary {
background: linear-gradient(135deg, #ff69b4 0%, #ff4500 100%);
border: none;
font-weight: 700;
color: white;
}
::v-deep .el-button--primary:hover {
background: linear-gradient(135deg, #ff4500 0%, #ff6347 100%);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 15px rgba(255, 105, 180, 0.5);
}
::v-deep .el-button--danger {
background: linear-gradient(135deg, #ff4500 0%, #ff6347 100%);
border: none;
color: white;
}
::v-deep .el-button--danger:hover {
background: linear-gradient(135deg, #ff6347 0%, #ff4500 100%);
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 15px rgba(255, 69, 0, 0.5);
}
/* 空状态 */
.empty-state ::v-deep .el-empty {
background: linear-gradient(135deg, #fffafa 0%, #f0ffff 100%);
padding: 40px 0;
}
.empty-state ::v-deep .el-empty__description {
color: #da70d6;
font-size: 18px;
margin-top: 20px;
font-weight: bold;
}
/* 标签 */
::v-deep .el-tag {
background-color: #ffb6c1;
border-color: #ff69b4;
color: #8b008b;
border-radius: 20px;
padding: 6px 14px;
font-weight: bold;
}
/* 动画效果 */
.message-list-enter-active {
transition: all 0.5s ease;
}
.message-list-leave-active {
transition: all 0.5s ease;
}
.message-list-enter-from {
opacity: 0;
transform: translateY(30px) rotate(5deg);
}
.message-list-leave-to {
opacity: 0;
transform: translateX(50px) rotate(-5deg);
}
.comment-list-enter-active {
transition: all 0.4s ease;
}
.comment-list-leave-active {
transition: all 0.3s ease;
}
.comment-list-enter-from {
opacity: 0;
transform: translateX(20px) scale(0.9);
}
.comment-list-leave-to {
opacity: 0;
transform: translateX(-20px) scale(0.9);
}
/* 响应式设计 */
@media (max-width: 768px) {
.message-board-container {
padding: 15px;
}
.card-header {
padding: 15px;
}
.search-form {
padding: 15px;
}
.el-col {
margin-bottom: 15px;
}
.search-buttons {
text-align: center;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.message-actions {
flex-wrap: wrap;
gap: 15px;
}
.message-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.message-actions-header {
margin-top: 0;
align-self: flex-start;
width: 100%;
justify-content: space-between;
}
.admin-actions {
gap: 15px;
}
.message-content {
padding: 15px;
}
.message-header,
.message-content,
.message-actions,
.comments-section {
padding-left: 15px;
padding-right: 15px;
}
.edit-message-form,
.comments-list {
padding: 12px;
}
.custom-dialog {
width: 90% !important;
}
}
@media (max-width: 480px) {
.message-board-container {
padding: 10px;
}
.card-header .el-icon,
.section-header h3 .el-icon {
font-size: 20px;
}
.header-title,
.section-header h3 {
font-size: 18px;
}
.message-textarea ::v-deep .el-textarea__inner {
padding: 12px;
font-size: 15px;
}
.user-details {
gap: 2px;
}
.username {
font-size: 16px;
}
.message-content {
font-size: 16px;
}
.comment-user {
font-size: 14px;
}
.comment-content {
font-size: 15px;
}
}
</style>
第四步 运行效果如图所示
案例17