a
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<view class="home-container">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input" @click="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">搜索wanandroid.com内容</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Banner轮播 -->
|
||||
<home-banner
|
||||
:banner-list="bannerList"
|
||||
@click="handleBannerClick"
|
||||
/>
|
||||
|
||||
<!-- 文章列表 -->
|
||||
<view class="article-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最新文章</text>
|
||||
<text class="section-count" v-if="total > 0">({{ total }})</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<common-loading
|
||||
v-if="loading && articleList.length === 0"
|
||||
:visible="true"
|
||||
text="加载中..."
|
||||
/>
|
||||
|
||||
<!-- 文章列表 -->
|
||||
<view v-else>
|
||||
<!-- 空状态 -->
|
||||
<empty-state
|
||||
v-if="!loading && articleList.length === 0"
|
||||
:title="'暂无文章'"
|
||||
:description="'下拉刷新试试'"
|
||||
:show-action="true"
|
||||
:action-text="'刷新'"
|
||||
@action="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 文章卡片列表 -->
|
||||
<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 class="load-more" v-if="articleList.length > 0">
|
||||
<uni-load-more
|
||||
:status="loadMoreStatus"
|
||||
:content-text="loadMoreText"
|
||||
@clickLoadMore="loadMoreData"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回到顶部 -->
|
||||
<view
|
||||
class="back-to-top"
|
||||
v-if="showBackTop"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<text class="back-top-icon">↑</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 导入公共组件
|
||||
import CommonLoading from '@/components/common/loading.vue'
|
||||
import EmptyState from '@/components/common/empty-state.vue'
|
||||
import HomeBanner from '@/components/home/banner.vue'
|
||||
|
||||
// 导入API
|
||||
import { getHomeData } from '@/common/api/home.api.js'
|
||||
|
||||
// 导入工具函数
|
||||
import { formatRelativeTime } from '@/common/utils/common.js'
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
components: {
|
||||
CommonLoading,
|
||||
EmptyState,
|
||||
HomeBanner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Banner数据
|
||||
bannerList: [],
|
||||
// 文章数据
|
||||
articleList: [],
|
||||
// 分页信息
|
||||
currentPage: 0,
|
||||
total: 0,
|
||||
pageCount: 0,
|
||||
// 加载状态
|
||||
loading: false,
|
||||
loadMoreStatus: 'more', // more, loading, noMore
|
||||
showBackTop: false,
|
||||
// 加载文本
|
||||
loadMoreText: {
|
||||
contentdown: '上拉显示更多',
|
||||
contentrefresh: '正在加载...',
|
||||
contentnomore: '没有更多数据了'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initHomeData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
console.log('首页下拉刷新')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
console.log('首页上拉加载更多')
|
||||
if (this.loadMoreStatus === 'more') {
|
||||
this.loadMoreData()
|
||||
}
|
||||
},
|
||||
|
||||
onPageScroll(e) {
|
||||
this.showBackTop = e.scrollTop > 400
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化首页数据
|
||||
async initHomeData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const homeData = await getHomeData(this.currentPage)
|
||||
this.bannerList = homeData.bannerList || []
|
||||
this.articleList = homeData.articleList || []
|
||||
this.total = homeData.total || 0
|
||||
this.pageCount = homeData.pageCount || 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.initHomeData()
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
},
|
||||
|
||||
// 加载更多数据
|
||||
async loadMoreData() {
|
||||
if (this.loadMoreStatus !== 'more') return
|
||||
|
||||
this.loadMoreStatus = 'loading'
|
||||
try {
|
||||
const nextPage = this.currentPage + 1
|
||||
const homeData = await getHomeData(nextPage)
|
||||
|
||||
// 追加数据
|
||||
this.articleList = [...this.articleList, ...(homeData.articleList || [])]
|
||||
this.currentPage = nextPage
|
||||
this.total = homeData.total || 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'
|
||||
}
|
||||
},
|
||||
|
||||
// 处理Banner点击
|
||||
handleBannerClick(item) {
|
||||
console.log('点击Banner:', item)
|
||||
this.$emit('banner-click', item)
|
||||
|
||||
if (item.url) {
|
||||
this.navigateToWebView(item.title, item.url)
|
||||
}
|
||||
},
|
||||
|
||||
// 处理文章点击
|
||||
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'
|
||||
})
|
||||
},
|
||||
|
||||
// 处理点赞
|
||||
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
|
||||
})
|
||||
},
|
||||
|
||||
// 回到顶部
|
||||
scrollToTop() {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: 300
|
||||
})
|
||||
},
|
||||
|
||||
// 工具函数
|
||||
formatRelativeTime
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
padding-bottom: 40rpx;
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 文章区域 */
|
||||
.article-section {
|
||||
margin-top: 20rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
/* 文章卡片 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 回到顶部 */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 160rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(60, 197, 31, 0.9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(60, 197, 31, 0.3);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-top-icon {
|
||||
color: white;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.back-to-top:active {
|
||||
background: rgba(60, 197, 31, 1);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="placeholder">
|
||||
<text class="placeholder-icon">👤</text>
|
||||
<text class="placeholder-title">我的页面</text>
|
||||
<text class="placeholder-desc">开发中,敬请期待...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('我的页面加载')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 120rpx;
|
||||
color: #007aff;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,718 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 返回按钮 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">项目详情</text>
|
||||
<view class="nav-actions">
|
||||
<view class="nav-action" @click="handleShare">
|
||||
<uni-icons type="share" size="24" color="#333"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目详情内容 -->
|
||||
<scroll-view class="content-scroll" scroll-y="true">
|
||||
<!-- 项目封面 -->
|
||||
<view class="project-cover">
|
||||
<image
|
||||
:src="project.cover || '/static/images/project-default.png'"
|
||||
mode="widthFix"
|
||||
class="cover-image"
|
||||
/>
|
||||
<view v-if="project.isNew" class="new-badge">NEW</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目基本信息 -->
|
||||
<view class="project-base-info">
|
||||
<text class="project-title">{{ project.title }}</text>
|
||||
<text class="project-desc">{{ project.desc }}</text>
|
||||
|
||||
<view class="project-meta">
|
||||
<view class="meta-item">
|
||||
<uni-icons type="person" size="18" color="#999"></uni-icons>
|
||||
<text class="meta-text">{{ project.author }}</text>
|
||||
</view>
|
||||
|
||||
<view class="meta-item">
|
||||
<uni-icons type="time" size="18" color="#999"></uni-icons>
|
||||
<text class="meta-text">{{ formatTime(project.publishTime) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="meta-item">
|
||||
<uni-icons type="eye" size="18" color="#999"></uni-icons>
|
||||
<text class="meta-text">{{ project.viewCount || 0 }} 浏览</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 交互按钮 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn" @click="handleLike">
|
||||
<uni-icons
|
||||
:type="project.liked ? 'heart-filled' : 'heart'"
|
||||
size="24"
|
||||
:color="project.liked ? '#ff4444' : '#333'"
|
||||
></uni-icons>
|
||||
<text class="action-text">{{ project.liked ? '已点赞' : '点赞' }}</text>
|
||||
<text class="action-count">{{ project.zan || 0 }}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-btn" @click="handleCollect">
|
||||
<uni-icons
|
||||
:type="project.collected ? 'star-filled' : 'star'"
|
||||
size="24"
|
||||
:color="project.collected ? '#ffaa00' : '#333'"
|
||||
></uni-icons>
|
||||
<text class="action-text">{{ project.collected ? '已收藏' : '收藏' }}</text>
|
||||
<text class="action-count">{{ project.collectCount || 0 }}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-btn" @click="viewSourceCode">
|
||||
<uni-icons type="code" size="24" color="#333"></uni-icons>
|
||||
<text class="action-text">源码</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目详情内容 -->
|
||||
<view class="project-content">
|
||||
<view class="section">
|
||||
<text class="section-title">项目介绍</text>
|
||||
<text class="section-content">
|
||||
{{ project.content || '这是一个优秀的Android项目,包含了完整的功能实现和最佳实践。项目采用现代化的架构设计,代码结构清晰,易于维护和扩展。' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="section" v-if="project.features && project.features.length > 0">
|
||||
<text class="section-title">主要功能</text>
|
||||
<view class="feature-list">
|
||||
<view v-for="(feature, index) in project.features" :key="index" class="feature-item">
|
||||
<uni-icons type="checkmark" size="18" color="#3cc51f"></uni-icons>
|
||||
<text class="feature-text">{{ feature }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" v-if="project.techStack && project.techStack.length > 0">
|
||||
<text class="section-title">技术栈</text>
|
||||
<view class="tech-tags">
|
||||
<view v-for="tech in project.techStack" :key="tech" class="tech-tag">
|
||||
<text>{{ tech }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">使用说明</text>
|
||||
<text class="section-content">
|
||||
1. 克隆项目到本地\n
|
||||
2. 使用Android Studio打开项目\n
|
||||
3. 同步Gradle依赖\n
|
||||
4. 连接设备或启动模拟器\n
|
||||
5. 运行项目
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 相关项目 -->
|
||||
<view class="related-projects" v-if="relatedProjects.length > 0">
|
||||
<text class="section-title">相关项目</text>
|
||||
<scroll-view class="related-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="related-list">
|
||||
<view
|
||||
v-for="related in relatedProjects"
|
||||
:key="related.id"
|
||||
class="related-card"
|
||||
@click="viewRelatedProject(related)"
|
||||
>
|
||||
<image :src="related.cover" mode="aspectFill" class="related-cover"></image>
|
||||
<text class="related-title">{{ related.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-bar">
|
||||
<button class="primary-btn" @click="downloadProject">下载项目</button>
|
||||
<button class="secondary-btn" @click="viewSourceCode">查看源码</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
projectId: null,
|
||||
project: {
|
||||
id: 101,
|
||||
title: '玩Android Material Design客户端',
|
||||
desc: '基于Uniapp框架开发的玩Android客户端,完全遵循Material Design 3设计规范',
|
||||
author: 'Material Design',
|
||||
publishTime: 1773715075615,
|
||||
zan: 42,
|
||||
collectCount: 28,
|
||||
viewCount: 156,
|
||||
liked: false,
|
||||
collected: false,
|
||||
cover: '/static/images/projects/project1.png',
|
||||
isNew: true,
|
||||
sourceUrl: 'https://github.com/example/wanandroid-material',
|
||||
content: '这是一个完整的玩Android客户端项目,采用Uniapp框架开发,支持Android和iOS平台。项目完全遵循Material Design 3设计规范,提供了优秀的用户体验。',
|
||||
features: [
|
||||
'Material Design 3设计规范',
|
||||
'响应式布局',
|
||||
'暗色模式支持',
|
||||
'离线缓存',
|
||||
'手势操作',
|
||||
'动画效果'
|
||||
],
|
||||
techStack: ['Uniapp', 'Vue.js', 'JavaScript', 'CSS3', 'HTML5']
|
||||
},
|
||||
|
||||
relatedProjects: [
|
||||
{
|
||||
id: 102,
|
||||
title: 'Android性能优化',
|
||||
cover: '/static/images/projects/project2.png'
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
title: 'Flutter电商应用',
|
||||
cover: '/static/images/projects/project3.png'
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
title: 'Compose组件库',
|
||||
cover: '/static/images/projects/project4.png'
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
title: 'Kotlin协程',
|
||||
cover: '/static/images/projects/project5.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('项目详情页面加载,参数:', options)
|
||||
this.projectId = options.id || 101
|
||||
|
||||
// 模拟从API获取项目详情
|
||||
this.fetchProjectDetail()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取项目详情
|
||||
async fetchProjectDetail() {
|
||||
try {
|
||||
// 这里可以调用API获取项目详情
|
||||
// const res = await uni.request({
|
||||
// url: `http://localhost:3000/api/project/detail/${this.projectId}/json`,
|
||||
// method: 'GET'
|
||||
// })
|
||||
|
||||
// 模拟API响应
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
|
||||
setTimeout(() => {
|
||||
// 根据ID更新项目数据
|
||||
if (this.projectId == 102) {
|
||||
this.project = {
|
||||
...this.project,
|
||||
id: 102,
|
||||
title: 'Android性能优化实战',
|
||||
desc: '包含内存优化、启动优化、卡顿监控等实战案例',
|
||||
author: '性能优化组',
|
||||
zan: 36,
|
||||
collectCount: 45,
|
||||
liked: true,
|
||||
cover: '/static/images/project2.png',
|
||||
content: '这是一个Android性能优化实战项目,涵盖了内存优化、启动优化、卡顿监控等多个方面的最佳实践。',
|
||||
features: [
|
||||
'内存泄漏检测',
|
||||
'启动时间优化',
|
||||
'卡顿监控',
|
||||
'电量优化',
|
||||
'网络优化'
|
||||
],
|
||||
techStack: ['Android', 'Kotlin', 'Profiler', 'Systrace']
|
||||
}
|
||||
} else if (this.projectId == 103) {
|
||||
this.project = {
|
||||
...this.project,
|
||||
id: 103,
|
||||
title: 'Flutter跨平台电商应用',
|
||||
desc: '使用Flutter开发的完整电商应用,支持iOS和Android',
|
||||
author: 'Flutter团队',
|
||||
zan: 89,
|
||||
collectCount: 67,
|
||||
collected: true,
|
||||
cover: '/static/images/project3.png',
|
||||
content: '这是一个使用Flutter开发的跨平台电商应用,支持商品浏览、购物车、订单管理、支付等完整功能。',
|
||||
features: [
|
||||
'跨平台支持',
|
||||
'商品分类浏览',
|
||||
'购物车功能',
|
||||
'订单管理',
|
||||
'支付集成'
|
||||
],
|
||||
techStack: ['Flutter', 'Dart', 'Provider', 'Firebase']
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
console.error('获取项目详情失败:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
// 分享项目
|
||||
handleShare() {
|
||||
uni.share({
|
||||
provider: 'weixin',
|
||||
scene: 'WXSceneSession',
|
||||
type: 0,
|
||||
title: this.project.title,
|
||||
summary: this.project.desc,
|
||||
href: `https://example.com/project/${this.project.id}`,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 点赞处理
|
||||
handleLike() {
|
||||
this.project.liked = !this.project.liked
|
||||
this.project.zan = this.project.liked ? this.project.zan + 1 : Math.max(0, this.project.zan - 1)
|
||||
|
||||
uni.showToast({
|
||||
title: this.project.liked ? '已点赞' : '已取消点赞',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
|
||||
// 收藏处理
|
||||
handleCollect() {
|
||||
this.project.collected = !this.project.collected
|
||||
this.project.collectCount = this.project.collected ?
|
||||
this.project.collectCount + 1 :
|
||||
Math.max(0, this.project.collectCount - 1)
|
||||
|
||||
uni.showToast({
|
||||
title: this.project.collected ? '已收藏' : '已取消收藏',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
|
||||
// 查看源码
|
||||
viewSourceCode() {
|
||||
if (this.project.sourceUrl) {
|
||||
uni.setClipboardData({
|
||||
data: this.project.sourceUrl,
|
||||
success: () => {
|
||||
uni.showModal({
|
||||
title: '源码地址已复制',
|
||||
content: '是否在浏览器中打开?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 在浏览器中打开
|
||||
plus.runtime.openURL(this.project.sourceUrl)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '暂无源码地址',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下载项目
|
||||
downloadProject() {
|
||||
uni.showModal({
|
||||
title: '下载项目',
|
||||
content: '确定要下载该项目吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '下载中...' })
|
||||
|
||||
// 模拟下载过程
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '下载成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 这里可以触发实际的文件下载
|
||||
// const downloadTask = uni.downloadFile({
|
||||
// url: 'https://example.com/project.zip',
|
||||
// success: (downloadResult) => {
|
||||
// uni.saveFile({
|
||||
// tempFilePath: downloadResult.tempFilePath,
|
||||
// success: (saveResult) => {
|
||||
// console.log('文件保存成功:', saveResult.savedFilePath)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 查看相关项目
|
||||
viewRelatedProject(relatedProject) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/project-detail/project-detail?id=${relatedProject.id}`
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '未知时间'
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// 1分钟内
|
||||
if (diff < 60 * 1000) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
// 1小时内
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 1000))}分钟前`
|
||||
}
|
||||
|
||||
// 1天内
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`
|
||||
}
|
||||
|
||||
// 1周内
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`
|
||||
}
|
||||
|
||||
// 显示具体日期
|
||||
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
height: 88rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
/* 内容滚动区域 */
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 项目封面 */
|
||||
.project-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
overflow: hidden;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
position: absolute;
|
||||
top: 30rpx;
|
||||
right: 30rpx;
|
||||
background-color: #ff4444;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
/* 项目基本信息 */
|
||||
.project-base-info {
|
||||
padding: 40rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 交互按钮栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-count {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 项目详情内容 */
|
||||
.project-content {
|
||||
padding: 40rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* 功能列表 */
|
||||
.feature-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 技术栈标签 */
|
||||
.tech-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
/* 相关项目 */
|
||||
.related-projects {
|
||||
padding: 40rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.related-scroll {
|
||||
white-space: nowrap;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.related-list {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
width: 200rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.related-cover {
|
||||
width: 100%;
|
||||
height: 150rpx;
|
||||
}
|
||||
|
||||
.related-title {
|
||||
display: block;
|
||||
padding: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx;
|
||||
background-color: #ffffff;
|
||||
margin-top: 20rpx;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background-color: transparent;
|
||||
color: #3cc51f;
|
||||
border: 1rpx solid #3cc51f;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,687 @@
|
||||
<template>
|
||||
<view class="project-container">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input" @click="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">搜索项目...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<project-category-tabs
|
||||
:categories="categories"
|
||||
:active-category="selectedCategory"
|
||||
@change="handleCategoryChange"
|
||||
/>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<scroll-view
|
||||
class="project-scroll"
|
||||
scroll-y
|
||||
@scrolltolower="loadMoreData"
|
||||
:scroll-with-animation="true"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<common-loading
|
||||
v-if="loading && projectList.length === 0"
|
||||
:visible="true"
|
||||
text="加载中..."
|
||||
/>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<empty-state
|
||||
v-if="!loading && projectList.length === 0"
|
||||
:title="'暂无项目'"
|
||||
:description="selectedCategory ? `当前分类"${selectedCategory.name}"暂无项目` : '暂无项目数据'"
|
||||
:show-action="true"
|
||||
:action-text="'刷新'"
|
||||
@action="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 项目网格 -->
|
||||
<view v-else class="project-grid">
|
||||
<view
|
||||
class="project-card"
|
||||
v-for="project in projectList"
|
||||
:key="project.id"
|
||||
@click="handleProjectClick(project)"
|
||||
>
|
||||
<!-- 项目封面 -->
|
||||
<view class="project-cover">
|
||||
<image
|
||||
:src="getProjectCover(project)"
|
||||
mode="aspectFill"
|
||||
class="cover-image"
|
||||
lazy-load
|
||||
/>
|
||||
<view class="cover-overlay">
|
||||
<text class="cover-title">{{ project.title }}</text>
|
||||
</view>
|
||||
<view class="project-badges">
|
||||
<text class="badge new" v-if="isNewProject(project)">NEW</text>
|
||||
<text class="badge hot" v-if="isHotProject(project)">HOT</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目信息 -->
|
||||
<view class="project-info">
|
||||
<text class="project-title">{{ project.title }}</text>
|
||||
<text class="project-desc" v-if="project.desc">{{ project.desc }}</text>
|
||||
|
||||
<!-- 项目元数据 -->
|
||||
<view class="project-meta">
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">👤</text>
|
||||
<text class="meta-text">{{ project.author || '未知作者' }}</text>
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">🕒</text>
|
||||
<text class="meta-text">{{ formatRelativeTime(project.publishTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目统计 -->
|
||||
<view class="project-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-icon">❤️</text>
|
||||
<text class="stat-text">{{ project.zan || 0 }}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-icon">⭐</text>
|
||||
<text class="stat-text">{{ project.collectCount || 0 }}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-icon">👁️</text>
|
||||
<text class="stat-text">{{ project.viewCount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目标签 -->
|
||||
<view class="project-tags" v-if="project.tags && project.tags.length > 0">
|
||||
<text
|
||||
class="tag"
|
||||
v-for="tag in project.tags.slice(0, 3)"
|
||||
:key="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</text>
|
||||
<text class="tag-more" v-if="project.tags.length > 3">
|
||||
+{{ project.tags.length - 3 }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目操作 -->
|
||||
<view class="project-actions">
|
||||
<view class="action-item" @click.stop="handleLike(project)">
|
||||
<text class="action-icon" :class="{liked: project.liked}">❤️</text>
|
||||
<text class="action-text">{{ project.liked ? '已赞' : '点赞' }}</text>
|
||||
</view>
|
||||
<view class="action-item" @click.stop="handleCollect(project)">
|
||||
<text class="action-icon" :class="{collected: project.collected}">⭐</text>
|
||||
<text class="action-text">{{ project.collected ? '已收藏' : '收藏' }}</text>
|
||||
</view>
|
||||
<view class="action-item" @click.stop="handleViewSource(project)">
|
||||
<text class="action-icon">💻</text>
|
||||
<text class="action-text">源码</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="projectList.length > 0">
|
||||
<uni-load-more
|
||||
:status="loadMoreStatus"
|
||||
:content-text="loadMoreText"
|
||||
@clickLoadMore="loadMoreData"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 导入公共组件
|
||||
import CommonLoading from '@/components/common/loading.vue'
|
||||
import EmptyState from '@/components/common/empty-state.vue'
|
||||
import ProjectCategoryTabs from '@/components/project/category-tabs.vue'
|
||||
|
||||
// 导入API
|
||||
import { getProjectModuleData, getProjectList } from '@/common/api/project.api.js'
|
||||
|
||||
// 导入工具函数
|
||||
import { formatRelativeTime } from '@/common/utils/common.js'
|
||||
|
||||
export default {
|
||||
name: 'ProjectPage',
|
||||
components: {
|
||||
CommonLoading,
|
||||
EmptyState,
|
||||
ProjectCategoryTabs
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 分类数据
|
||||
categories: [],
|
||||
selectedCategory: null,
|
||||
// 项目数据
|
||||
projectList: [],
|
||||
// 分页信息
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
pageCount: 0,
|
||||
// 加载状态
|
||||
loading: false,
|
||||
loadMoreStatus: 'more', // more, loading, noMore
|
||||
// 加载文本
|
||||
loadMoreText: {
|
||||
contentdown: '上拉显示更多',
|
||||
contentrefresh: '正在加载...',
|
||||
contentnomore: '没有更多项目了'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.initProjectData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
console.log('项目页面下拉刷新')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化项目数据
|
||||
async initProjectData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const projectData = await getProjectModuleData()
|
||||
this.categories = projectData.categories || []
|
||||
this.selectedCategory = projectData.selectedCategory
|
||||
this.projectList = projectData.projectList || []
|
||||
this.total = projectData.total || 0
|
||||
this.pageCount = projectData.pageCount || 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 = 1
|
||||
this.projectList = []
|
||||
await this.initProjectData()
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
},
|
||||
|
||||
// 加载更多数据
|
||||
async loadMoreData() {
|
||||
if (this.loadMoreStatus !== 'more') return
|
||||
|
||||
this.loadMoreStatus = 'loading'
|
||||
try {
|
||||
const nextPage = this.currentPage + 1
|
||||
const projectData = await getProjectList(this.selectedCategory.id, nextPage)
|
||||
|
||||
// 追加数据
|
||||
this.projectList = [...this.projectList, ...(projectData.datas || [])]
|
||||
this.currentPage = nextPage
|
||||
this.total = projectData.total || 0
|
||||
this.pageCount = projectData.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) {
|
||||
this.loadMoreStatus = 'noMore'
|
||||
} else {
|
||||
this.loadMoreStatus = 'more'
|
||||
}
|
||||
},
|
||||
|
||||
// 处理分类切换
|
||||
async handleCategoryChange(category) {
|
||||
if (this.selectedCategory?.id === category.id) return
|
||||
|
||||
this.selectedCategory = category
|
||||
this.currentPage = 1
|
||||
this.projectList = []
|
||||
this.loadMoreStatus = 'more'
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const projectData = await getProjectList(category.id, 1)
|
||||
this.projectList = projectData.datas || []
|
||||
this.total = projectData.total || 0
|
||||
this.pageCount = projectData.pageCount || 0
|
||||
|
||||
// 更新加载状态
|
||||
this.updateLoadMoreStatus()
|
||||
} catch (error) {
|
||||
console.error('切换分类失败:', error)
|
||||
uni.showToast({
|
||||
title: '切换分类失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 处理项目点击
|
||||
handleProjectClick(project) {
|
||||
console.log('点击项目:', project)
|
||||
this.$emit('project-click', project)
|
||||
|
||||
// 跳转到项目详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/project-detail/project-detail?id=${project.id}`
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到搜索页面
|
||||
goToSearch() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/search?type=project'
|
||||
})
|
||||
},
|
||||
|
||||
// 处理点赞
|
||||
handleLike(project) {
|
||||
console.log('点赞项目:', project.id)
|
||||
project.liked = !project.liked
|
||||
project.zan = project.liked ? (project.zan || 0) + 1 : Math.max(0, (project.zan || 0) - 1)
|
||||
|
||||
uni.showToast({
|
||||
title: project.liked ? '点赞成功' : '取消点赞',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
},
|
||||
|
||||
// 处理收藏
|
||||
handleCollect(project) {
|
||||
console.log('收藏项目:', project.id)
|
||||
project.collected = !project.collected
|
||||
project.collectCount = project.collected ?
|
||||
(project.collectCount || 0) + 1 :
|
||||
Math.max(0, (project.collectCount || 0) - 1)
|
||||
|
||||
uni.showToast({
|
||||
title: project.collected ? '收藏成功' : '取消收藏',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
},
|
||||
|
||||
// 查看源码
|
||||
handleViewSource(project) {
|
||||
console.log('查看源码:', project.id)
|
||||
if (project.link) {
|
||||
uni.setClipboardData({
|
||||
data: project.link,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '源码地址已复制',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '暂无源码地址',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 获取项目封面图
|
||||
getProjectCover(project) {
|
||||
// 如果有封面图,直接使用
|
||||
if (project.envelopePic) {
|
||||
return project.envelopePic
|
||||
}
|
||||
|
||||
// 根据项目ID生成不同的默认封面
|
||||
const defaultCovers = [
|
||||
'/static/images/projects/project1.png',
|
||||
'/static/images/projects/project2.png',
|
||||
'/static/images/projects/project3.png',
|
||||
'/static/images/projects/project4.png',
|
||||
'/static/images/projects/project5.png'
|
||||
]
|
||||
|
||||
const index = (project.id || 0) % defaultCovers.length
|
||||
return defaultCovers[index]
|
||||
},
|
||||
|
||||
// 判断是否是新项目(7天内)
|
||||
isNewProject(project) {
|
||||
if (!project.publishTime) return false
|
||||
const publishTime = new Date(project.publishTime).getTime()
|
||||
const now = Date.now()
|
||||
return now - publishTime < 7 * 24 * 60 * 60 * 1000
|
||||
},
|
||||
|
||||
// 判断是否是热门项目(点赞数超过50)
|
||||
isHotProject(project) {
|
||||
return (project.zan || 0) > 50
|
||||
},
|
||||
|
||||
// 工具函数
|
||||
formatRelativeTime
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 项目滚动区域 */
|
||||
.project-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 项目网格 */
|
||||
.project-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 30rpx;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
width: calc(50% - 15rpx);
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.project-card:active {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 项目封面 */
|
||||
.project-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cover-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-badges {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge.new {
|
||||
background-color: #ff4444;
|
||||
}
|
||||
|
||||
.badge.hot {
|
||||
background-color: #ff9900;
|
||||
}
|
||||
|
||||
/* 项目信息 */
|
||||
.project-info {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 15rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 项目元数据 */
|
||||
.project-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 项目统计 */
|
||||
.project-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 项目标签 */
|
||||
.project-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 6rpx 12rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tag-more {
|
||||
padding: 6rpx 12rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 项目操作 */
|
||||
.project-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.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: #f0f0f0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 36rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.action-icon.liked {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.action-icon.collected {
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,752 @@
|
||||
<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>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="placeholder">
|
||||
<text class="placeholder-icon">📱</text>
|
||||
<text class="placeholder-title">公众号页面</text>
|
||||
<text class="placeholder-desc">开发中,敬请期待...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('公众号页面加载')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 120rpx;
|
||||
color: #07c160;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user