ai-robot-core/ai-service-admin/src/views/admin/script-flow/index.vue

434 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="script-flow-page">
<div class="page-header">
<div class="header-content">
<div class="title-section">
<h1 class="page-title">话术流程管理</h1>
<p class="page-desc">编排多步骤的话术流程引导用户按固定步骤完成信息收集</p>
</div>
<div class="header-actions">
<el-button type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建流程
</el-button>
</div>
</div>
</div>
<el-card shadow="hover" class="flow-card" v-loading="loading">
<el-table :data="flows" stripe style="width: 100%">
<el-table-column prop="name" label="流程名称" min-width="180" />
<el-table-column prop="description" label="描述" min-width="200">
<template #default="{ row }">
{{ row.description || '-' }}
</template>
</el-table-column>
<el-table-column prop="step_count" label="步骤数" width="100" />
<el-table-column prop="linked_rule_count" label="关联规则" width="100" />
<el-table-column prop="is_enabled" label="状态" width="80">
<template #default="{ row }">
<el-switch
v-model="row.is_enabled"
@change="handleToggleEnabled(row)"
active-color="#67C23A"
/>
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" width="180">
<template #default="{ row }">
{{ formatDate(row.updated_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button type="info" link size="small" @click="handlePreview(row)">
<el-icon><View /></el-icon>
预览
</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑流程' : '新建流程'"
width="900px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form :model="formData" :rules="formRules" ref="formRef" label-width="80px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入流程名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="启用状态">
<el-switch v-model="formData.is_enabled" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="描述">
<el-input v-model="formData.description" type="textarea" :rows="2" placeholder="请输入描述(可选)" />
</el-form-item>
<el-divider content-position="left">步骤配置</el-divider>
<div class="steps-editor">
<draggable
v-model="formData.steps"
item-key="step_id"
handle=".drag-handle"
animation="200"
>
<template #item="{ element, index }">
<div class="step-item">
<div class="step-header">
<div class="drag-handle">
<el-icon><Rank /></el-icon>
</div>
<span class="step-order">步骤 {{ index + 1 }}</span>
<el-button type="danger" link size="small" @click="removeStep(index)">
删除
</el-button>
</div>
<div class="step-content">
<el-form-item label="话术内容">
<el-input
v-model="element.content"
type="textarea"
:rows="3"
placeholder="请输入话术内容,支持 {{variable}} 占位符"
/>
</el-form-item>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="等待输入">
<el-switch v-model="element.wait_for_input" />
</el-form-item>
</el-col>
<el-col :span="8" v-if="element.wait_for_input">
<el-form-item label="超时(秒)">
<el-input-number v-model="element.timeout_seconds" :min="5" :max="300" />
</el-form-item>
</el-col>
<el-col :span="8" v-if="element.wait_for_input">
<el-form-item label="超时动作">
<el-select v-model="element.timeout_action" style="width: 100%;">
<el-option
v-for="opt in TIMEOUT_ACTION_OPTIONS"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
</template>
</draggable>
<el-button type="primary" plain @click="addStep" style="width: 100%; margin-top: 12px;">
<el-icon><Plus /></el-icon>
添加步骤
</el-button>
</div>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">
{{ isEdit ? '保存' : '创建' }}
</el-button>
</template>
</el-dialog>
<el-drawer v-model="previewDrawer" title="流程预览" size="500px" destroy-on-close>
<flow-preview v-if="currentFlow" :flow="currentFlow" />
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, View, Rank } from '@element-plus/icons-vue'
import draggable from 'vuedraggable'
import {
listScriptFlows,
createScriptFlow,
updateScriptFlow,
deleteScriptFlow,
getScriptFlow
} from '@/api/script-flow'
import { TIMEOUT_ACTION_OPTIONS } from '@/types/script-flow'
import type { ScriptFlow, ScriptFlowDetail, ScriptFlowCreate, ScriptFlowUpdate, FlowStep } from '@/types/script-flow'
import FlowPreview from './components/FlowPreview.vue'
const loading = ref(false)
const flows = ref<ScriptFlow[]>([])
const dialogVisible = ref(false)
const previewDrawer = ref(false)
const isEdit = ref(false)
const submitting = ref(false)
const formRef = ref()
const currentFlow = ref<ScriptFlowDetail | null>(null)
const currentEditId = ref('')
const generateStepId = () => `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
const defaultFormData = (): ScriptFlowCreate => ({
name: '',
description: '',
steps: [],
is_enabled: true
})
const formData = ref<ScriptFlowCreate>(defaultFormData())
const formRules = {
name: [{ required: true, message: '请输入流程名称', trigger: 'blur' }]
}
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const loadFlows = async () => {
loading.value = true
try {
const res = await listScriptFlows()
flows.value = res.data || []
} catch (error) {
ElMessage.error('加载流程列表失败')
} finally {
loading.value = false
}
}
const handleCreate = () => {
isEdit.value = false
currentEditId.value = ''
formData.value = defaultFormData()
dialogVisible.value = true
}
const handleEdit = async (row: ScriptFlow) => {
isEdit.value = true
currentEditId.value = row.id
try {
const detail = await getScriptFlow(row.id)
formData.value = {
name: detail.name,
description: detail.description || '',
steps: detail.steps || [],
is_enabled: detail.is_enabled
}
dialogVisible.value = true
} catch (error) {
ElMessage.error('加载流程详情失败')
}
}
const handleDelete = async (row: ScriptFlow) => {
try {
await ElMessageBox.confirm('确定要删除该流程吗?', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteScriptFlow(row.id)
ElMessage.success('删除成功')
loadFlows()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
const handleToggleEnabled = async (row: ScriptFlow) => {
try {
await updateScriptFlow(row.id, { is_enabled: row.is_enabled })
ElMessage.success(row.is_enabled ? '已启用' : '已禁用')
} catch (error) {
row.is_enabled = !row.is_enabled
ElMessage.error('操作失败')
}
}
const handlePreview = async (row: ScriptFlow) => {
try {
currentFlow.value = await getScriptFlow(row.id)
previewDrawer.value = true
} catch (error) {
ElMessage.error('加载流程详情失败')
}
}
const addStep = () => {
formData.value.steps.push({
step_id: generateStepId(),
order: formData.value.steps.length + 1,
content: '',
wait_for_input: true,
timeout_seconds: 30,
timeout_action: 'repeat',
next_conditions: []
})
}
const removeStep = (index: number) => {
formData.value.steps.splice(index, 1)
formData.value.steps.forEach((step, i) => {
step.order = i + 1
})
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
} catch {
return
}
if (formData.value.steps.length === 0) {
ElMessage.warning('请至少添加一个步骤')
return
}
submitting.value = true
try {
const submitData = {
...formData.value,
steps: formData.value.steps.map((step, index) => ({
...step,
order: index + 1
}))
}
if (isEdit.value) {
const updateData: ScriptFlowUpdate = submitData
await updateScriptFlow(currentEditId.value, updateData)
ElMessage.success('保存成功')
} else {
await createScriptFlow(submitData)
ElMessage.success('创建成功')
}
dialogVisible.value = false
loadFlows()
} catch (error) {
ElMessage.error(isEdit.value ? '保存失败' : '创建失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
loadFlows()
})
</script>
<style scoped>
.script-flow-page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
flex-wrap: wrap;
}
.title-section {
flex: 1;
min-width: 300px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.page-desc {
margin: 0;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.flow-card {
border-radius: 8px;
}
.steps-editor {
max-height: 400px;
overflow-y: auto;
padding: 12px;
background-color: var(--el-fill-color-lighter);
border-radius: 6px;
}
.step-item {
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
margin-bottom: 12px;
overflow: hidden;
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background-color: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-light);
}
.drag-handle {
cursor: move;
color: var(--el-text-color-secondary);
}
.step-order {
font-weight: 600;
color: var(--el-text-color-primary);
}
.step-content {
padding: 16px;
}
</style>