a
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user