feat: enhance collection categories page
This commit is contained in:
@@ -7,64 +7,440 @@
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</page-meta>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="collectData"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
height="100%"
|
||||
use-page-scroll
|
||||
>
|
||||
<u-swipe-action
|
||||
:show="item.show"
|
||||
:index="index"
|
||||
v-for="(item, index) in collectData"
|
||||
:key="item.id"
|
||||
@click="handleCollect"
|
||||
:options="options"
|
||||
btn-width="120"
|
||||
>
|
||||
<news-card :item="item" :newsId="item.article_id"></news-card>
|
||||
</u-swipe-action>
|
||||
</z-paging>
|
||||
|
||||
<view class="collection-page">
|
||||
<scroll-view scroll-x class="collection-tabs" :show-scrollbar="false">
|
||||
<view class="collection-tabs__inner">
|
||||
<view v-for="tab in collectionTabs" :key="tab.key" class="collection-tab"
|
||||
:class="{ 'collection-tab--active': activeType === tab.key }"
|
||||
@tap="switchType(tab.key)">
|
||||
<u-icon :name="tab.icon" size="28" :color="activeType === tab.key ? '#1468f5' : '#7b8492'" />
|
||||
<text>{{ tab.label }}</text>
|
||||
<text v-if="tab.key === 'article' && articleCount" class="collection-tab__count">{{ articleCount }}</text>
|
||||
<text v-if="tab.key === 'crypto' && cryptoCount" class="collection-tab__count">{{ cryptoCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view scroll-y class="collection-scroll" refresher-enabled :refresher-triggered="refreshing"
|
||||
@refresherrefresh="refreshCollection">
|
||||
<view v-if="loading" class="collection-state">
|
||||
<u-loading mode="circle" color="#1468f5" />
|
||||
<text>加载收藏中...</text>
|
||||
</view>
|
||||
|
||||
<template v-else-if="activeType === 'article'">
|
||||
<view v-if="articles.length" class="collection-list">
|
||||
<view v-for="item in articles" :key="item.id || item.article_id" class="article-item"
|
||||
@tap="openArticle(item)">
|
||||
<image v-if="item.image" class="article-item__image" :src="item.image" mode="aspectFill" />
|
||||
<view v-else class="article-item__image article-item__image--empty">
|
||||
<u-icon name="file-text" size="38" color="#8cb4f4" />
|
||||
</view>
|
||||
<view class="article-item__body">
|
||||
<text class="article-item__title">{{ item.title || '未命名文章' }}</text>
|
||||
<text v-if="item.desc" class="article-item__desc">{{ item.desc }}</text>
|
||||
<view class="article-item__meta">
|
||||
<text>{{ item.collect_time || item.create_time || '' }}</text>
|
||||
<view class="article-item__remove" @tap.stop="removeArticle(item)">
|
||||
<u-icon name="star-fill" size="26" color="#f0ad4e" />
|
||||
<text>取消收藏</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="collection-state collection-state--empty">
|
||||
<u-icon name="star" size="72" color="#cbd5e1" />
|
||||
<text class="collection-state__title">暂无资讯收藏</text>
|
||||
<text class="collection-state__desc">收藏文章后会显示在这里</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeType === 'crypto'">
|
||||
<view v-if="coins.length" class="collection-list coin-list">
|
||||
<view v-for="coin in coins" :key="coin.symbol" class="coin-item" @tap="openCoin(coin)">
|
||||
<image v-if="coin.image" class="coin-item__icon" :src="coin.image" mode="aspectFit" />
|
||||
<view v-else class="coin-item__icon coin-item__icon--fallback">{{ coin.symbol.slice(0, 1) }}</view>
|
||||
<view class="coin-item__name">
|
||||
<text>{{ coin.symbol }}</text>
|
||||
<text>{{ coin.name_zh || coin.name }}</text>
|
||||
</view>
|
||||
<view class="coin-item__quote">
|
||||
<text>${{ formatPrice(coin.current_price) }}</text>
|
||||
<text :class="coin.price_change_percentage_24h >= 0 ? 'is-up' : 'is-down'">
|
||||
{{ formatChange(coin.price_change_percentage_24h) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="coin-item__remove" @tap.stop="removeCoin(coin)">
|
||||
<u-icon name="star-fill" size="28" color="#f0ad4e" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="collection-state collection-state--empty">
|
||||
<u-icon name="star" size="72" color="#cbd5e1" />
|
||||
<text class="collection-state__title">暂无加密货币收藏</text>
|
||||
<text class="collection-state__desc">在行情页点击星标即可加入收藏</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else class="collection-state collection-state--empty">
|
||||
<u-icon name="star" size="72" color="#cbd5e1" />
|
||||
<text class="collection-state__title">暂无{{ activeTabLabel }}收藏</text>
|
||||
<text class="collection-state__desc">该分类的收藏内容会显示在这里</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, shallowRef } from 'vue'
|
||||
import { getCollect, cancelCollect } from '@/api/news'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { cancelCollect, getCollect } from '@/api/news'
|
||||
import {
|
||||
cancelCryptoFavorite,
|
||||
formatPrice,
|
||||
getCryptoFavorites,
|
||||
getCryptoMarket,
|
||||
type CryptoMarketCoin
|
||||
} from '@/api/crypto'
|
||||
|
||||
const paging = shallowRef()
|
||||
const options = reactive([
|
||||
{
|
||||
text: '取消收藏',
|
||||
style: {
|
||||
color: '#FFFFFF',
|
||||
backgroundColor: '#FF2C3C'
|
||||
}
|
||||
}
|
||||
])
|
||||
const collectData: any = ref([])
|
||||
type CollectionType = 'article' | 'post' | 'match' | 'crypto'
|
||||
|
||||
const queryList = async (pageNo, pageSize) => {
|
||||
const { lists } = await getCollect()
|
||||
lists.forEach((item: any) => {
|
||||
item.show = false
|
||||
})
|
||||
collectData.value = lists
|
||||
paging.value.complete(lists)
|
||||
const collectionTabs: Array<{ key: CollectionType; label: string; icon: string }> = [
|
||||
{ key: 'article', label: '资讯', icon: 'file-text' },
|
||||
{ key: 'post', label: '帖子', icon: 'chat' },
|
||||
{ key: 'match', label: '比赛', icon: 'calendar' },
|
||||
{ key: 'crypto', label: '加密货币', icon: 'star' }
|
||||
]
|
||||
|
||||
const activeType = ref<CollectionType>('article')
|
||||
const articles = ref<any[]>([])
|
||||
const coins = ref<CryptoMarketCoin[]>([])
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
|
||||
const articleCount = computed(() => articles.value.length)
|
||||
const cryptoCount = computed(() => coins.value.length)
|
||||
const activeTabLabel = computed(() => collectionTabs.find((tab) => tab.key === activeType.value)?.label || '')
|
||||
|
||||
const loadArticles = async () => {
|
||||
const result = await getCollect()
|
||||
articles.value = Array.isArray(result?.lists) ? result.lists : []
|
||||
}
|
||||
|
||||
const handleCollect = async (index: number): Promise<void> => {
|
||||
const loadCrypto = async () => {
|
||||
const [market, favorites] = await Promise.all([getCryptoMarket(), getCryptoFavorites()])
|
||||
const symbols = Array.isArray(favorites?.symbols)
|
||||
? favorites.symbols.map((symbol) => String(symbol).toUpperCase())
|
||||
: []
|
||||
const symbolSet = new Set(symbols)
|
||||
coins.value = Array.isArray(market?.coins)
|
||||
? market.coins.filter((coin) => symbolSet.has(String(coin.symbol).toUpperCase()))
|
||||
: []
|
||||
}
|
||||
|
||||
const loadCollection = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const article_id: number = collectData.value[index].article_id
|
||||
await cancelCollect({ id: article_id })
|
||||
uni.$u.toast('已取消收藏')
|
||||
paging.value.reload()
|
||||
} catch (err) {
|
||||
//TODO handle the exception
|
||||
console.log('取消收藏报错=>', err)
|
||||
if (activeType.value === 'article') {
|
||||
await loadArticles()
|
||||
} else if (activeType.value === 'crypto') {
|
||||
await loadCrypto()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Collection] load failed:', error)
|
||||
uni.showToast({ title: '收藏加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchType = (type: CollectionType) => {
|
||||
if (activeType.value === type) return
|
||||
activeType.value = type
|
||||
loadCollection()
|
||||
}
|
||||
|
||||
const refreshCollection = () => {
|
||||
refreshing.value = true
|
||||
loadCollection()
|
||||
}
|
||||
|
||||
const openArticle = (item: any) => {
|
||||
const id = item.article_id || item.id
|
||||
if (id) uni.navigateTo({ url: `/pages/news_detail/news_detail?id=${id}` })
|
||||
}
|
||||
|
||||
const removeArticle = async (item: any) => {
|
||||
const id = item.article_id || item.id
|
||||
if (!id) return
|
||||
try {
|
||||
await cancelCollect({ id: Number(id) })
|
||||
articles.value = articles.value.filter((article) => (article.article_id || article.id) !== id)
|
||||
uni.showToast({ title: '已取消收藏', icon: 'none' })
|
||||
} catch (error) {
|
||||
console.error('[Collection] remove article failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const openCoin = (coin: CryptoMarketCoin) => {
|
||||
uni.navigateTo({ url: `/pages/crypto/crypto_detail?symbol=${encodeURIComponent(coin.symbol)}` })
|
||||
}
|
||||
|
||||
const removeCoin = async (coin: CryptoMarketCoin) => {
|
||||
try {
|
||||
await cancelCryptoFavorite(coin.symbol)
|
||||
coins.value = coins.value.filter((item) => item.symbol !== coin.symbol)
|
||||
uni.showToast({ title: '已取消收藏', icon: 'none' })
|
||||
} catch (error) {
|
||||
console.error('[Collection] remove crypto failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatChange = (value: number) => {
|
||||
const number = Number(value || 0)
|
||||
return `${number >= 0 ? '+' : ''}${number.toFixed(2)}%`
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
if (activeType.value === 'article' || activeType.value === 'crypto') loadCollection()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.collection-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.collection-tabs {
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #edf0f5;
|
||||
|
||||
&__inner {
|
||||
display: inline-flex;
|
||||
min-width: 100%;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.collection-tab {
|
||||
min-width: 150rpx;
|
||||
height: 88rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
position: relative;
|
||||
color: #697386;
|
||||
font-size: 27rpx;
|
||||
|
||||
&--active {
|
||||
color: #1468f5;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 34rpx;
|
||||
right: 34rpx;
|
||||
bottom: 0;
|
||||
height: 5rpx;
|
||||
border-radius: 5rpx 5rpx 0 0;
|
||||
background: #1468f5;
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
min-width: 28rpx;
|
||||
padding: 1rpx 7rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #edf4ff;
|
||||
color: #1468f5;
|
||||
font-size: 18rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.collection-scroll {
|
||||
height: calc(100vh - 88rpx);
|
||||
}
|
||||
|
||||
.collection-list {
|
||||
padding: 16rpx 20rpx 36rpx;
|
||||
}
|
||||
|
||||
.article-item,
|
||||
.coin-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
margin-bottom: 14rpx;
|
||||
padding: 18rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #e8edf5;
|
||||
border-radius: 18rpx;
|
||||
box-shadow: 0 5rpx 18rpx rgba(30, 71, 120, 0.04);
|
||||
}
|
||||
|
||||
.article-item {
|
||||
align-items: stretch;
|
||||
|
||||
&__image {
|
||||
width: 190rpx;
|
||||
height: 138rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12rpx;
|
||||
background: #edf3fb;
|
||||
|
||||
&--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: #1f2937;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: -webkit-box;
|
||||
margin-top: 8rpx;
|
||||
overflow: hidden;
|
||||
color: #8a94a5;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.35;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
margin-top: auto;
|
||||
color: #a0a8b5;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
&__remove,
|
||||
.article-item__remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5rpx;
|
||||
flex-shrink: 0;
|
||||
color: #e3a33c;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.coin-item {
|
||||
min-height: 92rpx;
|
||||
|
||||
&__icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__icon--fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1468f5;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__name {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
|
||||
text:first-child {
|
||||
color: #1f2937;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
overflow: hidden;
|
||||
color: #8a94a5;
|
||||
font-size: 21rpx;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__quote {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 5rpx;
|
||||
|
||||
text:first-child {
|
||||
color: #1f2937;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
text:last-child {
|
||||
font-size: 21rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__remove {
|
||||
width: 44rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-up { color: #16a36a; }
|
||||
.is-down { color: #ef5350; }
|
||||
|
||||
.collection-state {
|
||||
min-height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
color: #8d98a8;
|
||||
font-size: 24rpx;
|
||||
|
||||
&__title {
|
||||
color: #596579;
|
||||
font-size: 29rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
color: #a1aab8;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user