407 lines
12 KiB
Vue
407 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { Message } from "view-ui-plus";
|
||
import { noticeApi } from "@/api/module/notice";
|
||
import type { NoticeDetailVo, NextPrevVo, ReadGroup } from "@/types/notice";
|
||
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
||
import Utils from "@/utils";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
const serid = computed(() => Number(route.query.serid) || 0);
|
||
/* type=0 管理视角(不标读) / type=1 查看(标读);注意 0 是合法值不能用 || 兜底 */
|
||
const viewType = computed(() => {
|
||
const t = Number(route.query.type);
|
||
return t === 0 ? 0 : 1;
|
||
});
|
||
/* 上一/下一篇的序列类型:管理视角走已发(type=0),查看走已收(type=1) */
|
||
const navType = computed(() => (viewType.value === 0 ? "0" : "1"));
|
||
|
||
const detail = ref<NoticeDetailVo | null>(null);
|
||
const loading = ref(false);
|
||
const readGroups = ref<ReadGroup[]>([]);
|
||
const showRead = ref(false);
|
||
const prevId = ref(0);
|
||
const nextId = ref(0);
|
||
|
||
onMounted(async () => {
|
||
await load();
|
||
});
|
||
|
||
async function load() {
|
||
if (!serid.value) return;
|
||
loading.value = true;
|
||
try {
|
||
detail.value = await noticeApi.modify(serid.value, viewType.value).send();
|
||
const nav: NextPrevVo = await noticeApi.getNextPrevNotice(serid.value, navType.value).send();
|
||
prevId.value = nav.prevId || 0;
|
||
nextId.value = nav.nextId || 0;
|
||
} catch (e: any) {
|
||
Message.error(e.message || "加载通知失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function goNav(id: number) {
|
||
if (!id) return;
|
||
router.replace({ path: route.path, query: { ...route.query, serid: id } });
|
||
load();
|
||
}
|
||
|
||
/* 阅读进度(同旧版:已查看百分比) */
|
||
const readPercent = computed(() => {
|
||
const total = Number(detail.value?.numcount) || 0;
|
||
const read = Number(detail.value?.readnumcount) || 0;
|
||
if (!total) return 0;
|
||
return Math.round((read / total) * 100);
|
||
});
|
||
const unreadCount = computed(() => Math.max((Number(detail.value?.numcount) || 0) - (Number(detail.value?.readnumcount) || 0), 0));
|
||
|
||
async function openRead() {
|
||
try {
|
||
readGroups.value = await noticeApi.orgizeModify(serid.value).send();
|
||
showRead.value = true;
|
||
} catch (e: any) {
|
||
Message.error(e.message || "加载阅读情况失败");
|
||
}
|
||
}
|
||
|
||
/* 编辑(管理视角) */
|
||
function onEdit() {
|
||
router.push({ path: "/list/new", query: { serid: serid.value } });
|
||
}
|
||
|
||
/* 打印 */
|
||
function onPrint() {
|
||
if (!detail.value) return;
|
||
const win = window.open("", "_blank");
|
||
if (!win) return;
|
||
win.document.write(`
|
||
<html>
|
||
<head><title>${detail.value.title || ""}</title></head>
|
||
<body style="font-family:'Microsoft YaHei',sans-serif;padding:24px;">
|
||
<h2>${detail.value.title || ""}</h2>
|
||
<p style="color:#666;font-size:13px;">
|
||
来源:${detail.value.DeptName || ""} 发布人:${detail.value.TeaName || ""} 发布时间:${detail.value.EditDate || ""}
|
||
</p>
|
||
<hr/>
|
||
<div>${detail.value.Contents || ""}</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
win.document.close();
|
||
win.focus();
|
||
win.print();
|
||
}
|
||
|
||
function downFile(id: number) {
|
||
if (!id) return;
|
||
window.location.href = noticeApi.fileDownUrl(id);
|
||
}
|
||
function downAll() {
|
||
window.location.href = noticeApi.allFileDownUrl(serid.value);
|
||
}
|
||
|
||
/** 展示名:后端 name=原始文件名,filename=存储相对路径(notice/2026xxxx/uuid.ext) */
|
||
function displayName(item: { name?: string; filename?: string }) {
|
||
return item.name || item.filename || "";
|
||
}
|
||
/* 附件图标(按扩展名,图标名对齐旧版 matchSuffix → icon-word/excel/ppt/pdf1/txt/tupian1/yasuo/weizhi) */
|
||
function getFileIcon(name: string) {
|
||
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
||
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
||
if (["xls", "xlsx"].includes(suffix)) return "icon-excel";
|
||
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
||
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
||
if (["txt"].includes(suffix)) return "icon-txt";
|
||
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian1";
|
||
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
||
return "icon-weizhi";
|
||
}
|
||
/* 可在线预览(open-file-viewer 支持:图片/pdf/txt/docx/xlsx/pptx;doc/zip 等仅下载) */
|
||
function getIsPreview(name: string) {
|
||
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
||
return ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "txt", "pdf", "docx", "xlsx", "pptx"].includes(suffix);
|
||
}
|
||
/* 附件预览弹窗(open-file-viewer) */
|
||
const previewState = ref<{ show: boolean; fileId: number | null; fileName: string }>({
|
||
show: false,
|
||
fileId: null,
|
||
fileName: "",
|
||
});
|
||
function openPreview(item: { name?: string; filename?: string; fileid?: number; id?: number }) {
|
||
previewState.value = {
|
||
show: true,
|
||
fileId: Number(item.fileid || item.id) || null,
|
||
fileName: displayName(item),
|
||
};
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-wrapper">
|
||
<div class="main-container" v-if="detail">
|
||
<div class="ops-top">
|
||
<div class="nav">
|
||
<Button size="small" :disabled="!prevId" @click="goNav(prevId)">上一篇</Button>
|
||
<Button size="small" :disabled="!nextId" @click="goNav(nextId)">下一篇</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理视角:阅读进度统计条(同旧版已发通知详情) -->
|
||
<template v-if="viewType === 0">
|
||
<h2 class="notice-title2">{{ detail.title }}</h2>
|
||
<div class="stat-bar">
|
||
<div class="progress-box">
|
||
<Progress :percent="readPercent" hide-info :stroke-width="10" />
|
||
<span class="percent-label">已查看 {{ readPercent }}%</span>
|
||
</div>
|
||
<div class="stat-nums" title="点击查看阅读情况" @click="openRead">
|
||
<span class="item">总人数 <b class="blue">{{ detail.numcount || 0 }}</b> 人</span>
|
||
<span class="item">已读 <b class="orange">{{ detail.readnumcount || 0 }}</b> 人</span>
|
||
<span class="item">未读 <b class="blue">{{ unreadCount }}</b> 人</span>
|
||
</div>
|
||
<div class="stat-tags">
|
||
<Tag>{{ detail.DeptName }}</Tag>
|
||
<Tag>{{ detail.TeaName }}</Tag>
|
||
<Tag>{{ detail.EditDate }}</Tag>
|
||
</div>
|
||
</div>
|
||
<div class="divider"></div>
|
||
</template>
|
||
|
||
<!-- 查看视角:普通标题 -->
|
||
<h2 v-else class="notice-title2">{{ detail.title }}</h2>
|
||
|
||
<div v-if="viewType !== 0" class="meta">
|
||
<span>发文单位:{{ detail.DeptName || "—" }}</span>
|
||
<span>发布人:{{ detail.TeaName || "—" }}</span>
|
||
<span>发布时间:{{ detail.EditDate }}</span>
|
||
<span v-if="detail.isDsfs">定时发送:{{ detail.DsfsTime }}</span>
|
||
</div>
|
||
|
||
<div class="content" v-html="detail.Contents"></div>
|
||
|
||
<div v-if="detail.ChaoSend && detail.ChaoSend.length" class="files">
|
||
<div class="files-title">附件({{ detail.ChaoSend.length }})<a class="down-all" @click="downAll">全部下载</a></div>
|
||
<div v-for="f in detail.ChaoSend" :key="f.fileid || f.id" class="file-item">
|
||
<i class="iconfont file-type-icon" :class="getFileIcon(displayName(f))"></i>
|
||
<span class="file-name" :title="displayName(f)">{{ displayName(f) }}</span>
|
||
<span class="file-size">{{ Utils.getFileSize(f.size) }}</span>
|
||
<a class="file-op" v-if="getIsPreview(displayName(f))" @click="openPreview(f)">查看</a>
|
||
<a class="file-op" @click="downFile(f.fileid || f.id)">下载</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理视角底部操作(同旧版:编辑 / 打印) -->
|
||
<div v-if="viewType === 0" class="bottom-btns">
|
||
<Button @click="onEdit">编辑</Button>
|
||
<Button style="margin-left: 10px" @click="onPrint">打印</Button>
|
||
</div>
|
||
|
||
<!-- 查看阅读情况(同旧版:绿=已读 红=未读) -->
|
||
<Modal v-model="showRead" title="查看阅读情况" :width="620">
|
||
<div class="read-tip">提示:<span class="green">绿色</span>表示已读,<span class="red">红色</span>表示未读</div>
|
||
<div class="read-list">
|
||
<div v-for="g in readGroups" :key="g.GroupName" class="read-group">
|
||
<div class="read-group-name">{{ g.GroupName }}:</div>
|
||
<div class="read-users">
|
||
<span
|
||
v-for="t in g.teaslist"
|
||
:key="t.account || t.teaname"
|
||
:class="t.states === 'read' ? 'green' : 'red'"
|
||
class="user-name"
|
||
>{{ t.teaname }}</span
|
||
>
|
||
</div>
|
||
<div v-if="!g.teaslist || !g.teaslist.length" class="empty">暂无人员</div>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<Button type="primary" @click="showRead = false">确定</Button>
|
||
</template>
|
||
</Modal>
|
||
</div>
|
||
<div class="main-container" v-else-if="loading">加载中...</div>
|
||
<div class="main-container empty" v-else>通知不存在或已被删除</div>
|
||
</div>
|
||
<!-- 附件预览弹窗 -->
|
||
<FilePreviewModal v-model="previewState.show" :file-id="previewState.fileId" :file-name="previewState.fileName" />
|
||
</template>
|
||
|
||
<style scoped>
|
||
.ops-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
.nav {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.notice-title2 {
|
||
font-size: 20px;
|
||
margin: 0 0 12px;
|
||
}
|
||
.meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 18px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px dashed #e8e8e8;
|
||
font-size: 13px;
|
||
color: #808695;
|
||
}
|
||
/* 管理视角统计条 */
|
||
.stat-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
padding: 4px 0;
|
||
font-size: 13px;
|
||
color: #17233d;
|
||
flex-wrap: wrap;
|
||
}
|
||
.progress-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: 300px;
|
||
}
|
||
.percent-label {
|
||
color: #f60;
|
||
white-space: nowrap;
|
||
font-size: 13px;
|
||
}
|
||
.stat-nums {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
}
|
||
.stat-nums .item {
|
||
color: #8a94a6;
|
||
}
|
||
.stat-nums b {
|
||
font-size: 20px;
|
||
margin: 0 2px;
|
||
}
|
||
.stat-nums .blue {
|
||
color: #5c9edc;
|
||
}
|
||
.stat-nums .orange {
|
||
color: #f60;
|
||
}
|
||
.stat-tags {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.divider {
|
||
border-bottom: 1px dashed #e8e8e8;
|
||
margin: 8px 0;
|
||
}
|
||
.content {
|
||
padding: 16px 0;
|
||
line-height: 1.8;
|
||
word-break: break-all;
|
||
}
|
||
.files {
|
||
border-top: 1px dashed #e8e8e8;
|
||
padding-top: 12px;
|
||
}
|
||
.files-title {
|
||
font-size: 13px;
|
||
color: #808695;
|
||
margin-bottom: 8px;
|
||
}
|
||
.down-all {
|
||
margin-left: 12px;
|
||
color: #2d8cf0;
|
||
cursor: pointer;
|
||
}
|
||
.file-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: #515a6e;
|
||
margin: 0 16px 8px 0;
|
||
font-size: 13px;
|
||
.file-type-icon {
|
||
font-size: 16px;
|
||
color: #515a6e;
|
||
}
|
||
.file-name {
|
||
max-width: 320px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.file-size {
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
.file-op {
|
||
color: #2d8cf0;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
.bottom-btns {
|
||
border-top: 1px dashed #e8e8e8;
|
||
padding-top: 14px;
|
||
margin-top: 8px;
|
||
}
|
||
/* 阅读情况弹窗 */
|
||
.read-tip {
|
||
background: #e8f4ff;
|
||
border: 1px solid #d3e9ff;
|
||
color: #515a6e;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.read-list {
|
||
max-height: 380px;
|
||
overflow-y: auto;
|
||
}
|
||
.read-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.read-group-name {
|
||
flex: none;
|
||
width: 130px;
|
||
text-align: right;
|
||
color: #515a6e;
|
||
font-size: 13px;
|
||
line-height: 22px;
|
||
}
|
||
.read-users {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px 14px;
|
||
}
|
||
.user-name {
|
||
font-size: 13px;
|
||
line-height: 22px;
|
||
cursor: default;
|
||
}
|
||
.green {
|
||
color: #19be6b;
|
||
}
|
||
.red {
|
||
color: #ed4014;
|
||
}
|
||
.empty {
|
||
color: #999;
|
||
font-size: 13px;
|
||
}
|
||
</style>
|