feat: 新增疯言疯语功能并优化UI样式

- 添加疯言疯语服务及页面,支持随机字符颜色变化效果
- 引入汉仪唐韵字体并优化全局字体设置
- 重构日期工具函数,优化时间显示格式
- 改进左侧模块布局,添加文章/分类/标签统计
- 优化浮动按钮组件,增加动态过渡效果
- 调整多个页面的背景透明度,提升视觉一致性
- 完善文章保存页面样式和交互逻辑
- 更新关于页面内容,增加个人介绍和技术栈展示
- 修复路由状态管理问题,优化页面跳转逻辑
This commit is contained in:
qingfeng1121
2025-11-05 16:11:46 +08:00
parent a927ad5a4d
commit ad893b3e5c
19 changed files with 1226 additions and 600 deletions

BIN
public/fonts/HanTang.woff2 Normal file

Binary file not shown.

View File

@@ -34,13 +34,35 @@
</div>
</div>
<div id="bot" :class="{ 'botrelative': scrollY }">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tabs v-model="activeName" stretch="true" class="demo-tabs">
<el-tab-pane label="个人简介" name="first">
<div class="mylogo">
<el-avatar :src="state.circleUrl" />
<el-avatar class="mylogo_avatar" :src="state.circleUrl" />
</div>
<a href="#">
<h6 class="mylogo_name logo-text">清疯不颠</h6>
</a>
<h6 class="mylogo_description">重度精神失常患者</h6>
<div class="stat-container">
<div>
<a href="#" class="stat-link">
<span class="site-state-item-count">{{ articleCount }}</span>
<span class="site-state-item-name">文章</span>
</a>
</div>
<div>
<a href="#" class="stat-link">
<span class="site-state-item-count">{{ categoryCount }}</span>
<span class="site-state-item-name">分类</span>
</a>
</div>
<div>
<a href="#" class="stat-link">
<span class="site-state-item-count">{{ AttributeCount }}</span>
<span class="site-state-item-name">标签</span>
</a>
</div>
</div>
<p>清疯不颠</p>
<p>重度精神失常患者</p>
</el-tab-pane>
<el-tab-pane label="功能" name="second">
</el-tab-pane>
@@ -52,6 +74,7 @@
<script lang="ts" setup>
import { reactive, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { articleService, categoryService, categoryAttributeService } from "@/services";
// 当前激活菜单
const activeIndex = ref('/:type')
@@ -74,6 +97,52 @@ router.beforeEach((to) => {
activeIndex.value = to.path
})
// 文章数量状态
const articleCount = ref(0)
// 分类数量状态
const categoryCount = ref(0)
// 标签数量状态
const AttributeCount = ref(0)
// 获取文章数量
const fetchArticleCount = async () => {
try {
const response = await articleService.getAllArticles();
articleCount.value = response.data?.length || 0
// 这里应该调用API获取实际的文章数量
// 暂时设置为模拟数据
} catch (error) {
console.error('获取文章数量失败:', error)
articleCount.value = 0
}
}
// 获取分类数量
const fetchCategoryCount = async () => {
try {
const response = await categoryService.getAllCategories();
categoryCount.value = response.data?.length || 0
// 这里应该调用API获取实际的分类数量
// 暂时设置为模拟数据
} catch (error) {
console.error('获取分类数量失败:', error)
categoryCount.value = 0
}
}
// 获取标签数量
const fetchAttributeCount = async () => {
try {
const response = await categoryAttributeService.getAllAttributes();
AttributeCount.value = response.data?.length || 0
// 这里应该调用API获取实际的标签数量
// 暂时设置为模拟数据
} catch (error) {
console.error('获取标签数量失败:', error)
AttributeCount.value = 0
}
}
// 控制底部模块吸顶效果
const scrollY = ref(false)
const handleScroll = () => {
@@ -83,6 +152,9 @@ const handleScroll = () => {
// 生命周期管理事件监听,防止内存泄漏
onMounted(() => {
window.addEventListener('scroll', handleScroll)
fetchArticleCount() // 组件挂载时获取文章数量
fetchCategoryCount() // 组件挂载时获取分类数量
fetchAttributeCount() // 组件挂载时获取标签数量
})
onUnmounted(() => {
@@ -100,17 +172,18 @@ onUnmounted(() => {
#top {
height: 100px;
border-radius: 10px;
background-color: rgba(102, 161, 216, 0.9); /* 蓝色半透明背景 */
background-color: rgba(102, 161, 216, 0.9);
/* 蓝色半透明背景 */
}
#alld #top .top1 {
#alld #top .top1 {
padding-top: 20px;
margin-bottom: 0;
text-align: center;
color: white;
}
#alld #top .top2 {
#alld #top .top2 {
text-align: center;
color: white;
}
@@ -118,13 +191,15 @@ onUnmounted(() => {
/* 内容区域样式 */
#cont {
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9); /* 白色半透明背景 */
background-color: rgba(255, 255, 255, 0.9);
/* 白色半透明背景 */
}
.cont1 {
text-align: center;
padding: 25px 10px 25px 10px ;
background-color: rgba(102, 161, 216, 0.9); /* 蓝色半透明背景 */
padding: 25px 10px 25px 10px;
background-color: rgba(102, 161, 216, 0.9);
/* 蓝色半透明背景 */
border-radius: 10px 10px 0 0;
}
@@ -141,21 +216,27 @@ onUnmounted(() => {
.cont2 {
margin-top: 20px;
}
.cont2 .el-menu-vertical-demo{
.cont2 .el-menu-vertical-demo {
display: block;
background-color: rgba(0, 0, 0,0 ); /* 白色半透明背景 */
}
.cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3){
border-radius: 0 0 10px 10px;
}
.cont2 .el-menu-vertical-demo .el-menu-item:hover{
background-color: rgba(64, 158, 255, 0.9);
}
.cont2 .el-menu-vertical-demo .el-menu-item.is-active:hover{
color: black; /* 蓝色半透明背景 */
background-color: rgba(0, 0, 0, 0);
/* 白色半透明背景 */
}
.cont2 .el-menu-vertical-demo .el-menu-item.is-active{
.cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3) {
border-radius: 0 0 10px 10px;
}
.cont2 .el-menu-vertical-demo .el-menu-item:hover {
background-color: rgba(64, 158, 255, 0.9);
}
.cont2 .el-menu-vertical-demo .el-menu-item.is-active:hover {
color: black;
/* 蓝色半透明背景 */
}
.cont2 .el-menu-vertical-demo .el-menu-item.is-active {
color: var(--nav-is-active);
}
@@ -212,22 +293,68 @@ onUnmounted(() => {
/* 底部标签页样式 */
#bot {
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9); /* 白色半透明背景 */
padding: 15px;
background-color: rgba(255, 255, 255, 0.9);
/* 白色半透明背景 */
padding: 10px;
}
.el-tabs__nav-scroll .el-tabs__nav .el-tabs__item{
.site-state-item-count {
display: block;
text-align: center;
color: #32325d;
font-weight: bold;
}
.demo-tabs {
transition: all 0.3s ease-in-out;
}
.el-tabs__nav-scroll .el-tabs__nav .el-tabs__item {
margin-left: 100px;
padding: 10px 20px;
}
/* 头像样式 */
.mylogo {
text-align: center;
margin-bottom: 10px;
.mylogo_name {
font-size: 13px;
}
.mylogo_description {
font-size: 13px;
opacity: 0.8;
color: #c21f30;
margin: 10px 0;
}
.el-avatar {
width: 80px;
height: 80px;
.stat-container {
display: flex;
gap: 10px;
justify-content: center;
font-size: 10px;
}
.stat-container div:not(:first-child) {
border-left: 1px solid #ccc;
padding-left: 10px;
}
/* 头像样式 */
#pane-first .mylogo {
text-align: center;
padding: 6px;
margin-bottom: 0px;
}
.mylogo_avatar {
height: 60px;
width: 60px;
}
/* 直接应用到el-avatar组件上的悬浮效果 */
.el-avatar.mylogo_avatar {
transition: transform 0.3s ease;
}
.el-avatar.mylogo_avatar:hover {
transform: scale(1.2);
}
/* 吸顶效果 */
@@ -243,6 +370,7 @@ onUnmounted(() => {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);

View File

@@ -62,11 +62,11 @@
<!-- 左侧模块 -->
<div class="leftmodluecontainer" v-if="isleftmodluecontainer">
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classnonsenset }" v-if="windowwidth" />
<LeftModule class="leftmodluepage" :class="{ 'nonsensetmargintop': classmoduleorrouter}" v-if="windowwidth" />
</div>
<!-- 内容模块 -->
<RouterView class="RouterViewpage" :class="{'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classnonsenset }" />
<RouterView class="RouterViewpage" :class="{'forbidwidth': !isleftmodluecontainer, 'nonsensetmargintop': classmoduleorrouter }" />
</div>
<!-- 分页区域 -->
@@ -86,18 +86,33 @@ const router = useRouter();
const route = useRoute();
// 全局状态管理
import { useGlobalStore } from '@/store/globalStore'
import { Card } from 'ant-design-vue';
const globalStore = useGlobalStore()
const Login = computed(() => globalStore.Login)
// 响应式状态
// 文章标题,用于显示在页面上的标题内容
const Cardtitle = ref('');
// 控制模块或路由相关的CSS类名
const classmoduleorrouter = ref(false);
// 控制左侧模块容器是否显示
const isleftmodluecontainer = ref(true);
// 控制hero区域的CSS类名
const classhero = ref(false);
// 控制内容区域是否可见
const isconts = ref(false);
// 控制左侧模块是否处于滚动状态
const isScrollingleftmodlue = ref(false);
// 顶部导航栏的样式状态transparent/solid/hide
const elrowtop = ref('transparent');
// 控制疯言疯语页面标题区域的显示
const classnonsenset = ref(false);
// 判断窗口是否为宽屏大于768px
const windowwidth = ref(true);
const activeIndex = ref('home');
const localhome= 'home';
@@ -194,9 +209,12 @@ const performSearch = () => {
/**
* 根据路由路径设置页面状态
*/
const updatePageState = (url: string) => {
classhero.value = url !== localhome;
classnonsenset.value = url === 'nonsense';
const updatePageState = () => {
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
classhero.value = false;
} else {
classhero.value = true;
}
};
/**
@@ -244,45 +262,63 @@ const handleScroll = () => {
} else {
elrowtop.value = window.scrollY > 100 ? 'solid' : 'transparent';
}
// 首页内容区滚动动画
if (rpsliturl[1] === localhome) {
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
isconts.value = window.scrollY > 200;
isScrollingleftmodlue.value = window.scrollY > 600;
}
};
/**
* 更新文章标题和相关状态
*/
const updateArticleTitle = () => {
let articledata: any = null;
// 优先使用attributeId参数新接口
if (rpsliturl[2] === 'aericletype') {
articledata = globalStore.getValue('attribute')?.name;
}
// 搜索标题
else if (rpsliturl[2] === 'aericletitle') {
articledata = globalStore.getValue('title')?.name;
}
// 疯言疯语页面
else if (rpsliturl[1] === 'nonsense') {
articledata = "疯言疯语";
}
// 特殊页面不需要显示标题
if (rpsliturl[1] === 'article-list' || rpsliturl[1] === 'message' || rpsliturl[1] === 'about') {
classnonsenset.value = false;
classmoduleorrouter.value = false;
}
// 在主页且articledata为空时不显示标题
else if (rpsliturl[1] === localhome && !articledata) {
classnonsenset.value = false;
classmoduleorrouter.value = false;
}
// 显示文章标题
else if (articledata) {
Cardtitle.value = articledata;
classnonsenset.value = true;
classmoduleorrouter.value = true;
}
};
/**
* 监听路由变化
*/
watch(() => route.path, () => {
rpsliturl = route.path.split('/');
updatePageState(rpsliturl[1]);
updatePageState();
setActiveIndex(rpsliturl[1]);
console.log(rpsliturl[1])
let articledata;
// 优先使用attributeId参数新接口
if (rpsliturl[2]==='aericletype') {
articledata = globalStore.getValue('attribute')
}
// 搜索标题
if (rpsliturl[2]==='aericletitle') {
articledata = globalStore.getValue('title')
}
if (rpsliturl[1]==='nonsense') {
articledata = "疯言疯语"
}
// hero 标题
if (articledata) {
Cardtitle.value = articledata.name
classhero.value = true;
}
updateArticleTitle();
// 跳转后回到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
// 首页内容区滚动动画仅大屏下生效
if (rpsliturl[1] === localhome && rpsliturl[2] == '') {
// isconts.value = window.innerWidth <= 768 ? true : false;
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
// 首页时启动打字机效果
startTypewriter();
} else {

View File

@@ -2,7 +2,11 @@
<div class="establish-container">
<!-- 弹出的按钮容器 -->
<!-- <div class="expanded-buttons" :class="{ 'show': isExpanded }"> -->
<button v-for="(btn) in isbuttonsave()" :class="{ 'show': isExpanded }" :key="btn.id" class="action-button"
<button
v-for="(btn, index) in isbuttonsave()"
:class="['action-button', { 'show': isExpanded }]"
:style="getButtonStyle(index)"
:key="btn.id"
@click="handleButtonClick(btn)">
<!-- <i :class="btn.icon"></i> -->
<span>{{ btn.label }}</span>
@@ -18,34 +22,82 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router';
import { articleService, messageService, categoryAttributeService ,loginService} from '@/services'
import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { articleService, messageService, categoryAttributeService, nonsenseService, categoryService, loginService } from '@/services'
// 全局状态管理
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 路由参数
const router = useRouter()
const route = useRoute()
// 定义响应式状态
const isExpanded = ref(false)
const buttons = [
{ id: 1, label: '新建文章', icon: 'icon-add-article' },
{ id: 2, label: '新建分类', icon: 'icon-create-category' },
{ id: 3, label: '疯言疯语', icon: 'icon-upload-file' },
{ id: 4, label: '新建标签', icon: 'icon-new-tag' },
{ id: 5, label: '登出', icon: 'icon-logout' }
]
const buttonsave = [
{ id: 1, label: '修改文章', icon: 'icon-add-article' },
{ id: 2, label: '删除删除', icon: 'icon-create-category' },
// 疯言疯语模态框状态
const isNonsenseModalVisible = ref(false)
const nonsenseContent = ref('')
// 基础按钮配置
const baseButtons = [
{ id: 'logout', label: '登出', icon: 'icon-logout' }
]
// 页面特定按钮配置
const pageButtons = {
// 文章详情页按钮
article: [
{ id: 'edit-article', label: '修改', icon: 'icon-add-article' },
{ id: 'delete-article', label: '删除', icon: 'icon-create-category' },
...baseButtons
],
// 首页按钮
home: [
{ id: 'create-article', label: '新建', icon: 'icon-add-article' },
{ id: 'published-articles', label: '已发表', icon: 'icon-new-tag' },
{ id: 'unpublished-articles', label: '未发表', icon: 'icon-new-tag' },
...baseButtons
],
// 疯言疯语页面按钮
nonsense: [
{ id: 'create-nonsense', label: '说说', icon: 'icon-upload-file' },
...baseButtons
],
// 分类页面按钮
category: [
{ id: 'create-category', label: '新建', icon: 'icon-create-category' },
...baseButtons
],
// 标签页面按钮
tag: [
{ id: 'create-tag', label: '新建', icon: 'icon-new-tag' },
...baseButtons
],
// 文章保存页面按钮
articlesave: [
{ id: 'view-articles', label: '查看文章列表', icon: 'icon-new-tag' },
...baseButtons
],
// 默认按钮
default: baseButtons
}
// 根据当前页面返回对应的按钮配置
const isbuttonsave = () => {
if (globalStore.getValue('localpath').name == 'article') {
return buttonsave
try {
// 获取当前页面路径名称
const currentPath = globalStore.getValue('localpath')?.name || 'default';
// 返回对应页面的按钮配置,如果没有则返回默认配置
return pageButtons[currentPath] || pageButtons.default;
} catch (error) {
console.error('获取页面按钮配置失败:', error);
return pageButtons.default;
}
return buttons
}
// 切换按钮显示状态
const toggleExpand = (event) => {
@@ -55,54 +107,86 @@ const toggleExpand = (event) => {
}
// 处理按钮点击事件
const handleButtonClick = (button) => {
console.log('点击了按钮:', button.label)
// 可以在这里添加具体的按钮点击逻辑
if (button.label == '新建文章') {
router.push({
path: '/articlesave',
})
// 显示疯言疯语模态框
const showNonsenseModal = () => {
nonsenseContent.value = '' // 清空输入框
isNonsenseModalVisible.value = true
ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', {
confirmButtonText: '保存',
cancelButtonText: '取消',
inputPlaceholder: '在这里输入您想说的话...',
inputType: 'textarea',
inputRows: 4,
showCancelButton: true
}).then(({ value }) => {
// 保存疯言疯语
saveNonsense(value)
}).catch(() => {
// 取消操作,静默处理
})
}
// 保存疯言疯语
const saveNonsense = (content) => {
if (!content || content.trim() === '') {
ElMessage.warning('内容不能为空')
return
}
if (button.label == '新建分类') {
globalStore.setValue('localpath', { name: 'category', id: 0 })
// 调用服务保存疯言疯语
nonsenseService.saveNonsense({
content: content.trim(),
time: new Date()
}).then(response => {
if (response.code === 200) {
ElMessage.success('疯言疯语发布成功')
} else {
ElMessage.error(response.message || '发布失败')
}
}).catch(err => handleErrorResponse(err, '发布失败'))
}
// 处理错误响应的工具函数
const handleErrorResponse = (error, defaultMessage = '操作失败') => {
console.error('操作失败:', error)
ElMessage.error(error.message || defaultMessage)
}
// 删除文章方法
const deleteArticle = () => {
if (!route.query.id) {
ElMessage.warning('缺少文章ID参数')
return
}
if (button.label == '疯言疯语') {
globalStore.setValue('localpath', { name: 'message', id: 0 })
}
if (button.label == '新建标签') {
globalStore.setValue('localpath', { name: 'tag', id: 0 })
}
if (button.label == '修改文章') {
globalStore.setValue('localpath', { name: 'article', id: Number(route.query.id) })
}
if (button.label == '删除删除') {
// 确认删除
ElMessageBox.confirm('确定删除该文章吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 调用删除文章接口
articleService.deleteArticle(Number(route.query.id)).then(response => {
if (response.code == 200) {
// 确认删除
ElMessageBox.confirm('确定删除该文章吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 调用删除文章接口
articleService.deleteArticle(Number(route.query.id))
.then(response => {
if (response.code === 200) {
ElMessage.success('文章删除成功')
// 跳转回文章列表页
router.push({ path: '/home' })
} else {
ElMessage.error(response.message || '删除失败')
}
}).catch(err => {
ElMessage.error(err.message || '删除失败')
})
}).catch(() => {
// 取消删除
})
}
if (button.label == '登出') {
// 调用登出接口
loginService.logout().then(response => {
if (response.code == 200) {
.catch(err => handleErrorResponse(err, '删除失败'))
}).catch(() => {
// 取消删除,静默处理
})
}
// 登出方法
const logout = () => {
// 调用登出接口
loginService.logout()
.then(response => {
if (response.code === 200) {
ElMessage.success('登出成功')
// 清空全局状态
globalStore.clearAll()
@@ -113,13 +197,98 @@ const handleButtonClick = (button) => {
} else {
ElMessage.error(response.message || '登出失败')
}
}).catch(err => {
ElMessage.error(err.message || '登出失败')
})
}
isExpanded.value = false // 点击后收起
.catch(err => handleErrorResponse(err, '登出失败'))
}
// 处理按钮点击事件
const handleButtonClick = (button) => {
console.log('点击了按钮:', button.id, button.label)
// 使用按钮ID进行处理更可靠且易于维护
switch (button.id) {
// 新增操作
case 'create-article':
// router.push({ path: '/articlesave' })
break
case 'create-category':
// router.push({ path: '/categorysave' })
break
case 'create-tag':
// router.push({ path: '/tagsave' })
break
case 'create-nonsense':
// 显示疯言疯语模态框
showNonsenseModal()
break
// 修改操作
case 'edit-article':
if (route.query.id) {
globalStore.setValue('localpath', { name: 'article', id: Number(route.query.id) })
router.push({ path: '/articlesave', query: { id: route.query.id } })
} else {
ElMessage.warning('缺少文章ID参数')
}
break
// 删除操作
case 'delete-article':
deleteArticle();
break
break
// 查看操作
case 'published-articles':
router.push({ path: '/home', query: { status: 1 } })
break
case 'unpublished-articles':
router.push({ path: '/home', query: { status: 0 } })
break
case 'view-articles':
router.push({ path: '/home' })
break
// 登出操作
case 'logout':
logout();
break
default:
console.warn('未处理的按钮类型:', button.id, button.label)
ElMessage.info(`功能 ${button.label} 暂未实现`)
}
// 点击后收起按钮菜单
isExpanded.value = false
}
// 为每个按钮计算动态样式
const getButtonStyle = (index) => {
// 计算过渡时间,确保显示时从上到下逐渐出现,隐藏时从下到上逐渐消失
// 显示时间0.9s - index * 0.2s (递减)
// 隐藏时间0.3s + index * 0.2s (递增)
const showDelay = Math.max(0.1, 0.2 + index * 0.2);
const hideDelay = 0.3 + index * 0.2;
// 根据是否展开返回不同的过渡样式
if (isExpanded.value) {
return {
transition: `all ${showDelay}s ease-in-out`
};
} else {
return {
transition: `all ${hideDelay}s ease-in-out`
};
}
};
// 点击外部区域收起按钮的处理函数
const handleClickOutside = (e) => {
const mainButton = document.querySelector('.main-button')
@@ -150,11 +319,11 @@ onBeforeUnmount(() => {
<style scoped>
.establish-container {
width: 150px;
width: 230px;
position: fixed;
z-index: 1000;
bottom: 50px;
right: -10px;
right: -80px;
}
/* 主圆形按钮样式 */
@@ -211,7 +380,6 @@ onBeforeUnmount(() => {
margin-bottom: 10px;
opacity: 0;
/* 从右侧滑入 */
min-width: 120px;
}
.action-button:hover span {
@@ -230,63 +398,14 @@ onBeforeUnmount(() => {
font-size: 14px;
}
/* 确保按钮按顺序显示,形成卷帘效果 */
.action-button.show:nth-child(1) {
transition: all 0.9s ease-in-out;
margin-left: 0;
/* 按钮展开状态样式 */
.action-button.show {
margin-left: 70px;
opacity: 1;
}
.action-button.show:nth-child(2) {
transition: all 0.7s ease-in-out;
margin-left: 0;
opacity: 1;
}
.action-button.show:nth-child(3) {
transition: all 0.5s ease-in-out;
margin-left: 0;
opacity: 1;
}
.action-button.show:nth-child(4) {
transition: all 0.3s ease-in-out;
margin-left: 0;
opacity: 1;
}
.action-button.show:nth-child(5) {
transition: all 0.2s ease-in-out;
margin-left: 0;
opacity: 1;
}
/* 确保按钮按顺序关闭,形成卷帘效果 */
.action-button:nth-child(1) {
transition: all 0.3s ease-in-out;
margin-left: 150px;
opacity: 1;
}
.action-button:nth-child(2) {
transition: all 0.5s ease-in-out;
margin-left: 150px;
opacity: 1;
}
.action-button:nth-child(3) {
transition: all 0.7s ease-in-out;
margin-left: 150px;
opacity: 1;
}
.action-button:nth-child(4) {
transition: all 0.9s ease-in-out;
margin-left: 150px;
opacity: 1;
}
.action-button:nth-child(5) {
transition: all 1.1s ease-in-out;
/* 按钮默认状态样式 */
.action-button {
margin-left: 150px;
opacity: 1;
}

View File

@@ -5,17 +5,22 @@ import { ElMessage } from 'element-plus'
// 创建axios实例
const api = axios.create({
baseURL:'http://localhost:8080/api', // API基础URL
timeout: 10000 // 请求超时时间
timeout: 10000, // 请求超时时间
withCredentials: true // 允许跨域请求携带凭证如cookies
})
// 请求拦截器
api.interceptors.request.use(
config => {
// 从localStorage获取token
const token = localStorage.getItem('token')
let token = localStorage.getItem('token')
if (token) {
// 设置Authorization头
config.headers['Authorization'] = `Bearer ${token}`
// 确保不重复添加Bearer前缀
if (!token.startsWith('Bearer ')) {
config.headers['Authorization'] = `Bearer ${token}`
} else {
config.headers['Authorization'] = token
}
}
return config
},

View File

@@ -5,6 +5,14 @@ import api from './apiService'
* 分类属性服务类
*/
class CategoryAttributeService {
/**
* 获取所有分类属性
* @returns {Promise<import('../types').ApiResponse<import('../types').CategoryAttribute[]>>}
*/
getAllAttributes() {
return api.get('/category-attributes')
}
/**
* 根据ID获取分类属性
* @param {number} attributeid - 属性ID

View File

@@ -4,6 +4,8 @@ import categoryService from './categoryService'
import categoryAttributeService from './categoryAttributeService'
import loginService from './loginService'
import messageService from './messageService'
import nonsenseService from './nonsenseService'
// 导出服务类供特殊场景使用
@@ -12,5 +14,6 @@ export {
categoryService,
categoryAttributeService,
loginService,
messageService
messageService,
nonsenseService
}

View File

@@ -0,0 +1,37 @@
// 留言相关API服务
import apiService from './apiService'
class NonsenseService {
/**
* 获取所有随机内容
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
*/
getAllNonsense() {
return apiService.get('/nonsense')
}
/**
* 保存随机内容
* @param {import('../types').Nonsense} nonsense - 随机内容对象
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
*/
saveNonsense(nonsense){
return apiService.post('/nonsense', nonsense)
}
/**
* 删除随机内容
* @param {number} id - 随机内容ID
* @returns {Promise<import('../types').ApiResponse<boolean>>}
*/
deleteNonsense(id){
return apiService.delete(`/nonsense/${id}`)
}
/**
* 更新随机内容
* @param {import('../types').Nonsense} nonsense - 随机内容对象
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
*/
updateNonsense(nonsense){
return apiService.put('/nonsense', nonsense)
}
}
export default new NonsenseService()

View File

@@ -1,4 +1,15 @@
/* 汉仪唐韵字体声明 */
@font-face {
font-family: 'HanTang';
src: url('/fonts/HanTang.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root {
/* 全局字体设置 */
--main-font-family: 'HanTang', sans-serif;
/* 页面通用间距和圆角 */
--main-padding: 8px 10%;
/* 内容区内边距 */
@@ -75,6 +86,8 @@
/* 页面背景设置 */
body {
font-family: var(--main-font-family);
/* 设置全局字体 */
background-image: var(--body-background-img);
/* 背景图片 */
background-size: 120% 120%;
@@ -86,7 +99,17 @@ body {
background-attachment: fixed;
/* 背景固定 */
}
/* a 标签样式 */
a {
font-family: var(--main-font-family);
text-decoration: none;
color: inherit;
cursor: default;
display: inline-block;
}
p, li ,ul,ol,dl,dt,dd,span,strong,em,code,pre,input,textarea,select,option,label,th,td,tr,tbody,td,th,div{
font-family: var(--main-font-family);
}
/* 标题通用样式 */
h1,
h2,
@@ -94,7 +117,7 @@ h3,
h4,
h5,
h6 {
font-family: inherit;
font-family: var(--main-font-family);
font-weight: 400;
line-height: 1.5;
color: var(--font-color-title);
@@ -128,7 +151,7 @@ p {
align-items: center;
width: 100%;
height: 100%;
font-family: 'Microsoft YaHei', 'Ma Shan Zheng', cursive;
font-family: 'HanTang', sans-serif;
font-size: 1rem;
font-weight: bold;
background: linear-gradient(94.75deg, rgb(60, 172, 247) 0%, rgb(131, 101, 253) 43.66%, rgb(255, 141, 112) 64.23%, rgb(247, 201, 102) 83.76%, rgb(172, 143, 100) 100%);

View File

@@ -47,6 +47,7 @@ export interface Message {
createdAt: string
replyid?: number
likes?: number
messageimg?: string
}
/**
@@ -61,6 +62,7 @@ export interface MessageDto {
parentid?: number
replyid?: number
articleid?: number
messageimg?: string
}
/**

View File

@@ -1,12 +1,12 @@
// 日期格式化工具函数
/**
* 格式化日期为指定格式
* 格式化日期为指定格式,默认去掉秒,且三天内显示为今天、昨天、前天
* @param {string|Date} date - 日期对象或日期字符串
* @param {string} format - 格式化模板, 'YYYY-MM-DD HH:mm:ss'
* @param {string} format - 格式化模板,默认 'YYYY-MM-DD HH:mm'
* @returns {string} 格式化后的日期字符串
*/
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm') => {
if (!date) return ''
// 如果是字符串,转换为日期对象
@@ -15,12 +15,19 @@ export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
// 检查日期对象是否有效
if (isNaN(dateObj.getTime())) return ''
// 检查是否在三天内,如果是则返回相对时间
const relativeTime = getRelativeDay(dateObj)
if (relativeTime) {
const hours = String(dateObj.getHours()).padStart(2, '0')
const minutes = String(dateObj.getMinutes()).padStart(2, '0')
return `${relativeTime} ${hours}:${minutes}`
}
const year = dateObj.getFullYear()
const month = String(dateObj.getMonth() + 1).padStart(2, '0')
const day = String(dateObj.getDate()).padStart(2, '0')
const hours = String(dateObj.getHours()).padStart(2, '0')
const minutes = String(dateObj.getMinutes()).padStart(2, '0')
const seconds = String(dateObj.getSeconds()).padStart(2, '0')
// 替换模板中的日期部分
return format
@@ -29,7 +36,31 @@ export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
/**
* 获取相对天数(今天、昨天、前天)
* @param {Date} dateObj - 日期对象
* @returns {string|null} 相对天数或null
*/
export const getRelativeDay = (dateObj) => {
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const targetDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate())
// 计算天数差
const diffTime = today - targetDate
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) {
return '今天'
} else if (diffDays === 1) {
return '昨天'
} else if (diffDays === 2) {
return '前天'
}
return null
}
/**

View File

@@ -3,65 +3,99 @@
<div class="about-content-wrapper">
<!-- 页面头部 -->
<div class="about-page-header">
<h1 class="about-page-title">关于</h1>
<div class="about-page-subtitle">一个热爱技术的全栈开发者</div>
<h1 class="about-page-title">关于</h1>
</div>
<!-- 关于内容 -->
<div class="about-main-content">
<div class="about-personal-intro">
<p>你好欢迎来到我的个人博客我是一名热爱技术的全栈开发者热衷于探索新技术和解决复杂问题</p>
<p>这个博客是我分享技术见解学习心得和生活感悟的地方希望通过这个平台能够与更多志同道合的朋友交流和学习</p>
<h4>名字</h4>
<h5>
&nbsp; &nbsp; 让我回忆回忆...大一的时候还是上学的日子好哈哈哈哈哈跟室友一块玩游戏因为我的Steam名字叫清风慢慢的这个名儿就这么成了我的外号说来也怪被他们这么一叫心里那点初入大学的陌生和拘谨
好像真被一阵风吹散了似的
哈哈哈哈哈我还蛮喜欢这个外号的
<br></br>
&nbsp; &nbsp; 后来有一天也不知道怎么了精神状态不是很好感觉要控制不住要发疯了突然这疯子就像钉子一样扎在我脑海里哈哈哈哈于是就干脆改名叫清疯清风清疯念起来几乎没差
但内核却从一种理想的淡然切换成了真实的带点毛边的鲜活悠悠清风荡我心只是如今这阵清风熬成了清疯终于在我心里刮起一场疯疯啦
<br></br>
&nbsp; &nbsp; 又过些日子玩新游戏要起名正盯着输入框发呆清疯两个字在脑海里冒出来疯癫我好像是疯但也没完全颠嘛那种在理智边界试探却绝不越线的微妙感一下对味了干脆就叫清疯不颠
名字一出自己先乐了这不就是我吗表面疯癫内心门儿清简直是我们这代人精神状态的绝佳注脚
<br></br>
哈哈哈哈哈哈俗话说天才在左疯子在右在我这儿大概是左脑负责疯右脑负责颠两个家伙吵吵闹闹反而让我在这个世界里自得其乐
</h5>
</div>
<div class="about-personal-intro">
<h4>疯言疯语</h4>
<h5>
&nbsp; &nbsp; 我并没有网站的开发经验这是我的第一个项目所以我并不清楚该如何去写这个页面就干脆当一个我发疯的地方吧哈哈哈哈哈 所以你有看到彩色的小鹿吗
</h5>
</div>
<div class="about-skill-section">
<h3>前端技术栈</h3>
<h4>前端技术栈</h4>
<div class="skills-display-list">
<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML" target="_blank" class="skill-tag-link"><el-tag type="primary">HTML5</el-tag></a>
<a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS" target="_blank" class="skill-tag-link"><el-tag type="primary">CSS3</el-tag></a>
<a href="https://tailwindcss.com/" target="_blank" class="skill-tag-link"><el-tag type="primary">Tailwind CSS</el-tag></a>
<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript" target="_blank" class="skill-tag-link"><el-tag type="primary">JavaScript (ES6+)</el-tag></a>
<a href="https://www.typescriptlang.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">TypeScript</el-tag></a>
<a href="https://vuejs.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Vue.js 3</el-tag></a>
<a href="https://pinia.vuejs.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Pinia</el-tag></a>
<a href="https://react.dev/" target="_blank" class="skill-tag-link"><el-tag type="primary">React 18</el-tag></a>
<a href="https://nodejs.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Node.js</el-tag></a>
&nbsp; &nbsp;
<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML" target="_blank" class="skill-tag-link"><el-tag
type="primary">HTML5</el-tag></a>
<a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS" target="_blank" class="skill-tag-link"><el-tag
type="primary">CSS3</el-tag></a>
<a href="https://tailwindcss.com/" target="_blank" class="skill-tag-link"><el-tag type="primary">Tailwind
CSS</el-tag></a>
<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript" target="_blank"
class="skill-tag-link"><el-tag type="primary">JavaScript (ES6+)</el-tag></a>
<a href="https://www.typescriptlang.org/" target="_blank" class="skill-tag-link"><el-tag
type="primary">TypeScript</el-tag></a>
<a href="https://vuejs.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Vue.js
3</el-tag></a>
<a href="https://pinia.vuejs.org/" target="_blank" class="skill-tag-link"><el-tag
type="primary">Pinia</el-tag></a>
<a href="https://react.dev/" target="_blank" class="skill-tag-link"><el-tag type="primary">React
18</el-tag></a>
<a href="https://nodejs.org/" target="_blank" class="skill-tag-link"><el-tag
type="primary">Node.js</el-tag></a>
<a href="https://vite.dev/" target="_blank" class="skill-tag-link"><el-tag type="primary">Vite</el-tag></a>
<a href="https://webpack.js.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Webpack</el-tag></a>
<a href="https://element-plus.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Element Plus</el-tag></a>
<a href="https://git-scm.com/" target="_blank" class="skill-tag-link"><el-tag type="primary">Git</el-tag></a>
<a href="https://webpack.js.org/" target="_blank" class="skill-tag-link"><el-tag
type="primary">Webpack</el-tag></a>
<a href="https://element-plus.org/" target="_blank" class="skill-tag-link"><el-tag type="primary">Element
Plus</el-tag></a>
<a href="https://git-scm.com/" target="_blank" class="skill-tag-link"><el-tag
type="primary">Git</el-tag></a>
</div>
</div>
<div class="about-skill-section">
<h3>后端技术栈</h3>
<h4>后端技术栈</h4>
<div class="skills-display-list">
<a href="https://spring.io/projects/spring-boot" target="_blank" class="skill-tag-link"><el-tag type="success">Spring Boot 3.x</el-tag></a>
<a href="https://spring.io/projects/spring-security" target="_blank" class="skill-tag-link"><el-tag type="success">Spring Security</el-tag></a>
<a href="https://spring.io/projects/spring-data-jpa" target="_blank" class="skill-tag-link"><el-tag type="success">Spring Data JPA</el-tag></a>
<a href="https://mybatis.org/mybatis-3/" target="_blank" class="skill-tag-link"><el-tag type="success">MyBatis-Plus</el-tag></a>
<a href="https://www.mysql.com/" target="_blank" class="skill-tag-link"><el-tag type="success">MySQL 8</el-tag></a>
<a href="https://www.postgresql.org/" target="_blank" class="skill-tag-link"><el-tag type="success">PostgreSQL</el-tag></a>
&nbsp; &nbsp;
<a href="https://spring.io/projects/spring-boot" target="_blank" class="skill-tag-link"><el-tag
type="success">Spring Boot 3.x</el-tag></a>
<a href="https://spring.io/projects/spring-security" target="_blank" class="skill-tag-link"><el-tag
type="success">Spring Security</el-tag></a>
<a href="https://spring.io/projects/spring-data-jpa" target="_blank" class="skill-tag-link"><el-tag
type="success">Spring Data JPA</el-tag></a>
<a href="https://mybatis.org/mybatis-3/" target="_blank" class="skill-tag-link"><el-tag
type="success">MyBatis-Plus</el-tag></a>
<a href="https://www.mysql.com/" target="_blank" class="skill-tag-link"><el-tag type="success">MySQL
8</el-tag></a>
<a href="https://www.postgresql.org/" target="_blank" class="skill-tag-link"><el-tag
type="success">PostgreSQL</el-tag></a>
<a href="https://redis.io/" target="_blank" class="skill-tag-link"><el-tag type="success">Redis</el-tag></a>
<a href="https://projectlombok.org/" target="_blank" class="skill-tag-link"><el-tag type="success">Lombok</el-tag></a>
<a href="https://mapstruct.org/" target="_blank" class="skill-tag-link"><el-tag type="success">MapStruct</el-tag></a>
<a href="https://maven.apache.org/" target="_blank" class="skill-tag-link"><el-tag type="success">Maven</el-tag></a>
<a href="https://gradle.org/" target="_blank" class="skill-tag-link"><el-tag type="success">Gradle</el-tag></a>
<a href="https://www.oracle.com/java/technologies/java17.html" target="_blank" class="skill-tag-link"><el-tag type="success">Java 17+</el-tag></a>
<a href="https://projectlombok.org/" target="_blank" class="skill-tag-link"><el-tag
type="success">Lombok</el-tag></a>
<a href="https://mapstruct.org/" target="_blank" class="skill-tag-link"><el-tag
type="success">MapStruct</el-tag></a>
<a href="https://maven.apache.org/" target="_blank" class="skill-tag-link"><el-tag
type="success">Maven</el-tag></a>
<a href="https://gradle.org/" target="_blank" class="skill-tag-link"><el-tag
type="success">Gradle</el-tag></a>
<a href="https://www.oracle.com/java/technologies/java17.html" target="_blank"
class="skill-tag-link"><el-tag type="success">Java 17+</el-tag></a>
</div>
</div>
<div class="about-hobbies-section">
<h3>兴趣爱好</h3>
<ul class="hobbies-list">
<li>阅读技术书籍和博客</li>
<li>参与开源项目</li>
<li>学习新技术和框架</li>
</ul>
</div>
<div class="about-contact-section">
<h3>联系方式</h3>
<h4>联系方式</h4>
<p>如果你有任何问题或建议欢迎随时联系我</p>
<div class="contact-options">
<el-button type="primary" plain @click="goToMessageBoard">留言板</el-button>
@@ -95,7 +129,7 @@ const goToMessageBoard = () => {
.about-content-wrapper {
max-width: 900px;
margin: 0 auto;
background-color: white;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@@ -139,11 +173,12 @@ const goToMessageBoard = () => {
/* 个人介绍样式 */
.about-personal-intro {
margin-bottom: 32px;
text-align: left;
}
.about-personal-intro p {
.about-personal-intro h5 {
color: #34495e;
margin-bottom: 16px;
text-align: justify;
}
/* 技能部分样式 */
@@ -151,11 +186,8 @@ const goToMessageBoard = () => {
margin-bottom: 32px;
}
.about-skill-section h3 {
color: #2c3e50;
font-size: 1.3rem;
margin-bottom: 16px;
font-weight: 600;
.about-skill-section h4 {
text-align: left;
}
/* 技能列表样式 */
@@ -236,15 +268,10 @@ const goToMessageBoard = () => {
font-weight: 600;
}
.about-contact-section p {
margin-bottom: 16px;
text-align: justify;
}
/* 联系方式选项样式 */
.contact-options {
display: flex;
gap: 10px;
text-align: center;
}
/* 响应式设计 */
@@ -252,32 +279,32 @@ const goToMessageBoard = () => {
.about-page-container {
padding: 20px 0;
}
.about-content-wrapper {
padding: 20px;
margin: 0 15px;
}
.about-page-title {
font-size: 1.8rem;
}
.about-page-subtitle {
font-size: 1rem;
}
.about-main-content {
font-size: 1rem;
}
.about-page-header {
margin-bottom: 32px;
}
.contact-options {
flex-direction: column;
}
.skills-display-list {
gap: 8px;
}

View File

@@ -181,7 +181,7 @@ onMounted(() => {
.article-detail-wrapper {
max-width: 900px;
margin: 0 auto;
background-color: white;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@@ -193,7 +193,7 @@ onMounted(() => {
max-width: 900px;
margin: 0 auto;
padding: 40px;
background-color: white;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
@@ -203,7 +203,7 @@ onMounted(() => {
max-width: 900px;
margin: 0 auto;
padding: 60px 40px;
background-color: white;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@@ -218,7 +218,7 @@ onMounted(() => {
max-width: 900px;
margin: 0 auto;
padding: 80px 40px;
background-color: white;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

View File

@@ -1,41 +1,40 @@
<template>
<div id="allstyle">
<div class="article-header-section">
<div style="text-align: center;">
<input type="text" v-model="Articleform.title" class="article-main-title" placeholder="请输入标题"
@focus="($event.target as HTMLInputElement).placeholder = ''"
@blur="($event.target as HTMLInputElement).placeholder = '请输入标题'"
style="border: none; outline: none; background: transparent; width: 100%; font-size: 2.5rem; font-weight: 700; line-height: 1.2; text-align: center;" />
<div class="article-save-container">
<div class="article-content-wrapper">
<div class="article-header-section">
<div class="title-container">
<input type="text" v-model="Articleform.title" class="article-main-title" placeholder="请输入标题"
@focus="($event.target as HTMLInputElement).placeholder = ''"
@blur="($event.target as HTMLInputElement).placeholder = '请输入标题'" />
</div>
<div class="article-meta-info">
<span class="meta-item date-item">
<i class="el-icon-document"></i>
<span> {{ new Date().toLocaleDateString() }} </span>
</span>
<span class="meta-item status-item">
<i class="el-icon-document"></i>
<el-select v-model="Articleform.status" placeholder="请选择状态" class="meta-select">
<el-option v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</span>
<span class="meta-item category-item">
<i class="el-icon-folder"></i>
<el-cascader :options="categorieoptions" v-model="selectedValues" @change="handleCascaderChange"
placeholder="选择分类和属性" class="meta-cascader">
<template #default="{ node, data }">
<span>{{ data.label }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</span>
</div>
</div>
<div class="article-meta-info" style="text-align: center;">
<span class="meta-item">
<i class="el-icon-document"></i>
<span> {{ new Date().toLocaleDateString() }} </span>
</span>
<span class="meta-item">
<i class="el-icon-document"></i>
<el-select v-model="Articleform.status" placeholder="请选择状态">
<el-option v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</span>
<span class="meta-item">
<i class="el-icon-folder"></i>
<el-cascader :options="categorieoptions" v-model="selectedValues" @change="handleCascaderChange"
placeholder="选择分类和属性">
<template #default="{ node, data }">
<span>{{ data.label }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</span>
<div class="editor-container">
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" />
</div>
</div>
<div>
<MdEditor v-model="Articleform.markdownscontent" class="markdown-editor" @on-save="handleSave" />
</div>
</div>
</template>
@@ -46,6 +45,8 @@ import 'md-editor-v3/lib/style.css';
import { categoryService, categoryAttributeService, articleService } from '@/services';
import type { Article } from '@/types/index.ts';
import { ElMessage } from 'element-plus';
// 路由
import router from '@/router/Router';
const Articleform = ref<Article>({
articleid: 0,
title: '',
@@ -155,6 +156,20 @@ const handleSave = (markdown) => {
console.log('API响应:', res);
if (res.code === 200) {
ElMessage.success('文章保存成功')
// 重置表单
Articleform.value = {
articleid: 0,
title: '',
content: '',
attributeid: 0,
categoryName: '',
createdAt: '',
updatedAt: '',
markdownscontent: ''
};
selectedValues.value = [];
// 返回列表页
router.push('/home');
} else {
ElMessage.error(res.message || '文章保存失败')
}
@@ -184,34 +199,66 @@ const handleSave = (markdown) => {
<style scoped>
.error-state-container .el-button {
margin-top: 16px;
/* 主容器 */
.article-save-container {
min-height: 100vh;
margin: 0;
}
/* 内容包装器 */
.article-content-wrapper {
max-width: 1000px;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
padding: 40px;
}
/* 文章头部区域 */
.article-header-section {
margin-bottom: 32px;
margin-bottom: 40px;
padding-bottom: 24px;
border-bottom: 2px solid #ecf0f1;
}
/* 标题容器 */
.title-container {
text-align: center;
margin-bottom: 30px;
}
/* 文章标题 */
.article-main-title {
font-size: 2rem;
width: 100%;
border: none;
outline: none;
background: transparent;
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
color: #2c3e50;
line-height: 1.4;
margin-bottom: 20px;
text-align: center;
padding: 10px 0;
transition: all 0.3s ease;
}
.article-main-title::placeholder {
color: #bdc3c7;
font-weight: 600;
margin-top: 20px;
}
.article-main-title:focus {
color: #2c3e50;
}
/* 文章元信息 */
.article-meta-info {
display: flex;
flex-wrap: wrap;
gap: 20px;
gap: 30px;
align-items: center;
justify-content: center;
color: #7f8c8d;
font-size: 0.95rem;
}
@@ -220,210 +267,138 @@ const handleSave = (markdown) => {
.meta-item {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
font-weight: 500;
}
/* 文章内容区域 */
.article-content-area {
font-size: 1.05rem;
line-height: 1.8;
color: #34495e;
margin-bottom: 32px;
}
/* 文章内容中的段落 */
.article-content-area p {
margin-bottom: 16px;
text-align: justify;
}
/* 文章内容中的二级标题 */
.article-content-area h2 {
color: #2c3e50;
font-size: 1.5rem;
margin: 32px 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid #ecf0f1;
font-weight: 600;
}
/* 文章内容中的三级标题 */
.article-content-area h3 {
color: #34495e;
font-size: 1.3rem;
margin: 24px 0 12px 0;
font-weight: 600;
}
/* 文章内容中的图片 */
.article-content-area img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 20px auto;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 文章内容中的引用 */
.article-content-area blockquote {
border-left: 4px solid #3498db;
padding-left: 16px;
color: #7f8c8d;
margin: 16px 0;
font-style: italic;
}
/* 文章底部区域 */
.article-footer-section {
padding-top: 24px;
border-top: 2px solid #ecf0f1;
margin-bottom: 32px;
}
/* 标签列表 */
.article-tag-list {
margin-bottom: 24px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* 文章操作按钮组 */
.article-actions-group {
display: flex;
justify-content: flex-start;
}
/* 相关文章区域 */
.related-articles-section {
padding-top: 32px;
border-top: 2px solid #ecf0f1;
}
/* 相关文章标题 */
.related-articles-section h3 {
font-size: 1.3rem;
color: #2c3e50;
margin-bottom: 16px;
font-weight: 600;
}
/* 相关文章列表容器 */
.related-articles-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 相关文章卡片 */
.related-article-card {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
}
/* 相关文章卡片悬停效果 */
.related-article-card:hover {
background-color: #e9ecef;
transform: translateX(5px);
border-color: #3498db;
}
.related-article-card i {
color: #3498db;
}
.related-article-card span {
font-size: 1rem;
color: #495057;
transition: color 0.3s ease;
}
.related-article-card:hover span {
.meta-item:hover {
color: #3498db;
}
/* 评论区样式 */
.comment-section {
margin-top: 32px;
/* 元信息选择器样式 */
.meta-select {
width: 160px;
}
/* 响应式设计 - 平板和手机 */
.meta-cascader {
width: 280px;
}
/* 编辑器容器 */
.editor-container {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* Markdown编辑器样式 */
.markdown-editor {
border-radius: 8px;
}
/* 响应式设计 - 平板 */
@media (max-width: 768px) {
#article-detail-page {
padding: 20px 0;
}
.article-detail-wrapper,
.loading-state-container,
.error-state-container,
.empty-state-container {
padding: 20px;
.article-content-wrapper {
padding: 30px 20px;
margin: 0 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
.article-main-title {
font-size: 1.5rem;
line-height: 1.5;
font-size: 2rem;
line-height: 1.4;
}
.article-meta-info {
flex-direction: column;
align-items: flex-start;
gap: 8px;
font-size: 0.9rem;
align-items: center;
gap: 15px;
}
.article-content-area {
font-size: 1rem;
line-height: 1.7;
.meta-item {
justify-content: center;
width: 100%;
}
.article-content-area h2 {
font-size: 1.3rem;
margin: 24px 0 12px 0;
}
.article-content-area h3 {
font-size: 1.2rem;
margin: 20px 0 10px 0;
}
.related-articles-section h3 {
font-size: 1.2rem;
}
.related-article-card {
padding: 10px;
gap: 8px;
}
.related-article-card span {
font-size: 0.95rem;
.meta-select,
.meta-cascader {
width: 100%;
max-width: 300px;
}
}
/* 响应式设计 - 小屏幕手机 */
/* 响应式设计 - 手机 */
@media (max-width: 480px) {
.article-detail-wrapper {
padding: 16px;
.article-save-container {
padding: 15px 0;
}
.article-content-wrapper {
padding: 20px 15px;
margin: 0 10px;
border-radius: 8px;
}
.article-main-title {
font-size: 1.35rem;
font-size: 1.75rem;
line-height: 1.5;
}
.article-meta-info {
font-size: 0.85rem;
font-size: 0.9rem;
gap: 12px;
}
.meta-select,
.meta-cascader {
max-width: 100%;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.article-save-container {
background-color: #1a1a1a;
}
.article-content-wrapper {
background-color: #2d2d2d;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.article-header-section {
border-bottom-color: #444;
}
.article-main-title {
color: #f0f0f0;
}
.article-main-title::placeholder {
color: #666;
}
.article-meta-info {
color: #bbb;
}
.meta-item:hover {
color: #4a9eff;
}
}
/* 级联选择器ul元素自适应高度 */
:deep(.el-cascader-menu__list) {
height: auto !important;
max-height: none !important;
overflow-y: visible !important;
}
/* 确保级联选择器的每个菜单项都能正确显示 */
:deep(.el-cascader-menu) {
height: auto !important;
min-height: 200px;
}
</style>

View File

@@ -19,7 +19,7 @@
<div v-if="article.marked" class="article-special-tag">标记文章</div>
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
<div class="article-meta-info">
<span class="article-publish-date">{{ formatDateDisplay(article.createdAt || article.createTime) }}</span>
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt || article.createTime) }}</span>
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
<span v-if="article.categoryName" class="article-category-badge"> {{ article.categoryName }} </span>
<span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
@@ -136,30 +136,6 @@ const handleArticleClick = (article) => {
})
}
/**
* 格式化日期显示
* @param {string} dateString - 日期字符串
* @returns {string} 格式化后的日期
*/
const formatDateDisplay = (dateString) => {
if (!dateString) return ''
try {
// 如果是今天或昨天的文章,显示相对时间
const date = new Date(dateString)
const now = new Date()
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24))
if (diffDays < 2) {
return formatRelativeTime(dateString)
}
// 否则显示具体日期
return formatDate(dateString, 'YYYY-MM-DD')
} catch (error) {
console.error('日期格式化错误:', error)
return dateString
}
}
// 组件挂载时获取数据
onMounted(() => {
@@ -188,7 +164,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 12px;
background-color: rgba(255, 255, 255, 0.95);
background-color: rgba(255, 255, 255, 0.85);
padding: 24px;
margin-bottom: 24px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);

View File

@@ -15,7 +15,7 @@
<div class="form-group">
<div class="input-wrapper">
<!-- <i class="el-icon-user input-icon"></i> -->
<input id="username" v-model="loginForm.username" type="text" placeholder="请输入用户名" class="form-input"
<input id="username" v-model="loginForm.username" type="text" placeholder="请输入用户名" class="form-input"
:class="{ 'input-error': loginForm.username }" @blur="validateField('username')" />
</div>
<span v-if="errors.username" class="error-message">{{ errors.username }}</span>
@@ -28,7 +28,8 @@
<input id="password" v-model="loginForm.password" :type="showPassword ? 'text' : 'password'"
placeholder="请输入密码" class="form-input" :class="{ 'input-error': loginForm.password }"
@blur="validateField('password')" />
<i :class="showPassword ? 'el-icon-view' : 'el-icon-view-off'" class="toggle-password-icon" @click="togglePassword"></i>
<i :class="showPassword ? 'el-icon-view' : 'el-icon-view-off'" class="toggle-password-icon"
@click="togglePassword"></i>
</div>
<span v-if="errors.password" class="error-message">{{ errors.password }}</span>
</div>
@@ -129,10 +130,10 @@ const validateForm = () => {
}
if (!loginForm.password) {
errors.value.password = '请输入密码'
errors.value.password = '请输入密码'
isValid = false
} else if (loginForm.password.length < 6) {
errors.value.password = '密码长度至少为6位'
errors.value.password = '密码长度至少为6位'
isValid = false
}
return isValid;
@@ -157,6 +158,10 @@ const handleLogin = async () => {
ElMessage.success('登录成功')
// 登录成功后,设置全局状态为已登录
globalStore.setLoginStatus(true)
// 登录成功后,添加全局状态
globalStore.setValue('loginhomestatus', {
status: 1 // 2:删除 1:已发布 0:发布登录
})
console.log('globalStore.Login', globalStore.Login)
// 保存登录状态token
if (user.token) {
@@ -165,9 +170,9 @@ const handleLogin = async () => {
if (user.username) {
// 记住用户名
globalStore.setUsername(user.username)
}
}
// 跳转到首页
router.push('/')
router.push('/')
} catch (error) {
ElMessage.error('登录失败,请检查用户名和密码')
console.error('登录错误:', error)
@@ -176,14 +181,14 @@ const handleLogin = async () => {
}
}
// 处理忘记密码
const handleForgotPassword = () => {
ElMessage.info('忘记密码功能开发中')
}
// 处理第三方登录
const handleSocialLogin = (provider) => {
ElMessage.info(`${provider} 登录功能开发中`)
}
// 处理忘记密码
const handleForgotPassword = () => {
ElMessage.info('忘记密码功能开发中')
}
// 处理第三方登录
const handleSocialLogin = (provider) => {
ElMessage.info(`${provider} 登录功能开发中`)
}
// mounted() {
// // 从localStorage中恢复用户名

View File

@@ -94,6 +94,7 @@ onMounted(() => {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
background-color: rgba(255, 255, 255, 0.85);
}
.editor:hover {
@@ -123,7 +124,9 @@ onMounted(() => {
font-size: 14px;
text-align: center;
}
.v-md-editor.v-md-editor--preview {
background-color: rgba(240, 240, 240, 0);
}
/* 确保编辑器内容在移动设备上正常显示 */
@media (max-width: 768px) {
.editor {

View File

@@ -14,7 +14,12 @@
<div v-for="comment in messageBoardData" :key="comment.messageid" class="comment-item-wrapper">
<div class="comment-header-info">
<!-- 头像 -->
<img :src="getAvatar()" class="user-avatar">
<div class="avatar-container">
<img v-if="getAvatarUrl(comment.messageimg)" :src="getAvatarUrl(comment.messageimg)" class="user-avatar" alt="头像">
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(comment.displayName || comment.nickname)">
{{ getInitialLetter(comment.displayName || comment.nickname) }}
</div>
</div>
<div class="user-meta-info">
<div class="user-nickname">{{ comment.displayName || comment.nickname }}</div>
<div class="comment-time">{{ formatDate(comment.createdAt) || '刚刚' }}</div>
@@ -23,16 +28,26 @@
<div class="comment-content-text" v-html="comment.content"></div>
<div class="comment-actions-bar">
<span class="like-button" @click="handleLike(comment)">
<span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes }}</span>
<span v-if="comment.likes && comment.likes > 0" class="like-count">{{ comment.likes
}}</span>
👍
</span>
<span class="reply-button" @click="handleReply(null, comment)">回复</span>
<!-- 删除按钮 -->
<span class="delete-button" @click="handleDelete(comment.messageid)"
v-if="globalStore.Login">删除</span>
</div>
<!-- 回复列表 -->
<div v-if="comment.replies && comment.replies && comment.replies.length > 0" class="reply-list-container">
<div v-if="comment.replies && comment.replies && comment.replies.length > 0"
class="reply-list-container">
<div v-for="reply in comment.replies" :key="reply.messageid" class="reply-item-wrapper">
<div class="reply-header-info">
<img :src="getAvatar()" class="user-avatar">
<div class="avatar-container">
<img v-if="getAvatarUrl(reply.messageimg)" :src="getAvatarUrl(reply.messageimg)" class="user-avatar" alt="头像">
<div v-else class="letter-avatar" :style="getLetterAvatarStyle(reply.displayName || reply.nickname)">
{{ getInitialLetter(reply.displayName || reply.nickname) }}
</div>
</div>
<div class="user-meta-info">
<div class="user-nickname">{{ reply.displayName || reply.nickname }}</div>
<div class="comment-time">{{ formatDate(reply.createdAt) || '刚刚' }}</div>
@@ -41,12 +56,14 @@
<div class="reply-content-text">{{ reply.content }}</div>
<div class="reply-actions-bar">
<span class="like-button" @click="handleLike(reply)">
<span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes }}</span>
<span v-if="reply.likes && reply.likes > 0" class="like-count">{{ reply.likes
}}</span>
👍
</span>
<span class="reply-button" @click="handleReply(comment, reply)">回复</span>
<!-- 删除按钮 -->
<span class="delete-button" @click="handleDelete(reply.messageid)" v-if=" globalStore.Login">删除</span>
<span class="delete-button" @click="handleDelete(reply.messageid)"
v-if="globalStore.Login">删除</span>
</div>
</div>
</div>
@@ -84,14 +101,8 @@
</el-form-item>
<el-form-item prop="captcha">
<div class="captcha-input-wrapper">
<el-input
v-model="form.captcha"
placeholder="验证码"
clearable
:disabled="submitting"
@focus="showCaptchaHint = true"
@blur="showCaptchaHint = false"
/>
<el-input v-model="form.captcha" placeholder="验证码" clearable :disabled="submitting"
@focus="showCaptchaHint = true" @blur="showCaptchaHint = false" />
<div class="captcha-hint-popup" @click="generateCaptcha" v-show="showCaptchaHint">
{{ captchaHint }}
<span class="refresh-icon"></span>
@@ -122,10 +133,10 @@ import { formatDate } from '@/utils/dateUtils'
// 定义组件属性
const props = defineProps({
comments: {
type: Number,
default: null
}
comments: {
type: Number,
default: null
}
})
const globalStore = useGlobalStore()
const messageBoardData = ref([]) // 留言板留言articleid为空的主留言及其回复
@@ -152,13 +163,13 @@ const form = reactive({
const generateCaptcha = () => {
// 随机选择数学题或字符验证码
const isMathCaptcha = Math.random() > 0.5
if (isMathCaptcha) {
// 简单数学题:加法或减法
const num1 = Math.floor(Math.random() * 10) + 1
const num2 = Math.floor(Math.random() * 10) + 1
const operator = Math.random() > 0.5 ? '+' : '-'
let answer
if (operator === '+') {
answer = num1 + num2
@@ -170,7 +181,7 @@ const generateCaptcha = () => {
captchaAnswer.value = (larger - smaller).toString()
return
}
captchaHint.value = `${num1} ${operator} ${num2} = ?`
captchaAnswer.value = answer.toString()
} else {
@@ -227,10 +238,53 @@ const rules = {
}
// 生成头像URL
const getAvatar = (email) => {
if (!email) return 'https://www.gravatar.com/avatar?d=mp&s=40'
return `https://www.gravatar.com/avatar/${email}?d=mp&s=40`
// 获取Gravatar头像URL
const getAvatarUrl = (email) => {
if (!email) return null;
// 简单验证邮箱格式
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) return null;
// 使用邮箱的MD5哈希这里简化处理实际项目中应该使用md5库
// 注意在实际项目中应该使用正确的MD5哈希函数
return `https://www.gravatar.com/avatar/${email}?d=404&s=40`;
}
// 获取名字的首字母
const getInitialLetter = (name) => {
if (!name || typeof name !== 'string') return '?';
// 移除可能的@回复前缀
const cleanName = name.replace(/^.+@\s*/, '');
// 获取第一个字符
const firstChar = cleanName.charAt(0).toUpperCase();
return firstChar;
}
// 获取首字母头像的样式
const getLetterAvatarStyle = (name) => {
// 颜色映射表,根据名字生成一致的颜色
const colors = [
'#4A90E2', '#50E3C2', '#F5A623', '#D0021B', '#9013FE',
'#B8E986', '#BD10E0', '#50E3C2', '#417505', '#7ED321',
'#BD10E0', '#F8E71C', '#8B572A', '#9B9B9B', '#4A4A4A'
];
// 根据名字生成一个一致的颜色索引
let hash = 0;
if (name) {
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
}
const colorIndex = Math.abs(hash) % colors.length;
return {
backgroundColor: colors[colorIndex],
color: 'white'
};
}
// 从后端获取留言列表
@@ -238,18 +292,18 @@ const fetchMessages = async () => {
try {
loading.value = true
let res = null
// 优先使用props传递的articleid其次使用globalStore中的数据
let articleid = props.comments || null
if (!articleid) {
// 安全获取文章ID如果globalStore中没有articleInfo则返回null
const articleData = globalStore.getValue('articleInfo')
articleid = (articleData && typeof articleData === 'object' && 'articleid' in articleData) ? articleData.articleid : null
}
form.articleid = articleid
// 根据是否有文章ID选择不同的API调用
if (articleid) {
res = await messageService.getMessagesByArticleId(articleid)
@@ -260,26 +314,26 @@ const fetchMessages = async () => {
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
}
}
// 验证响应结果
if (!res || !res.data) {
console.warn('未获取到留言数据')
messageBoardData.value = []
return
}
const allMessages = res.data
// 处理所有留言为主留言添加replies数组
const allMessagesWithReplies = allMessages.map(msg => ({
...msg,
replies: []
}))
// 分离主留言和回复
const mainMessages = []
const replies = []
allMessagesWithReplies.forEach(msg => {
if (msg.parentid && msg.parentid > 0) {
replies.push(msg)
@@ -287,7 +341,7 @@ const fetchMessages = async () => {
mainMessages.push(msg)
}
})
// 将回复添加到对应的主留言中
replies.forEach(reply => {
// 找到父留言
@@ -297,7 +351,7 @@ const fetchMessages = async () => {
if (reply.replyid) {
const repliedMsg = replies.find(msg => msg.messageid === reply.replyid)
if (repliedMsg) {
reply.displayName = `${reply.nickname}@${repliedMsg.nickname}`
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
} else {
reply.displayName = reply.nickname
}
@@ -307,7 +361,7 @@ const fetchMessages = async () => {
parentMsg.replies.push(reply)
}
})
// 更新留言板数据
messageBoardData.value = mainMessages
} catch (error) {
@@ -365,7 +419,7 @@ onMounted(() => {
const onSubmit = async () => {
if (!formRef.value) return;
// 表单验证
await formRef.value.validate((valid) => {
if (!valid) {
@@ -444,9 +498,9 @@ const handleLike = async (msg) => {
try {
// 显示加载状态或禁用按钮
msg.isLiking = true
const res = await messageService.likeMessage(msg.messageid)
if (res.success && res.data) {
// 更新点赞数
msg.likes = res.data.likes || 0
@@ -470,7 +524,7 @@ const handleDelete = async (msg) => {
return
}
const res = await messageService.deleteMessage(msg.messageid)
if (res.success) {
// 从列表中移除
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== msg.messageid)
@@ -497,11 +551,12 @@ const handleDelete = async (msg) => {
/* 留言列表容器 */
.message-list-wrapper {
margin-bottom: 24px;
background: #f8fafd;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.85);
padding: 16px;
min-height: 120px;
}
/* 留言板标题 */
.message-board-title {
color: #2c3e50;
@@ -519,23 +574,17 @@ const handleDelete = async (msg) => {
/* 评论列表容器 */
.comment-list-container {
background-color: #f5f5f5;
// background-color: #f5f5f5;
}
/* 评论项容器 */
.comment-item-wrapper {
background-color: #fff;
padding: 16px;
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.comment-item-wrapper:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
/* 评论头部信息 */
.comment-header-info {
@@ -544,15 +593,37 @@ const handleDelete = async (msg) => {
margin-bottom: 12px;
}
.avatar-container {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-right: 12px;
}
/* 用户头像 */
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
object-fit: cover;
}
/* 首字母头像 */
.letter-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
color: white;
text-transform: uppercase;
}
/* 用户元信息 */
.user-meta-info {
flex: 1;
@@ -584,8 +655,10 @@ const handleDelete = async (msg) => {
/* 评论操作栏 */
.comment-actions-bar {
padding-bottom: 8px;
text-align: right;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
color: #666;
}
@@ -627,6 +700,7 @@ const handleDelete = async (msg) => {
color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
// 删除按钮
.delete-button {
cursor: pointer;
@@ -635,17 +709,19 @@ const handleDelete = async (msg) => {
border-radius: 4px;
display: inline-block;
}
.delete-button:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
/* 回复列表容器 */
.reply-list-container {
margin-top: 16px;
padding-left: 52px;
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
/**
* 内联表单输入行样式
* 用于将表单输入项与标签或其他元素对齐
@@ -654,6 +730,7 @@ const handleDelete = async (msg) => {
display: flex;
align-items: center;
}
.form-input-row--inline div:nth-child(2) {
margin-left: 9%;
margin-right: 9%;
@@ -661,7 +738,7 @@ const handleDelete = async (msg) => {
/* 回复项容器 */
.reply-item-wrapper {
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
padding: 12px;
margin-bottom: 8px;
border-radius: 6px;
@@ -669,8 +746,8 @@ const handleDelete = async (msg) => {
}
.reply-item-wrapper:hover {
background-color: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
// background-color: #f0f0f0;
// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* 回复头部信息 */
@@ -700,13 +777,13 @@ const handleDelete = async (msg) => {
text-align: center;
color: #bbb;
padding: 32px 0;
background-color: #f8f9fa;
// background-color: #f8f9fa;
border-radius: 8px;
}
/* 评论表单区域 */
.comment-form-section {
background: #fff;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
@@ -721,7 +798,6 @@ const handleDelete = async (msg) => {
/* 回复预览容器 */
.reply-preview-container {
background: #f0f9ff;
border-left: 4px solid #409eff;
padding: 15px;
border-radius: 6px;
@@ -733,7 +809,6 @@ const handleDelete = async (msg) => {
.reply-preview-text {
margin-top: 6px;
padding: 10px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 4px;
font-style: italic;
color: #666;
@@ -794,31 +869,31 @@ const handleDelete = async (msg) => {
.message-board-container {
padding: 8px 0;
}
.message-list-wrapper {
padding: 12px;
}
.comment-form-section {
padding: 16px;
}
.form-input-row {
flex-direction: column;
gap: 8px;
}
.comment-header-info,
.reply-header-info {
flex-direction: column;
align-items: flex-start;
}
.user-avatar {
margin-right: 0;
margin-bottom: 8px;
}
.reply-list-container {
padding-left: 20px;
}

View File

@@ -7,37 +7,210 @@
:key="item.id"
>
<div class="nonsense-meta-info">
<span class="nonsense-time">{{ item.time }}</span>
<span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span>
</div>
<div class="nonsense-content">
<span
v-for="(char, index) in item.content.split('')"
:key="index"
:ref="el => setCharRef(el, item.id, index)"
:style="getCharStyle(item.id, index)"
>{{ char }}</span>
</div>
<div class="nonsense-content">{{ item.content }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import {nonsenseService } from '@/services'
import { ElMessage } from 'element-plus'
import { formatRelativeTime} from '@/utils/dateUtils'
/**
* 吐槽数据列表
* 仅站长可见/可发
*/
const nonsenseList = ref([
{
id: 1,
content: '嘿嘿 嘿嘿嘿流口水ing',
time: '2025-09-26 09:30'
const nonsenseList = ref([])
// 存储字符引用和样式的映射
const charRefs = ref(new Map())
const charStyles = ref(new Map())
// 定时器引用
let colorChangeTimer = null
/**
* 加载所有吐槽内容
*/
const loadNonsenseList = async () => {
try {
const response = await nonsenseService.getAllNonsense()
if (response.code === 200) {
nonsenseList.value = response.data
}else{
ElMessage.error('加载吐槽内容失败')
}
} catch (error) {
console.error('加载吐槽内容失败:', error)
} finally {
console.log('加载吐槽内容完成')
}
])
}
// 设置字符引用
const setCharRef = (el, itemId, index) => {
if (el) {
const key = `${itemId}_${index}`
charRefs.value.set(key, el)
}
}
// 获取字符样式
const getCharStyle = (itemId, index) => {
const key = `${itemId}_${index}`
return charStyles.value.get(key) || {}
}
// 生成随机颜色
const getRandomColor = () => {
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#6c5ce7', '#e84393', '#00b894', '#fdcb6e']
return colors[Math.floor(Math.random() * colors.length)]
}
// 随机改变部分字体颜色并添加信号故障效果
const randomChangeColors = () => {
const keys = Array.from(charRefs.value.keys())
if (keys.length === 0) return
// 随机选择20-30%的字符改变颜色
const countToChange = Math.floor(keys.length * (Math.random() * 0.1 + 0.2))
const shuffledKeys = [...keys].sort(() => 0.5 - Math.random())
const selectedKeys = shuffledKeys.slice(0, countToChange)
// 创建信号故障效果
createSignalGlitchEffect(selectedKeys)
}
// 创建数字噪点效果
const createDigitalNoiseEffect = (selectedKeys) => {
// 噪点字符集
const noiseChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?';
selectedKeys.forEach(key => {
const charElement = charRefs.value.get(key);
if (charElement) {
const rect = charElement.getBoundingClientRect();
const container = charElement.closest('.nonsense-item');
const containerRect = container.getBoundingClientRect();
// 生成3-5个噪点
const noiseCount = Math.floor(Math.random() * 3) + 3;
for (let i = 0; i < noiseCount; i++) {
const noiseEl = document.createElement('span');
noiseEl.textContent = noiseChars.charAt(Math.floor(Math.random() * noiseChars.length));
// 随机位置相对于原字符
const x = Math.random() * 30 - 20; // -20到20px
const y = Math.random() * 20 - 15; // -15到15px
// 随机大小和透明度
const size = Math.random() * 8 + 8; // 8-16px
const opacity = Math.random() * 0.6 + 0.2; // 0.2-0.8
noiseEl.style.cssText = `
position: absolute;
left: ${rect.left - containerRect.left + x}px;
top: ${rect.top - containerRect.top + y}px;
font-size: ${size}px;
opacity: ${opacity};
color: ${getRandomColor()};
pointer-events: none;
z-index: 1;
font-family: monospace;
transition: all 0.2s ease;
`;
container.appendChild(noiseEl);
// 噪点逐渐消失
setTimeout(() => {
noiseEl.style.opacity = '0';
setTimeout(() => {
if (container.contains(noiseEl)) {
container.removeChild(noiseEl);
}
}, 200);
}, Math.random() * 300 + 200); // 200-500ms后开始消失
}
}
});
};
// 创建信号故障效果
const createSignalGlitchEffect = (selectedKeys) => {
// 第一步:随机偏移字符位置,模拟信号干扰
selectedKeys.forEach(key => {
const glitchOffset = {
transform: `translate(${Math.random() * 6 - 3}px, ${Math.random() * 6 - 3}px)`,
opacity: Math.random() * 0.4 + 0.6, // 随机透明度
transition: 'all 0.1s ease'
}
charStyles.value.set(key, glitchOffset)
})
// 同时创建数字噪点效果
createDigitalNoiseEffect(selectedKeys);
// 第二步:快速恢复并闪烁
setTimeout(() => {
selectedKeys.forEach(key => {
const flashStyle = {
transform: 'translate(0, 0)',
opacity: Math.random() > 0.5 ? 0 : 1, // 闪烁效果
transition: 'all 0.05s ease'
}
charStyles.value.set(key, flashStyle)
})
// 第三步:最终设置新颜色
setTimeout(() => {
selectedKeys.forEach(key => {
const finalStyle = {
color: getRandomColor(),
opacity: 1,
transform: 'translate(0, 0)',
transition: 'all 0.3s ease'
}
charStyles.value.set(key, finalStyle)
})
}, 80)
}, 100)}
// 组件挂载时获取数据并启动定时器
onMounted(() => {
loadNonsenseList()
// 启动定时器
colorChangeTimer = setInterval(randomChangeColors, 1000)
})
// 组件卸载时清除定时器
onBeforeUnmount(() => {
if (colorChangeTimer) {
clearInterval(colorChangeTimer)
colorChangeTimer = null
}
})
</script>
<style scoped>
/* 吐槽页面主容器样式 */
.nonsense-container {
background: rgba(255,255,255,0.95);
/* background-color: rgba(255, 255, 255, 0.85); */
border-radius: 12px;
padding: 32px 20px 24px 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
/* box-shadow: 0 2px 12px rgba(0,0,0,0.06); */
transition: box-shadow 0.3s ease;
}
@@ -74,7 +247,7 @@ const nonsenseList = ref([
/* 吐槽项样式 */
.nonsense-item {
background: #f8fafd;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 10px;
padding: 18px 20px 14px 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.03);