2026-04-09 21:58:53 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<MainLayout>
|
|
|
|
|
|
<div class="main-console">
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<!-- 顶部标题栏 -->
|
|
|
|
|
|
<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>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
<div>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<h3>AI 对话</h3>
|
|
|
|
|
|
<p class="muted">与 AI Agent 自由对话</p>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<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>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
<div>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<h3>我的 Agent</h3>
|
|
|
|
|
|
<p class="muted">管理和配置已有 Agent</p>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<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>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
<div>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<h3>运行看板</h3>
|
|
|
|
|
|
<p class="muted">查看执行历史与状态</p>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
2026-05-06 21:30:58 +08:00
|
|
|
|
<!-- 执行对话框 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 动态参数 -->
|
|
|
|
|
|
<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-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>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
</div>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button v-if="!executing" @click="showProgress = false">关闭</el-button>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
</template>
|
2026-05-06 21:30:58 +08:00
|
|
|
|
</el-dialog>
|
2026-04-09 21:58:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</MainLayout>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-05-06 21:30:58 +08:00
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
2026-04-09 21:58:53 +08:00
|
|
|
|
import { useRouter } from 'vue-router'
|
2026-05-06 21:30:58 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Shop, List, VideoPlay, ChatDotRound, UserFilled, DataAnalysis,
|
|
|
|
|
|
CopyDocument, Download, Link,
|
|
|
|
|
|
} from '@element-plus/icons-vue'
|
2026-04-09 21:58:53 +08:00
|
|
|
|
import MainLayout from '@/components/MainLayout.vue'
|
2026-05-06 21:30:58 +08:00
|
|
|
|
import api from '@/api'
|
2026-04-09 21:58:53 +08:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
2026-05-06 21:30:58 +08:00
|
|
|
|
|
|
|
|
|
|
// ---------- 模板数据 ----------
|
|
|
|
|
|
interface TemplateItem {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
title: string
|
|
|
|
|
|
description: string
|
|
|
|
|
|
category: string
|
|
|
|
|
|
default_temperature: number
|
|
|
|
|
|
parameter_hints: string[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const templates = ref<TemplateItem[]>([])
|
2026-04-09 21:58:53 +08:00
|
|
|
|
const loading = ref(false)
|
2026-05-06 21:30:58 +08:00
|
|
|
|
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: {},
|
|
|
|
|
|
})
|
2026-04-09 21:58:53 +08:00
|
|
|
|
|
2026-05-06 21:30:58 +08:00
|
|
|
|
// 从 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 () => {
|
2026-04-09 21:58:53 +08:00
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
2026-05-06 21:30:58 +08:00
|
|
|
|
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('模板加载失败,请检查后端服务')
|
2026-04-09 21:58:53 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:58 +08:00
|
|
|
|
const handleCategoryChange = () => {
|
|
|
|
|
|
// 筛选由 computed 自动处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const categoryTagType = (cat: string) => {
|
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
|
customer_service: 'success',
|
|
|
|
|
|
dev: 'primary',
|
|
|
|
|
|
ops: 'warning',
|
|
|
|
|
|
education: 'danger',
|
|
|
|
|
|
}
|
|
|
|
|
|
return map[cat] || 'info'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getCategoryName = (cat: string) => {
|
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
|
customer_service: '客服场景',
|
|
|
|
|
|
dev: '研发辅助',
|
|
|
|
|
|
ops: '运维分析',
|
|
|
|
|
|
education: '学习教育',
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
return map[cat] || cat
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:58 +08:00
|
|
|
|
const openExecuteDialog = (tpl: TemplateItem) => {
|
|
|
|
|
|
selectedTemplate.value = tpl
|
|
|
|
|
|
executeForm.value = {
|
|
|
|
|
|
message: '',
|
|
|
|
|
|
parameters: {
|
|
|
|
|
|
temperature: tpl.default_temperature || 0.7,
|
|
|
|
|
|
enable_tools: true,
|
|
|
|
|
|
},
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
showExecute.value = true
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:58 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 21:30:58 +08:00
|
|
|
|
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)
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-05-06 21:30:58 +08:00
|
|
|
|
loadTemplates()
|
2026-04-09 21:58:53 +08:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.main-console {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
|
|
|
|
|
|
.page-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
margin-bottom: 20px;
|
2026-05-06 21:30:58 +08:00
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 12px;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
|
|
|
|
|
|
.template-grid {
|
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
margin-bottom: 24px;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
|
|
|
|
|
|
.template-card {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
height: 100%;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
display: flex;
|
2026-05-06 21:30:58 +08:00
|
|
|
|
flex-direction: column;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
.template-card:hover {
|
|
|
|
|
|
transform: translateY(-4px);
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
|
|
|
|
|
|
.card-header-area {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-desc {
|
2026-04-09 21:58:53 +08:00
|
|
|
|
color: var(--el-text-color-secondary);
|
2026-05-06 21:30:58 +08:00
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
margin: 0 0 12px 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 3;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
overflow: hidden;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
.card-footer {
|
2026-04-09 21:58:53 +08:00
|
|
|
|
display: flex;
|
2026-05-06 21:30:58 +08:00
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-links {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quick-card {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quick-card:hover {
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quick-inner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
2026-05-06 21:30:58 +08:00
|
|
|
|
.quick-inner h3 {
|
2026-04-09 21:58:53 +08:00
|
|
|
|
margin: 0;
|
2026-05-06 21:30:58 +08:00
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.muted {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
margin: 4px 0 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-area {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.result-area {
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
overflow-y: auto;
|
2026-04-09 21:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|