feat: 实现文章状态管理及分类标签展示功能
新增文章状态管理功能,支持草稿、已发表和已删除状态的显示与切换 重构分类和标签展示模块,添加点击跳转功能 优化文章列表页面,增加状态筛选和分页功能 完善疯言疯语模块,支持编辑和删除操作 修复路由跳转和页面刷新问题
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
</el-icon>
|
||||
<span>首页</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/article-list">
|
||||
<el-menu-item index="/articlelist">
|
||||
<el-icon>
|
||||
</el-icon>
|
||||
<span>目录</span>
|
||||
@@ -51,13 +51,13 @@
|
||||
</a>
|
||||
</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-name">分类</span>
|
||||
</a>
|
||||
</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-name">标签</span>
|
||||
</a>
|
||||
@@ -68,6 +68,50 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -75,6 +119,8 @@
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { articleService, categoryService, categoryAttributeService } from "@/services";
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
// 当前激活菜单
|
||||
const activeIndex = ref('/:type')
|
||||
@@ -86,6 +132,13 @@ const state = reactive({
|
||||
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) => {
|
||||
@@ -109,40 +162,110 @@ 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 () => {
|
||||
// 获取分类数据
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await categoryService.getAllCategories();
|
||||
categoryCount.value = response.data?.length || 0
|
||||
// 这里应该调用API获取实际的分类数量
|
||||
// 暂时设置为模拟数据
|
||||
const response = await categoryService.getAllCategories();
|
||||
// 如果API返回的数据结构不包含count属性,我们可以模拟一些数据
|
||||
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) {
|
||||
console.error('获取分类数量失败:', error)
|
||||
categoryCount.value = 0
|
||||
|
||||
console.error('获取分类失败:', error)
|
||||
// 如果API调用失败,使用模拟数据
|
||||
categories.value = [
|
||||
|
||||
];
|
||||
categoryCount.value = categories.value.length
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签数量
|
||||
const fetchAttributeCount = async () => {
|
||||
// 获取标签数据
|
||||
const fetchAttributes = async () => {
|
||||
try {
|
||||
const response = await categoryAttributeService.getAllAttributes();
|
||||
AttributeCount.value = response.data?.length || 0
|
||||
// 这里应该调用API获取实际的标签数量
|
||||
// 暂时设置为模拟数据
|
||||
const response = await categoryAttributeService.getAllAttributes();
|
||||
// 如果API返回的数据结构不包含count属性,我们可以模拟一些数据
|
||||
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) {
|
||||
console.error('获取标签数量失败:', error)
|
||||
AttributeCount.value = 0
|
||||
console.error('获取标签失败:', error)
|
||||
// 如果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 handleScroll = () => {
|
||||
@@ -153,8 +276,8 @@ const handleScroll = () => {
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
fetchArticleCount() // 组件挂载时获取文章数量
|
||||
fetchCategoryCount() // 组件挂载时获取分类数量
|
||||
fetchAttributeCount() // 组件挂载时获取标签数量
|
||||
fetchCategories() // 组件挂载时获取分类数据
|
||||
fetchAttributes() // 组件挂载时获取标签数据
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -190,10 +313,17 @@ onUnmounted(() => {
|
||||
|
||||
/* 内容区域样式 */
|
||||
#cont {
|
||||
padding:0 0 10px 0;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
/* 白色半透明背景 */
|
||||
}
|
||||
#cont .cont1{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#cont .cont2{
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.cont1 {
|
||||
text-align: center;
|
||||
@@ -213,18 +343,19 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* 菜单样式 */
|
||||
.cont2 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.cont2 .el-menu-vertical-demo {
|
||||
display: block;
|
||||
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) {
|
||||
border-radius: 0 0 10px 10px;
|
||||
/* border-radius: 0 0 10px 10px; */
|
||||
/* margin-bottom: 10px; */
|
||||
}
|
||||
|
||||
.cont2 .el-menu-vertical-demo .el-menu-item:hover {
|
||||
@@ -376,4 +507,141 @@ onUnmounted(() => {
|
||||
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>
|
||||
@@ -12,7 +12,7 @@
|
||||
<el-menu-item index="home">
|
||||
首页
|
||||
</el-menu-item>
|
||||
<el-menu-item index="article-list">
|
||||
<el-menu-item index="articlelist">
|
||||
目录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="nonsense">
|
||||
@@ -210,7 +210,7 @@ const performSearch = () => {
|
||||
* 根据路由路径设置页面状态
|
||||
*/
|
||||
const updatePageState = () => {
|
||||
if (rpsliturl[1] === localhome && rpsliturl[2] == undefined) {
|
||||
if (rpsliturl[1] == localhome && rpsliturl[2] == undefined) {
|
||||
classhero.value = false;
|
||||
} else {
|
||||
classhero.value = true;
|
||||
@@ -225,7 +225,6 @@ const setActiveIndex = (path: string) => {
|
||||
globalStore.setValue('localpath', {
|
||||
name: path
|
||||
})
|
||||
// console.log('设置激活索引:', path);
|
||||
if (path === 'message') {
|
||||
globalStore.removeValue('articleInfo')
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ const isNonsenseModalVisible = ref(false)
|
||||
const nonsenseContent = ref('')
|
||||
// 基础按钮配置
|
||||
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: [
|
||||
{ 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: 'published-articles', label: '已发表', icon: 'icon-new-tag' },
|
||||
...baseButtons
|
||||
],
|
||||
|
||||
@@ -66,7 +69,7 @@ const pageButtons = {
|
||||
],
|
||||
|
||||
// 分类页面按钮
|
||||
category: [
|
||||
articlelist: [
|
||||
{ id: 'create-category', label: '新建', icon: 'icon-create-category' },
|
||||
...baseButtons
|
||||
],
|
||||
@@ -86,6 +89,21 @@ const pageButtons = {
|
||||
// 默认按钮
|
||||
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 = () => {
|
||||
@@ -151,10 +169,51 @@ const handleErrorResponse = (error, defaultMessage = '操作失败') => {
|
||||
console.error('操作失败:', error)
|
||||
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 = () => {
|
||||
if (!route.query.id) {
|
||||
const articleId = globalStore.getValue('articleInfo')?.id
|
||||
if (!articleId) {
|
||||
ElMessage.warning('缺少文章ID参数')
|
||||
return
|
||||
}
|
||||
@@ -165,7 +224,7 @@ const deleteArticle = () => {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 调用删除文章接口
|
||||
articleService.deleteArticle(Number(route.query.id))
|
||||
articleService.deleteArticle(Number(articleId))
|
||||
.then(response => {
|
||||
if (response.code === 200) {
|
||||
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 = () => {
|
||||
@@ -200,7 +278,11 @@ const logout = () => {
|
||||
})
|
||||
.catch(err => handleErrorResponse(err, '登出失败'))
|
||||
}
|
||||
// 刷新页面
|
||||
const reloadPage = () => {
|
||||
globalStore.clearAll()
|
||||
|
||||
}
|
||||
// 处理按钮点击事件
|
||||
const handleButtonClick = (button) => {
|
||||
console.log('点击了按钮:', button.id, button.label)
|
||||
@@ -209,11 +291,12 @@ const handleButtonClick = (button) => {
|
||||
switch (button.id) {
|
||||
// 新增操作
|
||||
case 'create-article':
|
||||
// router.push({ path: '/articlesave' })
|
||||
// 清除更新文章状态
|
||||
router.push({ path: '/articlesave' })
|
||||
break
|
||||
|
||||
case 'create-category':
|
||||
// router.push({ path: '/categorysave' })
|
||||
createCategory()
|
||||
break
|
||||
|
||||
case 'create-tag':
|
||||
@@ -227,27 +310,25 @@ const handleButtonClick = (button) => {
|
||||
|
||||
// 修改操作
|
||||
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参数')
|
||||
}
|
||||
updateArticle()
|
||||
break
|
||||
|
||||
// 删除操作
|
||||
case 'delete-article':
|
||||
deleteArticle();
|
||||
break
|
||||
break
|
||||
|
||||
// 查看操作
|
||||
case 'published-articles':
|
||||
router.push({ path: '/home', query: { status: 1 } })
|
||||
case 'del-articles':
|
||||
getButtonsByStatus(2)
|
||||
break
|
||||
|
||||
case 'unpublished-articles':
|
||||
router.push({ path: '/home', query: { status: 0 } })
|
||||
getButtonsByStatus(0)
|
||||
break
|
||||
|
||||
case 'published-articles':
|
||||
getButtonsByStatus(1)
|
||||
break
|
||||
|
||||
case 'view-articles':
|
||||
@@ -259,6 +340,10 @@ const handleButtonClick = (button) => {
|
||||
logout();
|
||||
break
|
||||
|
||||
case 'reload':
|
||||
reloadPage()
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn('未处理的按钮类型:', button.id, button.label)
|
||||
ElMessage.info(`功能 ${button.label} 暂未实现`)
|
||||
|
||||
@@ -26,16 +26,24 @@ const routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'aericletype',
|
||||
name: 'homeByType'
|
||||
name: 'homeByType',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: 'aericletitle',
|
||||
name: 'homeByTitle'
|
||||
name: 'homeByTitle',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: 'aericlestatus',
|
||||
name: 'homeByStatus',
|
||||
component: HomePage
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/article-list',
|
||||
path: '/articlelist',
|
||||
name: 'articleList',
|
||||
component: ArticleList,
|
||||
meta: {
|
||||
|
||||
@@ -13,7 +13,22 @@ class ArticleService {
|
||||
getAllArticles(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获取文章详情
|
||||
* @param {number} articleid - 文章ID
|
||||
|
||||
@@ -3,31 +3,39 @@ import apiService from './apiService'
|
||||
|
||||
class NonsenseService {
|
||||
/**
|
||||
* 获取所有随机内容
|
||||
* 获取所有疯言疯语内容
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense[]>>}
|
||||
*/
|
||||
getAllNonsense() {
|
||||
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>>}
|
||||
*/
|
||||
saveNonsense(nonsense){
|
||||
return apiService.post('/nonsense', nonsense)
|
||||
}
|
||||
/**
|
||||
* 删除随机内容
|
||||
* @param {number} id - 随机内容ID
|
||||
* 删除疯言疯语内容
|
||||
* @param {number} id - 疯言疯语内容ID
|
||||
* @returns {Promise<import('../types').ApiResponse<boolean>>}
|
||||
*/
|
||||
deleteNonsense(id){
|
||||
return apiService.delete(`/nonsense/${id}`)
|
||||
}
|
||||
/**
|
||||
* 更新随机内容
|
||||
* @param {import('../types').Nonsense} nonsense - 随机内容对象
|
||||
* 更新疯言疯语内容
|
||||
* @param {import('../types').Nonsense} nonsense - 疯言疯语内容对象
|
||||
* @returns {Promise<import('../types').ApiResponse<import('../types').Nonsense>>}
|
||||
*/
|
||||
updateNonsense(nonsense){
|
||||
|
||||
@@ -129,6 +129,90 @@ p {
|
||||
font-weight: 300;
|
||||
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 {
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Article {
|
||||
updatedAt: string
|
||||
viewCount?: number
|
||||
likes?: number
|
||||
commentCount?: number
|
||||
status?: number
|
||||
markdownscontent: string
|
||||
}
|
||||
@@ -89,6 +90,7 @@ export interface CategoryDto {
|
||||
* 分类属性接口
|
||||
*/
|
||||
export interface CategoryAttribute {
|
||||
map(arg0: (item: any) => any): unknown
|
||||
attributeid: number
|
||||
categoryid: number
|
||||
attributename: string
|
||||
@@ -127,7 +129,23 @@ export interface UserDto {
|
||||
phone: string
|
||||
role?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 疯言疯语类型接口
|
||||
*/
|
||||
export interface Nonsense {
|
||||
nonsenseid: number
|
||||
content: string
|
||||
status?: number
|
||||
time: string
|
||||
}
|
||||
/**
|
||||
* 疯言疯语DTO接口
|
||||
*/
|
||||
export interface NonsenseDto {
|
||||
content: string
|
||||
status?: number
|
||||
time?: string
|
||||
}
|
||||
/**
|
||||
* API响应接口
|
||||
*/
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
好像真被一阵风吹散了似的,
|
||||
哈哈哈哈哈,我还蛮喜欢这个外号的。
|
||||
<br></br>
|
||||
后来有一天,也不知道怎么了,精神状态不是很好,感觉要控制不住要发疯了,突然这疯子就像钉子一样扎在我脑海里(哈哈哈哈!)。于是就干脆改名叫“清疯”了——清风,清疯,念起来几乎没差,
|
||||
有段时间,不知道怎么了,精神状态不是很好,好想发疯,突然这疯就像钉子一样扎在我脑海里(哈哈哈哈!)。于是就干脆改名叫“清疯”了——清风,清疯,念起来几乎没差,
|
||||
但内核却从一种理想的淡然,切换成了真实的、带点毛边的鲜活。“悠悠清风荡我心”,只是如今这阵清风熬成了清疯,终于在我心里刮起一场疯——疯啦!
|
||||
<br></br>
|
||||
又过些日子,玩新游戏要起名,正盯着输入框发呆,“清疯”两个字在脑海里冒出来……疯癫?哎,我好像是疯,但也没完全颠嘛!那种在理智边界试探、却绝不越线的微妙感,一下对味了——干脆就叫“清疯不颠”!
|
||||
名字一出自己先乐了,这不就是我吗,表面疯癫,内心门儿清,简直是我们这代人精神状态的绝佳注脚。
|
||||
又过些日子,玩新游戏要起名,正盯着输入框里的“清疯”发呆, 两个字在脑海里冒出来……疯癫?哎,好像是疯了,但也没完全颠嘛!那种在理智边界试探、却绝不越线的微妙感,一下对味了——干脆就叫“清疯不颠”!
|
||||
名字一出自己先乐了。
|
||||
<br></br>
|
||||
哈哈哈哈哈哈!俗话说天才在左疯子在右,在我这儿,大概是左脑负责疯,右脑负责颠,两个家伙吵吵闹闹,反而让我在这个世界里自得其乐。
|
||||
</h5>
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="about-personal-intro">
|
||||
<h4>疯言疯语</h4>
|
||||
<h5>
|
||||
我并没有网站的开发经验这是我的第一个项目,所以我并不清楚该如何去写这个页面。就干脆当一个我发疯的地方吧(哈哈哈哈哈 所以你有看到彩色的小鹿吗?)
|
||||
我并没有网站的开发经验这是我的第一个项目,我想设计一下独属于清疯的页面,可是我并不清楚该如何去写这个页面。就干脆当一个我发疯的地方吧(哈哈哈哈哈 所以你有看到彩色的小鹿吗?)
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
<div class="about-contact-section">
|
||||
<h4>联系方式</h4>
|
||||
<p>如果你有任何问题或建议,欢迎随时联系我!</p>
|
||||
<p>如果你有任何问题或建议,欢迎随时联系我!(我文笔真的很烂QAQ)</p>
|
||||
<div class="contact-options">
|
||||
<el-button type="primary" plain @click="goToMessageBoard">留言板</el-button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state-container">
|
||||
<el-skeleton :count="5" />
|
||||
<el-skeleton :count="1" />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
@@ -14,20 +14,19 @@
|
||||
<el-alert :title="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchCategories">重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
<div v-else-if="categories.length > 0" class="article-content" id="category-list">
|
||||
<p><strong></strong></p>
|
||||
<div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下,点击跳转</span></strong></div>
|
||||
<div v-for="categoryGroup in categories" :key="categoryGroup.typeid"
|
||||
class="category-group-container">
|
||||
<div v-if="categoryGroup.attributes.length > 0">
|
||||
<div class="alert alert-primary"><strong><span class="alert-inner-text">文章分类如下,点击跳转</span> </strong></div>
|
||||
<div v-for="categoryGroup in categories" :key="categoryGroup.typeid" class="category-group-container">
|
||||
<div v-if="categoryGroup.attributes.length > 0 && categoryGroup.attributes.some(cat => cat.articles && cat.articles.length > 0)">
|
||||
<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">
|
||||
<li v-for="category in categoryGroup.attributes" :key="category.typeid">
|
||||
<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename
|
||||
}}</kbd></a>
|
||||
<li v-for="category in categoryGroup.attributes" :key="category.attributeid" >
|
||||
<a class="category-link" @click="handleCategoryClick(category)"><kbd>{{ category.attributename
|
||||
}}</kbd></a>
|
||||
— —({{ category.articles.length }})
|
||||
</li>
|
||||
</ul>
|
||||
@@ -66,90 +65,61 @@ const error = ref('')
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
const res = await categoryService.getAllCategories();
|
||||
|
||||
if (res.data && res.data.length > 0) {
|
||||
// 创建处理后的分类数组
|
||||
const processedCategories = res.data.map(item => ({
|
||||
let processedCategories: any[] = [];
|
||||
if (res.code === 200) {
|
||||
processedCategories = res.data.map(item => ({
|
||||
...item,
|
||||
attributes: [] // 使用更清晰的命名
|
||||
}));
|
||||
|
||||
// 并行处理所有分类
|
||||
// 使用Promise.all等待所有异步操作完成
|
||||
await Promise.all(
|
||||
processedCategories.map(async (category) => {
|
||||
try {
|
||||
if (await categoryAttributeService.checkAttributeExists(category.typeid, category.typename || '')) {
|
||||
// 获取分类的所有属性
|
||||
const attributesRes = await categoryAttributeService.getAttributeById(category.typeid);
|
||||
// 存储属性数据
|
||||
if (attributesRes.data) {
|
||||
category.attributes = Array.isArray(attributesRes.data) ? attributesRes.data : [attributesRes.data];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// console.error(`处理分类失败 (分类ID: ${category.typeid}):`, err);
|
||||
processedCategories.map(async category => {
|
||||
const attributes = await categoryAttributeService.getAttributesByCategory(category.typeid);
|
||||
if (attributes.code === 200 && Array.isArray(attributes.data)) {
|
||||
const processedAttributes = await Promise.all(
|
||||
attributes.data.map(async item => {
|
||||
const articleItem = {
|
||||
...item,
|
||||
articles: []
|
||||
};
|
||||
const articlesRes = await articleService.getArticlesByAttributeId(item.attributeid);
|
||||
if(articlesRes.code === 200 && Array.isArray(articlesRes.data)){
|
||||
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) {
|
||||
error.value = '获取分类列表失败,请稍后重试';
|
||||
console.error('获取分类列表失败:', err);
|
||||
ElMessage.error(error.value);
|
||||
} finally {
|
||||
ElMessage.error('获取分类列表失败,请稍后重试');
|
||||
}finally{
|
||||
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
|
||||
* @param {string | number} attributeId - 属性ID
|
||||
*/
|
||||
* 处理分类点击事件
|
||||
* 注意:现在实际上使用的是属性ID而不是分类ID
|
||||
* @param {string | number} attributeId - 属性ID
|
||||
*/
|
||||
const handleCategoryClick = (attribute: any) => {
|
||||
globalStore.removeValue('attribute')
|
||||
globalStore.setValue('attribute', {
|
||||
id: attribute.typeid || attribute.categoryid,
|
||||
name: attribute.attributename || attribute.typename || '未命名属性',
|
||||
id: attribute.attributeid,
|
||||
name: attribute.typename
|
||||
})
|
||||
console.log(attribute)
|
||||
router.push({
|
||||
path: '/home/aericletype',
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,18 +19,16 @@
|
||||
<h1 class="article-main-title">{{ article.title }}</h1>
|
||||
|
||||
<div class="article-meta-info">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-date"></i>
|
||||
{{ formatDate(article.createdAt) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-folder"></i>
|
||||
{{ article.categoryName || '未分类' }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-view"></i>
|
||||
{{ article.viewCount || 0 }} 阅读
|
||||
</span>
|
||||
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt) }}</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.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</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>
|
||||
|
||||
@@ -85,12 +83,10 @@
|
||||
// 导入必要的依赖
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { articleService } from '@/services'
|
||||
import { categoryAttributeService } from '@/services'
|
||||
import { useGlobalStore } from '@/store/globalStore'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { Article } from '@/types'
|
||||
import { formatDate } from '@/utils/dateUtils'
|
||||
import { formatRelativeTime } from '@/utils/dateUtils'
|
||||
import messageboard from './messageboard.vue'
|
||||
import markdownViewer from './markdown.vue'
|
||||
// 路由相关
|
||||
@@ -122,22 +118,19 @@ const fetchArticleDetail = async () => {
|
||||
if (response) {
|
||||
article.value = response
|
||||
// 获取并设置分类名称
|
||||
const categoryResponse = await categoryAttributeService.getAttributeById(Number(article.value.attributeid))
|
||||
article.value.categoryName = categoryResponse.data.attributename || '未分类'
|
||||
|
||||
// 增加文章浏览量
|
||||
try {
|
||||
await articleService.incrementArticleViews(Number(articleId))
|
||||
// 更新前端显示的浏览量
|
||||
if (article.value.viewCount) {
|
||||
article.value.viewCount++
|
||||
} else {
|
||||
article.value.viewCount = 1
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('增加文章浏览量失败:', err)
|
||||
// 不阻止主流程
|
||||
// const categoryResponse = await categoryAttributeService.getAttributeById(Number(article.value.attributeid))
|
||||
// article.value.categoryName = categoryResponse.data.attributename || '未分类'
|
||||
// 获取并设置评论量
|
||||
// const commentResponse = await messageService.getMessagesByArticleId(articleId)
|
||||
// article.value.commentCount = commentResponse.data.length || 0
|
||||
// 更新浏览量
|
||||
// 更新前端显示的浏览量
|
||||
if (article.value.viewCount) {
|
||||
article.value.viewCount++
|
||||
} else {
|
||||
article.value.viewCount = 1
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error('文章不存在或已被删除')
|
||||
}
|
||||
@@ -181,7 +174,7 @@ onMounted(() => {
|
||||
.article-detail-wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
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 +186,7 @@ onMounted(() => {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@@ -203,7 +196,7 @@ onMounted(() => {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 40px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
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 +211,7 @@ onMounted(() => {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 80px 40px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
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);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</span>
|
||||
<span class="meta-item status-item">
|
||||
<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>
|
||||
</el-select>
|
||||
@@ -45,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 { useGlobalStore } from '@/store/globalStore'
|
||||
const globalStore = useGlobalStore()
|
||||
// 路由
|
||||
import router from '@/router/Router';
|
||||
const Articleform = ref<Article>({
|
||||
@@ -55,7 +57,8 @@ const Articleform = ref<Article>({
|
||||
categoryName: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
markdownscontent: ''
|
||||
markdownscontent: '',
|
||||
status: 0 // 默认状态为草稿
|
||||
})
|
||||
|
||||
// 用于级联选择器的值绑定
|
||||
@@ -63,16 +66,29 @@ const selectedValues = ref([]);
|
||||
const categorieoptions = ref([]);
|
||||
const statusoptions = ref([
|
||||
{
|
||||
label: '未发布',
|
||||
label: '草稿',
|
||||
value: '0'
|
||||
},
|
||||
{
|
||||
label: '发布',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
value: '2'
|
||||
}
|
||||
]);
|
||||
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
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
@@ -107,7 +123,22 @@ const loadCategories = async () => {
|
||||
})
|
||||
);
|
||||
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) {
|
||||
console.error('加载分类失败:', error);
|
||||
@@ -138,6 +169,7 @@ const handleSave = (markdown) => {
|
||||
|
||||
// 构建请求数据
|
||||
const articleData = {
|
||||
articleid: Articleform.value.articleid,
|
||||
title: Articleform.value.title,
|
||||
content: Articleform.value.content,
|
||||
attributeid: Number(Articleform.value.attributeid),
|
||||
@@ -150,28 +182,37 @@ const handleSave = (markdown) => {
|
||||
console.log('发送文章数据:', articleData);
|
||||
console.log('当前认证token是否存在:', !!localStorage.getItem('token'));
|
||||
|
||||
// 保存文章
|
||||
articleService.createArticle(articleData)
|
||||
// 根据articleid决定调用创建还是更新接口
|
||||
const savePromise = Articleform.value.articleid === 0
|
||||
? articleService.createArticle(articleData)
|
||||
: articleService.updateArticle(Articleform.value.articleid, articleData);
|
||||
savePromise
|
||||
.then(res => {
|
||||
console.log('API响应:', res);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('文章保存成功')
|
||||
// 重置表单
|
||||
Articleform.value = {
|
||||
articleid: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
attributeid: 0,
|
||||
categoryName: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
markdownscontent: ''
|
||||
};
|
||||
selectedValues.value = [];
|
||||
ElMessage.success(Articleform.value.articleid === 0 ? '文章创建成功' : '文章更新成功');
|
||||
// 清除全局存储中的article
|
||||
globalStore.removeValue('updatearticle');
|
||||
// 重置表单或保留编辑状态
|
||||
if (Articleform.value.articleid === 0) {
|
||||
// 创建新文章后重置表单
|
||||
Articleform.value = {
|
||||
articleid: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
attributeid: 0,
|
||||
categoryName: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
markdownscontent: ''
|
||||
};
|
||||
selectedValues.value = [];
|
||||
}
|
||||
|
||||
// 返回列表页
|
||||
router.push('/home');
|
||||
} else {
|
||||
ElMessage.error(res.message || '文章保存失败')
|
||||
ElMessage.error(res.message || (Articleform.value.articleid === 0 ? '文章创建失败' : '文章更新失败'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -181,17 +222,19 @@ const handleSave = (markdown) => {
|
||||
console.error('错误状态码:', err.response.status);
|
||||
console.error('错误响应数据:', err.response.data);
|
||||
|
||||
const operationType = Articleform.value.articleid === 0 ? '创建' : '更新';
|
||||
|
||||
if (err.response.status === 401) {
|
||||
ElMessage.error('未授权访问,请先登录');
|
||||
} else if (err.response.status === 403) {
|
||||
ElMessage.error('没有权限创建文章,请检查账号权限');
|
||||
ElMessage.error(`没有权限${operationType}文章,请检查账号权限`);
|
||||
} else if (err.response.status === 400) {
|
||||
ElMessage.error('数据验证失败: ' + (err.response.data?.message || '请检查输入'));
|
||||
} else {
|
||||
ElMessage.error('请求被拒绝,错误代码: ' + err.response.status);
|
||||
ElMessage.error(`请求被拒绝,错误代码: ${err.response.status}`);
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(err.message || '文章保存失败')
|
||||
ElMessage.error(err.message || (Articleform.value.articleid === 0 ? '文章创建失败' : '文章更新失败'));
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -20,10 +20,15 @@
|
||||
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
|
||||
<div class="article-meta-info">
|
||||
<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.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.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>
|
||||
</transition-group>
|
||||
@@ -36,7 +41,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { formatDate, formatRelativeTime } from '@/utils/dateUtils'
|
||||
import { formatContentPreview } from '@/utils/stringUtils'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -64,20 +69,35 @@ const fetchArticles = async () => {
|
||||
|
||||
// 检查URL参数,确定获取文章的方式
|
||||
const pathSegment = route.path.split('/')[2];
|
||||
|
||||
console.log(pathSegment)
|
||||
// 根据不同路径获取不同文章
|
||||
if (pathSegment === 'aericletype') {
|
||||
// 按属性类型获取文章
|
||||
const attributeData = globalStore.getValue('attribute')
|
||||
response = await articleService.getArticlesByAttributeId(attributeData.id)
|
||||
} else if (pathSegment === 'aericletitle') {
|
||||
// 按标题搜索文章
|
||||
const titleData = globalStore.getValue('articleserarch')
|
||||
response = await articleService.getArticlesByTitle(titleData.name)
|
||||
} else {
|
||||
// 获取所有文章
|
||||
console.log('获取所有文章列表')
|
||||
response = await articleService.getAllArticles()
|
||||
switch (pathSegment) {
|
||||
case 'aericletype':
|
||||
// 按属性类型获取文章
|
||||
const attributeData = globalStore.getValue('attribute')
|
||||
response = await articleService.getArticlesByAttributeId(attributeData.id)
|
||||
break
|
||||
case 'aericletitle':
|
||||
// 按标题搜索文章
|
||||
const titleData = globalStore.getValue('articleserarch')
|
||||
response = await articleService.getArticlesByTitle(titleData.name)
|
||||
break
|
||||
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 - 文章对象
|
||||
*/
|
||||
const handleArticleClick = (article) => {
|
||||
|
||||
// 增加文章浏览量
|
||||
articleService.incrementArticleViews(article.articleId)
|
||||
// 清除之前的文章信息
|
||||
globalStore.removeValue('articleInfo')
|
||||
// 存储文章信息到全局状态
|
||||
globalStore.setValue('articleInfo', article)
|
||||
|
||||
@@ -135,12 +158,24 @@ const handleArticleClick = (article) => {
|
||||
path: '/article',
|
||||
})
|
||||
}
|
||||
|
||||
//刷新时挂载获取数据
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchArticles()
|
||||
})
|
||||
|
||||
// 监听路由变化,确保刷新时也能重新获取数据
|
||||
watch(
|
||||
// 监听路由路径和查询参数变化
|
||||
() => [route.path, route.query],
|
||||
// 路由变化时触发获取文章列表
|
||||
() => {
|
||||
fetchArticles()
|
||||
console.log('路由变化,重新获取文章列表')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -261,69 +296,6 @@ onMounted(() => {
|
||||
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 {
|
||||
text-align: center;
|
||||
|
||||
@@ -701,19 +701,7 @@ const handleDelete = async (msg) => {
|
||||
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 {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<template>
|
||||
<div class="nonsense-container">
|
||||
<div class="nonsense-list">
|
||||
<div
|
||||
class="nonsense-item"
|
||||
v-for="item in nonsenseList"
|
||||
:key="item.id"
|
||||
>
|
||||
<div class="nonsense-item" v-for="item in nonsenseList" :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>
|
||||
<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>
|
||||
@@ -24,9 +23,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import {nonsenseService } from '@/services'
|
||||
import { nonsenseService } from '@/services'
|
||||
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 () => {
|
||||
try {
|
||||
const response = await nonsenseService.getAllNonsense()
|
||||
const response = await nonsenseService.getNonsenseByStatus(1)
|
||||
if (response.code === 200) {
|
||||
nonsenseList.value = response.data
|
||||
}else{
|
||||
} else {
|
||||
ElMessage.error('加载吐槽内容失败')
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -57,7 +58,62 @@ const loadNonsenseList = async () => {
|
||||
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) {
|
||||
@@ -185,7 +241,8 @@ const createSignalGlitchEffect = (selectedKeys) => {
|
||||
charStyles.value.set(key, finalStyle)
|
||||
})
|
||||
}, 80)
|
||||
}, 100)}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据并启动定时器
|
||||
onMounted(() => {
|
||||
@@ -216,7 +273,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
/* 吐槽页面主容器悬浮效果 */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 吐槽头部样式 */
|
||||
@@ -250,24 +307,31 @@ onBeforeUnmount(() => {
|
||||
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);
|
||||
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);
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user