feat: AI工作流优化

This commit is contained in:
Lesan 2025-03-28 15:41:59 +08:00
parent 05393006c1
commit 9262fe5767
7 changed files with 384 additions and 225 deletions

View File

@ -668,16 +668,27 @@ const remainingRouter: AppRouteRecordRaw[] = [
activeMenu: '/ai/knowledge'
}
},
// TODO @lesan:type =》 design 设计 AI 工作流
{
path: 'console/workflow/create',
component: () => import('@/views/ai/workflow/form/index.vue'),
name: 'AiWorkflowCreate',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '设计 AI 工作流',
activeMenu: '/ai/console/workflow'
}
},
{
path: 'console/workflow/:type/:id',
component: () => import('@/views/ai/workflow/manager/WorkflowModelForm.vue'),
component: () => import('@/views/ai/workflow/form/index.vue'),
name: 'AiWorkflowUpdate',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '修改 AI 工作流',
title: '设计 AI 工作流',
activeMenu: '/ai/console/workflow'
}
}

View File

@ -0,0 +1,54 @@
<template>
<el-form ref="formRef" :model="modelData" :rules="formRules" label-width="120px">
<el-row>
<el-col :span="24">
<el-form-item label="流程标识" prop="code">
<el-input v-model="modelData.code" placeholder="请输入流程标识" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="流程名称" prop="name">
<el-input v-model="modelData.name" placeholder="请输入流程名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="状态" prop="status">
<el-select v-model="modelData.status" placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="modelData.remark" :rows="2" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import { FormRules } from 'element-plus'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const modelData = defineModel<any>()
const formRef = ref() // Ref
const formRules = reactive<FormRules>({
code: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
})
/** 表单校验 */
const validate = async () => {
await formRef.value?.validate()
}
defineExpose({
validate
})
</script>

View File

@ -0,0 +1,49 @@
<template>
<div class="relative" style="width: 100%; height: 700px">
<Tinyflow
v-if="workflowData"
ref="tinyflowRef"
:className="'custom-class'"
:style="{ width: '100%', height: '100%' }"
:data="workflowData"
:provider="provider"
/>
<div class="absolute top-30px right-30px">
<el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">
测试
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
defineProps<{
provider: any
}>()
const tinyflowRef = ref()
const workflowData = inject('workflowData') as Ref
const testWorkflowModel = () => {
// TODO @lesan
}
/** 表单校验 */
const validate = async () => {
try {
//
if (!workflowData.value) {
throw new Error('请设计流程')
}
workflowData.value = tinyflowRef.value.getData()
return true
} catch (error) {
throw error
}
}
defineExpose({
validate
})
</script>

View File

@ -0,0 +1,234 @@
<template>
<ContentWrap>
<div class="mx-auto">
<!-- 头部导航栏 -->
<div
class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
>
<!-- 左侧标题 -->
<div class="w-200px flex items-center overflow-hidden">
<Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
<span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'">
{{ formData.name || '创建流程' }}
</span>
</div>
<!-- 步骤条 -->
<div class="flex-1 flex items-center justify-center h-full">
<div class="w-400px flex items-center justify-between h-full">
<div
v-for="(step, index) in steps"
:key="index"
class="flex items-center cursor-pointer mx-15px relative h-full"
:class="[
currentStep === index
? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
: 'text-gray-500'
]"
@click="handleStepClick(index)"
>
<div
class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
:class="[
currentStep === index
? 'bg-[#3473ff] text-white border-[#3473ff]'
: 'border-gray-300 bg-white text-gray-500'
]"
>
{{ index + 1 }}
</div>
<span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
</div>
</div>
</div>
<!-- 右侧按钮 -->
<div class="w-200px flex items-center justify-end gap-2">
<el-button type="primary" @click="handleSave"> </el-button>
</div>
</div>
<!-- 主体内容 -->
<div class="mt-50px">
<!-- 第一步基本信息 -->
<div v-if="currentStep === 0" class="mx-auto w-560px">
<BasicInfo v-model="formData" ref="basicInfoRef" />
</div>
<!-- 第二步工作流设计 -->
<WorkflowDesign
v-if="currentStep === 1"
v-model="formData"
:provider="provider"
ref="workflowDesignRef"
/>
</div>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { useTagsViewStore } from '@/store/modules/tagsView'
import { CommonStatusEnum } from '@/utils/constants'
import * as WorkflowApi from '@/api/ai/workflow'
import BasicInfo from './BasicInfo.vue'
import WorkflowDesign from './WorkflowDesign.vue'
import { ApiKeyApi } from '@/api/ai/model/apiKey'
const router = useRouter()
const { delView } = useTagsViewStore()
const route = useRoute()
const message = useMessage()
const basicInfoRef = ref()
const workflowDesignRef = ref()
const validateBasic = async () => {
await basicInfoRef.value?.validate()
}
const validateWorkflow = async () => {
await workflowDesignRef.value?.validate()
}
const currentStep = ref(-1)
const steps = [
{ title: '基本信息', validator: validateBasic },
{ title: '工作流设计', validator: validateWorkflow }
]
const formData: any = ref({
id: undefined,
name: '',
code: '',
remark: '',
graph: '',
status: CommonStatusEnum.ENABLE
})
// TODO @lesan
const provider = ref<any>()
const workflowData = ref<any>({})
provide('workflowData', workflowData)
/** 初始化数据 */
const actionType = route.params.type as string
const initData = async () => {
if (actionType === 'update') {
const workflowId = route.params.id as string
formData.value = await WorkflowApi.getWorkflow(workflowId)
workflowData.value = JSON.parse(formData.value.graph)
}
const apiKeys = await ApiKeyApi.getApiKeySimpleList()
provider.value = {
llm: () =>
apiKeys.map(({ id, name }) => ({
value: id,
label: name
})),
knowledge: () => [],
internal: () => []
}
currentStep.value = 0
}
/** 校验所有步骤数据是否完整 */
const validateAllSteps = async () => {
try {
//
try {
await validateBasic()
} catch (error) {
currentStep.value = 0
throw new Error('请完善基本信息')
}
//
try {
await validateWorkflow()
} catch (error) {
currentStep.value = 1
throw new Error('请完善工作流信息')
}
return true
} catch (error) {
throw error
}
}
/** 保存操作 */
const handleSave = async () => {
try {
//
await validateAllSteps()
//
const data = {
...formData.value
}
data.graph = JSON.stringify(workflowData.value)
if (actionType === 'update') {
await WorkflowApi.updateWorkflow(data)
} else {
await WorkflowApi.createWorkflow(data)
}
delView(unref(router.currentRoute))
await router.push({ name: 'AiWorkflow' })
} catch (error: any) {
console.error('保存失败:', error)
message.warning(error.message || '请完善所有步骤的必填信息')
}
}
/** 步骤切换处理 */
const handleStepClick = async (index: number) => {
try {
if (index !== 0) {
await validateBasic()
}
if (index !== 1) {
await validateWorkflow()
}
//
currentStep.value = index
} catch (error) {
console.error('步骤切换失败:', error)
message.warning('请先完善当前步骤必填信息')
}
}
/** 返回列表页 */
const handleBack = () => {
//
delView(unref(router.currentRoute))
//
router.push({ name: 'AiWorkflow' })
}
/** 初始化 */
onMounted(async () => {
await initData()
})
</script>
<style lang="scss" scoped>
.border-bottom {
border-bottom: 1px solid #dcdfe6;
}
.text-primary {
color: #3473ff;
}
.bg-primary {
background-color: #3473ff;
}
.border-primary {
border-color: #3473ff;
}
</style>

View File

@ -1,4 +1,3 @@
<!-- TODO @lesan要不直接放到 workflow 根目录 -->
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
@ -9,9 +8,9 @@
:inline="true"
label-width="68px"
>
<el-form-item label="流程标识" prop="definitionKey">
<el-form-item label="流程标识" prop="code">
<el-input
v-model="queryParams.definitionKey"
v-model="queryParams.code"
placeholder="请输入流程标识"
clearable
@keyup.enter="handleQuery"
@ -27,6 +26,16 @@
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
@ -56,22 +65,27 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="编号" align="center" prop="id" :show-overflow-tooltip="true" />
<el-table-column
label="流程标识"
align="center"
prop="definitionKey"
:show-overflow-tooltip="true"
/>
<el-table-column label="流程名称" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="流程标识" align="center" prop="code" />
<el-table-column label="流程名称" align="center" prop="name" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" width="220" fixed="right">
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="状态" align="center" key="status">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
disabled
/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right">
<template #default="scope">
<el-button
type="primary"
@ -81,14 +95,6 @@
>
修改
</el-button>
<el-button
type="primary"
link
@click="openModelForm('update', scope.row.id)"
v-hasPermi="['ai:workflow:update']"
>
流程图
</el-button>
<el-button
link
type="danger"
@ -110,16 +116,15 @@
</ContentWrap>
<!-- 添加或修改工作流对话框 -->
<WorkflowForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as WorkflowApi from '@/api/ai/workflow'
import { dateFormatter } from '@/utils/formatTime'
import WorkflowForm from './WorkflowForm.vue'
import { checkPermi } from '@/utils/permission'
/** AI 绘画 列表 */
defineOptions({ name: 'AiWorkflowManager' })
defineOptions({ name: 'AiWorkflow' })
const message = useMessage() //
const { t } = useI18n() //
@ -131,8 +136,9 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
definitionKey: '',
code: '',
name: '',
status: undefined,
createTime: []
})
const queryFormRef = ref() //
@ -175,13 +181,7 @@ const handleDelete = async (id: number) => {
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 修改流程模型弹窗 */
const openModelForm = async (type: string, id?: number) => {
const openForm = async (type: string, id?: number) => {
if (type === 'create') {
await push({ name: 'AiWorkflowCreate' })
} else {

View File

@ -1,106 +0,0 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-row>
<el-col :span="24">
<el-form-item label="流程标识" prop="definitionKey">
<el-input v-model="formData.definitionKey" placeholder="请输入流程标识" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入流程名称" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as WorkflowApi from '@/api/ai/workflow'
import { FormRules } from 'element-plus'
defineOptions({ name: 'AiWorkflowForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
definitionKey: '',
name: ''
})
const formRules = reactive<FormRules>({
definitionKey: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await WorkflowApi.getWorkflow(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
const data = formData.value
if (formType.value === 'create') {
await WorkflowApi.createWorkflow(data)
message.success(t('common.createSuccess'))
} else {
await WorkflowApi.updateWorkflow(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
definitionKey: '',
name: ''
}
formRef.value?.resetFields()
}
</script>

View File

@ -1,83 +0,0 @@
<!-- TODO @lesan要不叫搞个 design 单独一个路由 -->
<template>
<div style="width: 100%; height: calc(100vh - 160px)">
<Tinyflow
ref="tinyflowRef"
:className="'custom-class'"
:style="{ width: '100%', height: '100%' }"
v-if="initialData"
:data="initialData"
:provider="provider"
/>
</div>
<div class="absolute top-30px right-30px">
<el-button @click="updateWorkflowModel" type="primary" v-hasPermi="['ai:workflow:update']">
保存
</el-button>
<el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">
测试
</el-button>
</div>
</template>
<script setup lang="ts">
import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
import * as WorkflowApi from '@/api/ai/workflow'
import { ApiKeyApi } from '@/api/ai/model/apiKey'
const route = useRoute()
const message = useMessage() //
const { t } = useI18n() //
const tinyflowRef = ref()
// TODO @lesan
const provider = ref({ llm: () => [], knowledge: () => [], internal: () => [] })
const initialData = ref()
const loadData = async () => {
try {
const [apiKeys, flowData] = await Promise.all([
ApiKeyApi.getApiKeySimpleList(),
WorkflowApi.getWorkflow(route.params.id)
])
// provider
provider.value = {
llm: () =>
apiKeys.map(({ id, name }) => ({
value: id,
label: name
})),
knowledge: () => [],
internal: () => []
}
//
initialData.value = JSON.parse(flowData.model)
} catch {}
}
const updateWorkflowModel = async () => {
try {
const model = tinyflowRef.value.getData()
const data = {
model: JSON.stringify(model),
id: route.params.id
}
await message.confirm('确认保存流程模型?')
await WorkflowApi.updateWorkflowModel(data)
message.success(t('common.updateSuccess'))
await loadData()
} catch {}
}
const testWorkflowModel = () => {
// TODO @lesan
}
watchEffect(() => {
if (route.params.id) {
loadData()
}
})
</script>