zhxy-jzd/src/pages/base/jl/detail.vue
2025-11-10 16:42:11 +08:00

907 lines
23 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.

<!-- src/pages/view/notice/detail.vue -->
<template>
<BasicLayout>
<view class="notice-detail-page">
<view v-if="isLoading" class="loading-indicator">加载中...</view>
<view v-else-if="noticeDetail" class="detail-container">
<!-- 1. 主要内容卡片 -->
<view class="info-card main-content-card">
<view v-if="noticeDetail.jlfm" class="cover-image-container">
<image
:src="imagUrl(noticeDetail.jlfm)"
mode="aspectFill"
class="cover-image"
></image>
</view>
<text class="notice-title">{{ noticeDetail.jlmc }}</text>
<!-- 发布人和结束时间 -->
<view class="notice-meta">
<text class="meta-item">发布人: {{ noticeDetail.jsxm }}</text>
<text class="meta-item">结束时间: {{ noticeDetail.jljstime || noticeDetail.endTime }}</text>
</view>
<view class="notice-content">
<view v-if="!descExpanded">
<text class="desc-preview">{{ descPreview }}</text>
<u-button type="text" size="mini" class="more-btn" @click="descExpanded = true">更多</u-button>
</view>
<view v-else>
<rich-text :nodes="noticeDetail.jlms" class="desc-rich-text"></rich-text>
<u-button type="text" size="mini" class="more-btn" @click="descExpanded = false">收起</u-button>
</view>
</view>
<!-- 选择题选项区域 -->
<view v-if="noticeDetail.jlType === 'select' && optionsList.length > 0" class="options-section">
<view class="options-title">
<text>请选择一项:</text>
<text v-if="isAlreadyRelayed" class="already-relayed-hint">(您已选择)</text>
</view>
<view class="options-list">
<view
v-for="(option, index) in optionsList"
:key="index"
class="option-item"
:class="{ 'option-selected': selectedOption === option.key, 'option-disabled': isAlreadyRelayed && selectedOption !== option.key }"
@click="isAlreadyRelayed ? null : selectOption(option.key)"
>
<view class="option-radio">
<view v-if="selectedOption === option.key" class="radio-checked"></view>
</view>
<view class="option-content">
<text class="option-key">{{ option.key }}.</text>
<text class="option-value">{{ option.value }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 2. 附件卡片 -->
<view v-if="noticeDetail.jlfj" class="info-card attachment-card">
<text class="attachment-title">附件</text>
<view class="attachment-list">
<view
class="attachment-item"
@click="previewAttachment(noticeDetail.jlfj)"
>
<uni-icons type="paperplane" size="16" color="#409eff"></uni-icons>
<text class="attachment-name">{{ getFileName(noticeDetail.jlfj) }}</text>
</view>
</view>
</view>
<!-- 3. 学生完成状态卡片 -->
<view class="info-card feedback-card">
<text class="feedback-title">接龙完成情况 ({{ receivedCount }}/{{ totalStudents }})</text>
<view class="name-tags">
<view
v-for="stu in studentList"
:key="stu.id || stu.xsId"
class="name-tag"
:class="{ received: stu.jlts === '1' }"
>
<text>{{ stu.xsxm || stu.name }}</text>
<view v-if="stu.jlts === '1'" class="checkmark-icon">
<uni-icons type="checkmarkempty" size="12" color="#ffffff"></uni-icons>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">通知详情未找到</view>
</view>
<!-- 添加签名组件 - 只在需要时显示 -->
<BasicSign v-if="showSignature" ref="signCompRef" :title="signTitle"></BasicSign>
<template #bottom>
<view class="bottom-actions">
<button
v-if="!relaySuccess"
class="action-btn publish-btn"
@click="onRelayClick"
>
接龙
</button>
<button
v-else
class="action-btn return-btn"
@click="handleReturn"
>
返回
</button>
</view>
</template>
</BasicLayout>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getByJlIdApi, jlzxFindByJlParamsApi, relayFinishApi } from "@/api/base/server";
import { imagUrl } from "@/utils";
import { BASE_IMAGE_URL } from "@/config";
import { useUserStore } from "@/store/modules/user";
import { showLoading, hideLoading } from "@/utils/uniapp";
const noticeId = ref<string>("");
const noticeDetail = ref<any>(null);
const isLoading = ref(false);
const studentList = ref<any[]>([]);
const descExpanded = ref(false);
// 新增参数
const njId = ref<string>("");
const njmcId = ref<string>("");
const bjId = ref<string>("");
// 选择题相关
const optionsList = ref<any[]>([]);
const selectedOption = ref<string>("");
// 接龙成功状态
const relaySuccess = ref<boolean>(false);
// 签名相关
const signCompRef = ref<any>(null);
const signTitle = ref<string>("签名");
const sign_file = ref<string>("");
const showSignature = ref<boolean>(false);
// 获取当前学生信息
const userStore = useUserStore();
const currentStudent = computed(() => userStore.curXs);
const descPreview = computed(() => {
if (!noticeDetail.value?.jlms) return '';
const div = document.createElement('div');
div.innerHTML = noticeDetail.value.jlms;
return div.innerText.replace(/\n/g, '').slice(0, 100) + (div.innerText.length > 100 ? '...' : '');
});
// 获取文件名
const getFileName = (filePath: string) => {
if (!filePath) return '';
const parts = filePath.split('/');
return parts[parts.length - 1] || filePath;
};
// Computed properties for feedback status
const receivedCount = computed(() => {
return studentList.value.filter((s) => s.jlts === '1').length;
});
const totalStudents = computed(() => studentList.value.length);
// 判断当前学生是否已接龙
const isAlreadyRelayed = computed(() => {
const curStu = studentList.value.find(stu => stu.xsId === currentStudent.value?.id);
return curStu && curStu.jlts === '1';
});
// 选择选项
function selectOption(key: string) {
// 如果已接龙,不允许修改选择
if (isAlreadyRelayed.value) {
uni.showToast({ title: '您已接龙,无法修改选择', icon: 'none' });
return;
}
selectedOption.value = key;
}
// 返回上一页并刷新列表
function handleReturn() {
// 通过事件总线或页面跳转参数通知上一页刷新
// 方案1: 使用 uni.$emit 事件通知
uni.$emit('jlListRefresh');
// 方案2: 返回上一页index.vue 的 onShow 会自动刷新)
uni.navigateBack({
delta: 1
});
}
// 接龙按钮点击逻辑
async function onRelayClick() {
try {
// 判断当前学生是否已接龙
const curStu = studentList.value.find(stu => {
return stu.xsId === currentStudent.value?.id;
});
if (curStu && curStu.jlts === '1') {
uni.showToast({ title: '您已参与接龙,无须重复提交', icon: 'none' });
return;
}
// 如果是选择题接龙,必须选择选项
if (noticeDetail.value.jlType === 'select') {
if (!selectedOption.value) {
uni.showToast({ title: '请选择一项后再提交', icon: 'none' });
return;
}
}
if (noticeDetail.value && noticeDetail.value.mdqz == 1) {
// 需要签名
showSignature.value = true;
sign_file.value = '';
// 等待组件渲染完成
await nextTick();
const data = await signCompRef.value.getSyncSignature();
sign_file.value = data.base64;
showSignature.value = false;
submitRelay();
} else {
submitRelay();
}
} catch (e) {
showSignature.value = false;
uni.showToast({title: '获取详情失败', icon: 'none'});
}
}
// 提交接龙
async function submitRelay() {
const params: any = {
jlId: noticeId.value,
xsId: currentStudent.value?.id,
};
if (sign_file.value) {
params.qmFile = sign_file.value;
}
// 如果是选择题接龙,添加选中的选项
if (noticeDetail.value.jlType === 'select' && selectedOption.value) {
params.selectedOption = selectedOption.value;
// 查找选中选项的值
const selectedOpt = optionsList.value.find(opt => opt.key === selectedOption.value);
if (selectedOpt) {
params.selectedValue = selectedOpt.value;
}
}
showLoading('提交中...');
try {
const res:any = await relayFinishApi(params);
hideLoading();
if (res && res.resultCode === 1 ) {
uni.showToast({title: '接龙成功', icon: 'success'});
relaySuccess.value = true; // 设置接龙成功状态
await refreshStudentList();
} else {
uni.showToast({title: res?.msg || '接龙失败', icon: 'none'});
}
} catch (e) {
hideLoading();
uni.showToast({title: '网络异常', icon: 'none'});
}
}
// 刷新学生完成状态
async function refreshStudentList() {
try {
const params: any = { jlId: noticeId.value };
// 添加筛选参数
if (njId.value) params.njId = njId.value;
if (njmcId.value) params.njmcId = njmcId.value;
if (bjId.value) params.bjId = bjId.value;
const stuRes = await jlzxFindByJlParamsApi(params);
studentList.value = stuRes?.rows || stuRes?.result || [];
// 刷新后回显已选择的选项,并设置按钮状态
const curStu = studentList.value.find(stu => stu.xsId === currentStudent.value?.id);
if (curStu && curStu.jlts === '1') {
relaySuccess.value = true; // 设置接龙成功状态,显示返回按钮
if (curStu.selectedOption) {
selectedOption.value = curStu.selectedOption;
}
}
} catch (e) {
uni.showToast({ title: "刷新学生状态失败", icon: "none" });
}
}
onLoad(async (options) => {
if (options && options.jlId) {
noticeId.value = options.jlId;
// 接收新的参数
if (options.njId) njId.value = options.njId;
if (options.njmcId) njmcId.value = options.njmcId;
if (options.bjId) bjId.value = options.bjId;
isLoading.value = true;
if (options.openId) {
// 检查登录状态
const isLoggedIn = await userStore.loginByOpenId(options.openId);
if (!isLoggedIn) {
return;
}
}
// 1. 获取接龙详情(新接口)
try {
const detailRes = await getByJlIdApi({ jlId: noticeId.value });
noticeDetail.value = Array.isArray(detailRes) ? detailRes[0] : {};
// 解析选项配置
if (noticeDetail.value.jlType === 'select' && noticeDetail.value.jlOptions) {
try {
optionsList.value = JSON.parse(noticeDetail.value.jlOptions);
// 按 sort 排序
optionsList.value.sort((a, b) => (a.sort || 0) - (b.sort || 0));
} catch (e) {
console.error('解析选项配置失败:', e);
optionsList.value = [];
}
}
} catch (e) {
uni.showToast({ title: "加载接龙详情失败", icon: "none" });
}
// 2. 获取学生完成状态
try {
const params: any = { jlId: noticeId.value };
// 添加筛选参数
if (njId.value) params.njId = njId.value;
if (njmcId.value) params.njmcId = njmcId.value;
if (bjId.value) params.bjId = bjId.value;
const stuRes = await jlzxFindByJlParamsApi(params);
studentList.value = stuRes?.rows || stuRes?.result || [];
// 查找当前学生,回显已选择的选项
const curStu = studentList.value.find(stu => stu.xsId === currentStudent.value?.id);
if (curStu && curStu.jlts === '1') {
// 已接龙,显示返回按钮
relaySuccess.value = true;
if (curStu.selectedOption) {
selectedOption.value = curStu.selectedOption;
}
}
} catch (e) {
uni.showToast({ title: "加载学生状态失败", icon: "none" });
}
isLoading.value = false;
uni.setNavigationBarTitle({ title: "接龙情况" });
} else {
uni.showToast({ title: "加载失败缺少通知ID", icon: "none" });
}
});
// 附件预览函数
const previewAttachment = (filePath: string) => {
if (!filePath) {
uni.showToast({ title: "附件链接无效", icon: "none" });
return;
}
// 构建完整的文件URL
const baseUrl = BASE_IMAGE_URL; // 使用配置文件中的URL
const fullUrl = filePath.startsWith('http') ? filePath : baseUrl + filePath;
// 判断文件类型
const fileExtension = filePath.split('.').pop()?.toLowerCase();
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension || '');
// 如果是图片类型,直接预览
if (isImage) {
uni.previewImage({
urls: [fullUrl],
current: fullUrl
});
return;
}
uni.showLoading({ title: "正在准备附件..." });
uni.downloadFile({
url: fullUrl,
success: (res) => {
if (res.statusCode === 200) {
const tempFilePath = res.tempFilePath;
uni.hideLoading();
// Attempt to open the downloaded file
uni.openDocument({
filePath: tempFilePath,
success: function (res) {
// 打开文档成功
},
fail: function (err) {
uni.showToast({ title: "无法打开该文件类型", icon: "none" });
uni.hideLoading(); // Ensure loading is hidden on failure too
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "下载附件失败", icon: "none" });
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: "下载附件失败", icon: "none" });
},
});
};
watch(noticeDetail, (val) => {
// 监听数据变化
}, { immediate: true, deep: true });
// 添加学生列表数据监听器,处理状态字段兼容性
watch(studentList, (list) => {
list.forEach(stu => {
// 兼容后端返回的驼峰写法
if (stu.jlwcStatus && !stu.jlwc_status) {
stu.jlwc_status = stu.jlwcStatus;
}
// 兼容数字状态,将'1'转换为'A'
if (stu.jlwc_status === '1') {
stu.jlwc_status = 'A';
}
});
}, { immediate: true, deep: true });
</script>
<style scoped lang="scss">
.notice-detail-page {
background-color: #f4f5f7;
padding-bottom: 70px;
padding: 15px;
box-sizing: border-box;
}
.bottom-actions {
display: flex;
justify-content: space-around;
align-items: center;
padding: 12px 15px;
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
.action-btn {
width: 90%;
min-width: 90px;
margin: 0 5px;
font-size: 15px;
height: 40px;
line-height: 40px;
border-radius: 20px;
padding: 0 20px;
&::after {
border: none;
}
&.draft-btn {
background-color: #f4f4f5;
color: #909399;
border: 1px solid #e9e9eb;
}
&.preview-btn {
background-color: #ecf5ff;
color: #409eff;
border: 1px solid #d9ecff;
}
&.publish-btn {
background-color: #409eff;
color: #ffffff;
border: none;
}
&.draft-btn:active {
background-color: #e0e0e0;
}
&.preview-btn:active {
background-color: #d9ecff;
}
&.publish-btn:active {
background-color: #3a8ee6;
}
&.return-btn {
background-color: #67c23a;
color: #ffffff;
border: none;
}
&.return-btn:active {
background-color: #5daf34;
}
}
}
.loading-indicator,
.empty-state {
text-align: center;
color: #999;
padding: 40px 15px;
font-size: 14px;
}
.detail-container {
// 卡片之间的间距由卡片自身的 margin-bottom 控制
}
/* 通用卡片样式 */
.info-card {
background-color: #ffffff;
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
}
/* 主要内容卡片 */
.main-content-card {
.cover-placeholder {
background-color: #f0f0f0;
height: 120px;
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #ccc;
font-size: 14px;
margin-bottom: 15px;
text {
margin-top: 5px;
}
}
.cover-image-container {
margin-bottom: 15px;
.cover-image {
width: 100%;
height: 150px; // Or adjust as needed
border-radius: 6px;
display: block;
}
}
.notice-title {
font-size: 18px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 10px;
}
.notice-meta {
margin-bottom: 15px;
.meta-item {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
}
.notice-content {
position: relative;
.desc-preview {
display: block;
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 4px;
}
.desc-rich-text {
font-size: 14px;
color: #666;
line-height: 1.5;
}
}
// 设计稿中的添加附件占位
.add-attachment-placeholder {
display: flex;
align-items: center;
border: 1px dashed #dcdcdc;
border-radius: 6px;
padding: 15px;
margin-top: 15px;
.add-icon {
width: 30px;
height: 30px;
border: 1px solid #dcdcdc;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
}
.placeholder-text {
font-size: 14px;
color: #999;
}
}
}
/* 附件区域 */
.attachments-section {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
.sub-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
display: block;
}
.attachment-item {
display: flex;
align-items: center;
padding: 5px 0;
.attachment-name {
font-size: 14px;
color: #007aff;
margin-left: 5px;
}
}
}
/* 按名单填写/反馈情况卡片 */
.feedback-card {
// Optional: Add specific padding if needed
}
/* 附件卡片 */
.attachment-card {
.attachment-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.attachment-list {
.attachment-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.attachment-name {
font-size: 14px;
color: #409eff;
margin-left: 8px;
flex: 1;
}
}
}
}
.feedback-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.name-tags {
display: flex;
flex-wrap: wrap;
gap: 8px 10px; // Row gap, Column gap
.name-tag,
.modify-btn {
// Apply same sizing logic to modify button if it exists
position: relative;
font-size: 13px;
padding: 5px 8px; // Adjust horizontal padding slightly if needed
border-radius: 4px;
text-align: center;
flex-grow: 0;
flex-shrink: 0;
box-sizing: border-box;
// 调整为每行4个确保能显示4个汉字
flex-basis: calc((100% - 30px) / 4); // 4个学生3个间隔
height: 30px; // Slightly taller height
line-height: 20px; // Adjust line-height for vertical centering
border: 1px solid #f4f4f5;
// 允许文字换行
white-space: normal;
word-break: break-all; // Break long names if they don't fit
}
.name-tag {
background-color: #f4f4f5;
color: #909399;
position: relative;
&.received {
background-color: #e1f3d8;
color: #67c23a;
border-color: #b3e19d;
}
&.currentStudent {
background-color: #e6f7ff;
color: #1890ff;
border-color: #91d5ff;
border-width: 2px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
.checkmark-icon {
position: absolute;
top: -1px;
right: -1px;
width: 16px;
height: 16px;
background-color: #67c23a;
border-bottom-left-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
padding: 1px;
box-sizing: border-box;
.uni-icons {
// Icon adjustments if needed
}
}
}
// Modify button (usually hidden in detail)
.modify-btn {
// ... styles ...
flex-basis: calc((100% - 30px) / 4); // Keep consistent basis
}
}
/* 选择题选项区域 */
.options-section {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
.options-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
.already-relayed-hint {
font-size: 13px;
font-weight: normal;
color: #67c23a;
}
}
.options-list {
.option-item {
display: flex;
align-items: flex-start;
padding: 12px;
margin-bottom: 10px;
background-color: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
transition: all 0.3s ease;
&.option-selected {
background-color: #e6f7ff;
border-color: #409eff;
}
&.option-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.option-radio {
width: 20px;
height: 20px;
border: 2px solid #d9d9d9;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
transition: all 0.3s ease;
.radio-checked {
width: 10px;
height: 10px;
background-color: #409eff;
border-radius: 50%;
}
}
&.option-selected .option-radio {
border-color: #409eff;
}
.option-content {
flex: 1;
display: flex;
flex-wrap: wrap;
line-height: 1.6;
.option-key {
font-size: 15px;
font-weight: bold;
color: #333;
margin-right: 6px;
}
.option-value {
flex: 1;
font-size: 15px;
color: #333;
}
}
}
}
}
/* 更多按钮样式 */
.more-btn {
font-size: 16px !important;
color: #409eff !important;
font-weight: 500;
}
/* 列表项样式的卡片 */
.list-item-card {
padding: 5px 15px; // 减少上下 padding
}
.list-item-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0; // 行内上下 padding
border-bottom: 1px solid #f0f0f0;
&.no-border {
border-bottom: none;
}
.list-label {
font-size: 15px;
color: #333;
}
.list-value {
display: flex;
align-items: center;
font-size: 15px;
color: #666;
text {
margin-right: 3px;
}
}
}
</style>