|
@@ -0,0 +1,1283 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-dialog v-model="dialogVisible" :title="title" width="1200px" top="5vh" append-to-body destroy-on-close class="audit-dialog">
|
|
|
|
|
+ <div class="audit-container">
|
|
|
|
|
+ <!-- 左侧:文档编辑区域 -->
|
|
|
|
|
+ <div class="preview-section">
|
|
|
|
|
+ <div class="preview-header">
|
|
|
|
|
+ <div class="file-info">
|
|
|
|
|
+ <el-icon class="file-icon"><Document /></el-icon>
|
|
|
|
|
+ <span class="file-name">{{ document?.fileName || t('document.document.documentAudit.untitledDocument') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="header-actions">
|
|
|
|
|
+ <el-tooltip :content="t('document.document.documentAudit.viewVersionsTooltip')" placement="bottom">
|
|
|
|
|
+ <el-button size="small" @click="handleViewVersions" :loading="loadingVersions">
|
|
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
|
|
+ {{ t('document.document.documentAudit.viewVersions') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ <el-tooltip :content="t('document.document.documentAudit.cleanCommentsTooltip')" placement="bottom">
|
|
|
|
|
+ <el-button size="small" @click="handleCleanComments" :loading="cleaningComments">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
|
|
+ {{ t('document.document.documentAudit.cleanComments') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ <el-tooltip :content="t('document.document.documentAudit.copySignatureTooltip')" placement="bottom">
|
|
|
|
|
+ <el-button type="primary" size="small" @click="handleCopyAvatar" class="copy-avatar-btn" :loading="copyingAvatar">
|
|
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
|
|
+ {{ t('document.document.documentAudit.copySignature') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="preview-container">
|
|
|
|
|
+ <!-- WPS 编辑器容器 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="document?.ossId"
|
|
|
|
|
+ ref="wpsContainerRef"
|
|
|
|
|
+ class="wps-container"
|
|
|
|
|
+ @dragenter="handleDragEnter"
|
|
|
|
|
+ @dragleave="handleDragLeave"
|
|
|
|
|
+ @dragover.prevent="handleDragOver"
|
|
|
|
|
+ @drop.prevent="handleDrop"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 拖拽提示层 -->
|
|
|
|
|
+ <div v-if="isDragging" class="drag-overlay">
|
|
|
|
|
+ <div class="drag-hint">
|
|
|
|
|
+ <el-icon class="drag-icon"><Upload /></el-icon>
|
|
|
|
|
+ <p>{{ t('document.document.documentAudit.dropImageHint') }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 降级方案:iframe 预览 -->
|
|
|
|
|
+ <iframe v-if="wpsError && document?.url" :src="document.url" class="document-iframe" frameborder="0"></iframe>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <div v-if="wpsLoading" class="loading-state">
|
|
|
|
|
+ <el-icon class="is-loading"><Loading /></el-icon>
|
|
|
|
|
+ <p>{{ t('document.document.documentAudit.loadingEditor') }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 错误状态 -->
|
|
|
|
|
+ <div v-if="wpsError && !document?.url" class="error-state">
|
|
|
|
|
+ <el-result icon="error" :title="t('document.document.documentAudit.loadFailed')" :sub-title="wpsError">
|
|
|
|
|
+ <template #extra>
|
|
|
|
|
+ <el-button type="primary" @click="() => initWpsEditor(false)">{{ t('document.document.documentAudit.reload') }}</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-result>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 空状态 -->
|
|
|
|
|
+ <el-empty v-if="!document?.ossId" :description="t('document.document.documentAudit.noDocument')" :image-size="100" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧:审核表单区域 -->
|
|
|
|
|
+ <div class="audit-section">
|
|
|
|
|
+ <div class="section-header">
|
|
|
|
|
+ <el-icon><Edit /></el-icon>
|
|
|
|
|
+ <span>{{ t('document.document.documentAudit.auditInfo') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-position="top" class="audit-form">
|
|
|
|
|
+ <!-- 文档信息卡片 -->
|
|
|
|
|
+ <el-card shadow="never" class="info-card">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="card-header">
|
|
|
|
|
+ <el-icon><InfoFilled /></el-icon>
|
|
|
|
|
+ <span>{{ t('document.document.documentAudit.documentInfo') }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div class="info-item">
|
|
|
|
|
+ <span class="info-label">{{ t('document.document.documentAudit.documentName') }}:</span>
|
|
|
|
|
+ <span class="info-value">{{ document?.fileName || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-item">
|
|
|
|
|
+ <span class="info-label">{{ t('document.document.documentAudit.documentId') }}:</span>
|
|
|
|
|
+ <span class="info-value">{{ document?.id || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 审核结果 -->
|
|
|
|
|
+ <el-form-item :label="t('document.document.auditForm.result')" prop="result">
|
|
|
|
|
+ <el-radio-group v-model="auditForm.result" class="result-radio-group">
|
|
|
|
|
+ <el-radio label="3" border>
|
|
|
|
|
+ <div class="radio-content">
|
|
|
|
|
+ <el-icon class="radio-icon success"><CircleCheck /></el-icon>
|
|
|
|
|
+ <div class="radio-text">
|
|
|
|
|
+ <div class="radio-title">{{ t('document.document.auditForm.pass') }}</div>
|
|
|
|
|
+ <div class="radio-desc">{{ t('document.document.documentAudit.passDesc') }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-radio>
|
|
|
|
|
+ <el-radio label="2" border>
|
|
|
|
|
+ <div class="radio-content">
|
|
|
|
|
+ <el-icon class="radio-icon danger"><CircleClose /></el-icon>
|
|
|
|
|
+ <div class="radio-text">
|
|
|
|
|
+ <div class="radio-title">{{ t('document.document.auditForm.reject') }}</div>
|
|
|
|
|
+ <div class="radio-desc">{{ t('document.document.documentAudit.rejectDesc') }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 驳回原因 -->
|
|
|
|
|
+ <el-form-item v-if="auditForm.result === '2'" :label="t('document.document.auditForm.reason')" prop="reason">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="auditForm.reason"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="6"
|
|
|
|
|
+ :placeholder="t('document.document.auditForm.reasonPlaceholder')"
|
|
|
|
|
+ maxlength="500"
|
|
|
|
|
+ show-word-limit
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 审核提示 -->
|
|
|
|
|
+ <el-alert v-if="auditForm.result === '3'" :title="t('document.document.documentAudit.passAlert')" type="success" :closable="false" show-icon />
|
|
|
|
|
+ <el-alert v-if="auditForm.result === '2'" :title="t('document.document.documentAudit.rejectAlert')" type="warning" :closable="false" show-icon />
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="dialog-footer">
|
|
|
|
|
+ <el-button @click="handleCancel" size="large">
|
|
|
|
|
+ <el-icon><Close /></el-icon>
|
|
|
|
|
+ {{ t('document.document.button.cancel') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button type="primary" @click="submitForm" :loading="loading" size="large">
|
|
|
|
|
+ <el-icon><Check /></el-icon>
|
|
|
|
|
+ {{ t('document.document.button.submit') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 历史版本对话框 -->
|
|
|
|
|
+ <el-dialog v-model="showVersionDialog" :title="t('document.document.documentAudit.historyVersions')" width="800px" append-to-body destroy-on-close">
|
|
|
|
|
+ <el-table :data="versionList" v-loading="loadingVersions" stripe>
|
|
|
|
|
+ <el-table-column prop="version" :label="t('document.document.documentAudit.versionNumber')" width="150" align="center" />
|
|
|
|
|
+ <el-table-column prop="createTime" :label="t('document.document.documentAudit.createTime')" min-width="180" align="center" />
|
|
|
|
|
+ <el-table-column prop="updateTime" :label="t('document.document.documentAudit.updateTime')" min-width="180" align="center" />
|
|
|
|
|
+ <el-table-column :label="t('document.document.documentAudit.action')" width="120" align="center" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button type="primary" size="small" @click="handleSelectVersion(row.version)"> {{ t('document.document.documentAudit.select') }} </el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="showVersionDialog = false">{{ t('document.document.button.cancel') }}</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue';
|
|
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
|
|
+import type { FormInstance } from 'element-plus';
|
|
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
+import { Document, Edit, InfoFilled, CircleCheck, CircleClose, Close, Check, Loading, Upload, Picture, Delete, Clock } from '@element-plus/icons-vue';
|
|
|
|
|
+import { useUserStore } from '@/store/modules/user';
|
|
|
|
|
+import { cleanDocumentComments, getFileVersionList, getFinalFile, initWpsDocument, cancelWpsDocument, type FileVersion } from '@/api/wps/save';
|
|
|
|
|
+
|
|
|
|
|
+interface Document {
|
|
|
|
|
+ id: number | string;
|
|
|
|
|
+ name?: string;
|
|
|
|
|
+ ossId?: number | string;
|
|
|
|
|
+ fileName?: string;
|
|
|
|
|
+ url?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface AuditData {
|
|
|
|
|
+ documentId: number | string;
|
|
|
|
|
+ result: number;
|
|
|
|
|
+ rejectReason?: string;
|
|
|
|
|
+ ossId?: number | string; // 最终的 ossId
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Props {
|
|
|
|
|
+ modelValue: boolean;
|
|
|
|
|
+ document?: Document | null;
|
|
|
|
|
+ title?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Emits {
|
|
|
|
|
+ (e: 'update:modelValue', value: boolean): void;
|
|
|
|
|
+ (e: 'submit', data: AuditData): void; // 提交审核数据
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps<Props>();
|
|
|
|
|
+const emit = defineEmits<Emits>();
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+const userStore = useUserStore();
|
|
|
|
|
+
|
|
|
|
|
+const dialogVisible = ref(false);
|
|
|
|
|
+const loading = ref(false);
|
|
|
|
|
+const auditFormRef = ref<FormInstance>();
|
|
|
|
|
+
|
|
|
|
|
+// WPS 编辑器相关
|
|
|
|
|
+const wpsContainerRef = ref<HTMLDivElement>();
|
|
|
|
|
+const wpsLoading = ref(false);
|
|
|
|
|
+const wpsError = ref('');
|
|
|
|
|
+const isDragging = ref(false);
|
|
|
|
|
+const copyingAvatar = ref(false);
|
|
|
|
|
+const cleaningComments = ref(false);
|
|
|
|
|
+const currentVersion = ref(1); // 当前文档版本号
|
|
|
|
|
+const showVersionDialog = ref(false); // 显示历史版本对话框
|
|
|
|
|
+const versionList = ref<FileVersion[]>([]); // 历史版本列表
|
|
|
|
|
+const loadingVersions = ref(false); // 加载历史版本中
|
|
|
|
|
+let wpsInstance: any = null;
|
|
|
|
|
+let dragCounter = 0; // 用于跟踪拖拽进入/离开次数
|
|
|
|
|
+
|
|
|
|
|
+// WPS 配置
|
|
|
|
|
+const WPS_APP_ID = 'SX20260105YMMIXV';
|
|
|
|
|
+
|
|
|
|
|
+// 审核表单数据
|
|
|
|
|
+const auditForm = ref({
|
|
|
|
|
+ id: 0 as number | string,
|
|
|
|
|
+ result: '3',
|
|
|
|
|
+ reason: ''
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 审核表单验证规则
|
|
|
|
|
+const auditRules = reactive({
|
|
|
|
|
+ result: [
|
|
|
|
|
+ {
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ message: t('document.document.auditRule.resultRequired'),
|
|
|
|
|
+ trigger: 'change'
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ reason: [
|
|
|
|
|
+ {
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ message: t('document.document.auditRule.reasonRequired'),
|
|
|
|
|
+ trigger: 'blur'
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 获取文件类型
|
|
|
|
|
+const getFileType = (fileName: string) => {
|
|
|
|
|
+ const name = fileName.toLowerCase();
|
|
|
|
|
+ if (name.endsWith('.docx') || name.endsWith('.doc')) return 'w';
|
|
|
|
|
+ if (name.endsWith('.xlsx') || name.endsWith('.xls')) return 's';
|
|
|
|
|
+ if (name.endsWith('.pptx') || name.endsWith('.ppt')) return 'p';
|
|
|
|
|
+ if (name.endsWith('.pdf')) return 'f';
|
|
|
|
|
+ return 'w';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 初始化 WPS 编辑器
|
|
|
|
|
+// shouldCallInitApi: 是否需要调用后端初始化接口(只在 dialog 首次打开时为 true)
|
|
|
|
|
+const initWpsEditor = async (shouldCallInitApi = false) => {
|
|
|
|
|
+ if (!wpsContainerRef.value || !props.document?.ossId) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ wpsLoading.value = true;
|
|
|
|
|
+ wpsError.value = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 检查 WPS SDK
|
|
|
|
|
+ if (!(window as any).WebOfficeSDK) {
|
|
|
|
|
+ wpsError.value = 'WPS SDK 未加载';
|
|
|
|
|
+ wpsLoading.value = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const WebOfficeSDK = (window as any).WebOfficeSDK;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取文件类型
|
|
|
|
|
+ const officeType = getFileType(props.document.fileName || '', props.document.url);
|
|
|
|
|
+
|
|
|
|
|
+ // 只在 dialog 首次打开时调用后端接口初始化文档
|
|
|
|
|
+ if (shouldCallInitApi) {
|
|
|
|
|
+ console.log('[WPS] 调用后端初始化接口,ossId:', props.document.ossId);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const initRes = await initWpsDocument(props.document.ossId);
|
|
|
|
|
+ const backendVersion = initRes.data; // data 直接是版本号
|
|
|
|
|
+ currentVersion.value = backendVersion;
|
|
|
|
|
+ console.log('[WPS] 后端返回版本号:', backendVersion);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[WPS] 调用后端初始化接口失败:', err);
|
|
|
|
|
+ wpsLoading.value = false;
|
|
|
|
|
+ wpsError.value = '初始化文档失败';
|
|
|
|
|
+
|
|
|
|
|
+ // 显示错误提示
|
|
|
|
|
+ ElMessage.error('初始化文档失败: ' + (err.message || '未知错误'));
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭对话框
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ dialogVisible.value = false;
|
|
|
|
|
+ }, 1500);
|
|
|
|
|
+
|
|
|
|
|
+ return; // 终止初始化流程
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('[WPS] 使用当前版本号重新初始化编辑器,版本:', currentVersion.value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 ossId + 当前版本号组成 fileId
|
|
|
|
|
+ const fileId = `${props.document.ossId}_${currentVersion.value}`;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[WPS] 初始化配置:', {
|
|
|
|
|
+ appId: WPS_APP_ID,
|
|
|
|
|
+ officeType: officeType,
|
|
|
|
|
+ fileId: fileId,
|
|
|
|
|
+ ossId: props.document.ossId,
|
|
|
|
|
+ version: currentVersion.value,
|
|
|
|
|
+ fileName: props.document.fileName,
|
|
|
|
|
+ fileUrl: props.document.url
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 标准初始化配置(按官方文档)
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ // 必需参数
|
|
|
|
|
+ appId: WPS_APP_ID,
|
|
|
|
|
+ officeType: officeType,
|
|
|
|
|
+ fileId: fileId,
|
|
|
|
|
+
|
|
|
|
|
+ // 可选参数
|
|
|
|
|
+ mount: wpsContainerRef.value,
|
|
|
|
|
+ // 指定当前用户ID为编辑者ID
|
|
|
|
|
+ userId: String(userStore.userId)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化 WPS 编辑器
|
|
|
|
|
+ wpsInstance = WebOfficeSDK.init(config);
|
|
|
|
|
+
|
|
|
|
|
+ wpsLoading.value = false;
|
|
|
|
|
+ console.log('[WPS] 编辑器初始化成功');
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[WPS] 初始化失败:', err);
|
|
|
|
|
+ wpsError.value = err.message || '初始化失败';
|
|
|
|
|
+ wpsLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 销毁 WPS 编辑器
|
|
|
|
|
+const destroyWpsEditor = () => {
|
|
|
|
|
+ if (wpsInstance) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (wpsInstance.destroy) {
|
|
|
|
|
+ wpsInstance.destroy();
|
|
|
|
|
+ }
|
|
|
|
|
+ wpsInstance = null;
|
|
|
|
|
+ console.log('[WPS] 编辑器已销毁');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[WPS] 销毁编辑器失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 保存文档
|
|
|
|
|
+const saveWpsDocument = async () => {
|
|
|
|
|
+ if (!wpsInstance) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[WPS] 开始保存文档');
|
|
|
|
|
+ const result = await wpsInstance.save();
|
|
|
|
|
+ console.log('[WPS] 保存成功:', result);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[WPS] 保存失败:', err);
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 拖拽进入
|
|
|
|
|
+const handleDragEnter = (e: DragEvent) => {
|
|
|
|
|
+ dragCounter++;
|
|
|
|
|
+ if (dragCounter === 1) {
|
|
|
|
|
+ isDragging.value = true;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 拖拽离开
|
|
|
|
|
+const handleDragLeave = (e: DragEvent) => {
|
|
|
|
|
+ dragCounter--;
|
|
|
|
|
+ if (dragCounter === 0) {
|
|
|
|
|
+ isDragging.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 拖拽悬停
|
|
|
|
|
+const handleDragOver = (e: DragEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 处理拖放
|
|
|
|
|
+const handleDrop = async (e: DragEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+
|
|
|
|
|
+ isDragging.value = false;
|
|
|
|
|
+ dragCounter = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (!wpsInstance) {
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.wpsNotInitialized'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理图片拖放
|
|
|
|
|
+ const files = e.dataTransfer?.files;
|
|
|
|
|
+ if (!files || files.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 只处理第一个文件
|
|
|
|
|
+ const file = files[0];
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否是图片
|
|
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.onlyImageSupported'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[WPS] 开始插入图片:', file.name);
|
|
|
|
|
+
|
|
|
|
|
+ // 读取图片为 base64
|
|
|
|
|
+ const reader = new FileReader();
|
|
|
|
|
+ reader.onload = async (event) => {
|
|
|
|
|
+ const base64 = event.target?.result as string;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取 WPS Application 对象
|
|
|
|
|
+ const app = await wpsInstance.Application;
|
|
|
|
|
+
|
|
|
|
|
+ if (!app) {
|
|
|
|
|
+ ElMessage.error('无法获取 WPS Application 对象');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据文件类型插入图片
|
|
|
|
|
+ const officeType = getFileType(props.document?.fileName || '');
|
|
|
|
|
+
|
|
|
|
|
+ if (officeType === 'w') {
|
|
|
|
|
+ // Word 文档:插入图片到光标位置
|
|
|
|
|
+ const selection = await app.ActiveDocument.Application.Selection;
|
|
|
|
|
+ await selection.InlineShapes.AddPicture(base64);
|
|
|
|
|
+ ElMessage.success(t('document.document.documentAudit.imageInserted'));
|
|
|
|
|
+ } else if (officeType === 's') {
|
|
|
|
|
+ // Excel 表格:插入图片到当前单元格
|
|
|
|
|
+ const activeSheet = await app.ActiveSheet;
|
|
|
|
|
+ const activeCell = await app.ActiveCell;
|
|
|
|
|
+ const row = await activeCell.Row;
|
|
|
|
|
+ const col = await activeCell.Column;
|
|
|
|
|
+ await activeSheet.Shapes.AddPicture(base64, false, true, col * 64, row * 20, 200, 150);
|
|
|
|
|
+ ElMessage.success(t('document.document.documentAudit.imageInserted'));
|
|
|
|
|
+ } else if (officeType === 'p') {
|
|
|
|
|
+ // PowerPoint:插入图片到当前幻灯片
|
|
|
|
|
+ const activeSlide = await app.ActivePresentation.Slides.Item(await app.ActiveWindow.Selection.SlideRange.SlideIndex);
|
|
|
|
|
+ await activeSlide.Shapes.AddPicture(base64, false, true, 100, 100, 200, 150);
|
|
|
|
|
+ ElMessage.success(t('document.document.documentAudit.imageInserted'));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.currentTypeNotSupported'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[WPS] 图片插入成功');
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[WPS] 插入图片失败:', err);
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.insertImageFailed') + ': ' + err.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ reader.onerror = () => {
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.readImageFailed'));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ reader.readAsDataURL(file);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[WPS] 处理拖放失败:', err);
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.dropHandleFailed'));
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 复制头像到剪贴板
|
|
|
|
|
+const handleCopyAvatar = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 先让用户选择审核结果
|
|
|
|
|
+ let reviewResult: string;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(t('document.document.copySignature.selectResultMessage'), t('document.document.copySignature.selectResult'), {
|
|
|
|
|
+ confirmButtonText: t('document.document.copySignature.pass'),
|
|
|
|
|
+ cancelButtonText: t('document.document.copySignature.reject'),
|
|
|
|
|
+ distinguishCancelAndClose: true,
|
|
|
|
|
+ closeOnClickModal: false,
|
|
|
|
|
+ closeOnPressEscape: false,
|
|
|
|
|
+ type: 'info'
|
|
|
|
|
+ });
|
|
|
|
|
+ // 点击确认按钮 = 通过
|
|
|
|
|
+ reviewResult = 'pass';
|
|
|
|
|
+ } catch (action) {
|
|
|
|
|
+ if (action === 'cancel') {
|
|
|
|
|
+ // 点击取消按钮 = 驳回
|
|
|
|
|
+ reviewResult = 'reject';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 点击关闭或按 ESC = 取消操作
|
|
|
|
|
+ console.log('[签名] 用户取消选择');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ copyingAvatar.value = true;
|
|
|
|
|
+ console.log('[签名] 开始生成审核信息图片,审核结果:', reviewResult);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前用户昵称
|
|
|
|
|
+ const reviewerName = userStore.nickname || userStore.name || t('document.document.copySignature.unknown');
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前时间
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const year = now.getFullYear();
|
|
|
|
|
+ const month = now.getMonth() + 1;
|
|
|
|
|
+ const day = now.getDate();
|
|
|
|
|
+ const hour = now.getHours();
|
|
|
|
|
+ const minute = now.getMinutes();
|
|
|
|
|
+ const reviewTime = t('document.document.copySignature.timeFormat', { year, month, day, hour, minute });
|
|
|
|
|
+
|
|
|
|
|
+ // 审核结果文本
|
|
|
|
|
+ const resultText = reviewResult === 'pass' ? t('document.document.copySignature.passText') : t('document.document.copySignature.rejectText');
|
|
|
|
|
+
|
|
|
|
|
+ // 根据审核结果确定颜色
|
|
|
|
|
+ const color = reviewResult === 'pass' ? '#00aa00' : '#ff0000';
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[签名] 审核结果:', reviewResult, '颜色:', color, '结果文本:', resultText);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建 canvas
|
|
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
|
|
+
|
|
|
|
|
+ if (!ctx) {
|
|
|
|
|
+ ElMessage.error(t('document.document.copySignature.canvasNotSupported'));
|
|
|
|
|
+ copyingAvatar.value = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置 canvas 尺寸
|
|
|
|
|
+ const width = 300;
|
|
|
|
|
+ const height = 100;
|
|
|
|
|
+ canvas.width = width;
|
|
|
|
|
+ canvas.height = height;
|
|
|
|
|
+
|
|
|
|
|
+ // 不填充背景,保持透明
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制边框(5px 粗,根据审核结果变色)
|
|
|
|
|
+ ctx.strokeStyle = color;
|
|
|
|
|
+ ctx.lineWidth = 5;
|
|
|
|
|
+ ctx.strokeRect(2.5, 2.5, width - 5, height - 5);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置字体样式(根据审核结果变色)
|
|
|
|
|
+ ctx.fillStyle = color;
|
|
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
|
|
+
|
|
|
|
|
+ // 第一行:审核人(左边)和审核结果(右边)
|
|
|
|
|
+ ctx.font = 'bold 18px Arial, "Microsoft YaHei", sans-serif';
|
|
|
|
|
+ const reviewerText = t('document.document.copySignature.reviewer', { name: reviewerName });
|
|
|
|
|
+ ctx.fillText(reviewerText, 20, height / 3);
|
|
|
|
|
+
|
|
|
|
|
+ // 审核结果靠右显示
|
|
|
|
|
+ const resultWidth = ctx.measureText(resultText).width;
|
|
|
|
|
+ ctx.fillText(resultText, width - resultWidth - 20, height / 3);
|
|
|
|
|
+
|
|
|
|
|
+ // 第二行:审核时间(居左对齐,加粗)
|
|
|
|
|
+ ctx.font = 'bold 16px Arial, "Microsoft YaHei", sans-serif';
|
|
|
|
|
+ const line2 = t('document.document.copySignature.reviewTime', { time: reviewTime });
|
|
|
|
|
+ ctx.fillText(line2, 20, (height * 2) / 3);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[签名] Canvas 绘制完成');
|
|
|
|
|
+
|
|
|
|
|
+ // 将 canvas 转换为 Blob
|
|
|
|
|
+ canvas.toBlob(async (blob) => {
|
|
|
|
|
+ if (!blob) {
|
|
|
|
|
+ ElMessage.error(t('document.document.copySignature.generateFailed'));
|
|
|
|
|
+ copyingAvatar.value = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[签名] 图片生成成功,大小:', blob.size);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 复制到剪贴板
|
|
|
|
|
+ await navigator.clipboard.write([
|
|
|
|
|
+ new ClipboardItem({
|
|
|
|
|
+ 'image/png': blob
|
|
|
|
|
+ })
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage({
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: t('document.document.copySignature.copySuccess'),
|
|
|
|
|
+ duration: 3000
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 显示使用提示
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ ElMessage({
|
|
|
|
|
+ type: 'info',
|
|
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
|
|
+ message: t('document.document.copySignature.usageHint'),
|
|
|
|
|
+ duration: 6000,
|
|
|
|
|
+ showClose: true
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[签名] 复制成功');
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[签名] 复制失败:', err);
|
|
|
|
|
+
|
|
|
|
|
+ // 根据错误类型显示不同提示
|
|
|
|
|
+ if (err.message?.includes('clipboard') || err.message?.includes('Clipboard')) {
|
|
|
|
|
+ ElMessage({
|
|
|
|
|
+ type: 'warning',
|
|
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
|
|
+ message: t('document.document.copySignature.browserNotSupported'),
|
|
|
|
|
+ duration: 6000,
|
|
|
|
|
+ showClose: true
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage({
|
|
|
|
|
+ type: 'error',
|
|
|
|
|
+ message: t('document.document.copySignature.copyFailed', { error: err.message || t('document.document.copySignature.unknownError') }),
|
|
|
|
|
+ duration: 5000
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ copyingAvatar.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 'image/png');
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[签名] 生成图片失败:', err);
|
|
|
|
|
+ ElMessage.error(t('document.document.copySignature.generateFailed') + ': ' + (err.message || t('document.document.copySignature.unknownError')));
|
|
|
|
|
+ copyingAvatar.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 清空批注
|
|
|
|
|
+const handleCleanComments = async () => {
|
|
|
|
|
+ if (!props.document?.id || !props.document?.ossId) {
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.documentInfoIncomplete'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(t('document.document.documentAudit.cleanCommentsConfirm'), t('document.document.documentAudit.cleanCommentsTitle'), {
|
|
|
|
|
+ confirmButtonText: t('document.document.message.confirmButton'),
|
|
|
|
|
+ cancelButtonText: t('document.document.message.cancelButton'),
|
|
|
|
|
+ type: 'warning'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ cleaningComments.value = true;
|
|
|
|
|
+ console.log('[清空批注] 开始清空文档批注,文档ID:', props.document.id, 'ossId:', props.document.ossId, '当前版本:', currentVersion.value);
|
|
|
|
|
+
|
|
|
|
|
+ // 调用后端接口清空批注,获取新版本号
|
|
|
|
|
+ const res = await cleanDocumentComments(props.document.ossId);
|
|
|
|
|
+ const newVersion = res.data; // 后端返回的新版本号
|
|
|
|
|
+ currentVersion.value = newVersion;
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success(t('document.document.documentAudit.cleanCommentsSuccess'));
|
|
|
|
|
+ console.log('[清空批注] 批注清空成功,新版本号:', newVersion);
|
|
|
|
|
+
|
|
|
|
|
+ // 销毁当前 WPS 编辑器
|
|
|
|
|
+ destroyWpsEditor();
|
|
|
|
|
+
|
|
|
|
|
+ // 等待 DOM 更新
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+
|
|
|
|
|
+ // 使用新版本号重新初始化 WPS 编辑器(不调用后端初始化接口)
|
|
|
|
|
+ await initWpsEditor(false);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[清空批注] WPS 编辑器已使用新版本重新初始化,fileId:', `${props.document.ossId}_${currentVersion.value}`);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ if (err === 'cancel') {
|
|
|
|
|
+ console.log('[清空批注] 用户取消操作');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.error('[清空批注] 清空失败:', err);
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.cleanCommentsFailed') + ': ' + (err.message || t('document.document.copySignature.unknownError')));
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ cleaningComments.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 查看历史版本
|
|
|
|
|
+const handleViewVersions = async () => {
|
|
|
|
|
+ if (!props.document?.ossId) {
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.documentInfoIncomplete'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ loadingVersions.value = true;
|
|
|
|
|
+ showVersionDialog.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[历史版本] 获取历史版本列表,ossId:', props.document.ossId);
|
|
|
|
|
+
|
|
|
|
|
+ const res = await getFileVersionList(props.document.ossId);
|
|
|
|
|
+ versionList.value = res.data || [];
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[历史版本] 获取成功,版本数量:', versionList.value.length);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[历史版本] 获取失败:', err);
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.getVersionsFailed') + ': ' + (err.message || t('document.document.copySignature.unknownError')));
|
|
|
|
|
+ showVersionDialog.value = false;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadingVersions.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 选择历史版本
|
|
|
|
|
+const handleSelectVersion = async (version: number) => {
|
|
|
|
|
+ if (!props.document?.ossId) {
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.documentInfoIncomplete'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[历史版本] 选择版本:', version, 'ossId:', props.document.ossId);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新当前版本号
|
|
|
|
|
+ currentVersion.value = version;
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭历史版本对话框
|
|
|
|
|
+ showVersionDialog.value = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 销毁当前 WPS 编辑器
|
|
|
|
|
+ destroyWpsEditor();
|
|
|
|
|
+
|
|
|
|
|
+ // 等待 DOM 更新
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+
|
|
|
|
|
+ // 使用选择的版本号重新初始化 WPS 编辑器(不调用后端初始化接口)
|
|
|
|
|
+ await initWpsEditor(false);
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success(t('document.document.documentAudit.switchVersionSuccess', { version }));
|
|
|
|
|
+ console.log('[历史版本] WPS 编辑器已切换到版本:', version, 'fileId:', `${props.document.ossId}_${version}`);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[历史版本] 切换版本失败:', err);
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.switchVersionFailed') + ': ' + (err.message || t('document.document.copySignature.unknownError')));
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 监听 modelValue 变化
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.modelValue,
|
|
|
|
|
+ (val) => {
|
|
|
|
|
+ dialogVisible.value = val;
|
|
|
|
|
+ if (val && props.document) {
|
|
|
|
|
+ // 重置版本号为 1
|
|
|
|
|
+ currentVersion.value = 1;
|
|
|
|
|
+
|
|
|
|
|
+ auditForm.value = {
|
|
|
|
|
+ id: props.document.id,
|
|
|
|
|
+ result: '3',
|
|
|
|
|
+ reason: ''
|
|
|
|
|
+ };
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ auditFormRef.value?.clearValidate();
|
|
|
|
|
+ // 自动初始化 WPS 编辑器,使用版本号 1(调用后端初始化接口)
|
|
|
|
|
+ if (props.document?.ossId) {
|
|
|
|
|
+ console.log('[WPS] 对话框打开,初始化编辑器,版本号:', currentVersion.value);
|
|
|
|
|
+ initWpsEditor(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// 监听dialogVisible变化
|
|
|
|
|
+watch(dialogVisible, async (val) => {
|
|
|
|
|
+ emit('update:modelValue', val);
|
|
|
|
|
+ if (!val) {
|
|
|
|
|
+ // 对话框关闭时,调用取消接口
|
|
|
|
|
+ if (props.document?.ossId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[WPS] 对话框关闭,调用取消接口,ossId:', props.document.ossId);
|
|
|
|
|
+ await cancelWpsDocument(props.document.ossId);
|
|
|
|
|
+ console.log('[WPS] 取消接口调用成功');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[WPS] 取消接口调用失败:', err);
|
|
|
|
|
+ // 取消接口失败不影响关闭流程
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auditForm.value = {
|
|
|
|
|
+ id: 0,
|
|
|
|
|
+ result: '3',
|
|
|
|
|
+ reason: ''
|
|
|
|
|
+ };
|
|
|
|
|
+ // 关闭对话框时重置版本号
|
|
|
|
|
+ currentVersion.value = 1;
|
|
|
|
|
+ destroyWpsEditor();
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 取消操作
|
|
|
|
|
+const handleCancel = () => {
|
|
|
|
|
+ dialogVisible.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 提交表单
|
|
|
|
|
+const submitForm = () => {
|
|
|
|
|
+ auditFormRef.value?.validate(async (valid: boolean) => {
|
|
|
|
|
+ if (valid) {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 如果使用了 WPS 编辑器,先保存文档
|
|
|
|
|
+ let savedFileInfo = null;
|
|
|
|
|
+ if (wpsInstance) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ savedFileInfo = await saveWpsDocument();
|
|
|
|
|
+ if (savedFileInfo) {
|
|
|
|
|
+ console.log('[审核提交] 文档保存成功:', savedFileInfo);
|
|
|
|
|
+ ElMessage.success(t('document.document.documentAudit.documentSaved'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[审核提交] 保存文档失败:', err);
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.documentSaveFailed'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前 fileId
|
|
|
|
|
+ const currentFileId = `${props.document?.ossId}_${currentVersion.value}`;
|
|
|
|
|
+ console.log('[审核提交] 当前 fileId:', currentFileId);
|
|
|
|
|
+
|
|
|
|
|
+ // 调用接口获取最终文档信息
|
|
|
|
|
+ let finalOssId = props.document?.ossId;
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[审核提交] 获取最终文档信息...');
|
|
|
|
|
+ const finalFileRes = await getFinalFile(currentFileId);
|
|
|
|
|
+ finalOssId = finalFileRes.data.ossId;
|
|
|
|
|
+ console.log('[审核提交] 获取到最终 ossId:', finalOssId);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[审核提交] 获取最终文档信息失败:', err);
|
|
|
|
|
+ ElMessage.warning(t('document.document.documentAudit.getFinalFileFailed'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建审核数据
|
|
|
|
|
+ const auditData: AuditData = {
|
|
|
|
|
+ documentId: auditForm.value.id,
|
|
|
|
|
+ result: parseInt(auditForm.value.result),
|
|
|
|
|
+ rejectReason: auditForm.value.reason,
|
|
|
|
|
+ ossId: finalOssId // 使用最终的 ossId
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[审核提交] 提交审核数据到父组件:', auditData);
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭对话框
|
|
|
|
|
+ dialogVisible.value = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 通过 emit 将审核数据传递给父组件
|
|
|
|
|
+ emit('submit', auditData);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('[审核提交] 处理失败:', error);
|
|
|
|
|
+ ElMessage.error(t('document.document.documentAudit.processFailed') + ': ' + (error as any).message || t('document.document.copySignature.unknownError'));
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 组件卸载前清理
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ destroyWpsEditor();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.audit-dialog {
|
|
|
|
|
+ :deep(.el-dialog__header) {
|
|
|
|
|
+ padding: 20px 24px;
|
|
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .el-dialog__title {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-dialog__body) {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ height: calc(80vh - 140px);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-dialog__footer) {
|
|
|
|
|
+ padding: 16px 24px;
|
|
|
|
|
+ border-top: 1px solid #e4e7ed;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audit-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ gap: 1px;
|
|
|
|
|
+ background: #e4e7ed;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-section {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .file-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .file-icon {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .file-name {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ max-width: 600px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .header-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .copy-avatar-btn {
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ margin-right: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 68px;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ .wps-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .document-iframe {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .drag-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: rgba(64, 158, 255, 0.1);
|
|
|
|
|
+ border: 2px dashed #409eff;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ z-index: 9999;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+
|
|
|
|
|
+ .drag-hint {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+
|
|
|
|
|
+ .drag-icon {
|
|
|
|
|
+ font-size: 64px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ p {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .loading-state {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ p {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-result),
|
|
|
|
|
+ :deep(.el-empty) {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audit-section {
|
|
|
|
|
+ width: 400px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audit-form {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+
|
|
|
|
|
+ &::-webkit-scrollbar {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #dcdfe6;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: #c0c4cc;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-card {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-card__header) {
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-card__body) {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+
|
|
|
|
|
+ &:not(:last-child) {
|
|
|
|
|
+ border-bottom: 1px solid #f0f2f5;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-label {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ min-width: 80px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-value {
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.result-radio-group {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-radio) {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+
|
|
|
|
|
+ &.is-bordered {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border: 2px solid #dcdfe6;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.is-checked {
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-radio__input {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-radio__label {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.radio-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+
|
|
|
|
|
+ .radio-icon {
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ &.success {
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.danger {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .radio-text {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ .radio-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .radio-desc {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-form-item) {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-form-item__label {
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-alert) {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialog-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ min-width: 100px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ margin-right: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 响应式设计
|
|
|
|
|
+@media (max-width: 1400px) {
|
|
|
|
|
+ .audit-dialog {
|
|
|
|
|
+ :deep(.el-dialog) {
|
|
|
|
|
+ width: 95% !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .audit-section {
|
|
|
|
|
+ width: 350px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 1200px) {
|
|
|
|
|
+ .audit-container {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .preview-section {
|
|
|
|
|
+ height: 50%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .audit-section {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 50%;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|