feat(uniapp): restore home page design
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<view class="ai-entry" :class="[`ai-entry--${size}`, `ai-entry--align-${align}`]" @tap="goAssistant">
|
||||
<text class="ai-entry__line ai-entry__line--top">AI</text>
|
||||
<text class="ai-entry__line ai-entry__line--bottom">助手</text>
|
||||
<view class="ai-entry" :class="[`ai-entry--${size}`, `ai-entry--align-${align}`, `ai-entry--${variant}`]"
|
||||
@tap="goAssistant">
|
||||
<template v-if="variant === 'orb'">
|
||||
<view class="ai-entry__orb">
|
||||
<view class="ai-entry__face">
|
||||
<view class="ai-entry__eye" />
|
||||
<view class="ai-entry__eye" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="ai-entry__line ai-entry__line--top">AI</text>
|
||||
<text class="ai-entry__line ai-entry__line--bottom">助手</text>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -9,9 +20,11 @@
|
||||
withDefaults(defineProps<{
|
||||
align?: 'left' | 'right'
|
||||
size?: 'compact' | 'regular' | 'large'
|
||||
variant?: 'badge' | 'orb'
|
||||
}>(), {
|
||||
align: 'left',
|
||||
size: 'regular'
|
||||
size: 'regular',
|
||||
variant: 'badge'
|
||||
})
|
||||
|
||||
const goAssistant = () => {
|
||||
@@ -60,6 +73,45 @@ const goAssistant = () => {
|
||||
--entry-bottom-font: 14rpx;
|
||||
}
|
||||
|
||||
&--orb {
|
||||
--entry-width: 72rpx;
|
||||
--entry-height: 72rpx;
|
||||
--entry-radius: 50%;
|
||||
|
||||
background: #f7f9ff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&__orb {
|
||||
width: 54rpx;
|
||||
height: 54rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 34% 28%, #8ec4ff 0%, #387df6 48%, #1557dc 100%);
|
||||
box-shadow: 0 5rpx 10rpx rgba(20, 104, 245, 0.2);
|
||||
}
|
||||
|
||||
&__face {
|
||||
width: 37rpx;
|
||||
height: 25rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 0 6rpx;
|
||||
border-radius: 14rpx;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__eye {
|
||||
width: 6rpx;
|
||||
height: 11rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #1f61e8;
|
||||
}
|
||||
|
||||
&__line {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
<navigator :url="`/pages/news_detail/news_detail?id=${item.id}`" class="feed-card">
|
||||
<view class="feed-card__body">
|
||||
<view class="feed-card__info">
|
||||
<view class="feed-card__title">{{ item.title }}</view>
|
||||
<view v-if="descText" class="feed-card__desc">{{ descText }}</view>
|
||||
<text class="feed-card__title">{{ item.title }}</text>
|
||||
<text v-if="descText" class="feed-card__desc">{{ descText }}</text>
|
||||
<view class="feed-card__footer">
|
||||
<text class="feed-card__author">{{ authorText }}</text>
|
||||
<view class="feed-card__footer-right">
|
||||
<text class="feed-card__meta">{{ item.comment_count || item.click || 0 }}阅读</text>
|
||||
<text class="feed-card__meta">{{ formatTime(item.create_time) }}</text>
|
||||
<text class="feed-card__time">{{ formatRelativeTime(item.create_time) }}</text>
|
||||
<view class="feed-card__comments">
|
||||
<view class="feed-card__comment-icon" />
|
||||
<text>{{ item.comment_count || item.click || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<UImage v-if="item.image" :src="item.image" width="220" height="140" border-radius="8" mode="aspectFill"
|
||||
class="feed-card__img" />
|
||||
<image v-if="item.image && !imageFailed" :src="item.image" mode="aspectFill" class="feed-card__img"
|
||||
referrerpolicy="no-referrer" @error="imageFailed = true" />
|
||||
</view>
|
||||
</navigator>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import UImage from '@/uni_modules/vk-uview-ui/components/u-image/u-image.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -31,119 +31,145 @@ const props = withDefaults(
|
||||
}
|
||||
)
|
||||
|
||||
const imageFailed = ref(false)
|
||||
|
||||
const stripHtml = (html: string) => {
|
||||
if (!html) return ''
|
||||
return html.replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim()
|
||||
return html.replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
const descText = computed(() => {
|
||||
if (props.item.desc) return props.item.desc
|
||||
if (props.item.content) return stripHtml(props.item.content).substring(0, 100)
|
||||
return ''
|
||||
return stripHtml(props.item.desc || props.item.content || '').slice(0, 72)
|
||||
})
|
||||
|
||||
const authorText = computed(() => {
|
||||
const rawAuthor = String(props.item.author || '')
|
||||
const cleanedAuthor = rawAuthor.replace(/@懂球帝/g, '').trim()
|
||||
return cleanedAuthor || '体育小编'
|
||||
return cleanedAuthor || 'SB 体育头条'
|
||||
})
|
||||
|
||||
const formatTime = (t: string) => {
|
||||
if (!t) return ''
|
||||
const d = new Date(t.replace(/-/g, '/'))
|
||||
if (isNaN(d.getTime())) return t
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const h = String(d.getHours()).padStart(2, '0')
|
||||
const min = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${m}-${day} ${h}:${min}`
|
||||
const formatRelativeTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(String(time).replace(/-/g, '/'))
|
||||
const delta = Date.now() - date.getTime()
|
||||
if (Number.isNaN(date.getTime()) || delta < 0) return String(time)
|
||||
const minute = 60 * 1000
|
||||
if (delta < minute) return '刚刚'
|
||||
if (delta < 60 * minute) return `${Math.floor(delta / minute)}分钟前`
|
||||
if (delta < 24 * 60 * minute) return `${Math.floor(delta / (60 * minute))}小时前`
|
||||
if (delta < 7 * 24 * 60 * minute) return `${Math.floor(delta / (24 * 60 * minute))}天前`
|
||||
return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feed-card {
|
||||
display: block;
|
||||
padding: 20rpx 20rpx;
|
||||
padding: 22rpx 20rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
border-top: 1rpx solid #edf0f5;
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
min-height: 144rpx;
|
||||
}
|
||||
|
||||
&__info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
line-height: 1.45;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #151b26;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 8rpx;
|
||||
color: #7c8798;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.42;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
min-width: 0;
|
||||
margin-top: auto;
|
||||
padding-top: 12rpx;
|
||||
gap: 16rpx;
|
||||
white-space: nowrap;
|
||||
color: #909aaa;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__author,
|
||||
&__time {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__author {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
max-width: 156rpx;
|
||||
}
|
||||
|
||||
&__footer-right {
|
||||
&__comments {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12rpx;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
gap: 6rpx;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
&__comment-icon {
|
||||
position: relative;
|
||||
width: 24rpx;
|
||||
height: 19rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid #8e99a9;
|
||||
border-radius: 10rpx;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 1rpx;
|
||||
bottom: -5rpx;
|
||||
width: 7rpx;
|
||||
height: 7rpx;
|
||||
box-sizing: border-box;
|
||||
border-right: 2rpx solid #8e99a9;
|
||||
border-bottom: 2rpx solid #8e99a9;
|
||||
background: #fff;
|
||||
transform: rotate(28deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
width: 216rpx;
|
||||
height: 144rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12rpx;
|
||||
background: #eef1f5;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
+501
-190
@@ -1,60 +1,122 @@
|
||||
<template>
|
||||
<view class="index">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="top-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="top-bar__content">
|
||||
<view class="top-bar__logo">
|
||||
<image src="/static/images/logo.png" mode="aspectFit" class="top-bar__logo-img" />
|
||||
</view>
|
||||
<view class="top-bar__search" @tap="goSearch">
|
||||
<UIcon name="search" size="28" color="#999" />
|
||||
<text class="top-bar__search-text">{{ searchPlaceholder }}</text>
|
||||
</view>
|
||||
<view class="top-bar__actions">
|
||||
<AiAssistantEntry />
|
||||
<UIcon name="search" size="30" color="#8C95A5" />
|
||||
<text class="top-bar__search-text">搜索赛事、球队、球员</text>
|
||||
</view>
|
||||
<AiAssistantEntry size="compact" variant="orb" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Feed流内容 -->
|
||||
<scroll-view scroll-y class="feed-wrapper" :style="{ height: feedHeight + 'px' }" @scrolltolower="loadMore"
|
||||
:lower-threshold="100">
|
||||
<!-- 频道Tab栏 -->
|
||||
<view class="channel-bar">
|
||||
<scroll-view scroll-x class="channel-bar__scroll" :scroll-left="channelScrollLeft"
|
||||
scroll-with-animation>
|
||||
<view class="channel-bar__list">
|
||||
<view v-for="(item, index) in allChannels" :key="item.channelKey" class="channel-bar__item"
|
||||
:class="{ 'channel-bar__item--active': currentChannel === index }"
|
||||
@tap="switchChannel(index)">
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="channel-bar__more" @tap="goChannelManage">
|
||||
<UIcon name="list" size="38" color="#666" />
|
||||
<view class="channel-bar">
|
||||
<view class="channel-bar__list">
|
||||
<view v-for="item in primaryChannels" :key="item.channelKey" class="channel-bar__item"
|
||||
:class="{ 'channel-bar__item--active': currentChannel === item.index }" @tap="switchChannel(item.index)">
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="channel-bar__more" @tap="goChannelManage">
|
||||
<text>更多</text>
|
||||
<UIcon name="arrow-down" size="22" color="#667085" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-for="item in feedList" :key="item.id">
|
||||
<FeedTranslateCard v-if="hasTranslatedContent(item)" :item="item" />
|
||||
<FeedVideoCard v-else-if="item.is_video" :item="item" />
|
||||
<FeedCard v-else :item="item" />
|
||||
</template>
|
||||
<view class="feed-loading" v-if="loading">
|
||||
<ULoading mode="circle" />
|
||||
<text class="feed-loading__text">加载中...</text>
|
||||
</view>
|
||||
<view class="feed-bottom" v-if="noMore && feedList.length">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
<view class="feed-empty" v-if="!loading && !feedList.length">
|
||||
<text>暂无内容</text>
|
||||
<scroll-view scroll-y class="feed-wrapper" @scrolltolower="loadMore" :lower-threshold="100">
|
||||
<view class="home-content">
|
||||
<view v-if="focusMatches.length" class="focus-section">
|
||||
<view class="section-heading">
|
||||
<text class="section-heading__title">今日焦点</text>
|
||||
<view class="section-heading__action" @tap="goAllMatches">
|
||||
<text>全部赛程</text>
|
||||
<UIcon name="arrow-right" size="22" color="#1468F5" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="focus-section__list">
|
||||
<view v-for="(match, index) in focusMatches" :key="match.id" class="focus-match"
|
||||
@tap="goMatch(match.id)">
|
||||
<view class="focus-match__head">
|
||||
<text class="focus-match__league" :class="{ 'focus-match__league--warm': index % 2 }">
|
||||
{{ match.league_name || '赛事' }}
|
||||
</text>
|
||||
<text class="focus-match__time">{{ formatFocusMeta(match) }}</text>
|
||||
</view>
|
||||
<view class="focus-match__teams">
|
||||
<view class="focus-match__team">
|
||||
<image v-if="match.home_icon" :src="match.home_icon" mode="aspectFit"
|
||||
class="focus-match__badge" referrerpolicy="no-referrer" />
|
||||
<view v-else class="focus-match__badge focus-match__badge--fallback">
|
||||
<text>{{ getTeamInitial(match.home_team) }}</text>
|
||||
</view>
|
||||
<text class="focus-match__team-name">{{ match.home_team || '主队' }}</text>
|
||||
</view>
|
||||
<text class="focus-match__versus">VS</text>
|
||||
<view class="focus-match__team">
|
||||
<image v-if="match.away_icon" :src="match.away_icon" mode="aspectFit"
|
||||
class="focus-match__badge" referrerpolicy="no-referrer" />
|
||||
<view v-else class="focus-match__badge focus-match__badge--fallback">
|
||||
<text>{{ getTeamInitial(match.away_team) }}</text>
|
||||
</view>
|
||||
<text class="focus-match__team-name">{{ match.away_team || '客队' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<navigator v-if="featuredArticle" :url="`/pages/news_detail/news_detail?id=${featuredArticle.id}`"
|
||||
class="featured-card">
|
||||
<image v-if="featuredArticle.image && !featuredImageFailed" class="featured-card__image"
|
||||
:src="featuredArticle.image" mode="aspectFill" referrerpolicy="no-referrer"
|
||||
@error="featuredImageFailed = true" />
|
||||
<view class="featured-card__shade" />
|
||||
<view class="featured-card__content">
|
||||
<text class="featured-card__tag">{{ getArticleCategoryName(featuredArticle) }}</text>
|
||||
<view class="featured-card__copy">
|
||||
<text class="featured-card__title">{{ featuredArticle.title }}</text>
|
||||
<text v-if="getArticleSummary(featuredArticle)" class="featured-card__desc">
|
||||
{{ getArticleSummary(featuredArticle) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="featured-card__meta">
|
||||
<text>{{ getArticleAuthor(featuredArticle) }}</text>
|
||||
<text>{{ formatRelativeTime(featuredArticle.create_time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</navigator>
|
||||
|
||||
<view class="latest-section">
|
||||
<view class="section-heading latest-section__heading">
|
||||
<text class="section-heading__title">最新资讯</text>
|
||||
<view class="latest-section__filter">
|
||||
<UIcon name="filter" size="28" color="#7A8494" />
|
||||
<text>筛选</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="latest-section__list">
|
||||
<FeedCard v-for="item in regularFeedList" :key="item.id" :item="item" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="feed-loading">
|
||||
<ULoading mode="circle" />
|
||||
<text class="feed-loading__text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="noMore && feedList.length" class="feed-bottom">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
<view v-if="!loading && !feedList.length" class="feed-empty">
|
||||
<text>暂无内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- #ifdef MP -->
|
||||
<MpPrivacyPopup></MpPrivacyPopup>
|
||||
<MpPrivacyPopup />
|
||||
<!-- #endif -->
|
||||
|
||||
<GlobalPopup />
|
||||
@@ -63,14 +125,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { getArticleList, getArticleCate } from '@/api/news'
|
||||
import { getMatchList } from '@/api/match'
|
||||
import { resolveShortLink } from '@/api/share'
|
||||
import { getLocalMatchDate } from '@/utils/match-time'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import AiAssistantEntry from '@/components/ai-assistant-entry/ai-assistant-entry.vue'
|
||||
import FeedCard from '@/components/feed-card/feed-card.vue'
|
||||
import FeedTranslateCard from '@/components/feed-translate-card/feed-translate-card.vue'
|
||||
import FeedVideoCard from '@/components/feed-video-card/feed-video-card.vue'
|
||||
import GlobalPopup from '@/components/global-popup/global-popup.vue'
|
||||
import UIcon from '@/uni_modules/vk-uview-ui/components/u-icon/u-icon.vue'
|
||||
import ULoading from '@/uni_modules/vk-uview-ui/components/u-loading/u-loading.vue'
|
||||
@@ -81,33 +143,13 @@ import MpPrivacyPopup from './component/mp-privacy-popup.vue'
|
||||
// #endif
|
||||
|
||||
const statusBarHeight = ref(0)
|
||||
const feedHeight = ref(500)
|
||||
let windowHeight = 667
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
// #ifdef APP-PLUS || MP
|
||||
statusBarHeight.value = res.statusBarHeight || 44
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
statusBarHeight.value = 0
|
||||
// #endif
|
||||
windowHeight = res.windowHeight || 667
|
||||
const topBarH = statusBarHeight.value + 44
|
||||
const tabbarH = uni.upx2px(100)
|
||||
feedHeight.value = windowHeight - topBarH - tabbarH
|
||||
}
|
||||
})
|
||||
const calcFeedHeight = () => {
|
||||
const query = uni.createSelectorQuery()
|
||||
query.select('.top-bar').boundingClientRect()
|
||||
query.exec((rects: any[]) => {
|
||||
const topBarH = rects[0]?.height || (statusBarHeight.value + 44)
|
||||
const tabbarH = uni.upx2px(100)
|
||||
feedHeight.value = windowHeight - topBarH - tabbarH
|
||||
})
|
||||
}
|
||||
|
||||
const searchPlaceholder = ref('搜索世博头条')
|
||||
|
||||
const RECOMMEND_CHANNEL = {
|
||||
id: '__recommend__',
|
||||
@@ -126,13 +168,30 @@ const MY_CHANNEL = {
|
||||
const articleChannels = ref<any[]>([])
|
||||
const allChannels = ref<any[]>([])
|
||||
const currentChannel = ref(0)
|
||||
const channelScrollLeft = ref(0)
|
||||
const feedList = ref<any[]>([])
|
||||
const focusMatches = ref<any[]>([])
|
||||
const featuredImageFailed = ref(false)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
const pageNo = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const primaryChannels = computed(() => {
|
||||
return allChannels.value
|
||||
.map((item, index) => ({ ...item, index }))
|
||||
.filter((item) => item.specialType !== 'mychannel')
|
||||
.slice(0, 5)
|
||||
})
|
||||
|
||||
const featuredArticle = computed(() => {
|
||||
return feedList.value.find((item) => item.image) || feedList.value[0] || null
|
||||
})
|
||||
|
||||
const regularFeedList = computed(() => {
|
||||
const featuredId = featuredArticle.value?.id
|
||||
return feedList.value.filter((item) => item.id !== featuredId)
|
||||
})
|
||||
|
||||
const buildAllChannels = (channels: any[] = []) => {
|
||||
const merged = [{ ...RECOMMEND_CHANNEL }]
|
||||
if (userStore.isLogin) {
|
||||
@@ -161,16 +220,26 @@ const findChannelIndexById = (id: any) => {
|
||||
|
||||
const loadChannels = async () => {
|
||||
try {
|
||||
const allCate = await getArticleCate()
|
||||
articleChannels.value = allCate || []
|
||||
syncAllChannels()
|
||||
} catch (e) {
|
||||
console.log('获取分类失败=>', e)
|
||||
articleChannels.value = (await getArticleCate()) || []
|
||||
} catch (error) {
|
||||
console.log('获取分类失败=>', error)
|
||||
articleChannels.value = []
|
||||
} finally {
|
||||
syncAllChannels()
|
||||
}
|
||||
}
|
||||
|
||||
const loadFocusMatches = async () => {
|
||||
try {
|
||||
const data = await getMatchList()
|
||||
const priorityMatches = [...(data?.live || []), ...(data?.upcoming || [])]
|
||||
focusMatches.value = priorityMatches.slice(0, 2)
|
||||
} catch (error) {
|
||||
console.log('获取首页焦点赛程失败=>', error)
|
||||
focusMatches.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const switchChannel = (index: number) => {
|
||||
if (currentChannel.value === index) return
|
||||
currentChannel.value = index
|
||||
@@ -181,6 +250,7 @@ const reloadFeed = () => {
|
||||
pageNo.value = 1
|
||||
noMore.value = false
|
||||
feedList.value = []
|
||||
featuredImageFailed.value = false
|
||||
fetchFeedList()
|
||||
}
|
||||
|
||||
@@ -204,6 +274,7 @@ const fetchFeedList = async () => {
|
||||
const newList = lists || []
|
||||
if (pageNo.value === 1) {
|
||||
feedList.value = newList
|
||||
featuredImageFailed.value = false
|
||||
} else {
|
||||
feedList.value = [...feedList.value, ...newList]
|
||||
}
|
||||
@@ -211,43 +282,63 @@ const fetchFeedList = async () => {
|
||||
noMore.value = true
|
||||
}
|
||||
pageNo.value++
|
||||
} catch (e) {
|
||||
console.log('feed加载失败=>', e)
|
||||
} catch (error) {
|
||||
console.log('首页资讯加载失败=>', error)
|
||||
uni.showToast({ title: '加载失败,请稍后重试', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
fetchFeedList()
|
||||
const getArticleSummary = (item: any) => {
|
||||
return String(item?.desc || item?.content || '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
const hasTranslatedContent = (item: any) => {
|
||||
return !!String(item?.translated_content || item?.worldcup_translated_content || '').trim()
|
||||
const getArticleAuthor = (item: any) => {
|
||||
return String(item?.author || 'SB 体育头条').replace(/@懂球帝/g, '').trim() || 'SB 体育头条'
|
||||
}
|
||||
|
||||
const goSearch = () => {
|
||||
uni.navigateTo({ url: '/pages/search/search' })
|
||||
const getArticleCategoryName = (item: any) => {
|
||||
return articleChannels.value.find((channel) => String(channel.id) === String(item?.cid))?.name || '体育'
|
||||
}
|
||||
|
||||
const goChannelManage = () => {
|
||||
uni.navigateTo({ url: '/pages/channel/channel-manage' })
|
||||
const formatRelativeTime = (time: string) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(String(time).replace(/-/g, '/'))
|
||||
const delta = Date.now() - date.getTime()
|
||||
if (Number.isNaN(date.getTime()) || delta < 0) return String(time)
|
||||
const minute = 60 * 1000
|
||||
if (delta < minute) return '刚刚'
|
||||
if (delta < 60 * minute) return `${Math.floor(delta / minute)}分钟前`
|
||||
if (delta < 24 * 60 * minute) return `${Math.floor(delta / (60 * minute))}小时前`
|
||||
if (delta < 7 * 24 * 60 * minute) return `${Math.floor(delta / (24 * 60 * minute))}天前`
|
||||
return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
// 处理短链接跳转参数
|
||||
handleRedirect(options)
|
||||
const formatFocusMeta = (match: any) => {
|
||||
const date = getLocalMatchDate(match?.match_time, match)
|
||||
const time = date
|
||||
? `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
: '待定'
|
||||
return `${match?.round_name || '赛程'} ${time}`
|
||||
}
|
||||
|
||||
await loadChannels()
|
||||
fetchFeedList()
|
||||
nextTick(() => {
|
||||
setTimeout(calcFeedHeight, 100)
|
||||
})
|
||||
})
|
||||
const getTeamInitial = (name: string) => String(name || '?').trim().slice(0, 1)
|
||||
|
||||
const loadMore = () => fetchFeedList()
|
||||
|
||||
const goSearch = () => uni.navigateTo({ url: '/pages/search/search' })
|
||||
const goChannelManage = () => uni.navigateTo({ url: '/pages/channel/channel-manage' })
|
||||
const goAllMatches = () => uni.switchTab({ url: '/packages_match/pages/worldcup' })
|
||||
const goMatch = (id: number | string) => {
|
||||
if (id) uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${id}` })
|
||||
}
|
||||
|
||||
const handleRedirect = async (options: any) => {
|
||||
// onLoad options 或 URL query 中获取 slc
|
||||
let slc = options?.slc || ''
|
||||
// #ifdef H5
|
||||
if (!slc) {
|
||||
@@ -258,60 +349,55 @@ const handleRedirect = async (options: any) => {
|
||||
if (!slc) return
|
||||
|
||||
try {
|
||||
const fingerprint = generateFingerprint()
|
||||
const res = await resolveShortLink({
|
||||
code: slc,
|
||||
fingerprint,
|
||||
referer: document?.referrer || ''
|
||||
fingerprint: generateFingerprint(),
|
||||
referer: typeof document === 'undefined' ? '' : document.referrer || ''
|
||||
})
|
||||
|
||||
// 存储邀请码
|
||||
if (res.params?.invite_code) {
|
||||
uni.setStorageSync('invite_code', res.params.invite_code)
|
||||
}
|
||||
|
||||
// 跳转到目标页面
|
||||
if (res.params?.invite_code) uni.setStorageSync('invite_code', res.params.invite_code)
|
||||
if (res.path && res.page_type !== 'invite') {
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: res.path })
|
||||
}, 300)
|
||||
setTimeout(() => uni.navigateTo({ url: res.path }), 300)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('短链接解析失败', e)
|
||||
} catch (error) {
|
||||
console.error('短链接解析失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const generateFingerprint = (): string => {
|
||||
// #ifdef H5
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.font = '14px Arial'
|
||||
ctx.fillText('fp', 2, 2)
|
||||
const context = canvas.getContext('2d')
|
||||
if (context) {
|
||||
context.textBaseline = 'top'
|
||||
context.font = '14px Arial'
|
||||
context.fillText('fp', 2, 2)
|
||||
}
|
||||
const raw =
|
||||
navigator.userAgent +
|
||||
navigator.language +
|
||||
(screen.width + 'x' + screen.height) +
|
||||
screen.colorDepth +
|
||||
new Date().getTimezoneOffset() +
|
||||
canvas.toDataURL()
|
||||
const raw = `${navigator.userAgent}${navigator.language}${screen.width}x${screen.height}${screen.colorDepth}${new Date().getTimezoneOffset()}${canvas.toDataURL()}`
|
||||
let hash = 0
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
hash = (hash << 5) - hash + raw.charCodeAt(i)
|
||||
for (let index = 0; index < raw.length; index++) {
|
||||
hash = (hash << 5) - hash + raw.charCodeAt(index)
|
||||
hash |= 0
|
||||
}
|
||||
return Math.abs(hash).toString(16)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return ''
|
||||
// #endif
|
||||
}
|
||||
|
||||
let channelsLoaded = false
|
||||
onLoad(async (options: any) => {
|
||||
handleRedirect(options)
|
||||
await Promise.all([loadChannels(), loadFocusMatches()])
|
||||
fetchFeedList()
|
||||
})
|
||||
|
||||
onShow(async () => {
|
||||
syncAllChannels()
|
||||
// iOS授权弹窗关闭后数据可能未加载,检测并重试
|
||||
if (!channelsLoaded && articleChannels.value.length === 0) {
|
||||
await loadChannels()
|
||||
fetchFeedList()
|
||||
@@ -335,167 +421,392 @@ const onChannelChanged = (data: any) => {
|
||||
reloadFeed()
|
||||
}
|
||||
}
|
||||
|
||||
uni.$on('channelChanged', onChannelChanged)
|
||||
onUnload(() => {
|
||||
uni.$off('channelChanged', onChannelChanged)
|
||||
})
|
||||
onUnload(() => uni.$off('channelChanged', onChannelChanged))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.index {
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background: #f6f7f9;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-bar {
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
z-index: 10;
|
||||
background: #fff;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 24rpx;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 24rpx 14rpx;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-img {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 12rpx;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 18rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
min-width: 0;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
gap: 14rpx;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
gap: 12rpx;
|
||||
background: #f3f4f6;
|
||||
|
||||
&-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
color: #8c95a5;
|
||||
font-size: 27rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 频道Tab栏 */
|
||||
.channel-bar {
|
||||
height: 86rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
height: 80rpx;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
height: 80rpx;
|
||||
}
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #edf0f5;
|
||||
|
||||
&__list {
|
||||
white-space: nowrap;
|
||||
height: 80rpx;
|
||||
padding: 0 8rpx;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: inline-block;
|
||||
padding: 0 16rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #252b36;
|
||||
font-size: 29rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: #0053d8;
|
||||
font-weight: bold;
|
||||
color: #1468f5;
|
||||
font-weight: 700;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 0;
|
||||
width: 48rpx;
|
||||
height: 6rpx;
|
||||
background: #0053d8;
|
||||
border-radius: 3rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #1468f5;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__more {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
width: 104rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1rpx solid #f0f0f0;
|
||||
align-items: center;
|
||||
gap: 2rpx;
|
||||
flex-shrink: 0;
|
||||
color: #252b36;
|
||||
font-size: 28rpx;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed流 */
|
||||
.feed-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #f5f5f5;
|
||||
background: #f6f7f9;
|
||||
}
|
||||
|
||||
.home-content {
|
||||
padding: 16rpx 24rpx 42rpx;
|
||||
}
|
||||
|
||||
.focus-section,
|
||||
.latest-section {
|
||||
background: #fff;
|
||||
border-radius: 22rpx;
|
||||
}
|
||||
|
||||
.focus-section {
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(29, 45, 78, 0.04);
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
height: 54rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14rpx;
|
||||
|
||||
&__title {
|
||||
color: #151b26;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rpx;
|
||||
color: #1468f5;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.focus-match {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16rpx 14rpx 18rpx;
|
||||
border: 1rpx solid #e7eaf0;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__league {
|
||||
max-width: 92rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
color: #235bde;
|
||||
background: #eaf0ff;
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&--warm {
|
||||
color: #ef6321;
|
||||
background: #fff0e8;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #7d8797;
|
||||
font-size: 21rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__teams {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
&__team {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
|
||||
&--fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #edf2ff;
|
||||
color: #1468f5;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__team-name {
|
||||
width: 100%;
|
||||
color: #202633;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__versus {
|
||||
color: #161d29;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.featured-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 400rpx;
|
||||
margin-top: 18rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(135deg, #164b91 0%, #142839 100%);
|
||||
box-shadow: 0 10rpx 30rpx rgba(20, 37, 65, 0.1);
|
||||
|
||||
&__image,
|
||||
&__shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__shade {
|
||||
background: linear-gradient(180deg, rgba(8, 18, 29, 0.08) 20%, rgba(7, 15, 24, 0.82) 100%);
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
align-self: flex-start;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 10rpx;
|
||||
color: #fff;
|
||||
background: #1468f5;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
&__copy {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.32;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 25rpx;
|
||||
line-height: 1.5;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
margin-top: 16rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-size: 23rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-section {
|
||||
margin-top: 18rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&__heading {
|
||||
padding: 20rpx 20rpx 8rpx;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
color: #7a8494;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx 0;
|
||||
gap: 12rpx;
|
||||
padding: 30rpx 0;
|
||||
|
||||
&__text {
|
||||
color: #98a2b3;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-bottom {
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
.feed-bottom,
|
||||
.feed-empty {
|
||||
color: #98a2b3;
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feed-bottom {
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.feed-empty {
|
||||
text-align: center;
|
||||
padding: 100rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user