|
|
@@ -0,0 +1,337 @@
|
|
|
+<template>
|
|
|
+ <view class="academic-panel">
|
|
|
+ <InformationBannerSwiper />
|
|
|
+
|
|
|
+ <InformationSearchBar @search="handleSearch" @region-change="handleRegionChange" />
|
|
|
+
|
|
|
+ <view class="article-list">
|
|
|
+ <view
|
|
|
+ v-for="item in articleList"
|
|
|
+ :key="item.id"
|
|
|
+ class="article-item"
|
|
|
+ @click="handleArticleClick(item)"
|
|
|
+ >
|
|
|
+ <view class="article-title">
|
|
|
+ {{ item.title }}
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="getArticleSummary(item)" class="article-desc">
|
|
|
+ {{ getArticleSummary(item) }}
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="article-footer">
|
|
|
+ <view class="article-info">
|
|
|
+ <text v-if="item.area">{{ item.area }}</text>
|
|
|
+ <text v-if="item.area && formatDate(item.createTime)"> · </text>
|
|
|
+ <text v-if="formatDate(item.createTime)">
|
|
|
+ {{ formatDate(item.createTime) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <button
|
|
|
+ class="forward-button"
|
|
|
+ hover-class="forward-button--hover"
|
|
|
+ open-type="share"
|
|
|
+ :data-title="item.title"
|
|
|
+ :data-id="item.id"
|
|
|
+ data-active-tab="academic"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <image class="forward-icon" :src="shareIconUrl" mode="aspectFit" />
|
|
|
+ <text>转发</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="!loading && articleList.length === 0" class="empty-state">
|
|
|
+ <view class="empty-title">暂无内容</view>
|
|
|
+ <view class="empty-desc">换个区域或关键词试试</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="articleList.length > 0" class="load-more">
|
|
|
+ <text v-if="loading">加载中...</text>
|
|
|
+ <text v-else-if="finished">没有更多了</text>
|
|
|
+ <text v-else>上拉加载更多</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, reactive, ref } from 'vue'
|
|
|
+
|
|
|
+import { getArticlePageApi } from '@/services/modules/information'
|
|
|
+import type {
|
|
|
+ ArticleItem,
|
|
|
+ ArticlePageRequest,
|
|
|
+ ArticlePageResponse,
|
|
|
+} from '@/services/modules/information/type'
|
|
|
+
|
|
|
+import InformationBannerSwiper from './InformationBannerSwiper.vue'
|
|
|
+import InformationSearchBar from './InformationSearchBar.vue'
|
|
|
+
|
|
|
+interface ArticleSearchParams {
|
|
|
+ region: string
|
|
|
+ keyword: string
|
|
|
+}
|
|
|
+
|
|
|
+interface RegionOption {
|
|
|
+ label: string
|
|
|
+ value: string
|
|
|
+}
|
|
|
+
|
|
|
+const shareIconUrl = 'https://yy-cloud-oss.oss-cn-beijing.aliyuncs.com/img/fenxiang.png'
|
|
|
+
|
|
|
+const pageParams = reactive<ArticlePageRequest>({
|
|
|
+ current: 1,
|
|
|
+ size: 10,
|
|
|
+ title: '',
|
|
|
+ area: '',
|
|
|
+})
|
|
|
+
|
|
|
+const articleList = ref<ArticleItem[]>([])
|
|
|
+const loading = ref(false)
|
|
|
+const finished = ref(false)
|
|
|
+
|
|
|
+let requestSeq = 0
|
|
|
+
|
|
|
+const buildRequestParams = (current: number): ArticlePageRequest => {
|
|
|
+ const requestParams: ArticlePageRequest = {
|
|
|
+ current,
|
|
|
+ size: pageParams.size,
|
|
|
+ }
|
|
|
+
|
|
|
+ const title = pageParams.title?.trim()
|
|
|
+ const area = pageParams.area?.trim()
|
|
|
+
|
|
|
+ if (title) {
|
|
|
+ requestParams.title = title
|
|
|
+ }
|
|
|
+
|
|
|
+ if (area) {
|
|
|
+ requestParams.area = area
|
|
|
+ }
|
|
|
+
|
|
|
+ return requestParams
|
|
|
+}
|
|
|
+
|
|
|
+const getArticleList = async (current = 1, append = false): Promise<void> => {
|
|
|
+ if (loading.value) return
|
|
|
+ if (append && finished.value) return
|
|
|
+
|
|
|
+ const seq = ++requestSeq
|
|
|
+
|
|
|
+ if (!append) {
|
|
|
+ finished.value = false
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await getArticlePageApi(buildRequestParams(current))
|
|
|
+ const pageResponse: ArticlePageResponse = res.data
|
|
|
+
|
|
|
+ if (seq !== requestSeq) return
|
|
|
+
|
|
|
+ const records = Array.isArray(pageResponse.records) ? pageResponse.records : []
|
|
|
+
|
|
|
+ articleList.value = append ? articleList.value.concat(records) : records
|
|
|
+
|
|
|
+ pageParams.current = pageResponse.current
|
|
|
+
|
|
|
+ finished.value =
|
|
|
+ records.length < pageParams.size ||
|
|
|
+ pageResponse.current >= pageResponse.pages ||
|
|
|
+ articleList.value.length >= pageResponse.total
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取文章分页失败:', error)
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '内容加载失败',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ if (seq === requestSeq) {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const refresh = async (): Promise<void> => {
|
|
|
+ pageParams.current = 1
|
|
|
+ await getArticleList(1, false)
|
|
|
+}
|
|
|
+
|
|
|
+const loadMore = async (): Promise<void> => {
|
|
|
+ await getArticleList(pageParams.current + 1, true)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = (params: ArticleSearchParams): void => {
|
|
|
+ pageParams.title = params.keyword
|
|
|
+ pageParams.area = params.region ?? ''
|
|
|
+ void refresh()
|
|
|
+}
|
|
|
+
|
|
|
+const handleRegionChange = (region: RegionOption): void => {
|
|
|
+ console.log('region', region)
|
|
|
+
|
|
|
+ pageParams.area = region.value ? region.label : ''
|
|
|
+
|
|
|
+ void refresh()
|
|
|
+}
|
|
|
+
|
|
|
+const handleArticleClick = (item: ArticleItem): void => {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages-sub-packages/information/detail/index?type=academic&id=${item.id}`,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (value: string): string => {
|
|
|
+ return value ? value.slice(0, 10) : ''
|
|
|
+}
|
|
|
+
|
|
|
+const stripHtml = (value: string): string => {
|
|
|
+ if (!value) return ''
|
|
|
+
|
|
|
+ return value
|
|
|
+ .replace(/<[^>]+>/g, '')
|
|
|
+ .replace(/ /g, ' ')
|
|
|
+ .replace(/&/g, '&')
|
|
|
+ .replace(/</g, '<')
|
|
|
+ .replace(/>/g, '>')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .trim()
|
|
|
+}
|
|
|
+
|
|
|
+const getArticleSummary = (item: ArticleItem): string => {
|
|
|
+ return stripHtml(item.description || item.content || '')
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ void refresh()
|
|
|
+})
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ refresh,
|
|
|
+ loadMore,
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.academic-panel {
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.article-list {
|
|
|
+ padding: 8rpx 32rpx 32rpx;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.article-item {
|
|
|
+ padding: 30rpx 0 28rpx;
|
|
|
+ border-bottom: 2rpx solid #f2f2f2;
|
|
|
+}
|
|
|
+
|
|
|
+.article-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 46rpx;
|
|
|
+ color: #333;
|
|
|
+ word-break: break-all;
|
|
|
+ overflow: hidden;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+}
|
|
|
+
|
|
|
+.article-desc {
|
|
|
+ margin-top: 16rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 38rpx;
|
|
|
+ color: #666;
|
|
|
+ word-break: break-all;
|
|
|
+ overflow: hidden;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+}
|
|
|
+
|
|
|
+.article-footer {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 24rpx;
|
|
|
+ margin-top: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.article-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 36rpx;
|
|
|
+ color: #999;
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.forward-button {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 164rpx;
|
|
|
+ height: 58rpx;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+ border: none;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ background: #ecf6fe;
|
|
|
+ line-height: 58rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.forward-button::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.forward-button--hover {
|
|
|
+ opacity: 0.82;
|
|
|
+}
|
|
|
+
|
|
|
+.forward-icon {
|
|
|
+ width: 30rpx;
|
|
|
+ height: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.forward-button text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #3fa3f1;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-state {
|
|
|
+ padding: 140rpx 0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-desc {
|
|
|
+ margin-top: 12rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.load-more {
|
|
|
+ padding: 28rpx 0 34rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+</style>
|