Files
NoticeFront/src/views/notice/NoticeDetails.vue
T
2026-09-08 20:22:52 +08:00

407 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>