提交代码修改
This commit is contained in:
1 parent
fbb8dd6bd5
commit
97f7619bca
9 files changed
+1315
-19
No files matched your search
@@ -99,7 +99,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -746,8 +750,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuMsgDetailDialog">
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs, watchEffect } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// @ts-ignore
|
||||
import request from '@/utils/request';
|
||||
// 获取当前组件实例
|
||||
@@ -819,6 +824,8 @@ const loading = ref(false);
|
||||
const dialogLoading = ref(false); // 对话框整体加载状态
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const photoLoading = ref(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref(''); // 异步加载的照片URL
|
||||
|
||||
// 历史记录相关数据
|
||||
const historyLoading = ref(false);
|
||||
@@ -964,6 +971,50 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为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 '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取用户历史数据 */
|
||||
const getUserHistoryData = async (batchId: number, userId?: number) => {
|
||||
if (!batchId) {
|
||||
@@ -1171,6 +1222,25 @@ const rules = reactive({
|
||||
gender: [{ required: true, message: '性别不能为空', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 属性变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -1666,4 +1736,30 @@ const handleClose = () => {
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -153,7 +153,11 @@
|
||||
:before-upload="beforePhotoUpload"
|
||||
:http-request="handlePhotoUpload"
|
||||
>
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
@@ -1433,6 +1437,8 @@ import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue'
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import { ElMessageBox } from 'element-plus/es/components/message-box/index';
|
||||
import request from '@/utils/request';
|
||||
import service from '@/utils/request';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -1510,6 +1516,10 @@ const historyRecords = ref<any[]>([]);
|
||||
// 变更详情相关数据
|
||||
const changedFields = ref<any[]>([]); // 存储变更的字段信息
|
||||
|
||||
// 异步加载的图片URL
|
||||
const asyncPhotoUrl = ref<string>('');
|
||||
const photoLoading = ref<boolean>(false);
|
||||
|
||||
const studentFormRef = ref<any>();
|
||||
|
||||
/** 获取历史记录数据 */
|
||||
@@ -2369,8 +2379,110 @@ const handleApprove = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await service({
|
||||
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 '';
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖定时器
|
||||
let photoLoadTimer: number | null = null;
|
||||
|
||||
// 监听photoPath变化,异步加载图片
|
||||
watch(
|
||||
() => form.photoPath,
|
||||
async (newPhotoPath, oldPhotoPath) => {
|
||||
// 如果路径没有变化,不重复加载
|
||||
if (newPhotoPath === oldPhotoPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
}
|
||||
|
||||
// 清理之前的blob URL
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
|
||||
// 防抖处理,避免频繁调用
|
||||
photoLoadTimer = window.setTimeout(async () => {
|
||||
if (newPhotoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(newPhotoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
}, 100); // 100ms 防抖延迟
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
|
||||
// 重置表单数据到默认值
|
||||
const resetFormToDefault = () => {
|
||||
// 清理定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
photoLoadTimer = null;
|
||||
}
|
||||
|
||||
// 清理blob URL,避免内存泄漏
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
photoLoading.value = false;
|
||||
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
@@ -2788,6 +2900,27 @@ const handleClose = () => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 照片加载样式 */
|
||||
.photo-loading {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c939d;
|
||||
font-size: 14px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.photo-loading .is-loading {
|
||||
font-size: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 变更字段样式 */
|
||||
.changed-field-label {
|
||||
color: #f56c6c !important;
|
||||
|
||||
@@ -99,7 +99,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -745,10 +749,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuMsgDetailDialog">
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs, watchEffect } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
// @ts-ignore
|
||||
import request from '@/utils/request';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -818,6 +823,8 @@ const loading = ref(false);
|
||||
const dialogLoading = ref(false); // 对话框整体加载状态
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const photoLoading = ref(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref(''); // 异步加载的照片URL
|
||||
|
||||
// 历史记录相关数据
|
||||
const historyLoading = ref(false);
|
||||
@@ -963,6 +970,50 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为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 '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取用户历史数据 */
|
||||
const getUserHistoryData = async (batchId: number) => {
|
||||
if (!batchId) {
|
||||
@@ -1167,6 +1218,25 @@ const rules = reactive({
|
||||
gender: [{ required: true, message: '性别不能为空', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 属性变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -1662,4 +1732,30 @@ const handleClose = () => {
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -105,7 +105,11 @@
|
||||
:before-upload="beforePhotoUpload"
|
||||
:http-request="handlePhotoUpload"
|
||||
>
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
@@ -751,9 +755,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuMsgDialog">
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs, watchEffect } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import request from '@/utils/request';
|
||||
import service from '@/utils/request';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -828,6 +834,10 @@ const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const historyLoading = ref(false);
|
||||
const historyRecords = ref<any[]>([]);
|
||||
|
||||
// 异步加载的图片URL
|
||||
const asyncPhotoUrl = ref<string>('');
|
||||
const photoLoading = ref<boolean>(false);
|
||||
|
||||
const studentFormRef = ref<any>();
|
||||
|
||||
/** 获取历史记录数据 */
|
||||
@@ -1339,6 +1349,19 @@ const submitForm = () => {
|
||||
|
||||
// 重置表单数据到默认值
|
||||
const resetFormToDefault = () => {
|
||||
// 清理定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
photoLoadTimer = null;
|
||||
}
|
||||
|
||||
// 清理blob URL,避免内存泄漏
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
photoLoading.value = false;
|
||||
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
@@ -1455,12 +1478,114 @@ const handleClose = () => {
|
||||
// 重置历史记录数据
|
||||
historyRecords.value = [];
|
||||
|
||||
// 清理定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
photoLoadTimer = null;
|
||||
}
|
||||
|
||||
// 清理blob URL,避免内存泄漏
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
photoLoading.value = false;
|
||||
|
||||
// 重置表单
|
||||
nextTick(() => {
|
||||
studentFormRef.value?.resetFields();
|
||||
resetFormToDefault(); // 重置表单数据到默认值
|
||||
});
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await service({
|
||||
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 '';
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖定时器
|
||||
let photoLoadTimer: number | null = null;
|
||||
|
||||
// 监听photoPath变化,异步加载图片
|
||||
watch(
|
||||
() => form.photoPath,
|
||||
async (newPhotoPath, oldPhotoPath) => {
|
||||
// 如果路径没有变化,不重复加载
|
||||
if (newPhotoPath === oldPhotoPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
}
|
||||
|
||||
// 清理之前的blob URL
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
|
||||
// 防抖处理,避免频繁调用
|
||||
photoLoadTimer = setTimeout(async () => {
|
||||
if (newPhotoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(newPhotoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
}, 100); // 100ms 防抖延迟
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1742,4 +1867,25 @@ const handleClose = () => {
|
||||
cursor: help;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 照片加载样式 */
|
||||
.photo-loading {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c939d;
|
||||
font-size: 14px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.photo-loading .is-loading {
|
||||
font-size: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -70,7 +70,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -725,9 +729,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuPersonalMsg">
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs, watchEffect } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
@@ -779,6 +784,8 @@ const activeTab = ref('basic'); // 默认显示基础信息
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const loading = ref(false); // 加载状态
|
||||
const photoLoading = ref(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref(''); // 异步加载的照片URL
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
@@ -999,6 +1006,69 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为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 '';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时初始化数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([getRegionTree(), getDormBuildingTree()]);
|
||||
@@ -1090,6 +1160,32 @@ onMounted(async () => {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Tab页样式优化 */
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 0;
|
||||
|
||||
@@ -66,7 +66,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -721,10 +725,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuPersonalMsg">
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs, watch, computed } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs, watch, computed, watchEffect } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import api from '@/api/xg/xsjcxx/stuRoster/index';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
|
||||
// 定义props
|
||||
interface Props {
|
||||
@@ -798,6 +803,8 @@ const activeTab = ref('basic'); // 默认显示基础信息
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const loading = ref(false); // 加载状态
|
||||
const photoLoading = ref<boolean>(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref<string>(''); // 异步加载的照片URL
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
@@ -1021,6 +1028,50 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为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 '';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听学生ID变化,当弹窗打开且有学生ID时获取数据
|
||||
watch(
|
||||
() => [props.visible, props.studentId],
|
||||
@@ -1048,6 +1099,25 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时初始化基础数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([getRegionTree(), getDormBuildingTree()]);
|
||||
@@ -1161,6 +1231,32 @@ defineOptions({
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Tab页样式优化 */
|
||||
:deep(.el-tabs__header .el-tabs__item.is-active) {
|
||||
color: var(--el-color-primary) !important;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user