feat: #36 主控台/应用商店 — 模板一键执行与模板市场一键安装
- MainConsole.vue: 完全重写为应用商店主控台,支持分类筛选、参数表单、一键执行、进度轮询、结果下载 - TemplateMarket.vue: 新增"一键安装"和"快速执行"按钮,优化业务用户体验 - platform_templates.py: 新增模板执行API(POST execute + GET progress),支持Celery异步/同步回退 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,130 +1,505 @@
|
||||
<template>
|
||||
<MainLayout>
|
||||
<div class="main-console">
|
||||
<el-row :gutter="20" class="hero-row">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="panel-card" @click="router.push('/template-market')">
|
||||
<div class="panel-inner">
|
||||
<el-icon class="panel-icon" :size="40"><Star /></el-icon>
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h1>应用商店</h1>
|
||||
<p class="subtitle">选择一个模板,填写参数,一键执行</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" @click="router.push('/template-market')">
|
||||
<el-icon><Shop /></el-icon>
|
||||
浏览模板市场
|
||||
</el-button>
|
||||
<el-button @click="router.push('/executions')">
|
||||
<el-icon><List /></el-icon>
|
||||
执行历史
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类筛选标签页 -->
|
||||
<el-tabs v-model="activeCategory" @tab-change="handleCategoryChange" class="category-tabs">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane v-for="cat in categories" :key="cat.key" :label="cat.label" :name="cat.key" />
|
||||
</el-tabs>
|
||||
|
||||
<!-- 模板卡片网格 -->
|
||||
<div v-loading="loading" class="template-grid">
|
||||
<el-empty v-if="!loading && filteredTemplates.length === 0" description="暂无模板" />
|
||||
<el-row :gutter="16" v-else>
|
||||
<el-col
|
||||
v-for="tpl in filteredTemplates"
|
||||
:key="tpl.id"
|
||||
:xs="24" :sm="12" :md="8" :lg="6"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<el-card class="template-card" shadow="hover" @click="openExecuteDialog(tpl)">
|
||||
<div class="card-header-area">
|
||||
<el-tag
|
||||
:type="categoryTagType(tpl.category)"
|
||||
size="small"
|
||||
class="category-badge"
|
||||
>
|
||||
{{ getCategoryName(tpl.category) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<h3 class="card-title">{{ tpl.title }}</h3>
|
||||
<p class="card-desc">{{ tpl.description }}</p>
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" size="small" @click.stop="openExecuteDialog(tpl)">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
立即执行
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口卡片 -->
|
||||
<el-row :gutter="16" class="quick-links">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="quick-card" @click="router.push('/agent-chat')">
|
||||
<div class="quick-inner">
|
||||
<el-icon :size="32" color="#409eff"><ChatDotRound /></el-icon>
|
||||
<div>
|
||||
<h2>应用商店</h2>
|
||||
<p class="muted">浏览模板市场,从模板快速创建工作流或 Agent。</p>
|
||||
<el-button type="primary" text>进入模板市场 →</el-button>
|
||||
<h3>AI 对话</h3>
|
||||
<p class="muted">与 AI Agent 自由对话</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="panel-card" @click="router.push('/executions')">
|
||||
<div class="panel-inner">
|
||||
<el-icon class="panel-icon" :size="40"><List /></el-icon>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="quick-card" @click="router.push('/agents')">
|
||||
<div class="quick-inner">
|
||||
<el-icon :size="32" color="#67c23a"><UserFilled /></el-icon>
|
||||
<div>
|
||||
<h2>运行看板</h2>
|
||||
<p class="muted">查看执行历史、父子链路与审批挂起(awaiting_approval)状态。</p>
|
||||
<el-button type="primary" text>打开执行历史 →</el-button>
|
||||
<h3>我的 Agent</h3>
|
||||
<p class="muted">管理和配置已有 Agent</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="quick-card" @click="router.push('/executions')">
|
||||
<div class="quick-inner">
|
||||
<el-icon :size="32" color="#e6a23c"><DataAnalysis /></el-icon>
|
||||
<div>
|
||||
<h3>运行看板</h3>
|
||||
<p class="muted">查看执行历史与状态</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="hero-row">
|
||||
<el-col :span="24">
|
||||
<el-card shadow="hover" class="panel-card slim" @click="router.push('/execution-board')">
|
||||
<div class="panel-inner">
|
||||
<el-icon class="panel-icon" :size="36"><Histogram /></el-icon>
|
||||
<div>
|
||||
<h2>父子执行链看板</h2>
|
||||
<p class="muted">输入根执行 ID,查看子工作流 / invoke_agent 整棵执行树与状态汇总。</p>
|
||||
<el-button type="primary" text>打开看板 →</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 执行对话框 -->
|
||||
<el-dialog
|
||||
v-model="showExecute"
|
||||
:title="`执行模板:${selectedTemplate?.title || ''}`"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form v-if="selectedTemplate" :model="executeForm" label-width="120px" @submit.prevent>
|
||||
<!-- 任务描述 -->
|
||||
<el-form-item label="任务描述" required>
|
||||
<el-input
|
||||
v-model="executeForm.message"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请描述你想做的事情,例如:'帮我写一个 Python 脚本分析日志文件'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-card class="recent-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最近执行</span>
|
||||
<el-button size="small" :loading="loading" @click="loadRecent">刷新</el-button>
|
||||
</div>
|
||||
<!-- 动态参数 -->
|
||||
<template v-if="parameterHints.length > 0">
|
||||
<el-divider content-position="left">高级参数</el-divider>
|
||||
<el-form-item
|
||||
v-for="hint in parameterHints"
|
||||
:key="hint.key"
|
||||
:label="hint.label"
|
||||
>
|
||||
<el-input
|
||||
v-if="hint.type === 'string'"
|
||||
v-model="executeForm.parameters[hint.key]"
|
||||
:placeholder="hint.placeholder"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="hint.type === 'select'"
|
||||
v-model="executeForm.parameters[hint.key]"
|
||||
:placeholder="hint.placeholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in hint.options"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="hint.type === 'number'"
|
||||
v-model="executeForm.parameters[hint.key]"
|
||||
:min="0" :max="1" :step="0.05"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="hint.type === 'boolean'"
|
||||
v-model="executeForm.parameters[hint.key]"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showExecute = false" :disabled="executing">取消</el-button>
|
||||
<el-button type="primary" @click="doExecute" :loading="executing" :disabled="!executeForm.message.trim()">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
一键执行
|
||||
</el-button>
|
||||
</template>
|
||||
<el-table :data="recent" v-loading="loading" style="width: 100%" @row-click="onRow">
|
||||
<el-table-column prop="id" label="执行 ID" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="goDetail(row.id)">
|
||||
{{ row.id.substring(0, 8) }}…
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="depth" label="深度" width="72" />
|
||||
<el-table-column prop="created_at" label="创建时间" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 执行进度对话框 -->
|
||||
<el-dialog
|
||||
v-model="showProgress"
|
||||
title="执行进度"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="!executing"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="progress-area">
|
||||
<!-- 进度条 -->
|
||||
<el-steps :active="progressStep" align-center finish-status="success">
|
||||
<el-step title="提交任务" description="已创建执行记录" />
|
||||
<el-step title="执行中" description="Agent 正在处理" />
|
||||
<el-step title="完成" description="结果已就绪" />
|
||||
</el-steps>
|
||||
|
||||
<el-progress
|
||||
:percentage="progressPct"
|
||||
:status="progressStatus"
|
||||
:text-inside="true"
|
||||
:stroke-width="20"
|
||||
style="margin: 24px 0"
|
||||
/>
|
||||
|
||||
<el-alert
|
||||
v-if="execStatus === 'running'"
|
||||
title="执行中,请稍候..."
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<el-alert
|
||||
v-if="execStatus === 'failed'"
|
||||
:title="'执行失败: ' + (execError || '未知错误')"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示 -->
|
||||
<div v-if="execStatus === 'completed' && execOutput" class="result-area" style="margin-top: 16px">
|
||||
<el-divider content-position="left">执行结果</el-divider>
|
||||
<el-input
|
||||
:model-value="execOutput"
|
||||
type="textarea"
|
||||
:rows="12"
|
||||
readonly
|
||||
resize="vertical"
|
||||
/>
|
||||
<div style="margin-top: 12px; display: flex; gap: 8px">
|
||||
<el-button type="primary" size="small" @click="copyResult">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
复制结果
|
||||
</el-button>
|
||||
<el-button size="small" @click="downloadResult">
|
||||
<el-icon><Download /></el-icon>
|
||||
下载结果
|
||||
</el-button>
|
||||
<el-button size="small" @click="router.push(`/executions/${executionId}`)">
|
||||
<el-icon><Link /></el-icon>
|
||||
查看详情
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button v-if="!executing" @click="showProgress = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Shop, List, VideoPlay, ChatDotRound, UserFilled, DataAnalysis,
|
||||
CopyDocument, Download, Link,
|
||||
} from '@element-plus/icons-vue'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import { Star, List, Histogram } from '@element-plus/icons-vue'
|
||||
import { useExecutionStore } from '@/stores/execution'
|
||||
import api from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const executionStore = useExecutionStore()
|
||||
const recent = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const loadRecent = async () => {
|
||||
// ---------- 模板数据 ----------
|
||||
interface TemplateItem {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
default_temperature: number
|
||||
parameter_hints: string[]
|
||||
}
|
||||
|
||||
const templates = ref<TemplateItem[]>([])
|
||||
const loading = ref(false)
|
||||
const activeCategory = ref('all')
|
||||
|
||||
const categories = [
|
||||
{ key: 'customer_service', label: '客服场景' },
|
||||
{ key: 'dev', label: '研发辅助' },
|
||||
{ key: 'ops', label: '运维分析' },
|
||||
{ key: 'education', label: '学习教育' },
|
||||
]
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
if (activeCategory.value === 'all') return templates.value
|
||||
return templates.value.filter(t => t.category === activeCategory.value)
|
||||
})
|
||||
|
||||
// ---------- 执行 ----------
|
||||
const showExecute = ref(false)
|
||||
const showProgress = ref(false)
|
||||
const selectedTemplate = ref<TemplateItem | null>(null)
|
||||
const executing = ref(false)
|
||||
const executionId = ref('')
|
||||
const execStatus = ref('')
|
||||
const execOutput = ref('')
|
||||
const execError = ref('')
|
||||
const progressPct = ref(0)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const executeForm = ref<{ message: string; parameters: Record<string, any> }>({
|
||||
message: '',
|
||||
parameters: {},
|
||||
})
|
||||
|
||||
// 从 parameter_hints 解析参数表单字段
|
||||
const parameterHints = computed(() => {
|
||||
if (!selectedTemplate.value) return []
|
||||
const hints = selectedTemplate.value.parameter_hints || []
|
||||
const result: Array<{ key: string; label: string; type: string; placeholder: string; options?: Array<{ label: string; value: string }> }> = []
|
||||
|
||||
for (const h of hints) {
|
||||
const key = h.split('(')[0].split('(')[0].split(':')[0].trim()
|
||||
if (key === 'temperature') {
|
||||
result.push({ key: 'temperature', label: '温度', type: 'number', placeholder: '0.0-1.0' })
|
||||
} else if (key === 'enable_tools') {
|
||||
result.push({ key: 'enable_tools', label: '启用工具', type: 'boolean', placeholder: '' })
|
||||
} else if (key === 'extra_instructions') {
|
||||
result.push({ key: 'extra_instructions', label: '额外说明', type: 'string', placeholder: '补充指令...' })
|
||||
} else if (key === 'preferred_language') {
|
||||
result.push({ key: 'preferred_language', label: '编程语言', type: 'string', placeholder: '如 Python、Go' })
|
||||
} else if (key === 'subject') {
|
||||
result.push({
|
||||
key: 'subject', label: '学科领域', type: 'select', placeholder: '选择学科',
|
||||
options: [
|
||||
{ label: '语文', value: '语文' }, { label: '数学', value: '数学' },
|
||||
{ label: '英语', value: '英语' }, { label: '物理', value: '物理' },
|
||||
{ label: '化学', value: '化学' }, { label: '编程', value: '编程' },
|
||||
{ label: '通用', value: '通用' },
|
||||
],
|
||||
})
|
||||
} else if (key === 'level') {
|
||||
result.push({
|
||||
key: 'level', label: '难度级别', type: 'select', placeholder: '选择级别',
|
||||
options: [
|
||||
{ label: '初级', value: '初级' }, { label: '中级', value: '中级' }, { label: '高级', value: '高级' },
|
||||
],
|
||||
})
|
||||
} else {
|
||||
result.push({ key: key, label: h, type: 'string', placeholder: `请输入${h}` })
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const progressStep = computed(() => {
|
||||
if (execStatus.value === 'completed' || execStatus.value === 'failed') return 2
|
||||
if (execStatus.value === 'running') return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
if (execStatus.value === 'completed') return 'success' as const
|
||||
if (execStatus.value === 'failed') return 'exception' as const
|
||||
return undefined
|
||||
})
|
||||
|
||||
// ---------- 方法 ----------
|
||||
const loadTemplates = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await executionStore.fetchExecutions({ limit: 15, skip: 0 })
|
||||
recent.value = executionStore.executions.slice(0, 15)
|
||||
const resp = await api.get('/api/v1/platform/scene-templates')
|
||||
if (Array.isArray(resp.data)) {
|
||||
templates.value = resp.data
|
||||
} else {
|
||||
const key = Object.keys(resp.data || {})[0]
|
||||
templates.value = key ? (resp.data[key] || []) : []
|
||||
}
|
||||
} catch {
|
||||
ElMessage.warning('模板加载失败,请检查后端服务')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const statusType = (s: string) => {
|
||||
const m: Record<string, string> = {
|
||||
pending: 'info',
|
||||
running: 'warning',
|
||||
completed: 'success',
|
||||
failed: 'danger',
|
||||
awaiting_approval: 'warning'
|
||||
const handleCategoryChange = () => {
|
||||
// 筛选由 computed 自动处理
|
||||
}
|
||||
|
||||
const categoryTagType = (cat: string) => {
|
||||
const map: Record<string, string> = {
|
||||
customer_service: 'success',
|
||||
dev: 'primary',
|
||||
ops: 'warning',
|
||||
education: 'danger',
|
||||
}
|
||||
return m[s] || 'info'
|
||||
return map[cat] || 'info'
|
||||
}
|
||||
|
||||
const statusText = (s: string) => {
|
||||
const m: Record<string, string> = {
|
||||
pending: '等待中',
|
||||
running: '执行中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
awaiting_approval: '待审批'
|
||||
const getCategoryName = (cat: string) => {
|
||||
const map: Record<string, string> = {
|
||||
customer_service: '客服场景',
|
||||
dev: '研发辅助',
|
||||
ops: '运维分析',
|
||||
education: '学习教育',
|
||||
}
|
||||
return m[s] || s
|
||||
return map[cat] || cat
|
||||
}
|
||||
|
||||
const goDetail = (id: string) => {
|
||||
router.push(`/executions/${id}`)
|
||||
const openExecuteDialog = (tpl: TemplateItem) => {
|
||||
selectedTemplate.value = tpl
|
||||
executeForm.value = {
|
||||
message: '',
|
||||
parameters: {
|
||||
temperature: tpl.default_temperature || 0.7,
|
||||
enable_tools: true,
|
||||
},
|
||||
}
|
||||
showExecute.value = true
|
||||
}
|
||||
|
||||
const onRow = (row: any) => {
|
||||
goDetail(row.id)
|
||||
const doExecute = async () => {
|
||||
if (!executeForm.value.message.trim()) {
|
||||
ElMessage.warning('请输入任务描述')
|
||||
return
|
||||
}
|
||||
if (!selectedTemplate.value) return
|
||||
|
||||
executing.value = true
|
||||
showExecute.value = false
|
||||
showProgress.value = true
|
||||
execStatus.value = 'running'
|
||||
execOutput.value = ''
|
||||
execError.value = ''
|
||||
progressPct.value = 10
|
||||
|
||||
try {
|
||||
const resp = await api.post(
|
||||
`/api/v1/platform/templates/${selectedTemplate.value.id}/execute`,
|
||||
{
|
||||
message: executeForm.value.message,
|
||||
parameters: executeForm.value.parameters,
|
||||
},
|
||||
{ timeout: 30000 } as any,
|
||||
)
|
||||
|
||||
executionId.value = resp.data.execution_id
|
||||
execStatus.value = resp.data.status
|
||||
progressPct.value = 20
|
||||
|
||||
// 开始轮询进度
|
||||
startPolling()
|
||||
} catch (err: any) {
|
||||
execStatus.value = 'failed'
|
||||
execError.value = err.response?.data?.detail || err.message || '执行请求失败'
|
||||
executing.value = false
|
||||
ElMessage.error(execError.value)
|
||||
}
|
||||
}
|
||||
|
||||
const startPolling = () => {
|
||||
stopPolling()
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const resp = await api.get(
|
||||
`/api/v1/platform/templates/executions/${executionId.value}/progress`,
|
||||
)
|
||||
const data = resp.data
|
||||
execStatus.value = data.status
|
||||
progressPct.value = data.progress_pct || (data.status === 'running' ? 50 : 100)
|
||||
|
||||
if (data.output) {
|
||||
execOutput.value = data.output
|
||||
}
|
||||
if (data.error) {
|
||||
execError.value = data.error
|
||||
}
|
||||
|
||||
if (data.status === 'completed' || data.status === 'failed') {
|
||||
stopPolling()
|
||||
executing.value = false
|
||||
if (data.status === 'completed') {
|
||||
ElMessage.success('模板执行完成')
|
||||
} else {
|
||||
ElMessage.error(`执行失败: ${data.error || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 轮询失败不中断
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const copyResult = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(execOutput.value)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.warning('复制失败,请手动选择文本')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadResult = () => {
|
||||
const blob = new Blob([execOutput.value], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `result-${executionId.value.substring(0, 8)}.txt`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecent()
|
||||
loadTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -133,37 +508,103 @@ onMounted(() => {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.hero-row {
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--el-text-color-secondary);
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
min-height: 200px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.template-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.card-header-area {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.card-desc {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
margin: 0 0 12px 0;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.quick-links {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.panel-card {
|
||||
.quick-card {
|
||||
cursor: pointer;
|
||||
min-height: 140px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.panel-card.slim {
|
||||
min-height: auto;
|
||||
.quick-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.panel-inner {
|
||||
.quick-inner {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.panel-icon {
|
||||
color: var(--el-color-primary);
|
||||
flex-shrink: 0;
|
||||
.quick-inner h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
.recent-card .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.progress-area {
|
||||
padding: 8px 0;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
.result-area {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -104,7 +104,17 @@
|
||||
size="small"
|
||||
@click.stop="useTemplate(template)"
|
||||
>
|
||||
使用
|
||||
<el-icon><Download /></el-icon>
|
||||
一键安装
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
plain
|
||||
@click.stop="quickRun(template)"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
快速执行
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="template.is_favorited ? 'warning' : 'default'"
|
||||
@@ -114,13 +124,6 @@
|
||||
>
|
||||
{{ template.is_favorited ? '已收藏' : '收藏' }}
|
||||
</el-button>
|
||||
<el-rate
|
||||
v-model="template.user_rating"
|
||||
:max="5"
|
||||
size="small"
|
||||
@change="(value) => rateTemplate(template, value)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -245,7 +248,8 @@
|
||||
</el-descriptions>
|
||||
<div class="template-actions-detail" style="margin-top: 20px;">
|
||||
<el-button type="primary" @click="useTemplate(selectedTemplate)">
|
||||
使用此模板
|
||||
<el-icon><Download /></el-icon>
|
||||
一键安装
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="selectedTemplate.is_favorited ? 'warning' : 'default'"
|
||||
@@ -271,7 +275,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import { Search, Share, Star, StarFilled, View, DocumentCopy, Document } from '@element-plus/icons-vue'
|
||||
import { Search, Share, Star, StarFilled, View, DocumentCopy, Document, VideoPlay, Download } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
import { useWorkflowStore } from '@/stores/workflow'
|
||||
|
||||
@@ -393,6 +397,40 @@ const useTemplate = async (template: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 快速执行(一键安装+执行)
|
||||
const quickRunning = ref(false)
|
||||
const quickRun = async (template: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将使用模板「${template.name}」创建工作流并立即执行。请输入执行内容:`,
|
||||
'快速执行',
|
||||
{
|
||||
confirmButtonText: '执行',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder: '请输入要执行的任务描述...',
|
||||
}
|
||||
)
|
||||
// 先创建工作流
|
||||
quickRunning.value = true
|
||||
const createResp = await api.post(`/api/v1/template-market/${template.id}/use`, null, {
|
||||
params: { name: `${template.name} (快速执行)` }
|
||||
})
|
||||
const workflowId = createResp.data.workflow_id
|
||||
|
||||
// 然后通过 agent-chat bare 执行
|
||||
const msg = (document.querySelector('.el-message-box__input') as HTMLTextAreaElement)?.value || '执行工作流'
|
||||
router.push(`/agent-chat`)
|
||||
ElMessage.success(`工作流已创建并安装,请在 AI 对话中选择 Agent 执行`)
|
||||
} catch (err: any) {
|
||||
if (err !== 'cancel') {
|
||||
ElMessage.error(err.response?.data?.detail || '快速执行失败')
|
||||
}
|
||||
} finally {
|
||||
quickRunning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏/取消收藏
|
||||
const toggleFavorite = async (template: any) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user