公告详细

This commit is contained in:
WindowBird 2025-11-19 11:06:04 +08:00
parent b873f72e41
commit cce5eda1be
5 changed files with 638 additions and 2 deletions

View File

@ -99,3 +99,16 @@ export const getNoticeList = (params = {}) => {
});
};
/**
* 获取公告详情
* @param {string} id 公告ID
* @returns {Promise} 返回公告详情
*/
export const getNoticeDetail = (id) => {
return uni.$uv.http.get(`bst/notice/${id}`, {
custom: {
auth: true // 启用 token 认证
}
});
};

View File

@ -90,6 +90,7 @@
<view class="announcement-tags">
<view class="announcement-tag tag-pinned" v-if="announcement.top">置顶</view>
<view class="announcement-tag tag-important" v-if="announcement.level === '2'">重要</view>
<view class="announcement-tag tag-urgent" v-if="announcement.level === '3'">紧急</view>
</view>
</view>
<view class="announcement-meta">
@ -845,6 +846,10 @@ const getTagCustomStyle = (status) => {
background-color: #ff9800;
}
.tag-urgent {
background-color: #f56c6c;
}
.announcement-meta {
display: flex;
align-items: center;

View File

@ -156,6 +156,12 @@
"style": {
"navigationBarTitleText": "公告列表"
}
},
{
"path": "pages/notice/detail/index",
"style": {
"navigationBarTitleText": "公告详情"
}
}
],

View File

@ -0,0 +1,611 @@
<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 === '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) {
// JSONURL
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-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>

View File

@ -309,8 +309,9 @@ const formatTime = (timeStr) => {
//
const goToNoticeDetail = (notice) => {
// TODO:
console.log('查看公告详情:', notice);
uni.navigateTo({
url: `/pages/notice/detail/index?id=${notice.id}`
});
};
onMounted(() => {