|
|
@@ -0,0 +1,685 @@
|
|
|
+<template>
|
|
|
+ <div class="notification-drawer-content">
|
|
|
+ <!-- 顶部 Tab:通知 / 消息 -->
|
|
|
+ <div class="drawer-tabs">
|
|
|
+ <div class="tab-item" :class="{ active: activeTab === 'notice' }" @click="activeTab = 'notice'">通知</div>
|
|
|
+ <div class="tab-item" :class="{ active: activeTab === 'message' }" @click="activeTab = 'message'">消息</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="drawer-body">
|
|
|
+ <!-- 左侧分类:通知 Tab 与 消息 Tab 不同 -->
|
|
|
+ <div class="drawer-side">
|
|
|
+ <template v-if="activeTab === 'notice'">
|
|
|
+ <div
|
|
|
+ v-for="cat in noticeCategories"
|
|
|
+ :key="cat.key"
|
|
|
+ class="side-item"
|
|
|
+ :class="{ active: activeCategory === cat.key }"
|
|
|
+ @click="switchNoticeCategory(cat.key)"
|
|
|
+ >
|
|
|
+ <el-icon class="side-icon">
|
|
|
+ <component :is="cat.icon" />
|
|
|
+ </el-icon>
|
|
|
+ <span class="side-text">{{ cat.title }}</span>
|
|
|
+ <el-badge v-if="cat.unread > 0" :value="cat.unread" class="side-badge" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div
|
|
|
+ v-for="cat in messageCategories"
|
|
|
+ :key="cat.key"
|
|
|
+ class="side-item"
|
|
|
+ :class="{ active: messageCategory === cat.key }"
|
|
|
+ @click="switchMessageCategory(cat.key)"
|
|
|
+ >
|
|
|
+ <el-icon class="side-icon">
|
|
|
+ <component :is="cat.icon" />
|
|
|
+ </el-icon>
|
|
|
+ <span class="side-text">{{ cat.title }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧:通知列表 -->
|
|
|
+ <div v-if="activeTab === 'notice'" class="drawer-main-wrap">
|
|
|
+ <div class="drawer-main">
|
|
|
+ <div v-if="currentLoading" class="empty-tip">
|
|
|
+ <el-icon class="is-loading" :size="24">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ <span style="margin-left: 8px">加载中...</span>
|
|
|
+ </div>
|
|
|
+ <template v-else>
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in currentList"
|
|
|
+ :key="item.id + '_' + index"
|
|
|
+ class="notice-card"
|
|
|
+ :class="{ unread: item.unread }"
|
|
|
+ >
|
|
|
+ <div class="card-row">
|
|
|
+ <span class="card-title">
|
|
|
+ {{ item.title }}
|
|
|
+ <span v-if="item.unread" class="unread-dot" />
|
|
|
+ </span>
|
|
|
+ <span class="card-date">{{ item.date }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="card-content">
|
|
|
+ {{ item.content }}
|
|
|
+ </div>
|
|
|
+ <div class="card-actions">
|
|
|
+ <el-button size="small" @click="handleViewDetail(item)"> 查看详情 </el-button>
|
|
|
+ <el-button size="small" @click="handleDelete(item, index)"> 删除 </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="currentList.length === 0" class="empty-tip">暂无数据</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <div v-if="currentPagination.total > 0" class="pagination-wrap">
|
|
|
+ <el-pagination
|
|
|
+ :current-page="currentPagination.pageNum"
|
|
|
+ :page-size="currentPagination.pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="currentPagination.total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ small
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧:消息列表(如图:头像 + 发送者 + 红点 + 内容 + 日期 + 删除) -->
|
|
|
+ <div v-else class="drawer-main-wrap">
|
|
|
+ <div class="drawer-main">
|
|
|
+ <div v-if="messageLoading" class="empty-tip">
|
|
|
+ <el-icon class="is-loading" :size="24">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ <span style="margin-left: 8px">加载中...</span>
|
|
|
+ </div>
|
|
|
+ <template v-else>
|
|
|
+ <div v-for="(item, index) in currentMessageList" :key="item.id + '_' + index" class="message-card">
|
|
|
+ <div class="message-avatar">
|
|
|
+ <el-avatar :size="40" :src="item.avatar">
|
|
|
+ <el-icon><UserFilled /></el-icon>
|
|
|
+ </el-avatar>
|
|
|
+ </div>
|
|
|
+ <div class="message-body">
|
|
|
+ <div class="message-row">
|
|
|
+ <span class="message-sender">{{ item.senderName }}</span>
|
|
|
+ <span class="message-date">{{ item.date }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="message-content">
|
|
|
+ {{ item.content }}
|
|
|
+ </div>
|
|
|
+ <div class="message-actions">
|
|
|
+ <el-button size="small" type="default" @click="handleMessageDelete(item, index)"> 删除 </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="currentMessageList.length === 0" class="empty-tip">暂无数据</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 详情弹窗 -->
|
|
|
+ <el-dialog v-model="detailVisible" :title="currentDetail?.title" width="500px">
|
|
|
+ <div class="detail-dialog-content">
|
|
|
+ {{ currentDetail?.content }}
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, onMounted, watch } from "vue";
|
|
|
+import { House, List, User, Loading, Message, UserFilled } from "@element-plus/icons-vue";
|
|
|
+import { localGet } from "@/utils";
|
|
|
+import {
|
|
|
+ getNoticeListForHeader,
|
|
|
+ readNoticeById,
|
|
|
+ getCountUnreadByType,
|
|
|
+ getNoFriendMessage,
|
|
|
+ getMessageList
|
|
|
+} from "@/api/modules/headerNotice";
|
|
|
+import type { NoFriendMessageItem } from "@/api/modules/headerNotice";
|
|
|
+
|
|
|
+interface NoticeItem {
|
|
|
+ id: string;
|
|
|
+ title: string;
|
|
|
+ content: string;
|
|
|
+ date: string;
|
|
|
+ unread: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+interface MessageItem {
|
|
|
+ id: string;
|
|
|
+ senderName: string;
|
|
|
+ content: string;
|
|
|
+ date: string;
|
|
|
+ unread: boolean;
|
|
|
+ avatar?: string;
|
|
|
+}
|
|
|
+
|
|
|
+const activeTab = ref<"notice" | "message">("notice");
|
|
|
+const activeCategory = ref("system");
|
|
|
+const messageCategory = ref("unfollowed");
|
|
|
+
|
|
|
+// 通知 Tab 分类
|
|
|
+const noticeTypeByKey: Record<string, number> = {
|
|
|
+ system: 1,
|
|
|
+ order: 2,
|
|
|
+ related: 0
|
|
|
+};
|
|
|
+const noticeCategories = ref([
|
|
|
+ { key: "system", title: "系统通知", icon: House, unread: 0 },
|
|
|
+ { key: "order", title: "订单提醒", icon: List, unread: 0 },
|
|
|
+ { key: "related", title: "与我相关", icon: User, unread: 0 }
|
|
|
+]);
|
|
|
+
|
|
|
+// 消息 Tab 分类(如图:未关注人消息、消息列表)
|
|
|
+const messageCategories = ref([
|
|
|
+ { key: "unfollowed", title: "未关注人消息", icon: User, unread: 0 },
|
|
|
+ { key: "messageList", title: "消息列表", icon: Message, unread: 0 }
|
|
|
+]);
|
|
|
+
|
|
|
+const listByCategory = ref<Record<string, NoticeItem[]>>({
|
|
|
+ system: [],
|
|
|
+ order: [],
|
|
|
+ related: []
|
|
|
+});
|
|
|
+
|
|
|
+// 消息列表数据:未关注人消息、消息列表均走接口
|
|
|
+const messageListByCategory = ref<Record<string, MessageItem[]>>({
|
|
|
+ unfollowed: [],
|
|
|
+ messageList: []
|
|
|
+});
|
|
|
+
|
|
|
+const messageLoading = ref(false);
|
|
|
+
|
|
|
+// 每个通知分类的分页与加载态
|
|
|
+const paginationByCategory = ref<Record<string, { pageNum: number; pageSize: number; total: number }>>({
|
|
|
+ system: { pageNum: 1, pageSize: 10, total: 0 },
|
|
|
+ order: { pageNum: 1, pageSize: 10, total: 0 },
|
|
|
+ related: { pageNum: 1, pageSize: 10, total: 0 }
|
|
|
+});
|
|
|
+const loadingByCategory = ref<Record<string, boolean>>({
|
|
|
+ system: false,
|
|
|
+ order: false,
|
|
|
+ related: false
|
|
|
+});
|
|
|
+
|
|
|
+const detailVisible = ref(false);
|
|
|
+const currentDetail = ref<NoticeItem | null>(null);
|
|
|
+
|
|
|
+const currentList = computed(() => {
|
|
|
+ return listByCategory.value[activeCategory.value] ?? [];
|
|
|
+});
|
|
|
+
|
|
|
+const currentPagination = computed(() => {
|
|
|
+ return paginationByCategory.value[activeCategory.value] ?? { pageNum: 1, pageSize: 10, total: 0 };
|
|
|
+});
|
|
|
+
|
|
|
+const currentLoading = computed(() => {
|
|
|
+ return loadingByCategory.value[activeCategory.value] ?? false;
|
|
|
+});
|
|
|
+
|
|
|
+const currentMessageList = computed(() => {
|
|
|
+ return messageListByCategory.value[messageCategory.value] ?? [];
|
|
|
+});
|
|
|
+
|
|
|
+function switchNoticeCategory(key: string) {
|
|
|
+ activeCategory.value = key;
|
|
|
+ const list = listByCategory.value[key] ?? [];
|
|
|
+ const loading = loadingByCategory.value[key];
|
|
|
+ if (list.length === 0 && !loading) {
|
|
|
+ paginationByCategory.value[key].pageNum = 1;
|
|
|
+ fetchNoticeList(key);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function switchMessageCategory(key: string) {
|
|
|
+ messageCategory.value = key;
|
|
|
+ if (key === "unfollowed") {
|
|
|
+ fetchNoFriendMessage();
|
|
|
+ } else if (key === "messageList") {
|
|
|
+ fetchMessageList();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 未关注人消息:/message/getNoFriendMessage,参数 receiverId
|
|
|
+function mapNoFriendToMessageItem(item: NoFriendMessageItem): MessageItem {
|
|
|
+ const createdTime = item.createdTime ?? "";
|
|
|
+ const dateStr = createdTime.includes(" ") ? createdTime.split(" ")[0].replace(/-/g, "/") : createdTime.replace(/-/g, "/");
|
|
|
+ return {
|
|
|
+ id: String(item.id ?? ""),
|
|
|
+ senderName: item.senderName ?? item.userName ?? "",
|
|
|
+ content: item.content ?? "",
|
|
|
+ date: dateStr,
|
|
|
+ unread: item.isRead === 0,
|
|
|
+ avatar: item.userImage ?? item.senderImg ?? item.storeImg ?? undefined
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchNoFriendMessage() {
|
|
|
+ const receiverId = getReceiverId();
|
|
|
+ if (!receiverId) {
|
|
|
+ messageListByCategory.value.unfollowed = [];
|
|
|
+ const cat = messageCategories.value.find(c => c.key === "unfollowed");
|
|
|
+ if (cat) cat.unread = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ messageLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getNoFriendMessage({ receiverId: "store_" + receiverId });
|
|
|
+ const data = res?.data ?? res;
|
|
|
+ const rawList = Array.isArray(data) ? data : data ? [data] : [];
|
|
|
+ const list: MessageItem[] = rawList.map((item: NoFriendMessageItem) => mapNoFriendToMessageItem(item));
|
|
|
+ messageListByCategory.value.unfollowed = list;
|
|
|
+ const unreadTotal = rawList.reduce(
|
|
|
+ (sum: number, item: NoFriendMessageItem) => sum + (item.notReadCount ?? (item.isRead === 0 ? 1 : 0)),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const cat = messageCategories.value.find(c => c.key === "unfollowed");
|
|
|
+ if (cat) cat.unread = unreadTotal;
|
|
|
+ } catch (e) {
|
|
|
+ messageListByCategory.value.unfollowed = [];
|
|
|
+ const cat = messageCategories.value.find(c => c.key === "unfollowed");
|
|
|
+ if (cat) cat.unread = 0;
|
|
|
+ } finally {
|
|
|
+ messageLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 消息列表:/message/getMessageList,参数 friendType 默认 0、receiverId,返回结构与 getNoFriendMessage 一致
|
|
|
+async function fetchMessageList() {
|
|
|
+ const receiverId = getReceiverId();
|
|
|
+ if (!receiverId) {
|
|
|
+ messageListByCategory.value.messageList = [];
|
|
|
+ const cat = messageCategories.value.find(c => c.key === "messageList");
|
|
|
+ if (cat) cat.unread = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ messageLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getMessageList({ receiverId: "store_" + receiverId, friendType: 0 });
|
|
|
+ const data = res?.data ?? res;
|
|
|
+ const rawList = Array.isArray(data) ? data : data ? [data] : [];
|
|
|
+ const list: MessageItem[] = rawList.map((item: NoFriendMessageItem) => mapNoFriendToMessageItem(item));
|
|
|
+ messageListByCategory.value.messageList = list;
|
|
|
+ const unreadTotal = rawList.reduce(
|
|
|
+ (sum: number, item: NoFriendMessageItem) => sum + (item.notReadCount ?? (item.isRead === 0 ? 1 : 0)),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ const cat = messageCategories.value.find(c => c.key === "messageList");
|
|
|
+ if (cat) cat.unread = unreadTotal;
|
|
|
+ } catch (e) {
|
|
|
+ messageListByCategory.value.messageList = [];
|
|
|
+ const cat = messageCategories.value.find(c => c.key === "messageList");
|
|
|
+ if (cat) cat.unread = 0;
|
|
|
+ } finally {
|
|
|
+ messageLoading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 与原有通知页面一致的 receiverId
|
|
|
+function getReceiverId(): string {
|
|
|
+ return localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
|
|
|
+}
|
|
|
+
|
|
|
+// 与原有通知页面一致的 context 解析
|
|
|
+function parseContext(context: string | undefined): string {
|
|
|
+ if (!context) return "";
|
|
|
+ try {
|
|
|
+ const parsed = typeof context === "string" ? JSON.parse(context) : context;
|
|
|
+ return parsed?.message || context;
|
|
|
+ } catch {
|
|
|
+ return context;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 统一请求:/alienStorePlatform/notice/getNoticeList,noticeType 系统=1、订单提醒=2、与我相关=0,分页查询
|
|
|
+async function fetchNoticeList(catKey: string) {
|
|
|
+ const receiverId = getReceiverId();
|
|
|
+ if (!receiverId) {
|
|
|
+ listByCategory.value[catKey] = [];
|
|
|
+ paginationByCategory.value[catKey].total = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const noticeType = noticeTypeByKey[catKey] ?? 1;
|
|
|
+ const pagination = paginationByCategory.value[catKey];
|
|
|
+ loadingByCategory.value[catKey] = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getNoticeListForHeader({
|
|
|
+ pageNum: pagination.pageNum,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ receiverId: "store_" + receiverId,
|
|
|
+ noticeType
|
|
|
+ });
|
|
|
+ const data = res?.data ?? res;
|
|
|
+ const records = data?.records ?? data?.list ?? [];
|
|
|
+ const total = data?.total ?? 0;
|
|
|
+ const list: NoticeItem[] = records.map((item: any) => ({
|
|
|
+ id: String(item.id),
|
|
|
+ title: item.title ?? "",
|
|
|
+ content: parseContext(item.context ?? item.content),
|
|
|
+ date: item.createdTime ?? "",
|
|
|
+ unread: !item.isRead
|
|
|
+ }));
|
|
|
+ listByCategory.value[catKey] = list;
|
|
|
+ paginationByCategory.value[catKey].total = total;
|
|
|
+ } catch (e) {
|
|
|
+ listByCategory.value[catKey] = [];
|
|
|
+ paginationByCategory.value[catKey].total = 0;
|
|
|
+ } finally {
|
|
|
+ loadingByCategory.value[catKey] = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 按类型拉取未读数量:noticeType 0-与我相关 1-系统通知 2-订单提醒
|
|
|
+async function fetchNoticeUnreadCounts() {
|
|
|
+ const receiverId = getReceiverId();
|
|
|
+ if (!receiverId) {
|
|
|
+ noticeCategories.value.forEach(c => (c.unread = 0));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const receiverIdParam = "store_" + receiverId;
|
|
|
+ try {
|
|
|
+ const [res1, res2, res0] = await Promise.all([
|
|
|
+ getCountUnreadByType({ noticeType: 1, receiverId: receiverIdParam }),
|
|
|
+ getCountUnreadByType({ noticeType: 2, receiverId: receiverIdParam }),
|
|
|
+ getCountUnreadByType({ noticeType: 0, receiverId: receiverIdParam })
|
|
|
+ ]);
|
|
|
+ const systemCat = noticeCategories.value.find(c => c.key === "system");
|
|
|
+ const orderCat = noticeCategories.value.find(c => c.key === "order");
|
|
|
+ const relatedCat = noticeCategories.value.find(c => c.key === "related");
|
|
|
+ if (systemCat) systemCat.unread = Number((res1 as any)?.data ?? (res1 as any) ?? 0);
|
|
|
+ if (orderCat) orderCat.unread = Number((res2 as any)?.data ?? (res2 as any) ?? 0);
|
|
|
+ if (relatedCat) relatedCat.unread = Number((res0 as any)?.data ?? (res0 as any) ?? 0);
|
|
|
+ } catch (e) {
|
|
|
+ noticeCategories.value.forEach(c => (c.unread = 0));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 进入通知页面时:用 countUnreadByType 刷新三个标签未读数,并拉取当前分类列表
|
|
|
+function refreshAllNoticeCategories() {
|
|
|
+ fetchNoticeUnreadCounts();
|
|
|
+ fetchNoticeList(activeCategory.value);
|
|
|
+}
|
|
|
+
|
|
|
+function handleSizeChange(size: number) {
|
|
|
+ const key = activeCategory.value;
|
|
|
+ paginationByCategory.value[key].pageSize = size;
|
|
|
+ paginationByCategory.value[key].pageNum = 1;
|
|
|
+ fetchNoticeList(key);
|
|
|
+}
|
|
|
+
|
|
|
+function handlePageChange(page: number) {
|
|
|
+ const key = activeCategory.value;
|
|
|
+ paginationByCategory.value[key].pageNum = page;
|
|
|
+ fetchNoticeList(key);
|
|
|
+}
|
|
|
+
|
|
|
+async function handleViewDetail(item: NoticeItem) {
|
|
|
+ if (item.unread) {
|
|
|
+ try {
|
|
|
+ const res: any = await readNoticeById({ id: item.id });
|
|
|
+ const ok = res?.code === 200 || res?.code === "200";
|
|
|
+ if (ok) {
|
|
|
+ const key = activeCategory.value;
|
|
|
+ const list = listByCategory.value[key] ?? [];
|
|
|
+ const idx = list.findIndex(i => i.id === item.id);
|
|
|
+ if (idx !== -1) list[idx].unread = false;
|
|
|
+ const cat = noticeCategories.value.find(c => c.key === key);
|
|
|
+ if (cat && cat.unread > 0) cat.unread -= 1;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 仍打开详情
|
|
|
+ }
|
|
|
+ }
|
|
|
+ currentDetail.value = item;
|
|
|
+ detailVisible.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function handleDelete(item: NoticeItem, index: number) {
|
|
|
+ const key = activeCategory.value;
|
|
|
+ listByCategory.value[key] = currentList.value.filter((_, i) => i !== index);
|
|
|
+ const cat = noticeCategories.value.find(c => c.key === key);
|
|
|
+ if (cat && item.unread) cat.unread = Math.max(0, cat.unread - 1);
|
|
|
+}
|
|
|
+
|
|
|
+function handleMessageDelete(item: MessageItem, index: number) {
|
|
|
+ const key = messageCategory.value;
|
|
|
+ messageListByCategory.value[key] = currentMessageList.value.filter((_, i) => i !== index);
|
|
|
+ const cat = messageCategories.value.find(c => c.key === key);
|
|
|
+ if (cat && item.unread) cat.unread = Math.max(0, cat.unread - 1);
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ refreshAllNoticeCategories();
|
|
|
+});
|
|
|
+
|
|
|
+watch(activeCategory, val => {
|
|
|
+ const list = listByCategory.value[val] ?? [];
|
|
|
+ const loading = loadingByCategory.value[val];
|
|
|
+ if (list.length === 0 && !loading) {
|
|
|
+ paginationByCategory.value[val].pageNum = 1;
|
|
|
+ fetchNoticeList(val);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+watch(activeTab, val => {
|
|
|
+ if (val === "notice") {
|
|
|
+ refreshAllNoticeCategories();
|
|
|
+ } else if (val === "message") {
|
|
|
+ messageCategory.value = "unfollowed";
|
|
|
+ fetchNoFriendMessage();
|
|
|
+ fetchMessageList();
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.notification-drawer-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 480px;
|
|
|
+}
|
|
|
+.drawer-tabs {
|
|
|
+ display: flex;
|
|
|
+ gap: 32px;
|
|
|
+ padding: 18px 0 14px;
|
|
|
+ border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
+ .tab-item {
|
|
|
+ padding-bottom: 6px;
|
|
|
+ font-size: 16px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ cursor: pointer;
|
|
|
+ &.active {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ border-bottom: 2px solid var(--el-color-primary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.drawer-body {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ gap: 20px;
|
|
|
+ min-height: 0;
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+.drawer-side {
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ width: 160px;
|
|
|
+ padding: 12px 0;
|
|
|
+ background: var(--el-fill-color-lighter);
|
|
|
+ border-radius: 8px;
|
|
|
+ .side-item {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 14px;
|
|
|
+ margin: 0 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 6px;
|
|
|
+ &.active {
|
|
|
+ font-weight: 500;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ background: var(--el-color-primary-light-9);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .side-icon {
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+ .side-text {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ .side-badge {
|
|
|
+ :deep(.el-badge__content) {
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.drawer-main-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+.drawer-main {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ padding-right: 4px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+.pagination-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding: 12px 0 0;
|
|
|
+ margin-top: 8px;
|
|
|
+ border-top: 1px solid var(--el-border-color-lighter);
|
|
|
+}
|
|
|
+.message-card {
|
|
|
+ display: flex;
|
|
|
+ gap: 14px;
|
|
|
+ padding: 14px 0;
|
|
|
+ border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+ .message-avatar {
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .message-body {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+ .message-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+ .message-sender {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ .unread-dot {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ background: var(--el-color-danger);
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .message-date {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ }
|
|
|
+ .message-content {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+ .message-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+}
|
|
|
+.notice-card {
|
|
|
+ padding: 18px 20px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .card-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ .card-title {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ .unread-dot {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ background: var(--el-color-danger);
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .card-date {
|
|
|
+ flex-shrink: 0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ }
|
|
|
+ .card-content {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+ .card-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+}
|
|
|
+.empty-tip {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 60px 20px;
|
|
|
+ font-size: 15px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.detail-dialog-content {
|
|
|
+ padding: 10px 0;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.8;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+}
|
|
|
+</style>
|