feat(documents): refresh detail pages and attachment cards

This commit is contained in:
Cheng Zhou
2026-06-08 11:03:34 +08:00
parent a3f6a04f35
commit 834b4f1d48
11 changed files with 2372 additions and 929 deletions
@@ -55,19 +55,28 @@
</el-table-column>
</el-table>
</template>
<div v-else class="upload-grid">
<div v-for="group in uploadGroups" :key="group.key" class="attachment-upload-card">
<div v-if="showUploadCardLabels" class="upload-card-label">{{ group.label }}</div>
<div v-else class="upload-grid" :class="{ 'upload-grid--three': uploadCardColumns === 3 }">
<div v-for="group in uploadGroups" :key="group.key" class="attachment-upload-item">
<el-upload
class="attachment-card-upload"
:show-file-list="false"
multiple
:disabled="!canUploadGroup(group) || isUploading(group.key)"
:auto-upload="false"
:on-change="(file: any) => queuePendingUpload(group.key, file)"
>
<div class="upload-trigger" :class="{ 'is-disabled': !canUploadGroup(group) }">
<el-icon class="upload-icon"><Document /></el-icon>
<span class="upload-text">点击上传文件</span>
<div
class="attachment-upload-card"
:class="{
'is-disabled': !canUploadGroup(group),
'attachment-upload-card--centered': centerUploadCardContent,
}"
>
<div v-if="showUploadCardLabels" class="upload-card-label">{{ group.label }}</div>
<div class="upload-trigger">
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<span class="upload-text">{{ group.uploadText || "点击上传文件" }}</span>
</div>
</div>
</el-upload>
<div v-if="pendingMap[group.key]?.length" class="pending-list">
@@ -112,7 +121,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Document } from "@element-plus/icons-vue";
import { UploadFilled } from "@element-plus/icons-vue";
import { fetchAttachments, deleteAttachment, uploadAttachment } from "../../api/attachments";
import { formatFileSize } from "./attachmentUtils";
import { useAuthStore } from "../../store/auth";
@@ -126,12 +135,14 @@ import { TEXT } from "../../locales";
type AttachmentEntityGroup = {
entityType: string;
label: string;
uploadText?: string;
};
type UploadGroup = {
key: string;
label: string;
entityType: string;
uploadText?: string;
};
type PendingUploadStatus = "pending" | "uploading" | "failed";
@@ -153,6 +164,9 @@ const props = defineProps<{
mode?: "table" | "upload";
maxSizeMb?: number;
refreshKey?: number;
hideUploadCardLabels?: boolean;
centerUploadCardContent?: boolean;
uploadCardColumns?: 2 | 3;
}>();
const attachments = ref<any[]>([]);
@@ -178,11 +192,13 @@ const uploadGroups = computed<UploadGroup[]>(() => {
key: group.entityType,
label: group.label,
entityType: group.entityType,
uploadText: group.uploadText,
}));
}
return [{ key: props.entityType, label: headerTitle.value, entityType: props.entityType }];
});
const showUploadCardLabels = computed(() => uploadGroups.value.length > 1);
const showUploadCardLabels = computed(() => !props.hideUploadCardLabels && uploadGroups.value.length > 1);
const uploadCardColumns = computed(() => props.uploadCardColumns || 2);
const currentRolePermissions = computed(() => {
const role = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
@@ -494,20 +510,64 @@ defineExpose({
gap: 16px;
}
.upload-grid--three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.attachment-upload-item {
min-width: 0;
}
.attachment-card-upload,
.attachment-card-upload :deep(.el-upload) {
display: block;
width: 100%;
}
.attachment-upload-card {
min-height: 72px;
padding: 14px 16px;
border: 1px dashed #d0dced;
border-radius: 8px;
background: #ffffff;
transition: border-color 0.2s ease, background-color 0.2s ease;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.attachment-upload-card:hover {
.attachment-upload-card:hover,
.attachment-upload-card:focus-within {
border-color: var(--ctms-primary);
background: #f8faff;
}
.attachment-upload-card.is-disabled {
cursor: not-allowed;
opacity: 0.62;
}
.attachment-upload-card--centered {
min-height: 112px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.attachment-upload-card--centered .upload-card-label {
align-self: flex-start;
margin-bottom: 0;
text-align: left;
}
.attachment-upload-card--centered .upload-trigger {
flex: 1;
flex-direction: column;
justify-content: center;
gap: 10px;
width: 100%;
}
.upload-card-label {
margin-bottom: 10px;
color: #4a6283;
@@ -520,25 +580,22 @@ defineExpose({
align-items: center;
gap: 8px;
min-height: 34px;
cursor: pointer;
}
.upload-trigger.is-disabled {
cursor: not-allowed;
opacity: 0.62;
}
.upload-icon {
font-size: 18px;
font-size: 22px;
line-height: 1;
color: #314a6b;
}
.upload-text {
color: var(--ctms-text-secondary);
font-size: 13px;
line-height: 1.4;
}
.upload-trigger:not(.is-disabled):hover .upload-text {
.attachment-upload-card:not(.is-disabled):hover .upload-text,
.attachment-upload-card:not(.is-disabled):hover .upload-icon {
color: var(--ctms-primary);
}