|
|
@@ -428,6 +428,120 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 文件上传 -->
|
|
|
+ <div class="c_t15" v-if="workDetail && workDetail.type === '84' && props.showData">
|
|
|
+ <div class="c_t15_title">
|
|
|
+ <div class="c_title">
|
|
|
+ <span>{{ workDetail.json.answerQ }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span class="c_t15_type">{{ lang.ssFile }}</span>
|
|
|
+ <div class="c_t15_msg" v-if="props.roleType == 1">
|
|
|
+ <div>{{ lang.ssAnswerCount }} {{ props.workArray.length }}<span
|
|
|
+ v-if="props.unsubmittedStudents.length > 0">/{{ props.workArray.length +
|
|
|
+ props.unsubmittedStudents.length
|
|
|
+ }}</span></div>
|
|
|
+ <span v-if="props.unsubmittedStudents.length > 0" @click.stop="viewUnsubmittedStudents()">{{
|
|
|
+ lang.ssViewSubmitStatus2 }}</span>
|
|
|
+ <div class="switch_btn" v-if="props.isCreator && props.roleType == 1">
|
|
|
+ <div class="switch">
|
|
|
+ <Switch :value="canValue" @update:value="handleCanChange" />
|
|
|
+ <span>{{ lang.ssShowResult }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="switch">
|
|
|
+ <Switch :value="likeValue" @update:value="handleLikeChange" />
|
|
|
+ <span>{{ lang.ssLike }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="downloadBtn" @click.stop="downloadAll" :class="{ disabled: downloadLoading }"
|
|
|
+ v-if="props.roleType == 1">
|
|
|
+ <IconDownload />
|
|
|
+ <span>{{ downloadLoading ? lang.ssDownloadLoading : lang.ssBatchDownload }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="c_t15_content" v-show="!lookWorkData">
|
|
|
+ <div class="c_t15_c_item" v-for="item in processedWorkArray" :key="item.id"
|
|
|
+ @click.stop="item.content && item.content.fileList.length > 0 ? lookWork(item.id) : ''">
|
|
|
+ <div class="c_t15_c_i_top">
|
|
|
+ <span>{{ item.name.charAt(0) }}</span>
|
|
|
+ <div>{{ item.name }}</div>
|
|
|
+ <div class="cast_sreen_btn" @click.stop="castScreen(item)"
|
|
|
+ v-if="props.isCreator && props.roleType == 1 && isFollowModeActive && canValue">
|
|
|
+ <IconCastScreen />
|
|
|
+ {{ lang.ssCastScreen }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bottom_btn" v-if="likeValue">
|
|
|
+ <div class="bottom_like" :class="{ 'active': item.isLikes }" @click.stop="handleLikeClick(item)">
|
|
|
+ <IconThumbsUp />
|
|
|
+ <span>{{ item.likesCount }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="c_t79_workDetail" v-if="lookWorkData" @click.stop="clickContent(false)">
|
|
|
+ <div class="c_t79_wd_top">
|
|
|
+ <img src="../../../assets/img/arrow_left.png" @click.stop="lookWork('')" v-if="!isCastScreen" />
|
|
|
+ <span>{{ lookWorkData.name.charAt(0) }}</span>
|
|
|
+ <div>{{ lookWorkData.name }}</div>
|
|
|
+ <div class="cast_sreen_btn" @click.stop="exitCastScreen"
|
|
|
+ v-if="(props.isCreator && props.roleType == 1 && isCastScreen) || (!isFollowModeActive && isCastScreen)">
|
|
|
+ <IconCastScreen />
|
|
|
+ {{ lang.ssExitCastScreen }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="c_t79_wd_content">
|
|
|
+ <!-- 图片 -->
|
|
|
+ <img
|
|
|
+ v-if="getFileType(lookWorkData.content.fileList[lookWorkIndex].url) === 'image'"
|
|
|
+ :src="lookWorkData.content.fileList[lookWorkIndex].url"
|
|
|
+ @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])" />
|
|
|
+ <!-- 视频 -->
|
|
|
+ <video
|
|
|
+ v-else-if="getFileType(lookWorkData.content.fileList[lookWorkIndex].url) === 'video'"
|
|
|
+ :src="lookWorkData.content.fileList[lookWorkIndex].url"
|
|
|
+ controls
|
|
|
+ @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])" />
|
|
|
+ <!-- 文档/PDF/Office -->
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="file-preview-card"
|
|
|
+ @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])">
|
|
|
+ <div class="file-preview-icon">
|
|
|
+ {{ getFileIcon(getFileType(lookWorkData.content.fileList[lookWorkIndex].url)) }}
|
|
|
+ </div>
|
|
|
+ <div class="file-preview-name">
|
|
|
+ {{ lookWorkData.content.fileList[lookWorkIndex].name || '文件' }}
|
|
|
+ </div>
|
|
|
+ <div class="file-preview-action">{{ lang.ssClickToPreview }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- <div class="nextAndUpBtn" v-if="lookWorkData.content.fileList.length>1">
|
|
|
+ <span :class="{no_active:lookWorkIndex==0}" @click="changelookWorkIndex(0)">{{ lang.ssPrevP }}</span>
|
|
|
+ <span :class="{no_active:lookWorkData.content.fileList.length-1<=lookWorkIndex}" @click="changelookWorkIndex(1)">{{ lang.ssNextP }}</span>
|
|
|
+ </div> -->
|
|
|
+ <div class="cq_changeBtn" v-if="lookWorkData.content.fileList.length > 1">
|
|
|
+ <div :class="{ cq_cb_disabled: lookWorkIndex <= 0 }" @click.stop="changelookWorkIndex(0)">
|
|
|
+ <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
|
|
|
+ <path
|
|
|
+ d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
|
|
|
+ fill=""></path>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <span>{{ lookWorkIndex + 1 }}/{{ lookWorkData.content.fileList.length }}</span>
|
|
|
+ <div :class="{ cq_cb_disabled: lookWorkIndex >= lookWorkData.content.fileList.length - 1 }"
|
|
|
+ @click.stop="changelookWorkIndex(1)">
|
|
|
+ <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
|
|
|
+ <path
|
|
|
+ d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
|
|
|
+ fill=""></path>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- AI应用 -->
|
|
|
<div class="c_t72" v-if="props.showData && props.showData.toolType === 72">
|
|
|
<div class="c_t72_title">
|
|
|
@@ -739,6 +853,44 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<previewImageTool ref="previewImageToolRef" />
|
|
|
+
|
|
|
+ <!-- 视频预览弹窗 -->
|
|
|
+ <Teleport to="body">
|
|
|
+ <div v-if="videoPreviewVisible" class="file-preview-modal" @click.self="closeVideoPreview">
|
|
|
+ <div class="file-preview-close" @click="closeVideoPreview">
|
|
|
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18" />
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <video :src="videoPreviewUrl" controls autoplay class="file-preview-video"></video>
|
|
|
+ </div>
|
|
|
+ </Teleport>
|
|
|
+
|
|
|
+ <!-- 文档预览弹窗 -->
|
|
|
+ <Teleport to="body">
|
|
|
+ <div v-if="docPreviewVisible" class="file-preview-modal" @click.self="closeDocPreview">
|
|
|
+ <div class="file-preview-close" @click="closeDocPreview">
|
|
|
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18" />
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <iframe
|
|
|
+ v-if="docPreviewType === 'pdf'"
|
|
|
+ :src="docPreviewUrl"
|
|
|
+ class="file-preview-frame"
|
|
|
+ frameborder="0"
|
|
|
+ ></iframe>
|
|
|
+ <div v-else class="file-preview-office">
|
|
|
+ <div class="file-preview-office-icon">📝</div>
|
|
|
+ <div class="file-preview-office-name">{{ docPreviewUrl.split('/').pop() }}</div>
|
|
|
+ <a :href="docPreviewUrl" target="_blank" class="file-preview-office-link">
|
|
|
+ {{ lang.ssClickToOpen }}
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Teleport>
|
|
|
<selectUserDialog ref="selectUserDialogRef" />
|
|
|
<echartsDialog ref="echartsDialogRef" />
|
|
|
</div>
|
|
|
@@ -1032,13 +1184,68 @@ const lookWorkData = computed(() => {
|
|
|
return _result
|
|
|
})
|
|
|
|
|
|
-// 查看图片
|
|
|
-const lookImage = (url: string) => {
|
|
|
- if (previewImageToolRef.value) {
|
|
|
- previewImageToolRef.value.previewImage(url)
|
|
|
+// 判断文件类型
|
|
|
+const getFileType = (url: string): 'image' | 'video' | 'pdf' | 'office' | 'other' => {
|
|
|
+ if (!url) return 'other'
|
|
|
+ const lower = url.toLowerCase().split('?')[0]
|
|
|
+ if (/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(lower)) return 'image'
|
|
|
+ if (/\.(mp4|webm|ogg|mov|avi)$/.test(lower)) return 'video'
|
|
|
+ if (/\.(pdf)$/.test(lower)) return 'pdf'
|
|
|
+ if (/\.(doc|docx|xls|xlsx|ppt|pptx)$/.test(lower)) return 'office'
|
|
|
+ return 'other'
|
|
|
+}
|
|
|
+
|
|
|
+const getFileIcon = (type: string) => {
|
|
|
+ switch (type) {
|
|
|
+ case 'pdf': return '📄'
|
|
|
+ case 'office': return '📝'
|
|
|
+ case 'video': return '🎬'
|
|
|
+ default: return '📎'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 查看文件(图片/视频/文档)
|
|
|
+const videoPreviewVisible = ref(false)
|
|
|
+const videoPreviewUrl = ref('')
|
|
|
+const docPreviewVisible = ref(false)
|
|
|
+const docPreviewUrl = ref('')
|
|
|
+const docPreviewType = ref('')
|
|
|
+
|
|
|
+const lookImage = (file: any) => {
|
|
|
+ const url = typeof file === 'string' ? file : file?.url
|
|
|
+ if (!url) return
|
|
|
+ const type = getFileType(url)
|
|
|
+ if (type === 'image') {
|
|
|
+ if (previewImageToolRef.value) {
|
|
|
+ previewImageToolRef.value.previewImage(url)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (type === 'video') {
|
|
|
+ videoPreviewUrl.value = url
|
|
|
+ videoPreviewVisible.value = true
|
|
|
+ document.body.style.overflow = 'hidden'
|
|
|
+ }
|
|
|
+ else if (type === 'pdf' || type === 'office') {
|
|
|
+ docPreviewUrl.value = url
|
|
|
+ docPreviewType.value = type
|
|
|
+ docPreviewVisible.value = true
|
|
|
+ document.body.style.overflow = 'hidden'
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ window.open(url, '_blank')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const closeVideoPreview = () => {
|
|
|
+ videoPreviewVisible.value = false
|
|
|
+ document.body.style.overflow = ''
|
|
|
+}
|
|
|
+
|
|
|
+const closeDocPreview = () => {
|
|
|
+ docPreviewVisible.value = false
|
|
|
+ document.body.style.overflow = ''
|
|
|
+}
|
|
|
+
|
|
|
const lookWorkIndex = ref<number>(0)
|
|
|
// 查看作业
|
|
|
const lookWork = (id: string) => {
|
|
|
@@ -3800,4 +4007,147 @@ onUnmounted(() => {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* 文件预览卡片 */
|
|
|
+.file-preview-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 24px;
|
|
|
+ min-height: 200px;
|
|
|
+ background: #f9fafb;
|
|
|
+ border: 2px dashed #d1d5db;
|
|
|
+ border-radius: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #FF9300;
|
|
|
+ background: #fffbeb;
|
|
|
+ }
|
|
|
+
|
|
|
+ &--mini {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ padding: 0;
|
|
|
+ min-height: unset;
|
|
|
+ gap: 0;
|
|
|
+ border-style: solid;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-card--mini .file-preview-icon {
|
|
|
+ font-size: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-name {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #374151;
|
|
|
+ max-width: 200px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-action {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #3b82f6;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 文件预览弹窗 */
|
|
|
+.file-preview-modal {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.85);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 9999;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-close {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(255, 255, 255, 0.15);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.25);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-video {
|
|
|
+ max-width: 90vw;
|
|
|
+ max-height: 85vh;
|
|
|
+ border-radius: 8px;
|
|
|
+ object-fit: contain;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-frame {
|
|
|
+ width: 90vw;
|
|
|
+ height: 85vh;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: none;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-office {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 40px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ min-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-office-icon {
|
|
|
+ font-size: 64px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-office-name {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #374151;
|
|
|
+ max-width: 300px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.file-preview-office-link {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 10px 24px;
|
|
|
+ background: #3b82f6;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ text-decoration: none;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ transition: background 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #2563eb;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|