OfficeSystem/pages/notice/detail/index.vue
2025-11-19 15:28:32 +08:00

617 lines
14 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.

<template>
<view class="notice-detail-page">
<scroll-view class="content-scroll" scroll-y>
<!-- 标题和标签区域 -->
<view class="notice-header">
<view class="title-row">
<text class="notice-title" :class="{ 'pinned': noticeDetail.top }">
{{ noticeDetail.title || '加载中...' }}
</text>
<view class="notice-tags">
<view v-if="noticeDetail.top" class="notice-tag tag-pinned">置顶</view>
<view v-if="noticeDetail.level === '1'" class="notice-tag tag-general">一般</view>
<view v-if="noticeDetail.level === '2'" class="notice-tag tag-important">重要</view>
<view v-if="noticeDetail.level === '3'" class="notice-tag tag-urgent">紧急</view>
</view>
</view>
<!-- 元信息 -->
<view class="meta-row">
<view class="meta-item">
<text class="meta-icon">👤</text>
<text class="meta-text">{{ noticeDetail.userName || '未知' }}</text>
</view>
<view class="meta-item">
<text class="meta-icon">🕐</text>
<text class="meta-text">{{ formatTime(noticeDetail.createTime) }}</text>
</view>
</view>
</view>
<!-- 公告内容区域 -->
<view class="notice-content-card">
<view class="content-wrapper">
<text class="content-text">{{ noticeDetail.content || '' }}</text>
</view>
</view>
<!-- 接收信息区域 -->
<view class="receive-section" v-if="hasReceiveInfo">
<view class="section-title">接收信息</view>
<!-- 接收用户 -->
<view class="receive-item" v-if="noticeDetail.receiveUserList && noticeDetail.receiveUserList.length > 0">
<text class="receive-label">接收用户:</text>
<view class="receive-users">
<view
class="user-item"
v-for="user in noticeDetail.receiveUserList"
:key="user.userId"
>
<view class="user-avatar">
<image
v-if="user.avatar"
:src="user.avatar"
class="avatar-img"
mode="aspectFill"
/>
<text v-else class="avatar-text">{{ user.nickName?.charAt(0) || '?' }}</text>
</view>
<text class="user-name">{{ user.nickName || user.userName || '未知' }}</text>
</view>
</view>
</view>
<!-- 接收部门 -->
<view class="receive-item" v-if="noticeDetail.receiveDeptList && noticeDetail.receiveDeptList.length > 0">
<text class="receive-label">接收部门:</text>
<view class="receive-depts">
<view
class="dept-tag"
v-for="dept in noticeDetail.receiveDeptList"
:key="dept.deptId"
>
{{ dept.deptName }}
</view>
</view>
</view>
</view>
<!-- 附件区域 -->
<view class="attachment-section" v-if="hasAttachments">
<view class="section-title">附件</view>
<!-- 图片附件(三列布局) -->
<view class="attachment-images-wrapper" v-if="imageAttachments.length > 0">
<view
class="attachment-image-item"
v-for="(attach, imgIndex) in imageAttachments"
:key="imgIndex"
@click="previewImages(imageAttachments, imgIndex)"
>
<image
:src="getAttachmentUrl(attach)"
mode="aspectFill"
class="attachment-image"
/>
</view>
</view>
<!-- 非图片附件(列表样式) -->
<view class="attachment-list" v-if="fileAttachments.length > 0">
<view
class="attachment-item"
v-for="(attach, index) in fileAttachments"
:key="index"
@click="previewAttachment(attach)"
>
<text class="attachment-icon">{{ getFileIcon(attach.name || attach) }}</text>
<view class="attachment-info">
<text class="attachment-name">{{ attach.name || attach }}</text>
<text class="attachment-size" v-if="attach.size">{{ formatFileSize(attach.size) }}</text>
</view>
<text class="attachment-download">下载</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getNoticeDetail } from '@/api';
// 页面参数
const noticeId = ref('');
const noticeDetail = ref({});
const loading = ref(false);
// 计算是否有接收信息
const hasReceiveInfo = computed(() => {
return (noticeDetail.value.receiveUserList && noticeDetail.value.receiveUserList.length > 0) ||
(noticeDetail.value.receiveDeptList && noticeDetail.value.receiveDeptList.length > 0);
});
// 计算是否有附件
const hasAttachments = computed(() => {
return noticeDetail.value.attaches &&
(Array.isArray(noticeDetail.value.attaches) ? noticeDetail.value.attaches.length > 0 : true);
});
// 附件列表
const attachmentList = computed(() => {
if (!noticeDetail.value.attaches) return [];
// 如果是字符串,尝试解析
if (typeof noticeDetail.value.attaches === 'string') {
try {
const parsed = JSON.parse(noticeDetail.value.attaches);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
// 如果不是JSON可能是逗号分隔的URL字符串
return noticeDetail.value.attaches.split(',').filter(url => url.trim()).map(url => ({
url: url.trim(),
name: url.trim().split('/').pop() || '附件'
}));
}
}
// 如果是数组,直接返回
if (Array.isArray(noticeDetail.value.attaches)) {
return noticeDetail.value.attaches;
}
return [];
});
// 图片附件列表
const imageAttachments = computed(() => {
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
return attachmentList.value.filter(attach => {
const url = attach.url || attach;
const name = attach.name || attach;
const ext = (url || name).split('.').pop()?.toLowerCase();
return ext && imageExts.includes(ext);
});
});
// 非图片附件列表
const fileAttachments = computed(() => {
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
return attachmentList.value.filter(attach => {
const url = attach.url || attach;
const name = attach.name || attach;
const ext = (url || name).split('.').pop()?.toLowerCase();
return !ext || !imageExts.includes(ext);
});
});
// 获取页面参数并加载数据
onLoad((options) => {
if (options && options.id) {
noticeId.value = options.id;
loadNoticeDetail();
} else {
uni.showToast({
title: '缺少公告ID',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
// 加载公告详情
const loadNoticeDetail = async () => {
if (!noticeId.value) return;
loading.value = true;
try {
const res = await getNoticeDetail(noticeId.value);
if (res) {
noticeDetail.value = res;
}
} catch (error) {
console.error('加载公告详情失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} finally {
loading.value = false;
}
};
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return '';
return timeStr;
};
// 获取文件图标
const getFileIcon = (fileName) => {
if (!fileName) return '📄';
const ext = fileName.split('.').pop()?.toLowerCase();
const iconMap = {
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'ppt': '📙',
'pptx': '📙',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️',
'zip': '📦',
'rar': '📦',
'txt': '📄',
'mp4': '🎬',
'mp3': '🎵'
};
return iconMap[ext] || '📄';
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (!bytes) return '';
if (bytes < 1024) return bytes + 'B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + 'KB';
return (bytes / (1024 * 1024)).toFixed(2) + 'MB';
};
// 获取附件URL
const getAttachmentUrl = (attach) => {
return attach.url || attach;
};
// 预览图片(支持多图预览)
const previewImages = (attachments, currentIndex) => {
if (!attachments || attachments.length === 0) return;
const imageUrls = attachments.map(attach => getAttachmentUrl(attach));
const currentUrl = imageUrls[currentIndex] || imageUrls[0];
uni.previewImage({
urls: imageUrls,
current: currentUrl
});
};
// 预览附件(非图片文件)
const previewAttachment = (attach) => {
const url = getAttachmentUrl(attach);
if (!url) return;
// 非图片文件,尝试下载或打开
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功');
},
fail: () => {
uni.showToast({
title: '无法打开此文件',
icon: 'none'
});
}
});
}
},
fail: () => {
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
});
};
</script>
<style lang="scss" scoped>
.notice-detail-page {
width: 100%;
height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.content-scroll {
flex: 1;
width: 100%;
}
.notice-header {
background: #fff;
padding: 20px 16px;
margin-bottom: 8px;
}
.title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.notice-title {
flex: 1;
font-size: 20px;
font-weight: 600;
color: #333;
line-height: 1.5;
&.pinned {
color: #f56c6c;
}
}
.notice-tags {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
flex-wrap: wrap;
}
.notice-tag {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
color: #fff;
white-space: nowrap;
}
.tag-pinned {
background-color: #f56c6c;
}
.tag-general {
background-color: #4caf50;
}
.tag-important {
background-color: #ff9800;
}
.tag-urgent {
background-color: #f56c6c;
}
.meta-row {
display: flex;
align-items: center;
gap: 20px;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
}
.meta-icon {
font-size: 14px;
}
.meta-text {
font-size: 14px;
color: #666;
}
.notice-content-card {
background: #fff;
padding: 20px 16px;
margin-bottom: 8px;
}
.content-wrapper {
min-height: 100px;
}
.content-text {
font-size: 16px;
color: #333;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
}
.receive-section {
background: #fff;
padding: 20px 16px;
margin-bottom: 8px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.receive-item {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.receive-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.receive-users {
display: flex;
flex-direction: column;
gap: 12px;
}
.user-item {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e3f2fd;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-text {
font-size: 16px;
color: #2885ff;
font-weight: 500;
}
.user-name {
font-size: 14px;
color: #333;
}
.receive-depts {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.dept-tag {
padding: 6px 12px;
border-radius: 16px;
background: #f0f0f0;
font-size: 14px;
color: #666;
}
.attachment-section {
background: #fff;
padding: 20px 16px;
margin-bottom: 8px;
}
/* 图片附件三列布局 */
.attachment-images-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.attachment-image-item {
/* 一行三个:每个图片宽度 = (100% - 2个gap) / 3 */
width: calc((100% - 16px) / 3);
aspect-ratio: 1;
border-radius: 4px;
overflow: hidden;
background-color: #e0e0e0;
flex-shrink: 0;
}
.attachment-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 非图片附件列表样式 */
.attachment-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.attachment-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
transition: background 0.2s;
&:active {
background: #e0e0e0;
}
}
.attachment-icon {
font-size: 24px;
flex-shrink: 0;
}
.attachment-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.attachment-name {
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachment-size {
font-size: 12px;
color: #999;
}
.attachment-download {
font-size: 14px;
color: #2885ff;
flex-shrink: 0;
}
.loading-state {
padding: 60px 20px;
text-align: center;
}
.loading-text {
font-size: 14px;
color: #999;
}
</style>