feat: implement scenario-driven sales SOP platform
This commit is contained in:
77
codes/web/src/views/ScenariosView.vue
Normal file
77
codes/web/src/views/ScenariosView.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import type { Scenario } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const open = ref(false)
|
||||
const keyword = ref('')
|
||||
const items = ref<Scenario[]>([])
|
||||
const form = reactive({ name: '', industry: '', role_name: '', goal: '', trigger_text: '', visibility: 'tenant' })
|
||||
const columns = [
|
||||
{ title: '场景名称', dataIndex: 'name', key: 'name' }, { title: '行业', dataIndex: 'industry', key: 'industry', width: 140 },
|
||||
{ title: '适用角色', dataIndex: 'role_name', key: 'role_name', width: 160 }, { title: '状态', dataIndex: 'status', key: 'status', width: 110 },
|
||||
{ title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 180 }, { title: '', key: 'action', width: 80 },
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try { items.value = (await api.get<{ items: Scenario[] }>('/scenarios', { params: { keyword: keyword.value } })).items } finally { loading.value = false }
|
||||
}
|
||||
async function create() {
|
||||
try {
|
||||
const item = await api.post<Scenario>('/scenarios', form)
|
||||
message.success('场景已创建')
|
||||
open.value = false
|
||||
router.push(`/scenarios/${item.id}`)
|
||||
} catch (error) { message.error(apiMessage(error)) }
|
||||
}
|
||||
function statusText(value: string) { return ({ draft: '草稿', active: '使用中', archived: '已归档' } as Record<string,string>)[value] || value }
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div class="page-heading">
|
||||
<div><h1>场景与 SOP</h1><p>从业务触发点开始,定义信息字段与可执行话术流程。</p></div>
|
||||
<a-button type="primary" @click="open = true"><PlusOutlined />新建场景</a-button>
|
||||
</div>
|
||||
<section class="surface">
|
||||
<div class="toolbar">
|
||||
<a-input v-model:value="keyword" allow-clear placeholder="搜索名称或行业" style="width: 280px" @press-enter="load"><template #prefix><SearchOutlined /></template></a-input>
|
||||
<span class="muted">共 {{ items.length }} 个场景</span>
|
||||
</div>
|
||||
<a-table :columns="columns" :data-source="items" :loading="loading" row-key="id" :pagination="false" :scroll="{ x: 800 }" :custom-row="(record: Scenario) => ({ onClick: () => router.push(`/scenarios/${record.id}`) })">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'"><div class="scenario-name"><b>{{ record.name }}</b><small>{{ record.goal }}</small></div></template>
|
||||
<template v-else-if="column.key === 'status'"><a-tag :color="record.status === 'active' ? 'green' : 'default'">{{ statusText(record.status) }}</a-tag></template>
|
||||
<template v-else-if="column.key === 'updated_at'">{{ new Date(record.updated_at).toLocaleString('zh-CN') }}</template>
|
||||
<template v-else-if="column.key === 'action'"><a-button type="link" size="small">配置</a-button></template>
|
||||
</template>
|
||||
<template #emptyText><div class="empty-copy">还没有业务场景。创建第一个场景后,就可以配置字段和 SOP。</div></template>
|
||||
</a-table>
|
||||
</section>
|
||||
|
||||
<a-drawer v-model:open="open" title="创建业务场景" width="520" :destroy-on-close="true">
|
||||
<a-form layout="vertical" :model="form">
|
||||
<a-form-item label="场景名称" name="name" :rules="[{ required: true, message: '请输入场景名称' }]"><a-input v-model:value="form.name" placeholder="例如:宠物医生问诊问药" /></a-form-item>
|
||||
<div class="form-grid"><a-form-item label="所属行业" name="industry" :rules="[{ required: true, message: '请输入行业' }]"><a-input v-model:value="form.industry" placeholder="宠物医疗" /></a-form-item><a-form-item label="适用角色" name="role_name" :rules="[{ required: true, message: '请输入角色' }]"><a-input v-model:value="form.role_name" placeholder="医生、客服" /></a-form-item></div>
|
||||
<a-form-item label="执行目标" name="goal" :rules="[{ required: true, message: '请输入执行目标' }]"><a-textarea v-model:value="form.goal" :rows="3" placeholder="这个场景最终要推动什么结果" /></a-form-item>
|
||||
<a-form-item label="触发条件" name="trigger_text" :rules="[{ required: true, message: '请输入触发条件' }]"><a-textarea v-model:value="form.trigger_text" :rows="3" placeholder="什么时候进入这个场景" /></a-form-item>
|
||||
<a-form-item label="可见范围"><a-radio-group v-model:value="form.visibility"><a-radio value="tenant">全企业</a-radio><a-radio value="team">团队</a-radio><a-radio value="private">仅自己</a-radio></a-radio-group></a-form-item>
|
||||
<a-button type="primary" block @click="create">创建并继续配置</a-button>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scenario-name { max-width: 460px; display: flex; flex-direction: column; cursor: pointer; }.scenario-name b { font-size: 14px; }.scenario-name small { overflow: hidden; margin-top: 4px; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
:deep(.ant-table-row) { cursor: pointer; }
|
||||
@media (max-width: 600px) { .form-grid { grid-template-columns: 1fr; gap: 0; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user