Files
airz-uniapp/packages/content/news/news.uvue
T
hujun 2a9ff403c4 feat: 接入内容服务端真实接口并优化页面交互
- 迁移内容数据到服务端接口,移除本地模拟数据
- 首页新增推荐课程加载与未读消息检查
- 资讯、课程、搜索页面支持分页加载与错误重试
- 登录页新增恢复课程、消息、帮助页的路由逻辑
- 评估分析页新增处理中状态动效与阶段提示
- API 环境切换到正式域名 airz.hqling.cloud
2026-10-01 20:26:41 +08:00

98 lines
6.7 KiB
Plaintext

<template>
<view class="page" :style="pageStyle()">
<AppHeader title="新闻" :back="false" />
<view class="news-tabs">
<view v-for="item in tabs" :key="item" :class="tab == item ? 'news-tab active-tab' : 'news-tab'" @click="changeTab(item)"><text :class="tab == item ? 'tab-text active-text' : 'tab-text'">{{ item }}</text></view>
</view>
<scroll-view scroll-y :style="contentStyle(314, false)">
<view class="news-content">
<view v-if="tab == '推荐' && articles.length > 0" class="headline" @click="openArticle(headline.id)">
<view class="headline-copy"><text class="headline-tag">{{ headline.category }}</text><text class="headline-title">{{ headline.title }}</text><text class="headline-date">{{ headline.date }}</text></view>
<image class="headline-art" :src="headline.image" mode="aspectFill"></image>
</view>
<view class="list-head"><text class="list-title">{{ tab == '推荐' ? '最新资讯' : tab }}</text><text class="list-count">{{ total }} 篇</text></view>
<view v-if="loading" class="content-state"><text>正在加载资讯…</text></view>
<view v-else-if="error.length > 0" class="content-state"><text>{{ error }}</text><text class="content-retry" @click="reload">重新加载</text></view>
<view v-else-if="articles.length == 0" class="content-state"><text>暂无资讯</text></view>
<view class="article-list">
<view class="article" v-for="(item, index) in filteredArticles" :key="item.id" :class="index == filteredArticles.length - 1 ? 'last-article' : ''" @click="openArticle(item.id)">
<view class="article-copy"><text class="article-title">{{ item.title }}</text><text class="article-summary">{{ item.summary }}</text><text class="article-meta">{{ item.category }} · {{ item.date }}</text></view>
<image class="article-art" :src="item.image" mode="aspectFill"></image>
</view>
</view>
<text v-if="moreError.length > 0" class="content-state">{{ moreError }}</text>
<text v-if="hasMore" class="content-retry" @click="more">{{ loadingMore ? '正在加载…' : '加载更多' }}</text>
</view>
</scroll-view>
<BottomNav active="news" />
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import { fetchArticles, emptyArticle, NewsArticle } from '@/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue'
import BottomNav from '@/components/BottomNav.uvue'
const { contentStyle, pageStyle } = usePageLayout()
const tabs: string[] = ['推荐', '企业融资', '个人融资', '政策解读']
const tab = ref('推荐'); const articles = ref<NewsArticle[]>([]); const total = ref(0); const hasMore = ref(false)
const loading = ref(false); const loadingMore = ref(false); const error = ref(''); const moreError = ref('')
let generation = 0; let visible = false; let page = 1
const headline = computed((): NewsArticle => articles.value.length > 0 ? articles.value[0] : emptyArticle())
const filteredArticles = computed((): NewsArticle[] => tab.value == '推荐' ? articles.value.slice(1) : articles.value)
async function load(append: boolean) {
if (!visible || loading.value || loadingMore.value || (append && !hasMore.value)) return
const request = append ? generation : ++generation
if (append) { loadingMore.value = true; moreError.value = '' }
else { loading.value = true; error.value = ''; articles.value = []; total.value = 0; hasMore.value = false; page = 1 }
try {
const result = await fetchArticles(tab.value == '推荐' ? '' : tab.value, append ? page + 1 : 1)
// 换分类或离页后,旧列表响应不能覆盖新选择。
if (!visible || request != generation) return
articles.value = append ? articles.value.concat(result.items) : result.items
total.value = result.total; hasMore.value = result.hasMore
if (append) page++
} catch (err) {
if (visible && request == generation) {
const message = err instanceof Error ? err.message : '资讯加载失败,请重试'
if (append) moreError.value = message; else error.value = message
}
} finally { if (request == generation) { loading.value = false; loadingMore.value = false } }
}
function reload() { load(false) }
const more = () => { load(true) }
const changeTab = (value: string) => { if (value == tab.value) return; generation++; loading.value = false; loadingMore.value = false; tab.value = value; reload() }
const openArticle = (id: string) => { uni.navigateTo({ url: '/packages/content/article/article?id=' + id }) }
onShow(() => { visible = true; loading.value = false; loadingMore.value = false; reload() })
onHide(() => { visible = false; generation++ })
onUnload(() => { visible = false; generation++ })
</script>
<style>
.content-state { padding: 30rpx 0; color: #66758a; font-size: 25rpx; line-height: 40rpx; }
.content-retry { color: #0868f7; font-size: 25rpx; padding: 22rpx 0; }
.news-tabs { height: 88rpx; flex-direction: row; align-items: center; background-color: #ffffff; border-bottom: 1rpx solid #e8eef6; padding: 0 22rpx; }
.news-tab { flex: 1; height: 88rpx; justify-content: center; align-items: center; border-bottom: 4rpx solid transparent; }
.active-tab { border-bottom-color: #0868f7; }
.tab-text { color: #66758a; font-size: 26rpx; }
.active-text { color: #0868f7; font-weight: 700; }
.news-content { padding: 24rpx 28rpx 90rpx; }
.headline { min-height: 256rpx; border-radius: 20rpx; background-color: #123c7b; flex-direction: row; overflow: hidden; }
.headline-copy { flex: 1; padding: 26rpx 20rpx 26rpx 26rpx; }
.headline-tag { color: #b8ddff; font-size: 22rpx; font-weight: 600; }
.headline-title { color: #ffffff; font-size: 31rpx; line-height: 45rpx; font-weight: 700; margin-top: 12rpx; }
.headline-date { color: #c4d7ed; font-size: 22rpx; margin-top: 16rpx; }
.headline-art { width: 212rpx; height: 282rpx; flex-shrink: 0; align-self: center; }
.list-head { flex-direction: row; justify-content: space-between; align-items: center; margin: 30rpx 0 16rpx; }
.list-title { color: #172640; font-size: 30rpx; font-weight: 700; }
.list-count { color: #66758a; font-size: 22rpx; }
.article-list { background-color: #ffffff; border-radius: 18rpx; padding: 0 22rpx; }
.article { padding: 26rpx 0; flex-direction: row; border-bottom: 1rpx solid #e8eef6; align-items: flex-start; }
.last-article { border-bottom-width: 0; }
.article-copy { flex: 1; padding-right: 18rpx; }
.article-title { color: #172640; font-size: 29rpx; line-height: 42rpx; font-weight: 600; }
.article-summary { color: #66758a; font-size: 24rpx; line-height: 36rpx; margin-top: 10rpx; }
.article-meta { color: #66758a; font-size: 22rpx; line-height: 32rpx; margin-top: 14rpx; }
.article-art { width: 148rpx; height: 132rpx; border-radius: 12rpx; flex-shrink: 0; margin-top: 4rpx; }
</style>