mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 01:18:54 +08:00
feat(seller): 系统消息页改为顶部 tab 筛选与卡片式列表
- 左侧菜单改为顶部 pill tab(未读带数量角标) - 表格改为卡片列表:类型图标 + 标题加粗 + 内容摘要 + 时间操作 - 点击消息就地展开内容,去掉返回按钮 - 修复标为已读后错误刷新全量列表的问题
This commit is contained in:
@@ -1,92 +1,195 @@
|
||||
.message-main-con {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.message-page {
|
||||
height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.message-mainlist-con {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
padding: 12px 8px;
|
||||
.message-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mes-type-item {
|
||||
.message-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.message-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 40px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
padding: 7px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: #f5f7fa;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: #fff;
|
||||
background: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
|
||||
.message-tab-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.message-tab-badge {
|
||||
background: #fff;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.message-tab-badge {
|
||||
min-width: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--el-color-danger);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-content-con {
|
||||
.message-list-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 4px 16px 12px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 4px 16px 12px;
|
||||
}
|
||||
|
||||
.page-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
.message-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 8px;
|
||||
border-bottom: 1px solid #f2f4f8;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.message-content-top-bar {
|
||||
&.expanded {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.message-card-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
margin: 0 -16px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
|
||||
.mes-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
&.unread {
|
||||
background: var(--el-color-primary-light-8);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.read {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
&.recycleBin {
|
||||
background: var(--el-color-danger-light-8);
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.message-card-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.message-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.unread {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.message-card-summary {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #a8abb2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.mes-time-con {
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
.message-card-side {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.message-card-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.message-card-ops {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-card-content {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
|
||||
.message-card-content-inner {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.message-content-body {
|
||||
padding: 0 8px;
|
||||
line-height: 1.8;
|
||||
color: #606266;
|
||||
word-break: break-word;
|
||||
|
||||
:deep(p) {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mes-time-icon {
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
.page-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
@@ -1,58 +1,67 @@
|
||||
<template>
|
||||
<div class="message-main-con">
|
||||
<div class="message-mainlist-con">
|
||||
<div
|
||||
v-for="item in mesTypeList"
|
||||
:key="item.type"
|
||||
class="mes-type-item"
|
||||
:class="{ active: currentMessageType === item.type }"
|
||||
@click="setCurrentMesType(item.type)"
|
||||
>
|
||||
<span class="mes-type-btn-text">{{ item.title }}</span>
|
||||
<el-badge
|
||||
v-if="item.type === 'unread' && unReadCount > 0"
|
||||
:value="unReadCount"
|
||||
class="message-count-badge-outer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-content-con">
|
||||
<div v-if="showMesTitleList" class="message-title-list-con">
|
||||
<el-table
|
||||
ref="messageList"
|
||||
v-loading="loading"
|
||||
:data="currentMesList"
|
||||
:empty-text="noDataText"
|
||||
:show-header="false"
|
||||
style="width: 100%"
|
||||
<div class="message-page">
|
||||
<div class="message-panel">
|
||||
<div class="message-tabs">
|
||||
<div
|
||||
v-for="item in mesTypeList"
|
||||
:key="item.type"
|
||||
class="message-tab"
|
||||
:class="{ active: currentMessageType === item.type }"
|
||||
@click="setCurrentMesType(item.type)"
|
||||
>
|
||||
<el-table-column min-width="300">
|
||||
<template #default="{ row }">
|
||||
<a class="link-text mes-title-link" @click="openMessage(row)">{{ row.title }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon class="mes-time-icon"><Clock /></el-icon>
|
||||
<span>{{ row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="currentMessageType == 'unread'">
|
||||
<a class="link-text" @click="markAsRead(row)">标为已读</a>
|
||||
</template>
|
||||
<template v-else-if="currentMessageType == 'read'">
|
||||
<a class="link-text" @click="deleteMes(row)">删除</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="link-text" @click="restoreMes(row)">还原</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="deleteReal(row)">彻底删除</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-icon class="message-tab-icon"><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.title }}</span>
|
||||
<span
|
||||
v-if="item.type === 'unread' && unReadCount > 0"
|
||||
class="message-tab-badge"
|
||||
>{{ unReadCount > 99 ? "99+" : unReadCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="message-list-wrap">
|
||||
<template v-if="currentMesList.length">
|
||||
<div
|
||||
v-for="row in currentMesList"
|
||||
:key="row.id"
|
||||
class="message-card"
|
||||
:class="{ expanded: expandedId === row.id }"
|
||||
@click="toggleMessage(row)"
|
||||
>
|
||||
<div class="message-card-icon" :class="currentMessageType">
|
||||
<el-icon :size="18"><component :is="currentTypeIcon" /></el-icon>
|
||||
</div>
|
||||
<div class="message-card-main">
|
||||
<div
|
||||
class="message-card-title"
|
||||
:class="{ unread: currentMessageType === 'unread' }"
|
||||
>{{ row.title }}</div>
|
||||
<div class="message-card-summary">{{ contentText(row.content) }}</div>
|
||||
</div>
|
||||
<div class="message-card-side">
|
||||
<span class="message-card-time">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>{{ row.createTime }}</span>
|
||||
</span>
|
||||
<span class="message-card-ops" @click.stop>
|
||||
<template v-if="currentMessageType === 'unread'">
|
||||
<a class="link-text" @click="markAsRead(row)">标为已读</a>
|
||||
</template>
|
||||
<template v-else-if="currentMessageType === 'read'">
|
||||
<a class="link-text danger" @click="deleteMes(row)">删除</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="link-text" @click="restoreMes(row)">还原</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text danger" @click="deleteReal(row)">彻底删除</a>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="expandedId === row.id" class="message-card-content" @click.stop>
|
||||
<div class="message-card-content-inner" v-html="$filters.xssFilter(row.content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" :description="noDataText" :image-size="110" />
|
||||
<div class="page-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
@@ -66,28 +75,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="message-view-content-con">
|
||||
<div class="message-content-top-bar">
|
||||
<el-button type="primary" link @click="backMesTitleList">
|
||||
<el-icon><ArrowLeft /></el-icon>返回
|
||||
</el-button>
|
||||
<h3 class="mes-title">{{ mes.title }}</h3>
|
||||
</div>
|
||||
<p class="mes-time-con">
|
||||
<el-icon class="mes-time-icon"><Clock /></el-icon>{{ mes.time }}
|
||||
</p>
|
||||
<div class="message-content-body">
|
||||
<p class="message-content" v-html="$filters.xssFilter(mes.content)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { Clock, ArrowLeft } from "@element-plus/icons-vue";
|
||||
import { Bell, ChatDotRound, Clock, Delete } from "@element-plus/icons-vue";
|
||||
import * as API_Index from "@/api/index";
|
||||
|
||||
defineOptions({ name: "message_index" });
|
||||
@@ -96,9 +91,9 @@ const route = useRoute();
|
||||
|
||||
const loading = ref(true);
|
||||
const mesTypeList = ref([
|
||||
{ type: "unread", title: "未读消息" },
|
||||
{ type: "read", title: "已读消息" },
|
||||
{ type: "recycleBin", title: "回收站" },
|
||||
{ type: "unread", title: "未读消息", icon: Bell },
|
||||
{ type: "read", title: "已读消息", icon: ChatDotRound },
|
||||
{ type: "recycleBin", title: "回收站", icon: Delete },
|
||||
]);
|
||||
const params = reactive({
|
||||
status: "UN_READY",
|
||||
@@ -110,17 +105,13 @@ const params = reactive({
|
||||
const total = ref(0);
|
||||
const currentMesList = ref([]);
|
||||
const currentMessageType = ref("unread");
|
||||
const showMesTitleList = ref(true);
|
||||
const currentTypeIcon = computed(() => {
|
||||
const item = mesTypeList.value.find((i) => i.type === currentMessageType.value);
|
||||
return item ? item.icon : Bell;
|
||||
});
|
||||
const expandedId = ref("");
|
||||
const unReadCount = ref(0);
|
||||
const noDataText = ref("暂无未读消息");
|
||||
const mes = reactive({
|
||||
title: "",
|
||||
time: "",
|
||||
content: "",
|
||||
});
|
||||
|
||||
// messageList:模板 ref 对应声明(原 Options API 中未在 script 内使用,仅保留绑定)
|
||||
const messageList = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
getAll();
|
||||
@@ -130,6 +121,14 @@ watch(() => route, (to) => {
|
||||
if (to.name == "message_index") getAll();
|
||||
});
|
||||
|
||||
function contentText(html) {
|
||||
if (!html) return "";
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
const text = (div.textContent || "").replace(/\s+/g, " ").trim();
|
||||
return text.length > 80 ? text.slice(0, 80) + "…" : text;
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
refreshMessage();
|
||||
}
|
||||
@@ -152,6 +151,11 @@ function refreshMessage() {
|
||||
}
|
||||
});
|
||||
}
|
||||
function refreshUnreadCount() {
|
||||
API_Index.getAllMessage(params).then((res) => {
|
||||
if (res.success) unReadCount.value = res.result.UN_READY.total;
|
||||
});
|
||||
}
|
||||
function getAll() {
|
||||
API_Index.getAllMessage(params).then((res) => {
|
||||
loading.value = false;
|
||||
@@ -161,12 +165,9 @@ function getAll() {
|
||||
}
|
||||
});
|
||||
}
|
||||
function backMesTitleList() {
|
||||
showMesTitleList.value = true;
|
||||
}
|
||||
function setCurrentMesType(type) {
|
||||
if (currentMessageType.value !== type) {
|
||||
showMesTitleList.value = true;
|
||||
expandedId.value = "";
|
||||
}
|
||||
currentMessageType.value = type;
|
||||
if (type == "unread") noDataText.value = "暂无未读消息";
|
||||
@@ -175,17 +176,29 @@ function setCurrentMesType(type) {
|
||||
params.pageNumber = 1;
|
||||
refreshMessage();
|
||||
}
|
||||
function openMessage(row) {
|
||||
showMesTitleList.value = false;
|
||||
mes.title = row.title;
|
||||
mes.time = row.createTime;
|
||||
getContent(row);
|
||||
function toggleMessage(row) {
|
||||
if (expandedId.value === row.id) {
|
||||
expandedId.value = "";
|
||||
return;
|
||||
}
|
||||
expandedId.value = row.id;
|
||||
if (currentMessageType.value === "unread") {
|
||||
API_Index.read(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
refreshMessage();
|
||||
refreshUnreadCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function markAsRead(v) {
|
||||
loading.value = true;
|
||||
API_Index.read(v.id).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) getAll();
|
||||
if (res.success) {
|
||||
refreshMessage();
|
||||
refreshUnreadCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteMes(v) {
|
||||
@@ -203,18 +216,11 @@ function restoreMes(v) {
|
||||
}
|
||||
function deleteReal(v) {
|
||||
loading.value = true;
|
||||
API_Index.clearMessage(v.id).then((res) => {
|
||||
API_Index.clearMessage(v.id).then(res => {
|
||||
loading.value = false;
|
||||
if (res.success) refreshMessage();
|
||||
});
|
||||
}
|
||||
function getContent(v) {
|
||||
mes.content = v.content;
|
||||
API_Index.read(v.id).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) getAll();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -223,12 +229,17 @@ function getContent(v) {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&.danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
}
|
||||
.mes-title-link {
|
||||
margin-right: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user