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