OfficeSystem/pages/notice/detail/index.vue
2025-11-22 14:41:00 +08:00

725 lines
16 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 class="admin-actions" v-if="isAdmin && !loading && noticeDetail.id">
<uv-button type="warning" size="normal" @click="handleEdit">修改</uv-button>
<uv-button type="error" size="normal" @click="handleDelete">删除</uv-button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import { getNoticeDetail, deleteNotice } from '@/api';
import { useUserStore } from '@/store/user';
// 页面参数
const noticeId = ref('');
const noticeDetail = ref({});
const loading = ref(false);
// 用户角色判断
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const isAdmin = computed(() => {
if (!userInfo.value || !Array.isArray(userInfo.value.roles)) return false;
return userInfo.value.roles.some((role) => ['admin', 'sys_admin'].includes(role));
});
// 计算是否有接收信息
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'
});
}
});
};
// 编辑公告
const handleEdit = () => {
if (!noticeId.value) {
uni.showToast({
title: '公告ID无效',
icon: 'none'
});
return;
}
uni.navigateTo({
url: `/pages/notice/edit/index?id=${noticeId.value}`
});
};
// 删除公告
const handleDelete = async () => {
if (!noticeId.value) {
uni.showToast({
title: '公告ID无效',
icon: 'none'
});
return;
}
uni.showModal({
title: '删除确认',
content: '删除后将无法恢复,确认删除该公告吗?',
confirmText: '删除',
confirmColor: '#f56c6c',
success: async (res) => {
if (!res.confirm) return;
try {
await deleteNotice([noticeId.value]);
uni.showToast({
title: '删除成功',
icon: 'success'
});
// 延迟返回,让用户看到成功提示
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (error) {
console.error('删除公告失败:', error);
uni.showToast({
title: error?.message || '删除失败',
icon: 'none'
});
}
}
});
};
// 监听公告更新事件
const handleNoticeUpdated = () => {
loadNoticeDetail();
};
onMounted(() => {
uni.$on('notice:updated', handleNoticeUpdated);
});
onBeforeUnmount(() => {
uni.$off('notice:updated', handleNoticeUpdated);
});
</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%;
padding-bottom: 80px; /* 为底部按钮留出空间 */
}
.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;
}
/* 管理员操作按钮区域 */
.admin-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 12px 16px;
display: flex;
gap: 12px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
z-index: 10;
justify-content: space-evenly;
}
.admin-actions::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: #f0f0f0;
}
</style>