✨ 站内消息.
This commit is contained in:
@@ -21,20 +21,21 @@ export interface MessageRecordResponse {
|
||||
relKey: string;
|
||||
title: string;
|
||||
content: string;
|
||||
contentHtml: string;
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询系统消息列表
|
||||
*/
|
||||
export function getMessageList(request: MessageQueryRequest) {
|
||||
export function getSystemMessageList(request: MessageQueryRequest) {
|
||||
return axios.post<Array<MessageRecordResponse>>('/infra/system-message/list', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询系统消息数量
|
||||
*/
|
||||
export function getMessageCount(queryUnread: boolean) {
|
||||
export function getSystemMessageCount(queryUnread: boolean) {
|
||||
return axios.get<Record<string, number>>('/infra/system-message/count', { params: { queryUnread } });
|
||||
}
|
||||
|
||||
@@ -48,27 +49,27 @@ export function checkHasUnreadMessage() {
|
||||
/**
|
||||
* 更新系统消息为已读
|
||||
*/
|
||||
export function updateMessageRead(id: number) {
|
||||
export function updateSystemMessageRead(id: number) {
|
||||
return axios.put('/infra/system-message/read', undefined, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全部系统消息为已读
|
||||
*/
|
||||
export function updateMessageReadAll(classify: string) {
|
||||
export function updateSystemMessageReadAll(classify: string) {
|
||||
return axios.put('/infra/system-message/read-all', undefined, { params: { classify } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除系统消息
|
||||
*/
|
||||
export function deleteMessage(id: number) {
|
||||
export function deleteSystemMessage(id: number) {
|
||||
return axios.delete('/infra/system-message/delete', { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已读的系统消息
|
||||
*/
|
||||
export function clearMessage(classify: string) {
|
||||
export function clearSystemMessage(classify: string) {
|
||||
return axios.delete('/infra/system-message/clear', { params: { classify } });
|
||||
}
|
||||
|
||||
@@ -80,11 +80,11 @@
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</li>
|
||||
<!-- 消息列表 -->
|
||||
<li v-if="false">
|
||||
<a-tooltip content="消息通知">
|
||||
<!-- 系统消息 -->
|
||||
<li>
|
||||
<a-tooltip content="系统消息" :show-arrow="false">
|
||||
<div class="message-box-trigger">
|
||||
<a-badge :count="9" dot>
|
||||
<a-badge :count="messageCount" dot>
|
||||
<a-button class="nav-btn"
|
||||
type="outline"
|
||||
shape="circle"
|
||||
@@ -95,9 +95,11 @@
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-popover trigger="click"
|
||||
:arrow-style="{ display: 'none' }"
|
||||
:content-style="{ padding: 0, minWidth: '400px' }"
|
||||
content-class="message-popover">
|
||||
content-class="message-popover"
|
||||
position="br"
|
||||
:show-arrow="false"
|
||||
:popup-style="{ marginLeft: '198px' }"
|
||||
:content-style="{ padding: 0, width: '498px' }">
|
||||
<div ref="messageRef" class="ref-btn" />
|
||||
<template #content>
|
||||
<message-box />
|
||||
@@ -202,7 +204,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { computed, inject, onMounted, onUnmounted, ref } from 'vue';
|
||||
import useLocale from '@/hooks/locale';
|
||||
import useUser from '@/hooks/user';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -214,6 +216,7 @@
|
||||
import { preferenceTipsKey } from './const';
|
||||
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants';
|
||||
import { openNewRoute } from '@/router';
|
||||
import { checkHasUnreadMessage } from '@/api/system/message';
|
||||
import SystemMenuTree from '@/components/system/menu/tree/index.vue';
|
||||
import MessageBox from '@/components/system/message-box/index.vue';
|
||||
import UpdatePasswordModal from '@/components/user/user/update-password-modal/index.vue';
|
||||
@@ -258,6 +261,9 @@
|
||||
const messageRef = ref();
|
||||
// 语言
|
||||
const localeRef = ref();
|
||||
// 消息数量
|
||||
const messageCount = ref(0);
|
||||
const messageIntervalId = ref();
|
||||
|
||||
// 打开应用设置
|
||||
const openAppSetting = inject(openAppSettingKey) as () => void;
|
||||
@@ -302,6 +308,18 @@
|
||||
await logout();
|
||||
};
|
||||
|
||||
// 获取是否有未读的消息
|
||||
const pullHasUnreadMessage = () => {
|
||||
// 有未读的消息直接返回
|
||||
if (messageCount.value) {
|
||||
return;
|
||||
}
|
||||
// 查询
|
||||
checkHasUnreadMessage().then(({ data }) => {
|
||||
messageCount.value = data ? 1 : 0;
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭偏好提示
|
||||
const closePreferenceTip = (ack: boolean) => {
|
||||
tippedPreference.value = false;
|
||||
@@ -310,6 +328,18 @@
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 查询未读消息
|
||||
pullHasUnreadMessage();
|
||||
// 注册未读消息轮询
|
||||
messageIntervalId.value = setInterval(pullHasUnreadMessage, 30000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理消息轮询
|
||||
clearInterval(messageIntervalId.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
20
orion-ops-ui/src/components/system/message-box/const.ts
Normal file
20
orion-ops-ui/src/components/system/message-box/const.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 消息状态
|
||||
export const MessageStatus = {
|
||||
UNREAD: 0,
|
||||
READ: 1,
|
||||
};
|
||||
|
||||
// 查询数量
|
||||
export const messageLimit = 15;
|
||||
|
||||
// 默认消息分类 通知
|
||||
export const defaultClassify = 'NOTICE';
|
||||
|
||||
// 消息分类 字典项
|
||||
export const messageClassifyKey = 'messageClassify';
|
||||
|
||||
// 消息类型 字典项
|
||||
export const messageTypeKey = 'messageType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [messageClassifyKey, messageTypeKey];
|
||||
@@ -1,108 +1,246 @@
|
||||
<template>
|
||||
<a-spin style="display: block" :loading="loading">
|
||||
<a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
|
||||
<a-tab-pane v-for="item in tabList" :key="item.key">
|
||||
<template #title>
|
||||
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
|
||||
<div class="full">
|
||||
<!-- 消息分类 -->
|
||||
<a-spin class="message-classify-container"
|
||||
:hide-icon="true"
|
||||
:loading="fetchLoading">
|
||||
<a-tabs v-model:activeKey="currentClassify"
|
||||
type="rounded"
|
||||
:hide-content="true"
|
||||
@change="loadClassifyMessage">
|
||||
<!-- 消息列表 -->
|
||||
<a-tab-pane v-for="item in toOptions(messageClassifyKey)"
|
||||
:key="item.value">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="usn">{{ item.label }} ({{ classifyCount[item.value] || 0 }})</span>
|
||||
</template>
|
||||
<!-- 消息列表 -->
|
||||
</a-tab-pane>
|
||||
<!-- 右侧操作 -->
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<!-- 状态 -->
|
||||
<a-switch v-model="queryUnread"
|
||||
type="round"
|
||||
checked-text="未读"
|
||||
unchecked-text="全部"
|
||||
@change="changeMessageStatus" />
|
||||
<!-- 全部已读 -->
|
||||
<a-button class="header-button"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="setAllRead">
|
||||
全部已读
|
||||
</a-button>
|
||||
<!-- 清空 -->
|
||||
<a-button class="header-button"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="clearAllMessage">
|
||||
清空
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-result v-if="!renderList.length" status="404">
|
||||
<template #subtitle>暂无内容</template>
|
||||
</a-result>
|
||||
<list :render-list="renderList"
|
||||
:unread-count="unreadCount"
|
||||
@item-click="handleItemClick" />
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-button type="text" @click="emptyList">
|
||||
清空
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
</a-tabs>
|
||||
</a-spin>
|
||||
<!-- 消息列表 -->
|
||||
<list :fetch-loading="fetchLoading"
|
||||
:message-loading="messageLoading"
|
||||
:has-more="hasMore"
|
||||
:message-list="messageList"
|
||||
@load="loadMessage"
|
||||
@click="clickMessage"
|
||||
@view="viewMessage"
|
||||
@delete="deleteMessage" />
|
||||
<!-- 模态框 -->
|
||||
<modal ref="modalRef"
|
||||
@delete="deleteMessage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'messageBox'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MessageRecord, MessageListType } from '@/api/system/message';
|
||||
import { ref, reactive, toRefs, computed } from 'vue';
|
||||
import { queryMessageList, setMessageStatus } from '@/api/system/message';
|
||||
import type { MessageRecordResponse } from '@/api/system/message';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import {
|
||||
clearSystemMessage,
|
||||
deleteSystemMessage,
|
||||
getSystemMessageCount,
|
||||
getSystemMessageList,
|
||||
updateSystemMessageRead,
|
||||
updateSystemMessageReadAll
|
||||
} from '@/api/system/message';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys, messageClassifyKey, messageTypeKey, defaultClassify, messageLimit, MessageStatus } from './const';
|
||||
import List from './list.vue';
|
||||
import Modal from './modal.vue';
|
||||
import { clearHtmlTag, replaceHtmlTag } from '@/utils';
|
||||
|
||||
interface TabItem {
|
||||
key: string;
|
||||
title: string;
|
||||
avatar?: string;
|
||||
}
|
||||
const { loading: fetchLoading, setLoading: setFetchLoading } = useLoading();
|
||||
const { loading: messageLoading, setLoading: setMessageLoading } = useLoading();
|
||||
const { loadKeys, toOptions, getDictValue } = useDictStore();
|
||||
const router = useRouter();
|
||||
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const currentClassify = ref(defaultClassify);
|
||||
const queryUnread = ref(false);
|
||||
const classifyCount = ref<Record<string, number>>({});
|
||||
const messageList = ref<Array<MessageRecordResponse>>([]);
|
||||
const hasMore = ref(true);
|
||||
const modalRef = ref();
|
||||
|
||||
const messageType = ref('message');
|
||||
const messageData = reactive<{
|
||||
renderList: MessageRecord[];
|
||||
messageList: MessageRecord[];
|
||||
}>({
|
||||
renderList: [],
|
||||
messageList: [],
|
||||
});
|
||||
toRefs(messageData);
|
||||
const tabList: TabItem[] = [
|
||||
{
|
||||
key: 'message',
|
||||
title: '消息',
|
||||
},
|
||||
{
|
||||
key: 'notice',
|
||||
title: '通知',
|
||||
},
|
||||
{
|
||||
key: 'todo',
|
||||
title: '待办',
|
||||
},
|
||||
];
|
||||
// 修改消息状态
|
||||
const changeMessageStatus = async () => {
|
||||
hasMore.value = true;
|
||||
messageList.value = [];
|
||||
// 查询数量
|
||||
queryMessageCount();
|
||||
// 加载列表
|
||||
await loadMessage();
|
||||
};
|
||||
|
||||
async function fetchSourceData() {
|
||||
setLoading(true);
|
||||
// 获取数量
|
||||
const queryMessageCount = async () => {
|
||||
setFetchLoading(true);
|
||||
try {
|
||||
const { data } = await queryMessageList();
|
||||
messageData.messageList = data;
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
const { data } = await getSystemMessageCount(queryUnread.value);
|
||||
classifyCount.value = data;
|
||||
} catch (ex) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setFetchLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function readMessage(data: MessageListType) {
|
||||
const ids = data.map((item) => item.id);
|
||||
await setMessageStatus({ ids });
|
||||
await fetchSourceData();
|
||||
}
|
||||
// 查询分类消息
|
||||
const loadClassifyMessage = async () => {
|
||||
hasMore.value = true;
|
||||
messageList.value = [];
|
||||
await loadMessage();
|
||||
};
|
||||
|
||||
const renderList = computed(() => {
|
||||
return messageData.messageList.filter(
|
||||
(item) => messageType.value === item.type
|
||||
);
|
||||
// 加载消息
|
||||
const loadMessage = async () => {
|
||||
hasMore.value = true;
|
||||
setFetchLoading(true);
|
||||
try {
|
||||
const maxId = messageList.value.length
|
||||
? messageList.value[messageList.value.length - 1].id
|
||||
: undefined;
|
||||
// 查询数据
|
||||
const { data } = await getSystemMessageList({
|
||||
limit: messageLimit,
|
||||
classify: currentClassify.value,
|
||||
queryUnread: queryUnread.value,
|
||||
maxId,
|
||||
});
|
||||
data.forEach(s => {
|
||||
messageList.value.push({
|
||||
...s,
|
||||
content: clearHtmlTag(s.content),
|
||||
contentHtml: replaceHtmlTag(s.content),
|
||||
});
|
||||
});
|
||||
hasMore.value = data.length === messageLimit;
|
||||
} catch (ex) {
|
||||
} finally {
|
||||
setFetchLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置全部已读
|
||||
const setAllRead = async () => {
|
||||
setMessageLoading(true);
|
||||
try {
|
||||
// 设置为已读
|
||||
await updateSystemMessageReadAll(currentClassify.value);
|
||||
// 修改状态
|
||||
messageList.value.forEach(s => s.status = MessageStatus.READ);
|
||||
} catch (ex) {
|
||||
} finally {
|
||||
setMessageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理已读消息
|
||||
const clearAllMessage = async () => {
|
||||
setMessageLoading(true);
|
||||
try {
|
||||
// 清理消息
|
||||
await clearSystemMessage(currentClassify.value);
|
||||
} catch (ex) {
|
||||
} finally {
|
||||
setMessageLoading(false);
|
||||
}
|
||||
// 查询消息
|
||||
await changeMessageStatus();
|
||||
};
|
||||
|
||||
// 点击消息
|
||||
const clickMessage = (message: MessageRecordResponse) => {
|
||||
// 设置为已读
|
||||
if (message.status === MessageStatus.UNREAD) {
|
||||
updateSystemMessageRead(message.id);
|
||||
message.status = MessageStatus.READ;
|
||||
}
|
||||
const redirectComponent = getDictValue(messageTypeKey, message.type, 'redirectComponent');
|
||||
if (redirectComponent && redirectComponent !== '0') {
|
||||
// 跳转组件
|
||||
router.push({ name: redirectComponent, query: { key: message.relKey } });
|
||||
} else {
|
||||
// 打开消息模态框
|
||||
modalRef.value.open(message);
|
||||
}
|
||||
};
|
||||
|
||||
// 查看消息
|
||||
const viewMessage = async (message: MessageRecordResponse) => {
|
||||
setMessageLoading(true);
|
||||
try {
|
||||
// 设置为已读
|
||||
if (message.status === MessageStatus.UNREAD) {
|
||||
await updateSystemMessageRead(message.id);
|
||||
message.status = MessageStatus.READ;
|
||||
}
|
||||
// 打开消息模态框
|
||||
modalRef.value.open(message);
|
||||
} catch (ex) {
|
||||
} finally {
|
||||
setMessageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除消息
|
||||
const deleteMessage = async (message: MessageRecordResponse) => {
|
||||
setMessageLoading(true);
|
||||
try {
|
||||
// 删除消息
|
||||
await deleteSystemMessage(message.id);
|
||||
// 减少数量
|
||||
classifyCount.value[currentClassify.value] -= 1;
|
||||
// 移除
|
||||
const index = messageList.value.findIndex(s => s.id === message.id);
|
||||
messageList.value.splice(index, 1);
|
||||
} catch (ex) {
|
||||
} finally {
|
||||
setMessageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载字典值
|
||||
onMounted(() => {
|
||||
loadKeys(dictKeys);
|
||||
});
|
||||
const unreadCount = computed(() => {
|
||||
return renderList.value.filter((item) => !item.status).length;
|
||||
});
|
||||
const getUnreadList = (type: string) => {
|
||||
const list = messageData.messageList.filter(
|
||||
(item) => item.type === type && !item.status
|
||||
);
|
||||
return list;
|
||||
};
|
||||
const formatUnreadLength = (type: string) => {
|
||||
const list = getUnreadList(type);
|
||||
return list.length ? `(${list.length})` : '';
|
||||
};
|
||||
const handleItemClick = (items: MessageListType) => {
|
||||
if (renderList.value.length) readMessage([...items]);
|
||||
};
|
||||
const emptyList = () => {
|
||||
messageData.messageList = [];
|
||||
};
|
||||
fetchSourceData();
|
||||
|
||||
// 获取消息
|
||||
onMounted(changeMessageStatus);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -110,20 +248,23 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-list-item-meta) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
padding: 14px 0 12px 16px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.arco-result-subtitle {
|
||||
color: rgb(var(--gray-6));
|
||||
.message-classify-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
|
||||
.header-button {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,149 +1,216 @@
|
||||
<template>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item v-for="item in renderList"
|
||||
:key="item.id"
|
||||
action-layout="vertical"
|
||||
:style="{
|
||||
opacity: item.status ? 0.5 : 1,
|
||||
}">
|
||||
<template #extra>
|
||||
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||
</template>
|
||||
<div class="item-wrap" @click="onItemClick(item)">
|
||||
<a-list-item-meta>
|
||||
<template v-if="item.avatar" #avatar>
|
||||
<a-avatar shape="circle">
|
||||
<img v-if="item.avatar" :src="item.avatar" />
|
||||
<icon-desktop v-else />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<a-space :size="4">
|
||||
<span>{{ item.title }}</span>
|
||||
<a-typography-text type="secondary">
|
||||
{{ item.subTitle }}
|
||||
</a-typography-text>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #description>
|
||||
<div>
|
||||
<a-typography-paragraph :ellipsis="{
|
||||
rows: 1,
|
||||
}">
|
||||
{{ item.content }}
|
||||
</a-typography-paragraph>
|
||||
<a-typography-text v-if="item.type === 'message'"
|
||||
class="time-text">
|
||||
{{ item.time }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</div>
|
||||
</a-list-item>
|
||||
<template #footer>
|
||||
<a-space fill
|
||||
:size="0"
|
||||
:class="{ 'add-border-top': renderList.length < showMax }">
|
||||
<div class="footer-wrap">
|
||||
<a-link @click="allRead">全部已读</a-link>
|
||||
</div>
|
||||
<div class="footer-wrap">
|
||||
<a-link>查看更多</a-link>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
<div v-if="renderList.length && renderList.length < 3"
|
||||
:style="{ height: (showMax - renderList.length) * 86 + 'px' }">
|
||||
<!-- 消息列表 -->
|
||||
<a-spin class="message-list-container" :loading="messageLoading">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="!messageList.length && fetchLoading">
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton class="skeleton-wrapper" :animation="true">
|
||||
<a-skeleton-line :rows="3"
|
||||
:line-height="86"
|
||||
:line-spacing="8" />
|
||||
</a-skeleton>
|
||||
</div>
|
||||
</a-list>
|
||||
<!-- 无数据 -->
|
||||
<div v-else-if="!messageList.length && !fetchLoading">
|
||||
<a-result status="404">
|
||||
<template #subtitle>暂无内容</template>
|
||||
</a-result>
|
||||
</div>
|
||||
<!-- 消息容器 -->
|
||||
<div v-else class="message-list-wrapper">
|
||||
<a-scrollbar style="overflow-y: auto; height: 100%;">
|
||||
<!-- 消息列表-->
|
||||
<div v-for="message in messageList"
|
||||
class="message-item"
|
||||
:class="[ message.status === MessageStatus.READ ? 'message-item-read' : 'message-item-unread' ]"
|
||||
@click="emits('click', message)">
|
||||
<!-- 标题 -->
|
||||
<div class="message-item-title">
|
||||
<!-- 标题 -->
|
||||
<div class="message-item-title-text text-ellipsis" :title="message.title">
|
||||
{{ message.title }}
|
||||
</div>
|
||||
<!-- tag -->
|
||||
<div class="message-item-title-status">
|
||||
<template v-if="getDictValue(messageTypeKey, message.type, 'tagVisible', false)">
|
||||
<a-tag size="mini" :color="getDictValue(messageTypeKey, message.type, 'tagColor')">
|
||||
{{ getDictValue(messageTypeKey, message.type, 'tagLabel') }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<div class="message-item-title-actions">
|
||||
<!-- 查看 -->
|
||||
<a-button class="mr4"
|
||||
size="mini"
|
||||
type="text"
|
||||
@click.stop="emits('view', message)">
|
||||
查看
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-button size="mini"
|
||||
type="text"
|
||||
status="danger"
|
||||
@click.stop="emits('delete', message)">
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文本 -->
|
||||
<div v-html="message.contentHtml"
|
||||
class="message-item-content text-ellipsis"
|
||||
:title="message.content" />
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="fetchLoading"
|
||||
class="skeleton-wrapper"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="3"
|
||||
:line-height="86"
|
||||
:line-spacing="8" />
|
||||
</a-skeleton>
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="hasMore" class="load-more-wrapper">
|
||||
<a-button size="small"
|
||||
:fetchLoading="fetchLoading"
|
||||
@click="() => emits('load')">
|
||||
加载更多
|
||||
</a-button>
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'messageBoxList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MessageListType, MessageRecord } from '@/api/system/message';
|
||||
import type { MessageRecordResponse } from '@/api/system/message';
|
||||
import { MessageStatus, messageTypeKey } from './const';
|
||||
import { useDictStore } from '@/store';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
renderList: MessageListType;
|
||||
unreadCount?: number;
|
||||
}>(), {
|
||||
unreadCount: 0,
|
||||
});
|
||||
const emits = defineEmits(['load', 'click', 'view', 'delete']);
|
||||
const props = defineProps<{
|
||||
fetchLoading: boolean;
|
||||
messageLoading: boolean;
|
||||
hasMore: boolean;
|
||||
messageList: Array<MessageRecordResponse>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['itemClick']);
|
||||
const allRead = () => {
|
||||
emit('itemClick', [...props.renderList]);
|
||||
};
|
||||
const { getDictValue } = useDictStore();
|
||||
|
||||
const onItemClick = (item: MessageRecord) => {
|
||||
if (!item.status) {
|
||||
emit('itemClick', [item]);
|
||||
}
|
||||
};
|
||||
const showMax = 3;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-list) {
|
||||
.arco-list-item {
|
||||
min-height: 86px;
|
||||
border-bottom: 1px solid rgb(var(--gray-3));
|
||||
@gap: 8px;
|
||||
@message-height: 86px;
|
||||
@actions-width: 82px;
|
||||
|
||||
.skeleton-wrapper {
|
||||
padding: 8px 12px 0 12px;
|
||||
}
|
||||
|
||||
.message-list-container {
|
||||
width: 100%;
|
||||
height: 344px;
|
||||
display: block;
|
||||
|
||||
.message-list-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arco-list-item-extra {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
.load-more-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-item {
|
||||
height: @message-height;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
color: var(--color-text-1);
|
||||
cursor: pointer;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
width: calc(100% - @actions-width - @gap);
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
&-status {
|
||||
width: @actions-width;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
width: @actions-width;
|
||||
display: none;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
|
||||
button {
|
||||
padding: 0 6px !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arco-list-item-meta-content {
|
||||
flex: 1;
|
||||
&-content {
|
||||
color: var(--color-text-1);
|
||||
text-overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
.message-item:hover {
|
||||
background: var(--color-fill-1);
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
|
||||
.arco-empty {
|
||||
.message-item-title-status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-list-footer {
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
border-top: none;
|
||||
|
||||
.arco-space-item {
|
||||
width: 100%;
|
||||
border-right: 1px solid rgb(var(--gray-3));
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.add-border-top {
|
||||
border-top: 1px solid rgb(var(--gray-3));
|
||||
}
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.arco-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.add-border {
|
||||
border-top: 1px solid rgb(var(--gray-3));
|
||||
.message-item-title-actions {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-item-read {
|
||||
.message-item-title-text, .message-item-title-status, .message-item-content {
|
||||
opacity: .65;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-scrollbar) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.arco-scrollbar-track-direction-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
49
orion-ops-ui/src/components/system/message-box/modal.vue
Normal file
49
orion-ops-ui/src/components/system/message-box/modal.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
title-align="start"
|
||||
:title="record.title"
|
||||
:top="80"
|
||||
:width="720"
|
||||
:align-center="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="删除"
|
||||
:hide-cancel="true"
|
||||
:ok-button-props="{ status: 'danger' }"
|
||||
:body-style="{ padding: '20px' }"
|
||||
@ok="emits('delete', record)">
|
||||
<div class="content" v-html="record.contentHtml" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'messageBoxModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MessageRecordResponse } from '@/api/system/message';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emits = defineEmits(['delete']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
|
||||
const record = ref<MessageRecordResponse>({} as MessageRecordResponse);
|
||||
|
||||
// 打开
|
||||
const open = (message: MessageRecordResponse) => {
|
||||
record.value = message;
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.content {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -28,10 +28,14 @@
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 跳转日志
|
||||
onMounted(async () => {
|
||||
const idParam = route.query.id;
|
||||
const keyParam = route.query.key;
|
||||
if (idParam) {
|
||||
await panel.value?.openLog(Number.parseInt(idParam as string));
|
||||
} else if (keyParam) {
|
||||
await panel.value?.openLog(Number.parseInt(keyParam as string));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -26,10 +26,13 @@
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from '@/components/exec/log/const';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getExecCommandLog } from '@/api/exec/exec-command-log';
|
||||
import ExecCommandPanel from './components/exec-command-panel.vue';
|
||||
import ExecLogPanel from '@/components/exec/log/panel/index.vue';
|
||||
|
||||
const { visible: logVisible, setVisible: setLogVisible } = useVisible();
|
||||
const route = useRoute();
|
||||
|
||||
const log = ref();
|
||||
|
||||
@@ -41,12 +44,30 @@
|
||||
});
|
||||
};
|
||||
|
||||
// 打开日志
|
||||
const openLogWithId = async (id: number) => {
|
||||
setLogVisible(true);
|
||||
// 查询日志
|
||||
const { data } = await getExecCommandLog(id);
|
||||
openLog(data);
|
||||
};
|
||||
|
||||
// 加载字典值
|
||||
onMounted(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 跳转日志
|
||||
onMounted(async () => {
|
||||
const idParam = route.query.id;
|
||||
const keyParam = route.query.key;
|
||||
if (idParam) {
|
||||
await openLogWithId(Number.parseInt(idParam as string));
|
||||
} else if (keyParam) {
|
||||
await openLogWithId(Number.parseInt(keyParam as string));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
Reference in New Issue
Block a user