优化站内信模块
This commit is contained in:
parent
d3886e2475
commit
22e0e75c35
2
backend/components.d.ts
vendored
2
backend/components.d.ts
vendored
@ -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']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
@ -66,6 +67,7 @@ declare module 'vue' {
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
MessageDetailDialog: typeof import('./src/components/MessageDetailDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
||||
27
backend/src/api/sitereminder.js
Normal file
27
backend/src/api/sitereminder.js
Normal file
@ -0,0 +1,27 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/** 获取我的消息列表 */
|
||||
export function getMySiteReminders(params) {
|
||||
return request({
|
||||
url: "/backend/sitereminder/myList",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 标记消息为已读 */
|
||||
export function readSiteReminder(id) {
|
||||
return request({
|
||||
url: "/backend/sitereminder/read",
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 一键全部已读 */
|
||||
export function readAllSiteReminders() {
|
||||
return request({
|
||||
url: "/backend/sitereminder/readall",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
@ -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-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>
|
||||
|
||||
@ -65,6 +95,12 @@
|
||||
</div>
|
||||
|
||||
<div class="message-center">
|
||||
<!-- 详情对话框 -->
|
||||
<MessageDetailDialog
|
||||
v-model="detailVisible"
|
||||
:reminder="currentReminder"
|
||||
@read-success="handleReadSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -74,8 +110,10 @@ import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { logout } from "@/api/login";
|
||||
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
|
||||
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Loading, Message } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
|
||||
import MessageDetailDialog from "./MessageDetailDialog.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@ -130,7 +168,86 @@ async function refreshCache() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadMenu);
|
||||
// 站内信消息中心逻辑
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMessagesChanged = () => {
|
||||
fetchUnreadCount();
|
||||
fetchMessages();
|
||||
};
|
||||
|
||||
let timer: any = null;
|
||||
|
||||
// 根据菜单列表和当前路径计算出的面包屑导航
|
||||
const breadcrumbs = computed(() => {
|
||||
@ -300,7 +417,8 @@ const themeIcon = computed(() => isDark.value ? Sunny : Moon);
|
||||
let mediaQuery: MediaQueryList | null = null;
|
||||
let handleChange: ((e: MediaQueryListEvent) => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadMenu();
|
||||
initTheme();
|
||||
|
||||
// 监听系统主题变化
|
||||
@ -313,6 +431,12 @@ onMounted(() => {
|
||||
}
|
||||
};
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
|
||||
if (authStore.token) {
|
||||
fetchUnreadCount();
|
||||
timer = setInterval(fetchUnreadCount, 60000);
|
||||
window.addEventListener('site-messages-changed', handleMessagesChanged);
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
@ -320,6 +444,10 @@ onUnmounted(() => {
|
||||
if (mediaQuery && handleChange) {
|
||||
mediaQuery.removeEventListener('change', handleChange);
|
||||
}
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
window.removeEventListener('site-messages-changed', handleMessagesChanged);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -487,4 +615,133 @@ 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;
|
||||
}
|
||||
</style>
|
||||
|
||||
118
backend/src/components/MessageDetailDialog.vue
Normal file
118
backend/src/components/MessageDetailDialog.vue
Normal file
@ -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>
|
||||
@ -1,10 +1,3 @@
|
||||
我完全懂了:
|
||||
**我只帮你把「你的需求」优化润色成一段标准、清晰、可直接喂给AI的指令**,不替它写代码、不改结构,让AI根据你现有项目自己去改。
|
||||
|
||||
下面这段你**直接复制发给AI**即可:
|
||||
|
||||
---
|
||||
|
||||
# 【可直接投喂AI·优化版需求说明】
|
||||
你好,我现在需要对我的项目进行**多租户二级域名绑定官网系统**的整体改造,请根据我的现有项目结构和需求,帮我完成所有代码修改。
|
||||
|
||||
|
||||
151
go/controllers/backend_sitereminder.go
Normal file
151
go/controllers/backend_sitereminder.go
Normal file
@ -0,0 +1,151 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type BackendSiteReminderController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendSiteReminderController) backendClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMyList GET /backend/sitereminder/myList
|
||||
func (c *BackendSiteReminderController) GetMyList() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
var isRead *int8
|
||||
if isReadStr := c.GetString("isRead"); isReadStr != "" {
|
||||
if val, err := strconv.Atoi(isReadStr); err == nil {
|
||||
v := int8(val)
|
||||
isRead = &v
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := services.ListReminders(uint64(claims.UserID), "tenant", page, pageSize, isRead)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取消息列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkRead POST /backend/sitereminder/read
|
||||
func (c *BackendSiteReminderController) MarkRead() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.MarkReminderRead(p.ID, uint64(claims.UserID), "tenant")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkAllRead POST /backend/sitereminder/readall
|
||||
func (c *BackendSiteReminderController) MarkAllRead() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
err = services.MarkAllRemindersRead(uint64(claims.UserID), "tenant")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /backend/sitereminder/delete
|
||||
func (c *BackendSiteReminderController) Delete() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteReminder(p.ID, uint64(claims.UserID), "tenant")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@ -97,12 +97,44 @@ type emailFormPayload struct {
|
||||
Password string `json:"password"`
|
||||
Encryption string `json:"encryption"`
|
||||
Timeout interface{} `json:"timeout"`
|
||||
Status interface{} `json:"status"`
|
||||
}
|
||||
type testEmailPayload struct {
|
||||
emailFormPayload
|
||||
TestEmail string `json:"testEmail"`
|
||||
}
|
||||
|
||||
func parseInt8Flexible(v interface{}) int8 {
|
||||
if v == nil {
|
||||
return 1
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case bool:
|
||||
if x {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case float64:
|
||||
return int8(x)
|
||||
case int:
|
||||
return int8(x)
|
||||
case int8:
|
||||
return x
|
||||
case string:
|
||||
s := strings.TrimSpace(x)
|
||||
if s == "" {
|
||||
return 1
|
||||
}
|
||||
n, err := strconv.ParseInt(s, 10, 8)
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
return int8(n)
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func parseUintFlexible(v interface{}) uint {
|
||||
if v == nil {
|
||||
return 0
|
||||
@ -191,7 +223,8 @@ func (c *PlatformEmailController) EditInfo() {
|
||||
s := strings.TrimSpace(p.FromName)
|
||||
fn = &s
|
||||
}
|
||||
err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, 1, nil)
|
||||
status := parseInt8Flexible(p.Status)
|
||||
err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, status, nil)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存邮箱配置失败: "+err.Error())
|
||||
return
|
||||
|
||||
339
go/controllers/platform_sitereminder.go
Normal file
339
go/controllers/platform_sitereminder.go
Normal file
@ -0,0 +1,339 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type PlatformSiteReminderController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformSiteReminderController) platformClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "platform" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *PlatformSiteReminderController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetConfig GET /platform/sitereminder/config
|
||||
func (c *PlatformSiteReminderController) GetConfig() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
cfg, err := services.GetSiteReminderConfig()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cfg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// SaveConfig POST /platform/sitereminder/config
|
||||
func (c *PlatformSiteReminderController) SaveConfig() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
RetentionDays int `json:"retention_days"`
|
||||
AutoRead int8 `json:"auto_read"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := services.SaveSiteReminderConfig(p.RetentionDays, p.AutoRead); err != nil {
|
||||
c.jsonErr(500, 500, "保存配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Send POST /platform/sitereminder/send
|
||||
func (c *PlatformSiteReminderController) Send() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
TargetType string `json:"target_type"` // platform, tenant_all, role, tenant
|
||||
TargetRoleID uint64 `json:"target_role_id"`
|
||||
TargetTenantID uint64 `json:"target_tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
p.Title = strings.TrimSpace(p.Title)
|
||||
p.Content = strings.TrimSpace(p.Content)
|
||||
if p.Title == "" || p.Content == "" {
|
||||
c.jsonErr(400, 400, "标题与内容不能为空")
|
||||
return
|
||||
}
|
||||
if p.TargetType == "" {
|
||||
c.jsonErr(400, 400, "发送目标类型不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.SendSiteReminder(p.Title, p.Content, uint64(claims.UserID), "platform", p.TargetType, p.TargetRoleID, p.TargetTenantID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "发送失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "发送成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMyList GET /platform/sitereminder/myList
|
||||
func (c *PlatformSiteReminderController) GetMyList() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
var isRead *int8
|
||||
if isReadStr := c.GetString("isRead"); isReadStr != "" {
|
||||
if val, err := strconv.Atoi(isReadStr); err == nil {
|
||||
v := int8(val)
|
||||
isRead = &v
|
||||
}
|
||||
}
|
||||
|
||||
list, total, err := services.ListReminders(uint64(claims.UserID), "platform", page, pageSize, isRead)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取消息列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkRead POST /platform/sitereminder/read
|
||||
func (c *PlatformSiteReminderController) MarkRead() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.MarkReminderRead(p.ID, uint64(claims.UserID), "platform")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MarkAllRead POST /platform/sitereminder/readall
|
||||
func (c *PlatformSiteReminderController) MarkAllRead() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
err = services.MarkAllRemindersRead(uint64(claims.UserID), "platform")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /platform/sitereminder/delete
|
||||
func (c *PlatformSiteReminderController) Delete() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteReminder(p.ID, uint64(claims.UserID), "platform")
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetSentList GET /platform/sitereminder/sentList
|
||||
func (c *PlatformSiteReminderController) GetSentList() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
|
||||
list, total, err := services.ListSentReminders(uint64(claims.UserID), page, pageSize)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取发送列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateSent POST /platform/sitereminder/updateSent
|
||||
func (c *PlatformSiteReminderController) UpdateSent() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
BatchID string `json:"batch_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetRoleID uint64 `json:"target_role_id"`
|
||||
TargetTenantID uint64 `json:"target_tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
p.Title = strings.TrimSpace(p.Title)
|
||||
p.Content = strings.TrimSpace(p.Content)
|
||||
if p.BatchID == "" || p.Title == "" || p.Content == "" {
|
||||
c.jsonErr(400, 400, "批次号、标题与内容不能为空")
|
||||
return
|
||||
}
|
||||
if p.TargetType == "" {
|
||||
c.jsonErr(400, 400, "发送目标类型不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.UpdateSentReminder(p.BatchID, p.Title, p.Content, p.TargetType, p.TargetRoleID, p.TargetTenantID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "修改失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "修改成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteSentBatch POST /platform/sitereminder/deleteSent
|
||||
func (c *PlatformSiteReminderController) DeleteSentBatch() {
|
||||
if _, err := c.platformClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
BatchID string `json:"batch_id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BatchID == "" {
|
||||
c.jsonErr(400, 400, "批次号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteSentReminderBatch(p.BatchID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@ -66,6 +66,9 @@ func Init(_ string) {
|
||||
|
||||
new(CmsArticleCategory),
|
||||
new(CmsArticle),
|
||||
|
||||
new(SystemSiteReminder),
|
||||
new(SystemReminderList),
|
||||
)
|
||||
|
||||
// 创建全局 Ormer
|
||||
|
||||
26
go/models/system_reminderlist.go
Normal file
26
go/models/system_reminderlist.go
Normal file
@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SystemReminderList 站内信消息列表表 yz_system_reminderlist
|
||||
type SystemReminderList struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Title string `orm:"column(title);size(255)" json:"title"`
|
||||
Content string `orm:"column(content);type(text)" json:"content"`
|
||||
SenderID uint64 `orm:"column(sender_id);default(0)" json:"sender_id"`
|
||||
SenderType string `orm:"column(sender_type);size(32);default('system')" json:"sender_type"` // system, platform, tenant
|
||||
ReceiverID uint64 `orm:"column(receiver_id)" json:"receiver_id"`
|
||||
ReceiverType string `orm:"column(receiver_type);size(32)" json:"receiver_type"` // platform, tenant
|
||||
IsRead int8 `orm:"column(is_read);default(0)" json:"is_read"` // 0-未读, 1-已读
|
||||
ReadTime *time.Time `orm:"column(read_time);type(datetime);null" json:"read_time"`
|
||||
CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
BatchID string `orm:"column(batch_id);size(64);default('')" json:"batch_id"`
|
||||
TargetType string `orm:"column(target_type);size(32);default('')" json:"target_type"`
|
||||
TargetRoleID uint64 `orm:"column(target_role_id);default(0)" json:"target_role_id"`
|
||||
TargetTenantID uint64 `orm:"column(target_tenant_id);default(0)" json:"target_tenant_id"`
|
||||
}
|
||||
|
||||
func (m *SystemReminderList) TableName() string {
|
||||
return "yz_system_reminderlist"
|
||||
}
|
||||
16
go/models/system_sitereminder.go
Normal file
16
go/models/system_sitereminder.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SystemSiteReminder 站内信配置表 yz_system_sitereminder
|
||||
type SystemSiteReminder struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
RetentionDays int `orm:"column(retention_days);default(30)" json:"retention_days"`
|
||||
AutoRead int8 `orm:"column(auto_read);default(0)" json:"auto_read"`
|
||||
CreateTime *time.Time `orm:"column(create_time);type(datetime);null" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
|
||||
}
|
||||
|
||||
func (m *SystemSiteReminder) TableName() string {
|
||||
return "yz_system_sitereminder"
|
||||
}
|
||||
@ -51,6 +51,12 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos")
|
||||
beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos")
|
||||
|
||||
// 站内信(yz_system_reminderlist)
|
||||
beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList")
|
||||
beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead")
|
||||
beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead")
|
||||
beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete")
|
||||
|
||||
// 文件管理(yz_system_files / yz_system_files_category)
|
||||
beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate")
|
||||
beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles")
|
||||
|
||||
@ -126,6 +126,17 @@ func Register() {
|
||||
beego.Router("/platform/email/editinfo", &controllers.PlatformEmailController{}, "post:EditInfo")
|
||||
beego.Router("/platform/email/sendtestemail", &controllers.PlatformEmailController{}, "post:SendTestEmail")
|
||||
|
||||
// 站内信配置与发送(yz_system_sitereminder / yz_system_reminderlist)
|
||||
beego.Router("/platform/sitereminder/config", &controllers.PlatformSiteReminderController{}, "get:GetConfig;post:SaveConfig")
|
||||
beego.Router("/platform/sitereminder/send", &controllers.PlatformSiteReminderController{}, "post:Send")
|
||||
beego.Router("/platform/sitereminder/myList", &controllers.PlatformSiteReminderController{}, "get:GetMyList")
|
||||
beego.Router("/platform/sitereminder/read", &controllers.PlatformSiteReminderController{}, "post:MarkRead")
|
||||
beego.Router("/platform/sitereminder/readall", &controllers.PlatformSiteReminderController{}, "post:MarkAllRead")
|
||||
beego.Router("/platform/sitereminder/delete", &controllers.PlatformSiteReminderController{}, "post:Delete")
|
||||
beego.Router("/platform/sitereminder/sentList", &controllers.PlatformSiteReminderController{}, "get:GetSentList")
|
||||
beego.Router("/platform/sitereminder/updateSent", &controllers.PlatformSiteReminderController{}, "post:UpdateSent")
|
||||
beego.Router("/platform/sitereminder/deleteSent", &controllers.PlatformSiteReminderController{}, "post:DeleteSentBatch")
|
||||
|
||||
// 短信配置(yz_system_sms)
|
||||
beego.Router("/platform/sms/info", &controllers.PlatformSMSController{}, "get:GetSmsInfo")
|
||||
beego.Router("/platform/sms/editinfo", &controllers.PlatformSMSController{}, "post:EditSmsInfo")
|
||||
|
||||
@ -25,9 +25,6 @@ func UpsertFirstSystemEmail(fromAddress string, fromName *string, host string, p
|
||||
if timeout == 0 {
|
||||
timeout = 30
|
||||
}
|
||||
if status == 0 {
|
||||
status = 1
|
||||
}
|
||||
fromAddress = strings.TrimSpace(fromAddress)
|
||||
host = strings.TrimSpace(host)
|
||||
|
||||
|
||||
399
go/services/system_sitereminder.go
Normal file
399
go/services/system_sitereminder.go
Normal file
@ -0,0 +1,399 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// GetSiteReminderConfig 获取站内信配置(只读首条记录,不存在则初始化默认值)
|
||||
func GetSiteReminderConfig() (models.SystemSiteReminder, error) {
|
||||
var row models.SystemSiteReminder
|
||||
err := models.Orm.QueryTable(new(models.SystemSiteReminder)).OrderBy("id").Limit(1).One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
// 默认配置
|
||||
now := time.Now()
|
||||
row = models.SystemSiteReminder{
|
||||
RetentionDays: 30,
|
||||
AutoRead: 0,
|
||||
CreateTime: &now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
_, err = models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
return row, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
return row, err
|
||||
}
|
||||
|
||||
// SaveSiteReminderConfig 保存/更新配置
|
||||
func SaveSiteReminderConfig(retentionDays int, autoRead int8) error {
|
||||
if retentionDays <= 0 {
|
||||
retentionDays = 30
|
||||
}
|
||||
cfg, err := GetSiteReminderConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
cfg.RetentionDays = retentionDays
|
||||
cfg.AutoRead = autoRead
|
||||
cfg.UpdateTime = &now
|
||||
|
||||
_, err = models.Orm.Update(&cfg, "RetentionDays", "AutoRead", "UpdateTime")
|
||||
return err
|
||||
}
|
||||
|
||||
// SendSiteReminder 发送站内信
|
||||
// targetType: platform (平台端), tenant_all (管理端所有用户), role (平台角色), tenant (特定租户)
|
||||
func SendSiteReminder(title, content string, senderID uint64, senderType string, targetType string, targetRoleID uint64, targetTenantID uint64) error {
|
||||
var receiverIDs []uint64
|
||||
var receiverType string
|
||||
|
||||
switch targetType {
|
||||
case "platform":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询平台用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant_all":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询租户用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "role":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据角色查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据租户查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("未知的发送目标类型: %s", targetType)
|
||||
}
|
||||
|
||||
if len(receiverIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
batchID := fmt.Sprintf("%d_%d", now.UnixNano(), senderID)
|
||||
var reminders []models.SystemReminderList
|
||||
for _, rid := range receiverIDs {
|
||||
reminders = append(reminders, models.SystemReminderList{
|
||||
Title: title,
|
||||
Content: content,
|
||||
SenderID: senderID,
|
||||
SenderType: senderType,
|
||||
ReceiverID: rid,
|
||||
ReceiverType: receiverType,
|
||||
IsRead: 0,
|
||||
CreateTime: &now,
|
||||
BatchID: batchID,
|
||||
TargetType: targetType,
|
||||
TargetRoleID: targetRoleID,
|
||||
TargetTenantID: targetTenantID,
|
||||
})
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
_, err := models.Orm.InsertMulti(100, reminders)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListReminders 列表查询
|
||||
func ListReminders(receiverID uint64, receiverType string, page, pageSize int, isRead *int8) ([]models.SystemReminderList, int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
var list []models.SystemReminderList
|
||||
qs := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
if isRead != nil {
|
||||
qs = qs.Filter("is_read", *isRead)
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("-create_time", "-id").Limit(pageSize, offset).All(&list)
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// MarkReminderRead 标记单条已读
|
||||
func MarkReminderRead(id uint64, receiverID uint64, receiverType string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("id", id).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Update(map[string]interface{}{
|
||||
"is_read": 1,
|
||||
"read_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkAllRemindersRead 一键全部已读
|
||||
func MarkAllRemindersRead(receiverID uint64, receiverType string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Filter("is_read", 0).
|
||||
Update(map[string]interface{}{
|
||||
"is_read": 1,
|
||||
"read_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteReminder 删除消息
|
||||
func DeleteReminder(id uint64, receiverID uint64, receiverType string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("id", id).
|
||||
Filter("receiver_id", receiverID).
|
||||
Filter("receiver_type", receiverType).
|
||||
Update(map[string]interface{}{
|
||||
"delete_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// AutoCleanExpiredReminders 自动清理过期站内信
|
||||
func AutoCleanExpiredReminders() error {
|
||||
cfg, err := GetSiteReminderConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.RetentionDays <= 0 {
|
||||
return nil
|
||||
}
|
||||
expireTime := time.Now().AddDate(0, 0, -cfg.RetentionDays)
|
||||
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("create_time__lt", expireTime).
|
||||
Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
// ListSentReminders 获取已发送的消息列表(按 batch_id 分组)
|
||||
func ListSentReminders(senderID uint64, page, pageSize int) ([]models.SystemReminderList, int64, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
var total int64
|
||||
err := models.Orm.Raw("SELECT COUNT(DISTINCT batch_id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL", senderID).QueryRow(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var list []models.SystemReminderList
|
||||
_, err = models.Orm.Raw("SELECT * FROM yz_system_reminderlist WHERE id IN (SELECT MIN(id) FROM yz_system_reminderlist WHERE sender_id = ? AND delete_time IS NULL GROUP BY batch_id) ORDER BY id DESC LIMIT ? OFFSET ?", senderID, pageSize, offset).QueryRows(&list)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// UpdateSentReminder 更新已发出的消息(更新该批次下所有接收者的消息,支持修改目标接收群体)
|
||||
func UpdateSentReminder(batchID string, title, content, targetType string, targetRoleID, targetTenantID uint64) error {
|
||||
// 1. 获取当前发送者ID (从该批次中任意一条记录中获取)
|
||||
var firstRecord models.SystemReminderList
|
||||
err := models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).Limit(1).One(&firstRecord)
|
||||
if err != nil {
|
||||
return fmt.Errorf("找不到该批次的站内信记录: %w", err)
|
||||
}
|
||||
senderID := firstRecord.SenderID
|
||||
senderType := firstRecord.SenderType
|
||||
|
||||
// 2. 根据新的目标接收群体获取接收人列表
|
||||
var receiverIDs []uint64
|
||||
var receiverType string
|
||||
|
||||
switch targetType {
|
||||
case "platform":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询平台用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant_all":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询租户用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "role":
|
||||
receiverType = "platform"
|
||||
var list []models.AdminUser
|
||||
_, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("status", 1).Filter("role_id", targetRoleID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据角色查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
case "tenant":
|
||||
receiverType = "tenant"
|
||||
var list []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("status", 1).Filter("tid", targetTenantID).Filter("delete_time__isnull", true).All(&list, "id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("根据租户查询用户失败: %w", err)
|
||||
}
|
||||
for _, u := range list {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("未知的发送目标类型: %s", targetType)
|
||||
}
|
||||
|
||||
// 3. 获取该批次中现有的所有记录 (包括已删除的)
|
||||
var existingRecords []models.SystemReminderList
|
||||
_, err = models.Orm.QueryTable(new(models.SystemReminderList)).Filter("batch_id", batchID).All(&existingRecords)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取现有记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 建立 map 快速查找,Key 为 "receiverType_receiverID"
|
||||
existingMap := make(map[string]*models.SystemReminderList)
|
||||
for i := range existingRecords {
|
||||
key := fmt.Sprintf("%s_%d", existingRecords[i].ReceiverType, existingRecords[i].ReceiverID)
|
||||
existingMap[key] = &existingRecords[i]
|
||||
}
|
||||
|
||||
newReceiverMap := make(map[string]bool)
|
||||
for _, rid := range receiverIDs {
|
||||
key := fmt.Sprintf("%s_%d", receiverType, rid)
|
||||
newReceiverMap[key] = true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 事务处理
|
||||
err = models.Orm.DoTx(func(c context.Context, txOrm orm.TxOrmer) error {
|
||||
// A. 对于已经不在新接收者列表中的用户,软删除
|
||||
for key, rec := range existingMap {
|
||||
if !newReceiverMap[key] {
|
||||
if rec.DeleteTime == nil {
|
||||
rec.DeleteTime = &now
|
||||
if _, e := txOrm.Update(rec, "DeleteTime"); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// B. 对于仍然在新接收者列表中的用户,更新标题、内容、以及 target 信息;如果原来被删除了,清除 delete_time
|
||||
var newInserts []models.SystemReminderList
|
||||
for _, rid := range receiverIDs {
|
||||
key := fmt.Sprintf("%s_%d", receiverType, rid)
|
||||
if rec, exists := existingMap[key]; exists {
|
||||
rec.Title = title
|
||||
rec.Content = content
|
||||
rec.TargetType = targetType
|
||||
rec.TargetRoleID = targetRoleID
|
||||
rec.TargetTenantID = targetTenantID
|
||||
|
||||
cols := []string{"Title", "Content", "TargetType", "TargetRoleID", "TargetTenantID"}
|
||||
if rec.DeleteTime != nil {
|
||||
rec.DeleteTime = nil
|
||||
rec.IsRead = 0
|
||||
rec.ReadTime = nil
|
||||
cols = append(cols, "DeleteTime", "IsRead", "ReadTime")
|
||||
}
|
||||
if _, e := txOrm.Update(rec, cols...); e != nil {
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
// C. 对于新增加的接收者,插入新记录
|
||||
newInserts = append(newInserts, models.SystemReminderList{
|
||||
Title: title,
|
||||
Content: content,
|
||||
SenderID: senderID,
|
||||
SenderType: senderType,
|
||||
ReceiverID: rid,
|
||||
ReceiverType: receiverType,
|
||||
IsRead: 0,
|
||||
CreateTime: &now,
|
||||
BatchID: batchID,
|
||||
TargetType: targetType,
|
||||
TargetRoleID: targetRoleID,
|
||||
TargetTenantID: targetTenantID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(newInserts) > 0 {
|
||||
if _, e := txOrm.InsertMulti(100, newInserts); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSentReminderBatch 删除已发送消息(删除该批次下所有记录)
|
||||
func DeleteSentReminderBatch(batchID string) error {
|
||||
now := time.Now()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemReminderList)).
|
||||
Filter("batch_id", batchID).
|
||||
Update(map[string]interface{}{
|
||||
"delete_time": &now,
|
||||
})
|
||||
return err
|
||||
}
|
||||
1
platform/components.d.ts
vendored
1
platform/components.d.ts
vendored
@ -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']
|
||||
|
||||
89
platform/src/api/sitereminder.js
Normal file
89
platform/src/api/sitereminder.js
Normal 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 },
|
||||
});
|
||||
}
|
||||
@ -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-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>
|
||||
280
platform/src/views/basicSettings/sitereminder/index.vue
Normal file
280
platform/src/views/basicSettings/sitereminder/index.vue
Normal file
@ -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>
|
||||
|
||||
|
||||
12
sql/upgrade_yz_system_reminderlist.sql
Normal file
12
sql/upgrade_yz_system_reminderlist.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- 升级已有的 yz_system_reminderlist 表,添加 batch_id 以及发送目标相关字段
|
||||
ALTER TABLE `yz_system_reminderlist`
|
||||
ADD COLUMN `batch_id` varchar(64) NOT NULL DEFAULT '' COMMENT '批次号/分组ID' AFTER `delete_time`,
|
||||
ADD COLUMN `target_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发送目标类型: platform, tenant_all, role, tenant' AFTER `batch_id`,
|
||||
ADD COLUMN `target_role_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标角色ID' AFTER `target_type`,
|
||||
ADD COLUMN `target_tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标租户ID' AFTER `target_role_id`,
|
||||
ADD INDEX `idx_batch` (`batch_id`);
|
||||
|
||||
-- 初始化已有记录的 batch_id (如果存在空值,使用 create_time 和 sender_id 进行分组初始化)
|
||||
UPDATE `yz_system_reminderlist`
|
||||
SET `batch_id` = CONCAT(UNIX_TIMESTAMP(COALESCE(`create_time`, NOW())), '_', `sender_id`)
|
||||
WHERE `batch_id` = '' OR `batch_id` IS NULL;
|
||||
36
sql/yz_system_sitereminder.sql
Normal file
36
sql/yz_system_sitereminder.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- 站内信配置表
|
||||
CREATE TABLE IF NOT EXISTS `yz_system_sitereminder` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`retention_days` int(11) NOT NULL DEFAULT '30' COMMENT '消息保留天数',
|
||||
`auto_read` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否自动标记已读: 0-否, 1-是',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内信配置表';
|
||||
|
||||
-- 默认插入一条配置数据
|
||||
INSERT INTO `yz_system_sitereminder` (`id`, `retention_days`, `auto_read`, `create_time`, `update_time`)
|
||||
VALUES (1, 30, 0, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE `update_time` = NOW();
|
||||
|
||||
-- 站内信消息列表表
|
||||
CREATE TABLE IF NOT EXISTS `yz_system_reminderlist` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`title` varchar(255) NOT NULL COMMENT '标题',
|
||||
`content` text NOT NULL COMMENT '内容',
|
||||
`sender_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '发送者ID (0为系统)',
|
||||
`sender_type` varchar(32) NOT NULL DEFAULT 'system' COMMENT '发送者类型: system, platform, tenant',
|
||||
`receiver_id` bigint(20) unsigned NOT NULL COMMENT '接收者ID',
|
||||
`receiver_type` varchar(32) NOT NULL DEFAULT 'receiver' COMMENT '接收者类型: platform, tenant',
|
||||
`is_read` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否已读: 0-未读, 1-已读',
|
||||
`read_time` datetime DEFAULT NULL COMMENT '已读时间',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间/发送时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
`batch_id` varchar(64) NOT NULL DEFAULT '' COMMENT '批次号/分组ID',
|
||||
`target_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发送目标类型: platform, tenant_all, role, tenant',
|
||||
`target_role_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标角色ID',
|
||||
`target_tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标租户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_receiver` (`receiver_type`, `receiver_id`, `is_read`),
|
||||
KEY `idx_batch` (`batch_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内信消息列表表';
|
||||
Loading…
Reference in New Issue
Block a user