|
@@ -2,50 +2,21 @@
|
|
|
<view class="academic-panel">
|
|
<view class="academic-panel">
|
|
|
<InformationBannerSwiper />
|
|
<InformationBannerSwiper />
|
|
|
|
|
|
|
|
- <InformationSearchBar @search="handleSearch" @region-change="handleRegionChange" />
|
|
|
|
|
|
|
+ <view class="search-section">
|
|
|
|
|
+ <InformationSearchBar @search="handleSearch" />
|
|
|
|
|
+ </view>
|
|
|
|
|
|
|
|
<view class="article-list">
|
|
<view class="article-list">
|
|
|
- <view
|
|
|
|
|
|
|
+ <AcademicArticleItem
|
|
|
v-for="item in articleList"
|
|
v-for="item in articleList"
|
|
|
:key="item.id"
|
|
: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>
|
|
|
|
|
|
|
+ :item="item"
|
|
|
|
|
+ :share-icon-url="shareIconUrl"
|
|
|
|
|
+ @select="handleArticleClick"
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
<view v-if="!loading && articleList.length === 0" class="empty-state">
|
|
<view v-if="!loading && articleList.length === 0" class="empty-state">
|
|
|
- <view class="empty-title">暂无内容</view>
|
|
|
|
|
- <view class="empty-desc">换个区域或关键词试试</view>
|
|
|
|
|
|
|
+ <wd-empty icon="no-content" tip="暂无内容" />
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<view v-if="articleList.length > 0" class="load-more">
|
|
<view v-if="articleList.length > 0" class="load-more">
|
|
@@ -58,68 +29,49 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
|
+import { onMounted, ref } from 'vue'
|
|
|
|
|
|
|
|
import { getArticlePageApi } from '@/services/modules/information'
|
|
import { getArticlePageApi } from '@/services/modules/information'
|
|
|
-import type {
|
|
|
|
|
- ArticleItem,
|
|
|
|
|
- ArticlePageRequest,
|
|
|
|
|
- ArticlePageResponse,
|
|
|
|
|
-} from '@/services/modules/information/type'
|
|
|
|
|
|
|
+import type { ArticleItem, ArticlePageRequest } from '@/services/modules/information/type'
|
|
|
|
|
|
|
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
|
|
+
|
|
|
|
|
+import AcademicArticleItem from './AcademicArticleItem.vue'
|
|
|
import InformationBannerSwiper from './InformationBannerSwiper.vue'
|
|
import InformationBannerSwiper from './InformationBannerSwiper.vue'
|
|
|
import InformationSearchBar from './InformationSearchBar.vue'
|
|
import InformationSearchBar from './InformationSearchBar.vue'
|
|
|
|
|
|
|
|
-interface ArticleSearchParams {
|
|
|
|
|
- region: string
|
|
|
|
|
- keyword: string
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-interface RegionOption {
|
|
|
|
|
- label: string
|
|
|
|
|
- value: string
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
|
|
+const PAGE_SIZE = 10
|
|
|
const shareIconUrl = 'https://yy-cloud-oss.oss-cn-beijing.aliyuncs.com/img/fenxiang.png'
|
|
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 userStore = useUserStore()
|
|
|
|
|
|
|
|
const articleList = ref<ArticleItem[]>([])
|
|
const articleList = ref<ArticleItem[]>([])
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
const finished = ref(false)
|
|
const finished = ref(false)
|
|
|
|
|
|
|
|
-let requestSeq = 0
|
|
|
|
|
|
|
+const currentPage = ref(1)
|
|
|
|
|
+const searchTitle = ref('')
|
|
|
|
|
+const searchArea = ref('')
|
|
|
|
|
|
|
|
-const buildRequestParams = (current: number): ArticlePageRequest => {
|
|
|
|
|
- const requestParams: ArticlePageRequest = {
|
|
|
|
|
- current,
|
|
|
|
|
- size: pageParams.size,
|
|
|
|
|
- }
|
|
|
|
|
|
|
+let latestRequestId = 0
|
|
|
|
|
|
|
|
- const title = pageParams.title?.trim()
|
|
|
|
|
- const area = pageParams.area?.trim()
|
|
|
|
|
-
|
|
|
|
|
- if (title) {
|
|
|
|
|
- requestParams.title = title
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const buildRequestParams = (current: number): ArticlePageRequest => {
|
|
|
|
|
+ const title = searchTitle.value.trim()
|
|
|
|
|
+ const area = searchArea.value.trim()
|
|
|
|
|
|
|
|
- if (area) {
|
|
|
|
|
- requestParams.area = area
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ current,
|
|
|
|
|
+ size: PAGE_SIZE,
|
|
|
|
|
+ shareUserId: userStore.currentUserInfo?.userId || '',
|
|
|
|
|
+ ...(title ? { title } : {}),
|
|
|
|
|
+ ...(area ? { area } : {}),
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return requestParams
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const getArticleList = async (current = 1, append = false): Promise<void> => {
|
|
const getArticleList = async (current = 1, append = false): Promise<void> => {
|
|
|
- if (loading.value) return
|
|
|
|
|
- if (append && finished.value) return
|
|
|
|
|
|
|
+ if (append && (loading.value || finished.value)) return
|
|
|
|
|
|
|
|
- const seq = ++requestSeq
|
|
|
|
|
|
|
+ const requestId = ++latestRequestId
|
|
|
|
|
|
|
|
if (!append) {
|
|
if (!append) {
|
|
|
finished.value = false
|
|
finished.value = false
|
|
@@ -128,21 +80,22 @@ const getArticleList = async (current = 1, append = false): Promise<void> => {
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const res = await getArticlePageApi(buildRequestParams(current))
|
|
|
|
|
- const pageResponse: ArticlePageResponse = res.data
|
|
|
|
|
|
|
+ const { data } = await getArticlePageApi(buildRequestParams(current))
|
|
|
|
|
|
|
|
- if (seq !== requestSeq) return
|
|
|
|
|
|
|
+ if (requestId !== latestRequestId) return
|
|
|
|
|
|
|
|
- const records = Array.isArray(pageResponse.records) ? pageResponse.records : []
|
|
|
|
|
|
|
+ const records = Array.isArray(data?.records) ? data.records : []
|
|
|
|
|
+ const page = Number(data?.current) || current
|
|
|
|
|
+ const pages = Number(data?.pages) || 0
|
|
|
|
|
+ const total = Number(data?.total) || 0
|
|
|
|
|
|
|
|
- articleList.value = append ? articleList.value.concat(records) : records
|
|
|
|
|
|
|
+ articleList.value = append ? [...articleList.value, ...records] : records
|
|
|
|
|
+ currentPage.value = page
|
|
|
|
|
|
|
|
- pageParams.current = pageResponse.current
|
|
|
|
|
|
|
+ const isLastPage = pages > 0 ? page >= pages : records.length < PAGE_SIZE
|
|
|
|
|
+ const isTotalLoaded = total > 0 ? articleList.value.length >= total : false
|
|
|
|
|
|
|
|
- finished.value =
|
|
|
|
|
- records.length < pageParams.size ||
|
|
|
|
|
- pageResponse.current >= pageResponse.pages ||
|
|
|
|
|
- articleList.value.length >= pageResponse.total
|
|
|
|
|
|
|
+ finished.value = records.length < PAGE_SIZE || isLastPage || isTotalLoaded
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('获取文章分页失败:', error)
|
|
console.error('获取文章分页失败:', error)
|
|
|
|
|
|
|
@@ -151,33 +104,25 @@ const getArticleList = async (current = 1, append = false): Promise<void> => {
|
|
|
icon: 'none',
|
|
icon: 'none',
|
|
|
})
|
|
})
|
|
|
} finally {
|
|
} finally {
|
|
|
- if (seq === requestSeq) {
|
|
|
|
|
|
|
+ if (requestId === latestRequestId) {
|
|
|
loading.value = false
|
|
loading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const refresh = async (): Promise<void> => {
|
|
const refresh = async (): Promise<void> => {
|
|
|
- pageParams.current = 1
|
|
|
|
|
- await getArticleList(1, false)
|
|
|
|
|
|
|
+ currentPage.value = 1
|
|
|
|
|
+ await getArticleList(1)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const loadMore = async (): Promise<void> => {
|
|
const loadMore = async (): Promise<void> => {
|
|
|
- await getArticleList(pageParams.current + 1, true)
|
|
|
|
|
|
|
+ await getArticleList(currentPage.value + 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 handleSearch = (params: { region: string; keyword: string }): void => {
|
|
|
|
|
+ searchTitle.value = params.keyword
|
|
|
|
|
+ searchArea.value = params.region
|
|
|
|
|
+ refresh()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleArticleClick = (item: ArticleItem): void => {
|
|
const handleArticleClick = (item: ArticleItem): void => {
|
|
@@ -186,30 +131,7 @@ const handleArticleClick = (item: ArticleItem): void => {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-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()
|
|
|
|
|
-})
|
|
|
|
|
|
|
+onMounted(refresh)
|
|
|
|
|
|
|
|
defineExpose({
|
|
defineExpose({
|
|
|
refresh,
|
|
refresh,
|
|
@@ -219,119 +141,29 @@ defineExpose({
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
.academic-panel {
|
|
.academic-panel {
|
|
|
- background: #fff;
|
|
|
|
|
|
|
+ min-height: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.article-list {
|
|
|
|
|
- padding: 8rpx 32rpx 32rpx;
|
|
|
|
|
|
|
+.search-section {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
background: #fff;
|
|
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;
|
|
|
|
|
|
|
+.article-list {
|
|
|
|
|
+ padding: 20rpx 28rpx 36rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.empty-state {
|
|
.empty-state {
|
|
|
- padding: 140rpx 0;
|
|
|
|
|
|
|
+ padding: 160rpx 0;
|
|
|
text-align: center;
|
|
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 {
|
|
.load-more {
|
|
|
- padding: 28rpx 0 34rpx;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+ padding: 30rpx 0 36rpx;
|
|
|
font-size: 24rpx;
|
|
font-size: 24rpx;
|
|
|
- color: #999;
|
|
|
|
|
|
|
+ line-height: 34rpx;
|
|
|
|
|
+ color: #a3acba;
|
|
|
|
|
+ text-align: center;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|