Просмотр исходного кода

feat(frontend): 添加GPU服务不可用提示,优化附件解析流程

何文松 2 дней назад
Родитель
Сommit
445a7d1653
1 измененных файлов с 128 добавлено и 23 удалено
  1. 128 23
      frontend/vue-demo/src/views/Editor.vue

+ 128 - 23
frontend/vue-demo/src/views/Editor.vue

@@ -139,6 +139,7 @@
           </div>
         </div>
 
+
       </div>
 
       <!-- 左侧拖拽分隔条 -->
@@ -153,6 +154,7 @@
             <div class="welcome">
               <h1>{{ greetingText }},{{ userName }}!<span>智能报告,洞察未来。</span></h1>
               <p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
+              <p class="welcome-version">v0.2.1</p>
             </div>
           </div>
         </div>
@@ -979,12 +981,14 @@
     <!-- 规则工作流弹窗 -->
     <el-dialog
       v-model="showRuleWorkflow"
-      :title="workflowTargetRule ? `编辑规则 - ${workflowTargetElement?.elementName || workflowTargetRule.elementKey}` : '新建规则'"
       fullscreen
       :close-on-click-modal="false"
       :show-close="false"
       class="rule-workflow-dialog"
     >
+      <template #header="{ }">
+        <span style="display: none;"></span>
+      </template>
       <RuleWorkflow
         v-if="showRuleWorkflow && currentProjectId"
         :key="workflowTargetRule?.id || 'new'"
@@ -1045,16 +1049,31 @@ const leftPanelTab = ref('projects')
 const sidebarShowAll = ref(false)
 
 const recentActivities = computed(() => {
-  // 基于项目列表生成最近操作(后续可接入真实 API)
+  // 基于项目列表生成最近操作 + 额外 mock 数据
   const acts = []
-  for (const p of projects.value.slice(0, 5)) {
+  
+  // 从项目列表生成
+  for (const p of projects.value.slice(0, 3)) {
     acts.push({
-      text: `${p.title}`,
+      text: `打开项目「${p.title}`,
       source: `@${userName.value}`,
       time: formatTime(p.updatedAt || p.createdAt)
     })
   }
-  return acts
+  
+  // 额外 mock 数据 - 模拟各种操作类型
+  const mockActivities = [
+    { text: '修改要素「评审对象」的值', source: '@张三', time: '10分钟前' },
+    { text: '执行规则「评审得分提取」', source: '@系统', time: '15分钟前' },
+    { text: '上传附件「核心要素评审记录表.docx」', source: '@李四', time: '30分钟前' },
+    { text: '新建规则「评审期自动计算」', source: '@张三', time: '1小时前' },
+    { text: '导出报告「成都院复审报告」', source: '@王五', time: '2小时前' },
+    { text: '修改要素「评审结论级别」的值', source: '@李四', time: '3小时前' },
+    { text: '删除附件「旧版评审表.xlsx」', source: '@张三', time: '昨天 16:30' },
+    { text: '创建项目「西南院标准化复审」', source: '@管理员', time: '昨天 10:15' },
+  ]
+  
+  return [...acts, ...mockActivities].slice(0, 10)
 })
 
 const projects = ref([])
@@ -1162,12 +1181,50 @@ function scrollToElement(item) {
   const docPaper = docPaperRef.value
   if (!docPaper) return
   
-  const selector = `[data-element-key="${item.elementKey}"]`
+  // 高亮系统使用 data-elem-key 属性
+  const selector = `[data-elem-key="${item.elementKey}"]`
   const el = docPaper.querySelector(selector)
   if (el) {
+    // 使用 IntersectionObserver 监听元素进入视口后再闪烁
+    const observer = new IntersectionObserver((entries) => {
+      entries.forEach(entry => {
+        if (entry.isIntersecting) {
+          observer.disconnect()
+          // 元素已在视口中,延迟一点再闪烁确保滚动稳定
+          setTimeout(() => {
+            el.classList.add('elem-flash')
+            setTimeout(() => el.classList.remove('elem-flash'), 1500)
+            // 触发点击以打开弹窗
+            el.click()
+          }, 100)
+        }
+      })
+    }, { threshold: 0.5 })
+    
+    observer.observe(el)
     el.scrollIntoView({ behavior: 'smooth', block: 'center' })
-    // 触发点击以打开弹窗
-    el.click()
+    
+    // 超时保护:如果 3 秒内没有触发,强制执行
+    setTimeout(() => {
+      observer.disconnect()
+    }, 3000)
+  } else {
+    // 如果没有找到高亮元素,尝试在文档中搜索值文本
+    if (item.valueText) {
+      const textToFind = item.valueText.slice(0, 50) // 取前50个字符搜索
+      const walker = document.createTreeWalker(docPaper, NodeFilter.SHOW_TEXT, null, false)
+      let node
+      while (node = walker.nextNode()) {
+        if (node.textContent.includes(textToFind)) {
+          const range = document.createRange()
+          range.selectNodeContents(node)
+          const rect = range.getBoundingClientRect()
+          node.parentElement?.scrollIntoView({ behavior: 'smooth', block: 'center' })
+          break
+        }
+      }
+    }
+    ElMessage.info(`未找到要素「${item.elementName}」的高亮位置`)
   }
 }
 
@@ -1572,11 +1629,11 @@ async function openSourceInViewer(inp, rule = null) {
     }
   }
   
-  // 查找附件
-  const att = attachments.value.find(a => a.id === attId)
-  console.log('[openSourceInViewer] 找到附件:', att)
+  // 查找附件(兼容 id 类型差异)
+  const att = attachments.value.find(a => String(a.id) === String(attId))
+  console.log('[openSourceInViewer] 找到附件:', att, '| attachments:', attachments.value.map(a => ({ id: a.id, type: typeof a.id })))
   if (!att) {
-    ElMessage.warning('未找到来源附件')
+    ElMessage.warning('未找到来源附件,sourceNodeId=' + attId)
     return
   }
   
@@ -2978,7 +3035,14 @@ async function loadAttachmentFile(att, fallbackName = 'file') {
     const resp = await fetch(url, { headers })
     if (!resp.ok) throw new Error(`下载失败(${resp.status})`)
     const blob = await resp.blob()
-    const file = new File([blob], att.displayName || att.fileName || fallbackName, {
+    // 优先使用带扩展名的fileName,否则用displayName+扩展名
+    let fileName = att.fileName || fallbackName
+    if (!fileName && att.displayName && att.fileType) {
+      fileName = `${att.displayName}.${att.fileType}`
+    } else if (!fileName && att.displayName) {
+      fileName = att.displayName
+    }
+    const file = new File([blob], fileName, {
       type: blob.type || 'application/octet-stream'
     })
     attachmentFileCache.set(att.id, file)
@@ -3237,7 +3301,8 @@ async function parseZipEntry(zf) {
       zf.parseResult = result.html || ''
       zf.isHtml = true
     } else if (ext === 'pdf' || ['png', 'jpg', 'jpeg'].includes(ext)) {
-      // PDF/图片: 提取为 blob,发送到 GPU 解析服务
+      // PDF/图片: GPU 解析服务当前不可用
+      throw new Error('PDF/图片解析需要 GPU 服务,暂不可用')
       const blob = await zipEntry.async('blob')
       const mimeMap = { pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg' }
       const file = new File([blob], zf.name.split('/').pop(), { type: mimeMap[ext] || 'application/octet-stream' })
@@ -3498,6 +3563,16 @@ async function handleParseAttachment(att) {
     return
   }
 
+  // PDF/图片:GPU 解析服务当前不可用,提前拦截
+  const gpuExts = ['pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp']
+  if (gpuExts.includes(ext)) {
+    const label = ext === 'pdf' ? 'PDF' : '图片'
+    state.status = 'idle'
+    state.progress = ''
+    ElMessage.warning(`${label}文件解析需要 GPU 服务,暂不可用`)
+    return
+  }
+
   try {
     // 1. 获取后端持久化的原始文件
     state.status = 'uploading'
@@ -4764,6 +4839,12 @@ onMounted(async () => {
         color: var(--text-3);
         line-height: 1.6;
       }
+      
+      .welcome-version {
+        margin-top: 24px;
+        font-size: 12px;
+        color: var(--text-4, #bbb);
+      }
     }
   }
 
@@ -7173,9 +7254,31 @@ onMounted(async () => {
     box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
   }
 }
+
 </style>
 
 <style lang="scss">
+// ==========================================
+// 点击要素时的闪烁效果(非 scoped,应用于动态生成的 HTML)
+// ==========================================
+.elem-flash {
+  animation: elem-flash-anim 0.4s ease-in-out 3 !important;
+  position: relative;
+}
+
+@keyframes elem-flash-anim {
+  0%, 100% {
+    outline: 3px solid rgba(24, 144, 255, 0.4);
+    outline-offset: 2px;
+    background-color: rgba(24, 144, 255, 0.1) !important;
+  }
+  50% {
+    outline: 5px solid rgba(24, 144, 255, 0.9);
+    outline-offset: 4px;
+    background-color: rgba(24, 144, 255, 0.25) !important;
+  }
+}
+
 // ==========================================
 // 附件/规则居中悬浮弹窗样式
 // ==========================================
@@ -7817,19 +7920,21 @@ onMounted(async () => {
   }
 }
 
-// 规则工作流弹窗样式
+// 规则工作流弹窗样式 - 完全隐藏 header
 .rule-workflow-dialog {
-  :deep(.el-dialog__body) {
-    padding: 0;
-    height: 100vh;
-    overflow: hidden;
+  &.el-dialog .el-dialog__header,
+  .el-dialog__header,
+  > .el-dialog__header,
+  > header {
+    display: none !important;
   }
   
-  :deep(.el-dialog__header) {
-    display: none !important;
-    height: 0 !important;
+  &.el-dialog .el-dialog__body,
+  .el-dialog__body,
+  > .el-dialog__body {
     padding: 0 !important;
-    margin: 0 !important;
+    height: 100vh !important;
+    overflow: hidden !important;
   }
 }
 </style>