Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

1176 lines
25 KiB
Vue

<template>
<view class="page-container">
<!-- 状态栏占位 -->
<view class="status-bar"></view>
<!-- 顶部搜索栏 -->
<view class="search-bar elevation-1">
<view class="search-container" @click="onSearchClick">
<view class="search-icon">🔍</view>
<view class="search-placeholder">搜索文章、项目、公众号...</view>
</view>
</view>
<!-- 主要内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onScrollToLower"
@scroll="onScroll"
>
<!-- Banner轮播 -->
<view class="banner-section" v-if="banners.length > 0">
<swiper
class="banner-swiper"
:indicator-dots="true"
:autoplay="true"
:interval="4000"
:duration="500"
circular
indicator-active-color="#6200EE"
>
<swiper-item v-for="(item, index) in banners" :key="index">
<view class="banner-item" @click="onBannerClick(item)">
<image
class="banner-image"
:src="item.imagePath"
mode="aspectFill"
lazy-load
/>
<view class="banner-overlay">
<view class="banner-title">{{ item.title }}</view>
<view class="banner-desc" v-if="item.desc">{{ item.desc }}</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
<!-- 搜索热词 -->
<view class="hot-words-section card elevation-1" v-if="hotWords.length > 0">
<view class="section-header">
<view class="section-title">
<view class="section-icon">🔥</view>
<text class="section-text">热门搜索</text>
</view>
<view class="section-action" @click="refreshHotWords">
<text class="action-text">换一批</text>
</view>
</view>
<view class="words-grid">
<view
class="word-chip"
v-for="(word, index) in hotWords"
:key="word.id"
:style="{ animationDelay: `${index * 0.1}s` }"
@click="onHotWordClick(word)"
>
<text class="word-text">{{ word.name }}</text>
</view>
</view>
</view>
<!-- 文章列表 -->
<view class="articles-section">
<view class="section-header">
<view class="section-title">
<view class="section-icon">📰</view>
<text class="section-text">最新文章</text>
<text class="section-count" v-if="total > 0">({{ total }})</text>
</view>
<view class="filter-tabs">
<view
class="filter-tab"
:class="{ active: filter === 'all' }"
@click="changeFilter('all')"
>
全部
</view>
<view
class="filter-tab"
:class="{ active: filter === 'new' }"
@click="changeFilter('new')"
>
最新
</view>
<view
class="filter-tab"
:class="{ active: filter === 'hot' }"
@click="changeFilter('hot')"
>
热门
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading && articles.length === 0">
<view class="loading-spinner"></view>
<text class="loading-text">正在加载...</text>
</view>
<!-- 文章列表 -->
<view v-else>
<!-- 置顶文章 -->
<view
class="article-card featured elevation-1"
v-for="article in featuredArticles"
:key="'featured-' + article.id"
@click="onArticleClick(article)"
>
<view class="article-badge">置顶</view>
<view class="article-content">
<view class="article-header">
<view class="article-title">{{ article.title }}</view>
<view class="article-tag new" v-if="article.fresh">NEW</view>
</view>
<view class="article-meta">
<view class="meta-item">
<view class="meta-icon">👤</view>
<text class="meta-text">{{ article.author || article.shareUser || '未知作者' }}</text>
</view>
<view class="meta-item">
<view class="meta-icon">🏷️</view>
<text class="meta-text">{{ article.superChapterName }}</text>
</view>
<view class="meta-item">
<view class="meta-icon">🕒</view>
<text class="meta-text">{{ formatTime(article.publishTime) }}</text>
</view>
</view>
<view class="article-stats">
<view class="stat-item">
<view class="stat-icon">❤️</view>
<text class="stat-count">{{ article.zan }}</text>
</view>
<view class="stat-item">
<view class="stat-icon">👁️</view>
<text class="stat-count">{{ article.id % 1000 }}</text>
</view>
</view>
</view>
</view>
<!-- 普通文章 -->
<view
class="article-card elevation-1"
v-for="article in filteredArticles"
:key="article.id"
@click="onArticleClick(article)"
>
<view class="article-content">
<view class="article-header">
<view class="article-title">{{ article.title }}</view>
<view class="article-tag new" v-if="article.fresh">NEW</view>
<view class="article-tag hot" v-if="article.zan > 50">HOT</view>
</view>
<view class="article-meta">
<view class="meta-item">
<view class="meta-icon">👤</view>
<text class="meta-text">{{ article.author || article.shareUser || '未知作者' }}</text>
</view>
<view class="meta-item">
<view class="meta-icon">🏷️</view>
<text class="meta-text">{{ article.chapterName }}</text>
</view>
</view>
<view class="article-footer">
<view class="footer-left">
<view class="time-text">{{ formatRelativeTime(article.publishTime) }}</view>
</view>
<view class="footer-right">
<view class="action-button" @click.stop="toggleCollect(article)">
<view class="action-icon" :class="{ collected: article.collect }">
{{ article.collect ? '⭐' : '☆' }}
</view>
</view>
<view class="action-button" @click.stop="shareArticle(article)">
<view class="action-icon">↗️</view>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore">
<view class="load-more-button" @click="loadMore" v-if="!loadingMore">
<text class="load-more-text">加载更多</text>
<view class="load-more-icon">↓</view>
</view>
<view class="loading-more" v-else>
<view class="loading-spinner small"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
<!-- 没有更多 -->
<view class="no-more" v-if="!hasMore && articles.length > 0">
<text class="no-more-text">—— 没有更多内容了 ——</text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && articles.length === 0">
<view class="empty-icon">📄</view>
<text class="empty-text">暂无文章</text>
<text class="empty-desc">下拉刷新试试</text>
</view>
</view>
</view>
<!-- 底部安全区域 -->
<view class="safe-area"></view>
</scroll-view>
<!-- 回到顶部按钮 -->
<view
class="back-to-top elevation-3"
:class="{ show: showBackTop }"
@click="scrollToTop"
>
<view class="back-top-icon">↑</view>
</view>
<!-- 浮动操作按钮 -->
<view class="fab" @click="onFabClick">
<view class="fab-icon">+</view>
</view>
</view>
</template>
<script>
import { api } from '@/utils/request.js'
export default {
data() {
return {
// Banner数据
banners: [],
// 搜索热词
hotWords: [],
// 文章数据
articles: [],
featuredArticles: [],
// 分页信息
currentPage: 0,
pageSize: 20,
total: 0,
hasMore: true,
// 加载状态
loading: false,
loadingMore: false,
refreshing: false,
// 筛选条件
filter: 'all',
// UI状态
showBackTop: false,
scrollTop: 0
}
},
computed: {
// 过滤后的文章
filteredArticles() {
if (this.filter === 'all') {
return this.articles
} else if (this.filter === 'new') {
return this.articles.filter(article => article.fresh)
} else if (this.filter === 'hot') {
return this.articles.filter(article => article.zan > 10)
}
return this.articles
}
},
onLoad() {
this.initData()
},
onPullDownRefresh() {
this.onRefresh()
},
onReachBottom() {
this.onScrollToLower()
},
onPageScroll(e) {
this.scrollTop = e.scrollTop
this.showBackTop = e.scrollTop > 300
},
methods: {
// 初始化数据
async initData() {
this.loading = true
try {
await Promise.all([
this.fetchBanners(),
this.fetchHotWords(),
this.fetchArticles(0)
])
} catch (error) {
console.error('初始化数据失败:', error)
this.showError('数据加载失败')
} finally {
this.loading = false
uni.stopPullDownRefresh()
}
},
// 刷新数据
async onRefresh() {
this.refreshing = true
this.currentPage = 0
this.articles = []
this.featuredArticles = []
try {
await Promise.all([
this.fetchBanners(),
this.fetchHotWords(),
this.fetchArticles(0)
])
this.showSuccess('刷新成功')
} catch (error) {
console.error('刷新失败:', error)
this.showError('刷新失败')
} finally {
this.refreshing = false
uni.stopPullDownRefresh()
}
},
// 滚动到底部
onScrollToLower() {
if (this.hasMore && !this.loadingMore) {
this.loadMore()
}
},
// 滚动事件
onScroll(e) {
this.scrollTop = e.detail.scrollTop
this.showBackTop = e.detail.scrollTop > 300
},
// 加载更多
async loadMore() {
if (!this.hasMore || this.loadingMore) return
this.loadingMore = true
try {
const nextPage = this.currentPage + 1
await this.fetchArticles(nextPage)
} catch (error) {
console.error('加载更多失败:', error)
this.showError('加载失败')
} finally {
this.loadingMore = false
}
},
// 获取Banner数据
async fetchBanners() {
try {
const data = await api.getBanners()
this.banners = data.data || []
} catch (error) {
console.error('获取Banner失败:', error)
}
},
// 获取搜索热词
async fetchHotWords() {
try {
const data = await api.getHotWords()
this.hotWords = data.data || []
} catch (error) {
console.error('获取热词失败:', error)
}
},
// 获取文章列表
async fetchArticles(page) {
try {
const data = await api.getArticles(page, this.pageSize)
const articles = data.data?.datas || []
if (page === 0) {
// 第一页,分离置顶文章
this.featuredArticles = articles.filter(article => article.type === 1)
this.articles = articles.filter(article => article.type !== 1)
} else {
this.articles = [...this.articles, ...articles]
}
this.currentPage = page
this.total = data.data?.total || 0
this.hasMore = !data.data?.over
} catch (error) {
console.error('获取文章失败:', error)
throw error
}
},
// 刷新热词
async refreshHotWords() {
uni.showLoading({ title: '刷新中...' })
try {
await this.fetchHotWords()
this.showSuccess('热词已更新')
} catch (error) {
this.showError('刷新失败')
} finally {
uni.hideLoading()
}
},
// 切换筛选条件
changeFilter(filter) {
this.filter = filter
},
// 点击搜索
onSearchClick() {
uni.navigateTo({
url: '/pages/search/search'
})
},
// 点击Banner
onBannerClick(item) {
console.log('点击Banner:', item)
this.navigateToWebView(item.title, item.url)
},
// 点击热词
onHotWordClick(word) {
console.log('点击热词:', word)
uni.navigateTo({
url: `/pages/search/search?keyword=${encodeURIComponent(word.name)}`
})
},
// 点击文章
onArticleClick(article) {
console.log('点击文章:', article)
this.navigateToWebView(article.title, article.link)
},
// 切换收藏
toggleCollect(article) {
article.collect = !article.collect
this.showSuccess(article.collect ? '已收藏' : '已取消收藏')
},
// 分享文章
shareArticle(article) {
uni.showActionSheet({
itemList: ['分享到微信', '分享到QQ', '复制链接'],
success: (res) => {
if (res.tapIndex === 0) {
this.showSuccess('已分享到微信')
} else if (res.tapIndex === 1) {
this.showSuccess('已分享到QQ')
} else if (res.tapIndex === 2) {
uni.setClipboardData({
data: article.link,
success: () => {
this.showSuccess('链接已复制')
}
})
}
}
})
},
// 点击FAB
onFabClick() {
uni.showActionSheet({
itemList: ['写文章', '分享链接', '反馈问题'],
success: (res) => {
if (res.tapIndex === 0) {
this.showInfo('写文章功能开发中')
} else if (res.tapIndex === 1) {
this.showInfo('分享功能开发中')
} else if (res.tapIndex === 2) {
this.showInfo('反馈功能开发中')
}
}
})
},
// 跳转到WebView
navigateToWebView(title, url) {
uni.navigateTo({
url: `/pages/webview/webview?title=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`
})
},
// 回到顶部
scrollToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 300
})
},
// 格式化时间
formatTime(timestamp) {
if (!timestamp) return ''
const date = new Date(timestamp)
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
},
// 格式化相对时间
formatRelativeTime(timestamp) {
if (!timestamp) return ''
const now = Date.now()
const diff = now - timestamp
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
if (diff < minute) {
return '刚刚'
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`
} else if (diff < 7 * day) {
return `${Math.floor(diff / day)}天前`
} else {
return this.formatTime(timestamp)
}
},
// 显示成功提示
showSuccess(message) {
uni.showToast({
title: message,
icon: 'success',
duration: 2000
})
},
// 显示错误提示
showError(message) {
uni.showToast({
title: message,
icon: 'none',
duration: 3000
})
},
// 显示信息提示
showInfo(message) {
uni.showToast({
title: message,
icon: 'none',
duration: 2000
})
}
}
}
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: #F5F5F5;
}
/* 状态栏 */
.status-bar {
height: var(--status-bar-height);
background: #6200EE;
}
/* 搜索栏 */
.search-bar {
background: #6200EE;
padding: 12px 16px;
}
.search-container {
background: rgba(255, 255, 255, 0.9);
border-radius: 24px;
padding: 12px 16px;
display: flex;
align-items: center;
transition: all 0.3s;
}
.search-container:active {
background: rgba(255, 255, 255, 1);
transform: scale(0.98);
}
.search-icon {
font-size: 20px;
margin-right: 12px;
color: #6200EE;
}
.search-placeholder {
font-size: 16px;
color: #757575;
flex: 1;
}
/* 内容区域 */
.content-scroll {
height: calc(100vh - var(--status-bar-height) - 60px);
}
/* Banner区域 */
.banner-section {
padding: 16px;
}
.banner-swiper {
height: 200px;
border-radius: 12px;
overflow: hidden;
}
.banner-item {
position: relative;
width: 100%;
height: 100%;
}
.banner-image {
width: 100%;
height: 100%;
}
.banner-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 16px;
}
.banner-title {
color: white;
font-size: 18px;
font-weight: 500;
margin-bottom: 4px;
}
.banner-desc {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
/* 热词区域 */
.hot-words-section {
margin: 0 16px 16px;
padding: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-title {
display: flex;
align-items: center;
}
.section-icon {
font-size: 20px;
margin-right: 8px;
color: #6200EE;
}
.section-text {
font-size: 18px;
font-weight: 500;
color: #212121;
}
.section-count {
font-size: 14px;
color: #757575;
margin-left: 8px;
}
.section-action {
padding: 6px 12px;
border-radius: 16px;
background: #F5F5F5;
transition: all 0.3s;
}
.section-action:active {
background: #E0E0E0;
transform: scale(0.95);
}
.action-text {
font-size: 14px;
color: #6200EE;
font-weight: 500;
}
.words-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
}
.word-chip {
background: #F5F5F5;
border-radius: 20px;
padding: 10px 16px;
text-align: center;
transition: all 0.3s;
animation: fadeInUp 0.5s ease-out;
animation-fill-mode: both;
}
.word-chip:active {
background: #E0E0E0;
transform: scale(0.95);
}
.word-text {
font-size: 14px;
color: #424242;
font-weight: 500;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 文章区域 */
.articles-section {
padding: 0 16px 32px;
}
.filter-tabs {
display: flex;
gap: 12px;
}
.filter-tab {
padding: 6px 16px;
border-radius: 20px;
background: #F5F5F5;
font-size: 14px;
color: #757575;
transition: all 0.3s;
}
.filter-tab.active {
background: #6200EE;
color: white;
}
.filter-tab:active {
transform: scale(0.95);
}
/* 文章卡片 */
.article-card {
background: white;
border-radius: 12px;
margin-bottom: 16px;
padding: 20px;
transition: all 0.3s;
position: relative;
}
.article-card.featured {
border-left: 4px solid #6200EE;
}
.article-card:active {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.article-badge {
position: absolute;
top: -8px;
left: 20px;
background: #6200EE;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.article-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.article-title {
flex: 1;
font-size: 18px;
font-weight: 500;
color: #212121;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.article-tag {
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
margin-left: 8px;
}
.article-tag.new {
background: #FF4081;
color: white;
}
.article-tag.hot {
background: #FF9800;
color: white;
}
.article-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
.meta-item {
display: flex;
align-items: center;
}
.meta-icon {
font-size: 16px;
margin-right: 6px;
opacity: 0.7;
}
.meta-text {
font-size: 14px;
color: #616161;
}
.article-stats {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
align-items: center;
}
.stat-icon {
font-size: 16px;
margin-right: 6px;
color: #757575;
}
.stat-count {
font-size: 14px;
color: #757575;
}
.article-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16px;
border-top: 1px solid #EEEEEE;
}
.time-text {
font-size: 14px;
color: #9E9E9E;
}
.footer-right {
display: flex;
gap: 12px;
}
.action-button {
width: 36px;
height: 36px;
border-radius: 50%;
background: #F5F5F5;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.action-button:active {
background: #E0E0E0;
transform: scale(0.9);
}
.action-icon {
font-size: 20px;
color: #757575;
}
.action-icon.collected {
color: #FF9800;
}
/* 加载状态 */
.loading-state {
padding: 60px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #E0E0E0;
border-top: 3px solid #6200EE;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
.loading-spinner.small {
width: 24px;
height: 24px;
border-width: 2px;
margin-bottom: 0;
margin-right: 12px;
}
.loading-text {
font-size: 16px;
color: #757575;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 加载更多 */
.load-more {
padding: 32px 0;
display: flex;
justify-content: center;
}
.load-more-button {
display: flex;
align-items: center;
padding: 12px 24px;
background: #6200EE;
color: white;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
}
.load-more-button:active {
background: #3700B3;
transform: scale(0.95);
}
.load-more-icon {
margin-left: 8px;
font-size: 20px;
}
.loading-more {
display: flex;
align-items: center;
}
.no-more {
padding: 32px 0;
text-align: center;
}
.no-more-text {
font-size: 14px;
color: #9E9E9E;
}
/* 空状态 */
.empty-state {
padding: 80px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-icon {
font-size: 64px;
color: #BDBDBD;
margin-bottom: 24px;
}
.empty-text {
font-size: 18px;
color: #757575;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #9E9E9E;
}
/* 安全区域 */
.safe-area {
height: env(safe-area-inset-bottom);
min-height: 20px;
}
/* 回到顶部 */
.back-to-top {
position: fixed;
right: 20px;
bottom: 100px;
width: 56px;
height: 56px;
background: #6200EE;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s;
z-index: 1000;
}
.back-to-top.show {
opacity: 1;
transform: translateY(0);
}
.back-to-top:active {
background: #3700B3;
transform: translateY(0) scale(0.95);
}
.back-top-icon {
color: white;
font-size: 24px;
font-weight: bold;
}
/* 浮动操作按钮 */
.fab {
position: fixed;
right: 20px;
bottom: 180px;
width: 56px;
height: 56px;
background: #03DAC6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(3, 218, 198, 0.3);
transition: all 0.3s;
z-index: 1000;
}
.fab:active {
background: #018786;
transform: scale(0.95);
}
.fab-icon {
color: white;
font-size: 24px;
font-weight: bold;
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.page-container {
background: #121212;
}
.search-bar {
background: #1E1E1E;
}
.search-container {
background: rgba(255, 255, 255, 0.1);
}
.search-placeholder {
color: #B0B0B0;
}
.article-card {
background: #1E1E1E;
}
.article-title {
color: #FFFFFF;
}
.meta-text {
color: #B0B0B0;
}
.article-footer {
border-top-color: #333333;
}
.action-button {
background: #2D2D2D;
}
.filter-tab {
background: #2D2D2D;
color: #B0B0B0;
}
.filter-tab.active {
background: #6200EE;
color: white;
}
}
</style>