Files
MyfronyProject/src/views/nonsense.vue
qingfeng1121 0151afcde7 feat: 添加分类树接口和分页组件,优化留言板功能
refactor: 重构文章服务和留言服务API调用方式
fix: 修复token验证逻辑和全局状态管理问题
style: 更新页脚样式和关于页面内容
docs: 添加类型定义和接口注释
2025-12-23 13:57:35 +08:00

436 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="nonsense-container">
<div class="nonsense-list">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state-container">
<el-skeleton :count="5" />
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-state-container">
<div>加载失败请稍后重试</div>
<el-button type="primary" @click="handleRetry">重试</el-button>
</div>
<!-- 空状态 -->
<div v-if="displayedNonsenseList.length === 0" class="empty-state-container">
<div>暂无吐槽内容</div>
</div>
<div class="nonsense-item" v-for="item in displayedNonsenseList" :key="item.id">
<div class="nonsense-meta-info">
<span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span>
<div class="article-status-badge-container" v-if="globalStore.Login">
<span v-if="item.status === 0" class="article-status-badge badge badge-warning">未发表</span>
<span v-if="item.status === 1" class="article-status-badge badge badge-success">已发表</span>
<span v-if="item.status === 2" class="article-status-badge badge badge-danger">已删除</span>
<span class="edit-button" @click="handleEdit(item)">编辑</span>
<span class="delete-button" @click="handleDelete(item.id)">删除</span>
</div>
</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>
<!-- 分页区域 -->
<div class="pagination-container">
<CustomPagination
v-model:page-num="pageNum"
:page-size="pageSize"
:total-pages="totalPages"
@page-change="handlePageChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { nonsenseService } from '@/services'
import { ElMessage } from 'element-plus'
import { ElMessageBox } from 'element-plus'
import { formatRelativeTime } from '@/utils/dateUtils'
import { useGlobalStore } from '@/store/globalStore'
import CustomPagination from '@/components/CustomPagination.vue'
const globalStore = useGlobalStore()
/**
* 吐槽数据列表
* 仅站长可见/可发
*/
const nonsenseList = ref([])
// 存储字符引用和样式的映射
const charRefs = ref(new Map())
const charStyles = ref(new Map())
// 显示的吐槽内容列表
const displayedNonsenseList = ref([])
// 加载状态
const loading = ref(false)
// 错误状态
const error = ref(false)
// 分页相关
const pageNum = ref(1) // 当前页码
const pageSize = ref(5) // 每页数量
const totalPages = ref(0) // 总页数
// 处理页码变化
const handlePageChange = (newPage) => {
pageNum.value = newPage
loadNonsenseList()
}
// 重试加载
const handleRetry = () => {
// 重置分页参数
pageNum.value = 1
totalPages.value = 0
error.value = false
loadNonsenseList()
}
// 定时器引用
let colorChangeTimer = null
/**
* 加载所有吐槽内容
*/
const loadNonsenseList = async () => {
// 设置加载状态
loading.value = true
error.value = false
try {
const response = await nonsenseService.getNonsenseByStatus({
status: 1,
pageNum: pageNum.value - 1,
pageSize: pageSize.value
})
// console.log(response)
if (response.code === 200) {
nonsenseList.value = response.data
// 计算总页数
totalPages.value = response.totalPages
// 显示当前页的内容
displayedNonsenseList.value = response.data
// 初始化字符样式
// initializeCharStyles()
// 开始颜色变化定时器
console.log(displayedNonsenseList.value)
} else {
ElMessage.error('加载吐槽内容失败')
error.value = true
}
} catch (err) {
console.error('加载吐槽内容失败:', err)
error.value = true
} finally {
// 结束加载状态
loading.value = false
// console.log('加载吐槽内容完成')
}
}
// 编辑吐槽内容
const handleEdit = (item) => {
// 清除更新文章状态
ElMessageBox.prompt('请输入您的疯言疯语:', '发表疯言疯语', {
confirmButtonText: '保存',
cancelButtonText: '取消',
inputValue: item.content,
inputType: 'textarea',
inputRows: 4,
showCancelButton: true
}).then(({ value }) => {
// 保存疯言疯语
updateNonsense(value, item.id)
}).catch(() => {
// 取消操作,静默处理
})
}
// 更新吐槽内容
const updateNonsense = (content, id) => {
if (!content || content.trim() === '') {
ElMessage.warning('内容不能为空')
return
}
// 调用服务更新疯言疯语
nonsenseService.updateNonsense({
id,
content: content.trim(),
time: new Date(),
status: 1
}).then(response => {
if (response.code === 200) {
ElMessage.success('疯言疯语更新成功')
loadNonsenseList()
} else {
ElMessage.error(response.message || '更新失败')
}
}).catch(err => handleErrorResponse(err, '更新失败'))
}
// 删除吐槽内容
const handleDelete = async (id) => {
try {
// 确认删除
const confirm = window.confirm('确定删除吗?')
if (!confirm) return
const response = await nonsenseService.deleteNonsense(id)
if (response.code === 200) {
ElMessage.success('删除成功')
loadNonsenseList()
} else {
ElMessage.error('删除失败')
}
} catch (error) {
console.error('删除失败:', error)
}
}
// 设置字符引用
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-color: rgba(255, 255, 255, 0.85); */
border-radius: 12px;
/* box-shadow: 0 2px 12px rgba(0,0,0,0.06); */
transition: box-shadow 0.3s ease;
}
/* 吐槽页面主容器悬浮效果 */
.nonsense-container:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
/* 吐槽头部样式 */
.nonsense-header {
text-align: center;
margin-bottom: 28px;
}
.nonsense-header h1 {
font-size: 2.2rem;
margin-bottom: 8px;
color: #409eff;
letter-spacing: 2px;
}
.nonsense-description {
color: #888;
font-size: 1rem;
margin-bottom: 0;
}
/* 吐槽列表样式 */
.nonsense-list {
display: flex;
flex-direction: column;
gap: 18px;
}
/* 吐槽项样式 */
.nonsense-item {
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);
position: relative;
transition: box-shadow 0.2s, transform 0.2s ease;
}
/* 吐槽项悬浮效果 */
.nonsense-item:hover {
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
transform: translateY(-2px);
}
/* 吐槽元信息样式 */
.nonsense-meta-info {
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 13px;
color: #aaa;
margin-bottom: 8px;
text-align: right;
}
.nonsense-meta-info span {
padding: 4px 8px;
margin-right: 8px;
}
/* 吐槽内容样式 */
.nonsense-content {
font-size: 1.08rem;
color: #333;
line-height: 1.8;
word-break: break-all;
font-style: italic;
}
/* 响应式设计 */
@media (max-width: 768px) {
.nonsense-container {
margin: 0;
}
.nonsense-header h1 {
font-size: 1.4rem;
}
.nonsense-content {
font-size: 0.98rem;
line-height: 1.6;
}
/* .nonsense-list {
gap: 12px;
} */
.nonsense-item {
padding: 14px 16px 10px 16px;
}
}
</style>