1716 lines
42 KiB
Vue
1716 lines
42 KiB
Vue
<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>
|