feat: 新增疯言疯语功能并优化UI样式
- 添加疯言疯语服务及页面,支持随机字符颜色变化效果 - 引入汉仪唐韵字体并优化全局字体设置 - 重构日期工具函数,优化时间显示格式 - 改进左侧模块布局,添加文章/分类/标签统计 - 优化浮动按钮组件,增加动态过渡效果 - 调整多个页面的背景透明度,提升视觉一致性 - 完善文章保存页面样式和交互逻辑 - 更新关于页面内容,增加个人介绍和技术栈展示 - 修复路由状态管理问题,优化页面跳转逻辑
This commit is contained in:
BIN
public/fonts/HanTang.woff2
Normal file
BIN
public/fonts/HanTang.woff2
Normal file
Binary file not shown.
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
37
src/services/nonsenseService.js
Normal file
37
src/services/nonsenseService.js
Normal 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()
|
||||
@@ -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%);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
让我回忆回忆...大一的时候(还是上学的日子好哈哈哈哈哈!!)跟室友一块玩游戏,因为我的Steam名字叫“清风”,慢慢的这个名儿就这么成了我的外号。说来也怪,被他们这么一叫,心里那点初入大学的陌生和拘谨,
|
||||
好像真被一阵风吹散了似的,
|
||||
哈哈哈哈哈,我还蛮喜欢这个外号的。
|
||||
<br></br>
|
||||
后来有一天,也不知道怎么了,精神状态不是很好,感觉要控制不住要发疯了,突然这疯子就像钉子一样扎在我脑海里(哈哈哈哈!)。于是就干脆改名叫“清疯”了——清风,清疯,念起来几乎没差,
|
||||
但内核却从一种理想的淡然,切换成了真实的、带点毛边的鲜活。“悠悠清风荡我心”,只是如今这阵清风熬成了清疯,终于在我心里刮起一场疯——疯啦!
|
||||
<br></br>
|
||||
又过些日子,玩新游戏要起名,正盯着输入框发呆,“清疯”两个字在脑海里冒出来……疯癫?哎,我好像是疯,但也没完全颠嘛!那种在理智边界试探、却绝不越线的微妙感,一下对味了——干脆就叫“清疯不颠”!
|
||||
名字一出自己先乐了,这不就是我吗,表面疯癫,内心门儿清,简直是我们这代人精神状态的绝佳注脚。
|
||||
<br></br>
|
||||
哈哈哈哈哈哈!俗话说天才在左疯子在右,在我这儿,大概是左脑负责疯,右脑负责颠,两个家伙吵吵闹闹,反而让我在这个世界里自得其乐。
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="about-personal-intro">
|
||||
<h4>疯言疯语</h4>
|
||||
<h5>
|
||||
我并没有网站的开发经验这是我的第一个项目,所以我并不清楚该如何去写这个页面。就干脆当一个我发疯的地方吧(哈哈哈哈哈 所以你有看到彩色的小鹿吗?)
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 平板和手机 */
|
||||
@media (max-width: 768px) {
|
||||
#article-detail-page {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.meta-cascader {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.article-detail-wrapper,
|
||||
.loading-state-container,
|
||||
.error-state-container,
|
||||
.empty-state-container {
|
||||
padding: 20px;
|
||||
/* 编辑器容器 */
|
||||
.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-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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
@@ -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中恢复用户名
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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为空的主留言及其回复)
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
|
||||
// 从后端获取留言列表
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user