You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

575 lines
11 KiB

3 months ago
<template>
<view>
<view class="message-board">
<view class="header">
<view class="title-container">
<text class="title">留言板</text>
<text class="subtitle">查看所有留言</text>
</view>
<view class="decor decor-1"></view>
<view class="decor decor-2"></view>
</view>
<view class="content">
<view class="stats-card">
<view class="stats-header">
<view class="stats-icon-container">
<view class="stats-icon">
<text class="icon-text">💬</text>
</view>
</view>
<text class="stats-title">全部留言</text>
<view class="stats-count">
<text class="count-text"> {{ total }} </text>
</view>
</view>
</view>
<view class="message-list">
<view v-if="loading && messageList.length === 0" class="empty-state">
<view class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
<view v-else-if="messageList.length === 0" class="empty-state">
<view class="empty-container">
<view class="empty-icon">📭</view>
<text class="empty-title">暂无留言</text>
<text class="empty-desc">还没有人留言呢</text>
</view>
</view>
<view v-else class="message-items">
<view v-for="(item, index) in messageList" :key="item.id" class="message-item"
:style="{ animationDelay: `${index * 100}ms` }">
<view class="user-info">
<view class="avatar">
<text class="avatar-text">{{ getAvatarText(item.nickname) }}</text>
</view>
<view class="user-details">
<view class="user-header">
<text class="username">{{ item.nickname }}</text>
<text class="message-id">#{{ item.id }}</text>
</view>
<text class="message-time">{{ formatTime(item.createtime) }}</text>
</view>
</view>
<view class="message-content">
<text class="message-text">{{ item.content }}</text>
</view>
</view>
</view>
</view>
<view v-if="pages > 1" class="pagination">
<view class="pagination-container">
<button @click="changePage(page - 1)" :disabled="page <= 1" class="page-button prev-button">
<text class="button-icon"></text>
<text>上一页</text>
</button>
<view class="page-info">
<text class="current-page">{{ page }}</text>
<text class="page-divider">/</text>
<text class="total-pages">{{ pages }}</text>
</view>
<button @click="changePage(page + 1)" :disabled="page >= pages" class="page-button next-button">
<text>下一页</text>
<text class="button-icon"></text>
</button>
</view>
</view>
<view class="refresh-container">
<button @click="refresh" :disabled="loading" class="refresh-button">
<text class="refresh-icon" :class="{ 'rotating': loading }">🔄</text>
<text>{{ loading ? '刷新中...' : '刷新' }}</text>
</button>
</view>
</view>
</view>
</view>
</template>
<script>
import {
getMessageList
} from '@/static/js/common';
export default {
props: {
isActive: {
type: Boolean,
default: true
}
},
data() {
return {
loading: false,
messageList: [],
total: 0,
page: 1,
limit: 10,
pages: 0
};
},
created() {
if (this.isActive) {
this.fetchMessages();
}
},
watch: {
isActive(newVal) {
if (newVal) {
this.fetchMessages();
}
}
},
methods: {
getAvatarText(nickname) {
return nickname ? nickname.charAt(0).toUpperCase() : '?';
},
formatTime(timestamp) {
if (!timestamp) return '未知时间';
const date = new Date(timestamp * 1000);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return '刚刚';
}
if (diff < 3600000) {
return `${Math.floor(diff / 60000)}分钟前`;
}
if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}小时前`;
}
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
return `${month}${day}${hour}:${minute}`;
},
async fetchMessages(pageNum = 1) {
try {
if (this.loading) return;
this.loading = true;
const params = {
page: pageNum,
limit: this.limit
};
const res = await getMessageList(params, {
loading: {
title: '加载留言中...'
},
error: true
});
const data = res.data || {};
this.messageList = data.list || [];
this.total = data.total || 0;
this.page = data.page || 1;
this.pages = data.pages || 0;
} catch (error) {
console.error('获取留言异常:', error);
} finally {
this.loading = false;
}
},
changePage(pageNum) {
if (pageNum < 1 || pageNum > this.pages || pageNum === this.page) return;
this.fetchMessages(pageNum);
uni.pageScrollTo({
scrollTop: 0,
duration: 300
});
},
refresh() {
if (this.loading) return;
this.fetchMessages(this.page);
}
}
};
</script>
<style lang="scss" scoped>
.message-board {
min-height: 100vh;
background-color: #d76388;
}
.header {
position: relative;
padding: 180rpx 30rpx 40rpx;
}
.title-container {
text-align: center;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 10rpx;
display: block;
}
.subtitle {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
display: block;
}
.decor {
position: absolute;
border-radius: 50%;
&.decor-1 {
top: 40rpx;
left: 30rpx;
width: 100rpx;
height: 100rpx;
background-color: rgba(255, 255, 255, 0.1);
filter: blur(30rpx);
}
&.decor-2 {
top: 80rpx;
right: 40rpx;
width: 80rpx;
height: 80rpx;
background-color: rgba(255, 192, 203, 0.2);
filter: blur(20rpx);
}
}
.content {
padding: 0 30rpx 40rpx;
}
.stats-card {
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10rpx);
border-radius: 30rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.stats-header {
display: flex;
align-items: center;
}
.stats-icon-container {
margin-right: 16rpx;
}
.stats-icon {
width: 50rpx;
height: 50rpx;
background-color: #4299e1;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.icon-text {
color: #ffffff;
font-size: 24rpx;
}
.stats-title {
color: #4a5568;
font-weight: 500;
font-size: 28rpx;
flex: 1;
}
.stats-count {
background-color: #f7fafc;
border-radius: 50rpx;
padding: 8rpx 20rpx;
}
.count-text {
color: #718096;
font-size: 24rpx;
}
.message-list {
margin-bottom: 30rpx;
}
.empty-state {
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10rpx);
border-radius: 30rpx;
padding: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 6rpx solid #e2e8f0;
border-top-color: #9f7aea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
.loading-text {
color: #718096;
font-size: 28rpx;
}
.empty-container {
text-align: center;
}
.empty-icon {
width: 100rpx;
height: 100rpx;
background-color: #f7fafc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 30rpx;
font-size: 50rpx;
}
.empty-title {
color: #2d3748;
font-size: 34rpx;
font-weight: 500;
margin-bottom: 10rpx;
display: block;
}
.empty-desc {
color: #718096;
font-size: 26rpx;
display: block;
}
.message-items {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.message-item {
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10rpx);
border-radius: 30rpx;
padding: 30rpx;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s;
animation: fadeIn 0.6s ease-out forwards;
&:active {
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
}
}
.user-info {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #a78bfa, #ec4899);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
}
.avatar-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
}
.user-details {
flex: 1;
min-width: 0;
}
.user-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6rpx;
}
.username {
font-weight: 500;
color: #2d3748;
font-size: 28rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-id {
font-size: 22rpx;
color: #a0aec0;
background-color: #f7fafc;
padding: 4rpx 16rpx;
border-radius: 50rpx;
}
.message-time {
font-size: 22rpx;
color: #a0aec0;
}
.message-content {
margin-left: 100rpx;
}
.message-text {
color: #4a5568;
font-size: 28rpx;
line-height: 1.6;
word-break: break-word;
}
.pagination {
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10rpx);
border-radius: 30rpx;
padding: 20rpx;
margin-bottom: 30rpx;
}
.pagination-container {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
}
.page-button {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 30rpx;
height: 70rpx;
font-size: 26rpx;
font-weight: 500;
color: #4a5568;
background-color: #ffffff;
border: 2rpx solid #e2e8f0;
border-radius: 12rpx;
&:active {
background-color: #f7fafc;
}
&[disabled] {
opacity: 0.5;
}
}
.page-info {
display: flex;
align-items: center;
gap: 10rpx;
padding: 0 20rpx;
}
.current-page,
.total-pages {
font-size: 26rpx;
color: #4a5568;
}
.page-divider {
color: #cbd5e0;
font-size: 26rpx;
}
.refresh-container {
text-align: center;
}
.refresh-button {
display: inline-flex;
align-items: center;
gap: 10rpx;
padding: 0 40rpx;
height: 80rpx;
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: none;
border-radius: 16rpx;
font-size: 28rpx;
font-weight: 500;
&:active {
background-color: rgba(255, 255, 255, 0.3);
}
&[disabled] {
opacity: 0.5;
}
}
.refresh-icon {
font-size: 28rpx;
&.rotating {
animation: spin 1s linear infinite;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(40rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>