Преглед на файлове

AI工作流销售端页面

lk преди 1 ден
родител
ревизия
6db461dfad
променени са 3 файла, в които са добавени 465 реда и са изтрити 0 реда
  1. 43 0
      src/api/company/aiWorkflow.js
  2. 13 0
      src/router/index.js
  3. 409 0
      src/views/company/aiWorkflow/index.vue

+ 43 - 0
src/api/company/aiWorkflow.js

@@ -0,0 +1,43 @@
+import request from '@/utils/request'
+
+/**
+ * 获取我的节点语音列表(分页)
+ * @param {Object} query - 查询参数
+ * @param {number} query.pageNum - 页码
+ * @param {number} query.pageSize - 每页数量
+ * @param {string} query.workflowName - 工作流名称(可选)
+ * @param {string} query.nodeName - 节点名称(可选)
+ * @param {string} query.nodeType - 节点类型(可选)
+ */
+export function getMyNodes(query) {
+  return request({
+    url: '/company/aiWorkflow/myNodes',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 上传节点语音
+ * @param {Object} data - 上传数据
+ * @param {number} data.nodeId - 节点ID
+ * @param {string} data.voiceUrl - 语音URL
+ */
+export function uploadNodeVoice(data) {
+  return request({
+    url: '/company/aiWorkflow/uploadVoice',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 删除节点语音
+ * @param {number} nodeId - 节点ID
+ */
+export function deleteNodeVoice(nodeId) {
+  return request({
+    url: '/company/aiWorkflow/deleteVoice/' + nodeId,
+    method: 'delete'
+  })
+}

+ 13 - 0
src/router/index.js

@@ -271,6 +271,19 @@ export const constantRoutes = [
       isIndependentPage: true // 标记为“独立页”
     }
   },
+  {
+  path: '/company/aiWorkflow',
+  component: Layout,
+  hidden: true,
+  children: [
+    {
+      path: 'index',
+      component: () => import('@/views/company/aiWorkflow/index'),
+      name: 'AiWorkflow',
+      meta: { title: 'AI工作流', activeMenu: '/company/aiWorkflow' }
+    }
+  ]
+},
 
 ]
 

+ 409 - 0
src/views/company/aiWorkflow/index.vue

@@ -0,0 +1,409 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索表单 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="工作流名称" prop="workflowName">
+        <el-input
+          v-model="queryParams.workflowName"
+          placeholder="请输入工作流名称"
+          clearable
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="节点名称" prop="nodeName">
+        <el-input
+          v-model="queryParams.nodeName"
+          placeholder="请输入节点名称"
+          clearable
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="节点类型" prop="nodeType">
+        <el-select
+          v-model="queryParams.nodeType"
+          placeholder="请选择节点类型"
+          clearable
+          size="small"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in nodeTypeOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-refresh"
+          size="mini"
+          @click="getList"
+        >刷新</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-table v-loading="loading" :data="nodeList" border>
+      <el-table-column label="节点ID" align="center" prop="nodeId" width="80" />
+      <el-table-column label="工作流名称" align="center" prop="workflowName" min-width="150" :show-overflow-tooltip="true" />
+      <el-table-column label="节点标识" align="center" prop="nodeKey" width="120" :show-overflow-tooltip="true" />
+      <el-table-column label="节点名称" align="center" prop="nodeName" min-width="150" :show-overflow-tooltip="true" />
+      <el-table-column label="节点类型" align="center" prop="nodeType" width="100">
+        <template slot-scope="scope">
+          <el-tag :type="getNodeTypeTag(scope.row.nodeType)">
+            {{ getNodeTypeLabel(scope.row.nodeType) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="语音状态" align="center" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.voiceUrl" type="success">已录制</el-tag>
+          <el-tag v-else type="info">未录制</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="语音" align="center" width="200">
+        <template slot-scope="scope">
+          <audio
+            v-if="scope.row.voiceUrl"
+            :src="scope.row.voiceUrl"
+            controls
+            style="height: 30px; width: 180px;"
+          ></audio>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <!-- <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-microphone"
+            @click="handleRecord(scope.row)"
+          >{{ scope.row.voiceUrl ? '重新录制' : '录制语音' }}</el-button>
+          <el-button
+            v-if="scope.row.voiceUrl"
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDeleteVoice(scope.row)"
+          >删除语音</el-button> -->
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 录制语音对话框 -->
+    <el-dialog
+      :title="'录制语音 - ' + currentNode.nodeName"
+      :visible.sync="recordDialogVisible"
+      width="500px"
+      append-to-body
+      @close="handleRecordDialogClose"
+    >
+      <div class="record-container">
+        <div class="record-info">
+          <p><strong>工作流:</strong>{{ currentNode.workflowName }}</p>
+          <p><strong>节点:</strong>{{ currentNode.nodeName }}</p>
+        </div>
+
+        <div class="record-controls">
+          <!-- 录音状态显示 -->
+          <div class="record-status">
+            <span v-if="isRecording" class="recording-indicator">
+              <i class="el-icon-loading"></i> 正在录音... {{ recordingTime }}s
+            </span>
+            <span v-else-if="audioBlob">录音完成</span>
+            <span v-else>点击开始录音</span>
+          </div>
+
+          <!-- 录音按钮 -->
+          <div class="record-buttons">
+            <el-button
+              v-if="!isRecording"
+              type="primary"
+              icon="el-icon-microphone"
+              circle
+              size="large"
+              @click="startRecording"
+            ></el-button>
+            <el-button
+              v-else
+              type="danger"
+              icon="el-icon-video-pause"
+              circle
+              size="large"
+              @click="stopRecording"
+            ></el-button>
+          </div>
+
+          <!-- 录音预览 -->
+          <div v-if="audioUrl" class="record-preview">
+            <audio :src="audioUrl" controls style="width: 100%;"></audio>
+            <el-button
+              type="text"
+              icon="el-icon-refresh"
+              @click="resetRecording"
+            >重新录制</el-button>
+          </div>
+        </div>
+      </div>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="recordDialogVisible = false">取 消</el-button>
+        <el-button
+          type="primary"
+          :disabled="!audioBlob"
+          :loading="uploading"
+          @click="submitVoice"
+        >上 传</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+
+<script>
+import { getMyNodes, uploadNodeVoice, deleteNodeVoice } from '@/api/company/aiWorkflow'
+
+export default {
+  name: 'AiWorkflow',
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 节点列表
+      nodeList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        workflowName: undefined,
+        nodeName: undefined,
+        nodeType: undefined
+      },
+      // 节点类型选项
+      nodeTypeOptions: [
+        { value: 'start', label: '开始' },
+        { value: 'end', label: '结束' },
+        { value: 'condition', label: '条件' },
+        { value: 'action', label: '动作' },
+        { value: 'ai', label: 'AI' },
+        { value: 'delay', label: '延迟' },
+        { value: 'http', label: 'HTTP' }
+      ],
+      // 录制对话框
+      recordDialogVisible: false,
+      currentNode: {},
+      // 录音相关
+      isRecording: false,
+      mediaRecorder: null,
+      audioChunks: [],
+      audioBlob: null,
+      audioUrl: null,
+      recordingTime: 0,
+      recordingTimer: null,
+      uploading: false
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    /** 查询节点列表 */
+    getList() {
+      this.loading = true
+      getMyNodes(this.queryParams).then(response => {
+        
+        this.nodeList = response.data || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    /** 获取节点类型标签颜色 */
+    getNodeTypeTag(type) {
+      const tagMap = {
+        start: 'success',
+        end: 'info',
+        condition: 'warning',
+        action: 'primary',
+        ai: '',
+        delay: 'info',
+        http: 'primary'
+      }
+      return tagMap[type] || ''
+    },
+    /** 获取节点类型标签文字 */
+    getNodeTypeLabel(type) {
+      const item = this.nodeTypeOptions.find(opt => opt.value === type)
+      return item ? item.label : type
+    },
+    /** 录制语音 */
+    handleRecord(row) {
+      this.currentNode = { ...row }
+      this.resetRecording()
+      this.recordDialogVisible = true
+    },
+    /** 开始录音 */
+    async startRecording() {
+      try {
+        const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
+        this.mediaRecorder = new MediaRecorder(stream)
+        this.audioChunks = []
+
+        this.mediaRecorder.ondataavailable = (event) => {
+          this.audioChunks.push(event.data)
+        }
+
+        this.mediaRecorder.onstop = () => {
+          this.audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' })
+          this.audioUrl = URL.createObjectURL(this.audioBlob)
+          stream.getTracks().forEach(track => track.stop())
+        }
+
+        this.mediaRecorder.start()
+        this.isRecording = true
+        this.recordingTime = 0
+        this.recordingTimer = setInterval(() => {
+          this.recordingTime++
+        }, 1000)
+      } catch (error) {
+        this.$message.error('无法访问麦克风,请检查权限设置')
+        console.error('录音错误:', error)
+      }
+    },
+    /** 停止录音 */
+    stopRecording() {
+      if (this.mediaRecorder && this.isRecording) {
+        this.mediaRecorder.stop()
+        this.isRecording = false
+        clearInterval(this.recordingTimer)
+      }
+    },
+    /** 重置录音 */
+    resetRecording() {
+      this.stopRecording()
+      this.audioBlob = null
+      this.audioUrl = null
+      this.recordingTime = 0
+    },
+    /** 录制对话框关闭 */
+    handleRecordDialogClose() {
+      this.resetRecording()
+    },
+    /** 上传语音 */
+    async submitVoice() {
+      if (!this.audioBlob) {
+        this.$message.warning('请先录制语音')
+        return
+      }
+
+      this.uploading = true
+      try {
+        // 创建FormData上传文件
+        const formData = new FormData()
+        formData.append('file', this.audioBlob, `voice_${this.currentNode.nodeId}.wav`)
+        formData.append('nodeId', this.currentNode.nodeId)
+
+        await uploadNodeVoice(formData)
+        this.$message.success('语音上传成功')
+        this.recordDialogVisible = false
+        this.getList()
+      } catch (error) {
+        this.$message.error('语音上传失败')
+      } finally {
+        this.uploading = false
+      }
+    },
+    /** 删除语音 */
+    handleDeleteVoice(row) {
+      this.$confirm('确认删除该节点的语音吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return deleteNodeVoice(row.nodeId)
+      }).then(() => {
+        this.$message.success('删除成功')
+        this.getList()
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.record-container {
+  padding: 20px;
+}
+.record-info {
+  margin-bottom: 20px;
+  padding: 10px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+.record-info p {
+  margin: 5px 0;
+}
+.record-controls {
+  text-align: center;
+}
+.record-status {
+  margin-bottom: 20px;
+  font-size: 14px;
+  color: #606266;
+}
+.recording-indicator {
+  color: #f56c6c;
+}
+.record-buttons {
+  margin-bottom: 20px;
+}
+.record-buttons .el-button {
+  width: 60px;
+  height: 60px;
+  font-size: 24px;
+}
+.record-preview {
+  margin-top: 20px;
+}
+</style>