Explorar o código

feat: 模板卡片显示解析状态,解析中时拦截进入编辑

1. 获取模板关联文档的解析任务状态
2. 模板卡片显示解析状态(解析中、解析失败)
3. 解析中:显示进度、按钮禁用、灰度遮罩
4. 解析失败:显示警告、禁止进入
5. 点击解析中的模板会打开任务中心
何文松 hai 4 semanas
pai
achega
3b7766407e

+ 10 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/TemplateListResponse.java

@@ -49,6 +49,15 @@ public class TemplateListResponse {
     @Schema(description = "更新时间")
     private Date updateTime;
     
+    @Schema(description = "基础文档ID")
+    private String baseDocumentId;
+    
+    @Schema(description = "解析状态: pending/processing/completed/failed")
+    private String parseStatus;
+    
+    @Schema(description = "解析进度 (0-100)")
+    private Integer parseProgress;
+    
     /**
      * 从实体转换
      */
@@ -63,6 +72,7 @@ public class TemplateListResponse {
         response.setRating(template.getRating());
         response.setCreateTime(template.getCreateTime());
         response.setUpdateTime(template.getUpdateTime());
+        response.setBaseDocumentId(template.getBaseDocumentId());
         return response;
     }
 }

+ 124 - 9
frontend/vue-demo/src/views/Home.vue

@@ -96,7 +96,11 @@
       </div>
       <el-row :gutter="16">
         <el-col :span="8" v-for="tpl in recommendTemplates" :key="tpl.id">
-          <div class="tpl-card card" @click="useTemplate(tpl)">
+          <div 
+            class="tpl-card card" 
+            :class="{ 'tpl-parsing': tpl.parseStatus === 'processing' || tpl.parseStatus === 'pending' }"
+            @click="useTemplate(tpl)"
+          >
             <el-button 
               class="tpl-delete-btn" 
               type="danger" 
@@ -105,19 +109,38 @@
               size="small"
               @click.stop="handleDeleteTemplate(tpl)"
             />
-            <div class="tpl-preview">{{ tpl.icon }}</div>
+            <div class="tpl-preview">
+              {{ tpl.icon }}
+              <!-- 解析中遮罩 -->
+              <div class="tpl-parsing-overlay" v-if="tpl.parseStatus === 'processing' || tpl.parseStatus === 'pending'">
+                <el-icon class="is-loading"><Loading /></el-icon>
+                <span>解析中 {{ tpl.parseProgress }}%</span>
+              </div>
+              <!-- 解析失败标记 -->
+              <div class="tpl-failed-overlay" v-if="tpl.parseStatus === 'failed'">
+                <el-icon><WarningFilled /></el-icon>
+                <span>解析失败</span>
+              </div>
+            </div>
             <div class="tpl-info">
               <div class="tpl-name">{{ tpl.name }}</div>
               <div class="tpl-meta">
                 <span>📊 {{ tpl.useCount }}次</span>
-                <span>⭐ {{ tpl.rating.toFixed(1) }}</span>
+                <span>⭐ {{ (tpl.rating || 0).toFixed(1) }}</span>
               </div>
               <div class="tpl-tags">
                 <span class="tpl-tag" v-if="tpl.isOfficial">官方</span>
                 <span class="tpl-tag hot" v-if="tpl.isHot">热门</span>
+                <span class="tpl-tag parsing" v-if="tpl.parseStatus === 'processing' || tpl.parseStatus === 'pending'">解析中</span>
+                <span class="tpl-tag failed" v-if="tpl.parseStatus === 'failed'">解析失败</span>
               </div>
-              <el-button type="primary" size="small" class="tpl-btn">
-                使用此模板
+              <el-button 
+                type="primary" 
+                size="small" 
+                class="tpl-btn"
+                :disabled="tpl.parseStatus === 'processing' || tpl.parseStatus === 'pending'"
+              >
+                {{ (tpl.parseStatus === 'processing' || tpl.parseStatus === 'pending') ? '解析中...' : '使用此模板' }}
               </el-button>
             </div>
           </div>
@@ -204,11 +227,11 @@
 <script setup>
 import { ref, reactive, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
-import { Promotion, UploadFilled, CircleCheckFilled, Delete } from '@element-plus/icons-vue'
+import { Promotion, UploadFilled, CircleCheckFilled, Delete, Loading, WarningFilled } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useTemplateStore } from '@/stores/template'
 import { useTaskCenterStore } from '@/stores/taskCenter'
-import { templateApi, parseApi } from '@/api'
+import { templateApi, parseApi, taskCenterApi } from '@/api'
 
 const router = useRouter()
 const templateStore = useTemplateStore()
@@ -283,14 +306,39 @@ async function refreshRecommendTemplates() {
     await templateStore.fetchTemplates()
     // 取前3个模板作为推荐(按使用次数排序)
     const sortedTemplates = [...templateStore.templates].sort((a, b) => (b.useCount || 0) - (a.useCount || 0))
-    recommendTemplates.value = sortedTemplates.slice(0, 3).map((t, i) => ({
+    const templates = sortedTemplates.slice(0, 3).map((t, i) => ({
       ...t,
       icon: templateIcons[i % templateIcons.length],
       useCount: t.useCount || 0,
       rating: t.rating || 0,
       isOfficial: t.isPublic,
-      isHot: (t.useCount || 0) >= 100 // 使用次数>=100标记为热门
+      isHot: (t.useCount || 0) >= 100,
+      parseStatus: null, // 解析状态
+      parseProgress: 0   // 解析进度
+    }))
+    
+    // 获取每个模板的解析状态
+    await Promise.all(templates.map(async (tpl) => {
+      if (tpl.baseDocumentId) {
+        try {
+          const task = await taskCenterApi.getByDocumentId(tpl.baseDocumentId)
+          if (task) {
+            tpl.parseStatus = task.status
+            tpl.parseProgress = task.progress || 0
+          }
+        } catch (e) {
+          // 没有解析任务,认为是完成状态
+          tpl.parseStatus = 'completed'
+          tpl.parseProgress = 100
+        }
+      } else {
+        // 没有基础文档,认为是空白模板
+        tpl.parseStatus = 'completed'
+        tpl.parseProgress = 100
+      }
     }))
+    
+    recommendTemplates.value = templates
   } catch (error) {
     console.error('获取模板列表失败:', error)
   }
@@ -308,7 +356,18 @@ function handleAiSubmit() {
   ElMessage.info('AI 功能开发中...')
 }
 
+// 使用模板(检查解析状态)
 function useTemplate(tpl) {
+  if (tpl.parseStatus === 'processing' || tpl.parseStatus === 'pending') {
+    ElMessage.warning('模板正在解析中,请稍后再试')
+    // 打开任务中心
+    taskCenterStore.open = true
+    return
+  }
+  if (tpl.parseStatus === 'failed') {
+    ElMessage.error('模板解析失败,请重新上传或联系管理员')
+    return
+  }
   router.push(`/editor/${tpl.id}`)
 }
 
@@ -607,6 +666,14 @@ async function handleUploadTemplate() {
   position: relative;
   margin-bottom: 16px;
   
+  &.tpl-parsing {
+    opacity: 0.85;
+    
+    .tpl-preview {
+      filter: grayscale(30%);
+    }
+  }
+  
   .tpl-delete-btn {
     position: absolute;
     top: 8px;
@@ -620,6 +687,54 @@ async function handleUploadTemplate() {
     opacity: 1;
   }
   
+  .tpl-preview {
+    position: relative;
+  }
+  
+  .tpl-parsing-overlay,
+  .tpl-failed-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 4px;
+    font-size: 12px;
+    border-radius: 10px 10px 0 0;
+    
+    .el-icon {
+      font-size: 24px;
+    }
+  }
+  
+  .tpl-parsing-overlay {
+    background: rgba(24, 144, 255, 0.15);
+    color: var(--primary);
+  }
+  
+  .tpl-failed-overlay {
+    background: rgba(255, 77, 79, 0.15);
+    color: var(--danger);
+  }
+  
+  .tpl-tags {
+    .tpl-tag {
+      &.parsing {
+        background: var(--primary-light);
+        color: var(--primary);
+      }
+      
+      &.failed {
+        background: #fff1f0;
+        color: var(--danger);
+      }
+    }
+  }
+  
   .tpl-btn {
     width: 100%;
   }