|
|
@@ -1,157 +1,1083 @@
|
|
|
<template>
|
|
|
- <el-dialog v-model="dialogVisible" :title="title" width="500px" append-to-body>
|
|
|
- <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="120px">
|
|
|
- <el-form-item :label="t('document.document.auditForm.result')" prop="result">
|
|
|
- <el-radio-group v-model="auditForm.result">
|
|
|
- <el-radio label="3">{{ t('document.document.auditForm.pass') }}</el-radio>
|
|
|
- <el-radio label="2">{{ t('document.document.auditForm.reject') }}</el-radio>
|
|
|
- </el-radio-group>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item :label="t('document.document.auditForm.reason')" prop="reason"
|
|
|
- v-if="auditForm.result === '2'">
|
|
|
- <el-input v-model="auditForm.reason" type="textarea" :rows="3"
|
|
|
- :placeholder="t('document.document.auditForm.reasonPlaceholder')" />
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <div class="dialog-footer">
|
|
|
- <el-button :loading="loading" type="primary" @click="submitForm">{{
|
|
|
- t('document.document.button.submit') }}</el-button>
|
|
|
- <el-button @click="handleCancel">{{ t('document.document.button.cancel') }}</el-button>
|
|
|
+ <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 || '未命名文档' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="header-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ draggable="true"
|
|
|
+ @dragstart="handleUserNameDragStart"
|
|
|
+ @dragend="handleUserNameDragEnd"
|
|
|
+ class="drag-username-btn"
|
|
|
+ >
|
|
|
+ <el-icon><User /></el-icon>
|
|
|
+ 拖我到文档中
|
|
|
+ </el-button>
|
|
|
+ </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>松开鼠标插入图片</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
+
|
|
|
+ <!-- 降级方案: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>正在加载编辑器...</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 错误状态 -->
|
|
|
+ <div v-if="wpsError && !document?.url" class="error-state">
|
|
|
+ <el-result icon="error" title="加载失败" :sub-title="wpsError">
|
|
|
+ <template #extra>
|
|
|
+ <el-button type="primary" @click="initWpsEditor">重新加载</el-button>
|
|
|
+ </template>
|
|
|
+ </el-result>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <el-empty v-if="!document?.ossId" description="暂无文档" :image-size="100" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧:审核表单区域 -->
|
|
|
+ <div class="audit-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <el-icon><Edit /></el-icon>
|
|
|
+ <span>审核信息</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>文档信息</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">文档名称:</span>
|
|
|
+ <span class="info-value">{{ document?.fileName || '-' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">文档ID:</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">文档审核通过</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">文档需要修改</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="审核通过后,文档将进入下一流程" type="success" :closable="false" show-icon />
|
|
|
+ <el-alert v-if="auditForm.result === '2'" title="驳回后,文档将退回给提交人修改" 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>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, watch, nextTick } from 'vue';
|
|
|
+import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue';
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
import type { FormInstance } from 'element-plus';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
+import { Document, Edit, InfoFilled, CircleCheck, CircleClose, Close, Check, Loading, Upload, User } from '@element-plus/icons-vue';
|
|
|
+import { useUserStore } from '@/store/modules/user';
|
|
|
|
|
|
interface Document {
|
|
|
- id: number | string;
|
|
|
- name?: string;
|
|
|
+ id: number | string;
|
|
|
+ name?: string;
|
|
|
+ ossId?: number | string;
|
|
|
+ fileName?: string;
|
|
|
+ url?: string;
|
|
|
}
|
|
|
|
|
|
interface AuditData {
|
|
|
- documentId: number | string;
|
|
|
- result: number;
|
|
|
- rejectReason?: string;
|
|
|
+ documentId: number | string;
|
|
|
+ result: number;
|
|
|
+ rejectReason?: string;
|
|
|
}
|
|
|
|
|
|
interface Props {
|
|
|
- modelValue: boolean;
|
|
|
- document?: Document | null;
|
|
|
- title?: string;
|
|
|
- auditApi: (data: AuditData) => Promise<any>;
|
|
|
+ modelValue: boolean;
|
|
|
+ document?: Document | null;
|
|
|
+ title?: string;
|
|
|
+ auditApi: (data: AuditData) => Promise<any>;
|
|
|
}
|
|
|
|
|
|
interface Emits {
|
|
|
- (e: 'update:modelValue', value: boolean): void;
|
|
|
- (e: 'success'): void;
|
|
|
+ (e: 'update:modelValue', value: boolean): void;
|
|
|
+ (e: 'success'): 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 isDraggingUserName = ref(false);
|
|
|
+let wpsInstance: any = null;
|
|
|
+let dragCounter = 0; // 用于跟踪拖拽进入/离开次数
|
|
|
+
|
|
|
+// WPS 配置
|
|
|
+const WPS_APP_ID = 'SX20251229FLIAPD';
|
|
|
+
|
|
|
// 审核表单数据
|
|
|
const auditForm = ref({
|
|
|
- id: 0 as number | string,
|
|
|
- result: '3', // 默认通过
|
|
|
- reason: ''
|
|
|
+ id: 0 as number | string,
|
|
|
+ result: '3',
|
|
|
+ reason: ''
|
|
|
});
|
|
|
|
|
|
// 审核表单验证规则
|
|
|
const auditRules = reactive({
|
|
|
- result: [
|
|
|
- {
|
|
|
- required: true,
|
|
|
- message: t('document.document.auditRule.resultRequired'),
|
|
|
- trigger: 'change'
|
|
|
+ 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 编辑器
|
|
|
+const initWpsEditor = async () => {
|
|
|
+ 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 || '');
|
|
|
+
|
|
|
+ // 生成文件 ID
|
|
|
+ const fileId = `${props.document.id}`;
|
|
|
+
|
|
|
+ console.log('[WPS] 初始化配置:', {
|
|
|
+ appId: WPS_APP_ID,
|
|
|
+ officeType: officeType,
|
|
|
+ fileId: fileId,
|
|
|
+ fileName: props.document.fileName,
|
|
|
+ fileUrl: props.document.url
|
|
|
+ });
|
|
|
+
|
|
|
+ // 标准初始化配置(按官方文档)
|
|
|
+ const config = {
|
|
|
+ // 必需参数
|
|
|
+ appId: WPS_APP_ID,
|
|
|
+ officeType: officeType,
|
|
|
+ fileId: fileId,
|
|
|
+
|
|
|
+ // 可选参数
|
|
|
+ mount: wpsContainerRef.value
|
|
|
+ };
|
|
|
+
|
|
|
+ // 初始化 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('WPS 编辑器未初始化');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否是拖拽用户名
|
|
|
+ const userName = e.dataTransfer?.getData('text/plain');
|
|
|
+ if (isDraggingUserName.value && userName) {
|
|
|
+ await insertUserNameToWps(userName);
|
|
|
+ isDraggingUserName.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理图片拖放
|
|
|
+ const files = e.dataTransfer?.files;
|
|
|
+ if (!files || files.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只处理第一个文件
|
|
|
+ const file = files[0];
|
|
|
+
|
|
|
+ // 检查是否是图片
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
+ ElMessage.warning('只支持插入图片文件');
|
|
|
+ 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;
|
|
|
}
|
|
|
- ],
|
|
|
- reason: [
|
|
|
- {
|
|
|
- required: true,
|
|
|
- message: t('document.document.auditRule.reasonRequired'),
|
|
|
- trigger: 'blur'
|
|
|
+
|
|
|
+ // 根据文件类型插入图片
|
|
|
+ const officeType = getFileType(props.document?.fileName || '');
|
|
|
+
|
|
|
+ if (officeType === 'w') {
|
|
|
+ // Word 文档:插入图片到光标位置
|
|
|
+ const selection = await app.ActiveDocument.Application.Selection;
|
|
|
+ await selection.InlineShapes.AddPicture(base64);
|
|
|
+ ElMessage.success('图片已插入');
|
|
|
+ } 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('图片已插入');
|
|
|
+ } 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('图片已插入');
|
|
|
+ } else {
|
|
|
+ ElMessage.warning('当前文档类型不支持插入图片');
|
|
|
}
|
|
|
- ]
|
|
|
-});
|
|
|
+
|
|
|
+ console.log('[WPS] 图片插入成功');
|
|
|
+ } catch (err: any) {
|
|
|
+ console.error('[WPS] 插入图片失败:', err);
|
|
|
+ ElMessage.error('插入图片失败: ' + err.message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ reader.onerror = () => {
|
|
|
+ ElMessage.error('读取图片文件失败');
|
|
|
+ };
|
|
|
+
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+
|
|
|
+ } catch (err: any) {
|
|
|
+ console.error('[WPS] 处理拖放失败:', err);
|
|
|
+ ElMessage.error('处理拖放失败');
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
-// 监听modelValue变化
|
|
|
-watch(
|
|
|
- () => props.modelValue,
|
|
|
- (val) => {
|
|
|
- dialogVisible.value = val;
|
|
|
- if (val && props.document) {
|
|
|
- auditForm.value = {
|
|
|
- id: props.document.id,
|
|
|
- result: '3', // 默认通过
|
|
|
- reason: ''
|
|
|
- };
|
|
|
- // 重置表单验证
|
|
|
- nextTick(() => {
|
|
|
- auditFormRef.value?.clearValidate();
|
|
|
+// 开始拖拽用户名
|
|
|
+const handleUserNameDragStart = (e: DragEvent) => {
|
|
|
+ const userName = userStore.userName || '当前用户';
|
|
|
+ e.dataTransfer!.effectAllowed = 'copy';
|
|
|
+ e.dataTransfer!.setData('text/plain', userName);
|
|
|
+ isDraggingUserName.value = true;
|
|
|
+ console.log('[拖拽] 开始拖拽用户名:', userName);
|
|
|
+};
|
|
|
+
|
|
|
+// 结束拖拽用户名
|
|
|
+const handleUserNameDragEnd = (e: DragEvent) => {
|
|
|
+ isDraggingUserName.value = false;
|
|
|
+ console.log('[拖拽] 结束拖拽用户名');
|
|
|
+};
|
|
|
+
|
|
|
+// 插入用户名到 WPS
|
|
|
+const insertUserNameToWps = async (userName: string) => {
|
|
|
+ if (!wpsInstance) {
|
|
|
+ ElMessage.warning('WPS 编辑器未初始化');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('[WPS] 开始插入用户名:', userName);
|
|
|
+
|
|
|
+ // 获取 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.TypeText(userName);
|
|
|
+ ElMessage.success(`已插入:${userName}`);
|
|
|
+ } else if (officeType === 's') {
|
|
|
+ // Excel 表格:在当前单元格插入文本
|
|
|
+ const activeCell = await app.ActiveCell;
|
|
|
+ await activeCell.put_Value(userName);
|
|
|
+ ElMessage.success(`已插入:${userName}`);
|
|
|
+ } else if (officeType === 'p') {
|
|
|
+ // PowerPoint:在当前文本框插入文本
|
|
|
+ try {
|
|
|
+ const selection = await app.ActiveWindow.Selection;
|
|
|
+ const textRange = await selection.TextRange;
|
|
|
+ await textRange.InsertAfter(userName);
|
|
|
+ ElMessage.success(`已插入:${userName}`);
|
|
|
+ } catch (err) {
|
|
|
+ ElMessage.warning('请先选择一个文本框');
|
|
|
+ }
|
|
|
+ } else if (officeType === 'f') {
|
|
|
+ // PDF 文档:添加文本批注
|
|
|
+ try {
|
|
|
+ const pdfDoc = await app.ActivePDF;
|
|
|
+
|
|
|
+ // 方法 1:尝试添加文本批注
|
|
|
+ try {
|
|
|
+ // 获取当前页面
|
|
|
+ const currentPage = await pdfDoc.CurrentPage;
|
|
|
+
|
|
|
+ // 在页面中心位置添加文本批注
|
|
|
+ const pageWidth = await currentPage.Width;
|
|
|
+ const pageHeight = await currentPage.Height;
|
|
|
+
|
|
|
+ // 添加文本批注(自由文本批注)
|
|
|
+ const annotation = await currentPage.AddAnnotation({
|
|
|
+ type: 'FreeText', // 自由文本类型
|
|
|
+ rect: {
|
|
|
+ left: pageWidth / 2 - 100,
|
|
|
+ top: pageHeight / 2 - 20,
|
|
|
+ right: pageWidth / 2 + 100,
|
|
|
+ bottom: pageHeight / 2 + 20
|
|
|
+ },
|
|
|
+ contents: userName,
|
|
|
+ color: { r: 255, g: 0, b: 0 }, // 红色
|
|
|
+ fontSize: 14
|
|
|
+ });
|
|
|
+
|
|
|
+ ElMessage.success(`已在 PDF 中添加文本:${userName}`);
|
|
|
+ } catch (err1) {
|
|
|
+ console.error('[WPS] 方法1失败,尝试方法2:', err1);
|
|
|
+
|
|
|
+ // 方法 2:使用简单的批注 API
|
|
|
+ try {
|
|
|
+ await pdfDoc.AddComment({
|
|
|
+ text: userName,
|
|
|
+ author: userStore.userName || '审核人',
|
|
|
+ color: '#FF0000'
|
|
|
});
|
|
|
+ ElMessage.success(`已添加批注:${userName}`);
|
|
|
+ } catch (err2) {
|
|
|
+ console.error('[WPS] 方法2失败,尝试方法3:', err2);
|
|
|
+
|
|
|
+ // 方法 3:使用文本标记
|
|
|
+ try {
|
|
|
+ const selection = await app.ActivePDF.Selection;
|
|
|
+ if (selection) {
|
|
|
+ await selection.AddTextMarkup({
|
|
|
+ type: 'Highlight',
|
|
|
+ text: userName,
|
|
|
+ color: '#FFFF00'
|
|
|
+ });
|
|
|
+ ElMessage.success(`已添加高亮标记:${userName}`);
|
|
|
+ } else {
|
|
|
+ throw new Error('未选中文本');
|
|
|
+ }
|
|
|
+ } catch (err3) {
|
|
|
+ console.error('[WPS] 方法3失败:', err3);
|
|
|
+
|
|
|
+ // 方法 4:使用印章功能
|
|
|
+ ElMessage.info('正在尝试添加文本印章...');
|
|
|
+ try {
|
|
|
+ await pdfDoc.AddStamp({
|
|
|
+ text: userName,
|
|
|
+ position: 'center',
|
|
|
+ color: '#FF0000',
|
|
|
+ fontSize: 14
|
|
|
+ });
|
|
|
+ ElMessage.success(`已添加文本印章:${userName}`);
|
|
|
+ } catch (err4) {
|
|
|
+ console.error('[WPS] 所有方法都失败了:', err4);
|
|
|
+ ElMessage.error('PDF 文本插入失败,请使用 WPS 自带的批注工具');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (err: any) {
|
|
|
+ console.error('[WPS] PDF 操作失败:', err);
|
|
|
+ ElMessage.error('PDF 操作失败: ' + err.message);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ElMessage.warning('当前文档类型不支持插入文本');
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[WPS] 用户名插入成功');
|
|
|
+ } catch (err: any) {
|
|
|
+ console.error('[WPS] 插入用户名失败:', err);
|
|
|
+ ElMessage.error('插入用户名失败: ' + err.message);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 监听 modelValue 变化
|
|
|
+watch(
|
|
|
+ () => props.modelValue,
|
|
|
+ (val) => {
|
|
|
+ dialogVisible.value = val;
|
|
|
+ if (val && props.document) {
|
|
|
+ auditForm.value = {
|
|
|
+ id: props.document.id,
|
|
|
+ result: '3',
|
|
|
+ reason: ''
|
|
|
+ };
|
|
|
+ nextTick(() => {
|
|
|
+ auditFormRef.value?.clearValidate();
|
|
|
+ // 自动初始化 WPS 编辑器
|
|
|
+ if (props.document?.ossId) {
|
|
|
+ initWpsEditor();
|
|
|
}
|
|
|
+ });
|
|
|
}
|
|
|
+ }
|
|
|
);
|
|
|
|
|
|
// 监听dialogVisible变化
|
|
|
watch(dialogVisible, (val) => {
|
|
|
- emit('update:modelValue', val);
|
|
|
- if (!val) {
|
|
|
- auditForm.value = {
|
|
|
- id: 0,
|
|
|
- result: '3',
|
|
|
- reason: ''
|
|
|
- };
|
|
|
- }
|
|
|
+ emit('update:modelValue', val);
|
|
|
+ if (!val) {
|
|
|
+ auditForm.value = {
|
|
|
+ id: 0,
|
|
|
+ result: '3',
|
|
|
+ reason: ''
|
|
|
+ };
|
|
|
+ destroyWpsEditor();
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
// 取消操作
|
|
|
const handleCancel = () => {
|
|
|
- dialogVisible.value = false;
|
|
|
+ dialogVisible.value = false;
|
|
|
};
|
|
|
|
|
|
// 提交表单
|
|
|
const submitForm = () => {
|
|
|
- auditFormRef.value?.validate(async (valid: boolean) => {
|
|
|
- if (valid) {
|
|
|
- loading.value = true;
|
|
|
- try {
|
|
|
- const auditData: AuditData = {
|
|
|
- documentId: auditForm.value.id,
|
|
|
- result: parseInt(auditForm.value.result),
|
|
|
- rejectReason: auditForm.value.reason
|
|
|
- };
|
|
|
- await props.auditApi(auditData);
|
|
|
- ElMessage.success(t('document.document.message.auditSuccess'));
|
|
|
- dialogVisible.value = false;
|
|
|
- emit('success');
|
|
|
- } catch (error) {
|
|
|
- console.error(t('document.document.message.auditFailed'), error);
|
|
|
- ElMessage.error(t('document.document.message.auditFailed'));
|
|
|
- } finally {
|
|
|
- loading.value = false;
|
|
|
+ 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('文档已保存');
|
|
|
}
|
|
|
+ } catch (err) {
|
|
|
+ console.error('保存文档失败:', err);
|
|
|
+ ElMessage.warning('文档保存失败,将继续提交审核');
|
|
|
+ }
|
|
|
}
|
|
|
- });
|
|
|
+
|
|
|
+ // 构建审核数据
|
|
|
+ const auditData: any = {
|
|
|
+ documentId: auditForm.value.id,
|
|
|
+ result: parseInt(auditForm.value.result),
|
|
|
+ rejectReason: auditForm.value.reason
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加保存的文件信息
|
|
|
+ if (savedFileInfo) {
|
|
|
+ auditData.fileInfo = savedFileInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ await props.auditApi(auditData);
|
|
|
+ ElMessage.success(t('document.document.message.auditSuccess'));
|
|
|
+ dialogVisible.value = false;
|
|
|
+ emit('success');
|
|
|
+ } catch (error) {
|
|
|
+ console.error(t('document.document.message.auditFailed'), error);
|
|
|
+ ElMessage.error(t('document.document.message.auditFailed'));
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
};
|
|
|
+
|
|
|
+// 组件卸载前清理
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ destroyWpsEditor();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-.dialog-footer {
|
|
|
+.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;
|
|
|
+
|
|
|
+ .drag-username-btn {
|
|
|
+ cursor: move;
|
|
|
+ user-select: none;
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ cursor: grabbing;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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;
|
|
|
- justify-content: flex-end;
|
|
|
- gap: 10px;
|
|
|
+ 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>
|