752 lines
18 KiB
Vue
752 lines
18 KiB
Vue
<template>
|
|
<view class="tree-container">
|
|
<!-- 顶部搜索栏 -->
|
|
<view class="search-bar">
|
|
<view class="search-input" @click="goToSearch">
|
|
<text class="search-icon">🔍</text>
|
|
<text class="search-placeholder">搜索体系分类...</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 双栏布局 -->
|
|
<view class="tree-layout">
|
|
<!-- 左侧分类树 -->
|
|
<view class="tree-sidebar">
|
|
<category-tree
|
|
:categories="categories"
|
|
:selected-category="selectedCategory"
|
|
:loading="loading"
|
|
@category-click="handleCategoryClick"
|
|
@subcategory-click="handleSubcategoryClick"
|
|
@toggle-expand="handleToggleExpand"
|
|
@search-click="goToSearch"
|
|
@refresh="refreshData"
|
|
/>
|
|
</view>
|
|
|
|
<!-- 右侧文章列表 -->
|
|
<view class="tree-content">
|
|
<!-- 当前分类信息 -->
|
|
<view class="current-category" v-if="selectedCategory">
|
|
<view class="category-header">
|
|
<text class="category-title">{{ selectedCategory.name }}</text>
|
|
<text class="category-count" v-if="total > 0">({{ total }}篇文章)</text>
|
|
</view>
|
|
|
|
<view class="category-path" v-if="selectedCategory.parentCategory">
|
|
<text class="path-text">父分类: {{ selectedCategory.parentCategory.name }}</text>
|
|
</view>
|
|
|
|
<view class="category-actions">
|
|
<view class="action-item" @click="refreshArticles">
|
|
<text class="action-icon">🔄</text>
|
|
<text class="action-text">刷新</text>
|
|
</view>
|
|
<view class="action-item" @click="shareCategory">
|
|
<text class="action-icon">↗️</text>
|
|
<text class="action-text">分享</text>
|
|
</view>
|
|
<view class="action-item" @click="collectCategory" v-if="!selectedCategory.collected">
|
|
<text class="action-icon">⭐</text>
|
|
<text class="action-text">收藏</text>
|
|
</view>
|
|
<view class="action-item" @click="uncollectCategory" v-else>
|
|
<text class="action-icon">⭐</text>
|
|
<text class="action-text">已收藏</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 文章列表 -->
|
|
<scroll-view
|
|
class="article-scroll"
|
|
scroll-y
|
|
@scrolltolower="loadMoreArticles"
|
|
:scroll-with-animation="true"
|
|
:show-scrollbar="false"
|
|
>
|
|
<!-- 加载状态 -->
|
|
<common-loading
|
|
v-if="articleLoading && articleList.length === 0"
|
|
:visible="true"
|
|
text="加载文章..."
|
|
/>
|
|
|
|
<!-- 空状态 -->
|
|
<empty-state
|
|
v-if="!articleLoading && articleList.length === 0 && selectedCategory"
|
|
:title="'暂无文章'"
|
|
:description="`当前分类"${selectedCategory.name}"暂无文章`"
|
|
:show-action="true"
|
|
:action-text="'刷新'"
|
|
@action="refreshArticles"
|
|
/>
|
|
|
|
<!-- 未选择分类提示 -->
|
|
<empty-state
|
|
v-if="!selectedCategory && !loading"
|
|
:title="'请选择分类'"
|
|
:description="'点击左侧分类查看对应文章'"
|
|
:icon="'📚'"
|
|
:show-action="false"
|
|
/>
|
|
|
|
<!-- 文章列表 -->
|
|
<view v-else-if="articleList.length > 0" class="article-list">
|
|
<view
|
|
class="article-card"
|
|
v-for="article in articleList"
|
|
:key="article.id"
|
|
@click="handleArticleClick(article)"
|
|
>
|
|
<view class="article-header">
|
|
<text class="article-title">{{ article.title }}</text>
|
|
<view class="article-tags">
|
|
<text class="tag fresh" v-if="article.fresh">新</text>
|
|
<text class="tag top" v-if="article.top">置顶</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="article-summary" v-if="article.desc">
|
|
{{ article.desc }}
|
|
</view>
|
|
|
|
<view class="article-meta">
|
|
<view class="meta-item">
|
|
<text class="meta-icon">👤</text>
|
|
<text class="meta-text">{{ article.author || article.shareUser || '未知作者' }}</text>
|
|
</view>
|
|
<view class="meta-item">
|
|
<text class="meta-icon">🏷️</text>
|
|
<text class="meta-text">{{ article.superChapterName }} · {{ article.chapterName }}</text>
|
|
</view>
|
|
<view class="meta-item">
|
|
<text class="meta-icon">🕒</text>
|
|
<text class="meta-text">{{ formatRelativeTime(article.publishTime) }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="article-actions">
|
|
<view class="action-item" @click.stop="handleLike(article)">
|
|
<text class="action-icon">❤️</text>
|
|
<text class="action-text">{{ article.zan || 0 }}</text>
|
|
</view>
|
|
<view class="action-item" @click.stop="handleCollect(article)">
|
|
<text class="action-icon" :class="{collected: article.collect}">⭐</text>
|
|
<text class="action-text" :class="{collected: article.collect}">
|
|
{{ article.collect ? '已收藏' : '收藏' }}
|
|
</text>
|
|
</view>
|
|
<view class="action-item" @click.stop="handleShare(article)">
|
|
<text class="action-icon">↗️</text>
|
|
<text class="action-text">分享</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 加载更多 -->
|
|
<view class="load-more" v-if="articleList.length > 0">
|
|
<uni-load-more
|
|
:status="loadMoreStatus"
|
|
:content-text="loadMoreText"
|
|
@clickLoadMore="loadMoreArticles"
|
|
/>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
// 导入公共组件
|
|
import CommonLoading from '@/components/common/loading.vue'
|
|
import EmptyState from '@/components/common/empty-state.vue'
|
|
import CategoryTree from '@/components/tree/category-tree.vue'
|
|
|
|
// 导入API
|
|
import { getTreeModuleData, getSystemArticles } from '@/common/api/tree.api.js'
|
|
|
|
// 导入工具函数
|
|
import { formatRelativeTime } from '@/common/utils/common.js'
|
|
|
|
export default {
|
|
name: 'TreePage',
|
|
components: {
|
|
CommonLoading,
|
|
EmptyState,
|
|
CategoryTree
|
|
},
|
|
data() {
|
|
return {
|
|
// 分类数据
|
|
categories: [],
|
|
selectedCategory: null,
|
|
// 文章数据
|
|
articleList: [],
|
|
// 分页信息
|
|
currentPage: 0,
|
|
total: 0,
|
|
pageCount: 0,
|
|
// 加载状态
|
|
loading: false,
|
|
articleLoading: false,
|
|
loadMoreStatus: 'more', // more, loading, noMore
|
|
// 加载文本
|
|
loadMoreText: {
|
|
contentdown: '上拉显示更多',
|
|
contentrefresh: '正在加载...',
|
|
contentnomore: '没有更多文章了'
|
|
}
|
|
}
|
|
},
|
|
|
|
onLoad() {
|
|
this.initTreeData()
|
|
},
|
|
|
|
onPullDownRefresh() {
|
|
console.log('体系页面下拉刷新')
|
|
this.refreshData()
|
|
},
|
|
|
|
methods: {
|
|
// 初始化体系数据
|
|
async initTreeData() {
|
|
this.loading = true
|
|
try {
|
|
const treeData = await getTreeModuleData()
|
|
this.categories = treeData.categories || []
|
|
this.selectedCategory = treeData.selectedCategory
|
|
this.articleList = treeData.articleList || []
|
|
this.total = treeData.total || 0
|
|
|
|
// 更新加载状态
|
|
this.updateLoadMoreStatus()
|
|
} catch (error) {
|
|
console.error('初始化体系数据失败:', error)
|
|
uni.showToast({
|
|
title: '数据加载失败',
|
|
icon: 'none',
|
|
duration: 2000
|
|
})
|
|
} finally {
|
|
this.loading = false
|
|
uni.stopPullDownRefresh()
|
|
}
|
|
},
|
|
|
|
// 刷新数据
|
|
async refreshData() {
|
|
this.currentPage = 0
|
|
this.articleList = []
|
|
await this.initTreeData()
|
|
uni.showToast({
|
|
title: '刷新成功',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
},
|
|
|
|
// 刷新文章
|
|
async refreshArticles() {
|
|
if (!this.selectedCategory) return
|
|
|
|
this.currentPage = 0
|
|
this.articleList = []
|
|
await this.loadArticles(this.selectedCategory.id, 0)
|
|
uni.showToast({
|
|
title: '文章刷新成功',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
},
|
|
|
|
// 加载文章
|
|
async loadArticles(cid, page = 0) {
|
|
this.articleLoading = true
|
|
try {
|
|
const articleData = await getSystemArticles(cid, page)
|
|
this.articleList = articleData?.datas || []
|
|
this.total = articleData?.total || 0
|
|
this.pageCount = articleData?.pageCount || 0
|
|
this.currentPage = page
|
|
|
|
// 更新加载状态
|
|
this.updateLoadMoreStatus()
|
|
} catch (error) {
|
|
console.error('加载文章失败:', error)
|
|
uni.showToast({
|
|
title: '加载文章失败',
|
|
icon: 'none',
|
|
duration: 2000
|
|
})
|
|
} finally {
|
|
this.articleLoading = false
|
|
}
|
|
},
|
|
|
|
// 加载更多文章
|
|
async loadMoreArticles() {
|
|
if (!this.selectedCategory || this.loadMoreStatus !== 'more') return
|
|
|
|
this.loadMoreStatus = 'loading'
|
|
try {
|
|
const nextPage = this.currentPage + 1
|
|
const articleData = await getSystemArticles(this.selectedCategory.id, nextPage)
|
|
|
|
// 追加数据
|
|
this.articleList = [...this.articleList, ...(articleData.datas || [])]
|
|
this.currentPage = nextPage
|
|
this.total = articleData.total || 0
|
|
this.pageCount = articleData.pageCount || 0
|
|
|
|
// 更新加载状态
|
|
this.updateLoadMoreStatus()
|
|
} catch (error) {
|
|
console.error('加载更多文章失败:', error)
|
|
uni.showToast({
|
|
title: '加载失败',
|
|
icon: 'none',
|
|
duration: 2000
|
|
})
|
|
this.loadMoreStatus = 'more'
|
|
}
|
|
},
|
|
|
|
// 更新加载更多状态
|
|
updateLoadMoreStatus() {
|
|
if (this.currentPage >= this.pageCount - 1) {
|
|
this.loadMoreStatus = 'noMore'
|
|
} else {
|
|
this.loadMoreStatus = 'more'
|
|
}
|
|
},
|
|
|
|
// 处理分类点击
|
|
async handleCategoryClick(category) {
|
|
if (this.selectedCategory?.id === category.id) return
|
|
|
|
this.selectedCategory = category
|
|
this.currentPage = 0
|
|
this.articleList = []
|
|
this.loadMoreStatus = 'more'
|
|
|
|
await this.loadArticles(category.id, 0)
|
|
},
|
|
|
|
// 处理子分类点击
|
|
async handleSubcategoryClick(subcategory) {
|
|
if (this.selectedCategory?.id === subcategory.id) return
|
|
|
|
this.selectedCategory = subcategory
|
|
this.currentPage = 0
|
|
this.articleList = []
|
|
this.loadMoreStatus = 'more'
|
|
|
|
await this.loadArticles(subcategory.id, 0)
|
|
},
|
|
|
|
// 处理分类展开/收起
|
|
handleToggleExpand(category) {
|
|
const index = this.categories.findIndex(c => c.id === category.id)
|
|
if (index !== -1) {
|
|
this.categories[index].expanded = category.expanded
|
|
this.categories = [...this.categories] // 触发响应式更新
|
|
}
|
|
},
|
|
|
|
// 处理文章点击
|
|
handleArticleClick(article) {
|
|
console.log('点击文章:', article)
|
|
this.$emit('article-click', article)
|
|
|
|
if (article.link) {
|
|
this.navigateToWebView(article.title, article.link)
|
|
}
|
|
},
|
|
|
|
// 跳转到WebView页面
|
|
navigateToWebView(title, url) {
|
|
uni.navigateTo({
|
|
url: `/pages/webview/webview?title=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`
|
|
})
|
|
},
|
|
|
|
// 跳转到搜索页面
|
|
goToSearch() {
|
|
uni.navigateTo({
|
|
url: '/pages/search/search?type=tree'
|
|
})
|
|
},
|
|
|
|
// 处理点赞
|
|
handleLike(article) {
|
|
console.log('点赞文章:', article.id)
|
|
// TODO: 实现点赞逻辑
|
|
uni.showToast({
|
|
title: '点赞成功',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
},
|
|
|
|
// 处理收藏
|
|
handleCollect(article) {
|
|
console.log('收藏文章:', article.id)
|
|
// TODO: 实现收藏逻辑
|
|
article.collect = !article.collect
|
|
uni.showToast({
|
|
title: article.collect ? '收藏成功' : '取消收藏',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
},
|
|
|
|
// 处理分享
|
|
handleShare(article) {
|
|
console.log('分享文章:', article.id)
|
|
// TODO: 实现分享逻辑
|
|
uni.showToast({
|
|
title: '分享功能开发中',
|
|
icon: 'none',
|
|
duration: 2000
|
|
})
|
|
},
|
|
|
|
// 分享分类
|
|
shareCategory() {
|
|
if (!this.selectedCategory) return
|
|
|
|
const shareText = `分享体系分类: ${this.selectedCategory.name}\n来自玩Android客户端`
|
|
uni.setClipboardData({
|
|
data: shareText,
|
|
success: () => {
|
|
uni.showToast({
|
|
title: '分享内容已复制',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
}
|
|
})
|
|
},
|
|
|
|
// 收藏分类
|
|
collectCategory() {
|
|
if (!this.selectedCategory) return
|
|
|
|
this.selectedCategory.collected = true
|
|
uni.showToast({
|
|
title: '分类收藏成功',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
},
|
|
|
|
// 取消收藏分类
|
|
uncollectCategory() {
|
|
if (!this.selectedCategory) return
|
|
|
|
this.selectedCategory.collected = false
|
|
uni.showToast({
|
|
title: '取消收藏成功',
|
|
icon: 'success',
|
|
duration: 1500
|
|
})
|
|
},
|
|
|
|
// 工具函数
|
|
formatRelativeTime
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tree-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
background-color: #f8f8f8;
|
|
}
|
|
|
|
/* 搜索栏 */
|
|
.search-bar {
|
|
padding: 20rpx 30rpx;
|
|
background-color: #fff;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.search-input {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx;
|
|
background-color: #f8f8f8;
|
|
border-radius: 50rpx;
|
|
border: 1rpx solid #e5e5e5;
|
|
}
|
|
|
|
.search-icon {
|
|
font-size: 32rpx;
|
|
margin-right: 16rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.search-placeholder {
|
|
font-size: 28rpx;
|
|
color: #999;
|
|
flex: 1;
|
|
}
|
|
|
|
/* 双栏布局 */
|
|
.tree-layout {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 左侧分类树 */
|
|
.tree-sidebar {
|
|
width: 320rpx;
|
|
background-color: #fff;
|
|
border-right: 1rpx solid #f0f0f0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 右侧内容区域 */
|
|
.tree-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 当前分类信息 */
|
|
.current-category {
|
|
padding: 30rpx;
|
|
background-color: #fff;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.category-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15rpx;
|
|
}
|
|
|
|
.category-title {
|
|
font-size: 36rpx;
|
|
font-weight: 600;
|
|
color: #333;
|
|
flex: 1;
|
|
}
|
|
|
|
.category-count {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
margin-left: 12rpx;
|
|
}
|
|
|
|
.category-path {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.path-text {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
background-color: #f8f8f8;
|
|
padding: 8rpx 16rpx;
|
|
border-radius: 8rpx;
|
|
}
|
|
|
|
.category-actions {
|
|
display: flex;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.action-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12rpx 24rpx;
|
|
background-color: #f8f8f8;
|
|
border-radius: 25rpx;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.action-item:active {
|
|
background-color: #f0f0f0;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.action-icon {
|
|
font-size: 28rpx;
|
|
margin-right: 8rpx;
|
|
}
|
|
|
|
.action-text {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
}
|
|
|
|
/* 文章滚动区域 */
|
|
.article-scroll {
|
|
flex: 1;
|
|
}
|
|
|
|
/* 文章列表 */
|
|
.article-list {
|
|
padding: 30rpx;
|
|
}
|
|
|
|
/* 文章卡片 */
|
|
.article-card {
|
|
background: #fff;
|
|
border-radius: 20rpx;
|
|
padding: 30rpx;
|
|
margin-bottom: 24rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.article-card:active {
|
|
transform: translateY(-2rpx);
|
|
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.article-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.article-title {
|
|
flex: 1;
|
|
font-size: 34rpx;
|
|
font-weight: 600;
|
|
color: #333;
|
|
line-height: 1.4;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.article-tags {
|
|
display: flex;
|
|
gap: 10rpx;
|
|
margin-left: 20rpx;
|
|
}
|
|
|
|
.tag {
|
|
padding: 6rpx 12rpx;
|
|
border-radius: 6rpx;
|
|
font-size: 22rpx;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.tag.fresh {
|
|
background-color: #ff4444;
|
|
color: #fff;
|
|
}
|
|
|
|
.tag.top {
|
|
background-color: #ff9900;
|
|
color: #fff;
|
|
}
|
|
|
|
.article-summary {
|
|
font-size: 28rpx;
|
|
color: #666;
|
|
line-height: 1.5;
|
|
margin-bottom: 24rpx;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 3;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.article-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 24rpx;
|
|
margin-bottom: 24rpx;
|
|
padding-bottom: 24rpx;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.meta-icon {
|
|
margin-right: 8rpx;
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.meta-text {
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.article-actions {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
}
|
|
|
|
.action-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 10rpx 20rpx;
|
|
border-radius: 12rpx;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.action-item:active {
|
|
background-color: #f8f8f8;
|
|
}
|
|
|
|
.action-icon {
|
|
font-size: 36rpx;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.action-icon.collected {
|
|
color: #ff9900;
|
|
}
|
|
|
|
.action-text {
|
|
font-size: 22rpx;
|
|
color: #666;
|
|
}
|
|
|
|
.action-text.collected {
|
|
color: #ff9900;
|
|
}
|
|
|
|
/* 加载更多 */
|
|
.load-more {
|
|
padding: 40rpx 0;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 750px) {
|
|
.tree-layout {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tree-sidebar {
|
|
width: 100%;
|
|
height: 300rpx;
|
|
border-right: none;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
}
|
|
</style> |