|
|
@@ -1,106 +1,251 @@
|
|
|
<template>
|
|
|
<div class="app-container">
|
|
|
+ <!-- 操作工具栏 -->
|
|
|
<el-row :gutter="10" class="mb8">
|
|
|
<el-col :span="1.5">
|
|
|
- <el-button type="primary" plain icon="Check" @click="handleReadAll" :disabled="unreadCount === 0">全部标为已读</el-button>
|
|
|
+ <el-button type="primary" plain icon="Check" @click="handleReadAll" :disabled="unreadCount === 0">
|
|
|
+ 全部标为已读
|
|
|
+ </el-button>
|
|
|
</el-col>
|
|
|
<el-col :span="1.5">
|
|
|
- <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">批量删除</el-button>
|
|
|
+ <el-button type="success" plain icon="Check" :disabled="selectCount === 0" @click="handleBatchRead">
|
|
|
+ 标记已读 ({{ selectCount }})
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button type="danger" plain icon="Delete" :disabled="selectCount === 0" @click="handleBatchDelete">
|
|
|
+ 批量删除 ({{ selectCount }})
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5" v-if="isAdmin">
|
|
|
+ <el-button type="warning" plain icon="Promotion" @click="handleBroadcast" v-hasPermi="['performance:message:broadcast']">
|
|
|
+ 群发通知
|
|
|
+ </el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <span class="refresh-tip" v-if="lastRefreshTime">
|
|
|
+ 上次刷新: {{ lastRefreshTime }}
|
|
|
+ <el-button link type="primary" @click="refreshAll">刷新</el-button>
|
|
|
+ </span>
|
|
|
</el-col>
|
|
|
- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
</el-row>
|
|
|
|
|
|
+ <!-- 双Tab -->
|
|
|
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
|
|
- <el-tab-pane label="全部消息" name="all" />
|
|
|
- <el-tab-pane :label="'未读消息 (' + unreadCount + ')'" name="unread" />
|
|
|
+ <el-tab-pane name="all">
|
|
|
+ <template #label>
|
|
|
+ <span>全部消息 <el-badge :value="total" :max="99" class="tab-badge" v-if="total > 0" /></span>
|
|
|
+ </template>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane name="unread">
|
|
|
+ <template #label>
|
|
|
+ <span>未读消息 <el-badge :value="unreadCount" :max="99" type="danger" class="tab-badge" v-if="unreadCount > 0" /></span>
|
|
|
+ </template>
|
|
|
+ </el-tab-pane>
|
|
|
</el-tabs>
|
|
|
|
|
|
- <el-table v-loading="loading" :data="messageList" @selection-change="handleSelectionChange" row-key="messageId">
|
|
|
+ <!-- 消息列表 -->
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="messageList"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ @row-click="handleRowClick"
|
|
|
+ row-key="messageId"
|
|
|
+ highlight-current-row
|
|
|
+ >
|
|
|
<el-table-column type="selection" width="50" align="center" />
|
|
|
- <el-table-column label="消息类型" align="center" prop="messageType" width="100">
|
|
|
+ <el-table-column label="" width="40" align="center">
|
|
|
<template #default="scope">
|
|
|
- <el-tag :type="getMessageTagType(scope.row.messageType)">{{ getMessageTypeLabel(scope.row.messageType) }}</el-tag>
|
|
|
+ <el-icon v-if="scope.row.status === 'UNREAD'" color="#409EFF" :size="10"><CircleCheckFilled /></el-icon>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="消息内容" align="center" prop="content" :show-overflow-tooltip="true">
|
|
|
+ <el-table-column label="消息类型" align="center" width="110">
|
|
|
<template #default="scope">
|
|
|
- <span :style="{ fontWeight: scope.row.status === 'UNREAD' ? 'bold' : 'normal', cursor: 'pointer' }" @click="handleRead(scope.row)">
|
|
|
- {{ scope.row.content }}
|
|
|
+ <el-tag :type="getTagType(scope.row.messageType)" :effect="scope.row.status === 'UNREAD' ? 'dark' : 'plain'" size="small">
|
|
|
+ {{ getTypeLabel(scope.row.messageType) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="消息标题" align="center" prop="title" width="160" :show-overflow-tooltip="true">
|
|
|
+ <template #default="scope">
|
|
|
+ <span :style="{ fontWeight: scope.row.status === 'UNREAD' ? 'bold' : 'normal' }">
|
|
|
+ {{ scope.row.title }}
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="接收时间" align="center" prop="createTime" width="160">
|
|
|
+ <el-table-column label="内容摘要" align="center" prop="content" :show-overflow-tooltip="true" min-width="280">
|
|
|
+ <template #default="scope">
|
|
|
+ <span
|
|
|
+ :style="{ fontWeight: scope.row.status === 'UNREAD' ? 'bold' : 'normal', color: scope.row.status === 'UNREAD' ? '#303133' : '#909399' }"
|
|
|
+ >{{ scope.row.content }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="发送时间" align="center" prop="createTime" width="160">
|
|
|
<template #default="scope">
|
|
|
- <span>{{ parseTime(scope.row.createTime) }}</span>
|
|
|
+ <el-tooltip :content="parseTime(scope.row.createTime)" placement="top">
|
|
|
+ <span>{{ formatRelativeTime(scope.row.createTime) }}</span>
|
|
|
+ </el-tooltip>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="状态" align="center" prop="status" width="80">
|
|
|
+ <el-table-column label="状态" align="center" width="80">
|
|
|
<template #default="scope">
|
|
|
- <dict-tag :options="statusOptions" :value="scope.row.status" />
|
|
|
+ <el-tag :type="scope.row.status === 'UNREAD' ? 'danger' : 'info'" size="small" effect="plain">
|
|
|
+ {{ scope.row.status === 'UNREAD' ? '未读' : '已读' }}
|
|
|
+ </el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" align="center" width="80">
|
|
|
+ <el-table-column label="操作" align="center" width="120">
|
|
|
<template #default="scope">
|
|
|
- <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
|
|
+ <el-button link type="primary" size="small" @click.stop="handleRead(scope.row)" v-if="scope.row.status === 'UNREAD'">标为已读</el-button>
|
|
|
+ <el-button link type="danger" size="small" @click.stop="handleDeleteSingle(scope.row)">删除</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
|
|
+
|
|
|
+ <!-- 消息详情对话框 -->
|
|
|
+ <el-dialog title="消息详情" v-model="detailOpen" width="550px" append-to-body @close="detailOpen = false">
|
|
|
+ <el-descriptions :column="1" border>
|
|
|
+ <el-descriptions-item label="消息类型">
|
|
|
+ <el-tag :type="getTagType(detailData.messageType)">{{ getTypeLabel(detailData.messageType) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="消息标题">{{ detailData.title }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="发送时间">{{ parseTime(detailData.createTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <el-tag :type="detailData.status === 'UNREAD' ? 'danger' : 'info'" size="small">
|
|
|
+ {{ detailData.status === 'UNREAD' ? '未读' : '已读' }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <el-divider content-position="left">消息内容</el-divider>
|
|
|
+ <div class="message-detail__content">{{ detailData.content }}</div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 群发通知对话框 -->
|
|
|
+ <el-dialog title="群发通知" v-model="broadcastOpen" width="600px" append-to-body @close="broadcastOpen = false">
|
|
|
+ <el-form :model="broadcastForm" :rules="broadcastRules" ref="broadcastRef" label-width="100px">
|
|
|
+ <el-form-item label="通知类型" prop="messageType">
|
|
|
+ <el-select v-model="broadcastForm.messageType" placeholder="请选择通知类型" style="width: 100%">
|
|
|
+ <el-option v-for="t in broadcastTypes" :key="t.value" :label="t.label" :value="t.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="目标角色" prop="targetRole">
|
|
|
+ <el-select v-model="broadcastForm.targetRole" placeholder="请选择接收角色" style="width: 100%">
|
|
|
+ <el-option label="全部用户" value="ALL" />
|
|
|
+ <el-option label="部门管理员" value="DEPT_ADMIN" />
|
|
|
+ <el-option label="院班子审核员" value="REVIEWER" />
|
|
|
+ <el-option label="普通员工" value="EMPLOYEE" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="通知标题" prop="title">
|
|
|
+ <el-input v-model="broadcastForm.title" placeholder="请输入通知标题" maxlength="100" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="通知内容" prop="content">
|
|
|
+ <el-input v-model="broadcastForm.content" type="textarea" :rows="4" placeholder="请输入通知内容" maxlength="500" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button type="primary" @click="submitBroadcast">发送通知</el-button>
|
|
|
+ <el-button @click="broadcastOpen = false">取 消</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="PerformanceMessage">
|
|
|
import { listMessage, readMessage, readAllMessage, delMessage, getUnreadCount } from '@/api/performance/message'
|
|
|
+import useUserStore from '@/store/modules/user'
|
|
|
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
|
|
|
-const statusOptions = ref([
|
|
|
- { label: '未读', value: 'UNREAD' },
|
|
|
- { label: '已读', value: 'READ' }
|
|
|
-])
|
|
|
+const userStore = useUserStore()
|
|
|
+const isAdmin = computed(() => userStore.roles.includes('admin') || userStore.roles.includes('ROLE_ADMIN'))
|
|
|
|
|
|
+// 消息类型配置
|
|
|
const messageTypeMap = {
|
|
|
- 'FILL_REMINDER': { label: '填报提醒', tagType: 'warning' },
|
|
|
- 'COOPERATION_REMINDER': { label: '协作提醒', tagType: 'info' },
|
|
|
- 'REVIEW_REMINDER': { label: '审核提醒', tagType: 'danger' },
|
|
|
+ 'FILL_REMINDER': { label: '填报提醒', tagType: '' },
|
|
|
+ 'REVIEW_REMINDER': { label: '审核提醒', tagType: 'warning' },
|
|
|
'OVERDUE_REMINDER': { label: '逾期提醒', tagType: 'danger' },
|
|
|
- 'REVIEW_RESULT': { label: '审核结果', tagType: 'success' }
|
|
|
+ 'COOPERATION_REMINDER': { label: '协作提醒', tagType: 'success' },
|
|
|
+ 'REVIEW_RESULT': { label: '结果反馈', tagType: 'primary' }
|
|
|
}
|
|
|
|
|
|
+const broadcastTypes = [
|
|
|
+ { label: '填报提醒', value: 'FILL_REMINDER' },
|
|
|
+ { label: '审核提醒', value: 'REVIEW_REMINDER' },
|
|
|
+ { label: '逾期提醒', value: 'OVERDUE_REMINDER' },
|
|
|
+ { label: '协作提醒', value: 'COOPERATION_REMINDER' },
|
|
|
+ { label: '结果反馈', value: 'REVIEW_RESULT' }
|
|
|
+]
|
|
|
+
|
|
|
+function getTypeLabel(type) {
|
|
|
+ return (messageTypeMap[type] || {}).label || type
|
|
|
+}
|
|
|
+
|
|
|
+function getTagType(type) {
|
|
|
+ return (messageTypeMap[type] || {}).tagType || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+// 状态
|
|
|
const messageList = ref([])
|
|
|
-const loading = ref(true)
|
|
|
-const showSearch = ref(true)
|
|
|
-const ids = ref([])
|
|
|
-const single = ref(true)
|
|
|
-const multiple = ref(true)
|
|
|
+const loading = ref(false)
|
|
|
const total = ref(0)
|
|
|
-const activeTab = ref('all')
|
|
|
const unreadCount = ref(0)
|
|
|
+const activeTab = ref('all')
|
|
|
+const selectCount = ref(0)
|
|
|
+const selectedIds = ref([])
|
|
|
+const detailOpen = ref(false)
|
|
|
+const broadcastOpen = ref(false)
|
|
|
+const lastRefreshTime = ref('')
|
|
|
+const refreshTimer = ref(null)
|
|
|
+
|
|
|
+const detailData = reactive({})
|
|
|
|
|
|
const data = reactive({
|
|
|
queryParams: {
|
|
|
pageNum: 1,
|
|
|
pageSize: 10,
|
|
|
status: undefined
|
|
|
+ },
|
|
|
+ broadcastForm: {
|
|
|
+ messageType: undefined,
|
|
|
+ targetRole: 'ALL',
|
|
|
+ title: undefined,
|
|
|
+ content: undefined
|
|
|
+ },
|
|
|
+ broadcastRules: {
|
|
|
+ messageType: [{ required: true, message: '请选择通知类型', trigger: 'change' }],
|
|
|
+ targetRole: [{ required: true, message: '请选择目标角色', trigger: 'change' }],
|
|
|
+ title: [{ required: true, message: '请输入通知标题', trigger: 'blur' }],
|
|
|
+ content: [{ required: true, message: '请输入通知内容', trigger: 'blur' }]
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const { queryParams } = toRefs(data)
|
|
|
+const { queryParams, broadcastForm, broadcastRules } = toRefs(data)
|
|
|
|
|
|
function getList() {
|
|
|
loading.value = true
|
|
|
listMessage(queryParams.value).then(res => {
|
|
|
- messageList.value = res.rows
|
|
|
- total.value = res.total
|
|
|
+ messageList.value = (res.rows || []).map(m => ({ ...m, _expanded: false }))
|
|
|
+ total.value = res.total || 0
|
|
|
loading.value = false
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function getUnreadCountApi() {
|
|
|
- getUnreadCount().then(res => {
|
|
|
+async function getUnreadCountApi() {
|
|
|
+ try {
|
|
|
+ const res = await getUnreadCount()
|
|
|
unreadCount.value = res.data || 0
|
|
|
- })
|
|
|
+ lastRefreshTime.value = new Date().toLocaleTimeString()
|
|
|
+ } catch { /* ignore */ }
|
|
|
+}
|
|
|
+
|
|
|
+function refreshAll() {
|
|
|
+ getList()
|
|
|
+ getUnreadCountApi()
|
|
|
}
|
|
|
|
|
|
function handleQuery() {
|
|
|
@@ -114,34 +259,54 @@ function handleTabChange(tab) {
|
|
|
}
|
|
|
|
|
|
function handleSelectionChange(selection) {
|
|
|
- ids.value = selection.map(item => item.messageId)
|
|
|
- single.value = selection.length !== 1
|
|
|
- multiple.value = !selection.length
|
|
|
+ selectedIds.value = selection.map(item => item.messageId)
|
|
|
+ selectCount.value = selection.length
|
|
|
}
|
|
|
|
|
|
-function handleRead(row) {
|
|
|
+function handleRowClick(row) {
|
|
|
+ detailData.messageId = row.messageId
|
|
|
+ detailData.messageType = row.messageType
|
|
|
+ detailData.title = row.title
|
|
|
+ detailData.content = row.content
|
|
|
+ detailData.createTime = row.createTime
|
|
|
+ detailData.status = row.status
|
|
|
+ detailOpen.value = true
|
|
|
+
|
|
|
if (row.status === 'UNREAD') {
|
|
|
- readMessage(row.messageId).then(() => {
|
|
|
- row.status = 'READ'
|
|
|
- getUnreadCountApi()
|
|
|
- })
|
|
|
+ handleRead(row)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function handleRead(row) {
|
|
|
+ readMessage(row.messageId).then(() => {
|
|
|
+ row.status = 'READ'
|
|
|
+ getUnreadCountApi()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function handleBatchRead() {
|
|
|
+ if (selectedIds.value.length === 0) return
|
|
|
+ const tasks = selectedIds.value.map(id => readMessage(id))
|
|
|
+ Promise.all(tasks).then(() => {
|
|
|
+ proxy.$modal.msgSuccess(`已标记 ${selectedIds.value.length} 条消息为已读`)
|
|
|
+ getList()
|
|
|
+ getUnreadCountApi()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
function handleReadAll() {
|
|
|
proxy.$modal.confirm('确认将所有消息标为已读?').then(() => {
|
|
|
return readAllMessage()
|
|
|
}).then(() => {
|
|
|
- proxy.$modal.msgSuccess('操作成功')
|
|
|
+ proxy.$modal.msgSuccess('全部已读')
|
|
|
getList()
|
|
|
getUnreadCountApi()
|
|
|
}).catch(() => {})
|
|
|
}
|
|
|
|
|
|
-function handleDelete(row) {
|
|
|
- const messageIds = row.messageId || ids.value.join(',')
|
|
|
- proxy.$modal.confirm('确认删除选中的消息?').then(() => {
|
|
|
- return delMessage(messageIds)
|
|
|
+function handleDeleteSingle(row) {
|
|
|
+ proxy.$modal.confirm('确认删除该消息?').then(() => {
|
|
|
+ return delMessage(row.messageId)
|
|
|
}).then(() => {
|
|
|
proxy.$modal.msgSuccess('删除成功')
|
|
|
getList()
|
|
|
@@ -150,19 +315,92 @@ function handleDelete(row) {
|
|
|
}
|
|
|
|
|
|
function handleBatchDelete() {
|
|
|
- handleDelete(null)
|
|
|
+ if (selectedIds.value.length === 0) {
|
|
|
+ proxy.$modal.msgWarning('请选择消息')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ proxy.$modal.confirm(`确认删除 ${selectedIds.value.length} 条消息?`).then(() => {
|
|
|
+ return delMessage(selectedIds.value.join(','))
|
|
|
+ }).then(() => {
|
|
|
+ proxy.$modal.msgSuccess('批量删除成功')
|
|
|
+ getList()
|
|
|
+ getUnreadCountApi()
|
|
|
+ }).catch(() => {})
|
|
|
}
|
|
|
|
|
|
-function getMessageTypeLabel(type) {
|
|
|
- return (messageTypeMap[type] || {}).label || type
|
|
|
+function handleBroadcast() {
|
|
|
+ broadcastForm.value.messageType = undefined
|
|
|
+ broadcastForm.value.targetRole = 'ALL'
|
|
|
+ broadcastForm.value.title = undefined
|
|
|
+ broadcastForm.value.content = undefined
|
|
|
+ broadcastOpen.value = true
|
|
|
}
|
|
|
|
|
|
-function getMessageTagType(type) {
|
|
|
- return (messageTypeMap[type] || {}).tagType || 'info'
|
|
|
+function submitBroadcast() {
|
|
|
+ proxy.$refs.broadcastRef.validate(valid => {
|
|
|
+ if (valid) {
|
|
|
+ // TODO: 调用API发送群发通知
|
|
|
+ proxy.$modal.msgSuccess('通知已发送')
|
|
|
+ broadcastOpen.value = false
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function formatRelativeTime(time) {
|
|
|
+ if (!time) return ''
|
|
|
+ const now = Date.now()
|
|
|
+ const t = new Date(time).getTime()
|
|
|
+ const diff = now - t
|
|
|
+ if (diff < 60000) return '刚刚'
|
|
|
+ if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
|
|
+ if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
|
|
+ if (diff < 172800000) return '昨天'
|
|
|
+ return proxy.parseTime(time)
|
|
|
+}
|
|
|
+
|
|
|
+// 自动刷新
|
|
|
+function startAutoRefresh() {
|
|
|
+ refreshTimer.value = setInterval(() => {
|
|
|
+ getUnreadCountApi()
|
|
|
+ }, 30000)
|
|
|
+}
|
|
|
+
|
|
|
+function stopAutoRefresh() {
|
|
|
+ if (refreshTimer.value) {
|
|
|
+ clearInterval(refreshTimer.value)
|
|
|
+ refreshTimer.value = null
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
getList()
|
|
|
getUnreadCountApi()
|
|
|
+ startAutoRefresh()
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ stopAutoRefresh()
|
|
|
})
|
|
|
</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.tab-badge {
|
|
|
+ margin-left: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.refresh-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ line-height: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-detail__content {
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: #F5F7FA;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.8;
|
|
|
+ color: #303133;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+</style>
|