公众号助手bug修复
This commit is contained in:
@@ -35,6 +35,7 @@ func ensureOfficialAccountArticle(task model.WorkerTask, workflow officialAccoun
|
|||||||
OutlineStyle: normalizeOfficialAccountOutlineStyle(workflow.Form.OutlineStyle),
|
OutlineStyle: normalizeOfficialAccountOutlineStyle(workflow.Form.OutlineStyle),
|
||||||
OutlineWords: normalizeOfficialAccountOutlineWords(workflow.Form.OutlineWords),
|
OutlineWords: normalizeOfficialAccountOutlineWords(workflow.Form.OutlineWords),
|
||||||
ContentTargetWords: normalizeOfficialAccountContentTargetWords(workflow.Form.ContentTargetWords),
|
ContentTargetWords: normalizeOfficialAccountContentTargetWords(workflow.Form.ContentTargetWords),
|
||||||
|
MaxContentImages: normalizeOfficialAccountMaxContentImages(workflow.Form.MaxContentImages),
|
||||||
Status: "draft",
|
Status: "draft",
|
||||||
}
|
}
|
||||||
if err := store.DB.Create(row).Error; err != nil {
|
if err := store.DB.Create(row).Error; err != nil {
|
||||||
@@ -56,6 +57,7 @@ func syncWorkflowFromOfficialAccountArticle(workflow *officialAccountWorkflowSta
|
|||||||
workflow.Form.OutlineStyle = normalizeOfficialAccountOutlineStyle(article.OutlineStyle)
|
workflow.Form.OutlineStyle = normalizeOfficialAccountOutlineStyle(article.OutlineStyle)
|
||||||
workflow.Form.OutlineWords = normalizeOfficialAccountOutlineWords(article.OutlineWords)
|
workflow.Form.OutlineWords = normalizeOfficialAccountOutlineWords(article.OutlineWords)
|
||||||
workflow.Form.ContentTargetWords = normalizeOfficialAccountContentTargetWords(article.ContentTargetWords)
|
workflow.Form.ContentTargetWords = normalizeOfficialAccountContentTargetWords(article.ContentTargetWords)
|
||||||
|
workflow.Form.MaxContentImages = normalizeOfficialAccountMaxContentImages(article.MaxContentImages)
|
||||||
|
|
||||||
if strings.TrimSpace(article.TopicCandidatesJSON) != "" {
|
if strings.TrimSpace(article.TopicCandidatesJSON) != "" {
|
||||||
var items []officialAccountTopicCandidate
|
var items []officialAccountTopicCandidate
|
||||||
@@ -110,6 +112,7 @@ func syncOfficialAccountArticleFromWorkflow(article *model.OfficialAccountArticl
|
|||||||
article.OutlineStyle = normalizeOfficialAccountOutlineStyle(workflow.Form.OutlineStyle)
|
article.OutlineStyle = normalizeOfficialAccountOutlineStyle(workflow.Form.OutlineStyle)
|
||||||
article.OutlineWords = normalizeOfficialAccountOutlineWords(workflow.Form.OutlineWords)
|
article.OutlineWords = normalizeOfficialAccountOutlineWords(workflow.Form.OutlineWords)
|
||||||
article.ContentTargetWords = normalizeOfficialAccountContentTargetWords(workflow.Form.ContentTargetWords)
|
article.ContentTargetWords = normalizeOfficialAccountContentTargetWords(workflow.Form.ContentTargetWords)
|
||||||
|
article.MaxContentImages = normalizeOfficialAccountMaxContentImages(workflow.Form.MaxContentImages)
|
||||||
article.SelectedTopic = workflow.Shared.SelectedTopic
|
article.SelectedTopic = workflow.Shared.SelectedTopic
|
||||||
article.SelectedTitle = workflow.Shared.SelectedTitle
|
article.SelectedTitle = workflow.Shared.SelectedTitle
|
||||||
article.Outline = workflow.Shared.Outline
|
article.Outline = workflow.Shared.Outline
|
||||||
@@ -174,6 +177,7 @@ func resetOfficialAccountArticle(article *model.OfficialAccountArticle, req offi
|
|||||||
article.OutlineWords = normalizeOfficialAccountOutlineWords(req.OutlineWords)
|
article.OutlineWords = normalizeOfficialAccountOutlineWords(req.OutlineWords)
|
||||||
article.Outline = ""
|
article.Outline = ""
|
||||||
article.ContentTargetWords = normalizeOfficialAccountContentTargetWords(req.ContentTargetWords)
|
article.ContentTargetWords = normalizeOfficialAccountContentTargetWords(req.ContentTargetWords)
|
||||||
|
article.MaxContentImages = normalizeOfficialAccountMaxContentImages(req.MaxContentImages)
|
||||||
article.Content = ""
|
article.Content = ""
|
||||||
article.ContentWithPrompts = ""
|
article.ContentWithPrompts = ""
|
||||||
article.ImagePromptsJSON = "[]"
|
article.ImagePromptsJSON = "[]"
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func executeOfficialAccountPageMarkdownStep(workflow *officialAccountWorkflowSta
|
|||||||
func buildOfficialAccountImagePrompts(workflow *officialAccountWorkflowState) []officialAccountImagePrompt {
|
func buildOfficialAccountImagePrompts(workflow *officialAccountWorkflowState) []officialAccountImagePrompt {
|
||||||
title := firstNonEmpty(workflow.Shared.SelectedTitle, workflow.Form.Keyword, "公众号文章")
|
title := firstNonEmpty(workflow.Shared.SelectedTitle, workflow.Form.Keyword, "公众号文章")
|
||||||
sections := extractOfficialAccountSections(workflow.Shared.Content)
|
sections := extractOfficialAccountSections(workflow.Shared.Content)
|
||||||
|
maxContentImages := normalizeOfficialAccountMaxContentImages(workflow.Form.MaxContentImages)
|
||||||
prompts := []officialAccountImagePrompt{
|
prompts := []officialAccountImagePrompt{
|
||||||
{
|
{
|
||||||
Key: "cover",
|
Key: "cover",
|
||||||
@@ -155,7 +156,7 @@ func buildOfficialAccountImagePrompts(workflow *officialAccountWorkflowState) []
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for idx, item := range sections {
|
for idx, item := range sections {
|
||||||
if idx >= 3 {
|
if maxContentImages > 0 && idx >= maxContentImages {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
prompts = append(prompts, officialAccountImagePrompt{
|
prompts = append(prompts, officialAccountImagePrompt{
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type officialAccountTaskCreateReq struct {
|
|||||||
OutlineStyle string `json:"outline_style"`
|
OutlineStyle string `json:"outline_style"`
|
||||||
OutlineWords int `json:"outline_words"`
|
OutlineWords int `json:"outline_words"`
|
||||||
ContentTargetWords int `json:"content_target_words"`
|
ContentTargetWords int `json:"content_target_words"`
|
||||||
|
MaxContentImages int `json:"max_content_images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type officialAccountStepReq struct {
|
type officialAccountStepReq struct {
|
||||||
@@ -67,6 +68,7 @@ type officialAccountTaskUpdateReq struct {
|
|||||||
OutlineStyle string `json:"outline_style"`
|
OutlineStyle string `json:"outline_style"`
|
||||||
OutlineWords int `json:"outline_words"`
|
OutlineWords int `json:"outline_words"`
|
||||||
ContentTargetWords int `json:"content_target_words"`
|
ContentTargetWords int `json:"content_target_words"`
|
||||||
|
MaxContentImages int `json:"max_content_images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type officialAccountWorkflowState struct {
|
type officialAccountWorkflowState struct {
|
||||||
@@ -87,6 +89,7 @@ type officialAccountForm struct {
|
|||||||
OutlineStyle string `json:"outline_style"`
|
OutlineStyle string `json:"outline_style"`
|
||||||
OutlineWords int `json:"outline_words"`
|
OutlineWords int `json:"outline_words"`
|
||||||
ContentTargetWords int `json:"content_target_words"`
|
ContentTargetWords int `json:"content_target_words"`
|
||||||
|
MaxContentImages int `json:"max_content_images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type officialAccountSharedState struct {
|
type officialAccountSharedState struct {
|
||||||
@@ -275,7 +278,8 @@ func UpdateOfficialAccountTask(c *gin.Context) {
|
|||||||
oldWorkflow.Form.Requirements != req.Requirements ||
|
oldWorkflow.Form.Requirements != req.Requirements ||
|
||||||
oldWorkflow.Form.OutlineStyle != normalizeOfficialAccountOutlineStyle(req.OutlineStyle) ||
|
oldWorkflow.Form.OutlineStyle != normalizeOfficialAccountOutlineStyle(req.OutlineStyle) ||
|
||||||
oldWorkflow.Form.OutlineWords != normalizeOfficialAccountOutlineWords(req.OutlineWords) ||
|
oldWorkflow.Form.OutlineWords != normalizeOfficialAccountOutlineWords(req.OutlineWords) ||
|
||||||
oldWorkflow.Form.ContentTargetWords != normalizeOfficialAccountContentTargetWords(req.ContentTargetWords)
|
oldWorkflow.Form.ContentTargetWords != normalizeOfficialAccountContentTargetWords(req.ContentTargetWords) ||
|
||||||
|
oldWorkflow.Form.MaxContentImages != normalizeOfficialAccountMaxContentImages(req.MaxContentImages)
|
||||||
|
|
||||||
workflow := oldWorkflow
|
workflow := oldWorkflow
|
||||||
if changed {
|
if changed {
|
||||||
@@ -289,6 +293,7 @@ func UpdateOfficialAccountTask(c *gin.Context) {
|
|||||||
OutlineStyle: req.OutlineStyle,
|
OutlineStyle: req.OutlineStyle,
|
||||||
OutlineWords: req.OutlineWords,
|
OutlineWords: req.OutlineWords,
|
||||||
ContentTargetWords: req.ContentTargetWords,
|
ContentTargetWords: req.ContentTargetWords,
|
||||||
|
MaxContentImages: req.MaxContentImages,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
workflow.Form.BusinessDomain = resolveOfficialAccountBusinessDomain(req.BusinessDomain, req.Keyword)
|
workflow.Form.BusinessDomain = resolveOfficialAccountBusinessDomain(req.BusinessDomain, req.Keyword)
|
||||||
@@ -300,6 +305,7 @@ func UpdateOfficialAccountTask(c *gin.Context) {
|
|||||||
workflow.Form.OutlineStyle = normalizeOfficialAccountOutlineStyle(req.OutlineStyle)
|
workflow.Form.OutlineStyle = normalizeOfficialAccountOutlineStyle(req.OutlineStyle)
|
||||||
workflow.Form.OutlineWords = normalizeOfficialAccountOutlineWords(req.OutlineWords)
|
workflow.Form.OutlineWords = normalizeOfficialAccountOutlineWords(req.OutlineWords)
|
||||||
workflow.Form.ContentTargetWords = normalizeOfficialAccountContentTargetWords(req.ContentTargetWords)
|
workflow.Form.ContentTargetWords = normalizeOfficialAccountContentTargetWords(req.ContentTargetWords)
|
||||||
|
workflow.Form.MaxContentImages = normalizeOfficialAccountMaxContentImages(req.MaxContentImages)
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
@@ -352,6 +358,7 @@ func UpdateOfficialAccountTask(c *gin.Context) {
|
|||||||
"outline_style": normalizeOfficialAccountOutlineStyle(req.OutlineStyle),
|
"outline_style": normalizeOfficialAccountOutlineStyle(req.OutlineStyle),
|
||||||
"outline_words": normalizeOfficialAccountOutlineWords(req.OutlineWords),
|
"outline_words": normalizeOfficialAccountOutlineWords(req.OutlineWords),
|
||||||
"content_target_words": normalizeOfficialAccountContentTargetWords(req.ContentTargetWords),
|
"content_target_words": normalizeOfficialAccountContentTargetWords(req.ContentTargetWords),
|
||||||
|
"max_content_images": normalizeOfficialAccountMaxContentImages(req.MaxContentImages),
|
||||||
})
|
})
|
||||||
logsJSON, _ := json.Marshal([]string{
|
logsJSON, _ := json.Marshal([]string{
|
||||||
fmt.Sprintf("%s 已切换主题并重置流程", now.Format("15:04:05")),
|
fmt.Sprintf("%s 已切换主题并重置流程", now.Format("15:04:05")),
|
||||||
@@ -556,6 +563,7 @@ func newOfficialAccountWorkflow(req officialAccountTaskCreateReq) officialAccoun
|
|||||||
OutlineStyle: normalizeOfficialAccountOutlineStyle(req.OutlineStyle),
|
OutlineStyle: normalizeOfficialAccountOutlineStyle(req.OutlineStyle),
|
||||||
OutlineWords: normalizeOfficialAccountOutlineWords(req.OutlineWords),
|
OutlineWords: normalizeOfficialAccountOutlineWords(req.OutlineWords),
|
||||||
ContentTargetWords: normalizeOfficialAccountContentTargetWords(req.ContentTargetWords),
|
ContentTargetWords: normalizeOfficialAccountContentTargetWords(req.ContentTargetWords),
|
||||||
|
MaxContentImages: normalizeOfficialAccountMaxContentImages(req.MaxContentImages),
|
||||||
},
|
},
|
||||||
Steps: map[string]*officialAccountStepState{},
|
Steps: map[string]*officialAccountStepState{},
|
||||||
Shared: officialAccountSharedState{
|
Shared: officialAccountSharedState{
|
||||||
@@ -611,6 +619,16 @@ func normalizeOfficialAccountContentTargetWords(value int) int {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeOfficialAccountMaxContentImages(value int) int {
|
||||||
|
if value <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if value > 20 {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func parseOfficialAccountWorkflow(value string) officialAccountWorkflowState {
|
func parseOfficialAccountWorkflow(value string) officialAccountWorkflowState {
|
||||||
if strings.TrimSpace(value) == "" {
|
if strings.TrimSpace(value) == "" {
|
||||||
return newOfficialAccountWorkflow(officialAccountTaskCreateReq{})
|
return newOfficialAccountWorkflow(officialAccountTaskCreateReq{})
|
||||||
@@ -634,6 +652,7 @@ func parseOfficialAccountWorkflow(value string) officialAccountWorkflowState {
|
|||||||
if workflow.Form.Tone == "" {
|
if workflow.Form.Tone == "" {
|
||||||
workflow.Form.Tone = "专业但好懂"
|
workflow.Form.Tone = "专业但好懂"
|
||||||
}
|
}
|
||||||
|
workflow.Form.MaxContentImages = normalizeOfficialAccountMaxContentImages(workflow.Form.MaxContentImages)
|
||||||
if workflow.Steps == nil {
|
if workflow.Steps == nil {
|
||||||
workflow.Steps = map[string]*officialAccountStepState{}
|
workflow.Steps = map[string]*officialAccountStepState{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type OfficialAccountArticle struct {
|
|||||||
OutlineWords int `gorm:"not null;default:600" json:"outline_words"`
|
OutlineWords int `gorm:"not null;default:600" json:"outline_words"`
|
||||||
Outline string `gorm:"type:text" json:"outline"`
|
Outline string `gorm:"type:text" json:"outline"`
|
||||||
ContentTargetWords int `gorm:"not null;default:1400" json:"content_target_words"`
|
ContentTargetWords int `gorm:"not null;default:1400" json:"content_target_words"`
|
||||||
|
MaxContentImages int `gorm:"not null;default:0" json:"max_content_images"`
|
||||||
Content string `gorm:"type:text" json:"content"`
|
Content string `gorm:"type:text" json:"content"`
|
||||||
ContentWithPrompts string `gorm:"type:text" json:"content_with_prompts"`
|
ContentWithPrompts string `gorm:"type:text" json:"content_with_prompts"`
|
||||||
ImagePromptsJSON string `gorm:"type:text" json:"image_prompts_json"`
|
ImagePromptsJSON string `gorm:"type:text" json:"image_prompts_json"`
|
||||||
|
|||||||
@@ -16,6 +16,14 @@
|
|||||||
<el-input v-model="taskForm.audience" placeholder="目标受众,例如:企业老板、销售顾问" />
|
<el-input v-model="taskForm.audience" placeholder="目标受众,例如:企业老板、销售顾问" />
|
||||||
<el-input v-model="taskForm.goal" placeholder="内容目标,例如:做认知、做转化、做留资" />
|
<el-input v-model="taskForm.goal" placeholder="内容目标,例如:做认知、做转化、做留资" />
|
||||||
<el-input v-model="taskForm.tone" placeholder="内容语气,例如:专业但好懂" />
|
<el-input v-model="taskForm.tone" placeholder="内容语气,例如:专业但好懂" />
|
||||||
|
<el-select v-model="taskForm.maxContentImages" placeholder="请选择配图上限">
|
||||||
|
<el-option
|
||||||
|
v-for="option in maxContentImageOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-subtitle">
|
<div class="panel-subtitle">
|
||||||
当前将按「{{ taskResolvedBusinessDomain.label }}」处理热点源、评分规则和 Prompt
|
当前将按「{{ taskResolvedBusinessDomain.label }}」处理热点源、评分规则和 Prompt
|
||||||
@@ -80,7 +88,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #node-official-step="{ id, data }">
|
<template #node-official-step="{ id, data }">
|
||||||
<div class="oa-node" :class="[data.statusClass, { selected: selectedNodeKey === id, bubbleOpen: isNodeExpanded(id) }]">
|
<div class="oa-node" :class="[data.statusClass, { selected: selectedNodeKey === id, bubbleOpen: isNodeExpanded(id) }]">
|
||||||
<Handle type="target" :position="Position.Left" />
|
<Handle type="target" :position="data.targetPosition" />
|
||||||
<div class="oa-node-head">
|
<div class="oa-node-head">
|
||||||
<span class="oa-node-index">{{ data.index }}</span>
|
<span class="oa-node-index">{{ data.index }}</span>
|
||||||
<span class="oa-node-badge" :class="data.statusClass">{{ data.statusText }}</span>
|
<span class="oa-node-badge" :class="data.statusClass">{{ data.statusText }}</span>
|
||||||
@@ -88,7 +96,11 @@
|
|||||||
<div class="oa-node-title">{{ data.label }}</div>
|
<div class="oa-node-title">{{ data.label }}</div>
|
||||||
<div class="oa-node-desc">{{ data.preview }}</div>
|
<div class="oa-node-desc">{{ data.preview }}</div>
|
||||||
<div class="oa-node-progress">
|
<div class="oa-node-progress">
|
||||||
<div class="oa-node-progress-fill" :style="{ width: `${data.progress}%` }" />
|
<div
|
||||||
|
class="oa-node-progress-fill"
|
||||||
|
:class="{ 'is-running': data.progressActive }"
|
||||||
|
:style="{ width: `${data.progress}%` }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="oa-node-foot">{{ data.progressText }}</div>
|
<div class="oa-node-foot">{{ data.progressText }}</div>
|
||||||
<div class="oa-node-actions">
|
<div class="oa-node-actions">
|
||||||
@@ -232,6 +244,10 @@
|
|||||||
<span>目标篇幅</span>
|
<span>目标篇幅</span>
|
||||||
<strong>提纲 {{ configForm.outlineWords || 600 }} 字 / 正文 {{ configForm.contentTargetWords || 1400 }} 字</strong>
|
<strong>提纲 {{ configForm.outlineWords || 600 }} 字 / 正文 {{ configForm.contentTargetWords || 1400 }} 字</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span>内容配图上限</span>
|
||||||
|
<strong>{{ formatMaxContentImages(configForm.maxContentImages) }}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-form-grid">
|
<div class="config-form-grid">
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
@@ -257,6 +273,19 @@
|
|||||||
<el-option v-for="option in contentWordOptions" :key="option" :label="`${option} 字`" :value="option" />
|
<el-option v-for="option in contentWordOptions" :key="option" :label="`${option} 字`" :value="option" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="config-field">
|
||||||
|
<div class="config-label">内容配图上限</div>
|
||||||
|
<div class="config-tip">默认按正文段落数动态生成,也可以手动限制最多生成多少张内容配图。</div>
|
||||||
|
<el-select v-model="configForm.maxContentImages" placeholder="请选择配图上限">
|
||||||
|
<el-option
|
||||||
|
v-for="option in maxContentImageOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<el-button type="primary" :loading="savingConfig" @click.stop="saveTaskConfig">保存任务配置</el-button>
|
<el-button type="primary" :loading="savingConfig" @click.stop="saveTaskConfig">保存任务配置</el-button>
|
||||||
@@ -353,10 +382,32 @@
|
|||||||
|
|
||||||
<template v-else-if="id === stepKeyImages">
|
<template v-else-if="id === stepKeyImages">
|
||||||
<div class="node-panel-title">图片结果</div>
|
<div class="node-panel-title">图片结果</div>
|
||||||
<div class="inline-meta">当前已生成 {{ (workflow.shared.image_refs || []).length }} 张图片</div>
|
<div class="inline-meta">
|
||||||
|
当前已生成 {{ (workflow.shared.image_refs || []).length }} 张图片
|
||||||
|
<template v-if="(workflow.shared.image_refs || []).length">
|
||||||
|
· 预览成功 {{ imagePreviewLoadedCount }} 张
|
||||||
|
<template v-if="imagePreviewFailedCount"> · 失败 {{ imagePreviewFailedCount }} 张</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div v-if="(workflow.shared.image_refs || []).length" class="image-grid">
|
<div v-if="(workflow.shared.image_refs || []).length" class="image-grid">
|
||||||
<div v-for="item in workflow.shared.image_refs || []" :key="item.key" class="image-card">
|
<div v-for="item in workflow.shared.image_refs || []" :key="item.key" class="image-card">
|
||||||
<img class="image-thumb" :src="item.url" :alt="item.section || item.key" />
|
<div class="image-status-row">
|
||||||
|
<div class="image-status-chip" :class="`is-${getImagePreviewStatus(item).state}`">
|
||||||
|
{{ getImagePreviewStatus(item).label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="getImagePreviewStatus(item).state === 'loading'" class="image-thumb image-thumb-placeholder">
|
||||||
|
图片预览加载中...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="getImagePreviewStatus(item).state === 'error'" class="image-thumb image-thumb-placeholder is-error">
|
||||||
|
{{ getImagePreviewStatus(item).message || '图片预览失败,建议重新生成' }}
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="image-thumb"
|
||||||
|
:src="getImagePreviewSource(item)"
|
||||||
|
:alt="item.section || item.key"
|
||||||
|
/>
|
||||||
<div class="image-section">{{ item.section || item.key }}</div>
|
<div class="image-section">{{ item.section || item.key }}</div>
|
||||||
<div class="image-prompt">{{ item.prompt }}</div>
|
<div class="image-prompt">{{ item.prompt }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -382,7 +433,7 @@
|
|||||||
导出预览 HTML
|
导出预览 HTML
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="workflow.shared.preview_html" class="preview-render" v-html="workflow.shared.preview_html" />
|
<div v-if="workflow.shared.preview_html" class="preview-render" v-html="resolvedPreviewHtml" />
|
||||||
<div v-else class="empty-text">当前还没有生成预览稿。</div>
|
<div v-else class="empty-text">当前还没有生成预览稿。</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -423,7 +474,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Handle type="source" :position="Position.Right" />
|
<Handle type="source" :position="data.sourcePosition" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
@@ -437,7 +488,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { BaseEdge, EdgeLabelRenderer, VueFlow, Handle, Position, MarkerType, getSmoothStepPath } from '@vue-flow/core'
|
import { BaseEdge, EdgeLabelRenderer, VueFlow, Handle, Position, MarkerType, getSmoothStepPath } from '@vue-flow/core'
|
||||||
@@ -466,6 +517,16 @@ const stepKeyImages = 'image_generation'
|
|||||||
const stepKeyPreview = 'preview_export'
|
const stepKeyPreview = 'preview_export'
|
||||||
const stepKeyPageMD = 'page_markdown'
|
const stepKeyPageMD = 'page_markdown'
|
||||||
const stepKeyConfig = 'task_configuration'
|
const stepKeyConfig = 'task_configuration'
|
||||||
|
const workflowStepKeys = [
|
||||||
|
stepKeyTopic,
|
||||||
|
stepKeyTitle,
|
||||||
|
stepKeyOutline,
|
||||||
|
stepKeyContent,
|
||||||
|
stepKeyImagePrompts,
|
||||||
|
stepKeyImages,
|
||||||
|
stepKeyPreview,
|
||||||
|
stepKeyPageMD,
|
||||||
|
]
|
||||||
|
|
||||||
const creatingTask = ref(false)
|
const creatingTask = ref(false)
|
||||||
const loadingWorkflow = ref(false)
|
const loadingWorkflow = ref(false)
|
||||||
@@ -479,12 +540,20 @@ const fittedTaskId = ref('')
|
|||||||
const canvasNodes = ref([])
|
const canvasNodes = ref([])
|
||||||
const canvasEdges = ref([])
|
const canvasEdges = ref([])
|
||||||
const canvasZoomOnScroll = ref(true)
|
const canvasZoomOnScroll = ref(true)
|
||||||
const canvasTranslateExtent = [[-320, -260], [4680, 1680]]
|
const canvasTranslateExtent = [[-320, -260], [4080, 1920]]
|
||||||
const canvasNodeExtent = [[40, 80], [4320, 1320]]
|
const canvasNodeExtent = [[40, 80], [3520, 1480]]
|
||||||
const expandedHotspotKeys = ref([])
|
const expandedHotspotKeys = ref([])
|
||||||
const pollingToken = ref(0)
|
const pollingToken = ref(0)
|
||||||
const pollingStepKey = ref('')
|
const pollingStepKey = ref('')
|
||||||
const exportingFormat = ref('')
|
const exportingFormat = ref('')
|
||||||
|
const imagePreviewSources = ref({})
|
||||||
|
const imageExportSources = ref({})
|
||||||
|
const imagePreviewStates = ref({})
|
||||||
|
const runningStepStartedAt = ref({})
|
||||||
|
const stepProgressHoldUntil = ref({})
|
||||||
|
const progressClock = ref(Date.now())
|
||||||
|
let progressClockTimer = null
|
||||||
|
const minRunningProgressVisibleMs = 3200
|
||||||
|
|
||||||
const taskForm = reactive({
|
const taskForm = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@@ -496,6 +565,7 @@ const taskForm = reactive({
|
|||||||
outlineStyle: '问题拆解型',
|
outlineStyle: '问题拆解型',
|
||||||
outlineWords: 600,
|
outlineWords: 600,
|
||||||
contentTargetWords: 1400,
|
contentTargetWords: 1400,
|
||||||
|
maxContentImages: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const stepPayload = reactive({
|
const stepPayload = reactive({
|
||||||
@@ -517,6 +587,7 @@ const configForm = reactive({
|
|||||||
outlineStyle: '问题拆解型',
|
outlineStyle: '问题拆解型',
|
||||||
outlineWords: 600,
|
outlineWords: 600,
|
||||||
contentTargetWords: 1400,
|
contentTargetWords: 1400,
|
||||||
|
maxContentImages: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const businessDomainOptions = [
|
const businessDomainOptions = [
|
||||||
@@ -581,6 +652,13 @@ const outlineStyleOptions = [
|
|||||||
|
|
||||||
const outlineWordOptions = [300, 500, 600, 800, 1000]
|
const outlineWordOptions = [300, 500, 600, 800, 1000]
|
||||||
const contentWordOptions = [1000, 1200, 1400, 1800, 2200]
|
const contentWordOptions = [1000, 1200, 1400, 1800, 2200]
|
||||||
|
const maxContentImageOptions = [
|
||||||
|
{ label: '动态(按正文段落)', value: 0 },
|
||||||
|
{ label: '最多 3 张内容图', value: 3 },
|
||||||
|
{ label: '最多 5 张内容图', value: 5 },
|
||||||
|
{ label: '最多 8 张内容图', value: 8 },
|
||||||
|
{ label: '最多 12 张内容图', value: 12 },
|
||||||
|
]
|
||||||
|
|
||||||
const workspaceKey = computed(() => route.meta.workspaceKey || 'wechat-official-account')
|
const workspaceKey = computed(() => route.meta.workspaceKey || 'wechat-official-account')
|
||||||
const selectedTaskId = computed(() => {
|
const selectedTaskId = computed(() => {
|
||||||
@@ -600,6 +678,10 @@ const taskSummary = computed(() => task.value?.summary || '通过节点式画布
|
|||||||
const taskStatusText = computed(() => task.value?.status || '待创建')
|
const taskStatusText = computed(() => task.value?.status || '待创建')
|
||||||
const completedStepCount = computed(() => Object.values(workflow.value?.steps || {}).filter(item => item.status === 'completed').length)
|
const completedStepCount = computed(() => Object.values(workflow.value?.steps || {}).filter(item => item.status === 'completed').length)
|
||||||
const currentStepLabel = computed(() => stepLabel(workflow.value?.current_step || stepKeyTopic))
|
const currentStepLabel = computed(() => stepLabel(workflow.value?.current_step || stepKeyTopic))
|
||||||
|
const imagePreviewItems = computed(() => workflow.value?.shared?.image_refs || [])
|
||||||
|
const imagePreviewLoadedCount = computed(() => imagePreviewItems.value.filter(item => getImagePreviewStatus(item).state === 'loaded').length)
|
||||||
|
const imagePreviewFailedCount = computed(() => imagePreviewItems.value.filter(item => getImagePreviewStatus(item).state === 'error').length)
|
||||||
|
const resolvedPreviewHtml = computed(() => replacePreviewHtmlImageSources(workflow.value?.shared?.preview_html || '', 'display'))
|
||||||
|
|
||||||
const workflowSteps = computed(() => {
|
const workflowSteps = computed(() => {
|
||||||
const steps = workflow.value?.steps || {}
|
const steps = workflow.value?.steps || {}
|
||||||
@@ -623,6 +705,7 @@ function getOutputRows(stepKey) {
|
|||||||
{ label: '当前标题', value: workflow.value?.shared?.selected_title || '未选择' },
|
{ label: '当前标题', value: workflow.value?.shared?.selected_title || '未选择' },
|
||||||
{ label: '提纲风格', value: article.value?.outline_style || configForm.outlineStyle || '问题拆解型' },
|
{ label: '提纲风格', value: article.value?.outline_style || configForm.outlineStyle || '问题拆解型' },
|
||||||
{ label: '正文字数', value: `${article.value?.content_target_words || configForm.contentTargetWords || 1400} 字` },
|
{ label: '正文字数', value: `${article.value?.content_target_words || configForm.contentTargetWords || 1400} 字` },
|
||||||
|
{ label: '内容配图上限', value: formatMaxContentImages(article.value?.max_content_images ?? configForm.maxContentImages) },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const step = workflowSteps.value.find(item => item.key === stepKey)
|
const step = workflowSteps.value.find(item => item.key === stepKey)
|
||||||
@@ -693,17 +776,25 @@ const flowNodes = computed(() => {
|
|||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
|
|
||||||
return nodes.map((item, index) => ({
|
return nodes.map((item, index) => {
|
||||||
|
const isBottomRow = isBottomRowStep(item.id)
|
||||||
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
type: 'official-step',
|
type: 'official-step',
|
||||||
position: { x: 80 + index * 410, y: 150 },
|
class: isNodeExpanded(item.id) ? 'node-expanded' : '',
|
||||||
|
position: getFlowNodePosition(item.id),
|
||||||
selectable: true,
|
selectable: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
sourcePosition: isBottomRow ? Position.Left : Position.Right,
|
||||||
|
targetPosition: isBottomRow ? Position.Right : Position.Left,
|
||||||
data: {
|
data: {
|
||||||
...item,
|
...item,
|
||||||
bubbleAlign: getNodeBubbleAlign(item.id),
|
bubbleAlign: getNodeBubbleAlign(item.id),
|
||||||
|
sourcePosition: isBottomRow ? Position.Left : Position.Right,
|
||||||
|
targetPosition: isBottomRow ? Position.Right : Position.Left,
|
||||||
},
|
},
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const flowEdges = computed(() => {
|
const flowEdges = computed(() => {
|
||||||
@@ -749,8 +840,9 @@ watch(flowEdges, (value) => {
|
|||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
function buildStepCard(index, key, state) {
|
function buildStepCard(index, key, state) {
|
||||||
const status = state?.status || 'pending'
|
const status = getDisplayStepStatus(key, state)
|
||||||
const rerunnable = status === 'completed' || status === 'error'
|
const rerunnable = status === 'completed' || status === 'error'
|
||||||
|
const progress = getDisplayStepProgress(key, state)
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
key,
|
key,
|
||||||
@@ -758,8 +850,9 @@ function buildStepCard(index, key, state) {
|
|||||||
status,
|
status,
|
||||||
statusText: stepStatusText(status),
|
statusText: stepStatusText(status),
|
||||||
statusClass: `status-${status}`,
|
statusClass: `status-${status}`,
|
||||||
progress: state?.progress || 0,
|
progress,
|
||||||
progressText: state?.progress_text || '等待执行',
|
progressActive: isStepProgressActive(key, status),
|
||||||
|
progressText: getStepProgressText(key, state, progress),
|
||||||
logs: state?.logs || [],
|
logs: state?.logs || [],
|
||||||
output: state?.output || {},
|
output: state?.output || {},
|
||||||
preview: stepPreview(key),
|
preview: stepPreview(key),
|
||||||
@@ -781,6 +874,11 @@ function stepPreview(key) {
|
|||||||
return '等待执行'
|
return '等待执行'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMaxContentImages(value) {
|
||||||
|
const count = Number(value) || 0
|
||||||
|
return count > 0 ? `最多 ${count} 张内容图` : '动态(按正文段落)'
|
||||||
|
}
|
||||||
|
|
||||||
function canRunStep(key) {
|
function canRunStep(key) {
|
||||||
const activeRunningKey = getActiveRunningStepKey()
|
const activeRunningKey = getActiveRunningStepKey()
|
||||||
if (activeRunningKey) return activeRunningKey === key
|
if (activeRunningKey) return activeRunningKey === key
|
||||||
@@ -808,6 +906,123 @@ function stepHintText(key) {
|
|||||||
return '当前节点暂不可执行'
|
return '当前节点暂不可执行'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markStepRunning(stepKey) {
|
||||||
|
if (!stepKey) return
|
||||||
|
if (runningStepStartedAt.value[stepKey]) return
|
||||||
|
runningStepStartedAt.value = {
|
||||||
|
...runningStepStartedAt.value,
|
||||||
|
[stepKey]: Date.now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function holdStepProgress(stepKey) {
|
||||||
|
if (!stepKey) return
|
||||||
|
const startedAt = runningStepStartedAt.value[stepKey]
|
||||||
|
if (!startedAt) return
|
||||||
|
const holdUntil = startedAt + minRunningProgressVisibleMs
|
||||||
|
if (holdUntil <= Date.now()) return
|
||||||
|
stepProgressHoldUntil.value = {
|
||||||
|
...stepProgressHoldUntil.value,
|
||||||
|
[stepKey]: holdUntil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStepRunning(stepKey) {
|
||||||
|
if (!stepKey || !runningStepStartedAt.value[stepKey]) return
|
||||||
|
const next = { ...runningStepStartedAt.value }
|
||||||
|
delete next[stepKey]
|
||||||
|
runningStepStartedAt.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStepProgressHold(stepKey) {
|
||||||
|
if (!stepKey || !stepProgressHoldUntil.value[stepKey]) return
|
||||||
|
const next = { ...stepProgressHoldUntil.value }
|
||||||
|
delete next[stepKey]
|
||||||
|
stepProgressHoldUntil.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseStepProgressState(stepKey) {
|
||||||
|
clearStepProgressHold(stepKey)
|
||||||
|
clearStepRunning(stepKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStepProgressHolding(stepKey) {
|
||||||
|
return Number(stepProgressHoldUntil.value[stepKey] || 0) > progressClock.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneExpiredStepProgressState(now = Date.now()) {
|
||||||
|
const expiredKeys = Object.entries(stepProgressHoldUntil.value)
|
||||||
|
.filter(([, holdUntil]) => Number(holdUntil) <= now)
|
||||||
|
.map(([key]) => key)
|
||||||
|
expiredKeys.forEach(key => releaseStepProgressState(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldKeepLocalProgress(stepKey) {
|
||||||
|
const startedAt = Number(runningStepStartedAt.value[stepKey] || 0)
|
||||||
|
return startedAt > 0 && progressClock.value - startedAt < minRunningProgressVisibleMs
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRunningStepProgress(workflowState = workflow.value) {
|
||||||
|
const currentRunningKey = findRunningStepKey(workflowState)
|
||||||
|
workflowStepKeys.forEach((key) => {
|
||||||
|
const isLocallyActive = runningStepKey.value === key || pollingStepKey.value === key
|
||||||
|
if (key === currentRunningKey || isLocallyActive) {
|
||||||
|
markStepRunning(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isStepProgressHolding(key) && !shouldKeepLocalProgress(key)) {
|
||||||
|
clearStepRunning(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStepProgressActive(stepKey, status = workflow.value?.steps?.[stepKey]?.status) {
|
||||||
|
return status === 'running'
|
||||||
|
|| runningStepKey.value === stepKey
|
||||||
|
|| pollingStepKey.value === stepKey
|
||||||
|
|| isStepProgressHolding(stepKey)
|
||||||
|
|| shouldKeepLocalProgress(stepKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayStepStatus(stepKey, state) {
|
||||||
|
const rawStatus = state?.status || 'pending'
|
||||||
|
if (rawStatus !== 'error' && isStepProgressActive(stepKey, rawStatus)) {
|
||||||
|
return 'running'
|
||||||
|
}
|
||||||
|
return rawStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSimulatedRunningProgress(stepKey) {
|
||||||
|
const startedAt = runningStepStartedAt.value[stepKey] || progressClock.value
|
||||||
|
const elapsed = Math.max(0, progressClock.value - startedAt)
|
||||||
|
const easedProgress = 4 + (91 - 4) * (1 - Math.exp(-elapsed / 7200))
|
||||||
|
return Math.min(91, Math.round(easedProgress))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayStepProgress(stepKey, state) {
|
||||||
|
const status = getDisplayStepStatus(stepKey, state)
|
||||||
|
const rawProgress = normalizeStepProgress(state?.progress)
|
||||||
|
if (status === 'running') return getSimulatedRunningProgress(stepKey)
|
||||||
|
if (status === 'completed') return 100
|
||||||
|
return rawProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCompletedProgressText(rawText) {
|
||||||
|
const cleaned = String(rawText || '')
|
||||||
|
.replace(/\s*[·•]?\s*\d+%\s*$/u, '')
|
||||||
|
.trim()
|
||||||
|
return cleaned || '已完成'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepProgressText(stepKey, state, progress) {
|
||||||
|
const status = getDisplayStepStatus(stepKey, state)
|
||||||
|
const rawText = String(state?.progress_text || '').trim()
|
||||||
|
if (status === 'running') return `正在处理中 · ${progress}%`
|
||||||
|
if (status === 'completed') return normalizeCompletedProgressText(rawText)
|
||||||
|
if (status === 'error') return rawText || '执行失败'
|
||||||
|
return rawText || '等待执行'
|
||||||
|
}
|
||||||
|
|
||||||
function getActiveRunningStepKey() {
|
function getActiveRunningStepKey() {
|
||||||
return runningStepKey.value || pollingStepKey.value || findRunningStepKey()
|
return runningStepKey.value || pollingStepKey.value || findRunningStepKey()
|
||||||
}
|
}
|
||||||
@@ -867,7 +1082,7 @@ function handleCanvasPointerDown(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fitCanvas() {
|
function fitCanvas() {
|
||||||
flowInstance.value?.fitView?.({ padding: 0.04, duration: 250, maxZoom: 1.08 })
|
flowInstance.value?.fitView?.({ padding: 0.035, duration: 250, maxZoom: 1.14 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNodeExpanded(stepKey) {
|
function isNodeExpanded(stepKey) {
|
||||||
@@ -898,6 +1113,33 @@ function getNodeBubbleWidth(stepKey) {
|
|||||||
return 500
|
return 500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flowNodeWidth = 360
|
||||||
|
const flowLeftX = 80
|
||||||
|
const flowTopY = 150
|
||||||
|
const flowBottomY = 470
|
||||||
|
const flowColumnGap = 470
|
||||||
|
|
||||||
|
function isBottomRowStep(stepKey) {
|
||||||
|
return [stepKeyImagePrompts, stepKeyImages, stepKeyPreview, stepKeyPageMD].includes(stepKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFlowNodePosition(stepKey) {
|
||||||
|
const topRow = {
|
||||||
|
[stepKeyConfig]: { x: flowLeftX + flowColumnGap * 0, y: flowTopY },
|
||||||
|
[stepKeyTopic]: { x: flowLeftX + flowColumnGap * 1, y: flowTopY },
|
||||||
|
[stepKeyTitle]: { x: flowLeftX + flowColumnGap * 2, y: flowTopY },
|
||||||
|
[stepKeyOutline]: { x: flowLeftX + flowColumnGap * 3, y: flowTopY },
|
||||||
|
[stepKeyContent]: { x: flowLeftX + flowColumnGap * 4, y: flowTopY },
|
||||||
|
}
|
||||||
|
const bottomRow = {
|
||||||
|
[stepKeyImagePrompts]: { x: flowLeftX + flowColumnGap * 4, y: flowBottomY },
|
||||||
|
[stepKeyImages]: { x: flowLeftX + flowColumnGap * 3, y: flowBottomY },
|
||||||
|
[stepKeyPreview]: { x: flowLeftX + flowColumnGap * 2, y: flowBottomY },
|
||||||
|
[stepKeyPageMD]: { x: flowLeftX + flowColumnGap * 1, y: flowBottomY },
|
||||||
|
}
|
||||||
|
return topRow[stepKey] || bottomRow[stepKey] || { x: flowLeftX, y: flowTopY }
|
||||||
|
}
|
||||||
|
|
||||||
function getPromptSlotLabel(item, index) {
|
function getPromptSlotLabel(item, index) {
|
||||||
if (item?.key === 'cover' || index === 0) return '封面配图'
|
if (item?.key === 'cover' || index === 0) return '封面配图'
|
||||||
return `内容配图 ${index}`
|
return `内容配图 ${index}`
|
||||||
@@ -911,6 +1153,129 @@ function getPromptPurpose(item, index) {
|
|||||||
return `用于“${sectionName}”这一段的说明配图,帮助读者更直观理解内容`
|
return `用于“${sectionName}”这一段的说明配图,帮助读者更直观理解内容`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getImagePreviewKey(item) {
|
||||||
|
return `${item?.key || 'image'}::${item?.url || ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImagePreviewStatus(item) {
|
||||||
|
const status = imagePreviewStates.value[getImagePreviewKey(item)]
|
||||||
|
if (!status) return { state: 'loading', label: '预览加载中', message: '' }
|
||||||
|
if (status.state === 'loaded') return { state: 'loaded', label: '预览成功', message: '' }
|
||||||
|
if (status.state === 'error') return { state: 'error', label: '预览失败', message: status.message || '图片预览失败,建议重新生成' }
|
||||||
|
return { state: 'loading', label: '预览加载中', message: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImagePreviewSource(item) {
|
||||||
|
return imagePreviewSources.value[getImagePreviewKey(item)] || item?.url || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageExportSource(item) {
|
||||||
|
return imageExportSources.value[getImagePreviewKey(item)] || item?.url || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function blobToDataURL(blob) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => resolve(typeof reader.result === 'string' ? reader.result : '')
|
||||||
|
reader.onerror = () => reject(new Error('图片转 data URL 失败'))
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function findImagePreviewItemByUrl(rawUrl) {
|
||||||
|
const normalized = String(rawUrl || '').trim()
|
||||||
|
if (!normalized) return null
|
||||||
|
return imagePreviewItems.value.find(item => String(item?.url || '').trim() === normalized) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacePreviewHtmlImageSources(rawHtml = '', mode = 'display') {
|
||||||
|
if (!String(rawHtml || '').trim()) return ''
|
||||||
|
if (typeof window === 'undefined' || typeof DOMParser === 'undefined') return rawHtml
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(rawHtml, 'text/html')
|
||||||
|
doc.querySelectorAll('img').forEach((img) => {
|
||||||
|
const rawSrc = String(img.getAttribute('src') || '').trim()
|
||||||
|
const item = findImagePreviewItemByUrl(rawSrc)
|
||||||
|
if (!item) return
|
||||||
|
const nextSrc = mode === 'export' ? getImageExportSource(item) : getImagePreviewSource(item)
|
||||||
|
if (nextSrc) {
|
||||||
|
img.setAttribute('src', nextSrc)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return doc.body.innerHTML || rawHtml
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeImagePreviewSources() {
|
||||||
|
Object.values(imagePreviewSources.value).forEach((value) => {
|
||||||
|
if (typeof value === 'string' && value.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
imagePreviewSources.value = {}
|
||||||
|
imageExportSources.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshImagePreviews(items = imagePreviewItems.value) {
|
||||||
|
revokeImagePreviewSources()
|
||||||
|
imagePreviewStates.value = {}
|
||||||
|
const list = Array.isArray(items) ? items : []
|
||||||
|
if (!list.length) return
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token') || ''
|
||||||
|
const nextSources = {}
|
||||||
|
const nextExportSources = {}
|
||||||
|
const nextStates = {}
|
||||||
|
|
||||||
|
await Promise.all(list.map(async (item) => {
|
||||||
|
const previewKey = getImagePreviewKey(item)
|
||||||
|
nextStates[previewKey] = { state: 'loading', message: '' }
|
||||||
|
const rawUrl = String(item?.url || '').trim()
|
||||||
|
if (!rawUrl) {
|
||||||
|
nextStates[previewKey] = { state: 'error', message: '没有拿到图片地址,建议重新生成' }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (/^https?:\/\//i.test(rawUrl) || rawUrl.startsWith('data:image/')) {
|
||||||
|
nextSources[previewKey] = rawUrl
|
||||||
|
nextExportSources[previewKey] = rawUrl
|
||||||
|
} else {
|
||||||
|
const response = await fetch(rawUrl, {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`图片接口返回 ${response.status}`)
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
nextSources[previewKey] = URL.createObjectURL(blob)
|
||||||
|
nextExportSources[previewKey] = await blobToDataURL(blob)
|
||||||
|
}
|
||||||
|
nextStates[previewKey] = { state: 'loaded', message: '' }
|
||||||
|
} catch (error) {
|
||||||
|
nextStates[previewKey] = {
|
||||||
|
state: 'error',
|
||||||
|
message: error?.message || '图片预览失败,建议重新生成',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
imagePreviewSources.value = nextSources
|
||||||
|
imageExportSources.value = nextExportSources
|
||||||
|
imagePreviewStates.value = nextStates
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensurePreviewExportSources(items = imagePreviewItems.value) {
|
||||||
|
const list = Array.isArray(items) ? items : []
|
||||||
|
if (!list.length) return
|
||||||
|
const hasMissingProtectedImage = list.some((item) => {
|
||||||
|
const rawUrl = String(item?.url || '').trim()
|
||||||
|
if (!rawUrl || /^https?:\/\//i.test(rawUrl) || rawUrl.startsWith('data:image/')) return false
|
||||||
|
return !imageExportSources.value[getImagePreviewKey(item)]
|
||||||
|
})
|
||||||
|
if (hasMissingProtectedImage) {
|
||||||
|
await refreshImagePreviews(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleNodePanelWheel() {
|
function handleNodePanelWheel() {
|
||||||
canvasZoomOnScroll.value = false
|
canvasZoomOnScroll.value = false
|
||||||
}
|
}
|
||||||
@@ -934,8 +1299,40 @@ function getProgressEdgeGeometry(edgeProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEdgeBubblePosition(sourceKey, targetKey, edgeProps) {
|
||||||
|
const sourceNode = getFlowNodePosition(sourceKey)
|
||||||
|
const targetNode = getFlowNodePosition(targetKey)
|
||||||
|
const sourceIsBottomRow = isBottomRowStep(sourceKey)
|
||||||
|
const targetIsBottomRow = isBottomRowStep(targetKey)
|
||||||
|
|
||||||
|
if (sourceNode.y === targetNode.y) {
|
||||||
|
const sourceEdgeX = sourceIsBottomRow ? sourceNode.x : sourceNode.x + flowNodeWidth
|
||||||
|
const targetEdgeX = targetIsBottomRow ? targetNode.x + flowNodeWidth : targetNode.x
|
||||||
|
return {
|
||||||
|
x: (sourceEdgeX + targetEdgeX) / 2,
|
||||||
|
y: edgeProps.labelY || (edgeProps.sourceY + edgeProps.targetY) / 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceKey === stepKeyContent && targetKey === stepKeyImagePrompts) {
|
||||||
|
return {
|
||||||
|
x: sourceNode.x + flowNodeWidth + 34,
|
||||||
|
y: (edgeProps.sourceY + edgeProps.targetY) / 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function getEdgeBubbleStyle(edgeProps) {
|
function getEdgeBubbleStyle(edgeProps) {
|
||||||
const { labelX, labelY } = getProgressEdgeGeometry(edgeProps)
|
const geometry = getProgressEdgeGeometry(edgeProps)
|
||||||
|
const bubblePosition = getEdgeBubblePosition(edgeProps.source, edgeProps.target, {
|
||||||
|
...edgeProps,
|
||||||
|
labelX: geometry.labelX,
|
||||||
|
labelY: geometry.labelY,
|
||||||
|
})
|
||||||
|
const labelX = bubblePosition?.x ?? geometry.labelX
|
||||||
|
const labelY = bubblePosition?.y ?? geometry.labelY
|
||||||
return {
|
return {
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY - 18}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY - 18}px)`,
|
||||||
}
|
}
|
||||||
@@ -960,8 +1357,8 @@ function normalizeStepProgress(progress) {
|
|||||||
|
|
||||||
function getEdgeProgressMeta(sourceKey, targetKey) {
|
function getEdgeProgressMeta(sourceKey, targetKey) {
|
||||||
const targetStep = workflow.value?.steps?.[targetKey]
|
const targetStep = workflow.value?.steps?.[targetKey]
|
||||||
const targetStatus = targetStep?.status || 'pending'
|
const targetStatus = getDisplayStepStatus(targetKey, targetStep)
|
||||||
const progress = normalizeStepProgress(targetStep?.progress)
|
const progress = getDisplayStepProgress(targetKey, targetStep)
|
||||||
const labelPrefix = stepShortLabel(targetKey)
|
const labelPrefix = stepShortLabel(targetKey)
|
||||||
const sourceCompleted = sourceKey === stepKeyConfig || workflow.value?.steps?.[sourceKey]?.status === 'completed'
|
const sourceCompleted = sourceKey === stepKeyConfig || workflow.value?.steps?.[sourceKey]?.status === 'completed'
|
||||||
|
|
||||||
@@ -1006,6 +1403,7 @@ async function createTask() {
|
|||||||
outline_style: taskForm.outlineStyle,
|
outline_style: taskForm.outlineStyle,
|
||||||
outline_words: taskForm.outlineWords,
|
outline_words: taskForm.outlineWords,
|
||||||
content_target_words: taskForm.contentTargetWords,
|
content_target_words: taskForm.contentTargetWords,
|
||||||
|
max_content_images: Number(taskForm.maxContentImages) || 0,
|
||||||
})
|
})
|
||||||
const taskData = res?.data?.task
|
const taskData = res?.data?.task
|
||||||
if (!taskData?.id) throw new Error('任务创建成功,但未返回任务 ID')
|
if (!taskData?.id) throw new Error('任务创建成功,但未返回任务 ID')
|
||||||
@@ -1033,6 +1431,7 @@ async function loadWorkflow(taskId = selectedTaskId.value, options = {}) {
|
|||||||
try {
|
try {
|
||||||
const res = await getOfficialAccountWorkflow(taskId)
|
const res = await getOfficialAccountWorkflow(taskId)
|
||||||
workflowDetail.value = res?.data || null
|
workflowDetail.value = res?.data || null
|
||||||
|
syncRunningStepProgress(res?.data?.workflow)
|
||||||
syncSelectionsFromWorkflow()
|
syncSelectionsFromWorkflow()
|
||||||
if (!preserveNodeState) {
|
if (!preserveNodeState) {
|
||||||
selectedNodeKey.value = workflow.value?.current_step || stepKeyTopic
|
selectedNodeKey.value = workflow.value?.current_step || stepKeyTopic
|
||||||
@@ -1077,6 +1476,7 @@ function syncSelectionsFromWorkflow() {
|
|||||||
configForm.outlineStyle = workflow.value?.form?.outline_style || '问题拆解型'
|
configForm.outlineStyle = workflow.value?.form?.outline_style || '问题拆解型'
|
||||||
configForm.outlineWords = workflow.value?.form?.outline_words || 600
|
configForm.outlineWords = workflow.value?.form?.outline_words || 600
|
||||||
configForm.contentTargetWords = workflow.value?.form?.content_target_words || 1400
|
configForm.contentTargetWords = workflow.value?.form?.content_target_words || 1400
|
||||||
|
configForm.maxContentImages = Number(workflow.value?.form?.max_content_images) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTaskConfig() {
|
async function saveTaskConfig() {
|
||||||
@@ -1100,6 +1500,7 @@ async function saveTaskConfig() {
|
|||||||
outline_style: configForm.outlineStyle,
|
outline_style: configForm.outlineStyle,
|
||||||
outline_words: Number(configForm.outlineWords) || 600,
|
outline_words: Number(configForm.outlineWords) || 600,
|
||||||
content_target_words: Number(configForm.contentTargetWords) || 1400,
|
content_target_words: Number(configForm.contentTargetWords) || 1400,
|
||||||
|
max_content_images: Number(configForm.maxContentImages) || 0,
|
||||||
})
|
})
|
||||||
workflowDetail.value = res?.data || null
|
workflowDetail.value = res?.data || null
|
||||||
syncSelectionsFromWorkflow()
|
syncSelectionsFromWorkflow()
|
||||||
@@ -1162,6 +1563,7 @@ async function runStep(stepKey) {
|
|||||||
try {
|
try {
|
||||||
runningStepKey.value = stepKey
|
runningStepKey.value = stepKey
|
||||||
pollingStepKey.value = stepKey
|
pollingStepKey.value = stepKey
|
||||||
|
markStepRunning(stepKey)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
ElMessage.success(`${stepLabel(stepKey)}已开始执行`)
|
ElMessage.success(`${stepLabel(stepKey)}已开始执行`)
|
||||||
await executeOfficialAccountWorkflowStep(selectedTaskId.value, stepKey, {
|
await executeOfficialAccountWorkflowStep(selectedTaskId.value, stepKey, {
|
||||||
@@ -1175,6 +1577,7 @@ async function runStep(stepKey) {
|
|||||||
await loadWorkflow(selectedTaskId.value, { silent: true, preserveNodeState: true, syncRuntime: false })
|
await loadWorkflow(selectedTaskId.value, { silent: true, preserveNodeState: true, syncRuntime: false })
|
||||||
await pollWorkflowUntilStepSettled(String(selectedTaskId.value), stepKey, { notifyOnFinish: true })
|
await pollWorkflowUntilStepSettled(String(selectedTaskId.value), stepKey, { notifyOnFinish: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
releaseStepProgressState(stepKey)
|
||||||
runningStepKey.value = ''
|
runningStepKey.value = ''
|
||||||
pollingStepKey.value = ''
|
pollingStepKey.value = ''
|
||||||
ElMessage.error(error?.message || `${stepLabel(stepKey)}执行失败`)
|
ElMessage.error(error?.message || `${stepLabel(stepKey)}执行失败`)
|
||||||
@@ -1184,15 +1587,20 @@ async function runStep(stepKey) {
|
|||||||
|
|
||||||
function findRunningStepKey(workflowState = workflow.value) {
|
function findRunningStepKey(workflowState = workflow.value) {
|
||||||
const steps = workflowState?.steps || {}
|
const steps = workflowState?.steps || {}
|
||||||
return [stepKeyTopic, stepKeyTitle, stepKeyOutline, stepKeyContent, stepKeyImagePrompts, stepKeyImages, stepKeyPreview, stepKeyPageMD]
|
return workflowStepKeys.find(key => steps?.[key]?.status === 'running') || ''
|
||||||
.find(key => steps?.[key]?.status === 'running') || ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadExport(format, filename) {
|
async function downloadExport(format, filename) {
|
||||||
if (!selectedTaskId.value) return
|
if (!selectedTaskId.value) return
|
||||||
try {
|
try {
|
||||||
exportingFormat.value = format
|
exportingFormat.value = format
|
||||||
const blob = await exportOfficialAccountDocument(selectedTaskId.value, format)
|
let blob = await exportOfficialAccountDocument(selectedTaskId.value, format)
|
||||||
|
if (format === 'preview_html') {
|
||||||
|
await ensurePreviewExportSources()
|
||||||
|
const rawHtml = await blob.text()
|
||||||
|
const resolvedHtml = replacePreviewHtmlImageSources(rawHtml, 'export')
|
||||||
|
blob = new Blob([resolvedHtml], { type: 'text/html;charset=utf-8' })
|
||||||
|
}
|
||||||
const url = window.URL.createObjectURL(blob)
|
const url = window.URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = url
|
link.href = url
|
||||||
@@ -1215,6 +1623,7 @@ async function pollWorkflowUntilStepSettled(taskId, stepKey, options = {}) {
|
|||||||
pollingToken.value = token
|
pollingToken.value = token
|
||||||
pollingStepKey.value = stepKey
|
pollingStepKey.value = stepKey
|
||||||
runningStepKey.value = stepKey
|
runningStepKey.value = stepKey
|
||||||
|
markStepRunning(stepKey)
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 240; attempt += 1) {
|
for (let attempt = 0; attempt < 240; attempt += 1) {
|
||||||
await wait(2000)
|
await wait(2000)
|
||||||
@@ -1224,6 +1633,14 @@ async function pollWorkflowUntilStepSettled(taskId, stepKey, options = {}) {
|
|||||||
await loadWorkflow(taskId, { silent: true, preserveNodeState: true, syncRuntime: false })
|
await loadWorkflow(taskId, { silent: true, preserveNodeState: true, syncRuntime: false })
|
||||||
const status = workflow.value?.steps?.[stepKey]?.status
|
const status = workflow.value?.steps?.[stepKey]?.status
|
||||||
if (status && status !== 'running') {
|
if (status && status !== 'running') {
|
||||||
|
if (status === 'completed') {
|
||||||
|
holdStepProgress(stepKey)
|
||||||
|
if (!isStepProgressHolding(stepKey)) {
|
||||||
|
releaseStepProgressState(stepKey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
releaseStepProgressState(stepKey)
|
||||||
|
}
|
||||||
pollingStepKey.value = ''
|
pollingStepKey.value = ''
|
||||||
runningStepKey.value = ''
|
runningStepKey.value = ''
|
||||||
await workerRuntime.loadTasks(workspaceKey.value)
|
await workerRuntime.loadTasks(workspaceKey.value)
|
||||||
@@ -1240,6 +1657,7 @@ async function pollWorkflowUntilStepSettled(taskId, stepKey, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pollingToken.value === token) {
|
if (pollingToken.value === token) {
|
||||||
|
releaseStepProgressState(stepKey)
|
||||||
pollingStepKey.value = ''
|
pollingStepKey.value = ''
|
||||||
runningStepKey.value = ''
|
runningStepKey.value = ''
|
||||||
ElMessage.warning(`${stepLabel(stepKey)}仍在后台执行,请稍后刷新查看结果`)
|
ElMessage.warning(`${stepLabel(stepKey)}仍在后台执行,请稍后刷新查看结果`)
|
||||||
@@ -1302,9 +1720,25 @@ watch(selectedTaskId, () => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(imagePreviewItems, (items) => {
|
||||||
|
void refreshImagePreviews(items)
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
progressClockTimer = window.setInterval(() => {
|
||||||
|
progressClock.value = Date.now()
|
||||||
|
pruneExpiredStepProgressState(progressClock.value)
|
||||||
|
}, 450)
|
||||||
await workerRuntime.loadTasks(workspaceKey.value)
|
await workerRuntime.loadTasks(workspaceKey.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (progressClockTimer) {
|
||||||
|
window.clearInterval(progressClockTimer)
|
||||||
|
progressClockTimer = null
|
||||||
|
}
|
||||||
|
revokeImagePreviewSources()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -1463,8 +1897,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.left-node-status.status-completed {
|
.left-node-status.status-completed {
|
||||||
background: rgba(103, 194, 58, 0.12);
|
background: rgba(34, 197, 94, 0.18);
|
||||||
color: #409800;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-node-status.status-running {
|
.left-node-status.status-running {
|
||||||
@@ -1785,7 +2219,37 @@ onMounted(async () => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-status-chip.is-loading {
|
||||||
|
background: rgba(37, 99, 235, 0.1);
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-status-chip.is-loaded {
|
||||||
|
background: rgba(34, 197, 94, 0.12);
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-status-chip.is-error {
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.image-thumb {
|
.image-thumb {
|
||||||
|
margin-top: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@@ -1794,6 +2258,23 @@ onMounted(async () => {
|
|||||||
background: #e2e8f0;
|
background: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-thumb-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb-placeholder.is-error {
|
||||||
|
border-color: rgba(248, 113, 113, 0.45);
|
||||||
|
background: rgba(254, 242, 242, 0.9);
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.action-row {
|
.action-row {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1911,7 +2392,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oa-node.status-completed {
|
.oa-node.status-completed {
|
||||||
background: #f6fff7;
|
background: #eefbf1;
|
||||||
|
border-color: rgba(134, 239, 172, 0.78);
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-node.status-running {
|
.oa-node.status-running {
|
||||||
@@ -1944,8 +2426,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oa-node-badge.status-completed {
|
.oa-node-badge.status-completed {
|
||||||
background: rgba(103, 194, 58, 0.12);
|
background: rgba(34, 197, 94, 0.18);
|
||||||
color: #409800;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-node-badge.status-running {
|
.oa-node-badge.status-running {
|
||||||
@@ -1985,6 +2467,14 @@ onMounted(async () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background: #2563eb;
|
background: #2563eb;
|
||||||
|
transition: width 0.45s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-node-progress-fill.is-running {
|
||||||
|
background: linear-gradient(90deg, #2563eb 0%, #60a5fa 45%, #93c5fd 55%, #2563eb 100%);
|
||||||
|
background-size: 220% 100%;
|
||||||
|
animation: oa-progress-flow 1.6s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-node-foot {
|
.oa-node-foot {
|
||||||
@@ -2001,6 +2491,16 @@ onMounted(async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes oa-progress-flow {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -20% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.node-expand-button {
|
.node-expand-button {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
@@ -2111,9 +2611,9 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oa-edge-bubble.is-completed {
|
.oa-edge-bubble.is-completed {
|
||||||
border-color: rgba(134, 239, 172, 0.95);
|
border-color: rgba(74, 222, 128, 0.95);
|
||||||
background: rgba(240, 253, 244, 0.96);
|
background: rgba(220, 252, 231, 0.98);
|
||||||
color: #15803d;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oa-edge-bubble.is-error {
|
.oa-edge-bubble.is-error {
|
||||||
@@ -2141,6 +2641,14 @@ onMounted(async () => {
|
|||||||
border: 2px solid #3b82f6 !important;
|
border: 2px solid #3b82f6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__node) {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__node.node-expanded) {
|
||||||
|
z-index: 40 !important;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.vue-flow__controls),
|
:deep(.vue-flow__controls),
|
||||||
:deep(.vue-flow__minimap),
|
:deep(.vue-flow__minimap),
|
||||||
:deep(.vue-flow__attribution) {
|
:deep(.vue-flow__attribution) {
|
||||||
|
|||||||
Reference in New Issue
Block a user