|
@@ -0,0 +1,2131 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="cancel-leave-edit-container">
|
|
|
|
|
+ <!-- 学生请假信息(只读展示) -->
|
|
|
|
|
+ <view class="student-info-section">
|
|
|
|
|
+ <view class="info-title">请假信息</view>
|
|
|
|
|
+ <view class="info-content">
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">学生姓名:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.studentName || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">学号:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.studentNo || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">班级名称:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.className || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">专业名称:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.majorName || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">学院名称:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.collegeName || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">请假类型:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.leaveTypeName || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">请假开始时间:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.startTime || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">请假结束时间:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.endTime || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">请假地点:</text>
|
|
|
|
|
+ <view class="info-value location-value">
|
|
|
|
|
+ <text>{{ '经纬度:' + leaveInfo.leaveLocation || '-' }}</text>
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ v-if="leaveInfo.leaveLocation"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ plain
|
|
|
|
|
+ :custom-style="{ width: 'auto', height: 'auto' }"
|
|
|
|
|
+ @click="openLeaveLocationInMap"
|
|
|
|
|
+ >
|
|
|
|
|
+ 查看位置
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item">
|
|
|
|
|
+ <text class="info-label">请假时长:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.leaveDuration || '0' }} 天</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="info-item span-2">
|
|
|
|
|
+ <text class="info-label">请假原因:</text>
|
|
|
|
|
+ <text class="info-value">{{ leaveInfo.leaveReason || '-' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 只读附件展示:与请假界面一致的风格(图片/文档分区网格) -->
|
|
|
|
|
+ <view v-if="leaveAttachmentList.length > 0" class="readonly-attachment">
|
|
|
|
|
+ <!-- 图片附件(只读) -->
|
|
|
|
|
+ <view v-if="leaveImageFiles.length > 0" class="image-section">
|
|
|
|
|
+ <view class="image-title">请假附件(图片)</view>
|
|
|
|
|
+ <view class="image-grid">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(image, index) in leaveImageFiles"
|
|
|
|
|
+ :key="image.uid || index"
|
|
|
|
|
+ class="image-item"
|
|
|
|
|
+ @click="handleImageClick(image, index, $event)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="image-container">
|
|
|
|
|
+ <image
|
|
|
|
|
+ :src="image.blobUrl || image.url"
|
|
|
|
|
+ class="attachment-image"
|
|
|
|
|
+ mode="aspectFill"
|
|
|
|
|
+ ></image>
|
|
|
|
|
+ <view v-if="image.status === 'success'" 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 v-if="leaveNonImageFiles.length > 0" class="custom-file-list">
|
|
|
|
|
+ <view class="file-title">请假附件(文档)</view>
|
|
|
|
|
+ <view class="file-grid">
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="file-card"
|
|
|
|
|
+ v-for="(file, index) in leaveNonImageFiles"
|
|
|
|
|
+ :key="file.uid || index"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="file-icon-container">
|
|
|
|
|
+ <u-icon
|
|
|
|
|
+ name="folder"
|
|
|
|
|
+ size="50"
|
|
|
|
|
+ :color="
|
|
|
|
|
+ file.status === 'error'
|
|
|
|
|
+ ? '#fa3534'
|
|
|
|
|
+ : file.status === 'success'
|
|
|
|
|
+ ? '#19be6b'
|
|
|
|
|
+ : '#909399'
|
|
|
|
|
+ "
|
|
|
|
|
+ />
|
|
|
|
|
+ <view v-if="file.status === 'success'" 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="image-name">
|
|
|
|
|
+ <text>{{ getShortFileName(file.name) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 销假表单 -->
|
|
|
|
|
+ <u-form
|
|
|
|
|
+ ref="formRef"
|
|
|
|
|
+ :model="cancelForm"
|
|
|
|
|
+ :rules="rules"
|
|
|
|
|
+ label-width="200rpx"
|
|
|
|
|
+ label-position="left"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="form-section">
|
|
|
|
|
+ <view class="section-title">销假信息</view>
|
|
|
|
|
+
|
|
|
|
|
+ <u-form-item label="销假时间" prop="cancelTime" required>
|
|
|
|
|
+ <view @click="handleCancelTimeClick" style="width: 100%">
|
|
|
|
|
+ <u-input
|
|
|
|
|
+ v-model="cancelForm.cancelTime"
|
|
|
|
|
+ placeholder="请选择销假时间"
|
|
|
|
|
+ readonly
|
|
|
|
|
+ border="none"
|
|
|
|
|
+ suffix-icon="arrow-down"
|
|
|
|
|
+ suffix-icon-style="color: #c8c9cc;"
|
|
|
|
|
+ style="pointer-events: none"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </u-form-item>
|
|
|
|
|
+ <u-datetime-picker
|
|
|
|
|
+ :show="showCancelTimePicker"
|
|
|
|
|
+ v-model="cancelTimeValue"
|
|
|
|
|
+ mode="datetime"
|
|
|
|
|
+ :minDate="minDate"
|
|
|
|
|
+ :maxDate="maxDate"
|
|
|
|
|
+ @confirm="handleCancelTimeConfirm"
|
|
|
|
|
+ @cancel="showCancelTimePicker = false"
|
|
|
|
|
+ ></u-datetime-picker>
|
|
|
|
|
+
|
|
|
|
|
+ <u-form-item label="销假地点" prop="cancelLocation" required>
|
|
|
|
|
+ <view class="location-row">
|
|
|
|
|
+ <u-button type="primary" size="mini" plain @click="handleGetLocation">
|
|
|
|
|
+ <u-icon name="map-fill" size="20" color="#2979ff" />
|
|
|
|
|
+ <text style="margin-left: 8rpx">
|
|
|
|
|
+ {{ isWechatBrowser ? '微信定位' : '定位' }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </u-form-item>
|
|
|
|
|
+ <u-form-item label="坐标">
|
|
|
|
|
+ <view v-if="cancelForm.cancelLocation" class="location-info">
|
|
|
|
|
+ <view class="location-coords-container">
|
|
|
|
|
+ <text class="location-coords">
|
|
|
|
|
+ 经纬度: {{ cancelForm.cancelLocation }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="location-action-container">
|
|
|
|
|
+ <u-button size="mini" type="primary" plain @click="openLocationInMap">
|
|
|
|
|
+ <text>查看位置</text>
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </u-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <u-form-item label="销假备注" prop="cancelRemark">
|
|
|
|
|
+ <u-textarea
|
|
|
|
|
+ v-model="cancelForm.cancelRemark"
|
|
|
|
|
+ placeholder="请输入销假备注"
|
|
|
|
|
+ :maxlength="300"
|
|
|
|
|
+ height="100"
|
|
|
|
|
+ count
|
|
|
|
|
+ />
|
|
|
|
|
+ </u-form-item>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 销假附件上传 -->
|
|
|
|
|
+ <view class="form-section">
|
|
|
|
|
+ <view class="section-title">附件上传</view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="upload-area">
|
|
|
|
|
+ <!-- 图片展示区域 -->
|
|
|
|
|
+ <view v-if="imageFiles.length > 0" class="image-section">
|
|
|
|
|
+ <view class="image-title">图片附件</view>
|
|
|
|
|
+ <view class="image-grid">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(image, index) in imageFiles"
|
|
|
|
|
+ :key="image.uid || index"
|
|
|
|
|
+ class="image-item"
|
|
|
|
|
+ @click="handleImageClick(image, index, $event)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="image-container">
|
|
|
|
|
+ <image
|
|
|
|
|
+ :src="image.blobUrl || image.url"
|
|
|
|
|
+ class="attachment-image"
|
|
|
|
|
+ mode="aspectFill"
|
|
|
|
|
+ ></image>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 上传中遮罩 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="image.status === 'uploading'"
|
|
|
|
|
+ class="upload-overlay"
|
|
|
|
|
+ >
|
|
|
|
|
+ <u-loading-icon
|
|
|
|
|
+ mode="circle"
|
|
|
|
|
+ size="24"
|
|
|
|
|
+ color="#fff"
|
|
|
|
|
+ ></u-loading-icon>
|
|
|
|
|
+ <text class="upload-text">{{ image.progress || 0 }}%</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- 错误遮罩 -->
|
|
|
|
|
+ <view v-if="image.status === 'error'" class="error-overlay">
|
|
|
|
|
+ <u-icon name="close-circle" size="24" color="#fff"></u-icon>
|
|
|
|
|
+ <text class="error-text">上传失败</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- 成功角标 -->
|
|
|
|
|
+ <view v-if="image.status === 'success'" 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
|
|
|
|
|
+ v-if="image.status === 'success'"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @tap="handleImageDownload(image)"
|
|
|
|
|
+ @click.stop.prevent="
|
|
|
|
|
+ handleImageDownloadClick(image, $event)
|
|
|
|
|
+ "
|
|
|
|
|
+ >
|
|
|
|
|
+ 下载
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ v-if="image.status === 'error'"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @click.stop.prevent="retryUploadFile(image, $event)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 重试
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="error"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @tap="handleImageDelete(image, index, $event)"
|
|
|
|
|
+ @click.stop.prevent="
|
|
|
|
|
+ handleImageDeleteClick(image, index, $event)
|
|
|
|
|
+ "
|
|
|
|
|
+ >
|
|
|
|
|
+ 删除
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="image-name">
|
|
|
|
|
+ <text>{{ getShortFileName(image.name) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 上传进度条(微信图片场景) -->
|
|
|
|
|
+ <view v-if="uploadProgress > 0 && uploadProgress < 100" class="upload-progress">
|
|
|
|
|
+ <u-line-progress
|
|
|
|
|
+ :percent="uploadProgress"
|
|
|
|
|
+ :show-percent="true"
|
|
|
|
|
+ active-color="#2979ff"
|
|
|
|
|
+ inactive-color="#f0f0f0"
|
|
|
|
|
+ height="8"
|
|
|
|
|
+ :striped="true"
|
|
|
|
|
+ :striped-active="true"
|
|
|
|
|
+ ></u-line-progress>
|
|
|
|
|
+ <text class="progress-text">微信图片上传进度: {{ uploadProgress }}%</text>
|
|
|
|
|
+ </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, index) in nonImageFiles"
|
|
|
|
|
+ :key="file.uid"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="file-icon-container">
|
|
|
|
|
+ <u-icon
|
|
|
|
|
+ :name="file.status === 'uploading' ? 'loading' : 'folder'"
|
|
|
|
|
+ size="50"
|
|
|
|
|
+ :color="
|
|
|
|
|
+ file.status === 'error'
|
|
|
|
|
+ ? '#fa3534'
|
|
|
|
|
+ : file.status === 'success'
|
|
|
|
|
+ ? '#19be6b'
|
|
|
|
|
+ : '#909399'
|
|
|
|
|
+ "
|
|
|
|
|
+ />
|
|
|
|
|
+ <view v-if="file.status === 'uploading'" class="upload-overlay">
|
|
|
|
|
+ <u-loading-icon
|
|
|
|
|
+ mode="circle"
|
|
|
|
|
+ size="24"
|
|
|
|
|
+ color="#fff"
|
|
|
|
|
+ ></u-loading-icon>
|
|
|
|
|
+ <text class="upload-text">{{ file.progress || 0 }}%</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="file.status === 'error'" class="error-overlay">
|
|
|
|
|
+ <u-icon name="close-circle" size="24" color="#fff"></u-icon>
|
|
|
|
|
+ <text class="error-text">上传失败</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="file.status === 'success'" 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
|
|
|
|
|
+ v-if="file.status === 'success'"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @tap="handleDownloadFile(file)"
|
|
|
|
|
+ @click.stop.prevent="handleDownloadFileClick(file, $event)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 下载
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ v-if="file.status === 'error'"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @click.stop.prevent="retryUploadFile(file, $event)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 重试
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ type="error"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @click.stop.prevent="
|
|
|
|
|
+ handleRemoveFile(
|
|
|
|
|
+ file,
|
|
|
|
|
+ index +
|
|
|
|
|
+ attachmentList.filter((f) => isImageFile(f))
|
|
|
|
|
+ .length,
|
|
|
|
|
+ $event,
|
|
|
|
|
+ )
|
|
|
|
|
+ "
|
|
|
|
|
+ >
|
|
|
|
|
+ 删除
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="file-name-display">
|
|
|
|
|
+ <text>{{ getShortFileName(file.name) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 统一附件上传按钮 -->
|
|
|
|
|
+ <view class="doc-upload-section">
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ icon="plus"
|
|
|
|
|
+ plain
|
|
|
|
|
+ @click="handleDocUpload"
|
|
|
|
|
+ style="margin-top: 20rpx; width: 100%"
|
|
|
|
|
+ >
|
|
|
|
|
+ 添加附件(图片)
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 上传提示信息 -->
|
|
|
|
|
+ <view class="upload-tips">
|
|
|
|
|
+ <text class="tip-item">
|
|
|
|
|
+ • 支持格式:JPG、PNG等 • 单个文件不超过10MB • 最多上传15个文件
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text class="tip-item">• 图片文件将以图片形式展示,支持预览和下载</text>
|
|
|
|
|
+ <text v-if="isWechatBrowser" class="tip-item wechat-tip">
|
|
|
|
|
+ • 在微信中使用可获得更好的图片选择体验
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </u-form>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 审批按钮 -->
|
|
|
|
|
+ <approval-button
|
|
|
|
|
+ :id="cancelForm.id"
|
|
|
|
|
+ :status="cancelForm.status"
|
|
|
|
|
+ :page-type="routeParams.type"
|
|
|
|
|
+ :task-id="routeParams.taskId"
|
|
|
|
|
+ :button-loading="buttonLoading"
|
|
|
|
|
+ @submitForm="handleSubmitForm"
|
|
|
|
|
+ @approvalVerifyOpen="handleApprovalVerifyOpen"
|
|
|
|
|
+ @handleApprovalRecord="handleApprovalRecord"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 审批弹窗 -->
|
|
|
|
|
+ <submit-verify
|
|
|
|
|
+ ref="submitVerifyRef"
|
|
|
|
|
+ :task-variables="taskVariables"
|
|
|
|
|
+ @submit-callback="handleSubmitCallback"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 审批记录 -->
|
|
|
|
|
+ <approval-record ref="approvalRecordRef" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 流程选择弹窗 -->
|
|
|
|
|
+ <u-popup
|
|
|
|
|
+ :show="showFlowSelectionPopup"
|
|
|
|
|
+ @close="handleFlowSelectionClose"
|
|
|
|
|
+ mode="center"
|
|
|
|
|
+ width="80%"
|
|
|
|
|
+ border-radius="12"
|
|
|
|
|
+ :custom-style="{ width: '80vw' }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="flow-selection-popup">
|
|
|
|
|
+ <view class="popup-header">
|
|
|
|
|
+ <text class="popup-title">选择工作流程</text>
|
|
|
|
|
+ <u-icon
|
|
|
|
|
+ name="close"
|
|
|
|
|
+ size="20"
|
|
|
|
|
+ color="#666"
|
|
|
|
|
+ @click="handleFlowSelectionClose"
|
|
|
|
|
+ ></u-icon>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="popup-content">
|
|
|
|
|
+ <text class="popup-tip">请选择合适的销假流程</text>
|
|
|
|
|
+ <view class="flow-options">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="item in flowCodeOptions"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ class="flow-option-item"
|
|
|
|
|
+ :class="{ selected: selectedFlowCode === item.value }"
|
|
|
|
|
+ @click="handleFlowCodeSelect(item.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="option-content">
|
|
|
|
|
+ <text class="option-label">{{ item.label }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <u-icon
|
|
|
|
|
+ v-if="selectedFlowCode === item.value"
|
|
|
|
|
+ name="checkmark-circle-fill"
|
|
|
|
|
+ size="20"
|
|
|
|
|
+ color="#2979ff"
|
|
|
|
|
+ ></u-icon>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="popup-actions">
|
|
|
|
|
+ <u-button @click="handleFlowSelectionClose">取消</u-button>
|
|
|
|
|
+ <u-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="handleFlowSelectionConfirm"
|
|
|
|
|
+ :disabled="!selectedFlowCode"
|
|
|
|
|
+ >
|
|
|
|
|
+ 确认
|
|
|
|
|
+ </u-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </u-popup>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { request, smartUploadFile, downloadFile, isWeChat } from '@/common/request.js';
|
|
|
|
|
+import ApprovalButton from '@/components/Process/approvalButton.vue';
|
|
|
|
|
+import SubmitVerify from '@/components/Process/submitVerify.vue';
|
|
|
|
|
+import ApprovalRecord from '@/components/Process/approvalRecord.vue';
|
|
|
|
|
+import weChatSDK from '@/utils/wechat.js';
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'CancelLeaveEdit',
|
|
|
|
|
+ components: { ApprovalButton, SubmitVerify, ApprovalRecord },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ return {
|
|
|
|
|
+ routeParams: {},
|
|
|
|
|
+ buttonLoading: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 流程相关
|
|
|
|
|
+ showFlowSelectionPopup: false,
|
|
|
|
|
+ selectedFlowCode: '',
|
|
|
|
|
+ flowCodeOptions: [{ label: '学生销假流程-杭州第一技师', value: 'hzdyjs-xj' }],
|
|
|
|
|
+
|
|
|
|
|
+ // 微信SDK
|
|
|
|
|
+ wechatSDK: weChatSDK,
|
|
|
|
|
+ isWechatBrowser: false,
|
|
|
|
|
+ wechatInitializing: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 定位
|
|
|
|
|
+ geoLatitude: null,
|
|
|
|
|
+ geoLongitude: null,
|
|
|
|
|
+
|
|
|
|
|
+ // 时间选择
|
|
|
|
|
+ showCancelTimePicker: false,
|
|
|
|
|
+ cancelTimeValue: now.getTime(),
|
|
|
|
|
+ minDate: new Date('2000-01-01 00:00:00').getTime(),
|
|
|
|
|
+ maxDate: new Date('2099-12-31 23:59:59').getTime(),
|
|
|
|
|
+
|
|
|
|
|
+ // 表单
|
|
|
|
|
+ cancelForm: {
|
|
|
|
|
+ id: undefined,
|
|
|
|
|
+ cancelTime: '',
|
|
|
|
|
+ cancelLocation: '',
|
|
|
|
|
+ cancelRemark: '',
|
|
|
|
|
+ cancelPhotoOssIds: '',
|
|
|
|
|
+ status: 'draft',
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 只读请假信息
|
|
|
|
|
+ leaveInfo: {
|
|
|
|
|
+ id: undefined,
|
|
|
|
|
+ studentId: '',
|
|
|
|
|
+ studentName: '',
|
|
|
|
|
+ studentNo: '',
|
|
|
|
|
+ classId: '',
|
|
|
|
|
+ className: '',
|
|
|
|
|
+ majorId: '',
|
|
|
|
|
+ majorName: '',
|
|
|
|
|
+ collegeId: '',
|
|
|
|
|
+ collegeName: '',
|
|
|
|
|
+ leaveTypeCode: '',
|
|
|
|
|
+ leaveTypeName: '',
|
|
|
|
|
+ startTime: '',
|
|
|
|
|
+ endTime: '',
|
|
|
|
|
+ leaveDuration: '',
|
|
|
|
|
+ leaveReason: '',
|
|
|
|
|
+ attachmentOssIds: '',
|
|
|
|
|
+ // 新增:请假地点(经纬度字符串,例如 "30.123456,120.123456")
|
|
|
|
|
+ leaveLocation: '',
|
|
|
|
|
+ cancelStatus: 'draft',
|
|
|
|
|
+ cancelTime: '',
|
|
|
|
|
+ cancelLocation: '',
|
|
|
|
|
+ cancelRemark: '',
|
|
|
|
|
+ cancelPhotoOssIds: '',
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 附件
|
|
|
|
|
+ leaveAttachmentList: [],
|
|
|
|
|
+ attachmentList: [], // 销假附件
|
|
|
|
|
+ uploadProgress: 0,
|
|
|
|
|
+
|
|
|
|
|
+ // 提交/流程
|
|
|
|
|
+ taskVariables: {},
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ imageFiles() {
|
|
|
|
|
+ return this.attachmentList.filter((file) => this.isImageFile(file));
|
|
|
|
|
+ },
|
|
|
|
|
+ nonImageFiles() {
|
|
|
|
|
+ return this.attachmentList.filter((file) => !this.isImageFile(file));
|
|
|
|
|
+ },
|
|
|
|
|
+ // 只读请假附件 - 图片/文档分组
|
|
|
|
|
+ leaveImageFiles() {
|
|
|
|
|
+ return this.leaveAttachmentList.filter((file) => this.isImageFile(file));
|
|
|
|
|
+ },
|
|
|
|
|
+ leaveNonImageFiles() {
|
|
|
|
|
+ return this.leaveAttachmentList.filter((file) => !this.isImageFile(file));
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ async onLoad(options) {
|
|
|
|
|
+ this.routeParams = options || {};
|
|
|
|
|
+ await this.checkWechatEnvironment();
|
|
|
|
|
+ this.initPageData();
|
|
|
|
|
+ },
|
|
|
|
|
+ onUnload() {
|
|
|
|
|
+ this.cleanupBlobUrls();
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ // ============== 微信SDK ==============
|
|
|
|
|
+ async checkWechatEnvironment() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.isWechatBrowser = this.wechatSDK.isWeChatBrowser();
|
|
|
|
|
+ if (this.isWechatBrowser) {
|
|
|
|
|
+ await this.initWechatSDK();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('微信环境检查失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async initWechatSDK() {
|
|
|
|
|
+ if (this.wechatInitializing) return;
|
|
|
|
|
+ this.wechatInitializing = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.wechatSDK.init({
|
|
|
|
|
+ debug: process.env.NODE_ENV === 'development',
|
|
|
|
|
+ jsApiList: ['chooseImage', 'previewImage', 'getLocation', 'openLocation'],
|
|
|
|
|
+ });
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.wechatSDK.checkJsApi(['chooseImage', 'getLocation']);
|
|
|
|
|
+ } catch (apiError) {
|
|
|
|
|
+ console.warn('API支持检查失败:', apiError.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('微信SDK初始化失败:', error);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.wechatInitializing = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 初始化与数据加载 ==============
|
|
|
|
|
+ async initPageData() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const id = this.routeParams.id; // 传入的是请假ID或销假ID
|
|
|
|
|
+ if (!id) {
|
|
|
|
|
+ uni.showToast({ title: '缺少记录ID', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 先尝试当作销假ID反查请假ID
|
|
|
|
|
+ let leaveId = id;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const leaveIdResp = await request({
|
|
|
|
|
+ url: `/studentCancelLeave/studentCancelLeave/getLeaveId/${id}`,
|
|
|
|
|
+ });
|
|
|
|
|
+ if (leaveIdResp && leaveIdResp.code === 200 && leaveIdResp.data) {
|
|
|
|
|
+ leaveId = leaveIdResp.data;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (_) {}
|
|
|
|
|
+
|
|
|
|
|
+ // 加载请假信息
|
|
|
|
|
+ await this.getLeaveDetail(leaveId);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取已有销假ID
|
|
|
|
|
+ try {
|
|
|
|
|
+ const cancelIdResp = await request({
|
|
|
|
|
+ url: `/studentCancelLeave/studentCancelLeave/getCancelLeaveId/${leaveId}`,
|
|
|
|
|
+ });
|
|
|
|
|
+ const cancelId = cancelIdResp?.data;
|
|
|
|
|
+ this.cancelForm.id = cancelId;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('获取销假ID失败:', e?.message || e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如已有销假记录,加载详情
|
|
|
|
|
+ if (this.cancelForm.id) {
|
|
|
|
|
+ await this.getCancelLeaveDetail(this.cancelForm.id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 用请假记录中的已存在销假信息初始化
|
|
|
|
|
+ Object.assign(this.cancelForm, {
|
|
|
|
|
+ cancelTime: this.leaveInfo.cancelTime || '',
|
|
|
|
|
+ cancelLocation: this.leaveInfo.cancelLocation || '',
|
|
|
|
|
+ cancelRemark: this.leaveInfo.cancelRemark || '',
|
|
|
|
|
+ cancelPhotoOssIds: this.leaveInfo.cancelPhotoOssIds || '',
|
|
|
|
|
+ status: this.leaveInfo.cancelStatus || 'draft',
|
|
|
|
|
+ });
|
|
|
|
|
+ if (this.leaveInfo.cancelPhotoOssIds) {
|
|
|
|
|
+ await this.getAttachmentFiles(this.leaveInfo.cancelPhotoOssIds, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('初始化页面失败:', error);
|
|
|
|
|
+ uni.showToast({ title: '初始化失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async getLeaveDetail(id) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await request({
|
|
|
|
|
+ url: `/studentLeave/studentLeave/${id}`,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ });
|
|
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
|
|
+ const leave = response.data;
|
|
|
|
|
+ Object.assign(this.leaveInfo, leave);
|
|
|
|
|
+ // web端将status映射为cancelStatus
|
|
|
|
|
+ this.leaveInfo.cancelStatus = leave.cancelStatus || 'draft';
|
|
|
|
|
+ this.cancelForm.status = this.leaveInfo.cancelStatus;
|
|
|
|
|
+
|
|
|
|
|
+ // 附件(只读)
|
|
|
|
|
+ if (leave.attachmentOssIds) {
|
|
|
|
|
+ await this.getAttachmentFiles(leave.attachmentOssIds, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: response.msg || '获取请假详情失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('获取请假详情失败:', e);
|
|
|
|
|
+ uni.showToast({ title: '获取请假详情失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async getCancelLeaveDetail(id) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await request({
|
|
|
|
|
+ url: `/studentCancelLeave/studentCancelLeave/${id}`,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ });
|
|
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
|
|
+ const cancelInfo = response.data;
|
|
|
|
|
+ Object.assign(this.cancelForm, {
|
|
|
|
|
+ id: cancelInfo.id,
|
|
|
|
|
+ cancelTime: cancelInfo.cancelTime || '',
|
|
|
|
|
+ cancelLocation: cancelInfo.cancelLocation || '',
|
|
|
|
|
+ cancelRemark: cancelInfo.cancelRemark || '',
|
|
|
|
|
+ cancelPhotoOssIds: cancelInfo.cancelPhotoOssIds || '',
|
|
|
|
|
+ status: cancelInfo.cancelStatus || 'draft',
|
|
|
|
|
+ });
|
|
|
|
|
+ if (cancelInfo.cancelPhotoOssIds) {
|
|
|
|
|
+ await this.getAttachmentFiles(cancelInfo.cancelPhotoOssIds, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: response.msg || '获取销假详情失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('获取销假详情失败:', e);
|
|
|
|
|
+ uni.showToast({ title: '获取销假详情失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ossIds: 字符串, isCancel: 是否为销假附件
|
|
|
|
|
+ async getAttachmentFiles(ossIds, isCancel) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await request({
|
|
|
|
|
+ url: `/resource/oss/listByIds/${ossIds}`,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ });
|
|
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
|
|
+ const files = response.data.map((item, index) => {
|
|
|
|
|
+ const fileType = this.getFileTypeBySuffix(item.fileSuffix);
|
|
|
|
|
+ const isImage = this.isImageFile({
|
|
|
|
|
+ name: item.originalName,
|
|
|
|
|
+ type: fileType,
|
|
|
|
|
+ });
|
|
|
|
|
+ let processedUrl = item.url;
|
|
|
|
|
+ if (isImage && item.url && item.url.startsWith('/resource/')) {
|
|
|
|
|
+ processedUrl = `/api${item.url}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 统一完整文件名
|
|
|
|
|
+ let fullFileName = item.originalName || `附件${index + 1}`;
|
|
|
|
|
+ if (
|
|
|
|
|
+ item.fileSuffix &&
|
|
|
|
|
+ !fullFileName
|
|
|
|
|
+ .toLowerCase()
|
|
|
|
|
+ .endsWith(`.${String(item.fileSuffix).toLowerCase()}`)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ fullFileName = `${fullFileName}.${item.fileSuffix}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ uid: `${isCancel ? 'cancel' : 'leave'}_${index}_${Date.now()}`,
|
|
|
|
|
+ name: fullFileName,
|
|
|
|
|
+ url: processedUrl,
|
|
|
|
|
+ ossId: item.ossId,
|
|
|
|
|
+ status: 'success',
|
|
|
|
|
+ type: fileType,
|
|
|
|
|
+ size: item.fileSize || 0,
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ if (isCancel) {
|
|
|
|
|
+ this.attachmentList = files;
|
|
|
|
|
+ console.log('销假附件列表:', this.attachmentList);
|
|
|
|
|
+ // 确保图片显示正常(与请假页一致)
|
|
|
|
|
+ await this.processImageFilesFor(this.attachmentList);
|
|
|
|
|
+ this.$nextTick(() => this.preloadImagesForList(this.attachmentList));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.leaveAttachmentList = files;
|
|
|
|
|
+ // 确保图片显示正常:处理为blob并预加载
|
|
|
|
|
+ console.log('请假附件列表:', this.leaveAttachmentList);
|
|
|
|
|
+ await this.processImageFilesFor(this.leaveAttachmentList);
|
|
|
|
|
+ this.$nextTick(() => this.preloadImagesForList(this.leaveAttachmentList));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('获取附件信息失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 校验与时间处理 ==============
|
|
|
|
|
+ rules: {
|
|
|
|
|
+ cancelTime: [{ required: true, message: '请选择销假时间' }],
|
|
|
|
|
+ cancelLocation: [{ required: true, message: '请输入销假地点' }],
|
|
|
|
|
+ },
|
|
|
|
|
+ handleCancelTimeClick() {
|
|
|
|
|
+ this.showCancelTimePicker = true;
|
|
|
|
|
+ },
|
|
|
|
|
+ handleCancelTimeConfirm(e) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const date = new Date(e.value || e);
|
|
|
|
|
+ const Y = date.getFullYear();
|
|
|
|
|
+ const M = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
|
|
|
+ const D = date.getDate().toString().padStart(2, '0');
|
|
|
|
|
+ const h = date.getHours().toString().padStart(2, '0');
|
|
|
|
|
+ const m = date.getMinutes().toString().padStart(2, '0');
|
|
|
|
|
+ const s = date.getSeconds().toString().padStart(2, '0');
|
|
|
|
|
+ this.cancelForm.cancelTime = `${Y}-${M}-${D} ${h}:${m}:${s}`;
|
|
|
|
|
+ this.cancelTimeValue = date.getTime();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.showCancelTimePicker = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 定位 ==============
|
|
|
|
|
+ isH5Web() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
|
|
|
+ } catch (_) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ isBrowserGeolocationSupported() {
|
|
|
|
|
+ return this.isH5Web() && !!navigator.geolocation;
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleGetLocation() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ uni.showLoading({ title: '定位中...', mask: true });
|
|
|
|
|
+ if (this.isWechatBrowser && this.wechatSDK.isInitialized) {
|
|
|
|
|
+ const result = await this.wechatSDK.getLocation({ type: 'gcj02' });
|
|
|
|
|
+ if (result.latitude && result.longitude) {
|
|
|
|
|
+ this.geoLatitude = parseFloat(result.latitude);
|
|
|
|
|
+ this.geoLongitude = parseFloat(result.longitude);
|
|
|
|
|
+ this.cancelForm.cancelLocation = `${this.geoLatitude.toFixed(
|
|
|
|
|
+ 6,
|
|
|
|
|
+ )},${this.geoLongitude.toFixed(6)}`;
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({ title: '微信定位成功', icon: 'success' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.isH5Web() && this.isBrowserGeolocationSupported()) {
|
|
|
|
|
+ const success = await new Promise((resolve) => {
|
|
|
|
|
+ navigator.geolocation.getCurrentPosition(
|
|
|
|
|
+ (pos) => {
|
|
|
|
|
+ const { latitude, longitude } = pos.coords;
|
|
|
|
|
+ this.geoLatitude = latitude;
|
|
|
|
|
+ this.geoLongitude = longitude;
|
|
|
|
|
+ this.cancelForm.cancelLocation = `${latitude.toFixed(
|
|
|
|
|
+ 6,
|
|
|
|
|
+ )},${longitude.toFixed(6)}`;
|
|
|
|
|
+ resolve(true);
|
|
|
|
|
+ },
|
|
|
|
|
+ () => resolve(false),
|
|
|
|
|
+ { enableHighAccuracy: true, timeout: 10000 },
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({ title: '定位成功', icon: 'success' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // uni-app 回退
|
|
|
|
|
+ await new Promise((resolve) => {
|
|
|
|
|
+ uni.getLocation({
|
|
|
|
|
+ type: 'gcj02',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ this.geoLatitude = res.latitude;
|
|
|
|
|
+ this.geoLongitude = res.longitude;
|
|
|
|
|
+ this.cancelForm.cancelLocation = `${res.latitude},${res.longitude}`;
|
|
|
|
|
+ resolve(true);
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => resolve(false),
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({ title: '定位成功', icon: 'success' });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('定位失败:', error);
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '定位失败',
|
|
|
|
|
+ content: '定位不可用,请手动输入或稍后再试',
|
|
|
|
|
+ showCancel: false,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async openLocationInMap() {
|
|
|
|
|
+ if (!this.cancelForm.cancelLocation) return;
|
|
|
|
|
+ const [latStr, lngStr] = this.cancelForm.cancelLocation.split(',');
|
|
|
|
|
+ const latitude = parseFloat(latStr);
|
|
|
|
|
+ const longitude = parseFloat(lngStr);
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.isWechatBrowser && this.wechatSDK.isInitialized) {
|
|
|
|
|
+ await this.wechatSDK.openLocation({ latitude, longitude, name: '销假位置' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('微信openLocation失败,使用浏览器方案');
|
|
|
|
|
+ }
|
|
|
|
|
+ // H5 打开地图
|
|
|
|
|
+ if (this.isH5Web()) {
|
|
|
|
|
+ const url = `https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&coord=${latitude},${longitude}`;
|
|
|
|
|
+ window.open(url);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 新增:查看请假地点
|
|
|
|
|
+ async openLeaveLocationInMap() {
|
|
|
|
|
+ if (!this.leaveInfo.leaveLocation) return;
|
|
|
|
|
+ const [latStr, lngStr] = String(this.leaveInfo.leaveLocation).split(',');
|
|
|
|
|
+ const latitude = parseFloat(latStr);
|
|
|
|
|
+ const longitude = parseFloat(lngStr);
|
|
|
|
|
+ if (Number.isNaN(latitude) || Number.isNaN(longitude)) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.isWechatBrowser && this.wechatSDK.isInitialized) {
|
|
|
|
|
+ await this.wechatSDK.openLocation({ latitude, longitude, name: '请假位置' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('微信openLocation失败,使用浏览器方案');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.isH5Web()) {
|
|
|
|
|
+ const url = `https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&coord=${latitude},${longitude}`;
|
|
|
|
|
+ window.open(url);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 附件相关 ==============
|
|
|
|
|
+ handleDocUpload() {
|
|
|
|
|
+ if (this.attachmentList.length >= 15) {
|
|
|
|
|
+ uni.showToast({ title: '最多上传15个文件', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 仅启用选择图片(与请假页一致的简化入口)
|
|
|
|
|
+ uni.showActionSheet({
|
|
|
|
|
+ itemList: ['选择图片'],
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
|
|
+ this.chooseDocuments();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ chooseDocuments() {
|
|
|
|
|
+ if (typeof uni.chooseFile === 'function') {
|
|
|
|
|
+ uni.chooseFile({
|
|
|
|
|
+ count: Math.min(5, 15 - this.attachmentList.length),
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.tempFiles && res.tempFiles.length > 0) {
|
|
|
|
|
+ this.handleFileUpload({ tempFiles: res.tempFiles });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ uni.showToast({ title: '选择文件失败', icon: 'none' });
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackupUpload();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ handleBackupUpload() {
|
|
|
|
|
+ uni.showActionSheet({
|
|
|
|
|
+ itemList: ['选择图片'],
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
|
|
+ uni.chooseImage({
|
|
|
|
|
+ count: 5,
|
|
|
|
|
+ sizeType: ['original', 'compressed'],
|
|
|
|
|
+ sourceType: ['album', 'camera'],
|
|
|
|
|
+ success: (imageRes) => {
|
|
|
|
|
+ const files = imageRes.tempFilePaths.map((path, index) => {
|
|
|
|
|
+ const tempFile = imageRes.tempFiles?.[index];
|
|
|
|
|
+ return {
|
|
|
|
|
+ name:
|
|
|
|
|
+ path.split('/').pop() ||
|
|
|
|
|
+ `image_${Date.now()}_${index}.jpg`,
|
|
|
|
|
+ path,
|
|
|
|
|
+ url: path,
|
|
|
|
|
+ tempFilePath: path,
|
|
|
|
|
+ size: tempFile?.size || 0,
|
|
|
|
|
+ type: tempFile?.type || 'image/jpeg',
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ const valid = files.filter((f) => this.checkFileSize(f));
|
|
|
|
|
+ if (valid.length > 0) this.handleFileUpload({ tempFiles: valid });
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ // 与请假页一致的文件上传处理
|
|
|
|
|
+ handleFileUpload(event) {
|
|
|
|
|
+ // 显示用户提示
|
|
|
|
|
+ uni.showToast({ title: '文件选择成功,开始上传...', icon: 'loading', duration: 2000 });
|
|
|
|
|
+
|
|
|
|
|
+ let files = [];
|
|
|
|
|
+ if (event?.file) {
|
|
|
|
|
+ files = Array.isArray(event.file) ? event.file : [event.file];
|
|
|
|
|
+ } else if (event?.tempFiles) {
|
|
|
|
|
+ files = event.tempFiles;
|
|
|
|
|
+ } else if (event?.tempFilePaths) {
|
|
|
|
|
+ files = event.tempFilePaths.map((path, index) => ({
|
|
|
|
|
+ url: path,
|
|
|
|
|
+ path: path,
|
|
|
|
|
+ tempFilePath: path,
|
|
|
|
|
+ name: `文件${index + 1}`,
|
|
|
|
|
+ type: '',
|
|
|
|
|
+ }));
|
|
|
|
|
+ } else if (event?.url || event?.path || event?.tempFilePath) {
|
|
|
|
|
+ files = [event];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (files.length === 0) {
|
|
|
|
|
+ uni.showToast({ title: '未获取到文件,请重试', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 数量限制
|
|
|
|
|
+ if (this.attachmentList.length + files.length > 15) {
|
|
|
|
|
+ uni.showToast({ title: '最多只能上传15个文件', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 大小过滤
|
|
|
|
|
+ const validFiles = files.filter((file) => this.checkFileSize(file));
|
|
|
|
|
+ if (validFiles.length === 0) return;
|
|
|
|
|
+ if (validFiles.length < files.length) {
|
|
|
|
|
+ const filteredCount = files.length - validFiles.length;
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: `已过滤 ${filteredCount} 个超限文件`,
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理并入列
|
|
|
|
|
+ const newFiles = validFiles.map((file, index) => {
|
|
|
|
|
+ const filePath = file.url || file.path || file.tempFilePath;
|
|
|
|
|
+ const fileName =
|
|
|
|
|
+ file.name || filePath?.split('/').pop() || `图片${Date.now()}_${index}.jpg`;
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...file,
|
|
|
|
|
+ uid: `upload_${Date.now()}_${index}`,
|
|
|
|
|
+ name: fileName,
|
|
|
|
|
+ url: filePath,
|
|
|
|
|
+ path: filePath,
|
|
|
|
|
+ tempFilePath: filePath,
|
|
|
|
|
+ status: 'uploading',
|
|
|
|
|
+ ossId: null,
|
|
|
|
|
+ type: file.type || this.getFileTypeByName(fileName),
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.attachmentList.push(...newFiles);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: `开始上传 ${newFiles.length} 个文件`,
|
|
|
|
|
+ icon: 'loading',
|
|
|
|
|
+ duration: 1500,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ newFiles.forEach((file) => {
|
|
|
|
|
+ this.uploadFileToServer(file).catch(() => {});
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ checkFileSize(file) {
|
|
|
|
|
+ if (!file || typeof file.size !== 'number') return true;
|
|
|
|
|
+ const isLt10M = file.size / 1024 / 1024 < 10;
|
|
|
|
|
+ if (!isLt10M) {
|
|
|
|
|
+ uni.showToast({ title: '文件大小不能超过10MB', icon: 'none' });
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ isImageFile(file) {
|
|
|
|
|
+ const name = file?.name || '';
|
|
|
|
|
+ const type = file?.type || '';
|
|
|
|
|
+ const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
|
|
|
|
|
+ return imageTypes.includes(type) || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(name);
|
|
|
|
|
+ },
|
|
|
|
|
+ getFileTypeBySuffix(suffix) {
|
|
|
|
|
+ const ext = (suffix || '').toLowerCase();
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ jpg: 'image/jpeg',
|
|
|
|
|
+ jpeg: 'image/jpeg',
|
|
|
|
|
+ png: 'image/png',
|
|
|
|
|
+ gif: 'image/gif',
|
|
|
|
|
+ bmp: 'image/bmp',
|
|
|
|
|
+ webp: 'image/webp',
|
|
|
|
|
+ pdf: 'application/pdf',
|
|
|
|
|
+ doc: 'application/msword',
|
|
|
|
|
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
|
|
|
+ txt: 'text/plain',
|
|
|
|
|
+ mp4: 'video/mp4',
|
|
|
|
|
+ };
|
|
|
|
|
+ return map[ext] || 'application/octet-stream';
|
|
|
|
|
+ },
|
|
|
|
|
+ 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;
|
|
|
|
|
+ },
|
|
|
|
|
+ getFileExt(fileName) {
|
|
|
|
|
+ const ext = fileName?.split('.')?.pop()?.toUpperCase();
|
|
|
|
|
+ return ext || 'FILE';
|
|
|
|
|
+ },
|
|
|
|
|
+ // 重试上传(与请假页一致)
|
|
|
|
|
+ async retryUploadFile(file, event) {
|
|
|
|
|
+ if (event) {
|
|
|
|
|
+ event.stopPropagation && event.stopPropagation();
|
|
|
|
|
+ event.preventDefault && event.preventDefault();
|
|
|
|
|
+ }
|
|
|
|
|
+ file.status = 'uploading';
|
|
|
|
|
+ file.progress = 0;
|
|
|
|
|
+ await this.uploadFileToServer(file);
|
|
|
|
|
+ },
|
|
|
|
|
+ handleImageClick() {},
|
|
|
|
|
+ handleImageDelete(image, index, event) {
|
|
|
|
|
+ if (event) {
|
|
|
|
|
+ event.stopPropagation && event.stopPropagation();
|
|
|
|
|
+ event.preventDefault && event.preventDefault();
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '确认删除',
|
|
|
|
|
+ content: '确定要删除这张图片吗?删除后无法恢复。',
|
|
|
|
|
+ confirmColor: '#ff5722',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ const attachmentIndex = this.attachmentList.findIndex(
|
|
|
|
|
+ (file) =>
|
|
|
|
|
+ file.uid === image.uid ||
|
|
|
|
|
+ (file.ossId && file.ossId === image.ossId) ||
|
|
|
|
|
+ (file.url && file.url === image.url),
|
|
|
|
|
+ );
|
|
|
|
|
+ if (attachmentIndex >= 0) {
|
|
|
|
|
+ this.attachmentList.splice(attachmentIndex, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (image.blobUrl && image.blobUrl.startsWith('blob:')) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ URL.revokeObjectURL(image.blobUrl);
|
|
|
|
|
+ } catch (_) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showToast({ title: '删除成功', icon: 'success' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ handleImageDeleteClick(image, index, event) {
|
|
|
|
|
+ if (event) {
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ event.stopImmediatePropagation && event.stopImmediatePropagation();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.handleImageDelete(image, index, event);
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleImageDownload(image, event, retryCount = 0) {
|
|
|
|
|
+ if (image?.status && image.status !== 'success') {
|
|
|
|
|
+ uni.showToast({ title: '图片尚未上传完成', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ uni.showLoading({ title: '正在下载图片...', mask: true });
|
|
|
|
|
+ if (image?.ossId) {
|
|
|
|
|
+ const downloadUrl = `/resource/oss/download/${image.ossId}`;
|
|
|
|
|
+ await this.downloadAndSaveImage(downloadUrl, image.name || '图片');
|
|
|
|
|
+ } else if (image?.tempFilePath || image?.path || image?.url) {
|
|
|
|
|
+ // 本地文件直接触发下载(H5)
|
|
|
|
|
+ if (typeof document !== 'undefined') {
|
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
|
+ link.href = image.tempFilePath || image.path || image.url;
|
|
|
|
|
+ link.download = image.name || 'image';
|
|
|
|
|
+ link.style.display = 'none';
|
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
|
+ link.click();
|
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({ title: '下载成功', icon: 'success' });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('图片下载过程出错:', error);
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ if (retryCount < 2) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: `下载失败,正在重试(${retryCount + 1}/2)...`,
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 1500,
|
|
|
|
|
+ });
|
|
|
|
|
+ setTimeout(() => this.handleImageDownload(image, event, retryCount + 1), 1500);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showToast({ title: '下载失败,请稍后重试', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ handleImageDownloadClick(image, event) {
|
|
|
|
|
+ if (event) {
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ event.stopImmediatePropagation && event.stopImmediatePropagation();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.handleImageDownload(image, event);
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleDownloadAttachment(file) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!file) return;
|
|
|
|
|
+ if (!file.ossId) {
|
|
|
|
|
+ if (file.tempFilePath || file.path || file.url) {
|
|
|
|
|
+ if (typeof document !== 'undefined') {
|
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
|
+ link.href = file.tempFilePath || file.path || file.url;
|
|
|
|
|
+ link.download = file.name || 'attachment';
|
|
|
|
|
+ link.style.display = 'none';
|
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
|
+ link.click();
|
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showToast({ title: '无法下载该文件', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 与请假页一致:根据类型分别处理
|
|
|
|
|
+ const downloadUrl = `/resource/oss/download/${file.ossId}`;
|
|
|
|
|
+ if (this.isImageFile(file)) {
|
|
|
|
|
+ await this.downloadAndSaveImage(downloadUrl, file.name || '图片');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await this.downloadAndOpenDocument(downloadUrl, file.name || '文件');
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showToast({ title: '下载成功', icon: 'success' });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('下载失败:', e);
|
|
|
|
|
+ uni.showToast({ title: '下载失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ handleDownloadFile(file, event) {
|
|
|
|
|
+ this.handleDownloadAttachment(file);
|
|
|
|
|
+ },
|
|
|
|
|
+ handleDownloadFileClick(file, event) {
|
|
|
|
|
+ if (event) {
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ event.stopImmediatePropagation && event.stopImmediatePropagation();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.handleDownloadAttachment(file);
|
|
|
|
|
+ },
|
|
|
|
|
+ handleRemoveFile(file, index, event) {
|
|
|
|
|
+ if (event) {
|
|
|
|
|
+ event.stopPropagation && event.stopPropagation();
|
|
|
|
|
+ event.preventDefault && event.preventDefault();
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '确认删除',
|
|
|
|
|
+ content: '确定要删除这个文件吗?',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ const fileIndex = this.attachmentList.findIndex((f) => f.uid === file.uid);
|
|
|
|
|
+ if (fileIndex > -1) {
|
|
|
|
|
+ this.attachmentList.splice(fileIndex, 1);
|
|
|
|
|
+ uni.showToast({ title: '文件删除成功', icon: 'success' });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ cleanupBlobUrls() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.attachmentList.forEach((f) => {
|
|
|
|
|
+ if (f.blobUrl && typeof URL !== 'undefined') {
|
|
|
|
|
+ URL.revokeObjectURL(f.blobUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ this.leaveAttachmentList.forEach((f) => {
|
|
|
|
|
+ if (f.blobUrl && typeof URL !== 'undefined') {
|
|
|
|
|
+ URL.revokeObjectURL(f.blobUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (_) {}
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 审批与提交 ==============
|
|
|
|
|
+ async handleApprovalVerifyOpen() {
|
|
|
|
|
+ this.$refs.submitVerifyRef.openDialog(this.routeParams.taskId || '');
|
|
|
|
|
+ },
|
|
|
|
|
+ handleApprovalRecord() {
|
|
|
|
|
+ if (!this.cancelForm.id) {
|
|
|
|
|
+ uni.showToast({ title: '请先保存数据', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$refs.approvalRecordRef && this.$refs.approvalRecordRef.init(this.cancelForm.id);
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleSubmitCallback() {
|
|
|
|
|
+ // uni.navigateBack({ delta: 1 });
|
|
|
|
|
+ console.log('handleSubmitCallback 回调成功');
|
|
|
|
|
+ // 审批提交成功提示
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '提交成功',
|
|
|
|
|
+ icon: 'success',
|
|
|
|
|
+ duration: 1500,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟执行后续操作,确保用户看到成功提示
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ // 发送事件通知请假列表页面刷新数据
|
|
|
|
|
+ uni.$emit('refreshLeaveList');
|
|
|
|
|
+
|
|
|
|
|
+ // 无论是编辑模式还是新增模式,都跳转到请假列表页面
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/xg/xsjcxx/xsqj/leave/index',
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 1600);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async handleSubmitForm(status) {
|
|
|
|
|
+ // 必填校验
|
|
|
|
|
+ if (!this.cancelForm.cancelTime) {
|
|
|
|
|
+ uni.showToast({ title: '销假时间不能为空', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.cancelForm.cancelLocation) {
|
|
|
|
|
+ uni.showToast({ title: '销假地点不能为空', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 汇总附件ossIds
|
|
|
|
|
+ const cancelPhotoOssIds = this.attachmentList
|
|
|
|
|
+ .filter((f) => f.status === 'success' && f.ossId)
|
|
|
|
|
+ .map((f) => f.ossId)
|
|
|
|
|
+ .join(',');
|
|
|
|
|
+ this.cancelForm.cancelPhotoOssIds = cancelPhotoOssIds;
|
|
|
|
|
+
|
|
|
|
|
+ // 构建保存数据
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ // 请假信息(使用扩展运算符包含所有leaveInfo字段,但排除id)
|
|
|
|
|
+ ...Object.fromEntries(
|
|
|
|
|
+ Object.entries(this.leaveInfo).filter(([key]) => key !== 'id'),
|
|
|
|
|
+ ),
|
|
|
|
|
+ // 销假信息
|
|
|
|
|
+ id: this.cancelForm.id,
|
|
|
|
|
+ leaveId: this.leaveInfo.id,
|
|
|
|
|
+ cancelTime: this.cancelForm.cancelTime,
|
|
|
|
|
+ cancelLocation: this.cancelForm.cancelLocation,
|
|
|
|
|
+ cancelRemark: this.cancelForm.cancelRemark,
|
|
|
|
|
+ cancelPhotoOssIds: this.cancelForm.cancelPhotoOssIds,
|
|
|
|
|
+ cancelStatus: status === 'draft' ? 'draft' : this.cancelForm.status || 'draft',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.buttonLoading = true;
|
|
|
|
|
+ let resp;
|
|
|
|
|
+ if (payload.id) {
|
|
|
|
|
+ resp = await request({
|
|
|
|
|
+ url: '/studentCancelLeave/studentCancelLeave',
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ data: payload,
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ resp = await request({
|
|
|
|
|
+ url: '/studentCancelLeave/studentCancelLeave',
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: payload,
|
|
|
|
|
+ });
|
|
|
|
|
+ if (resp && resp.data && resp.data.id) {
|
|
|
|
|
+ this.cancelForm.id = resp.data.id;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (status === 'draft') {
|
|
|
|
|
+ this.buttonLoading = false;
|
|
|
|
|
+ uni.showToast({ title: '暂存成功', icon: 'success' });
|
|
|
|
|
+ uni.navigateBack({ delta: 1 });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 正式提交:选流程或直接发起
|
|
|
|
|
+ if (!this.selectedFlowCode) {
|
|
|
|
|
+ // 首次触发,弹出流程选择
|
|
|
|
|
+ this.selectedFlowCode = this.flowCodeOptions[0].value;
|
|
|
|
|
+ this.showFlowSelectionPopup = true;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await this.startWorkflow();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('提交失败:', e);
|
|
|
|
|
+ uni.showToast({ title: '提交失败', icon: 'none' });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.buttonLoading = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleFlowCodeSelect(code) {
|
|
|
|
|
+ this.selectedFlowCode = code;
|
|
|
|
|
+ },
|
|
|
|
|
+ handleFlowSelectionClose() {
|
|
|
|
|
+ this.showFlowSelectionPopup = false;
|
|
|
|
|
+ },
|
|
|
|
|
+ async handleFlowSelectionConfirm() {
|
|
|
|
|
+ if (!this.selectedFlowCode) return;
|
|
|
|
|
+ this.showFlowSelectionPopup = false;
|
|
|
|
|
+ await this.startWorkflow();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async startWorkflow() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取班主任ID
|
|
|
|
|
+ const classTeacherResp = await request({
|
|
|
|
|
+ url: `/studentMsg/studentMsg/getClassTeacherId/${this.leaveInfo.studentId}`,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ });
|
|
|
|
|
+ const classTeacherId = classTeacherResp?.data;
|
|
|
|
|
+
|
|
|
|
|
+ this.taskVariables = {
|
|
|
|
|
+ bzrspr: classTeacherId,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const workflowResponse = await request({
|
|
|
|
|
+ url: '/workflow/task/startWorkFlow',
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ flowCode: this.selectedFlowCode,
|
|
|
|
|
+ businessId: this.cancelForm.id,
|
|
|
|
|
+ variables: this.taskVariables,
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (workflowResponse.code === 200) {
|
|
|
|
|
+ this.$refs.submitVerifyRef.openDialog(workflowResponse.data.taskId);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('启动工作流失败:', error);
|
|
|
|
|
+ uni.showToast({ title: '启动工作流失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ============== 只读附件图片处理与下载(对齐请假页) ==============
|
|
|
|
|
+ async getImageBlobUrl(imageUrl) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ let apiUrl = imageUrl;
|
|
|
|
|
+ if (imageUrl.startsWith('/api/')) {
|
|
|
|
|
+ apiUrl = imageUrl.replace('/api', '');
|
|
|
|
|
+ } else if (imageUrl.startsWith('http')) {
|
|
|
|
|
+ return imageUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ const response = await request({
|
|
|
|
|
+ url: apiUrl,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ responseType: 'arraybuffer',
|
|
|
|
|
+ header: { isToken: true },
|
|
|
|
|
+ });
|
|
|
|
|
+ const blob = new Blob([response], { type: 'image/*' });
|
|
|
|
|
+ const blobUrl = URL.createObjectURL(blob);
|
|
|
|
|
+ return blobUrl;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取图片blob失败:', error);
|
|
|
|
|
+ return imageUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async processImageFilesFor(list) {
|
|
|
|
|
+ const imageFiles = (list || []).filter((f) => this.isImageFile(f));
|
|
|
|
|
+ for (const file of imageFiles) {
|
|
|
|
|
+ if (file.url && !file.blobUrl) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const blobUrl = await this.getImageBlobUrl(file.url);
|
|
|
|
|
+ if (blobUrl) {
|
|
|
|
|
+ file.blobUrl = blobUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('处理图片失败:', file?.name, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async preloadImagesForList(list) {
|
|
|
|
|
+ const imageFiles = (list || []).filter((f) => this.isImageFile(f));
|
|
|
|
|
+ for (const file of imageFiles) {
|
|
|
|
|
+ if (file.blobUrl || file.url) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const img = new Image();
|
|
|
|
|
+ img.src = file.blobUrl || file.url;
|
|
|
|
|
+ } catch (_) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async downloadAndSaveImage(downloadUrl, fileName) {
|
|
|
|
|
+ const response = await downloadFile(downloadUrl, fileName);
|
|
|
|
|
+ if (response.statusCode === 200) {
|
|
|
|
|
+ uni.saveFile({
|
|
|
|
|
+ tempFilePath: response.tempFilePath,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ uni.showToast({ title: '下载成功', icon: 'success' });
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ uni.showToast({ title: '保存失败', icon: 'none' });
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async downloadAndOpenDocument(downloadUrl, fileName) {
|
|
|
|
|
+ const response = await downloadFile(downloadUrl, fileName);
|
|
|
|
|
+ if (response.statusCode === 200) {
|
|
|
|
|
+ uni.saveFile({
|
|
|
|
|
+ tempFilePath: response.tempFilePath,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ uni.showToast({ title: '下载成功', icon: 'success' });
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ uni.showToast({ title: '保存失败', icon: 'none' });
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 根据文件名获取文件类型(与请假页一致)
|
|
|
|
|
+ getFileTypeByName(fileName) {
|
|
|
|
|
+ if (!fileName) return '';
|
|
|
|
|
+ const ext = fileName.split('.').pop()?.toLowerCase();
|
|
|
|
|
+ const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
|
|
|
|
+ if (imageTypes.includes(ext)) {
|
|
|
|
|
+ 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}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return `file/${ext}`;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 上传文件到服务器(与请假页一致的调用方式)
|
|
|
|
|
+ async uploadFileToServer(file) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ file.status = 'uploading';
|
|
|
|
|
+ const filePath = file.url || file.tempFilePath || file.path;
|
|
|
|
|
+ if (!filePath) {
|
|
|
|
|
+ file.status = 'error';
|
|
|
|
|
+ throw new Error('文件路径不存在');
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showLoading({ title: `上传${file.name}...`, mask: true });
|
|
|
|
|
+ const response = await smartUploadFile('/resource/oss/upload', file, {
|
|
|
|
|
+ bizType: 'student_leave_attachment',
|
|
|
|
|
+ });
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ if (response && response.code === 200) {
|
|
|
|
|
+ file.url = response.data.url;
|
|
|
|
|
+ file.ossId = response.data.ossId;
|
|
|
|
|
+ file.status = 'success';
|
|
|
|
|
+ uni.showToast({ title: '上传成功', icon: 'success', duration: 1500 });
|
|
|
|
|
+ return response;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ file.status = 'error';
|
|
|
|
|
+ const errorMsg = response?.msg || '上传失败';
|
|
|
|
|
+ uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 });
|
|
|
|
|
+ throw new Error(errorMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ file.status = 'error';
|
|
|
|
|
+ let errorMessage = '上传失败';
|
|
|
|
|
+ if (error.message) {
|
|
|
|
|
+ if (error.message.includes('timeout'))
|
|
|
|
|
+ errorMessage = '上传超时,请检查网络连接';
|
|
|
|
|
+ else if (error.message.includes('网络'))
|
|
|
|
|
+ errorMessage = '网络连接失败,请检查网络';
|
|
|
|
|
+ else if (error.message.includes('文件路径'))
|
|
|
|
|
+ errorMessage = '文件选择异常,请重新选择';
|
|
|
|
|
+ else errorMessage = error.message;
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '上传失败',
|
|
|
|
|
+ content: `文件"${file.name}"上传失败:${errorMessage}`,
|
|
|
|
|
+ showCancel: true,
|
|
|
|
|
+ cancelText: '取消',
|
|
|
|
|
+ confirmText: '重试',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ file.status = 'pending';
|
|
|
|
|
+ this.uploadFileToServer(file);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const index = this.attachmentList.findIndex((f) => f.uid === file.uid);
|
|
|
|
|
+ if (index > -1) this.attachmentList.splice(index, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.cancel-leave-edit-container {
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.student-info-section {
|
|
|
|
|
+ background: #f8f9fa;
|
|
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.info-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ padding-bottom: 12rpx;
|
|
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
|
|
+}
|
|
|
|
|
+.info-content {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ row-gap: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.info-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 48rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.span-2 {
|
|
|
|
|
+ grid-column: 1 / -1;
|
|
|
|
|
+}
|
|
|
|
|
+.info-label {
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ margin-right: 8rpx;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+.info-value {
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+.location-value {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.readonly-attachment {
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.readonly-upload-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-file-card.readonly {
|
|
|
|
|
+ width: 240rpx;
|
|
|
|
|
+ height: 220rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-file-thumb {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 150rpx;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-thumb-image {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-file-placeholder {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+}
|
|
|
|
|
+.file-ext {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-file-name-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
|
|
+ height: 70rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-file-name {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ max-width: 180rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.download-icon {
|
|
|
|
|
+ padding: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-section {
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.location-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+.location-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+.location-coords-container {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.location-coords {
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+.location-action-container {
|
|
|
|
|
+ margin-left: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-area {
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.image-title,
|
|
|
|
|
+.file-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ margin: 8rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+.image-grid,
|
|
|
|
|
+.file-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.image-item,
|
|
|
|
|
+.file-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.image-container {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 220rpx;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+}
|
|
|
|
|
+.attachment-image {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-text,
|
|
|
|
|
+.error-text {
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.success-badge {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 8rpx;
|
|
|
|
|
+ bottom: 8rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ padding: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.image-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ padding: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.image-name {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ padding-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-actions {
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.upload-tips {
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.tip-item {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.wechat-tip {
|
|
|
|
|
+ color: #2979ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 流程选择弹窗 */
|
|
|
|
|
+.flow-selection-popup {
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.popup-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+.popup-title {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+.flow-options {
|
|
|
|
|
+ margin-top: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.flow-option-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 20rpx;
|
|
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.flow-option-item.selected {
|
|
|
|
|
+ border-color: #2979ff;
|
|
|
|
|
+ background: #f5faff;
|
|
|
|
|
+}
|
|
|
|
|
+.popup-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* === 以下样式从请假页复制,确保附件卡片区域完全一致 === */
|
|
|
|
|
+.upload-area {
|
|
|
|
|
+ padding: 20rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.doc-upload-section {
|
|
|
|
|
+ margin-top: 20rpx;
|
|
|
|
|
+ padding: 20rpx 0;
|
|
|
|
|
+ border-top: 2rpx dashed #e0e0e0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.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-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 6rpx;
|
|
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ border-top: 1rpx solid #f0f0f0;
|
|
|
|
|
+ flex-wrap: nowrap;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-actions .u-button {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 60rpx;
|
|
|
|
|
+ font-size: 24rpx !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-name-display {
|
|
|
|
|
+ padding: 8rpx 12rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.error-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background-color: rgba(250, 53, 52, 0.8);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-text,
|
|
|
|
|
+.error-text {
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.success-badge {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 8rpx;
|
|
|
|
|
+ right: 8rpx;
|
|
|
|
|
+ bottom: auto;
|
|
|
|
|
+ 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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|