优化站内信模块
This commit is contained in:
Vendored
+1
@@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/** 获取站内信配置 */
|
||||
export function getSiteReminderConfig() {
|
||||
return request({
|
||||
url: "/platform/sitereminder/config",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/** 保存站内信配置 */
|
||||
export function saveSiteReminderConfig(data) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/config",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 发送站内信 */
|
||||
export function sendSiteReminder(data) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/send",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取我的消息列表 */
|
||||
export function getMySiteReminders(params) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/myList",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 标记消息为已读 */
|
||||
export function readSiteReminder(id) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/read",
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 一键全部已读 */
|
||||
export function readAllSiteReminders() {
|
||||
return request({
|
||||
url: "/platform/sitereminder/readall",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除消息 */
|
||||
export function deleteSiteReminder(id) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/delete",
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取已发送的站内信列表(按 batch_id 分组) */
|
||||
export function getSentSiteReminders(params) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/sentList",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑/更新已发送的站内信 */
|
||||
export function updateSentSiteReminder(data) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/updateSent",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除已发送的站内信批次 */
|
||||
export function deleteSentSiteReminderBatch(batch_id) {
|
||||
return request({
|
||||
url: "/platform/sitereminder/deleteSent",
|
||||
method: "post",
|
||||
data: { batch_id },
|
||||
});
|
||||
}
|
||||
@@ -24,18 +24,48 @@
|
||||
:title="currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'" />
|
||||
|
||||
<!-- 消息中心 -->
|
||||
<el-dropdown trigger="click">
|
||||
<el-dropdown trigger="click" @visible-change="handleDropdownVisibleChange">
|
||||
<span class="el-dropdown-link" style="cursor: pointer;">
|
||||
<el-button circle class="message-btn" title="消息中心">
|
||||
<el-icon>
|
||||
<Bell />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-badge :value="unreadCount" :max="99" :hidden="unreadCount === 0" class="message-badge">
|
||||
<el-button circle class="message-btn" title="消息中心">
|
||||
<el-icon>
|
||||
<Bell />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-badge>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="message-menu" style="width: 260px;">
|
||||
<el-dropdown-item disabled>暂无新消息</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
<div class="message-dropdown-container">
|
||||
<div class="message-dropdown-header">
|
||||
<span class="title">站内通知 ({{ unreadCount }}条未读)</span>
|
||||
<el-link type="primary" :underline="false" class="mark-all-btn" v-if="unreadCount > 0" @click="handleMarkAllRead">全部已读</el-link>
|
||||
</div>
|
||||
<el-scrollbar max-height="300px">
|
||||
<div v-if="loadingMessages" class="loading-state">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="messages.length === 0" class="empty-state">
|
||||
<el-icon><Message /></el-icon>
|
||||
<span>暂无新消息</span>
|
||||
</div>
|
||||
<div v-else class="message-list">
|
||||
<div
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
class="message-item"
|
||||
:class="{ unread: item.is_read === 0 }"
|
||||
@click="handleMessageClick(item)"
|
||||
>
|
||||
<div class="message-item-header">
|
||||
<span class="message-title">{{ item.title }}</span>
|
||||
<span class="message-time">{{ formatTime(item.create_time) }}</span>
|
||||
</div>
|
||||
<div class="message-item-brief">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
@@ -66,6 +96,12 @@
|
||||
</div>
|
||||
|
||||
<div class="message-center">
|
||||
<!-- 详情对话框 -->
|
||||
<MessageDetailDialog
|
||||
v-model="detailVisible"
|
||||
:reminder="currentReminder"
|
||||
@read-success="handleReadSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -77,8 +113,10 @@ const emit = defineEmits(['collapse']);
|
||||
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { logout, getCurrentUser } from "@/api/login";
|
||||
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand } from '@element-plus/icons-vue';
|
||||
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand, Loading, Message } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
|
||||
import MessageDetailDialog from "@/views/basicSettings/sitereminder/components/detail.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -133,6 +171,87 @@ async function refreshCache() {
|
||||
}
|
||||
}
|
||||
|
||||
// 站内信消息中心逻辑
|
||||
const unreadCount = ref(0);
|
||||
const messages = ref<any[]>([]);
|
||||
const loadingMessages = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const currentReminder = ref<any>({});
|
||||
|
||||
const formatTime = (timeStr: any) => {
|
||||
if (!timeStr) return "-";
|
||||
const date = new Date(timeStr);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const fetchUnreadCount = async () => {
|
||||
if (!authStore.token) return;
|
||||
try {
|
||||
const res = await getMySiteReminders({ page: 1, pageSize: 1, isRead: 0 });
|
||||
if (res.code === 200) {
|
||||
unreadCount.value = res.data.total || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("fetchUnreadCount failed", err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!authStore.token) return;
|
||||
loadingMessages.value = true;
|
||||
try {
|
||||
const res = await getMySiteReminders({ page: 1, pageSize: 5 });
|
||||
if (res.code === 200) {
|
||||
messages.value = res.data.list || [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("fetchMessages failed", err);
|
||||
} finally {
|
||||
loadingMessages.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdownVisibleChange = (visible: boolean) => {
|
||||
if (visible) {
|
||||
fetchMessages();
|
||||
fetchUnreadCount();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMessageClick = (item: any) => {
|
||||
currentReminder.value = item;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
const handleReadSuccess = (id: number) => {
|
||||
const msg = messages.value.find(m => m.id === id);
|
||||
if (msg && msg.is_read === 0) {
|
||||
msg.is_read = 1;
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||
}
|
||||
fetchUnreadCount();
|
||||
};
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
const res = await readAllSiteReminders();
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("全部已读标记成功");
|
||||
unreadCount.value = 0;
|
||||
messages.value.forEach(m => m.is_read = 1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
let timer: any = null;
|
||||
|
||||
const handleMessagesChanged = () => {
|
||||
fetchUnreadCount();
|
||||
fetchMessages();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMenu();
|
||||
if (!authStore.token) return;
|
||||
@@ -144,6 +263,10 @@ onMounted(async () => {
|
||||
} catch (e) {
|
||||
console.error("getCurrentUser failed", e);
|
||||
}
|
||||
|
||||
fetchUnreadCount();
|
||||
timer = setInterval(fetchUnreadCount, 60000);
|
||||
window.addEventListener('site-messages-changed', handleMessagesChanged);
|
||||
});
|
||||
|
||||
// 根据菜单列表和当前路径计算出的面包屑导航
|
||||
@@ -346,6 +469,10 @@ onUnmounted(() => {
|
||||
if (mediaQuery && handleChange) {
|
||||
mediaQuery.removeEventListener('change', handleChange);
|
||||
}
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
window.removeEventListener('site-messages-changed', handleMessagesChanged);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -565,4 +692,145 @@ onUnmounted(() => {
|
||||
:deep(.el-button) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 消息中心下拉列表样式 */
|
||||
.message-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message-dropdown-container {
|
||||
width: 320px;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
background-color: var(--el-fill-color-blank);
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.mark-all-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
text-align: left; /* Ensure it is left-aligned */
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 4px;
|
||||
gap: 8px;
|
||||
|
||||
.message-title {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.message-item-brief {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-dropdown-footer {
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
text-align: center;
|
||||
background-color: var(--el-fill-color-blank);
|
||||
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="消息详情"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
align-center
|
||||
class="reminder-detail-dialog"
|
||||
>
|
||||
<div class="reminder-detail-container" v-loading="loading">
|
||||
<h3 class="reminder-title">{{ reminder.title }}</h3>
|
||||
<div class="reminder-meta">
|
||||
<span class="meta-item">
|
||||
<el-icon><User /></el-icon>
|
||||
发送者:{{ getSenderName(reminder) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
时间:{{ formatTime(reminder.create_time) }}
|
||||
</span>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="reminder-content">{{ reminder.content }}</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { User, Calendar } from "@element-plus/icons-vue";
|
||||
import { readSiteReminder } from "@/api/sitereminder";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
reminder: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
viewOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "read-success"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val)
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const getSenderName = (item: any) => {
|
||||
if (!item.sender_type) return "-";
|
||||
if (item.sender_type === "system") return "系统";
|
||||
if (item.sender_type === "platform") return `平台管理员 (ID: ${item.sender_id})`;
|
||||
if (item.sender_type === "tenant") return `租户用户 (ID: ${item.sender_id})`;
|
||||
return item.sender_type;
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: any) => {
|
||||
if (!timeStr) return "-";
|
||||
const date = new Date(timeStr);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, async (val) => {
|
||||
if (val && props.reminder && !props.viewOnly && props.reminder.is_read === 0) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await readSiteReminder(props.reminder.id);
|
||||
if (res.code === 200) {
|
||||
emit("read-success", props.reminder.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reminder-detail-container {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.reminder-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.reminder-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.reminder-content {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑已发站内信' : '发布站内信'"
|
||||
width="650px"
|
||||
destroy-on-close
|
||||
align-center
|
||||
class="reminder-send-dialog"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
label-position="right"
|
||||
v-loading="submitting"
|
||||
>
|
||||
<el-form-item label="消息标题" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入消息标题"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="接收目标" prop="target_type">
|
||||
<el-radio-group v-model="form.target_type" @change="handleTargetTypeChange">
|
||||
<el-radio-button label="platform">平台端</el-radio-button>
|
||||
<el-radio-button label="tenant_all">管理端 (所有租户)</el-radio-button>
|
||||
<el-radio-button label="role">平台角色</el-radio-button>
|
||||
<el-radio-button label="tenant">特定租户</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="form.target_type === 'role'"
|
||||
label="选择角色"
|
||||
prop="target_role_id"
|
||||
:rules="[{ required: true, message: '请选择接收角色', trigger: 'change' }]"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.target_role_id"
|
||||
placeholder="请选择平台角色"
|
||||
v-loading="loadingRoles"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in rolesList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="form.target_type === 'tenant'"
|
||||
label="选择租户"
|
||||
prop="target_tenant_id"
|
||||
:rules="[{ required: true, message: '请选择接收租户', trigger: 'change' }]"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.target_tenant_id"
|
||||
placeholder="请输入租户名称搜索"
|
||||
filterable
|
||||
v-loading="loadingTenants"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tenantsList"
|
||||
:key="item.id"
|
||||
:label="`${item.name} (${item.tenant_code})`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="消息内容" prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入详细的消息内容..."
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSend">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
{{ isEdit ? '保存修改' : '发送' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { Promotion } from "@element-plus/icons-vue";
|
||||
import { sendSiteReminder, updateSentSiteReminder } from "@/api/sitereminder";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
import { getTenantList } from "@/api/tenant";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
reminder: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val)
|
||||
});
|
||||
|
||||
const isEdit = computed(() => !!props.reminder);
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
|
||||
const form = ref({
|
||||
title: "",
|
||||
content: "",
|
||||
target_type: "platform",
|
||||
target_role_id: null as number | null,
|
||||
target_tenant_id: null as number | null
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
title: [{ required: true, message: "请输入消息标题", trigger: "blur" }],
|
||||
content: [{ required: true, message: "请输入消息内容", trigger: "blur" }],
|
||||
target_type: [{ required: true, message: "请选择接收目标", trigger: "change" }]
|
||||
};
|
||||
|
||||
const rolesList = ref<any[]>([]);
|
||||
const loadingRoles = ref(false);
|
||||
const tenantsList = ref<any[]>([]);
|
||||
const loadingTenants = ref(false);
|
||||
|
||||
const handleTargetTypeChange = (val: any) => {
|
||||
form.value.target_role_id = null;
|
||||
form.value.target_tenant_id = null;
|
||||
if (val === "role") {
|
||||
loadRoles();
|
||||
} else if (val === "tenant") {
|
||||
loadTenants();
|
||||
}
|
||||
};
|
||||
|
||||
const loadRoles = async () => {
|
||||
if (rolesList.value.length > 0) return;
|
||||
loadingRoles.value = true;
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
if (res.code === 200) {
|
||||
rolesList.value = (res.data || []).filter((r: any) => Number(r.cid) === 1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
loadingRoles.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadTenants = async () => {
|
||||
if (tenantsList.value.length > 0) return;
|
||||
loadingTenants.value = true;
|
||||
try {
|
||||
const res = await getTenantList({ page: 1, pageSize: 1000 });
|
||||
if (res.code === 200) {
|
||||
tenantsList.value = res.data?.list || res.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
loadingTenants.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
const res = await updateSentSiteReminder({
|
||||
batch_id: props.reminder.batch_id,
|
||||
title: form.value.title,
|
||||
content: form.value.content,
|
||||
target_type: form.value.target_type,
|
||||
target_role_id: form.value.target_role_id || 0,
|
||||
target_tenant_id: form.value.target_tenant_id || 0
|
||||
});
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("修改成功");
|
||||
emit("success");
|
||||
visible.value = false;
|
||||
} else {
|
||||
ElMessage.error(res.msg || "修改失败");
|
||||
}
|
||||
} else {
|
||||
const payload = {
|
||||
title: form.value.title,
|
||||
content: form.value.content,
|
||||
target_type: form.value.target_type,
|
||||
target_role_id: form.value.target_role_id || 0,
|
||||
target_tenant_id: form.value.target_tenant_id || 0
|
||||
};
|
||||
const res = await sendSiteReminder(payload);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("站内信发布成功");
|
||||
emit("success");
|
||||
visible.value = false;
|
||||
} else {
|
||||
ElMessage.error(res.msg || "发布失败");
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "操作失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val) {
|
||||
if (props.reminder) {
|
||||
form.value = {
|
||||
title: props.reminder.title,
|
||||
content: props.reminder.content,
|
||||
target_type: props.reminder.target_type || "platform",
|
||||
target_role_id: props.reminder.target_role_id || null,
|
||||
target_tenant_id: props.reminder.target_tenant_id || null
|
||||
};
|
||||
if (form.value.target_type === "role") {
|
||||
loadRoles();
|
||||
} else if (form.value.target_type === "tenant") {
|
||||
loadTenants();
|
||||
}
|
||||
} else {
|
||||
form.value = {
|
||||
title: "",
|
||||
content: "",
|
||||
target_type: "platform",
|
||||
target_role_id: null,
|
||||
target_tenant_id: null
|
||||
};
|
||||
rolesList.value = [];
|
||||
tenantsList.value = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<div class="header-title-wrapper">
|
||||
<h2>站内信发送历史</h2>
|
||||
<span class="header-subtitle">发布、编辑以及管理已发送的站内信</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleCompose">
|
||||
<el-icon><Plus /></el-icon>
|
||||
发布站内信
|
||||
</el-button>
|
||||
<el-button @click="fetchList">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新列表
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
|
||||
<!-- 列表 Table -->
|
||||
<el-table
|
||||
:data="reminderList"
|
||||
stripe
|
||||
v-loading="loading"
|
||||
style="width: 100%; margin-top: 15px;"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="handleView(row)" style="font-weight: 500;">
|
||||
{{ row.title }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收目标" width="220" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTargetTagType(row.target_type)" size="small">
|
||||
{{ getTargetLabel(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 发布/编辑对话框 -->
|
||||
<MessageComposeDialog
|
||||
v-model="composeVisible"
|
||||
:reminder="editReminder"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<MessageDetailDialog
|
||||
v-model="detailVisible"
|
||||
:reminder="currentReminder"
|
||||
:view-only="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
Refresh,
|
||||
Edit,
|
||||
Delete,
|
||||
RefreshLeft
|
||||
} from "@element-plus/icons-vue";
|
||||
import {
|
||||
getSentSiteReminders,
|
||||
deleteSentSiteReminderBatch
|
||||
} from "@/api/sitereminder";
|
||||
import MessageComposeDialog from "./components/edit.vue";
|
||||
import MessageDetailDialog from "./components/detail.vue";
|
||||
|
||||
const loading = ref(false);
|
||||
const reminderList = ref<any[]>([]);
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 发布与详情弹窗状态
|
||||
const composeVisible = ref(false);
|
||||
const editReminder = ref<any>(null);
|
||||
const detailVisible = ref(false);
|
||||
const currentReminder = ref<any>({});
|
||||
|
||||
// 接收目标标签类型
|
||||
const getTargetTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case "platform":
|
||||
return "primary";
|
||||
case "tenant_all":
|
||||
return "success";
|
||||
case "role":
|
||||
return "warning";
|
||||
case "tenant":
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
// 接收目标显示文字
|
||||
const getTargetLabel = (row: any) => {
|
||||
switch (row.target_type) {
|
||||
case "platform":
|
||||
return "平台端";
|
||||
case "tenant_all":
|
||||
return "管理端 (所有租户)";
|
||||
case "role":
|
||||
return `平台角色 (ID: ${row.target_role_id})`;
|
||||
case "tenant":
|
||||
return `特定租户 (ID: ${row.target_tenant_id})`;
|
||||
default:
|
||||
return row.target_type || "未知";
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: any) => {
|
||||
if (!timeStr) return "-";
|
||||
const date = new Date(timeStr);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
// 获取已发送列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
};
|
||||
const res = await getSentSiteReminders(params);
|
||||
if (res.code === 200) {
|
||||
reminderList.value = res.data.list || [];
|
||||
pagination.total = res.data.total || 0;
|
||||
} else {
|
||||
ElMessage.error(res.msg || "获取历史列表失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "获取历史列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row: any) => {
|
||||
currentReminder.value = row;
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑已发送
|
||||
const handleEdit = (row: any) => {
|
||||
editReminder.value = row;
|
||||
composeVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除已发送消息批次
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
"确定删除该发布批次的站内信吗?删除后接收者将不再看到该消息且无法恢复。",
|
||||
"提示",
|
||||
{ type: "warning" }
|
||||
);
|
||||
const res = await deleteSentSiteReminderBatch(row.batch_id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
window.dispatchEvent(new CustomEvent("site-messages-changed"));
|
||||
} else {
|
||||
ElMessage.error(res.msg || "删除失败");
|
||||
}
|
||||
} catch {
|
||||
// ignore cancel
|
||||
}
|
||||
};
|
||||
|
||||
// 分页大小变动
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.pageSize = val;
|
||||
pagination.page = 1;
|
||||
fetchList();
|
||||
};
|
||||
|
||||
// 页码变动
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.page = val;
|
||||
fetchList();
|
||||
};
|
||||
|
||||
// 打开写信弹框
|
||||
const handleCompose = () => {
|
||||
editReminder.value = null;
|
||||
composeVisible.value = true;
|
||||
};
|
||||
|
||||
// 成功回调 (发送或编辑成功)
|
||||
const handleSuccess = () => {
|
||||
fetchList();
|
||||
window.dispatchEvent(new CustomEvent("site-messages-changed"));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title-wrapper h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -75,6 +75,16 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态" prop="status">
|
||||
<el-switch
|
||||
v-model="emailForm.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="测试收件邮箱">
|
||||
<el-input
|
||||
v-model="testEmail"
|
||||
@@ -115,7 +125,8 @@ const emailForm = reactive({
|
||||
port: "",
|
||||
password: "",
|
||||
encryption: "ssl",
|
||||
timeout: 30
|
||||
timeout: 30,
|
||||
status: 1
|
||||
});
|
||||
|
||||
const validatePassword = (
|
||||
@@ -154,6 +165,7 @@ const loadEmailConfig = async () => {
|
||||
emailForm.password = item.password || "";
|
||||
emailForm.encryption = item.encryption || "ssl";
|
||||
emailForm.timeout = item.timeout != null ? item.timeout : 30;
|
||||
emailForm.status = item.status != null ? item.status : 1;
|
||||
} else {
|
||||
hasSavedConfig.value = false;
|
||||
}
|
||||
@@ -211,6 +223,7 @@ const handleReset = () => {
|
||||
emailForm.password = "";
|
||||
emailForm.encryption = "ssl";
|
||||
emailForm.timeout = 30;
|
||||
emailForm.status = 1;
|
||||
testEmail.value = "";
|
||||
emailFormRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
@@ -156,7 +156,8 @@
|
||||
</template>
|
||||
<el-form :model="formData.sitemsg" label-width="140px" label-position="right">
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="formData.sitemsg.enabled" />
|
||||
<el-switch v-model="formData.sitemsg.enabled" disabled />
|
||||
<span class="form-tip-inline" style="margin-left: 10px;">站内信功能为系统核心功能,必须开启</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="保留天数">
|
||||
<el-input-number v-model="formData.sitemsg.retention_days" :min="1" :max="365" controls-position="right" />
|
||||
@@ -393,6 +394,7 @@ import {
|
||||
} from "@element-plus/icons-vue";
|
||||
import { getSmsInfo, editSmsInfo, sendTestSms } from "@/api/sms";
|
||||
import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
|
||||
import { getSiteReminderConfig, saveSiteReminderConfig } from "@/api/sitereminder";
|
||||
|
||||
const STORAGE_KEY = "notification_settings_draft";
|
||||
const activeSubTab = ref("email");
|
||||
@@ -422,7 +424,7 @@ const formData = reactive({
|
||||
testPhone: "",
|
||||
},
|
||||
sitemsg: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
retention_days: 30,
|
||||
auto_read: false,
|
||||
},
|
||||
@@ -491,7 +493,7 @@ const loadEmailConfig = async () => {
|
||||
formData.email.password = item.password || "";
|
||||
formData.email.encryption = item.encryption || "ssl";
|
||||
formData.email.timeout = item.timeout != null ? item.timeout : 30;
|
||||
formData.email.enabled = Number(item.enabled ?? 0) === 1;
|
||||
formData.email.enabled = Number(item.status ?? item.enabled ?? 0) === 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载邮箱配置失败:", error);
|
||||
@@ -511,6 +513,19 @@ const loadSmsConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadSiteReminderConfig = async () => {
|
||||
try {
|
||||
const res = await getSiteReminderConfig();
|
||||
if (res.code === 200 && res.data) {
|
||||
formData.sitemsg.retention_days = res.data.retention_days ?? 30;
|
||||
formData.sitemsg.auto_read = res.data.auto_read === 1;
|
||||
formData.sitemsg.enabled = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载站内信配置失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveDraft = () => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(formData));
|
||||
};
|
||||
@@ -519,7 +534,7 @@ const handleReset = () => {
|
||||
const defaults = {
|
||||
email: { enabled: false, fromAddress: "", fromName: "", host: "", port: "", password: "", encryption: "ssl", timeout: 30, testEmail: "" },
|
||||
sms: { enabled: false, provider: "aliyun", access_key: "", secret_key: "", sign_name: "", template_code: "", backendUrl: "", apiKey: "", testPhone: "" },
|
||||
sitemsg: { enabled: false, retention_days: 30, auto_read: false },
|
||||
sitemsg: { enabled: true, retention_days: 30, auto_read: false },
|
||||
dingtalk: { enabled: false, webhook_url: "", secret: "" },
|
||||
webhook: { enabled: false, url: "", method: "POST", token: "" },
|
||||
feishu: { enabled: false, webhook_url: "", secret: "" },
|
||||
@@ -612,7 +627,7 @@ const handleSubmit = async () => {
|
||||
password: formData.email.password,
|
||||
encryption: formData.email.encryption,
|
||||
timeout: formData.email.timeout,
|
||||
enabled: formData.email.enabled ? 1 : 0
|
||||
status: formData.email.enabled ? 1 : 0
|
||||
});
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("邮箱配置保存成功");
|
||||
@@ -634,6 +649,16 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
throw new Error(res.msg || "保存自定义短信配置失败");
|
||||
}
|
||||
} else if (activeSubTab.value === 'sitemsg') {
|
||||
const res = await saveSiteReminderConfig({
|
||||
retention_days: formData.sitemsg.retention_days,
|
||||
auto_read: formData.sitemsg.auto_read ? 1 : 0
|
||||
});
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("站内信配置保存成功");
|
||||
} else {
|
||||
throw new Error(res.msg || "保存站内信配置失败");
|
||||
}
|
||||
}
|
||||
|
||||
saveDraft();
|
||||
@@ -649,6 +674,7 @@ onMounted(() => {
|
||||
loadDraft();
|
||||
loadSmsConfig();
|
||||
loadEmailConfig();
|
||||
loadSiteReminderConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user