diff --git a/App.vue b/App.vue
index ceaacbf..ede85fb 100644
--- a/App.vue
+++ b/App.vue
@@ -1,6 +1,7 @@
diff --git a/api/notification.js b/api/notification.js
new file mode 100644
index 0000000..c132fa0
--- /dev/null
+++ b/api/notification.js
@@ -0,0 +1,60 @@
+import { request } from "@/utils/request.js";
+
+export const RatingNotificationTypeEnum = {
+ TOPIC_SUBMIT: 'topic_submit',
+ TOPIC_APPROVED: 'topic_approved',
+ TOPIC_REJECTED: 'topic_rejected',
+ ITEM_SUBMIT: 'item_submit',
+ ITEM_APPROVED: 'item_approved',
+ ITEM_REJECTED: 'item_rejected',
+ COMMENT: 'comment',
+ COMMENT_LIKE: 'comment_like',
+ SYSTEM: 'system',
+}
+/**
+ * @description: 获取当前用户的消息列表
+ * @param {page: number, type?: RatingNotificationTypeEnum} data
+ * @return
+ */
+export const fetchNotificationList = async (data) => {
+ return request({
+ url: "/api/rating/personal/notification/list",
+ method: "GET",
+ data,
+ });
+};
+
+/**
+ * @description: 获取当前用户未读消息数量
+ * @return
+ */
+
+export const fetchUnreadCount = async () => {
+ return request({
+ url: "/api/rating/personal/notification/unread_count",
+ method: "GET",
+ });
+};
+
+/**
+ * @description: 标记指定消息为已读
+ * @param {id: number} data
+ * @return
+ */
+export const markAsRead = async (id) => {
+ return request({
+ url: "/api/rating/personal/notification/read/" + id,
+ method: "Patch",
+ });
+};
+
+/**
+ * @description: 标记所有消息为已读
+ * @return
+ */
+export const markAllAsRead = async () => {
+ return request({
+ url: "/api/rating/personal/notification/read_all",
+ method: "Patch",
+ });
+};
diff --git a/components/LoginPopup/LoginPopup.vue b/components/LoginPopup/LoginPopup.vue
index f5809ef..cf040b9 100644
--- a/components/LoginPopup/LoginPopup.vue
+++ b/components/LoginPopup/LoginPopup.vue
@@ -94,6 +94,7 @@ import { getPlatformProvider, isSinglePageMode } from "@/utils/system";
import { uploadImage } from "@/utils/common";
import { apiLogin } from "@/api/auth.js";
import { wxLogin, alipayLogin } from "@/utils/login.js";
+import { refreshMessageTabBarBadge } from "@/utils/tabBar";
import PrivacyPopup from "@/components/PrivacyPopup/PrivacyPopup.vue";
const popupRef = ref(null);
@@ -219,6 +220,7 @@ const handleAlipayLogin = async () => {
uni.hideLoading();
uni.showToast({ title: "登录成功", icon: "success" });
+ refreshMessageTabBarBadge();
emit("logind");
popupRef.value.close();
@@ -292,6 +294,7 @@ const confirmLogin = async () => {
userStore.setToken(loginRes.token);
uni.hideLoading();
uni.showToast({ title: "登录成功", icon: "success" });
+ refreshMessageTabBarBadge();
emit("logind");
popupRef.value.close();
diff --git a/pages/message/index.vue b/pages/message/index.vue
index e69de29..3fbfd79 100644
--- a/pages/message/index.vue
+++ b/pages/message/index.vue
@@ -0,0 +1,879 @@
+
+
+
+
+
+ 系统消息
+ 审核通知、评论互动和系统提醒都会在这里出现
+
+
+
+ {{ unreadCount }} 未读
+
+
+ {{ readingAll ? "处理中" : "全部已读" }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ totalCount }}
+ 全部消息
+
+
+
+ {{ unreadCount }}
+ 未读提醒
+
+
+
+ {{ todayCount }}
+ 今日新增
+
+
+
+
+
+ 登录后查看你的系统消息
+ 审核结果、评论互动、点赞提醒都会同步到这里。
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+ {{ activeFilter === "unread" ? "只看未读" : "全部消息" }}
+
+
+
+ 消息加载中...
+ 正在同步你的最新通知
+
+
+
+ 消息加载失败
+ {{ loadError }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title || typeLabelMap[item.type] || "系统通知" }}
+
+ {{ typeLabelMap[item.type] || "提醒" }}
+
+
+ {{ formatRelativeTime(item.createdAt) }}
+
+
+ {{ item.content || "你有一条新的系统消息" }}
+
+
+
+
+
+
+
+ {{ loading ? "加载中..." : hasMore ? "上拉加载更多" : "没有更多消息了" }}
+
+
+
+
+ {{ activeFilter === "unread" ? "暂时没有未读消息" : "还没有系统消息" }}
+ {{ activeFilter === "unread" ? "已帮你处理完所有提醒" : "新的审核、评论和互动提醒会显示在这里" }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/stores/user.js b/stores/user.js
index c532398..c73c84a 100644
--- a/stores/user.js
+++ b/stores/user.js
@@ -34,6 +34,8 @@ export const useUserStore = defineStore("user", {
this.userInfo = {};
uni.removeStorageSync("token");
uni.removeStorageSync("userInfo");
+ uni.removeTabBarBadge({ index: 3 });
+ uni.hideTabBarRedDot({ index: 3 });
},
loadUserInfo() {
const userStr = uni.getStorageSync("userInfo");
@@ -83,6 +85,8 @@ export const useUserStore = defineStore("user", {
this.token = "";
uni.removeStorageSync("userInfo");
uni.removeStorageSync("token");
+ uni.removeTabBarBadge({ index: 3 });
+ uni.hideTabBarRedDot({ index: 3 });
},
},
});
diff --git a/utils/tabBar.js b/utils/tabBar.js
new file mode 100644
index 0000000..85f0ac2
--- /dev/null
+++ b/utils/tabBar.js
@@ -0,0 +1,65 @@
+import { fetchUnreadCount } from "@/api/notification";
+
+const MESSAGE_TAB_INDEX = 3;
+
+function parseStoredUserInfo() {
+ const raw = uni.getStorageSync("userInfo");
+ if (!raw) return {};
+
+ try {
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
+ } catch (error) {
+ return {};
+ }
+}
+
+function hasLoginState() {
+ const token = uni.getStorageSync("token");
+ const userInfo = parseStoredUserInfo();
+ return !!token || !!userInfo?.nickName;
+}
+
+export function syncMessageTabBarBadge(unreadCount = 0) {
+ const count = Number(unreadCount) || 0;
+
+ if (count > 0) {
+ const text = count > 99 ? "99+" : String(count);
+ uni.setTabBarBadge({
+ index: MESSAGE_TAB_INDEX,
+ text,
+ });
+ return;
+ }
+
+ uni.removeTabBarBadge({
+ index: MESSAGE_TAB_INDEX,
+ });
+ uni.hideTabBarRedDot({
+ index: MESSAGE_TAB_INDEX,
+ });
+}
+
+export async function refreshMessageTabBarBadge() {
+ if (!hasLoginState()) {
+ syncMessageTabBarBadge(0);
+ return 0;
+ }
+
+ try {
+ const res = await fetchUnreadCount();
+ const count =
+ Number(
+ res?.data?.count ??
+ res?.data?.unreadCount ??
+ res?.count ??
+ res?.unreadCount ??
+ res?.data ??
+ 0
+ ) || 0;
+
+ syncMessageTabBarBadge(count);
+ return count;
+ } catch (error) {
+ return 0;
+ }
+}