2025-08-12 16:49:19 +08:00

405 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<!-- 内容区域 -->
<view class="content">
<view class="content-wrapper">
<!-- 公告列表 -->
<view class="notice-list">
<!-- 骨架屏 -->
<view class="skeleton-wrapper" v-if="isLoading && noticesList.length === 0">
<view class="skeleton-item" v-for="i in 5" :key="'skeleton-'+i">
<view class="skeleton-image"></view>
<view class="skeleton-content">
<view class="skeleton-line"></view>
<view class="skeleton-line short"></view>
<view class="skeleton-line shorter"></view>
</view>
</view>
</view>
<!-- 滚动区域 -->
<scroll-view scroll-y :style="'height:' + scrollViewHeight + 'px'" @scrolltolower="loadMore"
lower-threshold="100" :scroll-with-animation="true">
<view class="notice-item" v-for="(item,index) in noticesList" :key="item.id"
@click="goNoticeDetail(item.id)">
<image :src="item.cover" class="notice-image" mode="aspectFill" lazy-load
:show-menu-by-longpress="true" @error="handleImageError(index)"></image>
<view class="notice-content">
<view class="notice-title line-clamp-1">{{item.title}}</view>
<view class="notice-description line-clamp-2" v-html="item.desc"></view>
<view class="notice-footer">
<text class="notice-date">{{formatTime(item.createdAt,'YYYY-MM-DD')}}</text>
<text class="notice-more">查看详情 </text>
</view>
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more" v-if="noticesList.length > 0 && currentPage < totalPages">
<text>{{isLoading ? '加载中...' : '上拉加载更多'}}</text>
</view>
</scroll-view>
<!-- 空状态 -->
<view class="empty-state" v-if="noticesList.length === 0 && !isLoading">
<image src="/static/imgs/noData.png" class="empty-image"></image>
<text class="empty-text">暂无公告</text>
</view>
</view>
</view>
<Copyright />
</view>
</view>
</template>
<script>
import {
get
} from '@/utils/request';
import {
IMAGE_BASE_URL
} from '@/utils/config';
import {
formatTime
} from '@/utils/timeFormat';
import Copyright from '@/components/gx-copyright.vue';
export default {
components: {
Copyright
},
data() {
return {
formatTime,
IMAGE_BASE_URL,
noticesList: [],
cachedNotices: uni.getStorageSync('cachedNotices') || [],
isLoading: false,
currentPage: 1,
totalPages: 1,
lastRequestTime: 0,
scrollViewHeight: 500 // 初始高度mounted中会重新计算
};
},
mounted() {
this.calculateScrollHeight();
this.getNotices();
},
methods: {
// 计算滚动区域高度
calculateScrollHeight() {
const systemInfo = uni.getSystemInfoSync();
const query = uni.createSelectorQuery().in(this);
query.select('.container').boundingClientRect(data => {
if (data) {
// 减去顶部导航和其他固定元素的高度
this.scrollViewHeight = systemInfo.windowHeight - 50;
}
}).exec();
},
async getNotices() {
// 防止重复请求
const now = Date.now();
if (this.isLoading || (now - this.lastRequestTime < 1000)) return;
this.lastRequestTime = now;
// 第一页尝试使用缓存
if (this.currentPage === 1 && this.cachedNotices.length > 0) {
this.noticesList = [...this.cachedNotices];
// 仍然在后台更新数据
setTimeout(() => this.fetchNotices(), 300);
return;
}
await this.fetchNotices();
},
loadMore() {
if (!this.isLoading && this.currentPage < this.totalPages) {
console.log('触发加载更多,当前页:', this.currentPage);
this.currentPage += 1;
this.fetchNotices();
}
},
async fetchNotices() {
this.isLoading = true;
uni.showLoading({
title: '加载中...',
mask: true
});
try {
const res = await get('/api/v1/apps/home/notices', {
current: this.currentPage,
pageSize: 10, // 每页10条
}, {
timeout: 5000
});
if (res?.success) {
const processedData = res.data.map(item => ({
...item,
cover: this.processImageUrl(item.cover),
desc: this.filterHtmlTags(item.desc)
}));
if (this.currentPage === 1) {
this.noticesList = processedData;
// 缓存第一页数据
uni.setStorage({
key: 'cachedNotices',
data: processedData,
expire: 3600 // 1小时缓存
});
} else {
this.noticesList = this.noticesList.concat(processedData);
}
this.totalPages = Math.ceil(res.total / 10);
}
} catch (err) {
console.error('获取公告失败:', err);
if (this.currentPage === 1) {
uni.showToast({
title: '加载公告失败',
icon: 'none'
});
}
} finally {
uni.hideLoading();
this.isLoading = false;
}
},
processImageUrl(url) {
if (!url) return '/static/images/default-cover.jpg';
return url.startsWith('http') ? url : IMAGE_BASE_URL + url;
},
filterHtmlTags(str) {
if (!str) return '';
return str.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
},
handleImageError(index) {
this.$set(this.noticesList, index, {
...this.noticesList[index],
cover: '/static/images/default-cover.jpg'
});
},
goNoticeDetail(id) {
uni.navigateTo({
url: `/pages/serviceNoticeDetail/index?Id=${id}`,
animationType: 'slide-in-right'
});
}
}
};
</script>
<style lang="scss" scoped>
/* 基础变量 */
:root {
--primary-color: #3b8cff;
--secondary-color: #4a90e2;
--text-color: #333;
--text-light: #7d7d7d;
--bg-color: #f8f9fa;
--card-bg: #fff;
--border-color: rgba(59, 140, 255, 0.2);
--shadow: 0 4rpx 12rpx rgba(59, 140, 255, 0.1);
--radius: 16rpx;
}
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
background: linear-gradient(180deg, rgba(255, 241, 235, 0.8) 0%, rgba(192, 219, 250, 0.8) 100%);
position: relative;
}
.content {
flex: 1;
// border-radius: 30rpx 30rpx 0 0;
background-color: rgba(255, 255, 255, 0.85);
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
.content-wrapper {
padding: 30rpx 0;
.notice-list {
display: flex;
flex-direction: column;
gap: 24rpx;
padding: 0 30rpx;
.notice-item {
display: flex;
background: var(--card-bg);
border-radius: var(--radius);
overflow: hidden;
transition: all 0.3s ease;
box-shadow: var(--shadow);
border: 1rpx solid var(--border-color);
margin-bottom: 20rpx;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
.notice-image {
width: 220rpx;
height: 220rpx;
flex-shrink: 0;
background-color: #f5f5f5;
will-change: transform;
image-rendering: -webkit-optimize-contrast;
}
.notice-content {
flex: 1;
padding: 24rpx;
display: flex;
flex-direction: column;
.notice-title {
font-size: 32rpx;
font-weight: 600;
color: var(--secondary-color);
margin-bottom: 12rpx;
}
.notice-description {
font-size: 26rpx;
color: var(--text-light);
line-height: 1.5;
margin-bottom: 16rpx;
flex: 1;
}
.notice-footer {
display: flex;
justify-content: space-between;
align-items: center;
.notice-date {
font-size: 24rpx;
color: var(--text-light);
}
.notice-more {
font-size: 24rpx;
color: var(--primary-color);
}
}
}
}
.load-more {
text-align: center;
padding: 30rpx 0;
color: var(--text-light);
font-size: 26rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
color: var(--text-light);
}
}
/* 骨架屏样式 */
.skeleton-wrapper {
display: flex;
flex-direction: column;
gap: 24rpx;
.skeleton-item {
display: flex;
background: #fff;
border-radius: var(--radius);
overflow: hidden;
height: 220rpx;
.skeleton-image {
width: 220rpx;
height: 220rpx;
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 400% 100%;
animation: loading 1.5s ease infinite;
}
.skeleton-content {
flex: 1;
padding: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.skeleton-line {
height: 32rpx;
background: #f2f2f2;
border-radius: 8rpx;
margin-bottom: 16rpx;
animation: loading 1.5s ease infinite;
&.short {
width: 60%;
}
&.shorter {
width: 40%;
}
}
}
}
}
@keyframes loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
}
}
}
/* 通用工具类 */
.line-clamp-1 {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>