Przeglądaj źródła

!756 feat: AI工作流优化
Merge pull request !756 from Lesan/master-ai工作流

芋道源码 4 miesięcy temu
rodzic
commit
dafc54c7dd

+ 14 - 3
src/router/modules/remaining.ts

@@ -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'
         }
       }

+ 54 - 0
src/views/ai/workflow/form/BasicInfo.vue

@@ -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>

+ 49 - 0
src/views/ai/workflow/form/WorkflowDesign.vue

@@ -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>

+ 234 - 0
src/views/ai/workflow/form/index.vue

@@ -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>

+ 33 - 33
src/views/ai/workflow/manager/index.vue → src/views/ai/workflow/index.vue

@@ -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 {

+ 0 - 106
src/views/ai/workflow/manager/WorkflowForm.vue

@@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-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>

+ 0 - 83
src/views/ai/workflow/manager/WorkflowModelForm.vue

@@ -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>