feat: add user notification center

This commit is contained in:
hajimi
2026-08-03 21:44:30 +08:00
parent 17720e86e2
commit 9df5f5f620
17 changed files with 1401 additions and 5 deletions
+20
View File
@@ -15,6 +15,26 @@ export function setNoticeConfig(params: any) {
return request.post({ url: '/notice.notice/set', params })
}
// 用户站内消息列表
export function userNoticeLists(params: any) {
return request.get({ url: '/notice.userNotice/lists', params })
}
// 用户站内消息类型选项
export function userNoticeOptions() {
return request.get({ url: '/notice.userNotice/options' })
}
// 发布系统站内消息
export function sendUserNotice(params: any) {
return request.post({ url: '/notice.userNotice/send', params })
}
// 删除站内消息记录
export function deleteUserNotice(params: { ids: number[] }) {
return request.post({ url: '/notice.userNotice/delete', params })
}
// 短信设置列表
export function smsLists() {
return request.get({ url: '/notice.sms_config/getConfig' })
@@ -0,0 +1,190 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<div class="mb-4 flex items-center justify-between">
<div>
<div class="text-lg font-medium">站内消息</div>
<div class="mt-1 text-sm text-gray-400">查看互动通知并向全部或指定用户发布系统消息</div>
</div>
<el-button v-perms="['notice.userNotice/send']" type="primary" @click="openSendDialog">
发布系统消息
</el-button>
</div>
<el-form class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item label="关键词">
<el-input v-model="queryParams.keyword" class="w-[240px]" placeholder="标题、内容、用户昵称或账号"
clearable @keyup.enter="resetPage" />
</el-form-item>
<el-form-item label="消息类型">
<el-select v-model="queryParams.scene_id" class="w-[160px]" clearable placeholder="全部">
<el-option v-for="item in sceneOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="阅读状态">
<el-select v-model="queryParams.read" class="w-[130px]" clearable placeholder="全部">
<el-option label="未读" :value="0" />
<el-option label="已读" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="ID" prop="id" width="76" />
<el-table-column label="接收用户" min-width="160">
<template #default="{ row }">
<div>{{ row.user_nickname || '用户' }}</div>
<div class="text-xs text-gray-400">ID: {{ row.user_id }} {{ row.user_account ? `· ${row.user_account}` : '' }}</div>
</template>
</el-table-column>
<el-table-column label="类型" prop="type_desc" min-width="110">
<template #default="{ row }">
<el-tag size="small">{{ row.type_desc }}</el-tag>
</template>
</el-table-column>
<el-table-column label="消息内容" min-width="300" show-overflow-tooltip>
<template #default="{ row }">
<div class="font-medium">{{ row.title }}</div>
<div class="mt-1 text-sm text-gray-400">{{ row.content }}</div>
</template>
</el-table-column>
<el-table-column label="互动用户" min-width="130">
<template #default="{ row }">
<span v-if="row.source_user_id">{{ row.source_nickname || `用户 ${row.source_user_id}` }}</span>
<span v-else class="text-gray-400"></span>
</template>
</el-table-column>
<el-table-column label="跳转路径" prop="target_url" min-width="210" show-overflow-tooltip />
<el-table-column label="状态" min-width="90">
<template #default="{ row }">
<el-tag :type="row.read ? 'success' : 'warning'" size="small">
{{ row.read ? '已读' : '未读' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="发送时间" prop="create_time" min-width="165" />
<el-table-column label="操作" width="90" fixed="right">
<template #default="{ row }">
<el-button v-perms="['notice.userNotice/delete']" type="danger" link @click="handleDelete(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<el-dialog v-model="sendDialog.visible" title="发布系统消息" width="560px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="接收范围">
<el-radio-group v-model="sendDialog.recipient_scope">
<el-radio value="all">全部启用用户</el-radio>
<el-radio value="specified">指定用户</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="sendDialog.recipient_scope === 'specified'" label="用户 ID">
<el-input v-model="sendDialog.user_ids_text" type="textarea" :rows="2"
placeholder="多个用户 ID 用逗号或换行分隔" />
</el-form-item>
<el-form-item label="消息标题" required>
<el-input v-model="sendDialog.title" maxlength="50" show-word-limit placeholder="例如:平台公告" />
</el-form-item>
<el-form-item label="消息内容" required>
<el-input v-model="sendDialog.content" type="textarea" :rows="5" maxlength="1000" show-word-limit
placeholder="请输入消息内容" />
</el-form-item>
<el-form-item label="跳转路径">
<el-input v-model="sendDialog.target_url" placeholder="可选,例如 /pages/community/community" />
<div class="mt-1 text-xs text-gray-400">留空则只标记已读填写时必须是站内页面路径</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="sendDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="sendDialog.submitting" @click="handleSend">发布</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="messageUserNotice">
import { deleteUserNotice, sendUserNotice, userNoticeLists, userNoticeOptions } from '@/api/message'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
const queryParams = reactive({
keyword: '',
scene_id: '',
read: '' as number | string
})
const sceneOptions = ref<Array<{ value: number; label: string }>>([])
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: userNoticeLists,
params: queryParams
})
const sendDialog = reactive({
visible: false,
submitting: false,
recipient_scope: 'all',
user_ids_text: '',
title: '',
content: '',
target_url: ''
})
const getOptions = async () => {
const result: any = await userNoticeOptions()
sceneOptions.value = result?.scene_options || []
}
const openSendDialog = () => {
sendDialog.visible = true
sendDialog.recipient_scope = 'all'
sendDialog.user_ids_text = ''
sendDialog.title = ''
sendDialog.content = ''
sendDialog.target_url = ''
}
const handleSend = async () => {
const userIds = sendDialog.user_ids_text
.split(/[,\s]+/)
.map((item) => Number(item))
.filter((item) => Number.isInteger(item) && item > 0)
if (sendDialog.recipient_scope === 'specified' && userIds.length === 0) {
feedback.msgError('请至少填写一位指定用户ID')
return
}
sendDialog.submitting = true
try {
await sendUserNotice({
recipient_scope: sendDialog.recipient_scope,
user_ids: userIds,
title: sendDialog.title.trim(),
content: sendDialog.content.trim(),
target_url: sendDialog.target_url.trim()
})
sendDialog.visible = false
getLists()
} finally {
sendDialog.submitting = false
}
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定删除这条站内消息记录?')
await deleteUserNotice({ ids: [id] })
getLists()
}
getOptions()
getLists()
</script>