zlwu 1 год назад
Родитель
Сommit
4f5f4eedf7
2 измененных файлов с 2139 добавлено и 0 удалено
  1. 8 0
      mobile/src/pages.json
  2. 2131 0
      mobile/src/pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue

+ 8 - 0
mobile/src/pages.json

@@ -105,6 +105,14 @@
 				"navigationBarTextStyle": "white"
 			}
 		},
+		{
+			"path": "pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index",
+			"style": {
+				"navigationBarTitleText": "学生销假",
+				"navigationBarBackgroundColor": "#0051d9",
+				"navigationBarTextStyle": "white"
+			}
+		},
 		{
 			"path": "pages/wechat/js-sdk-test",
 			"style": {

+ 2131 - 0
mobile/src/pages/xg/xsjcxx/xsqj/cancelLeaveEdit/index.vue

@@ -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>