feat: 实现文章状态管理及分类标签展示功能

新增文章状态管理功能,支持草稿、已发表和已删除状态的显示与切换
重构分类和标签展示模块,添加点击跳转功能
优化文章列表页面,增加状态筛选和分页功能
完善疯言疯语模块,支持编辑和删除操作
修复路由跳转和页面刷新问题
This commit is contained in:
qingfeng1121
2025-11-08 11:16:15 +08:00
parent ad893b3e5c
commit 309aeaedc1
15 changed files with 840 additions and 325 deletions

View File

@@ -20,7 +20,7 @@
</el-icon> </el-icon>
<span>首页</span> <span>首页</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/article-list"> <el-menu-item index="/articlelist">
<el-icon> <el-icon>
</el-icon> </el-icon>
<span>目录</span> <span>目录</span>
@@ -51,13 +51,13 @@
</a> </a>
</div> </div>
<div> <div>
<a href="#" class="stat-link"> <a href="#" class="stat-link" @click.prevent="showCategories">
<span class="site-state-item-count">{{ categoryCount }}</span> <span class="site-state-item-count">{{ categoryCount }}</span>
<span class="site-state-item-name">分类</span> <span class="site-state-item-name">分类</span>
</a> </a>
</div> </div>
<div> <div>
<a href="#" class="stat-link"> <a href="#" class="stat-link" @click.prevent="showAttributes">
<span class="site-state-item-count">{{ AttributeCount }}</span> <span class="site-state-item-count">{{ AttributeCount }}</span>
<span class="site-state-item-name">标签</span> <span class="site-state-item-name">标签</span>
</a> </a>
@@ -68,6 +68,50 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<!-- 分类蒙板组件 -->
<Transition name="modal">
<div v-if="showCategoryModal" class="category-modal" @click.self="closeCategoryModal">
<div class="category-modal-content">
<div class="category-modal-header">
<h3>所有分类</h3>
<button class="category-modal-close" @click="closeCategoryModal">×</button>
</div>
<div class="category-modal-body">
<button
v-for="category in categories"
:key="category.typeid"
class="category-button"
@click="handleCategoryClick(category)"
>
{{ category.typename }} <span class="category-button-count">({{ category.count || 0 }})</span>
</button>
</div>
</div>
</div>
</Transition>
<!-- 标签蒙板组件 -->
<Transition name="modal">
<div v-if="showAttributeModal" class="category-modal" @click.self="closeAttributeModal">
<div class="category-modal-content">
<div class="category-modal-header">
<h3>所有标签</h3>
<button class="category-modal-close" @click="closeAttributeModal">×</button>
</div>
<div class="category-modal-body">
<button
v-for="attribute in attributes"
:key="attribute.attributeid"
class="category-button"
@click="handleAttributeClick(attribute)"
>
{{ attribute.attributename }} <span class="category-button-count">({{ attribute.count || 0 }})</span>
</button>
</div>
</div>
</div>
</Transition>
</div> </div>
</template> </template>
@@ -75,6 +119,8 @@
import { reactive, ref, onMounted, onUnmounted } from 'vue' import { reactive, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { articleService, categoryService, categoryAttributeService } from "@/services"; import { articleService, categoryService, categoryAttributeService } from "@/services";
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 当前激活菜单 // 当前激活菜单
const activeIndex = ref('/:type') const activeIndex = ref('/:type')
@@ -86,6 +132,13 @@ const state = reactive({
sizeList: ['small', '', 'large'] as const, sizeList: ['small', '', 'large'] as const,
}) })
// 分类相关状态
const categories = ref<any[]>([])
const showCategoryModal = ref(false)
// 标签相关状态
const attributes = ref<any[]>([])
const showAttributeModal = ref(false)
// 处理菜单选择跳转 // 处理菜单选择跳转
const handleSelect = (key: string) => { const handleSelect = (key: string) => {
@@ -109,40 +162,110 @@ const fetchArticleCount = async () => {
try { try {
const response = await articleService.getAllArticles(); const response = await articleService.getAllArticles();
articleCount.value = response.data?.length || 0 articleCount.value = response.data?.length || 0
// 这里应该调用API获取实际的文章数量
// 暂时设置为模拟数据
} catch (error) { } catch (error) {
console.error('获取文章数量失败:', error) console.error('获取文章数量失败:', error)
articleCount.value = 0 articleCount.value = 0
} }
} }
// 获取分类数 // 获取分类数
const fetchCategoryCount = async () => { const fetchCategories = async () => {
try { try {
const response = await categoryService.getAllCategories(); const response = await categoryService.getAllCategories();
categoryCount.value = response.data?.length || 0 // 如果API返回的数据结构不包含count属性我们可以模拟一些数据
// 这里应该调用API获取实际的分类数量 categories.value = response.data?.map((category: any) => ({
// 暂时设置为模拟数据 ...category,
count: 0
})) || [];
categories.value.forEach(async (category: any) => {
const attributeResponse = await categoryAttributeService.getAttributesByCategory(category.typeid)
if (attributeResponse.data?.length) {
category.count = attributeResponse.data?.length || 0
}
})
categoryCount.value = categories.value.length
} catch (error) { } catch (error) {
console.error('获取分类数量失败:', error)
categoryCount.value = 0 console.error('获取分类失败:', error)
// 如果API调用失败使用模拟数据
categories.value = [
];
categoryCount.value = categories.value.length
} }
} }
// 获取标签数 // 获取标签数
const fetchAttributeCount = async () => { const fetchAttributes = async () => {
try { try {
const response = await categoryAttributeService.getAllAttributes(); const response = await categoryAttributeService.getAllAttributes();
AttributeCount.value = response.data?.length || 0 // 如果API返回的数据结构不包含count属性我们可以模拟一些数据
// 这里应该调用API获取实际的标签数量 attributes.value = response.data?.map((attribute: any) => ({
// 暂时设置为模拟数据 ...attribute,
count: 0
})) || [];
attributes.value.forEach(async (attribute: any) => {
const articleResponse = await articleService.getArticlesByAttributeId(attribute.attributeid)
if (articleResponse.data?.length) {
attribute.count = articleResponse.data?.length || 0
}
})
AttributeCount.value = attributes.value.length
} catch (error) { } catch (error) {
console.error('获取标签数量失败:', error) console.error('获取标签失败:', error)
AttributeCount.value = 0 // 如果API调用失败使用模拟数据
attributes.value = [
];
AttributeCount.value = attributes.value.length
} }
} }
// 显示分类蒙板
const showCategories = () => {
showCategoryModal.value = true
}
// 关闭分类蒙板
const closeCategoryModal = () => {
showCategoryModal.value = false
}
// 处理分类点击
const handleCategoryClick = (category: any) => {
// 这里可以根据实际需求跳转到对应分类的文章列表页
console.log('点击了分类:', category.typename)
// 示例router.push(`/article-list?category=${category.typeid}`)
closeCategoryModal()
}
// 显示标签蒙板
const showAttributes = () => {
showAttributeModal.value = true
}
// 关闭标签蒙板
const closeAttributeModal = () => {
showAttributeModal.value = false
}
// 处理标签点击
const handleAttributeClick = (attribute: any) => {
// 重置全局属性状态
globalStore.removeValue('attribute')
globalStore.setValue('attribute', {
id: attribute.attributeid,
name: attribute.typename
})
console.log(attribute)
router.push({
path: '/home/aericletype',
})
closeAttributeModal()
}
// 控制底部模块吸顶效果 // 控制底部模块吸顶效果
const scrollY = ref(false) const scrollY = ref(false)
const handleScroll = () => { const handleScroll = () => {
@@ -153,8 +276,8 @@ const handleScroll = () => {
onMounted(() => { onMounted(() => {
window.addEventListener('scroll', handleScroll) window.addEventListener('scroll', handleScroll)
fetchArticleCount() // 组件挂载时获取文章数量 fetchArticleCount() // 组件挂载时获取文章数量
fetchCategoryCount() // 组件挂载时获取分类数 fetchCategories() // 组件挂载时获取分类数
fetchAttributeCount() // 组件挂载时获取标签数 fetchAttributes() // 组件挂载时获取标签数
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -190,10 +313,17 @@ onUnmounted(() => {
/* 内容区域样式 */ /* 内容区域样式 */
#cont { #cont {
padding:0 0 10px 0;
border-radius: 10px; border-radius: 10px;
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.9);
/* 白色半透明背景 */ /* 白色半透明背景 */
} }
#cont .cont1{
margin-bottom: 5px;
}
#cont .cont2{
margin-bottom: 0px;
}
.cont1 { .cont1 {
text-align: center; text-align: center;
@@ -213,18 +343,19 @@ onUnmounted(() => {
} }
/* 菜单样式 */ /* 菜单样式 */
.cont2 {
margin-top: 20px;
}
.cont2 .el-menu-vertical-demo { .cont2 .el-menu-vertical-demo {
display: block; display: block;
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
/* 白色半透明背景 */ /* 白色半透明背景 */
} }
.cont2 .el-menu-vertical-demo li {
font-size: 14px;
height: 35px;
}
.cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3) { .cont2 .el-menu-vertical-demo .el-menu-item:nth-child(3) {
border-radius: 0 0 10px 10px; /* border-radius: 0 0 10px 10px; */
/* margin-bottom: 10px; */
} }
.cont2 .el-menu-vertical-demo .el-menu-item:hover { .cont2 .el-menu-vertical-demo .el-menu-item:hover {
@@ -376,4 +507,141 @@ onUnmounted(() => {
transform: translateY(0); transform: translateY(0);
} }
} }
/* 分类蒙板样式 */
.category-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.category-modal-content {
background-color: white;
border-radius: 10px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.category-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.category-modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.category-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
}
.category-modal-close:hover {
background-color: #f5f5f5;
color: #333;
}
.category-modal-body {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
max-height: 60vh;
overflow-y: auto;
}
.category-button {
background-color: rgba(102, 161, 216, 0.1);
border: 1px solid rgba(102, 161, 216, 0.3);
border-radius: 6px;
padding: 10px 15px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
color: #333;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.category-button:hover {
background-color: rgba(102, 161, 216, 0.3);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(102, 161, 216, 0.2);
}
.category-button-count {
font-size: 12px;
color: #66a1d8;
font-weight: 500;
}
/* 蒙板动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-active .category-modal-content,
.modal-leave-active .category-modal-content {
transition: transform 0.3s ease;
}
.modal-enter-from .category-modal-content {
transform: scale(0.9);
}
.modal-leave-to .category-modal-content {
transform: scale(0.9);
}
/* 滚动条样式 */
.category-modal-body::-webkit-scrollbar {
width: 6px;
}
.category-modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.category-modal-body::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.category-modal-body::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style> </style>

View File

@@ -12,7 +12,7 @@
<el-menu-item index="home"> <el-menu-item index="home">
首页 首页
</el-menu-item> </el-menu-item>
<el-menu-item index="article-list"> <el-menu-item index="articlelist">
目录 目录
</el-menu-item> </el-menu-item>
<el-menu-item index="nonsense"> <el-menu-item index="nonsense">
@@ -210,7 +210,7 @@ const performSearch = () => {
* 根据路由路径设置页面状态 * 根据路由路径设置页面状态
*/ */
const updatePageState = () => { const updatePageState = () => {
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) { if (rpsliturl[1] == localhome && rpsliturl[2] == undefined) {
classhero.value = false; classhero.value = false;
} else { } else {
classhero.value = true; classhero.value = true;
@@ -225,7 +225,6 @@ const setActiveIndex = (path: string) => {
globalStore.setValue('localpath', { globalStore.setValue('localpath', {
name: path name: path
}) })
// console.log('设置激活索引:', path);
if (path === 'message') { if (path === 'message') {
globalStore.removeValue('articleInfo') globalStore.removeValue('articleInfo')
} }

View File

@@ -39,7 +39,9 @@ const isNonsenseModalVisible = ref(false)
const nonsenseContent = ref('') const nonsenseContent = ref('')
// 基础按钮配置 // 基础按钮配置
const baseButtons = [ const baseButtons = [
{ id: 'logout', label: '登出', icon: 'icon-logout' } { id: 'logout', label: '登出', icon: 'icon-logout' },
{ id: 'reload', label: '刷新', icon: 'icon-reload' }
] ]
// 页面特定按钮配置 // 页面特定按钮配置
@@ -54,8 +56,9 @@ const pageButtons = {
// 首页按钮 // 首页按钮
home: [ home: [
{ id: 'create-article', label: '新建', icon: 'icon-add-article' }, { id: 'create-article', label: '新建', icon: 'icon-add-article' },
{ id: 'published-articles', label: '已发表', icon: 'icon-new-tag' }, { id: 'del-articles', label: '已删除', icon: 'icon-new-tag' },
{ id: 'unpublished-articles', label: '未发表', icon: 'icon-new-tag' }, { id: 'unpublished-articles', label: '未发表', icon: 'icon-new-tag' },
{ id: 'published-articles', label: '已发表', icon: 'icon-new-tag' },
...baseButtons ...baseButtons
], ],
@@ -66,7 +69,7 @@ const pageButtons = {
], ],
// 分类页面按钮 // 分类页面按钮
category: [ articlelist: [
{ id: 'create-category', label: '新建', icon: 'icon-create-category' }, { id: 'create-category', label: '新建', icon: 'icon-create-category' },
...baseButtons ...baseButtons
], ],
@@ -86,6 +89,21 @@ const pageButtons = {
// 默认按钮 // 默认按钮
default: baseButtons default: baseButtons
} }
// 根据status状态获取按钮配置
const getButtonsByStatus = (status) => {
globalStore.removeValue('articlestatus')
globalStore.setValue('articlestatus', {
status: status
})
//跳转文章列表页面,添加时间戳参数确保页面刷新
try {
// 添加时间戳作为查询参数,确保页面强制刷新
const timestamp = new Date().getTime();
router.push({ path: `/home/aericlestatus`, query: { t: timestamp } })
} catch (error) {
console.error('页面跳转失败:', error)
}
}
// 根据当前页面返回对应的按钮配置 // 根据当前页面返回对应的按钮配置
const isbuttonsave = () => { const isbuttonsave = () => {
@@ -151,10 +169,51 @@ const handleErrorResponse = (error, defaultMessage = '操作失败') => {
console.error('操作失败:', error) console.error('操作失败:', error)
ElMessage.error(error.message || defaultMessage) ElMessage.error(error.message || defaultMessage)
} }
// articlelist新建
const createCategory = () => {
ElMessageBox.prompt('请输入分类名称:', '新建分类', {
confirmButtonText: '保存',
cancelButtonText: '取消',
inputPlaceholder: '请输入分类名称',
inputType: 'text',
showCancelButton: true,
// 输入验证
inputValidator: (value) => {
if (!value || value.trim() === '') {
return '分类名称不能为空';
}
if (value.trim().length < 2) {
return '分类名称至少需要2个字符';
}
return true;
}
}).then(({ value }) => {
// 保存分类
saveCategory(value.trim());
}).catch(() => {
// 取消操作,静默处理
});
};
// 保存分类
const saveCategory = (typename) => {
categoryService.saveCategory({
typename: typename
}).then(response => {
if (response.code === 200) {
ElMessage.success('分类创建成功');
// 刷新页面以显示新分类
const timestamp = new Date().getTime();
router.push({ path: `/home/aericlelist`, query: { t: timestamp } });
} else {
ElMessage.error(response.message || '创建分类失败');
}
}).catch(err => handleErrorResponse(err, '创建分类失败'));
};
// 删除文章方法 // 删除文章方法
const deleteArticle = () => { const deleteArticle = () => {
if (!route.query.id) { const articleId = globalStore.getValue('articleInfo')?.id
if (!articleId) {
ElMessage.warning('缺少文章ID参数') ElMessage.warning('缺少文章ID参数')
return return
} }
@@ -165,7 +224,7 @@ const deleteArticle = () => {
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
// 调用删除文章接口 // 调用删除文章接口
articleService.deleteArticle(Number(route.query.id)) articleService.deleteArticle(Number(articleId))
.then(response => { .then(response => {
if (response.code === 200) { if (response.code === 200) {
ElMessage.success('文章删除成功') ElMessage.success('文章删除成功')
@@ -180,6 +239,25 @@ const deleteArticle = () => {
// 取消删除,静默处理 // 取消删除,静默处理
}) })
} }
// 修改文章方法
const updateArticle = () => {
const articleId = globalStore.getValue('articleInfo')
if (!articleId) {
ElMessage.warning('缺少文章参数')
return
}
// 确认修改
ElMessageBox.confirm('确定修改该文章吗?', '修改确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
globalStore.setValue('updatearticle', articleId)
router.push({ path: '/articlesave' })
}).catch(() => {
// 取消修改,静默处理
})
}
// 登出方法 // 登出方法
const logout = () => { const logout = () => {
@@ -200,7 +278,11 @@ const logout = () => {
}) })
.catch(err => handleErrorResponse(err, '登出失败')) .catch(err => handleErrorResponse(err, '登出失败'))
} }
// 刷新页面
const reloadPage = () => {
globalStore.clearAll()
}
// 处理按钮点击事件 // 处理按钮点击事件
const handleButtonClick = (button) => { const handleButtonClick = (button) => {
console.log('点击了按钮:', button.id, button.label) console.log('点击了按钮:', button.id, button.label)
@@ -209,11 +291,12 @@ const handleButtonClick = (button) => {
switch (button.id) { switch (button.id) {
// 新增操作 // 新增操作
case 'create-article': case 'create-article':
// router.push({ path: '/articlesave' }) // 清除更新文章状态
router.push({ path: '/articlesave' })
break break
case 'create-category': case 'create-category':
// router.push({ path: '/categorysave' }) createCategory()
break break
case 'create-tag': case 'create-tag':
@@ -227,27 +310,25 @@ const handleButtonClick = (button) => {
// 修改操作 // 修改操作
case 'edit-article': case 'edit-article':
if (route.query.id) { updateArticle()
globalStore.setValue('localpath', { name: 'article', id: Number(route.query.id) })
router.push({ path: '/articlesave', query: { id: route.query.id } })
} else {
ElMessage.warning('缺少文章ID参数')
}
break break
// 删除操作 // 删除操作
case 'delete-article': case 'delete-article':
deleteArticle(); deleteArticle();
break break
break
// 查看操作 // 查看操作
case 'published-articles': case 'del-articles':
router.push({ path: '/home', query: { status: 1 } }) getButtonsByStatus(2)
break break
case 'unpublished-articles': case 'unpublished-articles':
router.push({ path: '/home', query: { status: 0 } }) getButtonsByStatus(0)
break
case 'published-articles':
getButtonsByStatus(1)
break break
case 'view-articles': case 'view-articles':
@@ -259,6 +340,10 @@ const handleButtonClick = (button) => {
logout(); logout();
break break
case 'reload':
reloadPage()
break
default: default:
console.warn('未处理的按钮类型:', button.id, button.label) console.warn('未处理的按钮类型:', button.id, button.label)
ElMessage.info(`功能 ${button.label} 暂未实现`) ElMessage.info(`功能 ${button.label} 暂未实现`)

View File

@@ -26,16 +26,24 @@ const routes = [
children: [ children: [
{ {
path: 'aericletype', path: 'aericletype',
name: 'homeByType' name: 'homeByType',
component: HomePage
}, },
{ {
path: 'aericletitle', path: 'aericletitle',
name: 'homeByTitle' name: 'homeByTitle',
component: HomePage
},
{
path: 'aericlestatus',
name: 'homeByStatus',
component: HomePage
} }
] ]
}, },
{ {
path: '/article-list', path: '/articlelist',
name: 'articleList', name: 'articleList',
component: ArticleList, component: ArticleList,
meta: { meta: {

View File

@@ -13,7 +13,22 @@ class ArticleService {
getAllArticles(params = {}) { getAllArticles(params = {}) {
return api.get('/articles/published', { params }) return api.get('/articles/published', { params })
} }
/**
* 根据状态获取文章列表
* @param {number} status - 文章状态0未发表 1已发表 2已删除
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getArticlesByStatus(status) {
return api.get(`/articles/status/${status}`)
}
/**
* 获取所有文章列表(包含已删除)
* @param {import('../types').PaginationParams} params - 查询参数
* @returns {Promise<import('../types').ApiResponse<import('../types').Article[]>>}
*/
getAllArticlesWithDeleted(params = {}) {
return api.get('/articles', { params })
}
/** /**
* 根据ID获取文章详情 * 根据ID获取文章详情
* @param {number} articleid - 文章ID * @param {number} articleid - 文章ID

View File

@@ -3,31 +3,39 @@ import apiService from './apiService'
class NonsenseService { class NonsenseService {
/** /**
* 获取所有随机内容 * 获取所有疯言疯语内容
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
*/ */
getAllNonsense() { getAllNonsense() {
return apiService.get('/nonsense') return apiService.get('/nonsense')
} }
/** /**
* 保存随机内容 * 根据状态获取疯言疯语内容
* @param {import('../types').Nonsense} nonsense - 随机内容对象 * @param {number} status - 状态值(1:已发表, 0:草稿)
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
*/
getNonsenseByStatus(status){
return apiService.get(`/nonsense/status/${status}`)
}
/**
* 保存疯言疯语内容
* @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
*/ */
saveNonsense(nonsense){ saveNonsense(nonsense){
return apiService.post('/nonsense', nonsense) return apiService.post('/nonsense', nonsense)
} }
/** /**
* 删除随机内容 * 删除疯言疯语内容
* @param {number} id - 随机内容ID * @param {number} id - 疯言疯语内容ID
* @returns {Promise<import('../types').ApiResponse<boolean>>} * @returns {Promise<import('../types').ApiResponse<boolean>>}
*/ */
deleteNonsense(id){ deleteNonsense(id){
return apiService.delete(`/nonsense/${id}`) return apiService.delete(`/nonsense/${id}`)
} }
/** /**
* 更新随机内容 * 更新疯言疯语内容
* @param {import('../types').Nonsense} nonsense - 随机内容对象 * @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>} * @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
*/ */
updateNonsense(nonsense){ updateNonsense(nonsense){

View File

@@ -129,6 +129,90 @@ p {
font-weight: 300; font-weight: 300;
line-height: 1.7; line-height: 1.7;
} }
/* 编辑按钮样式 */
.edit-button{
cursor: pointer;
transition: color 0.3s ease;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.edit-button:hover {
color: rgb(247, 243, 2);
background-color: rgba(231, 205, 60, 0.3);
}
/* 删除按钮样式 */
.delete-button {
cursor: pointer;
transition: color 0.3s ease;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.delete-button:hover {
color: rgb(231, 76, 60);
background-color: rgba(231, 76, 60, 0.1);
}
/* 文章span 样式 */
/* 发布日期样式 */
.article-publish-date {
font-weight: 500;
color: #95a5a6;
}
/* 阅读数量样式 */
.article-views-count {
display: flex;
align-items: center;
color: #95a5a6;
}
.article-views-count::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 分类标签样式 */
.article-category-badge {
display: flex;
align-items: center;
padding: 2px 10px;
background-color: rgba(52, 152, 219, 0.1);
color: #3498db;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
/* 点赞数量样式 */
.article-likes-count {
display: flex;
align-items: center;
color: #95a5a6;
}
.article-likes-count::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 评论数量样式 */
.article-comments-count {
display: flex;
align-items: center;
color: #95a5a6;
}
.article-comments-count::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 顶部导航栏样式 */ /* 顶部导航栏样式 */
.elrow-top { .elrow-top {

View File

@@ -14,6 +14,7 @@ export interface Article {
updatedAt: string updatedAt: string
viewCount?: number viewCount?: number
likes?: number likes?: number
commentCount?: number
status?: number status?: number
markdownscontent: string markdownscontent: string
} }
@@ -89,6 +90,7 @@ export interface CategoryDto {
* 分类属性接口 * 分类属性接口
*/ */
export interface CategoryAttribute { export interface CategoryAttribute {
map(arg0: (item: any) => any): unknown
attributeid: number attributeid: number
categoryid: number categoryid: number
attributename: string attributename: string
@@ -127,7 +129,23 @@ export interface UserDto {
phone: string phone: string
role?: number role?: number
} }
/**
* 疯言疯语类型接口
*/
export interface Nonsense {
nonsenseid: number
content: string
status?: number
time: string
}
/**
* 疯言疯语DTO接口
*/
export interface NonsenseDto {
content: string
status?: number
time?: string
}
/** /**
* API响应接口 * API响应接口
*/ */

View File

@@ -15,11 +15,11 @@
好像真被一阵风吹散了似的 好像真被一阵风吹散了似的
哈哈哈哈哈我还蛮喜欢这个外号的 哈哈哈哈哈我还蛮喜欢这个外号的
<br></br> <br></br>
&nbsp; &nbsp; 后来有一天不知道怎么了精神状态不是很好感觉要控制不住要发疯突然这疯就像钉子一样扎在我脑海里哈哈哈哈于是就干脆改名叫清疯清风清疯念起来几乎没差 &nbsp; &nbsp; 有段时间不知道怎么了精神状态不是很好好想发疯突然这疯就像钉子一样扎在我脑海里哈哈哈哈于是就干脆改名叫清疯清风清疯念起来几乎没差
但内核却从一种理想的淡然切换成了真实的带点毛边的鲜活悠悠清风荡我心只是如今这阵清风熬成了清疯终于在我心里刮起一场疯疯啦 但内核却从一种理想的淡然切换成了真实的带点毛边的鲜活悠悠清风荡我心只是如今这阵清风熬成了清疯终于在我心里刮起一场疯疯啦
<br></br> <br></br>
&nbsp; &nbsp; 又过些日子玩新游戏要起名正盯着输入框发呆清疯两个字在脑海里冒出来疯癫好像是疯但也没完全颠嘛那种在理智边界试探却绝不越线的微妙感一下对味了干脆就叫清疯不颠 &nbsp; &nbsp; 又过些日子玩新游戏要起名正盯着输入框里的清疯发呆 两个字在脑海里冒出来疯癫好像是疯但也没完全颠嘛那种在理智边界试探却绝不越线的微妙感一下对味了干脆就叫清疯不颠
名字一出自己先乐了这不就是我吗表面疯癫内心门儿清简直是我们这代人精神状态的绝佳注脚 名字一出自己先乐了
<br></br> <br></br>
哈哈哈哈哈哈俗话说天才在左疯子在右在我这儿大概是左脑负责疯右脑负责颠两个家伙吵吵闹闹反而让我在这个世界里自得其乐 哈哈哈哈哈哈俗话说天才在左疯子在右在我这儿大概是左脑负责疯右脑负责颠两个家伙吵吵闹闹反而让我在这个世界里自得其乐
</h5> </h5>
@@ -28,7 +28,7 @@
<div class="about-personal-intro"> <div class="about-personal-intro">
<h4>疯言疯语</h4> <h4>疯言疯语</h4>
<h5> <h5>
&nbsp; &nbsp; 我并没有网站的开发经验这是我的第一个项目所以我并不清楚该如何去写这个页面就干脆当一个我发疯的地方吧哈哈哈哈哈 所以你有看到彩色的小鹿吗 &nbsp; &nbsp; 我并没有网站的开发经验这是我的第一个项目我想设计一下独属于清疯的页面可是我并不清楚该如何去写这个页面就干脆当一个我发疯的地方吧哈哈哈哈哈 所以你有看到彩色的小鹿吗
</h5> </h5>
</div> </div>
@@ -96,7 +96,7 @@
<div class="about-contact-section"> <div class="about-contact-section">
<h4>联系方式</h4> <h4>联系方式</h4>
<p>如果你有任何问题或建议欢迎随时联系我</p> <p>如果你有任何问题或建议欢迎随时联系我我文笔真的很烂QAQ</p>
<div class="contact-options"> <div class="contact-options">
<el-button type="primary" plain @click="goToMessageBoard">留言板</el-button> <el-button type="primary" plain @click="goToMessageBoard">留言板</el-button>
</div> </div>

View File

@@ -6,7 +6,7 @@
<!-- 加载状态 --> <!-- 加载状态 -->
<div v-if="loading" class="loading-state-container"> <div v-if="loading" class="loading-state-container">
<el-skeleton :count="5" /> <el-skeleton :count="1" />
</div> </div>
<!-- 错误状态 --> <!-- 错误状态 -->
@@ -14,20 +14,19 @@
<el-alert :title="error" type="error" show-icon /> <el-alert :title="error" type="error" show-icon />
<el-button type="primary" @click="fetchCategories">重新加载</el-button> <el-button type="primary" @click="fetchCategories">重新加载</el-button>
</div> </div>
<!-- 分类列表 --> <!-- 分类列表 -->
<div v-else-if="categories.length > 0" class="article-content" id="category-list"> <div v-else-if="categories.length > 0" class="article-content" id="category-list">
<p><strong></strong></p> <p><strong></strong></p>
<div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下点击跳转</span></strong></div> <div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下点击跳转</span> </strong></div>
<div v-for="categoryGroup in categories" :key="categoryGroup.typeid" <div v-for="categoryGroup in categories" :key="categoryGroup.typeid" class="category-group-container">
class="category-group-container"> <div v-if="categoryGroup.attributes.length > 0 && categoryGroup.attributes.some(cat => cat.articles && cat.articles.length > 0)">
<div v-if="categoryGroup.attributes.length > 0">
<h2 id="header-id-1">{{ categoryGroup.typename }}</h2> <h2 id="header-id-1">{{ categoryGroup.typename }}</h2>
<span class="badge badge-primary"> {{ categoryGroup.attributes.length }} </span> <!-- 计算该分类组中实际有文章的属性数量 -->
<span class="badge badge-primary"> {{ categoryGroup.attributes.reduce((total, cat) => total + (cat.articles && cat.articles.length ? cat.articles.length : 0), 0) }} </span>
<ul class="category-item-list"> <ul class="category-item-list">
<li v-for="category in categoryGroup.attributes" :key="category.typeid"> <li v-for="category in categoryGroup.attributes" :key="category.attributeid" >
&nbsp;&nbsp;<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename &nbsp;&nbsp;<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename
}}</kbd></a> }}</kbd></a>
&nbsp; ({{ category.articles.length }}) &nbsp; ({{ category.articles.length }})
</li> </li>
</ul> </ul>
@@ -66,90 +65,61 @@ const error = ref('')
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
loading.value = true; loading.value = true;
error.value = ''; const res = await categoryService.getAllCategories();
let processedCategories: any[] = [];
const res = await categoryService.getAllCategories(); if (res.code === 200) {
processedCategories = res.data.map(item => ({
if (res.data && res.data.length > 0) {
// 创建处理后的分类数组
const processedCategories = res.data.map(item => ({
...item, ...item,
attributes: [] // 使用更清晰的命名 attributes: [] // 使用更清晰的命名
})); }));
// 并行处理所有分类 // 使用Promise.all等待所有异步操作完成
await Promise.all( await Promise.all(
processedCategories.map(async (category) => { processedCategories.map(async category => {
try { const attributes = await categoryAttributeService.getAttributesByCategory(category.typeid);
if (await categoryAttributeService.checkAttributeExists(category.typeid, category.typename || '')) { if (attributes.code === 200 && Array.isArray(attributes.data)) {
// 获取分类的所有属性 const processedAttributes = await Promise.all(
const attributesRes = await categoryAttributeService.getAttributeById(category.typeid); attributes.data.map(async item => {
// 存储属性数据 const articleItem = {
if (attributesRes.data) { ...item,
category.attributes = Array.isArray(attributesRes.data) ? attributesRes.data : [attributesRes.data]; articles: []
} };
} const articlesRes = await articleService.getArticlesByAttributeId(item.attributeid);
} catch (err) { if(articlesRes.code === 200 && Array.isArray(articlesRes.data)){
// console.error(`处理分类失败 (分类ID: ${category.typeid}):`, err); articleItem.articles = articlesRes.data;
}
return articleItem;
})
);
category.attributes = processedAttributes;
} }
}) })
); );
await getCategoryAttributes(processedCategories);
console.log('获取分类列表成功:', categories.value);
} else {
categories.value = [];
} }
categories.value = processedCategories;
console.log('获取分类列表成功:', categories.value);
} catch (err) { } catch (err) {
error.value = '获取分类列表失败,请稍后重试';
console.error('获取分类列表失败:', err); console.error('获取分类列表失败:', err);
ElMessage.error(error.value); ElMessage.error('获取分类列表失败,请稍后重试');
} finally { }finally{
loading.value = false; loading.value = false;
console.log('分类列表加载完成');
} }
};
// 处理所有分类及其属性的文章数据
const getCategoryAttributes = async (processedCategories: any[]) => {
// 遍历所有分类
for (const category of processedCategories) {
if (category && category.attributes && category.attributes.length > 0) {
console.log(`处理分类: ${category.typename || '未命名分类'}`);
// 并行获取每个属性下的文章
await Promise.all(
category.attributes.map(async (attribute) => {
try {
// 使用正确的方法名获取属性下的文章优先使用typeid
const idToUse = attribute.typeid || attribute.categoryid;
const articlesRes = await articleService.getArticlesByAttributeId(idToUse);
// 处理文章数据
attribute.articles = articlesRes.data && Array.isArray(articlesRes.data) ?
articlesRes.data : [];
} catch (err) {
console.error(`获取属性文章失败 (属性ID: ${attribute.typeid || attribute.categoryid}):`, err);
attribute.articles = [];
}
})
);
}
}
// 更新分类列表
categories.value = processedCategories;
console.log('所有分类属性文章数据处理完成');
} }
/** /**
* 处理分类点击事件 * 处理分类点击事件
* 注意现在实际上使用的是属性ID而不是分类ID * 注意现在实际上使用的是属性ID而不是分类ID
* @param {string | number} attributeId - 属性ID * @param {string | number} attributeId - 属性ID
*/ */
const handleCategoryClick = (attribute: any) => { const handleCategoryClick = (attribute: any) => {
globalStore.removeValue('attribute')
globalStore.setValue('attribute', { globalStore.setValue('attribute', {
id: attribute.typeid || attribute.categoryid, id: attribute.attributeid,
name: attribute.attributename || attribute.typename || '未命名属性', name: attribute.typename
}) })
console.log(attribute) console.log(attribute)
router.push({ router.push({
path: '/home/aericletype', path: '/home/aericletype',
}) })
} }

View File

@@ -19,18 +19,16 @@
<h1 class="article-main-title">{{ article.title }}</h1> <h1 class="article-main-title">{{ article.title }}</h1>
<div class="article-meta-info"> <div class="article-meta-info">
<span class="meta-item"> <span class="article-publish-date">{{ formatRelativeTime(article.createdAt) }}</span>
<i class="el-icon-date"></i> <span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
{{ formatDate(article.createdAt) }} <span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
</span> <span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
<span class="meta-item"> <span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
<i class="el-icon-folder"></i> <div class="article-status-badge-container" v-if="globalStore.Login">
{{ article.categoryName || '未分类' }} <span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
</span> <span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
<span class="meta-item"> <span v-if="article.status === 2" class="article-status-badge badge badge-danger">已删除</span>
<i class="el-icon-view"></i> </div>
{{ article.viewCount || 0 }} 阅读
</span>
</div> </div>
</div> </div>
@@ -85,12 +83,10 @@
// 导入必要的依赖 // 导入必要的依赖
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { articleService } from '@/services'
import { categoryAttributeService } from '@/services'
import { useGlobalStore } from '@/store/globalStore' import { useGlobalStore } from '@/store/globalStore'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { Article } from '@/types' import type { Article } from '@/types'
import { formatDate } from '@/utils/dateUtils' import { formatRelativeTime } from '@/utils/dateUtils'
import messageboard from './messageboard.vue' import messageboard from './messageboard.vue'
import markdownViewer from './markdown.vue' import markdownViewer from './markdown.vue'
// 路由相关 // 路由相关
@@ -122,22 +118,19 @@ const fetchArticleDetail = async () => {
if (response) { if (response) {
article.value = response article.value = response
// 获取并设置分类名称 // 获取并设置分类名称
const categoryResponse = await categoryAttributeService.getAttributeById(Number(article.value.attributeid)) // const categoryResponse = await categoryAttributeService.getAttributeById(Number(article.value.attributeid))
article.value.categoryName = categoryResponse.data.attributename || '未分类' // article.value.categoryName = categoryResponse.data.attributename || '未分类'
// 获取并设置评论量
// 增加文章浏览量 // const commentResponse = await messageService.getMessagesByArticleId(articleId)
try { // article.value.commentCount = commentResponse.data.length || 0
await articleService.incrementArticleViews(Number(articleId)) // 更新浏览量
// 更新前端显示的浏览量 // 更新前端显示的浏览量
if (article.value.viewCount) { if (article.value.viewCount) {
article.value.viewCount++ article.value.viewCount++
} else { } else {
article.value.viewCount = 1 article.value.viewCount = 1
}
} catch (err) {
console.error('增加文章浏览量失败:', err)
// 不阻止主流程
} }
} else { } else {
throw new Error('文章不存在或已被删除') throw new Error('文章不存在或已被删除')
} }
@@ -181,7 +174,7 @@ onMounted(() => {
.article-detail-wrapper { .article-detail-wrapper {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px; border-radius: 12px;
padding: 40px; padding: 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@@ -193,7 +186,7 @@ onMounted(() => {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 40px; padding: 40px;
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
@@ -203,7 +196,7 @@ onMounted(() => {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 60px 40px; padding: 60px 40px;
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px; border-radius: 12px;
text-align: center; text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@@ -218,7 +211,7 @@ onMounted(() => {
max-width: 900px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 80px 40px; padding: 80px 40px;
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.85);
border-radius: 12px; border-radius: 12px;
text-align: center; text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

View File

@@ -14,7 +14,7 @@
</span> </span>
<span class="meta-item status-item"> <span class="meta-item status-item">
<i class="el-icon-document"></i> <i class="el-icon-document"></i>
<el-select v-model="Articleform.status" placeholder="请选择状态" class="meta-select"> <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 v-for="item in statusoptions" :key="item.value" :label="item.label" :value="item.value">
</el-option> </el-option>
</el-select> </el-select>
@@ -45,6 +45,8 @@ import 'md-editor-v3/lib/style.css';
import { categoryService, categoryAttributeService, articleService } from '@/services'; import { categoryService, categoryAttributeService, articleService } from '@/services';
import type { Article } from '@/types/index.ts'; import type { Article } from '@/types/index.ts';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
// 路由 // 路由
import router from '@/router/Router'; import router from '@/router/Router';
const Articleform = ref<Article>({ const Articleform = ref<Article>({
@@ -55,7 +57,8 @@ const Articleform = ref<Article>({
categoryName: '', categoryName: '',
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
markdownscontent: '' markdownscontent: '',
status: 0 // 默认状态为草稿
}) })
// 用于级联选择器的值绑定 // 用于级联选择器的值绑定
@@ -63,16 +66,29 @@ const selectedValues = ref([]);
const categorieoptions = ref([]); const categorieoptions = ref([]);
const statusoptions = ref([ const statusoptions = ref([
{ {
label: '未发布', label: '草稿',
value: '0' value: '0'
}, },
{ {
label: '发布', label: '发布',
value: '1' value: '1'
},
{
label: '删除',
value: '2'
} }
]); ]);
const categories = ref([]); const categories = ref([]);
// 编辑文章
const editArticle = globalStore.getValue('updatearticle')
if (editArticle) {
Articleform.value = {
...editArticle,
// 确保status是字符串格式与statusoptions的value格式匹配
status: editArticle.status !== undefined ? String(editArticle.status) : '0'
}
}
// 初始化加载分类和属性构建级联选择器的options // 初始化加载分类和属性构建级联选择器的options
const loadCategories = async () => { const loadCategories = async () => {
try { try {
@@ -107,7 +123,22 @@ const loadCategories = async () => {
}) })
); );
categorieoptions.value = optionsData; categorieoptions.value = optionsData;
console.log(optionsData);
// 如果是编辑模式且有attributeid设置级联选择器的默认值
if (Articleform.value.articleid !== 0 && Articleform.value.attributeid !== 0) {
// 查找属性所属的分类
for (const category of optionsData) {
const foundAttribute = category.children.find(attr => attr.value === Articleform.value.attributeid.toString());
if (foundAttribute) {
// 设置级联选择器的值:[分类ID, 属性ID]
selectedValues.value = [category.value, foundAttribute.value];
break;
}
}
}
console.log('分类选项:', optionsData);
console.log('选中的值:', selectedValues.value);
} }
} catch (error) { } catch (error) {
console.error('加载分类失败:', error); console.error('加载分类失败:', error);
@@ -138,6 +169,7 @@ const handleSave = (markdown) => {
// 构建请求数据 // 构建请求数据
const articleData = { const articleData = {
articleid: Articleform.value.articleid,
title: Articleform.value.title, title: Articleform.value.title,
content: Articleform.value.content, content: Articleform.value.content,
attributeid: Number(Articleform.value.attributeid), attributeid: Number(Articleform.value.attributeid),
@@ -150,28 +182,37 @@ const handleSave = (markdown) => {
console.log('发送文章数据:', articleData); console.log('发送文章数据:', articleData);
console.log('当前认证token是否存在:', !!localStorage.getItem('token')); console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
// 保存文章 // 根据articleid决定调用创建还是更新接口
articleService.createArticle(articleData) const savePromise = Articleform.value.articleid === 0
? articleService.createArticle(articleData)
: articleService.updateArticle(Articleform.value.articleid, articleData);
savePromise
.then(res => { .then(res => {
console.log('API响应:', res); console.log('API响应:', res);
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('文章保存成功') ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功');
// 重置表单 // 清除全局存储中的article
Articleform.value = { globalStore.removeValue('updatearticle');
articleid: 0, // 重置表单或保留编辑状态
title: '', if (Articleform.value.articleid === 0) {
content: '', // 创建新文章后重置表单
attributeid: 0, Articleform.value = {
categoryName: '', articleid: 0,
createdAt: '', title: '',
updatedAt: '', content: '',
markdownscontent: '' attributeid: 0,
}; categoryName: '',
selectedValues.value = []; createdAt: '',
updatedAt: '',
markdownscontent: ''
};
selectedValues.value = [];
}
// 返回列表页 // 返回列表页
router.push('/home'); router.push('/home');
} else { } else {
ElMessage.error(res.message || '文章保存失败') ElMessage.error(res.message || (Articleform.value.articleid === 0 ? '文章创建失败' : '文章更新失败'));
} }
}) })
.catch(err => { .catch(err => {
@@ -181,17 +222,19 @@ const handleSave = (markdown) => {
console.error('错误状态码:', err.response.status); console.error('错误状态码:', err.response.status);
console.error('错误响应数据:', err.response.data); console.error('错误响应数据:', err.response.data);
const operationType = Articleform.value.articleid === 0 ? '创建' : '更新';
if (err.response.status === 401) { if (err.response.status === 401) {
ElMessage.error('未授权访问,请先登录'); ElMessage.error('未授权访问,请先登录');
} else if (err.response.status === 403) { } else if (err.response.status === 403) {
ElMessage.error('没有权限创建文章,请检查账号权限'); ElMessage.error(`没有权限${operationType}文章,请检查账号权限`);
} else if (err.response.status === 400) { } else if (err.response.status === 400) {
ElMessage.error('数据验证失败: ' + (err.response.data?.message || '请检查输入')); ElMessage.error('数据验证失败: ' + (err.response.data?.message || '请检查输入'));
} else { } else {
ElMessage.error('请求被拒绝,错误代码: ' + err.response.status); ElMessage.error(`请求被拒绝,错误代码: ${err.response.status}`);
} }
} else { } else {
ElMessage.error(err.message || '文章保存失败') ElMessage.error(err.message || (Articleform.value.articleid === 0 ? '文章创建失败' : '文章更新失败'));
} }
}) })
}; };

View File

@@ -20,10 +20,15 @@
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p> <p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
<div class="article-meta-info"> <div class="article-meta-info">
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt || article.createTime) }}</span> <span class="article-publish-date">{{ formatRelativeTime(article.createdAt || article.createTime) }}</span>
<span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</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> <span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
<span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span> <span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
<div class="article-status-badge-container" v-if="globalStore.Login">
<span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
<span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
<span v-if="article.status === 2" class="article-status-badge badge badge-danger">已删除</span>
</div>
</div> </div>
</div> </div>
</transition-group> </transition-group>
@@ -36,7 +41,7 @@
<script setup> <script setup>
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ref, onMounted } from 'vue' import { ref, onMounted, watch } from 'vue'
import { formatDate, formatRelativeTime } from '@/utils/dateUtils' import { formatDate, formatRelativeTime } from '@/utils/dateUtils'
import { formatContentPreview } from '@/utils/stringUtils' import { formatContentPreview } from '@/utils/stringUtils'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@@ -64,20 +69,35 @@ const fetchArticles = async () => {
// 检查URL参数确定获取文章的方式 // 检查URL参数确定获取文章的方式
const pathSegment = route.path.split('/')[2]; const pathSegment = route.path.split('/')[2];
console.log(pathSegment)
// 根据不同路径获取不同文章 // 根据不同路径获取不同文章
if (pathSegment === 'aericletype') { switch (pathSegment) {
// 按属性类型获取文章 case 'aericletype':
const attributeData = globalStore.getValue('attribute') // 按属性类型获取文章
response = await articleService.getArticlesByAttributeId(attributeData.id) const attributeData = globalStore.getValue('attribute')
} else if (pathSegment === 'aericletitle') { response = await articleService.getArticlesByAttributeId(attributeData.id)
// 按标题搜索文章 break
const titleData = globalStore.getValue('articleserarch') case 'aericletitle':
response = await articleService.getArticlesByTitle(titleData.name) // 按标题搜索文章
} else { const titleData = globalStore.getValue('articleserarch')
// 获取所有文章 response = await articleService.getArticlesByTitle(titleData.name)
console.log('获取所有文章列表') break
response = await articleService.getAllArticles() case 'aericlestatus':
// 按状态获取文章
const statusData = globalStore.getValue('articlestatus')
response = await articleService.getArticlesByStatus(statusData.status)
break
default:
// 默认情况下,根据用户权限决定获取方式
if (globalStore.Login) {
// 获取所有文章(包含已删除)
console.log('管理员获取所有文章列表(包含已删除)')
response = await articleService.getAllArticlesWithDeleted()
} else {
// 获取所有文章
console.log('获取所有文章列表')
response = await articleService.getAllArticles()
}
} }
// 为每个文章获取留言数量和分类名称 // 为每个文章获取留言数量和分类名称
@@ -126,7 +146,10 @@ const fetchArticles = async () => {
* @param {Object} article - 文章对象 * @param {Object} article - 文章对象
*/ */
const handleArticleClick = (article) => { const handleArticleClick = (article) => {
// 增加文章浏览量
articleService.incrementArticleViews(article.articleId)
// 清除之前的文章信息
globalStore.removeValue('articleInfo')
// 存储文章信息到全局状态 // 存储文章信息到全局状态
globalStore.setValue('articleInfo', article) globalStore.setValue('articleInfo', article)
@@ -135,12 +158,24 @@ const handleArticleClick = (article) => {
path: '/article', path: '/article',
}) })
} }
//刷新时挂载获取数据
// 组件挂载时获取数据 // 组件挂载时获取数据
onMounted(() => { onMounted(() => {
fetchArticles() fetchArticles()
}) })
// 监听路由变化,确保刷新时也能重新获取数据
watch(
// 监听路由路径和查询参数变化
() => [route.path, route.query],
// 路由变化时触发获取文章列表
() => {
fetchArticles()
console.log('路由变化,重新获取文章列表')
},
{ deep: true }
)
</script> </script>
<style scoped> <style scoped>
@@ -261,69 +296,6 @@ onMounted(() => {
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid rgba(0, 0, 0, 0.05);
} }
/* 发布日期样式 */
.article-publish-date {
font-weight: 500;
color: #95a5a6;
}
/* 阅读数量样式 */
.article-views-count {
display: flex;
align-items: center;
color: #95a5a6;
}
.article-views-count::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 分类标签样式 */
.article-category-badge {
display: flex;
align-items: center;
padding: 2px 10px;
background-color: rgba(52, 152, 219, 0.1);
color: #3498db;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.article-category-badge::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 点赞数量样式 */
.article-likes-count {
display: flex;
align-items: center;
color: #95a5a6;
}
.article-likes-count::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 评论数量样式 */
.article-comments-count {
display: flex;
align-items: center;
color: #95a5a6;
}
.article-comments-count::before {
content: '|';
margin-right: 12px;
color: #e0e0e0;
}
/* 空状态容器样式 */ /* 空状态容器样式 */
.empty-state-container { .empty-state-container {
text-align: center; text-align: center;

View File

@@ -701,19 +701,7 @@ const handleDelete = async (msg) => {
background-color: rgba(64, 158, 255, 0.1); background-color: rgba(64, 158, 255, 0.1);
} }
// 删除按钮
.delete-button {
cursor: pointer;
transition: color 0.3s ease;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.delete-button:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
/* 回复列表容器 */ /* 回复列表容器 */
.reply-list-container { .reply-list-container {

View File

@@ -1,21 +1,20 @@
<template> <template>
<div class="nonsense-container"> <div class="nonsense-container">
<div class="nonsense-list"> <div class="nonsense-list">
<div <div class="nonsense-item" v-for="item in nonsenseList" :key="item.id">
class="nonsense-item"
v-for="item in nonsenseList"
:key="item.id"
>
<div class="nonsense-meta-info"> <div class="nonsense-meta-info">
<span class="nonsense-time">{{ formatRelativeTime(item.time) }}</span> <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>
<div class="nonsense-content"> <div class="nonsense-content">
<span <span v-for="(char, index) in item.content.split('')" :key="index" :ref="el => setCharRef(el, item.id, index)"
v-for="(char, index) in item.content.split('')" :style="getCharStyle(item.id, index)">{{ char }}</span>
:key="index"
:ref="el => setCharRef(el, item.id, index)"
:style="getCharStyle(item.id, index)"
>{{ char }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -24,9 +23,11 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
import {nonsenseService } from '@/services' import { nonsenseService } from '@/services'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { formatRelativeTime} from '@/utils/dateUtils' import { formatRelativeTime } from '@/utils/dateUtils'
import { useGlobalStore } from '@/store/globalStore'
const globalStore = useGlobalStore()
/** /**
* 吐槽数据列表 * 吐槽数据列表
* 仅站长可见/可发 * 仅站长可见/可发
@@ -45,10 +46,10 @@ let colorChangeTimer = null
*/ */
const loadNonsenseList = async () => { const loadNonsenseList = async () => {
try { try {
const response = await nonsenseService.getAllNonsense() const response = await nonsenseService.getNonsenseByStatus(1)
if (response.code === 200) { if (response.code === 200) {
nonsenseList.value = response.data nonsenseList.value = response.data
}else{ } else {
ElMessage.error('加载吐槽内容失败') ElMessage.error('加载吐槽内容失败')
} }
} catch (error) { } catch (error) {
@@ -57,7 +58,62 @@ const loadNonsenseList = async () => {
console.log('加载吐槽内容完成') 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) => { const setCharRef = (el, itemId, index) => {
if (el) { if (el) {
@@ -82,12 +138,12 @@ const getRandomColor = () => {
const randomChangeColors = () => { const randomChangeColors = () => {
const keys = Array.from(charRefs.value.keys()) const keys = Array.from(charRefs.value.keys())
if (keys.length === 0) return if (keys.length === 0) return
// 随机选择20-30%的字符改变颜色 // 随机选择20-30%的字符改变颜色
const countToChange = Math.floor(keys.length * (Math.random() * 0.1 + 0.2)) const countToChange = Math.floor(keys.length * (Math.random() * 0.1 + 0.2))
const shuffledKeys = [...keys].sort(() => 0.5 - Math.random()) const shuffledKeys = [...keys].sort(() => 0.5 - Math.random())
const selectedKeys = shuffledKeys.slice(0, countToChange) const selectedKeys = shuffledKeys.slice(0, countToChange)
// 创建信号故障效果 // 创建信号故障效果
createSignalGlitchEffect(selectedKeys) createSignalGlitchEffect(selectedKeys)
} }
@@ -96,29 +152,29 @@ const randomChangeColors = () => {
const createDigitalNoiseEffect = (selectedKeys) => { const createDigitalNoiseEffect = (selectedKeys) => {
// 噪点字符集 // 噪点字符集
const noiseChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?'; const noiseChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?';
selectedKeys.forEach(key => { selectedKeys.forEach(key => {
const charElement = charRefs.value.get(key); const charElement = charRefs.value.get(key);
if (charElement) { if (charElement) {
const rect = charElement.getBoundingClientRect(); const rect = charElement.getBoundingClientRect();
const container = charElement.closest('.nonsense-item'); const container = charElement.closest('.nonsense-item');
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
// 生成3-5个噪点 // 生成3-5个噪点
const noiseCount = Math.floor(Math.random() * 3) + 3; const noiseCount = Math.floor(Math.random() * 3) + 3;
for (let i = 0; i < noiseCount; i++) { for (let i = 0; i < noiseCount; i++) {
const noiseEl = document.createElement('span'); const noiseEl = document.createElement('span');
noiseEl.textContent = noiseChars.charAt(Math.floor(Math.random() * noiseChars.length)); noiseEl.textContent = noiseChars.charAt(Math.floor(Math.random() * noiseChars.length));
// 随机位置相对于原字符 // 随机位置相对于原字符
const x = Math.random() * 30 - 20; // -20到20px const x = Math.random() * 30 - 20; // -20到20px
const y = Math.random() * 20 - 15; // -15到15px const y = Math.random() * 20 - 15; // -15到15px
// 随机大小和透明度 // 随机大小和透明度
const size = Math.random() * 8 + 8; // 8-16px const size = Math.random() * 8 + 8; // 8-16px
const opacity = Math.random() * 0.6 + 0.2; // 0.2-0.8 const opacity = Math.random() * 0.6 + 0.2; // 0.2-0.8
noiseEl.style.cssText = ` noiseEl.style.cssText = `
position: absolute; position: absolute;
left: ${rect.left - containerRect.left + x}px; left: ${rect.left - containerRect.left + x}px;
@@ -132,7 +188,7 @@ const createDigitalNoiseEffect = (selectedKeys) => {
transition: all 0.2s ease; transition: all 0.2s ease;
`; `;
container.appendChild(noiseEl); container.appendChild(noiseEl);
// 噪点逐渐消失 // 噪点逐渐消失
setTimeout(() => { setTimeout(() => {
noiseEl.style.opacity = '0'; noiseEl.style.opacity = '0';
@@ -158,10 +214,10 @@ const createSignalGlitchEffect = (selectedKeys) => {
} }
charStyles.value.set(key, glitchOffset) charStyles.value.set(key, glitchOffset)
}) })
// 同时创建数字噪点效果 // 同时创建数字噪点效果
createDigitalNoiseEffect(selectedKeys); createDigitalNoiseEffect(selectedKeys);
// 第二步:快速恢复并闪烁 // 第二步:快速恢复并闪烁
setTimeout(() => { setTimeout(() => {
selectedKeys.forEach(key => { selectedKeys.forEach(key => {
@@ -172,7 +228,7 @@ const createSignalGlitchEffect = (selectedKeys) => {
} }
charStyles.value.set(key, flashStyle) charStyles.value.set(key, flashStyle)
}) })
// 第三步:最终设置新颜色 // 第三步:最终设置新颜色
setTimeout(() => { setTimeout(() => {
selectedKeys.forEach(key => { selectedKeys.forEach(key => {
@@ -185,12 +241,13 @@ const createSignalGlitchEffect = (selectedKeys) => {
charStyles.value.set(key, finalStyle) charStyles.value.set(key, finalStyle)
}) })
}, 80) }, 80)
}, 100)} }, 100)
}
// 组件挂载时获取数据并启动定时器 // 组件挂载时获取数据并启动定时器
onMounted(() => { onMounted(() => {
loadNonsenseList() loadNonsenseList()
// 启动定时器 // 启动定时器
colorChangeTimer = setInterval(randomChangeColors, 1000) colorChangeTimer = setInterval(randomChangeColors, 1000)
}) })
@@ -216,7 +273,7 @@ onBeforeUnmount(() => {
/* 吐槽页面主容器悬浮效果 */ /* 吐槽页面主容器悬浮效果 */
.nonsense-container:hover { .nonsense-container:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
} }
/* 吐槽头部样式 */ /* 吐槽头部样式 */
@@ -247,27 +304,34 @@ onBeforeUnmount(() => {
/* 吐槽项样式 */ /* 吐槽项样式 */
.nonsense-item { .nonsense-item {
background-color: rgba(255, 255, 255, 0.85); background-color: rgba(255, 255, 255, 0.85);
border-radius: 10px; border-radius: 10px;
padding: 18px 20px 14px 20px; padding: 18px 20px 14px 20px;
box-shadow: 0 1px 4px rgba(0,0,0,0.03); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
position: relative; position: relative;
transition: box-shadow 0.2s, transform 0.2s ease; transition: box-shadow 0.2s, transform 0.2s ease;
} }
/* 吐槽项悬浮效果 */ /* 吐槽项悬浮效果 */
.nonsense-item:hover { .nonsense-item:hover {
box-shadow: 0 4px 16px rgba(64,158,255,0.12); box-shadow: 0 4px 16px rgba(64, 158, 255, 0.12);
transform: translateY(-2px); transform: translateY(-2px);
} }
/* 吐槽元信息样式 */ /* 吐槽元信息样式 */
.nonsense-meta-info { .nonsense-meta-info {
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 13px; font-size: 13px;
color: #aaa; color: #aaa;
margin-bottom: 8px; margin-bottom: 8px;
text-align: right; text-align: right;
} }
.nonsense-meta-info span {
padding: 4px 8px;
margin-right: 8px;
}
/* 吐槽内容样式 */ /* 吐槽内容样式 */
.nonsense-content { .nonsense-content {
@@ -284,20 +348,20 @@ onBeforeUnmount(() => {
padding: 14px 4px 10px 4px; padding: 14px 4px 10px 4px;
margin: 0 8px; margin: 0 8px;
} }
.nonsense-header h1 { .nonsense-header h1 {
font-size: 1.4rem; font-size: 1.4rem;
} }
.nonsense-content { .nonsense-content {
font-size: 0.98rem; font-size: 0.98rem;
line-height: 1.6; line-height: 1.6;
} }
.nonsense-list { .nonsense-list {
gap: 12px; gap: 12px;
} }
.nonsense-item { .nonsense-item {
padding: 14px 16px 10px 16px; padding: 14px 16px 10px 16px;
} }