2025-09-23 22:53:31 +08:00

1716 lines
42 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>
<BasicLayout>
<view class="px-15 pb-15">
<!-- 公文基本信息 -->
<view class="gw-info-section">
<view class="section-title">公文信息</view>
<view class="info-item">
<text class="label">公文标题</text>
<text class="value title-bold">{{ gwInfo.title }}</text>
</view>
<view class="info-item">
<text class="label">公文类型</text>
<text class="value">{{ gwInfo.docType }}</text>
</view>
<view class="info-item">
<text class="label">紧急程度</text>
<text class="value">{{ getUrgencyText(gwInfo.urgencyLevel) }}</text>
</view>
<view class="info-item">
<text class="label">创建人</text>
<text class="value">{{ getCreatorName(gwInfo.tjrId) }}</text>
</view>
<view class="info-item">
<text class="label">创建时间</text>
<text class="value">{{ formatTime(gwInfo.createdTime) }}</text>
</view>
</view>
<!-- 文件信息 -->
<view class="file-section">
<view class="section-title">附件</view>
<view class="file-list" v-if="hasAttachments">
<!-- 显示所有解析后的附件 -->
<view
v-for="(file, index) in parsedAttachments"
:key="(file as any).id || index"
class="file-item"
>
<view class="file-icon">
<image
:src="getFileIcon(getFileSuffix(file))"
class="icon-image"
mode="aspectFit"
/>
</view>
<view class="file-info">
<text class="file-name">{{ getFileName(file) }}</text>
</view>
<view class="file-actions">
<u-button
v-if="canPreview(getFileSuffix(file)) && !isVideo(getFileSuffix(file))"
text="预览"
size="mini"
type="primary"
@click="previewFile(file)"
/>
<u-button
v-if="isVideo(getFileSuffix(file))"
text="播放"
size="mini"
type="success"
@click="previewFile(file)"
/>
<u-button
v-if="isImage(getFileSuffix(file))"
text="查看"
size="mini"
type="warning"
@click="previewFile(file)"
/>
<u-button
text="下载"
size="mini"
type="info"
@click="downloadFileAction(file)"
/>
</view>
</view>
</view>
<view v-else class="no-files">
<text>暂无附件</text>
</view>
</view>
</view>
<!-- 审批流程 -->
<LcglSp :yw-id="gwId" yw-type="GW"/>
<!-- 操作记录详情弹窗 -->
<u-popup :show="showLogDetailModal" @close="showLogDetailModal = false" mode="center">
<view class="detail-modal">
<view class="detail-header">
<text class="detail-title">操作详情</text>
<u-button text="关闭" @click="showLogDetailModal = false"/>
</view>
<view class="detail-content">
<view class="detail-item" v-if="currentLog.beforeChange">
<text class="detail-label">变更前</text>
<text class="detail-value">{{ currentLog.beforeChange }}</text>
</view>
<view class="detail-item" v-if="currentLog.afterChange">
<text class="detail-label">变更后</text>
<text class="detail-value">{{ currentLog.afterChange }}</text>
</view>
<view class="detail-item" v-if="currentLog.remark">
<text class="detail-label">备注</text>
<text class="detail-value">{{ currentLog.remark }}</text>
</view>
</view>
</view>
</u-popup>
<!-- 下载路径选择弹窗 -->
<u-popup :show="showDownloadModal" @close="showDownloadModal = false" mode="center">
<view class="download-modal">
<view class="download-header">
<text class="download-title">文件下载</text>
</view>
<view class="download-content">
<view class="file-info-section">
<text class="file-label">文件名</text>
<text class="file-name">{{ downloadFileName }}</text>
</view>
<view class="path-info-section">
<text class="path-label">下载路径</text>
<text class="path-text">将下载到默认下载目录</text>
</view>
<view class="download-tips">
<text class="tips-text">点击确认后将开始下载文件</text>
</view>
<!-- 下载中状态 -->
<view class="download-progress" v-if="isDownloading">
<view class="progress-spinner"></view>
<text class="progress-text">正在下载文件请稍候...</text>
</view>
</view>
<view class="download-actions" v-if="!isDownloading">
<u-button
text="取消"
type="default"
size="large"
@click="cancelDownload"
style="margin-right: 10px;"
/>
<u-button
text="确认下载"
type="primary"
size="large"
@click="confirmDownload"
/>
</view>
</view>
</u-popup>
<template #bottom>
<YwConfirm v-if="showButton"
:spApi="gwSpApi"
:stopApi="gwStopApi"
:transferApi="gwTransferApi"
:params="spParams"
:showXt="false"
:showReject="true"
:showTransfer="true"
:showApprove="true"
:showReturn="false"
:showStop="false"
:showXtDk="false"/>
</template>
</BasicLayout>
</template>
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app";
import {ref, onMounted, computed} from "vue";
import BasicLayout from "@/components/BasicLayout/Layout.vue";
import {navigateTo} from "@/utils/uniapp";
import {getGwFlowByIdApi, gwSpApi, gwTransferApi, gwStopApi} from "@/api/routine/gw";
import dayjs from "dayjs";
import {useUserStore} from "@/store/modules/user";
import {imagUrl} from "@/utils";
import {
isVideo,
isImage,
canPreview,
previewFile as previewFileUtil,
previewVideo as previewVideoUtil,
previewImage as previewImageUtil,
downloadFile
} from "@/utils/filePreview";
import LcglSp from "@/components/LcglSp/index.vue";
import YwConfirm from "@/pages/components/YwConfirm/index.vue";
import {useDataStore} from "@/store/modules/data";
import { GwPageUtils } from "@/utils/gwPageUtils";
const { setGwData, getXxts } = useDataStore();
const spParams = computed(() => {
return{
xxtsId: getXxts.id,
ywId: gwId.value
};
});
const showButton = ref<boolean>(false);
// 类型定义
interface GwInfo {
id: string;
title: string;
gwNo: string;
gwType: string;
status: string;
createdBy: string;
createdTime: Date;
files?: FileInfo[]; // 改为可选,因为实际数据中可能没有这个字段
approvers?: Approver[];
ccUsers?: CCUser[];
operationLogs?: OperationLog[];
docType: string; // 新增
urgencyLevel: string; // 新增
tjrId: string; // 新增
spRule?: string; // 新增审批规则字段
// 附件相关字段(根据实际数据结构)
fileUrl?: string; // 文件URL
fileName?: string; // 文件名
fileFormat?: string; // 文件格式
}
interface FileInfo {
id?: string; // 文件ID
name: string;
size: number;
url: string;
resourName?: string; // 资源名称
resourUrl?: string; // 资源URL
resSuf?: string; // 文件后缀
}
interface Approver {
id: string;
userId?: string; // 用户ID
userName: string;
deptName: string;
order: number;
status: string;
approveStatus?: string; // 审批状态
approveRemark?: string; // 审批意见
approveTime?: Date; // 审批时间
}
interface CCUser {
id: string;
userName: string;
deptName: string;
status: string;
}
interface OperationLog {
id: string;
operatorName: string;
operationType: string;
operationContent: string;
operationTime: Date;
beforeChange?: string;
afterChange?: string;
remark?: string;
}
// 获取页面参数
const gwId = ref("");
// 弹窗控制
const showLogDetailModal = ref(false);
const showDownloadModal = ref(false);
const downloadUrl = ref('');
const downloadFileName = ref('');
const isDownloading = ref(false);
// 用户store
const {getUser, getJs} = useUserStore();
// 数据
const gwInfo = ref<GwInfo>({} as GwInfo);
const approvers = ref<Approver[]>([]);
const ccUsers = ref<CCUser[]>([]);
const operationLogs = ref<OperationLog[]>([]);
const currentLog = ref<OperationLog>({} as OperationLog);
// 抄送人展开状态
const ccExpanded = ref(false);
// 操作记录展开状态
const logExpanded = ref(false);
// 计算属性:解析后的附件列表
const parsedAttachments = computed(() => {
const attachments = [];
// 处理files数组
if (gwInfo.value.files && gwInfo.value.files.length > 0) {
attachments.push(...gwInfo.value.files);
}
// 处理逗号分隔的fileUrl字符串
else if (gwInfo.value.fileUrl) {
const fileUrls = gwInfo.value.fileUrl.split(',').map(url => url.trim()).filter(url => url);
// 解析fileName字符串如果存在
let fileNames: string[] = [];
if (gwInfo.value.fileName) {
fileNames = gwInfo.value.fileName.split(',').map(name => name.trim()).filter(name => name);
}
// 将URL字符串转换为文件对象
fileUrls.forEach((url, index) => {
// 优先使用解析出的文件名如果没有则从URL提取
let displayName = fileNames[index] || url.split('/').pop() || `文件${index + 1}`;
const fileFormat = url.split('.').pop() || 'unknown';
// 如果文件名包含扩展名去掉扩展名保持resourName不包含扩展名
let resourName = displayName;
if (displayName.includes('.' + fileFormat)) {
resourName = displayName.replace('.' + fileFormat, '');
}
attachments.push({
id: `file_${index}`,
name: displayName, // 保持原始文件名(可能包含扩展名)
url: url,
resourUrl: url,
resourName: resourName, // 不包含扩展名的文件名
resSuf: fileFormat,
size: 0 // 无法从URL获取大小
});
});
}
return attachments;
});
// 计算属性:是否有附件
const hasAttachments = computed(() => {
return parsedAttachments.value.length > 0;
});
// 计算属性:显示的抄送人列表
const displayedCcUsers = computed(() => {
if (ccExpanded.value || ccUsers.value.length <= 2) {
return ccUsers.value;
}
return ccUsers.value.slice(0, 2);
});
// 计算属性:显示的操作记录列表
const displayedOperationLogs = computed(() => {
if (logExpanded.value || operationLogs.value.length <= 2) {
return operationLogs.value;
}
return operationLogs.value.slice(0, 2);
});
// 计算属性:当前用户是否可以操作(转办/同意)
const canCurrentUserOperate = computed(() => {
const userStore = useUserStore();
const getUser = userStore.getUser;
const getJs = userStore.getJs;
const currentUserId = getJs?.id;
if (!currentUserId || !approvers.value || approvers.value.length === 0) {
return false;
}
// 查找当前用户对应的审批人记录
const currentUserApprover = approvers.value.find(approver => {
return approver.userId === currentUserId || approver.id === currentUserId;
});
if (!currentUserApprover) {
return false;
}
// 如果审批状态是approved或rejected则不能操作
const approveStatus = currentUserApprover.approveStatus;
if (approveStatus === 'approved' || approveStatus === 'rejected') {
return false;
}
return true;
});
// 获取公文信息
const getGwInfo = async () => {
try {
// 调用新的API接口获取公文流程信息
const response = await getGwFlowByIdApi(gwId.value);
if (response.resultCode === 1) {
// 设置公文信息
const data = response.result.gwInfo;
gwInfo.value = data;
// 记录当前页面数据
setGwData(data);
if (data.spResult && data.spResult != "A" && getXxts && getXxts.dbZt === "A") {
showButton.value = false;
GwPageUtils.updateXxts();
}
// 设置审批人、抄送人、操作日志信息
approvers.value = response.result.approvers || [];
ccUsers.value = response.result.ccUsers || [];
operationLogs.value = response.result.operationLogs || [];
} else {
throw new Error(response.message || '获取数据失败');
}
} catch (error) {
// 清空所有数据
gwInfo.value = {} as GwInfo;
approvers.value = [];
ccUsers.value = [];
operationLogs.value = [];
}
};
// 显示操作日志详情
const showLogDetail = (log: OperationLog) => {
currentLog.value = log;
showLogDetailModal.value = true;
};
// 切换抄送人展开状态
const toggleCcExpanded = () => {
ccExpanded.value = !ccExpanded.value;
};
// 切换操作记录展开状态
const toggleLogExpanded = () => {
logExpanded.value = !logExpanded.value;
};
// 驳回处理
const handleReject = () => {
uni.showModal({
title: "驳回公文",
content: "确定要驳回这个公文吗?",
success: async (res) => {
if (res.confirm) {
try {
// 这里调用驳回API
console.log("驳回公文:", gwId.value);
uni.showToast({
title: "驳回成功",
icon: "success",
});
} catch (error) {
console.error("驳回失败:", error);
uni.showToast({
title: "驳回失败",
icon: "error",
});
}
}
},
});
};
// 转办处理
const handleTransfer = () => {
// 构建跳转参数,包含所有必要的数据
const params = {
id: gwId.value,
title: encodeURIComponent(gwInfo.value.title || ''),
xxtsInfo: encodeURIComponent(JSON.stringify({
id: gwId.value, // 这里应该是xxtsInfo的ID需要根据实际数据结构调整
// 其他xxtsInfo相关字段
})),
gwInfo: encodeURIComponent(JSON.stringify(gwInfo.value)),
approvers: encodeURIComponent(JSON.stringify(approvers.value)),
ccUsers: encodeURIComponent(JSON.stringify(ccUsers.value)) // 添加抄送人数据
};
// 同时存储到本地存储,作为备用方案
uni.setStorageSync('transferData', {
xxtsInfo: {id: gwId.value},
gwInfo: gwInfo.value,
approvers: approvers.value,
ccUsers: ccUsers.value // 添加抄送人数据
});
// 跳转到转办页面
navigateTo(`/pages/view/routine/gwlz/gwTransfer?id=${params.id}&title=${params.title}&xxtsInfo=${params.xxtsInfo}&gwInfo=${params.gwInfo}&approvers=${params.approvers}&ccUsers=${params.ccUsers}`);
};
// 预览单个附件从gwInfo直接获取
const previewSingleFile = (event?: Event) => {
// 手动阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!gwInfo.value.fileUrl) {
return;
}
const fileUrl = imagUrl(gwInfo.value.fileUrl);
const fileName = gwInfo.value.fileName || '未知文件';
const fileFormat = gwInfo.value.fileFormat || '';
// 根据文件类型选择预览方式
if (isVideo(fileFormat)) {
handlePreviewVideoSingle(fileUrl, fileName);
} else if (isImage(fileFormat)) {
handlePreviewImageSingle(fileUrl);
} else if (canPreview(fileFormat)) {
handlePreviewDocumentSingle(fileUrl, fileName, fileFormat);
} else {
// 不支持预览的文件类型,直接下载
downloadSingleFile();
}
};
// 下载单个附件
const downloadSingleFile = (event?: Event) => {
// 手动阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!gwInfo.value.fileUrl) {
return;
}
const finalUrl = imagUrl(gwInfo.value.fileUrl);
const fileName = gwInfo.value.fileName || '未知文件';
const fileFormat = gwInfo.value.fileFormat || '';
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
// 显示下载路径选择弹窗
showDownloadPathModal(finalUrl, fullFileName);
};
// 文件预览
const previewFile = (file: FileInfo, event?: Event) => {
// 手动阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
// 确定文件URL和名称
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
const fileSuf = file.resSuf || file.name.split('.').pop() || '';
// 根据文件类型选择预览方式
if (isVideo(fileSuf)) {
handlePreviewVideo(file);
} else if (isImage(fileSuf)) {
handlePreviewImage(file);
} else if (canPreview(fileSuf)) {
handlePreviewDocument(file);
} else {
// 不支持预览的文件类型,直接下载
downloadFileAction(file);
}
};
// 文档预览
const handlePreviewDocument = (file: FileInfo) => {
const fileUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const fileName = file.resourName ? `${file.resourName}.${file.resSuf}` : file.name;
const fileSuf = file.resSuf || file.name.split('.').pop() || '';
previewFileUtil(fileUrl, fileName, fileSuf)
.catch((error: any) => {
// 预览失败时尝试下载
downloadFileAction(file);
});
};
// 单个附件文档预览
const handlePreviewDocumentSingle = (fileUrl: string, fileName: string, fileFormat: string) => {
const fullFileName = fileFormat ? `${fileName}.${fileFormat}` : fileName;
previewFileUtil(fileUrl, fullFileName, fileFormat)
.catch((error: any) => {
// 预览失败时尝试下载
downloadSingleFile();
});
};
// 视频预览
const handlePreviewVideo = (file: FileInfo) => {
const videoUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
const videoName = file.resourName || file.name;
previewVideoUtil(videoUrl, videoName)
.catch((error: any) => {
// 视频预览失败时的处理
});
};
// 单个附件视频预览
const handlePreviewVideoSingle = (videoUrl: string, videoName: string) => {
previewVideoUtil(videoUrl, videoName)
.catch((error: any) => {
// 视频预览失败时的处理
});
};
// 图片预览
const handlePreviewImage = (file: FileInfo) => {
const imageUrl = file.resourUrl ? imagUrl(file.resourUrl) : file.url;
previewImageUtil(imageUrl)
.catch((error: any) => {
// 图片预览失败时的处理
});
};
// 单个附件图片预览
const handlePreviewImageSingle = (imageUrl: string) => {
previewImageUtil(imageUrl)
.catch((error: any) => {
// 图片预览失败时的处理
});
};
// 文件下载 - 支持选择下载路径
const downloadFileAction = (file: FileInfo, event?: Event) => {
// 手动阻止事件冒泡
if (event) {
event.stopPropagation();
event.preventDefault();
}
const finalUrl = file.resourUrl ? imagUrl(file.resourUrl) : imagUrl(file.url);
// 获取原始文件名
const originalFileName = getOriginalFileName(file);
console.log('下载文件:', {finalUrl, originalFileName, file});
// 显示下载路径选择弹窗
showDownloadPathModal(finalUrl, originalFileName);
};
// 格式化时间
const formatTime = (time: any) => {
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
};
// 格式化文件大小
const formatFileSize = (size: any) => {
if (!size) return "0B";
if (size < 1024) return size + "B";
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + "KB";
return (size / (1024 * 1024)).toFixed(2) + "MB";
};
// 获取文件名
const getFileName = (file: FileInfo) => {
if (file.resourName) {
return file.resourName;
}
if (file.name) {
// 如果name包含扩展名去掉扩展名
const lastDotIndex = file.name.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.name.substring(0, lastDotIndex);
}
return file.name;
}
return '未知文件';
};
// 获取文件后缀
const getFileSuffix = (file: FileInfo) => {
if (file.resSuf) {
return file.resSuf;
}
if (file.name) {
const lastDotIndex = file.name.lastIndexOf('.');
if (lastDotIndex > 0) {
return file.name.substring(lastDotIndex + 1);
}
}
return 'unknown';
};
// 获取文件图标
const getFileIcon = (fileType: string) => {
const type = fileType.toLowerCase();
switch (type) {
case 'pdf':
return '/static/base/view/pdf.png';
case 'doc':
case 'docx':
return '/static/base/view/word.png';
case 'xls':
case 'xlsx':
return '/static/base/view/excel.png';
case 'ppt':
case 'pptx':
return '/static/base/view/ppt.png';
case 'zip':
case 'rar':
case '7z':
return '/static/base/view/zip.png';
default:
return '/static/base/view/more.png';
}
};
// 获取状态样式类
const getStatusClass = (status: any) => {
const statusMap: Record<string, string> = {
draft: "status-draft",
pending: "status-pending",
approved: "status-approved",
rejected: "status-rejected",
};
return statusMap[status] || "status-default";
};
// 获取状态文本
const getStatusText = (status: any) => {
const statusMap: Record<string, string> = {
draft: "草稿",
pending: "待审批",
approved: "已通过",
rejected: "已驳回",
};
return statusMap[status] || "未知";
};
// 获取审批人状态样式类
const getApproverStatusClass = (status: any) => {
const statusMap: Record<string, string> = {
pending: "status-pending",
approved: "status-approved",
rejected: "status-rejected",
skipped: "status-skipped",
};
return statusMap[status] || "status-default";
};
// 获取审批人状态文本
const getApproverStatusText = (status: any) => {
const statusMap: Record<string, string> = {
pending: "待审批",
approved: "已同意",
rejected: "已驳回",
skipped: "已跳过",
};
return statusMap[status] || "未知";
};
// 获取抄送人状态样式类
const getCCStatusClass = (status: any) => {
const statusMap: Record<string, string> = {
unread: "status-unread",
read: "status-read",
};
return statusMap[status] || "status-default";
};
// 获取抄送人状态文本
const getCCStatusText = (status: any) => {
const statusMap: Record<string, string> = {
unread: "未读",
read: "已读",
};
return statusMap[status] || "未知";
};
// 获取紧急程度样式类
const getUrgencyClass = (level: string) => {
const levelMap: Record<string, string> = {
normal: "urgency-normal",
high: "urgency-high",
urgent: "urgency-urgent",
"普通": "urgency-normal", // 添加中文值映射
"高": "urgency-high", // 添加中文值映射
"紧急": "urgency-urgent", // 添加中文值映射
};
return levelMap[level] || "urgency-default";
};
// 获取紧急程度文本
const getUrgencyText = (level: string) => {
const levelMap: Record<string, string> = {
normal: "普通",
high: "高",
urgent: "紧急",
"普通": "普通", // 添加中文值映射
"高": "高", // 添加中文值映射
"紧急": "紧急", // 添加中文值映射
};
return levelMap[level] || "未知";
};
// 获取原始文件名
const getOriginalFileName = (file: FileInfo) => {
// 优先使用resourName如果没有则使用name确保文件名正确
let fileName = '';
if (file.resourName) {
// 如果resourName已经包含扩展名直接使用
if (file.resSuf && !file.resourName.includes('.' + file.resSuf)) {
fileName = `${file.resourName}.${file.resSuf}`;
} else {
fileName = file.resourName;
}
} else if (file.name) {
// 如果name已经包含扩展名直接使用
if (file.resSuf && !file.name.includes('.' + file.resSuf)) {
fileName = `${file.name}.${file.resSuf}`;
} else {
fileName = file.name;
}
} else {
// 最后备用方案从URL提取文件名
const urlFileName = file.url.split('/').pop() || '未知文件';
fileName = urlFileName;
}
return fileName;
};
// 显示下载路径选择弹窗
const showDownloadPathModal = (url: string, fileName: string) => {
downloadUrl.value = url;
downloadFileName.value = fileName;
showDownloadModal.value = true;
};
// 确认下载
const confirmDownload = () => {
// 设置下载状态
isDownloading.value = true;
// 检查当前平台
const systemInfo = uni.getSystemInfoSync();
const {platform} = systemInfo;
// 备用检测方法
const isH5 = platform === 'web' || typeof window !== 'undefined';
console.log('平台检测:', {platform, systemInfo, isH5});
if (isH5) {
console.log('使用H5下载方式');
// H5环境下使用直接链接下载避免跨域问题
downloadForH5(downloadUrl.value, downloadFileName.value);
} else {
console.log('使用原生下载方式');
// 原生环境下使用原有下载方式
downloadFile(downloadUrl.value, downloadFileName.value)
.then(() => {
// 下载成功后关闭弹窗
isDownloading.value = false;
showDownloadModal.value = false;
uni.showToast({
title: "下载成功",
icon: "success",
});
})
.catch((error) => {
console.error('下载失败:', error);
// 下载失败后也关闭弹窗
isDownloading.value = false;
showDownloadModal.value = false;
uni.showToast({
title: "下载失败",
icon: "error",
});
});
}
};
// H5环境下的下载方法
const downloadForH5 = (url: string, fileName: string) => {
try {
console.log('H5下载开始:', {url, fileName});
// 直接使用强制下载方式,避免打开文件
forceDownload(url, fileName);
} catch (error) {
console.error('H5下载失败:', error);
// 如果整个方法失败,回退到直接链接方式
fallbackDownload(url, fileName);
}
};
// 强制下载方法
const forceDownload = (url: string, fileName: string) => {
try {
console.log('尝试强制下载:', {url, fileName});
// 方法1: 使用fetch下载文件内容然后创建blob下载
fetch(url, {
method: 'GET',
mode: 'cors',
cache: 'no-cache'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(blob => {
console.log('文件下载成功创建blob下载');
// 创建blob URL
const blobUrl = window.URL.createObjectURL(blob);
// 创建下载链接
const link = document.createElement('a');
link.href = blobUrl;
link.download = fileName;
link.style.display = 'none';
// 添加到DOM并触发点击
document.body.appendChild(link);
link.click();
// 清理DOM和blob URL
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
// 下载成功后关闭弹窗
isDownloading.value = false;
showDownloadModal.value = false;
uni.showToast({
title: "下载成功",
icon: "success",
});
})
.catch(error => {
console.error('Fetch下载失败尝试其他方法:', error);
// 如果fetch失败尝试其他方法
tryAlternativeDownload(url, fileName);
});
} catch (error) {
console.error('强制下载失败,尝试其他方法:', error);
tryAlternativeDownload(url, fileName);
}
};
// 尝试替代下载方法
const tryAlternativeDownload = (url: string, fileName: string) => {
console.log('尝试替代下载方法');
// 方法1: 使用XMLHttpRequest下载
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
if (xhr.status === 200) {
const blob = xhr.response;
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
// 下载成功后关闭弹窗
isDownloading.value = false;
showDownloadModal.value = false;
uni.showToast({
title: "下载成功",
icon: "success",
});
console.log('XMLHttpRequest下载成功');
} else {
throw new Error(`HTTP error! status: ${xhr.status}`);
}
};
xhr.onerror = function () {
console.error('XMLHttpRequest下载失败');
console.log('调用手动下载提示');
showManualDownloadModal(url, fileName);
};
xhr.send();
} catch (error) {
console.error('XMLHttpRequest失败:', error);
console.log('调用手动下载提示catch');
showManualDownloadModal(url, fileName);
}
};
// 显示手动下载提示
const showManualDownloadModal = (url: string, fileName: string) => {
console.log('尝试原生下载');
// 尝试使用浏览器原生下载
try {
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.target = '_blank';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log('原生下载已触发');
// 延迟关闭弹窗,给用户时间看到下载开始
setTimeout(() => {
isDownloading.value = false;
showDownloadModal.value = false;
}, 1000);
uni.showToast({
title: "开始下载",
icon: "success",
});
} catch (error) {
console.error('原生下载失败:', error);
// 下载失败也关闭弹窗
isDownloading.value = false;
showDownloadModal.value = false;
uni.showToast({
title: "下载失败",
icon: "error",
});
}
};
// 直接下载方法
const directDownload = (url: string, fileName: string) => {
try {
// 修改URL添加下载参数
const downloadUrl = url.includes('?')
? `${url}&download=1&attachment=1`
: `${url}?download=1&attachment=1`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = fileName;
link.target = '_self'; // 使用_self而不是_blank
link.style.display = 'none';
// 添加到DOM并触发点击
document.body.appendChild(link);
link.click();
// 清理DOM
document.body.removeChild(link);
uni.showToast({
title: "开始下载",
icon: "success",
});
} catch (error) {
console.error('直接下载失败:', error);
// 最后的方法:提示用户手动下载
uni.showModal({
title: '下载提示',
content: `由于浏览器限制,无法自动下载文件。请长按链接手动保存:\n${url}`,
showCancel: false,
confirmText: '知道了'
});
}
};
// 备用下载方法
const fallbackDownload = (url: string, fileName: string) => {
try {
console.log('使用备用下载方法');
// 使用直接下载方法
directDownload(url, fileName);
} catch (error) {
console.error('备用下载也失败:', error);
// 最后的方法:提示用户手动下载
uni.showModal({
title: '下载提示',
content: `由于浏览器限制,无法自动下载文件。请长按链接手动保存:\n${url}`,
showCancel: false,
confirmText: '知道了'
});
}
};
// 取消下载
const cancelDownload = () => {
showDownloadModal.value = false;
};
// 获取创建人名称
const getCreatorName = (tjrId: string) => {
if (!tjrId) return "未知";
try {
// 尝试从多种缓存中获取用户信息参考gwAdd.vue的实现
const teacherCache = uni.getStorageSync('teacherCache');
const globalTeacherData = uni.getStorageSync('globalTeacherData');
const localData = uni.getStorageSync('data');
// 方式1: 从teacherCache获取
if (teacherCache && teacherCache[tjrId]) {
return teacherCache[tjrId].jsxm || tjrId;
}
// 方式2: 从globalTeacherData获取
if (globalTeacherData && globalTeacherData.length > 0) {
const teacher = globalTeacherData.find((t: any) => t.id === tjrId);
if (teacher && teacher.jsxm) {
return teacher.jsxm;
}
}
// 方式3: 从data缓存中的allJs获取参考gwAdd.vue
if (localData && localData.allJs && localData.allJs.result) {
const allTeachers = localData.allJs.result;
const teacher = allTeachers.find((t: any) => t.id === tjrId);
if (teacher && teacher.jsxm) {
return teacher.jsxm;
}
}
// 如果缓存中没有找到返回ID
return `用户${tjrId}`;
} catch (error) {
return `用户${tjrId}`;
}
};
onLoad(async (data?: any) => {
const ret = await GwPageUtils.init(data);
if (!ret) {
return;
}
showButton.value = ret.dbFlag;
gwId.value = ret.gwId;
getGwInfo();
});
</script>
<style lang="scss" scoped>
.gw-info-section,
.file-section,
.approver-section,
.cc-section,
.log-section {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.log-section {
margin-bottom: 40px; // 操作记录区域增加底部间距
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #007aff;
padding-bottom: 5px;
}
.info-item {
display: flex;
margin-bottom: 15px;
.label {
width: 80px;
color: #666;
font-size: 14px;
}
.value {
flex: 1;
color: #333;
font-size: 14px;
&.title-bold {
font-weight: bold;
font-size: 16px;
}
}
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
&.status-draft {
background: #f0f0f0;
color: #666;
}
&.status-pending {
background: #fff7e6;
color: #fa8c16;
}
&.status-approved {
background: #f6ffed;
color: #52c41a;
}
&.status-rejected {
background: #fff2f0;
color: #ff4d4f;
}
}
.urgency-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
&.urgency-normal {
background: #f0f0f0;
color: #666;
}
&.urgency-high {
background: #fff7e6;
color: #fa8c16;
}
&.urgency-urgent {
background: #fff2f0;
color: #ff4d4f;
}
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
border: 1px solid #eee;
border-radius: 8px;
margin-bottom: 10px;
background: #fafafa;
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-icon {
margin-right: 12px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
.icon-image {
width: 28px;
height: 28px;
}
}
.file-info {
flex: 1;
.file-name {
display: block;
font-weight: 500;
margin-bottom: 4px;
color: #333;
font-size: 14px;
}
.file-size {
font-size: 12px;
color: #666;
margin-right: 8px;
}
.file-type {
font-size: 11px;
color: #999;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
}
}
.file-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: flex-end;
.u-button {
min-width: 20px;
height: 26px;
font-size: 12px;
border-radius: 13px;
padding: 0 6px;
}
}
}
.no-files {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
.approver-item,
.cc-item {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
.approver-info,
.cc-info {
flex: 1;
.order {
background: #007aff;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
margin-right: 8px;
}
.name {
font-weight: 500;
margin-right: 8px;
}
.dept {
color: #666;
font-size: 12px;
margin-right: 8px;
}
.status {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
&.status-pending {
background: #fff7e6;
color: #fa8c16;
}
&.status-approved {
background: #f6ffed;
color: #52c41a;
}
&.status-rejected {
background: #fff2f0;
color: #ff4d4f;
}
&.status-skipped {
background: #f0f0f0;
color: #666;
}
&.status-unread {
background: #fff7e6;
color: #fa8c16;
}
&.status-read {
background: #f6ffed;
color: #52c41a;
}
}
}
}
.approver-detail {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #007aff;
.approval-info {
display: flex;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
.info-label {
width: 80px;
color: #666;
font-size: 12px;
flex-shrink: 0;
}
.info-value {
flex: 1;
color: #333;
font-size: 12px;
word-break: break-all;
}
}
}
.more-button {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
margin-top: 10px;
margin-bottom: 20px; // 增加底部间距,避免被底部按钮挡住
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
&:active {
background: #e9ecef;
transform: translateY(1px);
}
.more-text {
font-size: 14px;
color: #007aff;
margin-right: 6px;
}
.more-icon {
font-size: 12px;
color: #007aff;
transition: transform 0.3s ease;
&.expanded {
transform: rotate(180deg);
}
}
}
.log-item {
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
.log-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.operator {
font-weight: 500;
color: #007aff;
}
.time {
font-size: 12px;
color: #666;
}
}
.log-content {
margin-bottom: 8px;
.type {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
margin-right: 8px;
}
.content {
color: #333;
}
}
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 15px;
border-top: 1px solid #eee;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
display: flex;
gap: 15px;
z-index: 1000;
}
.bottom-actions .u-button {
flex: 1;
height: 44px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
}
.modal-content {
padding: 20px;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.modal-title {
font-size: 16px;
font-weight: bold;
}
}
.search-section {
margin-bottom: 20px;
}
.position-section {
display: flex;
align-items: center;
.position-label {
margin-right: 10px;
}
}
}
.detail-modal {
width: 80vw;
max-width: 400px;
padding: 20px;
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.detail-title {
font-size: 16px;
font-weight: bold;
}
}
.detail-content {
.detail-item {
margin-bottom: 15px;
.detail-label {
font-weight: 500;
color: #666;
margin-right: 10px;
}
.detail-value {
color: #333;
word-break: break-all;
}
}
}
}
.download-modal {
width: 85vw;
max-width: 450px;
padding: 20px;
background: #fff;
border-radius: 12px;
.download-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
.download-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
}
.download-content {
margin-bottom: 25px;
.file-info-section {
display: flex;
align-items: flex-start;
margin-bottom: 15px;
.file-label {
font-weight: 500;
color: #666;
margin-right: 10px;
flex-shrink: 0;
width: 60px;
}
.file-name {
color: #333;
font-size: 14px;
word-break: break-all;
flex: 1;
background: #f8f9fa;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #e9ecef;
}
}
.path-info-section {
display: flex;
align-items: center;
margin-bottom: 15px;
.path-label {
font-weight: 500;
color: #666;
margin-right: 10px;
flex-shrink: 0;
width: 60px;
}
.path-text {
color: #007aff;
font-size: 14px;
flex: 1;
}
}
.download-tips {
text-align: center;
.tips-text {
color: #999;
font-size: 12px;
}
}
}
.download-actions {
display: flex;
justify-content: center;
gap: 10px;
.u-button {
flex: 1;
max-width: 120px;
}
}
.download-progress {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
.progress-spinner {
width: 24px;
height: 24px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007aff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.progress-text {
color: #666;
font-size: 14px;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>