|
@@ -96,7 +96,11 @@
|
|
|
</div>
|
|
</div>
|
|
|
<el-row :gutter="16">
|
|
<el-row :gutter="16">
|
|
|
<el-col :span="8" v-for="tpl in recommendTemplates" :key="tpl.id">
|
|
<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
|
|
<el-button
|
|
|
class="tpl-delete-btn"
|
|
class="tpl-delete-btn"
|
|
|
type="danger"
|
|
type="danger"
|
|
@@ -105,19 +109,38 @@
|
|
|
size="small"
|
|
size="small"
|
|
|
@click.stop="handleDeleteTemplate(tpl)"
|
|
@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-info">
|
|
|
<div class="tpl-name">{{ tpl.name }}</div>
|
|
<div class="tpl-name">{{ tpl.name }}</div>
|
|
|
<div class="tpl-meta">
|
|
<div class="tpl-meta">
|
|
|
<span>📊 {{ tpl.useCount }}次</span>
|
|
<span>📊 {{ tpl.useCount }}次</span>
|
|
|
- <span>⭐ {{ tpl.rating.toFixed(1) }}</span>
|
|
|
|
|
|
|
+ <span>⭐ {{ (tpl.rating || 0).toFixed(1) }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="tpl-tags">
|
|
<div class="tpl-tags">
|
|
|
<span class="tpl-tag" v-if="tpl.isOfficial">官方</span>
|
|
<span class="tpl-tag" v-if="tpl.isOfficial">官方</span>
|
|
|
<span class="tpl-tag hot" v-if="tpl.isHot">热门</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>
|
|
</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>
|
|
</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -204,11 +227,11 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
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 { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import { useTemplateStore } from '@/stores/template'
|
|
import { useTemplateStore } from '@/stores/template'
|
|
|
import { useTaskCenterStore } from '@/stores/taskCenter'
|
|
import { useTaskCenterStore } from '@/stores/taskCenter'
|
|
|
-import { templateApi, parseApi } from '@/api'
|
|
|
|
|
|
|
+import { templateApi, parseApi, taskCenterApi } from '@/api'
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
|
const templateStore = useTemplateStore()
|
|
const templateStore = useTemplateStore()
|
|
@@ -283,14 +306,39 @@ async function refreshRecommendTemplates() {
|
|
|
await templateStore.fetchTemplates()
|
|
await templateStore.fetchTemplates()
|
|
|
// 取前3个模板作为推荐(按使用次数排序)
|
|
// 取前3个模板作为推荐(按使用次数排序)
|
|
|
const sortedTemplates = [...templateStore.templates].sort((a, b) => (b.useCount || 0) - (a.useCount || 0))
|
|
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,
|
|
...t,
|
|
|
icon: templateIcons[i % templateIcons.length],
|
|
icon: templateIcons[i % templateIcons.length],
|
|
|
useCount: t.useCount || 0,
|
|
useCount: t.useCount || 0,
|
|
|
rating: t.rating || 0,
|
|
rating: t.rating || 0,
|
|
|
isOfficial: t.isPublic,
|
|
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) {
|
|
} catch (error) {
|
|
|
console.error('获取模板列表失败:', error)
|
|
console.error('获取模板列表失败:', error)
|
|
|
}
|
|
}
|
|
@@ -308,7 +356,18 @@ function handleAiSubmit() {
|
|
|
ElMessage.info('AI 功能开发中...')
|
|
ElMessage.info('AI 功能开发中...')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 使用模板(检查解析状态)
|
|
|
function useTemplate(tpl) {
|
|
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}`)
|
|
router.push(`/editor/${tpl.id}`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -607,6 +666,14 @@ async function handleUploadTemplate() {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
margin-bottom: 16px;
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
+ &.tpl-parsing {
|
|
|
|
|
+ opacity: 0.85;
|
|
|
|
|
+
|
|
|
|
|
+ .tpl-preview {
|
|
|
|
|
+ filter: grayscale(30%);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.tpl-delete-btn {
|
|
.tpl-delete-btn {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
top: 8px;
|
|
top: 8px;
|
|
@@ -620,6 +687,54 @@ async function handleUploadTemplate() {
|
|
|
opacity: 1;
|
|
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 {
|
|
.tpl-btn {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
}
|
|
}
|