405 lines
9.4 KiB
Vue
405 lines
9.4 KiB
Vue
<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(/ /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> |