|
|
@@ -167,15 +167,23 @@
|
|
|
<div class="student-photo">
|
|
|
<el-avatar
|
|
|
:size="100"
|
|
|
- :src="item.photoUrl"
|
|
|
- :icon="!item.photoUrl ? (item.gender === '0' ? 'Male' : 'Female') : undefined"
|
|
|
+ :src="getStudentPhotoUrl(item)"
|
|
|
+ :icon="!getStudentPhotoUrl(item) ? (item.gender === '0' ? 'Male' : 'Female') : undefined"
|
|
|
:class="{
|
|
|
- 'male-placeholder': !item.photoUrl && item.gender === '0',
|
|
|
- 'female-placeholder': !item.photoUrl && item.gender === '1'
|
|
|
+ 'male-placeholder': !getStudentPhotoUrl(item) && item.gender === '0',
|
|
|
+ 'female-placeholder': !getStudentPhotoUrl(item) && item.gender === '1'
|
|
|
}"
|
|
|
>
|
|
|
- <template v-if="!item.photoUrl">
|
|
|
+ <template v-if="!getStudentPhotoUrl(item)">
|
|
|
+ <!-- 加载中状态 -->
|
|
|
+ <div v-if="isPhotoLoading(item)" class="photo-loading-container">
|
|
|
+ <el-icon class="is-loading" size="24" color="#409eff">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <!-- 无照片状态 -->
|
|
|
<div
|
|
|
+ v-else
|
|
|
class="no-photo-placeholder"
|
|
|
:class="{ 'male-placeholder': item.gender === '0', 'female-placeholder': item.gender === '1' }"
|
|
|
>
|
|
|
@@ -187,6 +195,15 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-avatar>
|
|
|
+ <!-- 照片加载中状态文字 - 显示在头像底部 -->
|
|
|
+ <div v-if="isPhotoLoading(item)" class="photo-loading-text">
|
|
|
+ <el-text type="primary" size="small">
|
|
|
+ <el-icon class="is-loading mr-1" size="12">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ 图像加载中...
|
|
|
+ </el-text>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="student-info">
|
|
|
<h3 class="student-name">{{ item.name }}</h3>
|
|
|
@@ -281,10 +298,11 @@
|
|
|
import { ref, reactive, toRefs, watchEffect, onMounted, computed, getCurrentInstance } from 'vue';
|
|
|
import ElMessage from 'element-plus/es/components/message/index';
|
|
|
import ElMessageBox from 'element-plus/es/components/message-box/index';
|
|
|
-import { Search, Refresh, Setting, Download, List, Grid, View, Picture, Male, Female } from '@element-plus/icons-vue';
|
|
|
+import { Search, Refresh, Setting, Download, List, Grid, View, Picture, Male, Female, Loading } from '@element-plus/icons-vue';
|
|
|
import AdvancedSearchDialog from './components/AdvancedSearchDialog.vue';
|
|
|
import StudentDetailDialog from './components/StudentDetailDialog.vue';
|
|
|
import api from '@/api/xg/xsjcxx/stuRoster/index';
|
|
|
+import request from '@/utils/request';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as any;
|
|
|
|
|
|
@@ -297,6 +315,10 @@ const deptName = ref('');
|
|
|
const deptTreeRef = ref();
|
|
|
const selectedDepts = ref<number[]>([]);
|
|
|
|
|
|
+// 学生照片缓存
|
|
|
+const studentPhotoCache = ref<Map<string, string>>(new Map());
|
|
|
+const photoLoadingCache = ref<Map<string, boolean>>(new Map());
|
|
|
+
|
|
|
// 计算表格最大高度
|
|
|
const tableMaxHeight = computed(() => {
|
|
|
// 基础高度:视窗高度减去头部、搜索区域、工具栏、分页等区域的高度
|
|
|
@@ -834,6 +856,99 @@ const getStudentStatusLabel = (status: string) => {
|
|
|
return statusMap[status] || '未知';
|
|
|
};
|
|
|
|
|
|
+// 通过认证请求获取图片数据并转换为blob URL
|
|
|
+const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
|
|
+ try {
|
|
|
+ // 通过axios请求图片数据,这样会自动带上token和clientId
|
|
|
+ const response = await request({
|
|
|
+ url: '/resource/oss/download/' + imageUrl,
|
|
|
+ method: 'get',
|
|
|
+ responseType: 'blob',
|
|
|
+ timeout: 300000 // 300秒超时
|
|
|
+ });
|
|
|
+
|
|
|
+ // 由于axios响应拦截器对blob类型响应直接返回blob数据
|
|
|
+ // 这里response直接就是blob对象,而不是包含data属性的响应对象
|
|
|
+ const blob = response;
|
|
|
+
|
|
|
+ // 验证响应数据
|
|
|
+ if (!blob) {
|
|
|
+ throw new Error('响应数据为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!(blob instanceof Blob)) {
|
|
|
+ // 如果不是Blob,尝试获取实际内容用于调试
|
|
|
+ let actualContent = 'Unknown';
|
|
|
+ try {
|
|
|
+ if (typeof blob === 'string') {
|
|
|
+ actualContent = (blob as string).substring(0, 200);
|
|
|
+ } else if (blob && typeof blob.toString === 'function') {
|
|
|
+ actualContent = blob.toString().substring(0, 200);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ actualContent = 'Cannot convert to string';
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建blob URL
|
|
|
+ const blobUrl = URL.createObjectURL(blob);
|
|
|
+ return blobUrl;
|
|
|
+ } catch (error) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取学生照片URL(带缓存)
|
|
|
+const getStudentPhotoUrl = (student: any): string => {
|
|
|
+ const photoPath = student.photoPath;
|
|
|
+ if (!photoPath) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查缓存
|
|
|
+ if (studentPhotoCache.value.has(photoPath)) {
|
|
|
+ return studentPhotoCache.value.get(photoPath) || '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果正在加载,返回空字符串
|
|
|
+ if (photoLoadingCache.value.get(photoPath)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始异步加载
|
|
|
+ loadStudentPhoto(photoPath);
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+// 异步加载学生照片
|
|
|
+const loadStudentPhoto = async (photoPath: string) => {
|
|
|
+ // 标记为正在加载
|
|
|
+ photoLoadingCache.value.set(photoPath, true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const blobUrl = await getImageBlobUrl(photoPath);
|
|
|
+ if (blobUrl) {
|
|
|
+ studentPhotoCache.value.set(photoPath, blobUrl);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载学生照片失败:', error);
|
|
|
+ } finally {
|
|
|
+ // 标记加载完成
|
|
|
+ photoLoadingCache.value.set(photoPath, false);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 检查照片是否正在加载
|
|
|
+const isPhotoLoading = (student: any): boolean => {
|
|
|
+ const photoPath = student.photoPath;
|
|
|
+ if (!photoPath) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return photoLoadingCache.value.get(photoPath) || false;
|
|
|
+};
|
|
|
+
|
|
|
// 监听部门名称变化
|
|
|
watchEffect(() => {
|
|
|
if (deptTreeRef.value) {
|
|
|
@@ -1146,7 +1261,7 @@ onMounted(() => {
|
|
|
|
|
|
.student-card .card-header {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
+ align-items: flex-start;
|
|
|
margin-bottom: 12px;
|
|
|
gap: 16px;
|
|
|
flex-shrink: 0;
|
|
|
@@ -1154,6 +1269,39 @@ onMounted(() => {
|
|
|
|
|
|
.student-photo {
|
|
|
flex-shrink: 0;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 照片加载中状态文字样式 */
|
|
|
+.photo-loading-text {
|
|
|
+ margin-top: 4px;
|
|
|
+ text-align: center;
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-loading-text .el-text {
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-loading-text .el-icon {
|
|
|
+ animation: rotate 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0%,
|
|
|
+ 100% {
|
|
|
+ opacity: 1;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ opacity: 0.8;
|
|
|
+ transform: scale(1.05);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.student-info {
|
|
|
@@ -1225,6 +1373,40 @@ onMounted(() => {
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
+/* 照片加载状态样式 */
|
|
|
+.photo-loading-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ width: 100%;
|
|
|
+ background-color: #f5f7fa !important;
|
|
|
+ border-radius: 50%;
|
|
|
+ color: #8c8d91;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-loading-container .el-icon {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ animation: rotate 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-loading-container .loading-text {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes rotate {
|
|
|
+ from {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/* 男生占位符样式 */
|
|
|
.male-placeholder {
|
|
|
background: #e6f3ff !important;
|
|
|
@@ -1370,6 +1552,17 @@ onMounted(() => {
|
|
|
margin-top: 4px !important;
|
|
|
}
|
|
|
|
|
|
+ /* 移动端照片加载中文字样式调整 */
|
|
|
+ .photo-loading-text {
|
|
|
+ margin-top: 6px;
|
|
|
+ padding: 3px 6px;
|
|
|
+ font-size: 11px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .photo-loading-text .el-icon {
|
|
|
+ font-size: 10px !important;
|
|
|
+ }
|
|
|
+
|
|
|
.student-name {
|
|
|
font-size: 16px;
|
|
|
}
|