优化站内信模块

This commit is contained in:
2026-06-17 23:07:39 +08:00
parent d3886e2475
commit 22e0e75c35
25 changed files with 2538 additions and 39 deletions
+89
View File
@@ -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 },
});
}
+278 -10
View File
@@ -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>
+14 -1
View File
@@ -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>