1176 lines
25 KiB
Vue
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> |