|
|
@@ -0,0 +1,1753 @@
|
|
|
+<template>
|
|
|
+ <view class="violation-detail-container">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <u-navbar title="违纪详情" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
|
|
|
+ <view slot="left" @click="goBack">
|
|
|
+ <u-icon name="arrow-left" color="#fff" size="22"></u-icon>
|
|
|
+ </view>
|
|
|
+ </u-navbar>
|
|
|
+
|
|
|
+ <!-- 加载状态 -->
|
|
|
+ <u-loading-page :loading="loading" loading-text="加载中..."></u-loading-page>
|
|
|
+
|
|
|
+ <!-- 详情内容 -->
|
|
|
+ <view v-if="!loading" class="content-wrapper">
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <view v-if="!violationDetail" class="empty-container">
|
|
|
+ <u-empty text="未找到违纪记录" mode="data"></u-empty>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 违纪详情卡片 -->
|
|
|
+ <view v-else class="detail-card">
|
|
|
+ <!-- 基本信息 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="account" size="20" color="#3c9cff"></u-icon>
|
|
|
+ <text class="title-text">学生信息</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-grid">
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">姓名:</text>
|
|
|
+ <text class="value">{{ violationDetail.studentName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">学号:</text>
|
|
|
+ <text class="value">{{ violationDetail.studentNo || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">学院:</text>
|
|
|
+ <text class="value">{{ violationDetail.collegeName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">专业:</text>
|
|
|
+ <text class="value">{{ violationDetail.majorName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">班级:</text>
|
|
|
+ <text class="value">{{ violationDetail.className || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 宿舍信息 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="home" size="20" color="#3c9cff"></u-icon>
|
|
|
+ <text class="title-text">宿舍信息</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-grid">
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">校区:</text>
|
|
|
+ <text class="value">{{ violationDetail.campusName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">宿舍楼:</text>
|
|
|
+ <text class="value">{{ violationDetail.buildingName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">房间号:</text>
|
|
|
+ <text class="value">{{ violationDetail.roomName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">床位:</text>
|
|
|
+ <text class="value">{{ violationDetail.bedName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 违纪信息 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="warning" size="20" color="#ff6b6b"></u-icon>
|
|
|
+ <text class="title-text">违纪信息</text>
|
|
|
+ </view>
|
|
|
+ <view class="violation-info">
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">违纪时间:</text>
|
|
|
+ <text class="value">
|
|
|
+ {{ formatTime(violationDetail.violationTime) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">违纪类型:</text>
|
|
|
+ <view class="tag-container">
|
|
|
+ <u-tag
|
|
|
+ :text="
|
|
|
+ getDictLabel(
|
|
|
+ dormViolationTypeDict,
|
|
|
+ violationDetail.violationType,
|
|
|
+ )
|
|
|
+ "
|
|
|
+ :type="
|
|
|
+ getDictTagType(
|
|
|
+ dormViolationTypeDict,
|
|
|
+ violationDetail.violationType,
|
|
|
+ violationTypeFallbackMap,
|
|
|
+ )
|
|
|
+ "
|
|
|
+ size="mini"
|
|
|
+ ></u-tag>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="info-row">
|
|
|
+ <text class="label">违纪等级:</text>
|
|
|
+ <view class="tag-container">
|
|
|
+ <u-tag
|
|
|
+ :text="
|
|
|
+ getDictLabel(
|
|
|
+ dormViolationLevelDict,
|
|
|
+ violationDetail.violationLevel,
|
|
|
+ )
|
|
|
+ "
|
|
|
+ :type="
|
|
|
+ getDictTagType(
|
|
|
+ dormViolationLevelDict,
|
|
|
+ violationDetail.violationLevel,
|
|
|
+ violationLevelFallbackMap,
|
|
|
+ )
|
|
|
+ "
|
|
|
+ size="mini"
|
|
|
+ ></u-tag>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="info-row" v-if="violationDetail.violationDescription">
|
|
|
+ <text class="label">违纪描述:</text>
|
|
|
+ <text class="value description-text">
|
|
|
+ {{ violationDetail.violationDescription }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view class="info-row" v-if="violationDetail.remark">
|
|
|
+ <text class="label">备注:</text>
|
|
|
+ <text class="value description-text">{{ violationDetail.remark }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 处理信息 -->
|
|
|
+ <view class="section" v-if="violationDetail.processStatus">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="checkmark-circle" size="20" color="#19be6b"></u-icon>
|
|
|
+ <text class="title-text">处理信息</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-grid">
|
|
|
+ <view class="info-item">
|
|
|
+ <text class="label">处理状态:</text>
|
|
|
+ <u-tag
|
|
|
+ :text="getProcessStatusText(violationDetail.processStatus)"
|
|
|
+ :type="getProcessStatusType(violationDetail.processStatus)"
|
|
|
+ size="mini"
|
|
|
+ ></u-tag>
|
|
|
+ </view>
|
|
|
+ <view class="info-item" v-if="violationDetail.processTime">
|
|
|
+ <text class="label">处理时间:</text>
|
|
|
+ <text class="value">{{ formatTime(violationDetail.processTime) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item" v-if="violationDetail.processBy">
|
|
|
+ <text class="label">处理人:</text>
|
|
|
+ <text class="value">{{ violationDetail.processBy }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="info-item" v-if="violationDetail.processResult">
|
|
|
+ <text class="label">处理结果:</text>
|
|
|
+ <text class="value">{{ violationDetail.processResult }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 涉及其他学生 -->
|
|
|
+ <view class="section" v-if="involvedStudents.length > 0">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="account-fill" size="20" color="#ff9500"></u-icon>
|
|
|
+ <text class="title-text">涉及其他学生</text>
|
|
|
+ <text class="student-count">{{ involvedStudentsCount }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="involved-students">
|
|
|
+ <view
|
|
|
+ v-for="(student, index) in involvedStudents"
|
|
|
+ :key="student.id || index"
|
|
|
+ class="student-card"
|
|
|
+ >
|
|
|
+ <view class="student-header">
|
|
|
+ <!-- <view class="student-avatar">
|
|
|
+ <u-icon
|
|
|
+ :name="getGenderIcon(student.gender)"
|
|
|
+ size="24"
|
|
|
+ :color="getGenderColor(student.gender)"
|
|
|
+ ></u-icon>
|
|
|
+ </view> -->
|
|
|
+ <view class="student-basic">
|
|
|
+ <text class="student-name">{{ student.name || '-' }}</text>
|
|
|
+ <text class="student-no">{{ student.studentNo || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="student-gender">
|
|
|
+ <u-tag
|
|
|
+ :text="getGenderText(student.gender)"
|
|
|
+ :type="getGenderType(student.gender)"
|
|
|
+ size="mini"
|
|
|
+ ></u-tag>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="student-details">
|
|
|
+ <view class="detail-row">
|
|
|
+ <text class="detail-label">学院:</text>
|
|
|
+ <text class="detail-value">
|
|
|
+ {{ student.collegeName || '-' }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view class="detail-row">
|
|
|
+ <text class="detail-label">专业:</text>
|
|
|
+ <text class="detail-value">{{ student.majorName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="detail-row">
|
|
|
+ <text class="detail-label">班级:</text>
|
|
|
+ <text class="detail-value">{{ student.className || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 涉及其他学生(加载状态) -->
|
|
|
+ <view class="section" v-else-if="involvedStudentsLoading">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="account-fill" size="20" color="#ff9500"></u-icon>
|
|
|
+ <text class="title-text">涉及其他学生</text>
|
|
|
+ </view>
|
|
|
+ <view class="loading-involved-students">
|
|
|
+ <u-loading-icon mode="spinner" size="28"></u-loading-icon>
|
|
|
+ <text class="loading-text">正在加载涉及学生信息...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 涉及其他学生(空状态) -->
|
|
|
+ <view class="section" v-else-if="showInvolvedStudents">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="account-fill" size="20" color="#ff9500"></u-icon>
|
|
|
+ <text class="title-text">涉及其他学生</text>
|
|
|
+ </view>
|
|
|
+ <view class="empty-involved-students">
|
|
|
+ <u-empty text="暂无涉及其他学生信息" mode="data"></u-empty>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 涉及其他学生(错误状态) -->
|
|
|
+ <view class="section" v-else-if="involvedStudentsError">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="account-fill" size="20" color="#ff9500"></u-icon>
|
|
|
+ <text class="title-text">涉及其他学生</text>
|
|
|
+ </view>
|
|
|
+ <view class="error-involved-students">
|
|
|
+ <u-empty text="获取涉及学生信息失败" mode="error"></u-empty>
|
|
|
+ <view class="error-actions">
|
|
|
+ <u-button size="mini" type="primary" @click="retryGetInvolvedStudents">
|
|
|
+ 重试
|
|
|
+ </u-button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- 附件信息 -->
|
|
|
+ <view class="section" v-if="attachmentList.length > 0">
|
|
|
+ <view class="section-title">
|
|
|
+ <u-icon name="folder" size="20" color="#ff9500"></u-icon>
|
|
|
+ <text class="title-text">违纪附件</text>
|
|
|
+ </view>
|
|
|
+ <view class="attachment-area">
|
|
|
+ <!-- 图片展示区域 -->
|
|
|
+ <view v-if="imageFiles.length > 0" class="image-section">
|
|
|
+ <view class="image-title">图片附件</view>
|
|
|
+ <view class="image-grid">
|
|
|
+ <view
|
|
|
+ v-for="image in imageFiles"
|
|
|
+ :key="image.uid"
|
|
|
+ class="image-item"
|
|
|
+ @click="handleImagePreview(image)"
|
|
|
+ >
|
|
|
+ <!-- 图片容器 -->
|
|
|
+ <view class="image-container">
|
|
|
+ <image
|
|
|
+ :src="image.blobUrl || image.url"
|
|
|
+ class="attachment-image"
|
|
|
+ mode="aspectFill"
|
|
|
+ ></image>
|
|
|
+
|
|
|
+ <!-- 成功状态角标 -->
|
|
|
+ <view class="success-badge">
|
|
|
+ <u-icon
|
|
|
+ name="checkmark-circle-fill"
|
|
|
+ size="18"
|
|
|
+ color="#52c41a"
|
|
|
+ ></u-icon>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 图片操作按钮 -->
|
|
|
+ <view class="image-actions" @click.stop>
|
|
|
+ <u-button
|
|
|
+ size="mini"
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
+ @tap="handleImageDownload(image)"
|
|
|
+ @click.stop.prevent="
|
|
|
+ handleImageDownloadClick(image, $event)
|
|
|
+ "
|
|
|
+ >
|
|
|
+ 下载
|
|
|
+ </u-button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 文件名显示 -->
|
|
|
+ <view class="image-name">
|
|
|
+ <text>{{ getShortFileName(image.name) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 自定义文件列表(用于显示非图片文件) -->
|
|
|
+ <view class="custom-file-list" v-if="nonImageFiles.length > 0">
|
|
|
+ <view class="file-title">文档附件</view>
|
|
|
+ <view class="file-grid">
|
|
|
+ <view
|
|
|
+ class="file-card"
|
|
|
+ v-for="file in nonImageFiles"
|
|
|
+ :key="file.uid"
|
|
|
+ >
|
|
|
+ <!-- 文件图标容器 -->
|
|
|
+ <view class="file-icon-container">
|
|
|
+ <u-icon name="folder" size="50" color="#19be6b" />
|
|
|
+
|
|
|
+ <!-- 成功状态角标 -->
|
|
|
+ <view class="success-badge">
|
|
|
+ <u-icon
|
|
|
+ name="checkmark-circle-fill"
|
|
|
+ size="18"
|
|
|
+ color="#52c41a"
|
|
|
+ ></u-icon>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 文件操作按钮 -->
|
|
|
+ <view class="image-actions" @click.stop>
|
|
|
+ <u-button
|
|
|
+ size="mini"
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
+ @tap="handleDownloadFile(file)"
|
|
|
+ @click.stop.prevent="
|
|
|
+ handleDownloadFileClick(file, $event)
|
|
|
+ "
|
|
|
+ >
|
|
|
+ 下载
|
|
|
+ </u-button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 文件名显示 -->
|
|
|
+ <view class="file-name-display">
|
|
|
+ <text>{{ getShortFileName(file.name) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { request, downloadFile } from '@/common/request.js';
|
|
|
+import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'ViolationDetail',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ violationDetail: null,
|
|
|
+ violationId: null,
|
|
|
+
|
|
|
+ // 字典数据
|
|
|
+ dormViolationTypeDict: [],
|
|
|
+ dormViolationLevelDict: [],
|
|
|
+
|
|
|
+ // 附件列表
|
|
|
+ attachmentList: [],
|
|
|
+
|
|
|
+ // 涉及其他学生列表
|
|
|
+ involvedStudents: [],
|
|
|
+
|
|
|
+ // 涉及学生信息加载状态
|
|
|
+ involvedStudentsLoading: false,
|
|
|
+
|
|
|
+ // 涉及学生信息错误状态
|
|
|
+ involvedStudentsError: false,
|
|
|
+
|
|
|
+ // 学生信息缓存
|
|
|
+ studentCache: new Map(),
|
|
|
+
|
|
|
+ // 字典标签类型降级映射
|
|
|
+ violationTypeFallbackMap: {
|
|
|
+ WS001001: 'warning', // 垃圾乱扔
|
|
|
+ WS001002: 'warning', // 床铺不整
|
|
|
+ WS001003: 'warning', // 物品摆放混乱
|
|
|
+ JL001001: 'info', // 晚归违纪
|
|
|
+ JL001002: 'info', // 噪音扰民
|
|
|
+ JL001003: 'error', // 夜不归宿
|
|
|
+ AQ001001: 'error', // 违规用电
|
|
|
+ AQ001002: 'error', // 吸烟违纪
|
|
|
+ AQ001003: 'error', // 明火使用
|
|
|
+ WP001001: 'primary', // 损坏公物
|
|
|
+ WP001002: 'primary', // 私改设施
|
|
|
+ WP001003: 'error', // 盗用他人物品
|
|
|
+ },
|
|
|
+
|
|
|
+ violationLevelFallbackMap: {
|
|
|
+ 1: 'success', // 轻微
|
|
|
+ 2: 'warning', // 一般
|
|
|
+ 3: 'error', // 严重
|
|
|
+ 4: 'error', // 特别严重
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ // 图片文件(用于内部处理)
|
|
|
+ imageFiles() {
|
|
|
+ return this.attachmentList.filter((file) => this.isImageFile(file));
|
|
|
+ },
|
|
|
+
|
|
|
+ // 非图片文件(自定义显示)
|
|
|
+ nonImageFiles() {
|
|
|
+ return this.attachmentList.filter((file) => !this.isImageFile(file));
|
|
|
+ },
|
|
|
+
|
|
|
+ // 是否显示涉及学生卡片
|
|
|
+ showInvolvedStudents() {
|
|
|
+ return this.violationDetail && this.violationDetail.involvedStudents;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 涉及学生数量信息
|
|
|
+ involvedStudentsCount() {
|
|
|
+ if (!this.showInvolvedStudents) return '';
|
|
|
+ return `(${this.involvedStudents.length}人)`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+
|
|
|
+ onLoad(options) {
|
|
|
+ console.log('详情页面参数:', options);
|
|
|
+ if (options.id) {
|
|
|
+ this.violationId = options.id;
|
|
|
+ this.initData();
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '参数错误',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ setTimeout(() => {
|
|
|
+ this.goBack();
|
|
|
+ }, 1500);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onUnload() {
|
|
|
+ // 清理blob URL,防止内存泄漏
|
|
|
+ this.cleanupBlobUrls();
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ // 返回上一页
|
|
|
+ goBack() {
|
|
|
+ uni.navigateBack();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 初始化数据
|
|
|
+ async initData() {
|
|
|
+ await Promise.all([this.loadDictData(), this.getDetail()]);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 加载字典数据
|
|
|
+ async loadDictData() {
|
|
|
+ try {
|
|
|
+ const [typeDict, levelDict] = await Promise.all([
|
|
|
+ getDict('dorm_violation_type'),
|
|
|
+ getDict('dorm_violation_level'),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ this.dormViolationTypeDict = typeDict;
|
|
|
+ this.dormViolationLevelDict = levelDict;
|
|
|
+
|
|
|
+ console.log('字典数据加载完成:', {
|
|
|
+ type: this.dormViolationTypeDict.length,
|
|
|
+ level: this.dormViolationLevelDict.length,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载字典数据失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取字典标签
|
|
|
+ getDictLabel(dictData, value) {
|
|
|
+ return getDictLabel(dictData, value);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取字典标签类型
|
|
|
+ getDictTagType(dictData, value, fallbackMap) {
|
|
|
+ return getDictTagType(dictData, value, fallbackMap);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取违纪详情
|
|
|
+ async getDetail() {
|
|
|
+ if (!this.violationId) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '参数错误',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.loading = true;
|
|
|
+
|
|
|
+ // 清理之前的数据
|
|
|
+ this.involvedStudents = [];
|
|
|
+ this.attachmentList = [];
|
|
|
+ this.involvedStudentsLoading = false;
|
|
|
+ this.involvedStudentsError = false; // 重置错误状态
|
|
|
+ this.studentCache.clear(); // 清理学生信息缓存
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await request({
|
|
|
+ url: `/dormViolation/dormViolation/${this.violationId}`,
|
|
|
+ method: 'GET',
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ this.violationDetail = res.data;
|
|
|
+ console.log('获取违纪详情成功:', this.violationDetail);
|
|
|
+
|
|
|
+ // 获取附件信息
|
|
|
+ if (this.violationDetail.attachmentOssIds) {
|
|
|
+ await this.getAttachmentFiles(this.violationDetail.attachmentOssIds);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取涉及其他学生信息
|
|
|
+ if (this.violationDetail.involvedStudents) {
|
|
|
+ await this.getInvolvedStudents(this.violationDetail.involvedStudents);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: res?.msg || '获取详情失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取违纪详情失败:', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: '获取详情失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ this.involvedStudentsError = true; // 设置错误状态
|
|
|
+ } finally {
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取附件文件
|
|
|
+ async getAttachmentFiles(ossIds) {
|
|
|
+ try {
|
|
|
+ const response = await request({
|
|
|
+ url: `/resource/oss/listByIds/${ossIds}`,
|
|
|
+ method: 'GET',
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ const attachmentFiles = response.data.map((item, index) => {
|
|
|
+ // 使用fileSuffix字段判断文件类型
|
|
|
+ const fileType = this.getFileTypeBySuffix(item.fileSuffix);
|
|
|
+ const isImage = this.isImageFile({
|
|
|
+ name: item.originalName,
|
|
|
+ type: fileType,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理图片URL - 确保有正确的API前缀
|
|
|
+ let processedUrl = item.url;
|
|
|
+ if (isImage && item.url && item.url.startsWith('/resource/')) {
|
|
|
+ // 图片文件需要添加/api前缀
|
|
|
+ processedUrl = `/api${item.url}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构造完整的文件名(包含扩展名)
|
|
|
+ let fullFileName = item.originalName || `附件${index + 1}`;
|
|
|
+ if (
|
|
|
+ item.fileSuffix &&
|
|
|
+ !fullFileName
|
|
|
+ .toLowerCase()
|
|
|
+ .endsWith(`.${item.fileSuffix.toLowerCase()}`)
|
|
|
+ ) {
|
|
|
+ fullFileName = `${fullFileName}.${item.fileSuffix}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fileObj = {
|
|
|
+ uid: `existing_${index}_${Date.now()}`,
|
|
|
+ name: fullFileName,
|
|
|
+ url: processedUrl,
|
|
|
+ ossId: item.ossId,
|
|
|
+ status: 'success',
|
|
|
+ type: fileType,
|
|
|
+ // 确保图片文件有正确的显示属性
|
|
|
+ size: item.fileSize || 0,
|
|
|
+ // 为uview2 upload组件添加必要的属性
|
|
|
+ file: {
|
|
|
+ name: fullFileName,
|
|
|
+ type: fileType,
|
|
|
+ size: item.fileSize || 0,
|
|
|
+ path: processedUrl,
|
|
|
+ url: processedUrl,
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ return fileObj;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.attachmentList = attachmentFiles;
|
|
|
+
|
|
|
+ // 为图片文件获取blob数据以确保正确显示
|
|
|
+ await this.processImageFiles();
|
|
|
+
|
|
|
+ // 强制刷新组件显示
|
|
|
+ this.$nextTick(() => {
|
|
|
+ // 预加载图片以确保能正确显示
|
|
|
+ this.preloadImages();
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.warn('获取附件信息失败:', response.msg);
|
|
|
+ this.attachmentList = [];
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取附件失败:', error);
|
|
|
+ this.attachmentList = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取涉及其他学生信息
|
|
|
+ async getInvolvedStudents(involvedStudentsStr) {
|
|
|
+ if (!involvedStudentsStr) {
|
|
|
+ this.involvedStudents = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.involvedStudentsLoading = true;
|
|
|
+ this.involvedStudentsError = false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 解析学生ID字符串(逗号分隔)
|
|
|
+ const studentIds = involvedStudentsStr
|
|
|
+ .split(',')
|
|
|
+ .map((id) => id.trim())
|
|
|
+ .filter((id) => id && !isNaN(Number(id))); // 过滤掉空值和非数字ID
|
|
|
+
|
|
|
+ // 去重
|
|
|
+ const uniqueStudentIds = [...new Set(studentIds)];
|
|
|
+
|
|
|
+ if (uniqueStudentIds.length !== studentIds.length) {
|
|
|
+ console.log(`学生ID去重: ${studentIds.length} -> ${uniqueStudentIds.length}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uniqueStudentIds.length === 0) {
|
|
|
+ this.involvedStudents = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 限制最多获取10个学生信息,防止性能问题
|
|
|
+ const maxStudents = 10;
|
|
|
+ if (uniqueStudentIds.length > maxStudents) {
|
|
|
+ console.warn(
|
|
|
+ `涉及学生数量过多(${uniqueStudentIds.length}),只获取前${maxStudents}个`,
|
|
|
+ );
|
|
|
+ uniqueStudentIds.splice(maxStudents);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('涉及学生ID列表:', uniqueStudentIds);
|
|
|
+
|
|
|
+ // 并发获取所有学生的详细信息
|
|
|
+ const studentPromises = uniqueStudentIds.map(async (studentId) => {
|
|
|
+ try {
|
|
|
+ // 检查缓存
|
|
|
+ if (this.studentCache.has(studentId)) {
|
|
|
+ console.log(`从缓存获取学生${studentId}信息`);
|
|
|
+ return this.studentCache.get(studentId);
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await request({
|
|
|
+ url: `/dormViolation/dormViolation/student/${studentId}`,
|
|
|
+ method: 'GET',
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ console.log(`获取学生${studentId}信息成功:`, response.data);
|
|
|
+ // 缓存学生信息
|
|
|
+ this.studentCache.set(studentId, response.data);
|
|
|
+ return response.data;
|
|
|
+ } else {
|
|
|
+ console.warn(`获取学生${studentId}信息失败:`, response.msg);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`获取学生${studentId}信息异常:`, error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 等待所有请求完成
|
|
|
+ const students = await Promise.all(studentPromises);
|
|
|
+
|
|
|
+ // 过滤掉获取失败的学生信息,并验证必要字段
|
|
|
+ this.involvedStudents = students.filter((student) => {
|
|
|
+ if (!student) return false;
|
|
|
+
|
|
|
+ // 验证必要字段
|
|
|
+ const hasRequiredFields = student.id && student.name && student.studentNo;
|
|
|
+ if (!hasRequiredFields) {
|
|
|
+ console.warn('学生信息缺少必要字段:', student);
|
|
|
+ }
|
|
|
+
|
|
|
+ return hasRequiredFields;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 按学号排序
|
|
|
+ this.involvedStudents.sort((a, b) => {
|
|
|
+ return (a.studentNo || '').localeCompare(b.studentNo || '');
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('涉及学生信息获取完成:', this.involvedStudents);
|
|
|
+
|
|
|
+ // 如果有学生ID但获取失败,显示提示
|
|
|
+ if (studentIds.length > 0 && this.involvedStudents.length === 0) {
|
|
|
+ console.warn('所有涉及学生信息获取失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取涉及学生信息失败:', error);
|
|
|
+ this.involvedStudents = [];
|
|
|
+ this.involvedStudentsError = true; // 设置错误状态
|
|
|
+ } finally {
|
|
|
+ this.involvedStudentsLoading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重试获取涉及学生信息
|
|
|
+ async retryGetInvolvedStudents() {
|
|
|
+ this.involvedStudentsLoading = true;
|
|
|
+ this.involvedStudentsError = false;
|
|
|
+ await this.getInvolvedStudents(this.violationDetail.involvedStudents);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 根据文件后缀获取文件类型
|
|
|
+ getFileTypeBySuffix(fileSuffix) {
|
|
|
+ if (!fileSuffix) return '';
|
|
|
+
|
|
|
+ // 移除点号,转换为小写
|
|
|
+ const ext = fileSuffix.replace('.', '').toLowerCase();
|
|
|
+
|
|
|
+ // 图片类型判断
|
|
|
+ const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
|
|
+ if (imageTypes.includes(ext)) {
|
|
|
+ // 修正MIME类型映射
|
|
|
+ const mimeTypeMap = {
|
|
|
+ jpg: 'image/jpeg',
|
|
|
+ jpeg: 'image/jpeg',
|
|
|
+ png: 'image/png',
|
|
|
+ gif: 'image/gif',
|
|
|
+ bmp: 'image/bmp',
|
|
|
+ webp: 'image/webp',
|
|
|
+ };
|
|
|
+ return mimeTypeMap[ext] || `image/${ext}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 文档类型判断
|
|
|
+ const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
|
|
|
+ if (documentTypes.includes(ext)) {
|
|
|
+ return `file/${ext}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其他类型
|
|
|
+ return `file/${ext}`;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 文件类型判断
|
|
|
+ isImageFile(file) {
|
|
|
+ const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
|
|
|
+
|
|
|
+ // 优先使用文件MIME类型判断
|
|
|
+ if (file.type) {
|
|
|
+ return imageTypes.includes(file.type);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通过文件名判断
|
|
|
+ const fileName = file.name || file.url || '';
|
|
|
+ return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取缩写的文件名(保留后缀)
|
|
|
+ getShortFileName(fileName) {
|
|
|
+ if (!fileName) return '';
|
|
|
+
|
|
|
+ const lastDotIndex = fileName.lastIndexOf('.');
|
|
|
+ if (lastDotIndex === -1) {
|
|
|
+ // 没有后缀的文件名
|
|
|
+ return fileName.length > 12 ? fileName.substring(0, 12) + '...' : fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ const name = fileName.substring(0, lastDotIndex);
|
|
|
+ const ext = fileName.substring(lastDotIndex); // 包含点号的后缀
|
|
|
+
|
|
|
+ if (name.length <= 8) {
|
|
|
+ return fileName; // 文件名较短,直接返回
|
|
|
+ }
|
|
|
+
|
|
|
+ // 文件名较长,缩写中间部分,保留后缀
|
|
|
+ return name.substring(0, 8) + '...' + ext;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图片预览
|
|
|
+ handleImagePreview(image) {
|
|
|
+ if (image.status !== 'success') {
|
|
|
+ uni.showToast({
|
|
|
+ title: '图片尚未加载完成',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取所有成功上传的图片URL
|
|
|
+ const imageUrls = this.imageFiles
|
|
|
+ .filter((file) => file.status === 'success')
|
|
|
+ .map((file) => file.blobUrl || file.url);
|
|
|
+
|
|
|
+ if (imageUrls.length === 0) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '暂无可预览的图片',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 找到当前图片在成功列表中的索引
|
|
|
+ const currentUrl = image.blobUrl || image.url;
|
|
|
+ const currentIndex = imageUrls.findIndex((url) => url === currentUrl);
|
|
|
+
|
|
|
+ uni.previewImage({
|
|
|
+ urls: imageUrls,
|
|
|
+ current: currentIndex >= 0 ? currentIndex : 0,
|
|
|
+ success: () => {
|
|
|
+ // 预览成功
|
|
|
+ },
|
|
|
+ fail: (error) => {
|
|
|
+ console.error('图片预览失败:', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: '预览失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图片下载按钮点击处理(更强的事件处理)
|
|
|
+ handleImageDownloadClick(image, event) {
|
|
|
+ // 强制阻止事件冒泡和默认行为
|
|
|
+ if (event) {
|
|
|
+ event.stopPropagation();
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopImmediatePropagation();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用实际的下载方法
|
|
|
+ this.handleImageDownload(image, event);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 图片下载(带重试机制)
|
|
|
+ async handleImageDownload(image, event, retryCount = 0) {
|
|
|
+ // 事件阻止已由handleImageDownloadClick方法处理
|
|
|
+
|
|
|
+ if (image.status !== 'success') {
|
|
|
+ uni.showToast({
|
|
|
+ title: '图片尚未加载完成',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示下载进度提示
|
|
|
+ uni.showLoading({
|
|
|
+ title: '正在下载图片...',
|
|
|
+ mask: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 如果有ossId,通过API下载
|
|
|
+ if (image.ossId) {
|
|
|
+ const downloadUrl = `/resource/oss/download/${image.ossId}`;
|
|
|
+
|
|
|
+ // 调用下载接口并等待完成
|
|
|
+ const response = await downloadFile(downloadUrl, image.name);
|
|
|
+
|
|
|
+ if (response.statusCode === 200) {
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.saveFile({
|
|
|
+ tempFilePath: response.tempFilePath,
|
|
|
+ success: function (res) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '下载成功',
|
|
|
+ icon: 'success',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: function () {
|
|
|
+ uni.showToast({
|
|
|
+ title: '保存失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('图片下载过程出错:', error);
|
|
|
+
|
|
|
+ // 确保隐藏loading状态
|
|
|
+ uni.hideLoading();
|
|
|
+
|
|
|
+ // 重试机制(最多重试2次)
|
|
|
+ if (retryCount < 2) {
|
|
|
+ // 显示重试提示
|
|
|
+ uni.showToast({
|
|
|
+ title: `下载失败,正在重试(${retryCount + 1}/2)...`,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500,
|
|
|
+ });
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ this.handleImageDownload(image, event, retryCount + 1);
|
|
|
+ }, 1500); // 延迟1.5秒后重试
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 超过重试次数,显示最终错误信息
|
|
|
+ const errorMsg = error.message?.includes('网络')
|
|
|
+ ? '网络连接失败,请检查网络后重试'
|
|
|
+ : error.message?.includes('timeout')
|
|
|
+ ? '下载超时,请重试'
|
|
|
+ : '下载失败,已重试2次,请检查网络或稍后重试';
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: errorMsg,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 文件下载按钮点击处理(更强的事件处理)
|
|
|
+ handleDownloadFileClick(file, event) {
|
|
|
+ // 强制阻止事件冒泡和默认行为
|
|
|
+ if (event) {
|
|
|
+ event.stopPropagation();
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopImmediatePropagation();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用实际的下载方法
|
|
|
+ this.handleDownloadFile(file, event);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 文件下载(微信小程序专用版本)
|
|
|
+ async handleDownloadFile(file, event) {
|
|
|
+ // 事件阻止已由handleDownloadFileClick方法处理
|
|
|
+
|
|
|
+ if (!file.ossId) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '文件未加载完成',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({
|
|
|
+ title: `正在下载${file.name}...`,
|
|
|
+ mask: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 构建下载URL
|
|
|
+ const downloadUrl = `/resource/oss/download/${file.ossId}`;
|
|
|
+
|
|
|
+ if (this.isImageFile(file)) {
|
|
|
+ // 图片文件:下载并保存到相册
|
|
|
+ await this.downloadAndSaveImage(downloadUrl, file.name);
|
|
|
+ } else {
|
|
|
+ // 非图片文件:下载并直接打开预览
|
|
|
+ await this.downloadAndOpenDocument(downloadUrl, file.name);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('下载过程发生异常:', error);
|
|
|
+ uni.hideLoading();
|
|
|
+
|
|
|
+ let errorMsg = '下载失败,请重试';
|
|
|
+ if (error.message.includes('timeout')) {
|
|
|
+ errorMsg = '下载超时,请检查网络后重试';
|
|
|
+ } else if (error.message.includes('network')) {
|
|
|
+ errorMsg = '网络错误,请检查网络连接';
|
|
|
+ } else if (error.message.includes('404')) {
|
|
|
+ errorMsg = '文件不存在,请联系管理员';
|
|
|
+ } else if (error.message.includes('500')) {
|
|
|
+ errorMsg = '服务器错误,请稍后重试';
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.showModal({
|
|
|
+ title: '下载失败',
|
|
|
+ content: errorMsg,
|
|
|
+ showCancel: true,
|
|
|
+ cancelText: '取消',
|
|
|
+ confirmText: '重试',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ this.handleDownloadFile(file);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 下载图片并保存到相册
|
|
|
+ async downloadAndSaveImage(downloadUrl, fileName) {
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
+ // 调用下载接口并等待完成
|
|
|
+ const response = await downloadFile(downloadUrl, fileName);
|
|
|
+
|
|
|
+ if (response.statusCode === 200) {
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.saveFile({
|
|
|
+ tempFilePath: response.tempFilePath,
|
|
|
+ success: function (res) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '下载成功',
|
|
|
+ icon: 'success',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: function () {
|
|
|
+ uni.showToast({
|
|
|
+ title: '保存失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ uni.hideLoading();
|
|
|
+ resolve();
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 下载文档并打开预览
|
|
|
+ async downloadAndOpenDocument(downloadUrl, fileName) {
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
+ // 调用下载接口并等待完成
|
|
|
+ const response = await downloadFile(downloadUrl, fileName);
|
|
|
+
|
|
|
+ if (response.statusCode === 200) {
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.saveFile({
|
|
|
+ tempFilePath: response.tempFilePath,
|
|
|
+ success: function (res) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '下载成功',
|
|
|
+ icon: 'success',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: function () {
|
|
|
+ uni.showToast({
|
|
|
+ title: '保存失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ uni.hideLoading();
|
|
|
+ resolve();
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 预加载图片以确保能正确显示
|
|
|
+ async preloadImages() {
|
|
|
+ console.log('=== 预加载图片 ===');
|
|
|
+ const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
|
|
|
+
|
|
|
+ for (const file of imageFiles) {
|
|
|
+ if (file.url) {
|
|
|
+ try {
|
|
|
+ console.log(`预加载图片: ${file.name} - ${file.url}`);
|
|
|
+ // 创建一个Image对象来预加载图片
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => {
|
|
|
+ console.log(`图片预加载成功: ${file.name}`);
|
|
|
+ };
|
|
|
+ img.onerror = () => {
|
|
|
+ console.error(`图片预加载失败: ${file.name} - ${file.url}`);
|
|
|
+ };
|
|
|
+ img.src = file.url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`预加载图片出错: ${file.name}`, error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取图片的blob数据(用于uview2 upload组件显示)
|
|
|
+ async getImageBlobUrl(imageUrl) {
|
|
|
+ try {
|
|
|
+ console.log(`获取图片blob数据: ${imageUrl}`);
|
|
|
+
|
|
|
+ // 处理图片URL路径
|
|
|
+ let apiUrl = imageUrl;
|
|
|
+ if (imageUrl.startsWith('/api/')) {
|
|
|
+ // 移除/api前缀,因为request会自动添加baseURL
|
|
|
+ apiUrl = imageUrl.replace('/api', '');
|
|
|
+ } else if (imageUrl.startsWith('/resource/')) {
|
|
|
+ // 直接使用相对路径
|
|
|
+ apiUrl = imageUrl;
|
|
|
+ } else if (imageUrl.startsWith('http')) {
|
|
|
+ // 完整URL,需要特殊处理
|
|
|
+ console.log('检测到完整URL,直接返回原URL');
|
|
|
+ return imageUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`API请求URL: ${apiUrl}`);
|
|
|
+
|
|
|
+ // 使用request方法获取图片数据
|
|
|
+ const response = await request({
|
|
|
+ url: apiUrl,
|
|
|
+ method: 'GET',
|
|
|
+ responseType: 'arraybuffer', // 设置响应类型为arraybuffer
|
|
|
+ header: {
|
|
|
+ isToken: true, // 图片请求可能不需要token验证
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // 将arraybuffer转换为blob
|
|
|
+ const blob = new Blob([response], { type: 'image/*' });
|
|
|
+ const blobUrl = URL.createObjectURL(blob);
|
|
|
+
|
|
|
+ console.log(`成功创建blob URL: ${blobUrl}`);
|
|
|
+ return blobUrl;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`获取图片blob数据失败: ${imageUrl}`, error);
|
|
|
+ // 如果请求失败,返回原始URL作为备用方案
|
|
|
+ return imageUrl;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理图片文件,获取blob数据以确保正确显示
|
|
|
+ async processImageFiles() {
|
|
|
+ console.log('=== 处理图片文件 ===');
|
|
|
+ const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
|
|
|
+
|
|
|
+ for (const file of imageFiles) {
|
|
|
+ if (file.url && !file.blobUrl) {
|
|
|
+ try {
|
|
|
+ console.log(`处理图片文件: ${file.name}`);
|
|
|
+ const blobUrl = await this.getImageBlobUrl(file.url);
|
|
|
+ if (blobUrl) {
|
|
|
+ // 更新文件对象,添加blob URL,并标记为图片
|
|
|
+ file.blobUrl = blobUrl;
|
|
|
+ file.url = blobUrl; // 使用blob URL作为显示URL
|
|
|
+ file.fileType = 'image';
|
|
|
+ file.type =
|
|
|
+ file.type && file.type.startsWith('image') ? file.type : 'image';
|
|
|
+ file.isImage = true;
|
|
|
+ console.log(`图片文件处理成功: ${file.name} - ${blobUrl}`);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`处理图片文件失败: ${file.name}`, error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清理blob URL,防止内存泄漏
|
|
|
+ cleanupBlobUrls() {
|
|
|
+ console.log('=== 清理blob URL ===');
|
|
|
+ this.attachmentList.forEach((file) => {
|
|
|
+ if (file.blobUrl && file.blobUrl.startsWith('blob:')) {
|
|
|
+ try {
|
|
|
+ URL.revokeObjectURL(file.blobUrl);
|
|
|
+ console.log(`已清理blob URL: ${file.name}`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`清理blob URL失败: ${file.name}`, error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 清理涉及学生数据
|
|
|
+ this.involvedStudents = [];
|
|
|
+ this.involvedStudentsLoading = false;
|
|
|
+ this.involvedStudentsError = false; // 重置错误状态
|
|
|
+ this.studentCache.clear(); // 清理学生信息缓存
|
|
|
+ console.log('已清理涉及学生数据');
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化时间显示
|
|
|
+ formatTime(timeStr) {
|
|
|
+ if (!timeStr) return '-';
|
|
|
+ try {
|
|
|
+ const date = new Date(timeStr);
|
|
|
+ if (isNaN(date.getTime())) return timeStr;
|
|
|
+
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+ const hours = String(date.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
+ const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
+
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('时间格式化失败:', error);
|
|
|
+ return timeStr;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取处理状态文本
|
|
|
+ getProcessStatusText(status) {
|
|
|
+ const statusMap = {
|
|
|
+ 0: '待处理',
|
|
|
+ 1: '处理中',
|
|
|
+ 2: '已处理',
|
|
|
+ 3: '已撤销',
|
|
|
+ };
|
|
|
+ return statusMap[status] || '未知状态';
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取处理状态类型
|
|
|
+ getProcessStatusType(status) {
|
|
|
+ const typeMap = {
|
|
|
+ 0: 'warning',
|
|
|
+ 1: 'primary',
|
|
|
+ 2: 'success',
|
|
|
+ 3: 'info',
|
|
|
+ };
|
|
|
+ return typeMap[status] || 'info';
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取性别文本
|
|
|
+ getGenderText(gender) {
|
|
|
+ if (gender === '1') {
|
|
|
+ return '女';
|
|
|
+ } else if (gender === '0') {
|
|
|
+ return '男';
|
|
|
+ } else {
|
|
|
+ return '未知';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取性别类型
|
|
|
+ getGenderType(gender) {
|
|
|
+ if (gender === '1') {
|
|
|
+ return 'warning'; // 粉色
|
|
|
+ } else if (gender === '0') {
|
|
|
+ return 'primary'; // 蓝色
|
|
|
+ } else {
|
|
|
+ return 'info'; // 灰色
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取性别图标
|
|
|
+ getGenderIcon(gender) {
|
|
|
+ if (gender === '1') {
|
|
|
+ return 'woman';
|
|
|
+ } else if (gender === '0') {
|
|
|
+ return 'man';
|
|
|
+ } else {
|
|
|
+ return 'question';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取性别颜色
|
|
|
+ getGenderColor(gender) {
|
|
|
+ if (gender === '1') {
|
|
|
+ return '#ff69b4'; // 粉色
|
|
|
+ } else if (gender === '0') {
|
|
|
+ return '#3c9cff'; // 蓝色
|
|
|
+ } else {
|
|
|
+ return '#909399'; // 灰色
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.violation-detail-container {
|
|
|
+ background-color: #f5f6fa;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+.content-wrapper {
|
|
|
+ padding: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-container {
|
|
|
+ padding: 100rpx 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-card {
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.section {
|
|
|
+ padding: 24rpx;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ gap: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.title-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.student-count {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #909399;
|
|
|
+ margin-left: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.info-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.info-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.label {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #606266;
|
|
|
+ min-width: 120rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.value {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #303133;
|
|
|
+ flex: 1;
|
|
|
+ word-break: break-all;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.description-text {
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-container {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+// 附件区域样式
|
|
|
+.attachment-area {
|
|
|
+ padding: 20rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+// 图片展示区域样式
|
|
|
+.image-section {
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.image-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ padding-left: 10rpx;
|
|
|
+ border-left: 4rpx solid #2979ff;
|
|
|
+}
|
|
|
+
|
|
|
+.image-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280rpx, 1fr));
|
|
|
+ gap: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.image-item {
|
|
|
+ position: relative;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 2rpx solid #f0f0f0;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.image-item:active {
|
|
|
+ transform: scale(0.98);
|
|
|
+ border-color: #2979ff;
|
|
|
+}
|
|
|
+
|
|
|
+.image-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 200rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.success-badge {
|
|
|
+ position: absolute;
|
|
|
+ top: 8rpx;
|
|
|
+ right: 8rpx;
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
+ border-radius: 50%;
|
|
|
+ padding: 4rpx;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.image-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 6rpx;
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
+ background: #fafafa;
|
|
|
+ border-top: 1rpx solid #f0f0f0;
|
|
|
+ flex-wrap: nowrap;
|
|
|
+ justify-content: space-between;
|
|
|
+ position: relative;
|
|
|
+ z-index: 10;
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.image-actions .u-button {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 60rpx;
|
|
|
+ font-size: 24rpx !important;
|
|
|
+ pointer-events: auto;
|
|
|
+ position: relative;
|
|
|
+ z-index: 11;
|
|
|
+}
|
|
|
+
|
|
|
+.image-name {
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
+ background: #fff;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #666;
|
|
|
+ text-align: center;
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+// 自定义文件列表样式
|
|
|
+.custom-file-list {
|
|
|
+ margin-top: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.file-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ padding-left: 10rpx;
|
|
|
+ border-left: 4rpx solid #2979ff;
|
|
|
+}
|
|
|
+
|
|
|
+.file-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280rpx, 1fr));
|
|
|
+ gap: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.file-card {
|
|
|
+ position: relative;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 2rpx solid #f0f0f0;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.file-card:active {
|
|
|
+ transform: scale(0.98);
|
|
|
+ border-color: #2979ff;
|
|
|
+}
|
|
|
+
|
|
|
+.file-icon-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 200rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.file-name-display {
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
+ background: #fff;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #666;
|
|
|
+ text-align: center;
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+// 涉及其他学生样式
|
|
|
+.involved-students {
|
|
|
+ margin-top: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.student-card {
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 16rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ border: 1rpx solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.student-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.student-avatar {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.student-basic {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.student-name {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 4rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.student-no {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.student-gender {
|
|
|
+ margin-left: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.student-details {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8rpx;
|
|
|
+ margin-top: 12rpx;
|
|
|
+ padding-left: 0rpx !important; /* Align with avatar */
|
|
|
+}
|
|
|
+
|
|
|
+.detail-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-label {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #909399;
|
|
|
+ min-width: 100rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-value {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #303133;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+// 涉及其他学生(加载状态)样式
|
|
|
+.loading-involved-students {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 100rpx 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+// 涉及其他学生(空状态)样式
|
|
|
+.empty-involved-students {
|
|
|
+ padding: 100rpx 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+// 涉及其他学生(错误状态)样式
|
|
|
+.error-involved-students {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 100rpx 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.error-actions {
|
|
|
+ margin-top: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+// 响应式适配
|
|
|
+@media screen and (max-width: 750rpx) {
|
|
|
+ .file-grid {
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(240rpx, 1fr));
|
|
|
+ gap: 15rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-icon-container {
|
|
|
+ height: 160rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-grid {
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(240rpx, 1fr));
|
|
|
+ gap: 15rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-container {
|
|
|
+ height: 160rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .file-actions .u-button,
|
|
|
+ .image-actions .u-button {
|
|
|
+ font-size: 22rpx !important;
|
|
|
+ padding: 6rpx 12rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 涉及学生卡片响应式调整
|
|
|
+ .student-card {
|
|
|
+ padding: 12rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-avatar {
|
|
|
+ width: 50rpx;
|
|
|
+ height: 50rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-name {
|
|
|
+ font-size: 28rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-no {
|
|
|
+ font-size: 22rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-details {
|
|
|
+ padding-left: 50rpx;
|
|
|
+ gap: 6rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-label {
|
|
|
+ font-size: 24rpx;
|
|
|
+ min-width: 80rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-value {
|
|
|
+ font-size: 24rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .student-count {
|
|
|
+ font-size: 24rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载状态响应式调整
|
|
|
+ .loading-involved-students {
|
|
|
+ padding: 60rpx 20rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 错误状态响应式调整
|
|
|
+ .error-involved-students {
|
|
|
+ padding: 60rpx 20rpx;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// uview组件样式调整
|
|
|
+::v-deep .u-tag {
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+</style>
|