第一次提交

This commit is contained in:
2026-04-03 09:56:14 +08:00
commit 60e2c8debd
3598 changed files with 746659 additions and 0 deletions

View File

@@ -0,0 +1,393 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
title="查看详情"
:width="800"
:minHeight="600"
:showCancelBtn="false"
:showOkBtn="false"
:height="88"
:destroyOnClose="true"
@visible-change="handleVisibleChange"
>
<template #title>
<span class="basic-title">查看详情</span>
<div class="print-btn" @click="onPrinter">
<Icon icon="ant-design:printer-filled" />
<span class="print-text">打印</span>
</div>
</template>
<a-card class="daily-article">
<a-card-meta :title="content.titile">
<template #description>
<div class="article-desc">
<span>发布人{{ content.sender }}</span>
<span>发布时间{{ content.sendTime }}</span>
<span v-if="content.visitsNum">
<a-tooltip placement="top" title="访问次数" :autoAdjustOverflow="true">
<eye-outlined class="item-icon" /> {{ content.visitsNum }}
</a-tooltip>
</span>
</div>
</template>
</a-card-meta>
<a-divider />
<div v-html="content.msgContent" class="article-content"></div>
<div>
<a-button v-if="hasHref" @click="jumpToHandlePage">前往办理<ArrowRightOutlined /></a-button>
</div>
</a-card>
<template v-if="noticeFiles && noticeFiles.length > 0">
<div class="files-title">相关附件</div>
<template v-for="(file, index) in noticeFiles" :key="index">
<div class="files-area">
<div class="files-area-text">
<span>
<paper-clip-outlined />
<a
target="_blank"
rel="noopener noreferrer"
:title="file.fileName"
:href="getFileAccessHttpUrl(file.filePath)"
class="ant-upload-list-item-name"
>{{ file.fileName }}</a
>
</span>
</div>
<div class="files-area-operate">
<download-outlined class="item-icon" @click="handleDownloadFile(file.filePath)" />
<eye-outlined class="item-icon" @click="handleViewFile(file.filePath)" />
</div>
</div>
</template>
<a v-if="noticeFiles.length > 1" :href="downLoadFiles + '?id=' + content.id + '&token=' + getToken()" target="_blank" style="margin: 15px 6px; color: #5ac0fa">
<download-outlined class="item-icon" style="margin-right: 5px" /><span>批量下载所有附件</span>
</a>
</template>
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { ArrowRightOutlined, PaperClipOutlined, DownloadOutlined, EyeOutlined } from '@ant-design/icons-vue';
import { addVisitsNum } from '@/views/system/notice/notice.api';
import { useRouter } from 'vue-router';
import xss from 'xss';
import { options } from './XssWhiteList';
import { ref, unref } from 'vue';
import { getElectronFileUrl, getFileAccessHttpUrl } from '@/utils/common/compUtils';
import { useGlobSetting } from '@/hooks/setting';
import { encryptByBase64 } from '@/utils/cipher';
import { getToken } from '@/utils/auth';
import {defHttp} from "@/utils/http/axios";
import {$electron} from "@/electron";
const router = useRouter();
const glob = useGlobSetting();
const isUpdate = ref(true);
const content = ref<any>({});
const noticeFiles = ref([]);
/**
* 下载文件路径
*/
const downLoadFiles = `${glob.domainUrl}/sys/annountCement/downLoadFiles`;
const emit = defineEmits(['close', 'register']);
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
noticeFiles.value = [];
if (unref(isUpdate)) {
//data.record.msgContent = '<p>2323</p><input onmouseover=alert(1)>xss test';
// 代码逻辑说明: VUEN-1702 【禁止问题】sql注入漏洞
if (data.record.msgContent) {
// 代码逻辑说明: 【QQYUN-7049】3.6.0版本 通知公告中发布的富文本消息,在我的消息中查看没有样式---
data.record.msgContent = xss(data.record.msgContent, options);
}
// 代码逻辑说明: [QQYUN-12521]通知公告消息增加访问量
if (!data.record?.busId) {
await addVisitsNum({ id: data.record.id });
}
content.value = data.record;
if(content.value.sender){
const userInfo = await defHttp.get({ url: '/sys/user/queryUserComponentData?isMultiTranslate=true', params: { username: content.value.sender } });
content.value.sender = userInfo && userInfo?.records && userInfo?.records.length>0
?userInfo.records.find((item) => item.username === content.value.sender)?.realname : content.value.sender;
}
console.log('data---------->>>', data);
if (data.record?.files && data.record?.files.length > 0) {
noticeFiles.value = data.record.files.split(',').map((item) => {
return {
fileName: item.split('/').pop(),
filePath: item,
};
});
}
showHrefButton();
}
});
const hasHref = ref(false);
//查看消息详情可以跳转
function showHrefButton() {
if (content.value.busId) {
hasHref.value = true;
}
}
//跳转至办理页面
function jumpToHandlePage() {
let temp: any = content.value;
if (temp.busId) {
//这个busId是 任务ID
let jsonStr = temp.msgAbstract;
let query = {};
try {
if (jsonStr) {
let temp = JSON.parse(jsonStr);
if (temp) {
Object.keys(temp).map((k) => {
query[k] = temp[k];
});
}
}
} catch (e) {
console.log('参数解析异常', e);
}
console.log('query', query, jsonStr);
console.log('busId', temp.busId);
if (Object.keys(query).length > 0) {
// taskId taskDefKey procInsId
router.push({ path: '/task/handle/' + temp.busId, query: query });
} else {
router.push({ path: '/task/handle/' + temp.busId });
}
}
closeModal();
}
//打印
function onPrinter() {
// 获取要打印的内容
const printContent = document.querySelector('.daily-article');
if (!printContent) return;
// 创建一个iframe来处理打印
const printFrame = document.createElement('iframe');
printFrame.style.position = 'absolute';
printFrame.style.width = '0';
printFrame.style.height = '0';
printFrame.style.border = 'none';
printFrame.style.left = '-9999px';
printFrame.onload = function () {
const frameDoc = printFrame.contentDocument || printFrame.contentWindow?.document;
if (!frameDoc) return;
// 复制内容到iframe
const clone = printContent.cloneNode(true);
frameDoc.body.appendChild(clone);
// 添加打印样式
const style = frameDoc.createElement('style');
style.innerHTML = `
body {
margin: 0;
padding: 15px;
font-family: Arial, sans-serif;
}
img {
max-width: 100%;
height: auto;
}
@page {
size: auto;
margin: 15mm;
}
@media print {
body {
padding: 0;
}
}
.ant-card-meta-detail {
display: flex !important ;
justify-content: center !important;
align-items: center !important;
flex-direction: column !important;
}
.ant-card-meta-title {
font-size: 22px !important;
color: rgba(51, 51, 51, 0.88);
font-weight: 600;
font-size: 16px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.ant-card .ant-card-meta-description {
color: rgba(51, 51, 51, 0.45);
}
`;
frameDoc.head.appendChild(style);
// 确保图片加载完成
const images = frameDoc.getElementsByTagName('img');
let imagesToLoad = images.length;
const printWhenReady = () => {
if (imagesToLoad === 0) {
setTimeout(() => {
printFrame.contentWindow?.focus();
printFrame.contentWindow?.print();
document.body.removeChild(printFrame);
}, 300);
}
};
if (imagesToLoad === 0) {
printWhenReady();
} else {
Array.from(images).forEach((img) => {
img.onload = () => {
imagesToLoad--;
printWhenReady();
};
// 处理可能已经缓存的图片
if (img.complete && img.naturalWidth !== 0) {
imagesToLoad--;
printWhenReady();
}
});
}
};
document.body.appendChild(printFrame);
}
/**
* 下载文件
* @param filePath
*/
function handleDownloadFile(filePath) {
window.open(getFileAccessHttpUrl(filePath), '_blank');
}
/**
* 预览文件
* @param filePath
*/
function handleViewFile(filePath) {
if (filePath) {
console.log('glob.onlineUrl', glob.viewUrl);
let url = encodeURIComponent(encryptByBase64(filePath));
let previewUrl = `${glob.viewUrl}?url=` + url;
//update-begin-author:liusq---date:2025-12-16--for: JHHB-1139桌面端 文件预览统一修改
if($electron.isElectron()){
previewUrl = getElectronFileUrl(filePath);
}
//update-end-author:liusq---date:2025-12-16--for: JHHB-1139桌面端 文件预览统一修改
window.open(previewUrl, '_blank');
}
}
function handleVisibleChange(visible: boolean) {
if (!visible) {
emit('close');
}
}
</script>
<style scoped lang="less">
.daily-article {
:deep(.ant-card-meta-detail) {
display: flex !important;
justify-content: center !important;
align-items: center !important;
flex-direction: column !important;
}
:deep(.ant-card-meta-detail .ant-card-meta-title) {
font-size: 22px !important;
}
}
.print-btn {
position: absolute;
right: 100px;
top: 20px;
cursor: pointer;
color: #a3a3a5;
z-index: 999;
.print-text {
margin-left: 5px;
font-size: 14px;
}
&:hover {
color: #40a9ff;
}
}
.detail-iframe {
border: 0;
width: 100%;
height: 100%;
min-height: 600px;
}
.files-title {
font-size: 16px;
margin: 10px;
font-weight: 600;
color: #333;
}
.files-area {
display: flex;
align-items: center;
justify-content: flex-start;
margin: 6px;
&:hover {
background-color: #f5f5f5;
}
.files-area-text {
display: flex;
.ant-upload-list-item-name {
margin: 0 6px;
color: #56befa;
}
}
.files-area-operate {
display: flex;
margin-left: 10px;
.item-icon {
cursor: pointer;
margin: 0 6px;
&:hover {
color: #56befa;
}
}
}
}
.article-desc {
display: flex;
align-items: center;
span:not(:first-child) {
margin-left: 5px;
}
}
/* 确保打印内容中的图片有最大宽度限制 */
.article-content img {
max-width: 100%;
height: auto;
}
.basic-title{
position: relative;
display: flex;
padding-left: 7px;
font-size: 16px;
font-weight: 500;
line-height: 24px;
color: rgba(0,0,0,0.88);
cursor: move;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<component :is="currentModal" :formData="formData" v-model:visible="modalVisible"></component>
</template>
<script setup lang="ts" name="dynamic-notice">
import { ref, shallowRef, ComponentOptions, nextTick, defineAsyncComponent } from 'vue';
const props = defineProps({
path: { type: String, default: '' },
formData: { type: Object, default: {} },
});
const modalVisible = ref<Boolean>(false);
const currentModal = shallowRef<Nullable<ComponentOptions>>(null);
const formData = ref<any>(props.formData);
const componentType = {
};
/**
* 跟换组件和传值事件
*/
function detail() {
setTimeout(() => {
if (props.path) {
nextTick(() => {
currentModal.value = componentType[props.path];
formData.value = props.formData;
modalVisible.value = true;
});
}
}, 200);
}
defineExpose({
detail,
});
</script>

View File

@@ -0,0 +1,41 @@
//xss攻击白名单列表
export const options = {
whiteList: {
h1: ['style'],
h2: ['style'],
h3: ['style'],
h4: ['style'],
h5: ['style'],
h6: ['style'],
hr: ['style'],
span: ['style'],
strong: ['style'],
b: ['style'],
i: ['style'],
br: [],
p: ['style'],
pre: ['style'],
code: ['style'],
a: ['style', 'target', 'href', 'title', 'rel'],
img: ['style', 'src', 'title','width','height'],
div: ['style'],
table: ['style', 'width', 'border', 'height'],
tr: ['style'],
td: ['style', 'width', 'colspan'],
th: ['style', 'width', 'colspan'],
tbody: ['style'],
ul: ['style'],
li: ['style'],
ol: ['style'],
dl: ['style'],
dt: ['style'],
em: ['style'],
cite: ['style'],
section: ['style'],
header: ['style'],
footer: ['style'],
blockquote: ['style'],
audio: ['autoplay', 'controls', 'loop', 'preload', 'src'],
video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width'],
},
};

View File

@@ -0,0 +1,210 @@
<template>
<div>
<BasicTable @register="registerTable" :searchInfo="searchInfo" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" @click="handlerReadAllMsg">全部标注已读</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button>
批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<DetailModal @register="register" />
<keep-alive>
<component v-if="currentModal" v-bind="bindParams" :key="currentModal" :is="currentModal" @register="modalRegCache[currentModal].register" />
</keep-alive>
</div>
</template>
<script lang="ts" name="monitor-mynews" setup>
import {ref, onMounted, unref} from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import DetailModal from './DetailModal.vue';
import { getMyNewsList, editCementSend, syncNotic, readAllMsg, getOne, deleteAnnSend, deleteBatchAnnSend } from './mynews.api';
import { columns, searchFormSchema } from './mynews.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { getToken } from '/@/utils/auth';
import { useModal } from '/@/components/Modal';
import { useGlobSetting } from '/@/hooks/setting';
const glob = useGlobSetting();
const { createMessage } = useMessage();
const checkedKeys = ref<Array<string | number>>([]);
const content = ref({});
const searchInfo = { logType: '1' };
const [register, { openModal: openDetail }] = useModal();
import { useListPage } from '/@/hooks/system/useListPage';
import { getLogList } from '/@/views/monitor/log/log.api';
import { useRouter } from 'vue-router';
import { useAppStore } from '/@/store/modules/app';
import { useMessageHref } from '/@/views/system/message/components/useSysMessage';
const appStore = useAppStore();
const router = useRouter();
const { currentRoute } = useRouter();
const { goPage, currentModal, modalRegCache, bindParams } = useMessageHref();
// 代码逻辑说明: 【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
const querystring = currentRoute.value.query;
const findItem: any = searchFormSchema.find((item: any) => item.field === 'msgCategory');
if (findItem) {
if (querystring?.msgCategory) {
findItem.componentProps.defaultValue = querystring.msgCategory
} else if (querystring.noticeType) {
findItem.componentProps.defaultValue = querystring.noticeType;
} else {
findItem.componentProps.defaultValue = null
}
}
const { prefixCls, tableContext } = useListPage({
designScope: 'mynews-list',
tableProps: {
title: '我的消息',
api: getMyNewsList,
columns: columns,
formConfig: {
schemas: searchFormSchema,
// 代码逻辑说明: 【TV360X-545】我的消息列表不能通过时间范围查询---
fieldMapToTime: [['sendTime', ['sendTimeBegin', 'sendTimeEnd'], 'YYYY-MM-DD']],
},
beforeFetch: (params) => {
// 代码逻辑说明: 【QQYUN-13058】我的消息区分类型且支持根据url参数查询类型
if (params.msgCategory) {
if (['1', '2'].includes(params.msgCategory)) {
params.msgCategory = params.msgCategory;
} else {
params.noticeType = params.msgCategory;
delete params.msgCategory;
}
} else {
if (querystring?.msgCategory) {
params.msgCategory = querystring.msgCategory;
} else if (querystring.noticeType) {
params.noticeType = querystring.noticeType;
}
}
return params;
},
},
});
const [registerTable, { reload }, { rowSelection, selectedRows, selectedRowKeys }] = tableContext;
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '查看',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record.id),
},
ifShow: record.readFlag === 1
}
];
}
/**
* 查看
*/
function handleDetail(record) {
let anntId = record.anntId;
editCementSend({ anntId: anntId }).then((res) => {
reload();
syncNotic({ anntId: anntId });
});
const openModalFun = ()=>{
openDetail(true, {
record,
isUpdate: true,
});
}
goPage(record, openModalFun);
}
// 日志类型
function callback(key) {
searchInfo.logType = key;
reload();
}
//全部标记已读
function handlerReadAllMsg() {
readAllMsg({}, reload);
}
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
// 代码逻辑说明: 消息跳转,打开详情表单
onMounted(()=>{
initHrefModal();
});
function initHrefModal(){
let params = appStore.getMessageHrefParams;
if(params){
let anntId = params.id;
if(anntId){
editCementSend({ anntId: anntId }).then(() => {
reload();
syncNotic({ anntId: anntId });
});
}
let detailId = params.detailId;
if(detailId){
getOne(detailId).then(data=>{
console.log('getOne', data)
openDetail(true, {
record: data,
isUpdate: true,
});
appStore.setMessageHrefParams('')
})
}
}
}
function handleSuccess() {
selectedRowKeys.value = [];
reload();
}
/**
* 删除我的消息
*
* @param id
*/
async function handleDelete(id) {
await deleteAnnSend({ id: id }, handleSuccess);
}
/**
* 批量删除我的消息
*/
async function batchHandleDelete() {
let unRead = unref(selectedRows).filter((item) => item.readFlag == 0);
if (unref(unRead).length > 0) {
createMessage.warning('未阅读的消息禁止删除!');
return;
}
await deleteBatchAnnSend({ ids: selectedRowKeys.value }, handleSuccess);
}
</script>

View File

@@ -0,0 +1,94 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/sysAnnouncementSend/getMyAnnouncementSend',
editCementSend = '/sys/sysAnnouncementSend/editByAnntIdAndUserId',
readAllMsg = '/sys/sysAnnouncementSend/readAll',
syncNotic = '/sys/annountCement/syncNotic',
getOne = '/sys/sysAnnouncementSend/getOne',
delete = '/sys/sysAnnouncementSend/delete',
deleteBatch = '/sys/sysAnnouncementSend/deleteBatch',
}
/**
* 查询消息列表
* @param params
*/
export const getMyNewsList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 更新用户系统消息阅读状态
* @param params
*/
export const editCementSend = (params) => {
return defHttp.put({ url: Api.editCementSend, params });
};
/**
* 一键已读
* @param params
*/
export const readAllMsg = (params, handleSuccess) => {
Modal.confirm({
title: '确认操作',
content: '是否全部标注已读?',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.put({ url: Api.readAllMsg, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 同步消息
* @param params
*/
export const syncNotic = (params) => {
return defHttp.get({ url: Api.syncNotic, params });
};
/**
* 根据消息发送记录ID获取消息内容
* @param sendId
*/
export const getOne = (sendId) => {
return defHttp.get({ url: Api.getOne, params:{sendId} });
};
/**
* 删除用户通告阅读标记的数据
* @param params
* @param handleSuccess
*/
export const deleteAnnSend = (params, handleSuccess) =>{
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(()=>{
handleSuccess();
})
}
/**
* 批量删除用户通告阅读标记的数据
* @param params
* @param handleSuccess
*/
export const deleteBatchAnnSend = (params, handleSuccess) =>{
Modal.confirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(()=>{
handleSuccess();
})
},
});
}

View File

@@ -0,0 +1,102 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
export const columns: BasicColumn[] = [
{
title: '标题',
dataIndex: 'titile',
width: 100,
align: 'left',
},
{
title: '消息类型',
dataIndex: 'msgCategory',
width: 80,
customRender: ({ text }) => {
return render.renderDictNative(
text,
[
{ label: '通知公告', value: '1', color: 'blue' },
{ label: '系统消息', value: '2' },
],
true
);
},
},
{
title: '发布人',
dataIndex: 'sender',
width: 80,
},
{
title: '发布时间',
dataIndex: 'sendTime',
width: 80,
},
{
title: '优先级',
dataIndex: 'priority',
width: 80,
customRender: ({ text }) => {
const color = text == 'L' ? 'blue' : text == 'M' ? 'yellow' : 'red';
return render.renderTag(render.renderDict(text, 'priority'), color);
},
},
{
title: '阅读状态',
dataIndex: 'readFlag',
width: 80,
customRender: ({ text }) => {
return render.renderDictNative(
text,
[
{ label: '未读', value: '0', color: 'red' },
{ label: '已读', value: '1' },
],
true
);
},
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'titile',
label: '标题',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'sender',
label: '发布人',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'sendTime',
label: '发布时间',
component: 'RangeDate',
componentProps: {
valueType: 'Date',
},
colProps: { span: 6 },
},
{
field: 'msgCategory',
label: '消息类型',
component: 'Select',
componentProps: {
options: [
{ label: '通知公告', value: '1' },
{ label: '系统消息', value: '2' },
{ label: '日程计划', value: 'plan' },
{ label: '流程消息', value: 'flow' },
{ label: '会议', value: 'meeting' },
{ label: '知识库', value: 'file' },
{ label: '协同通知', value: 'collab' },
{ label: '督办通知', value: 'supe' },
],
},
colProps: { span: 6 },
},
];