<template>
|
<view class="page">
|
<view class="nav-shell">
|
<view class="safe-space" :style="'height:' + navTop + 'px'"></view>
|
<view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
|
<view class="nav-back" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
|
<text class="nav-title">人才资讯</text>
|
<view class="capsule-reserve" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
<view class="nav-bottom-space"></view>
|
</view>
|
|
<scroll-view :show-scrollbar="false" class="news-scroll" scroll-y lower-threshold="80" @scrolltolower="loadMoreNews">
|
<view v-if="initialLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载人才资讯</text></view>
|
<view v-else-if="initialError" class="list-state error-state"><text>人才资讯加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view>
|
<view v-else-if="hasLoaded && visibleNews.length == 0" class="list-state"><text>暂无人才资讯</text></view>
|
<view class="news-item" v-for="item in visibleNews" :key="item.id" @tap="openArticle(item.id)">
|
<text class="news-title">{{item.title}}</text>
|
<view class="news-body">
|
<view class="news-copy">
|
<text class="news-summary">{{item.summary}}</text>
|
<view class="news-meta"><text v-if="item.tag != ''" class="news-tag">{{item.tag}}</text><text class="news-time">{{item.time}}</text></view>
|
</view>
|
<!-- 缩略图用后台配置的封面;没配就留空(灰底占位),不再统一显示那张默认会议厅图。 -->
|
<image class="news-thumb" :src="item.image != null ? item.image : ''" mode="aspectFill" @error="replaceImage(item)"></image>
|
</view>
|
</view>
|
<view v-if="loadingMore" class="loading-row"><view class="loading-spinner"></view><text>正在加载</text></view>
|
<view v-else-if="loadMoreError" class="loading-row load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view>
|
<view v-else-if="hasLoaded && !hasMore && visibleNews.length > 0" class="no-more"><view></view><text>已经到底了</text><view></view></view>
|
<view class="bottom-space"></view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { isApiConfigured } from '../../config/api.uts'
|
import { getNews } from '../../services/portal.uts'
|
import { resolveApiUrl } from '../../services/http.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
|
const navTop = ref(28)
|
const navHeight = ref(32)
|
const navWidth = ref(88)
|
const navRight = ref(10)
|
// A full first page avoids a non-scrollable four-item list on common phone viewports.
|
const pageSize = 10
|
const visibleNews = ref<any[]>([])
|
const loadedCount = ref(0)
|
const currentPage = ref(0)
|
const initialLoading = ref(true)
|
const loadingMore = ref(false)
|
const hasLoaded = ref(false)
|
const hasMore = ref(true)
|
const initialError = ref(false)
|
const loadMoreError = ref(false)
|
const failInitialOnce = ref(false)
|
const failLoadMoreOnce = ref(false)
|
const forceEmpty = ref(false)
|
// 封面图加载失败时置空,交给灰底占位,避免出现破图。
|
const replaceImage = (item: any) => { item.image = '' }
|
|
onLoad((options: any) => {
|
// #ifdef MP-WEIXIN
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
const systemInfo = uni.getSystemInfoSync()
|
navTop.value = capsule.top
|
navHeight.value = capsule.height
|
navWidth.value = capsule.width
|
navRight.value = systemInfo.windowWidth - capsule.right
|
// #endif
|
if (options != null && options['previewState'] != null) {
|
const previewState = options['previewState'] as string
|
failInitialOnce.value = previewState == 'initialError'
|
failLoadMoreOnce.value = previewState == 'loadMoreError'
|
forceEmpty.value = previewState == 'empty'
|
}
|
loadInitialNews()
|
})
|
|
const allNews = [
|
{ id: 'news-20260603-senior-talent', title: '攀枝花市东区事业单位2026年直接考核招聘副高职称及以上人才公告', summary: '根据《中共中央组织部 人力资源社会保障部关于进一步做好事业单位公开招聘工作的通知》《社会发展〔2024〕57号》...', time: '2026-06-03 09:19' },
|
{ id: 'news-20260601-score-review', title: '关于攀枝花市东区事业单位2026年上半年公开考试招聘工作人员东区岗位加分情况、笔试总成绩及岗位排名、面试资格复审有关事宜的公告', summary: '根据《关于攀枝花市事业单位2026年上半年公开考试招聘工作人员的公告》规定,现将本次东区岗位考生加分情况...', time: '2026-06-01 16:36' },
|
{ id: 'news-20260529-teacher-review', title: '攀枝花市2026年上半年公开考试招聘中小学教师东区岗位面试资格复审及领取面试通知书公告', summary: '根据《攀枝花市2026年上半年公开考试招聘中小学教师公告》规定,现就本次东区岗位面试资格复审、领取面试通知书...', time: '2026-05-29 10:42' },
|
{ id: 'news-20260522-medical-check', title: '关于攀枝花市东区2026年春季综合类、教育系统事业单位引才体检有关事宜的公告', summary: '根据《攀枝花市2026年春季综合类事业单位引才公告》《攀枝花市2026年春季教育系统事业单位引才公告》规定...', time: '2026-05-22 11:01' },
|
{ id: 'news-20260518-service', title: '东区人才综合服务窗口办事指南', summary: '为进一步提升人才服务质效,现将人才综合服务窗口地址、受理时间和办理事项公告如下...', time: '2026-05-18 09:30' },
|
{ id: 'news-20260512-policy', title: '2026年度东区人才政策申报工作提示', summary: '符合条件的人才可按照申报指南准备材料,并在规定时间内完成线上或线下提交...', time: '2026-05-12 14:20' },
|
{ id: 'news-20260508-training', title: '东区青年人才能力提升培训报名通知', summary: '本次培训围绕政策解读、创新创业和职业能力提升设置专题课程,欢迎符合条件的人才报名...', time: '2026-05-08 10:05' },
|
{ id: 'news-20260428-event', title: '东区人才交流活动安排公告', summary: '活动将组织人才项目交流、企业需求对接和人才服务咨询,具体安排如下...', time: '2026-04-28 16:10' }
|
]
|
const formatNewsTime = (value: any): string => {
|
if (value == null) return ''
|
const text = '' + value
|
return text.length >= 16 ? text.substring(0, 16).replace('T', ' ') : text.replace('T', ' ')
|
}
|
const contentPreview = (value: any): string => {
|
if (value == null) return ''
|
const plain = (value as string).replace(/\s+/g, ' ').trim()
|
// 摘要区固定 3 行:按 3 行能放下的字数截断并补省略号,
|
// 避免文字被容器高度从中间切断(只露出半行、又没有省略号)。
|
return plain.length > 54 ? plain.substring(0, 54) + '...' : plain
|
}
|
// 标签只占一行:压掉换行/连续空格(text 组件会把 \n 渲染成换行),
|
// 放不下时由样式的单行省略号收尾。
|
const newsTagText = (value: any): string => {
|
if (value == null) return ''
|
return ('' + value).replace(/\s+/g, ' ').trim()
|
}
|
const mapNewsRecords = (records: any[]): any[] => {
|
const mapped: any[] = []
|
for (let index = 0; index < records.length; index++) {
|
const item: any = records[index]
|
mapped.push({
|
id: '' + item.id,
|
title: item.title != null ? item.title as string : '人才资讯',
|
summary: contentPreview(item.contentPreview),
|
tag: newsTagText(item.summary),
|
time: formatNewsTime(item.publishTime),
|
// 后台配置的封面图;相对路径补成接口域名,没配就留空走灰底占位。
|
image: item.coverUrl != null && ('' + item.coverUrl).trim() != '' ? resolveApiUrl('' + item.coverUrl) : ''
|
})
|
}
|
return mapped
|
}
|
const loadInitialNews = () => {
|
initialLoading.value = true
|
initialError.value = false
|
loadMoreError.value = false
|
hasLoaded.value = false
|
visibleNews.value = []
|
loadedCount.value = 0
|
currentPage.value = 0
|
if (isApiConfigured()) {
|
getNews(1, pageSize, (data: any) => {
|
const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []
|
visibleNews.value = mapNewsRecords(records)
|
loadedCount.value = records.length
|
currentPage.value = 1
|
hasMore.value = data != null && data.total != null ? records.length < data.total : false
|
initialLoading.value = false
|
hasLoaded.value = true
|
}, () => {
|
initialLoading.value = false
|
initialError.value = true
|
})
|
return
|
}
|
setTimeout(() => {
|
if (failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return }
|
const source = forceEmpty.value ? [] : allNews
|
const end = Math.min(pageSize, source.length)
|
const firstPage = []
|
let index = 0
|
while (index < end) { firstPage.push(source[index]); index++ }
|
visibleNews.value = firstPage
|
loadedCount.value = end
|
hasMore.value = end < source.length
|
initialLoading.value = false
|
hasLoaded.value = true
|
}, 450)
|
}
|
const retryInitialLoad = () => loadInitialNews()
|
const retryLoadMore = () => loadMoreNews()
|
const loadMoreNews = () => {
|
if (initialLoading.value || loadingMore.value || !hasMore.value) return
|
loadMoreError.value = false
|
loadingMore.value = true
|
if (isApiConfigured()) {
|
getNews(currentPage.value + 1, pageSize, (data: any) => {
|
const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []
|
const next = visibleNews.value.slice()
|
const mapped = mapNewsRecords(records)
|
for (let index = 0; index < mapped.length; index++) next.push(mapped[index])
|
visibleNews.value = next
|
loadedCount.value = next.length
|
currentPage.value = currentPage.value + 1
|
hasMore.value = data != null && data.total != null ? next.length < data.total : false
|
loadingMore.value = false
|
}, () => {
|
loadingMore.value = false
|
loadMoreError.value = true
|
})
|
return
|
}
|
setTimeout(() => {
|
if (failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return }
|
const next = visibleNews.value.slice()
|
const end = Math.min(loadedCount.value + pageSize, allNews.length)
|
let index = loadedCount.value
|
while (index < end) { next.push(allNews[index]); index++ }
|
visibleNews.value = next
|
loadedCount.value = end
|
hasMore.value = end < allNews.length
|
loadingMore.value = false
|
}, 550)
|
}
|
|
const goBack = () => safeBack('/pages/index/index')
|
const openArticle = (newsId: string) => uni.navigateTo({ url: '/pages/news-detail/news-detail?newsId=' + encodeURIComponent(newsId) })
|
</script>
|
|
<style>
|
.page { display: flex; flex-direction: column; width: 100%; height: 100vh; background-color: #FFFFFF; }
|
.nav-shell { flex-shrink: 0; background-image: linear-gradient(180deg, #FFF0B8 0%, #FFF8E4 58%, #FFFFFF 100%); }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; position: relative; height: 32px; box-sizing: border-box; align-items: center; justify-content: flex-end; }
|
.nav-back { display: flex; position: absolute; z-index: 9; left: 16rpx; top: 0; bottom: 0; width: 88rpx; min-height: 64rpx; justify-content: center; align-items: center; }
|
.nav-back image { width: 40rpx; height: 40rpx; }
|
.nav-title { position: absolute; left: 190rpx; right: 190rpx; color: #171717; font-size: 35rpx; font-weight: 700; text-align: center; white-space: nowrap; }
|
.capsule-reserve { width: 88px; height: 100%; flex-shrink: 0; }
|
.nav-bottom-space { height: 34rpx; }
|
.news-scroll { flex: 1; height: 0; background-color: #FFFFFF; }
|
.list-state { display:flex; height:430rpx; flex-direction:row; justify-content:center; align-items:center; color:#A0A4AA; font-size:25rpx; }
|
.error-state { flex-direction:column; }
|
.retry-text { margin-top:22rpx; color:#F26A38; }
|
.loading-row,.no-more { display:flex; height:90rpx; flex-direction:row; justify-content:center; align-items:center; color:#989DA5; font-size:22rpx; }
|
.loading-spinner { width:28rpx; height:28rpx; margin-right:13rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:news-loading 720ms linear infinite; }
|
.load-error { color:#F26A38; }
|
.no-more view { width:70rpx; height:1rpx; background-color:#E7E7E7; }
|
.no-more text { margin:0 18rpx; }
|
@keyframes news-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
|
.news-item { padding: 0 28rpx 32rpx; margin-bottom: 8rpx; border-bottom-width: 1rpx; border-bottom-color: #F3F3F3; }
|
.news-title { display: block; color: #252525; font-size: 30rpx; font-weight: 700; line-height: 41rpx; }
|
.news-body { display: flex; height: 174rpx; margin-top: 13rpx; flex-direction: row; }
|
.news-copy { display: flex; flex: 1; min-width: 0; padding-right: 18rpx; flex-direction: column; justify-content: space-between; }
|
/* 摘要固定 3 行(3×34rpx=102rpx),超出用省略号,不再从文字中间截断。 */
|
.news-summary { display: -webkit-box; height: 102rpx; overflow: hidden; -webkit-line-clamp: 3; -webkit-box-orient: vertical; color: #8E9299; font-size: 23rpx; line-height: 34rpx; }
|
.news-meta { display: flex; height: 42rpx; min-width: 0; flex-direction: row; align-items: center; }
|
/* 来源/摘要这行可能很长:按内容宽度排,放不下时收缩并省略,不去挤右侧时间。
|
flex-shrink 必须显式写 1:uni-app x 的全局样式给 view/text 默认 flex-shrink: 0,
|
不覆盖的话标签不会收缩,只会顶出卡片、把时间挤没(省略号也不会出现)。 */
|
.news-tag { display: block; flex: 0 1 auto; flex-grow: 0; flex-shrink: 1; flex-basis: auto; min-width: 0; padding: 5rpx 9rpx; border-radius: 6rpx; background-color: #FFF8EA; color: #F1A32D; font-size: 19rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.news-time { margin-left: 13rpx; color: #A5A8AE; font-size: 20rpx; flex-shrink: 0; }
|
/* 灰底占位:后台没配封面时不会出现破图或那张默认会议厅图。 */
|
.news-thumb { width: 236rpx; height: 168rpx; border-radius: 7rpx; background-color: #EEEEEE; flex-shrink: 0; }
|
.bottom-space { height: calc(28rpx + env(safe-area-inset-bottom)); }
|
</style>
|