功能(文档与桌面):完善文件预览下载与客户端构建基线
- 保存文档版本原始文件名,规范下载响应并持久化上传目录\n- 增加 PDF.js 预览、桌面保存打开流程及统一错误反馈\n- 统一 Node.js 22.13 构建基线并收紧临时文件权限门禁\n- 补充迁移、单元测试、发布检查与运维文档
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="attachment-list-panel unified-shell">
|
||||
<div
|
||||
class="attachment-list-panel"
|
||||
:class="{
|
||||
'unified-shell': displayMode === 'table',
|
||||
'attachment-list-panel--upload': displayMode === 'upload',
|
||||
}"
|
||||
>
|
||||
<template v-if="displayMode === 'table'">
|
||||
<div v-if="showHeader" class="header">
|
||||
<span>{{ headerTitle }}</span>
|
||||
@@ -60,7 +66,14 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div v-else class="upload-grid" :class="{ 'upload-grid--three': uploadCardColumns === 3 }">
|
||||
<div
|
||||
v-else
|
||||
class="upload-grid"
|
||||
:class="{
|
||||
'upload-grid--single': uploadGroups.length === 1,
|
||||
'upload-grid--three': uploadGroups.length > 1 && uploadCardColumns === 3,
|
||||
}"
|
||||
>
|
||||
<div v-for="group in uploadGroups" :key="group.key" class="attachment-upload-item">
|
||||
<el-upload
|
||||
v-if="!nativeFiles"
|
||||
@@ -92,7 +105,10 @@
|
||||
'is-disabled': !canUploadGroup(group),
|
||||
'attachment-upload-card--centered': centerUploadCardContent,
|
||||
}"
|
||||
role="button"
|
||||
:tabindex="canUploadGroup(group) ? 0 : -1"
|
||||
@click="pickPendingNative(group)"
|
||||
@keydown.enter.space.prevent="pickPendingNative(group)"
|
||||
>
|
||||
<div v-if="showUploadCardLabels" class="upload-card-label">{{ group.label }}</div>
|
||||
<div class="upload-trigger">
|
||||
@@ -107,11 +123,17 @@
|
||||
class="pending-file"
|
||||
:class="{ 'is-failed': item.status === 'failed' }"
|
||||
>
|
||||
<span class="pending-file-name">{{ item.file.name }}</span>
|
||||
<span v-if="item.status === 'failed'" class="pending-file-status">
|
||||
{{ item.error || TEXT.common.messages.uploadFailed }}
|
||||
<span class="pending-file-icon" aria-hidden="true">
|
||||
<el-icon><Document /></el-icon>
|
||||
</span>
|
||||
<el-button link type="danger" size="small" @click="removePendingUpload(group.key, item)">
|
||||
<span class="pending-file-info">
|
||||
<span class="pending-file-name">{{ item.file.name }}</span>
|
||||
<span class="pending-file-meta">
|
||||
<span>{{ formatFileSize(item.file.size) }}</span>
|
||||
<span class="pending-file-status">{{ pendingStatusText(item) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<el-button class="pending-file-remove" link type="danger" size="small" @click="removePendingUpload(group.key, item)">
|
||||
{{ TEXT.common.actions.remove }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -125,13 +147,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-if="previewVisible" append-to-body v-model="previewVisible" :title="previewTitle" width="720px" :close-on-click-modal="false">
|
||||
<el-dialog v-if="previewVisible" append-to-body v-model="previewVisible" :title="previewTitle" :width="previewType === 'pdf' ? 'min(86vw, 1440px)' : 'min(1180px, 94vw)'" :top="previewType === 'pdf' ? '8vh' : '3vh'" :close-on-click-modal="false" :show-close="previewType !== 'pdf'" :class="['pdf-preview-dialog', { 'pdf-preview-dialog--flush': previewType === 'pdf' }]">
|
||||
<div v-if="previewError" class="preview-error">
|
||||
{{ previewError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<img v-if="previewType === 'image'" :src="previewUrl" class="preview-media" />
|
||||
<iframe v-else-if="previewType === 'pdf'" :src="previewUrl" class="preview-frame" />
|
||||
<PdfViewer v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" :filename="previewTitle" show-close @close="previewVisible = false" />
|
||||
<div v-else class="preview-error">
|
||||
{{ TEXT.common.messages.previewNotSupported }}
|
||||
</div>
|
||||
@@ -140,9 +162,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { UploadFilled } from "@element-plus/icons-vue";
|
||||
import { Document, UploadFilled } from "@element-plus/icons-vue";
|
||||
import { fetchAttachments, deleteAttachment, downloadAttachment, uploadAttachment } from "../../api/attachments";
|
||||
import { formatFileSize } from "./attachmentUtils";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
@@ -162,6 +184,8 @@ type AttachmentEntityGroup = {
|
||||
uploadText?: string;
|
||||
};
|
||||
|
||||
const PdfViewer = defineAsyncComponent(() => import("../PdfViewer.vue"));
|
||||
|
||||
type UploadGroup = {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -291,6 +315,12 @@ const removePendingUpload = (fileType: string, item: PendingUploadItem) => {
|
||||
pendingMap[fileType] = (pendingMap[fileType] || []).filter((current) => pendingFileKey(current.file) !== pendingFileKey(item.file));
|
||||
};
|
||||
|
||||
const pendingStatusText = (item: PendingUploadItem) => {
|
||||
if (item.status === "uploading") return "正在上传";
|
||||
if (item.status === "failed") return item.error || TEXT.common.messages.uploadFailed;
|
||||
return "待上传";
|
||||
};
|
||||
|
||||
const pendingSnapshot = () =>
|
||||
uploadGroups.value.flatMap((group) => (pendingMap[group.key] || []).map((item) => `${group.key}:${pendingFileKey(item.file)}`));
|
||||
const hasPendingUploads = computed(() => uploadGroups.value.some((group) => (pendingMap[group.key] || []).length > 0));
|
||||
@@ -580,6 +610,11 @@ defineExpose({
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.attachment-list-panel--upload {
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -621,6 +656,10 @@ defineExpose({
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.upload-grid--single {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.attachment-upload-item {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -632,6 +671,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.attachment-upload-card {
|
||||
box-sizing: border-box;
|
||||
min-height: 72px;
|
||||
padding: 14px 16px;
|
||||
border: 1px dashed #d0dced;
|
||||
@@ -641,6 +681,14 @@ defineExpose({
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-grid--single .attachment-upload-card {
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.upload-grid--single .upload-trigger {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.attachment-upload-card:hover,
|
||||
.attachment-upload-card:focus-within {
|
||||
border-color: var(--ctms-primary);
|
||||
@@ -709,50 +757,90 @@ defineExpose({
|
||||
.pending-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pending-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
min-height: 52px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: var(--ctms-text-regular);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pending-file.is-failed {
|
||||
border-color: #fecaca;
|
||||
background: #fff7f7;
|
||||
color: var(--ctms-danger, #f56c6c);
|
||||
}
|
||||
|
||||
.pending-file-name {
|
||||
.pending-file-icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
background: #eaf1fb;
|
||||
color: #314a6b;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.pending-file-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.pending-file-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--ctms-text-main);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pending-file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pending-file-status {
|
||||
flex: 0 0 auto;
|
||||
color: var(--ctms-danger, #f56c6c);
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pending-file.is-failed .pending-file-status {
|
||||
color: var(--ctms-danger, #f56c6c);
|
||||
}
|
||||
|
||||
.pending-file-remove {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.upload-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
width: 100%;
|
||||
height: 520px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview-media {
|
||||
max-width: 100%;
|
||||
max-height: 520px;
|
||||
|
||||
Reference in New Issue
Block a user