865 lines
20 KiB
Vue
865 lines
20 KiB
Vue
<template>
|
|
<view class="message-page">
|
|
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
|
<view class="nav-content">
|
|
<view>
|
|
<text class="nav-title">系统消息</text>
|
|
<text class="nav-subtitle">审核通知、评论互动和系统提醒都会在这里出现</text>
|
|
</view>
|
|
<view class="nav-right">
|
|
<view v-if="unreadCount > 0" class="unread-chip">
|
|
<text class="unread-chip-text">{{ unreadCount }} 未读</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view :style="{ height: statusBarHeight + 64 + 'px' }"></view>
|
|
|
|
<view class="page-body">
|
|
<view v-if="!isLoggedIn" class="state-card login-card">
|
|
<text class="state-title">登录后查看你的系统消息</text>
|
|
<text class="state-desc">审核结果、评论互动、点赞提醒都会同步到这里。</text>
|
|
<button class="primary-btn" @tap="openLoginPopup('请先登录后查看消息')">立即登录</button>
|
|
</view>
|
|
|
|
<template v-else>
|
|
<view class="filter-bar">
|
|
<view class="filter-tabs">
|
|
<view
|
|
v-for="tab in filterTabs"
|
|
:key="tab.value"
|
|
class="filter-tab"
|
|
:class="{ active: activeFilter === tab.value }"
|
|
@tap="activeFilter = tab.value"
|
|
>
|
|
<text class="filter-tab-text">{{ tab.label }}</text>
|
|
</view>
|
|
</view>
|
|
<view
|
|
class="read-all-btn filter-read-all-btn"
|
|
:class="{ disabled: readingAll || unreadCount === 0 }"
|
|
@tap="handleMarkAllRead"
|
|
>
|
|
<text class="read-all-text">{{ readingAll ? "处理中" : "全部已读" }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="loading && !notificationList.length" class="state-card">
|
|
<text class="state-title">消息加载中...</text>
|
|
<text class="state-desc">正在同步你的最新通知</text>
|
|
</view>
|
|
|
|
<view v-else-if="loadError && !notificationList.length" class="state-card">
|
|
<text class="state-title">消息加载失败</text>
|
|
<text class="state-desc">{{ loadError }}</text>
|
|
<button class="primary-btn" @tap="refreshList">重新加载</button>
|
|
</view>
|
|
|
|
<view v-else-if="groupedNotifications.length" class="section-list">
|
|
<view v-for="section in groupedNotifications" :key="section.day" class="section-block">
|
|
<view class="section-header">
|
|
<text class="section-title">{{ formatDayLabel(section.day) }}</text>
|
|
</view>
|
|
<view
|
|
v-for="item in section.items"
|
|
:key="item.id"
|
|
class="message-card"
|
|
:class="{ unread: !item.isRead }"
|
|
@tap="handleItemTap(item)"
|
|
>
|
|
<view class="message-avatar-wrap">
|
|
<image class="message-avatar" :src="resolveAvatar(item)" mode="aspectFill" />
|
|
<view v-if="!item.isRead" class="message-dot"></view>
|
|
</view>
|
|
|
|
<view class="message-main">
|
|
<view class="message-head">
|
|
<view class="message-head-left">
|
|
<text class="message-title">{{ item.title || typeLabelMap[item.type] || "系统通知" }}</text>
|
|
<view
|
|
class="type-tag"
|
|
:style="{
|
|
color: resolveTypeStyle(item.type).color,
|
|
background: resolveTypeStyle(item.type).background
|
|
}"
|
|
>
|
|
<text class="type-tag-text">{{ typeLabelMap[item.type] || "提醒" }}</text>
|
|
</view>
|
|
</view>
|
|
<text class="message-time">{{ formatRelativeTime(item.createdAt) }}</text>
|
|
</view>
|
|
|
|
<text class="message-content">{{ item.content || "你有一条新的系统消息" }}</text>
|
|
|
|
<view class="message-footer">
|
|
<text class="message-meta">{{ resolveActionText(item) }}</text>
|
|
<text class="message-link">{{ resolveJumpText(item) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="list-status">
|
|
<text>{{ loading ? "加载中..." : hasMore ? "上拉加载更多" : "没有更多消息了" }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="state-card">
|
|
<text class="state-title">{{ activeFilter === "unread" ? "暂时没有未读消息" : "还没有系统消息" }}</text>
|
|
<text class="state-desc">{{ activeFilter === "unread" ? "已帮你处理完所有提醒" : "新的审核、评论和互动提醒会显示在这里" }}</text>
|
|
</view>
|
|
</template>
|
|
</view>
|
|
|
|
<LoginPopup ref="loginPopupRef" @logind="handleLoginSuccess" />
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from "vue";
|
|
import { onLoad, onPullDownRefresh, onReachBottom, onShow } from "@dcloudio/uni-app";
|
|
import { useUserStore } from "@/stores/user";
|
|
import { getStatusBarHeight } from "@/utils/system";
|
|
import { FILE_BASE_URL } from "@/utils/constants";
|
|
import { syncMessageTabBarBadge } from "@/utils/tabBar";
|
|
import {
|
|
fetchNotificationList,
|
|
fetchUnreadCount,
|
|
markAsRead,
|
|
markAllAsRead,
|
|
} from "@/api/notification";
|
|
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
|
|
|
|
const DEFAULT_AVATAR = "/static/images/icon/robot.png";
|
|
|
|
const userStore = useUserStore();
|
|
const loginPopupRef = ref(null);
|
|
const statusBarHeight = ref(getStatusBarHeight() || 20);
|
|
const loading = ref(false);
|
|
const readingAll = ref(false);
|
|
const page = ref(1);
|
|
const hasMore = ref(true);
|
|
const totalCount = ref(0);
|
|
const unreadCount = ref(0);
|
|
const loadError = ref("");
|
|
const activeFilter = ref("all");
|
|
const notificationList = ref([]);
|
|
const readingIds = ref([]);
|
|
|
|
const filterTabs = [
|
|
{ label: "全部", value: "all" },
|
|
{ label: "未读", value: "unread" },
|
|
];
|
|
|
|
const typeLabelMap = {
|
|
topic_submit: "话题提交",
|
|
topic_approved: "审核通过",
|
|
topic_rejected: "审核未过",
|
|
item_submit: "评分项提交",
|
|
item_approved: "评分项通过",
|
|
item_rejected: "评分项未过",
|
|
comment: "评论互动",
|
|
comment_like: "评论获赞",
|
|
system: "系统通知",
|
|
};
|
|
|
|
const typeStyleMap = {
|
|
topic_submit: { color: "#ff7d00", background: "rgba(255, 125, 0, 0.12)" },
|
|
topic_approved: { color: "#12b76a", background: "rgba(18, 183, 106, 0.12)" },
|
|
topic_rejected: { color: "#f04438", background: "rgba(240, 68, 56, 0.12)" },
|
|
item_submit: { color: "#7a5af8", background: "rgba(122, 90, 248, 0.12)" },
|
|
item_approved: { color: "#0ba5ec", background: "rgba(11, 165, 236, 0.12)" },
|
|
item_rejected: { color: "#ef6820", background: "rgba(239, 104, 32, 0.12)" },
|
|
comment: { color: "#ec4899", background: "rgba(236, 72, 153, 0.12)" },
|
|
comment_like: { color: "#f63d68", background: "rgba(246, 61, 104, 0.12)" },
|
|
system: { color: "#667085", background: "rgba(102, 112, 133, 0.12)" },
|
|
};
|
|
|
|
const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName);
|
|
let firstShow = true;
|
|
|
|
const todayCount = computed(() => {
|
|
const today = formatDateKey(new Date());
|
|
return notificationList.value.filter((item) => (item.day || formatDateKey(item.createdAt)) === today).length;
|
|
});
|
|
|
|
const filteredNotifications = computed(() => {
|
|
if (activeFilter.value === "unread") {
|
|
return notificationList.value.filter((item) => !item.isRead);
|
|
}
|
|
return notificationList.value;
|
|
});
|
|
|
|
const groupedNotifications = computed(() => {
|
|
const map = {};
|
|
filteredNotifications.value.forEach((item) => {
|
|
const day = item.day || formatDateKey(item.createdAt);
|
|
if (!map[day]) {
|
|
map[day] = [];
|
|
}
|
|
map[day].push(item);
|
|
});
|
|
|
|
return Object.keys(map)
|
|
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
|
|
.map((day) => ({
|
|
day,
|
|
items: map[day].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
|
|
}));
|
|
});
|
|
|
|
const openLoginPopup = (message = "请先登录后再操作") => {
|
|
loginPopupRef.value?.open();
|
|
if (message) {
|
|
uni.showToast({ title: message, icon: "none" });
|
|
}
|
|
};
|
|
|
|
const formatDateKey = (value) => {
|
|
const date = value instanceof Date ? value : new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
const year = date.getFullYear();
|
|
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
const day = `${date.getDate()}`.padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
const formatRelativeTime = (value) => {
|
|
if (!value) return "刚刚";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "刚刚";
|
|
|
|
const diff = Date.now() - date.getTime();
|
|
const minute = 60 * 1000;
|
|
const hour = 60 * minute;
|
|
|
|
if (diff < minute) return "刚刚";
|
|
if (diff < hour) return `${Math.max(1, Math.floor(diff / minute))}分钟前`;
|
|
if (diff < 24 * hour) return `${Math.max(1, Math.floor(diff / hour))}小时前`;
|
|
|
|
const month = date.getMonth() + 1;
|
|
const day = date.getDate();
|
|
const hours = `${date.getHours()}`.padStart(2, "0");
|
|
const minutes = `${date.getMinutes()}`.padStart(2, "0");
|
|
const currentYear = new Date().getFullYear();
|
|
if (date.getFullYear() === currentYear) {
|
|
return `${month}月${day}日 ${hours}:${minutes}`;
|
|
}
|
|
return `${date.getFullYear()}-${`${month}`.padStart(2, "0")}-${`${day}`.padStart(2, "0")}`;
|
|
};
|
|
|
|
const formatDayLabel = (day) => {
|
|
if (!day) return "更早";
|
|
const today = formatDateKey(new Date());
|
|
const yesterdayDate = new Date();
|
|
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
|
|
const yesterday = formatDateKey(yesterdayDate);
|
|
|
|
if (day === today) return "今天";
|
|
if (day === yesterday) return "昨天";
|
|
|
|
const date = new Date(day);
|
|
if (Number.isNaN(date.getTime())) return day;
|
|
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
|
};
|
|
|
|
const resolveAvatar = (item) => {
|
|
const avatar = item?.fromUser?.avatar;
|
|
if (!avatar) return DEFAULT_AVATAR;
|
|
return String(avatar).startsWith("http") ? avatar : `${FILE_BASE_URL}${avatar}`;
|
|
};
|
|
|
|
const resolveTypeStyle = (type) => {
|
|
return typeStyleMap[type] || typeStyleMap.system;
|
|
};
|
|
|
|
const resolveActionText = (item) => {
|
|
if (!item.isRead) return "点击后将标记为已读";
|
|
if (item.updatedAt && item.updatedAt !== item.createdAt) return "已处理";
|
|
return "查看详情";
|
|
};
|
|
|
|
const resolveJumpText = (item) => {
|
|
if (item.itemId) return "查看评分详情";
|
|
if (item.topicId) return "查看话题";
|
|
return "查看消息";
|
|
};
|
|
|
|
const normalizeListResponse = (res) => {
|
|
const source = res?.data || res || {};
|
|
const list = Array.isArray(source.list)
|
|
? source.list
|
|
: Array.isArray(res?.list)
|
|
? res.list
|
|
: [];
|
|
const total = Number(source.totalCount ?? res?.totalCount ?? list.length);
|
|
return { list, total };
|
|
};
|
|
|
|
const mergeNotificationList = (list, reset = false) => {
|
|
if (reset) {
|
|
notificationList.value = list;
|
|
return;
|
|
}
|
|
|
|
const map = new Map(notificationList.value.map((item) => [item.id, item]));
|
|
list.forEach((item) => {
|
|
map.set(item.id, item);
|
|
});
|
|
notificationList.value = Array.from(map.values()).sort(
|
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
);
|
|
};
|
|
|
|
const loadUnread = async () => {
|
|
if (!isLoggedIn.value) {
|
|
unreadCount.value = 0;
|
|
syncMessageTabBarBadge(0);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetchUnreadCount();
|
|
const count =
|
|
Number(
|
|
res?.data?.count ??
|
|
res?.data?.unreadCount ??
|
|
res?.count ??
|
|
res?.unreadCount ??
|
|
res?.data ??
|
|
0
|
|
) || 0;
|
|
unreadCount.value = count;
|
|
syncMessageTabBarBadge(count);
|
|
} catch (error) {
|
|
unreadCount.value = unreadCount.value || 0;
|
|
syncMessageTabBarBadge(unreadCount.value);
|
|
}
|
|
};
|
|
|
|
const loadNotifications = async ({ reset = false } = {}) => {
|
|
if (!isLoggedIn.value) {
|
|
notificationList.value = [];
|
|
totalCount.value = 0;
|
|
hasMore.value = false;
|
|
loadError.value = "";
|
|
return;
|
|
}
|
|
|
|
if (loading.value) return;
|
|
if (!reset && !hasMore.value) return;
|
|
|
|
try {
|
|
loading.value = true;
|
|
loadError.value = "";
|
|
const nextPage = reset ? 1 : page.value;
|
|
const res = await fetchNotificationList({ page: nextPage });
|
|
const { list, total } = normalizeListResponse(res);
|
|
totalCount.value = total;
|
|
mergeNotificationList(list, reset);
|
|
hasMore.value = notificationList.value.length < total && list.length > 0;
|
|
page.value = nextPage + 1;
|
|
} catch (error) {
|
|
console.error("获取消息列表失败:", error);
|
|
loadError.value = "网络开了点小差,稍后再试";
|
|
if (reset) {
|
|
notificationList.value = [];
|
|
totalCount.value = 0;
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
uni.stopPullDownRefresh();
|
|
}
|
|
};
|
|
|
|
const refreshList = async () => {
|
|
page.value = 1;
|
|
hasMore.value = true;
|
|
await Promise.all([loadUnread(), loadNotifications({ reset: true })]);
|
|
};
|
|
|
|
const setItemReadState = (id, isRead = true) => {
|
|
notificationList.value = notificationList.value.map((item) => {
|
|
if (item.id !== id) return item;
|
|
return {
|
|
...item,
|
|
isRead,
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleNavigate = (item) => {
|
|
if (item.itemId) {
|
|
uni.navigateTo({
|
|
url: `/pages/rating/detail?topicId=${item.topicId || ""}&itemId=${item.itemId}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (item.topicId) {
|
|
uni.navigateTo({
|
|
url: `/pages/rating/index?topicId=${item.topicId}`,
|
|
});
|
|
return;
|
|
}
|
|
};
|
|
|
|
const markSingleAsRead = async (item) => {
|
|
if (!item?.id || item.isRead || readingIds.value.includes(item.id)) return;
|
|
|
|
readingIds.value = [...readingIds.value, item.id];
|
|
try {
|
|
await markAsRead(item.id);
|
|
setItemReadState(item.id, true);
|
|
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
|
syncMessageTabBarBadge(unreadCount.value);
|
|
} catch (error) {
|
|
console.error("标记消息已读失败:", error);
|
|
} finally {
|
|
readingIds.value = readingIds.value.filter((id) => id !== item.id);
|
|
}
|
|
};
|
|
|
|
const handleItemTap = async (item) => {
|
|
await markSingleAsRead(item);
|
|
handleNavigate(item);
|
|
};
|
|
|
|
const handleMarkAllRead = async () => {
|
|
if (!isLoggedIn.value) {
|
|
openLoginPopup("请先登录后再操作");
|
|
return;
|
|
}
|
|
if (readingAll.value || unreadCount.value === 0) return;
|
|
|
|
try {
|
|
readingAll.value = true;
|
|
await markAllAsRead();
|
|
notificationList.value = notificationList.value.map((item) => ({
|
|
...item,
|
|
isRead: true,
|
|
}));
|
|
unreadCount.value = 0;
|
|
syncMessageTabBarBadge(0);
|
|
uni.showToast({ title: "已全部标记为已读", icon: "none" });
|
|
} catch (error) {
|
|
console.error("全部标记已读失败:", error);
|
|
uni.showToast({ title: "操作失败,请稍后再试", icon: "none" });
|
|
} finally {
|
|
readingAll.value = false;
|
|
}
|
|
};
|
|
|
|
const handleLoginSuccess = async () => {
|
|
await refreshList();
|
|
};
|
|
|
|
onLoad(() => {
|
|
refreshList();
|
|
});
|
|
|
|
onShow(() => {
|
|
if (firstShow) {
|
|
firstShow = false;
|
|
return;
|
|
}
|
|
refreshList();
|
|
});
|
|
|
|
onPullDownRefresh(() => {
|
|
refreshList();
|
|
});
|
|
|
|
onReachBottom(() => {
|
|
loadNotifications();
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.message-page {
|
|
min-height: 100vh;
|
|
background:
|
|
radial-gradient(circle at top, rgba(255, 116, 71, 0.1) 0, rgba(255, 116, 71, 0) 36%),
|
|
#f7f7f8;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.nav-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 100;
|
|
background: rgba(247, 247, 248, 0.9);
|
|
backdrop-filter: blur(18rpx);
|
|
}
|
|
|
|
.nav-content {
|
|
min-height: 64px;
|
|
padding: 0 28rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.nav-title {
|
|
display: block;
|
|
font-size: 38rpx;
|
|
font-weight: 700;
|
|
color: #18181b;
|
|
}
|
|
|
|
.nav-subtitle {
|
|
display: block;
|
|
margin-top: 8rpx;
|
|
font-size: 22rpx;
|
|
color: #8a8f99;
|
|
}
|
|
|
|
.nav-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.unread-chip {
|
|
padding: 10rpx 18rpx;
|
|
border-radius: 999rpx;
|
|
background: rgba(255, 59, 48, 0.1);
|
|
}
|
|
|
|
.unread-chip-text {
|
|
font-size: 22rpx;
|
|
font-weight: 600;
|
|
color: #ff4d4f;
|
|
}
|
|
|
|
.read-all-btn {
|
|
padding: 12rpx 20rpx;
|
|
border-radius: 999rpx;
|
|
background: #111111;
|
|
}
|
|
|
|
.filter-read-all-btn {
|
|
flex-shrink: 0;
|
|
padding: 10rpx 22rpx;
|
|
}
|
|
|
|
.read-all-btn.disabled {
|
|
opacity: 0.45;
|
|
}
|
|
|
|
.read-all-text {
|
|
font-size: 22rpx;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.page-body {
|
|
padding: 20rpx 24rpx 140rpx;
|
|
}
|
|
|
|
.hero-card {
|
|
padding: 30rpx 28rpx;
|
|
border-radius: 32rpx;
|
|
background: linear-gradient(135deg, #ffffff 0%, #fff5f2 100%);
|
|
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.05);
|
|
}
|
|
|
|
.hero-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.hero-title {
|
|
display: block;
|
|
font-size: 34rpx;
|
|
font-weight: 700;
|
|
color: #171717;
|
|
}
|
|
|
|
.hero-desc {
|
|
display: block;
|
|
margin-top: 10rpx;
|
|
font-size: 24rpx;
|
|
line-height: 1.6;
|
|
color: #7a7f88;
|
|
}
|
|
|
|
.hero-icon {
|
|
width: 96rpx;
|
|
height: 96rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.hero-stats {
|
|
margin-top: 28rpx;
|
|
padding-top: 24rpx;
|
|
border-top: 1rpx solid rgba(17, 17, 17, 0.06);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.hero-stat {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.hero-stat-num {
|
|
font-size: 34rpx;
|
|
font-weight: 700;
|
|
color: #111111;
|
|
}
|
|
|
|
.hero-stat-num.accent {
|
|
color: #ff5a36;
|
|
}
|
|
|
|
.hero-stat-label {
|
|
font-size: 22rpx;
|
|
color: #8a8f99;
|
|
}
|
|
|
|
.hero-divider {
|
|
width: 1rpx;
|
|
height: 56rpx;
|
|
background: rgba(17, 17, 17, 0.08);
|
|
}
|
|
|
|
.filter-bar {
|
|
margin-top: 24rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.filter-tabs {
|
|
padding: 8rpx;
|
|
border-radius: 999rpx;
|
|
background: #ffffff;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.filter-tab {
|
|
min-width: 112rpx;
|
|
height: 60rpx;
|
|
padding: 0 24rpx;
|
|
border-radius: 999rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.filter-tab.active {
|
|
background: #111111;
|
|
}
|
|
|
|
.filter-tab-text {
|
|
font-size: 24rpx;
|
|
font-weight: 600;
|
|
color: #8a8f99;
|
|
}
|
|
|
|
.filter-tab.active .filter-tab-text {
|
|
color: #ffffff;
|
|
}
|
|
|
|
.filter-hint {
|
|
font-size: 22rpx;
|
|
color: #9aa0aa;
|
|
}
|
|
|
|
.section-list {
|
|
margin-top: 20rpx;
|
|
}
|
|
|
|
.section-block + .section-block {
|
|
margin-top: 24rpx;
|
|
}
|
|
|
|
.section-header {
|
|
padding: 6rpx 8rpx 14rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 24rpx;
|
|
font-weight: 600;
|
|
color: #8a8f99;
|
|
}
|
|
|
|
.message-card {
|
|
position: relative;
|
|
display: flex;
|
|
gap: 20rpx;
|
|
padding: 24rpx;
|
|
border-radius: 28rpx;
|
|
background: #ffffff;
|
|
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.05);
|
|
}
|
|
|
|
.message-card + .message-card {
|
|
margin-top: 16rpx;
|
|
}
|
|
|
|
.message-card.unread {
|
|
background: linear-gradient(180deg, #ffffff 0%, #fff9f7 100%);
|
|
}
|
|
|
|
.message-avatar-wrap {
|
|
position: relative;
|
|
width: 84rpx;
|
|
height: 84rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.message-avatar {
|
|
width: 84rpx;
|
|
height: 84rpx;
|
|
border-radius: 50%;
|
|
background: #f4f5f7;
|
|
}
|
|
|
|
.message-dot {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 2rpx;
|
|
width: 18rpx;
|
|
height: 18rpx;
|
|
border-radius: 50%;
|
|
background: #ff4d4f;
|
|
border: 4rpx solid #ffffff;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.message-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.message-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.message-head-left {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.message-title {
|
|
font-size: 28rpx;
|
|
font-weight: 700;
|
|
color: #18181b;
|
|
}
|
|
|
|
.type-tag {
|
|
padding: 6rpx 14rpx;
|
|
border-radius: 999rpx;
|
|
}
|
|
|
|
.type-tag-text {
|
|
font-size: 20rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.message-time {
|
|
flex-shrink: 0;
|
|
font-size: 22rpx;
|
|
color: #a0a4ae;
|
|
}
|
|
|
|
.message-content {
|
|
display: block;
|
|
margin-top: 12rpx;
|
|
font-size: 26rpx;
|
|
line-height: 1.7;
|
|
color: #555b66;
|
|
}
|
|
|
|
.message-footer {
|
|
margin-top: 16rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.message-meta {
|
|
font-size: 22rpx;
|
|
color: #a0a4ae;
|
|
}
|
|
|
|
.message-link {
|
|
font-size: 22rpx;
|
|
font-weight: 600;
|
|
color: #ff5a36;
|
|
}
|
|
|
|
.state-card {
|
|
margin-top: 24rpx;
|
|
padding: 44rpx 32rpx;
|
|
border-radius: 32rpx;
|
|
background: #ffffff;
|
|
text-align: center;
|
|
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.05);
|
|
}
|
|
|
|
.login-card {
|
|
margin-top: 28rpx;
|
|
}
|
|
|
|
.state-title {
|
|
display: block;
|
|
font-size: 30rpx;
|
|
font-weight: 700;
|
|
color: #18181b;
|
|
}
|
|
|
|
.state-desc {
|
|
display: block;
|
|
margin-top: 14rpx;
|
|
font-size: 24rpx;
|
|
line-height: 1.7;
|
|
color: #8a8f99;
|
|
}
|
|
|
|
.primary-btn {
|
|
margin-top: 24rpx;
|
|
height: 84rpx;
|
|
line-height: 84rpx;
|
|
border-radius: 999rpx;
|
|
background: linear-gradient(135deg, #ff7a45 0%, #ff4d4f 100%);
|
|
color: #ffffff;
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.primary-btn::after {
|
|
border: none;
|
|
}
|
|
|
|
.list-status {
|
|
padding: 28rpx 0 12rpx;
|
|
text-align: center;
|
|
font-size: 22rpx;
|
|
color: #a0a4ae;
|
|
}
|
|
</style>
|