|
|
@@ -1,13 +1,6 @@
|
|
|
<template>
|
|
|
<div class="app-container">
|
|
|
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
|
|
|
- <el-form-item label="数据范围" prop="dataScope">
|
|
|
- <el-select v-model="queryParams.dataScope" placeholder="数据范围" size="small" @change="handleQuery">
|
|
|
- <el-option label="仅自己" value="self" />
|
|
|
- <el-option label="本组" value="group" />
|
|
|
- <el-option label="本公司" value="company" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
<el-form-item label="场景" prop="scenario">
|
|
|
<el-select v-model="queryParams.scenario" placeholder="请选择场景" clearable size="small">
|
|
|
<el-option v-for="s in scenarios" :key="s.code" :label="s.name" :value="s.code" />
|
|
|
@@ -15,7 +8,7 @@
|
|
|
</el-form-item>
|
|
|
<el-form-item label="状态" prop="status">
|
|
|
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
|
|
|
- <el-option label="已分析" value="analyzed" /><el-option label="待分析" value="pending" /><el-option label="已学习" value="learned" />
|
|
|
+ <el-option label="待分析" value="raw" /><el-option label="已分析" value="analyzed" /><el-option label="已学习" value="applied" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
@@ -24,58 +17,280 @@
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<el-row :gutter="10" class="mb8">
|
|
|
- <el-col :span="1.5"><el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddDialog">录入对话</el-button></el-col>
|
|
|
+ <el-col :span="1.5"><el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleImportDialog">批量导入</el-button></el-col>
|
|
|
<el-col :span="1.5"><el-button type="success" plain icon="el-icon-data-analysis" size="mini" @click="handleAnalyze">AI分析</el-button></el-col>
|
|
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
</el-row>
|
|
|
<el-table border v-loading="loading" :data="list">
|
|
|
<el-table-column label="ID" align="center" prop="id" width="60" />
|
|
|
- <el-table-column label="销冠姓名" align="center" prop="salespersonName" width="100" />
|
|
|
- <el-table-column label="客户问题" align="center" prop="customerQuestion" show-overflow-tooltip />
|
|
|
- <el-table-column label="销冠回答" align="center" prop="salesAnswer" show-overflow-tooltip />
|
|
|
+ <el-table-column label="销冠姓名" align="center" prop="salesperson_name" width="100" />
|
|
|
+ <el-table-column label="客户问题" align="center" prop="customer_question" show-overflow-tooltip />
|
|
|
+ <el-table-column label="销冠回答" align="center" prop="sales_answer" show-overflow-tooltip />
|
|
|
<el-table-column label="场景" align="center" prop="scenario" width="100" />
|
|
|
<el-table-column label="状态" align="center" prop="status" width="80">
|
|
|
<template slot-scope="scope">
|
|
|
- <el-tag :type="scope.row.status==='learned'?'success':scope.row.status==='analyzed'?'warning':'info'" size="small">{{ scope.row.status }}</el-tag>
|
|
|
+ <el-tag :type="scope.row.status==='applied'?'success':scope.row.status==='analyzed'?'warning':'info'" size="small">
|
|
|
+ {{ scope.row.status === 'raw' ? '待分析' : scope.row.status === 'analyzed' ? '已分析' : scope.row.status === 'applied' ? '已学习' : scope.row.status }}
|
|
|
+ </el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
|
|
|
+ <el-table-column label="创建时间" align="center" prop="create_time" width="160" />
|
|
|
</el-table>
|
|
|
<pagination v-show="total>0" :total="total" :page.sync="queryParams.page" :limit.sync="queryParams.size" @pagination="getList" />
|
|
|
- <!-- 录入对话弹窗 -->
|
|
|
- <el-dialog title="录入销冠对话" :visible.sync="addVisible" width="600px" append-to-body>
|
|
|
- <el-form ref="addForm" :model="addForm" :rules="addRules" label-width="100px">
|
|
|
- <el-form-item label="销冠姓名" prop="salespersonName"><el-input v-model="addForm.salespersonName" placeholder="请输入销冠姓名" /></el-form-item>
|
|
|
- <el-form-item label="场景" prop="scenario">
|
|
|
- <el-select v-model="addForm.scenario" placeholder="请选择场景"><el-option v-for="s in scenarios" :key="s.code" :label="s.name" :value="s.code" /></el-select>
|
|
|
+
|
|
|
+ <!-- ========== 批量导入弹窗 ========== -->
|
|
|
+ <el-dialog title="批量导入销冠对话" :visible.sync="importVisible" width="800px" append-to-body :close-on-click-modal="false">
|
|
|
+ <el-tabs v-model="importTab" @tab-click="resetPreview">
|
|
|
+ <!-- Tab1: Excel导入 -->
|
|
|
+ <el-tab-pane label="Excel导入" name="excel">
|
|
|
+ <el-alert type="info" :closable="false" show-icon style="margin-bottom:15px">
|
|
|
+ <template slot="title">Excel格式:第1列=客户问题,第2列=销冠回答,一行一组对话</template>
|
|
|
+ </el-alert>
|
|
|
+ <el-upload
|
|
|
+ ref="excelUpload"
|
|
|
+ action=""
|
|
|
+ :auto-upload="false"
|
|
|
+ :limit="1"
|
|
|
+ :on-change="handleExcelChange"
|
|
|
+ :on-remove="resetPreview"
|
|
|
+ accept=".xlsx,.xls"
|
|
|
+ drag
|
|
|
+ >
|
|
|
+ <i class="el-icon-upload"></i>
|
|
|
+ <div class="el-upload__text">将Excel文件拖到此处,或<em>点击上传</em></div>
|
|
|
+ <div slot="tip" class="el-upload__tip">仅支持 .xlsx / .xls 文件,第1列客户问题,第2列销冠回答</div>
|
|
|
+ </el-upload>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- Tab2: 文本输入 -->
|
|
|
+ <el-tab-pane label="文本输入" name="text">
|
|
|
+ <el-alert type="info" :closable="false" show-icon style="margin-bottom:15px">
|
|
|
+ <template slot="title">格式:a:客户说了什么(换行)b:销冠说了什么,每组对话之间空一行</template>
|
|
|
+ </el-alert>
|
|
|
+ <el-input
|
|
|
+ v-model="textContent"
|
|
|
+ type="textarea"
|
|
|
+ :rows="12"
|
|
|
+ placeholder="a: 你们这个产品多少钱? b: 咱们这个产品根据配置不同价格也不一样,您主要想用在哪个场景呢? a: 我主要是想用在客服场景 b: 客服场景的话我推荐咱们的专业版,目前有活动价特别划算,我给您详细介绍一下?"
|
|
|
+ />
|
|
|
+ <el-button type="primary" size="small" style="margin-top:10px" @click="parseTextContent">解析文本</el-button>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+
|
|
|
+ <!-- 公共表单字段 -->
|
|
|
+ <el-form :model="importForm" label-width="100px" style="margin-top:15px">
|
|
|
+ <el-form-item label="销冠姓名">
|
|
|
+ <el-input v-model="importForm.salespersonName" placeholder="请输入销冠姓名(可选)" style="width:300px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="场景">
|
|
|
+ <el-select v-model="importForm.scenario" placeholder="请选择场景(可选)" clearable style="width:300px">
|
|
|
+ <el-option v-for="s in scenarios" :key="s.code" :label="s.name" :value="s.code" />
|
|
|
+ </el-select>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="客户问题" prop="customerQuestion"><el-input v-model="addForm.customerQuestion" type="textarea" :rows="3" placeholder="请输入客户的问题" /></el-form-item>
|
|
|
- <el-form-item label="销冠回答" prop="salesAnswer"><el-input v-model="addForm.salesAnswer" type="textarea" :rows="3" placeholder="请输入销冠的回答" /></el-form-item>
|
|
|
</el-form>
|
|
|
- <div slot="footer"><el-button @click="addVisible=false">取消</el-button><el-button type="primary" @click="submitAdd">录入</el-button></div>
|
|
|
+
|
|
|
+ <!-- 预览表格 -->
|
|
|
+ <div v-if="previewList.length > 0" style="margin-top:15px">
|
|
|
+ <el-divider>预览数据(共 {{ previewList.length }} 条)</el-divider>
|
|
|
+ <el-table :data="previewList" border max-height="300" size="mini">
|
|
|
+ <el-table-column label="#" type="index" width="50" />
|
|
|
+ <el-table-column label="客户问题" prop="customer" show-overflow-tooltip />
|
|
|
+ <el-table-column label="销冠回答" prop="sales" show-overflow-tooltip />
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="importVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :disabled="previewList.length === 0" :loading="submitLoading" @click="submitImport">
|
|
|
+ 确认导入({{ previewList.length }} 条)
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
<script>
|
|
|
-import { listSalesCorpus, addCorpusDialog, analyzeCorpus, getCorpusScenarios } from '@/api/workflow/lobster'
|
|
|
+import { listSalesCorpus, batchImportCorpus, analyzeCorpus, getCorpusScenarios } from '@/api/workflow/lobster'
|
|
|
+import * as XLSX from 'xlsx'
|
|
|
+
|
|
|
export default {
|
|
|
name: 'SalesCorpus',
|
|
|
data() {
|
|
|
- return { loading: false, showSearch: true, list: [], total: 0, scenarios: [], addVisible: false,
|
|
|
- addForm: { salespersonName: '', scenario: '', customerQuestion: '', salesAnswer: '' },
|
|
|
- addRules: { customerQuestion: [{ required: true, message: '请输入客户问题', trigger: 'blur' }], salesAnswer: [{ required: true, message: '请输入销冠回答', trigger: 'blur' }] },
|
|
|
- queryParams: {
|
|
|
- dataScope: 'self', page: 1, size: 10, scenario: null, status: null } }
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ showSearch: true,
|
|
|
+ list: [],
|
|
|
+ total: 0,
|
|
|
+ scenarios: [],
|
|
|
+ queryParams: { page: 1, size: 10, scenario: null, status: null },
|
|
|
+ // 导入相关
|
|
|
+ importVisible: false,
|
|
|
+ importTab: 'excel',
|
|
|
+ textContent: '',
|
|
|
+ previewList: [],
|
|
|
+ submitLoading: false,
|
|
|
+ importForm: {
|
|
|
+ salespersonName: '',
|
|
|
+ scenario: ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.getList()
|
|
|
+ this.getScenarios()
|
|
|
},
|
|
|
- created() { this.getList(); this.getScenarios() },
|
|
|
methods: {
|
|
|
- getList() { this.loading = true; listSalesCorpus(this.queryParams).then(res => { let d = res.data || {}; this.list = d.list || []; this.total = d.total || 0; this.loading = false }).catch(() => { this.loading = false }) },
|
|
|
+ getList() {
|
|
|
+ this.loading = true
|
|
|
+ listSalesCorpus(this.queryParams).then(res => {
|
|
|
+ let d = res.data || {}
|
|
|
+ this.list = d.list || []
|
|
|
+ this.total = d.total || 0
|
|
|
+ this.loading = false
|
|
|
+ }).catch(() => { this.loading = false })
|
|
|
+ },
|
|
|
getScenarios() { getCorpusScenarios().then(res => { this.scenarios = res.data || [] }) },
|
|
|
handleQuery() { this.queryParams.page = 1; this.getList() },
|
|
|
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
|
|
- handleAddDialog() { this.addVisible = true },
|
|
|
- submitAdd() { this.$refs.addForm.validate(v => { if (!v) return; addCorpusDialog(this.addForm).then(() => { this.$message.success('录入成功'); this.addVisible = false; this.getList() }) }) },
|
|
|
- handleAnalyze() { this.$confirm('确定触发AI语料分析吗?分析可能需要几分钟。', '提示', { type: 'warning' }).then(() => { analyzeCorpus().then(res => { let d = res.data || {}; this.$message.success('分析完成: 总录入' + d.totalEntries + '条, 评分' + d.overallScore) }) }) }
|
|
|
+
|
|
|
+ // 打开导入弹窗
|
|
|
+ handleImportDialog() {
|
|
|
+ this.importVisible = true
|
|
|
+ this.previewList = []
|
|
|
+ this.textContent = ''
|
|
|
+ this.importForm = { salespersonName: '', scenario: '' }
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.excelUpload) this.$refs.excelUpload.clearFiles()
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // Excel文件变化时解析
|
|
|
+ handleExcelChange(file) {
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ try {
|
|
|
+ const data = new Uint8Array(e.target.result)
|
|
|
+ const workbook = XLSX.read(data, { type: 'array' })
|
|
|
+ const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
|
|
+ const json = XLSX.utils.sheet_to_json(sheet, { header: 1 })
|
|
|
+ this.previewList = []
|
|
|
+ for (let i = 0; i < json.length; i++) {
|
|
|
+ const row = json[i]
|
|
|
+ if (!row || row.length < 2) continue
|
|
|
+ const customer = String(row[0] || '').trim()
|
|
|
+ const sales = String(row[1] || '').trim()
|
|
|
+ // 跳过表头行
|
|
|
+ if (i === 0 && (customer === '客户问题' || customer === '客户' || customer === 'customer' || customer === 'Customer')) continue
|
|
|
+ if (customer && sales) {
|
|
|
+ this.previewList.push({ customer, sales })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (this.previewList.length === 0) {
|
|
|
+ this.$message.warning('未解析到有效数据,请检查Excel格式')
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.$message.error('Excel解析失败: ' + err.message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ reader.readAsArrayBuffer(file.raw)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 解析文本内容 (a:/b: 格式)
|
|
|
+ parseTextContent() {
|
|
|
+ if (!this.textContent.trim()) {
|
|
|
+ this.$message.warning('请先输入对话内容')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const lines = this.textContent.split('\n')
|
|
|
+ const pairs = []
|
|
|
+ let currentCustomer = ''
|
|
|
+ let currentSales = ''
|
|
|
+ let hasA = false
|
|
|
+ let hasB = false
|
|
|
+
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
+ const line = lines[i].trim()
|
|
|
+ if (!line) {
|
|
|
+ if (hasA && hasB) {
|
|
|
+ pairs.push({ customer: currentCustomer.trim(), sales: currentSales.trim() })
|
|
|
+ }
|
|
|
+ currentCustomer = ''
|
|
|
+ currentSales = ''
|
|
|
+ hasA = false
|
|
|
+ hasB = false
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (line.startsWith('a:') || line.startsWith('A:')) {
|
|
|
+ if (hasA && hasB) {
|
|
|
+ pairs.push({ customer: currentCustomer.trim(), sales: currentSales.trim() })
|
|
|
+ currentCustomer = ''
|
|
|
+ currentSales = ''
|
|
|
+ hasA = false
|
|
|
+ hasB = false
|
|
|
+ }
|
|
|
+ currentCustomer = line.substring(2).trim()
|
|
|
+ hasA = true
|
|
|
+ } else if (line.startsWith('b:') || line.startsWith('B:')) {
|
|
|
+ currentSales = line.substring(2).trim()
|
|
|
+ hasB = true
|
|
|
+ } else if (hasA && !hasB) {
|
|
|
+ currentCustomer += ' ' + line
|
|
|
+ } else if (hasB) {
|
|
|
+ currentSales += ' ' + line
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (hasA && hasB) {
|
|
|
+ pairs.push({ customer: currentCustomer.trim(), sales: currentSales.trim() })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pairs.length === 0) {
|
|
|
+ this.$message.warning('未解析到有效对话,请检查格式:a:客户说的 b:销冠说的')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.previewList = pairs
|
|
|
+ this.$message.success('解析成功,共 ' + pairs.length + ' 条对话')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重置预览
|
|
|
+ resetPreview() {
|
|
|
+ this.previewList = []
|
|
|
+ },
|
|
|
+
|
|
|
+ // 提交导入
|
|
|
+ submitImport() {
|
|
|
+ if (this.previewList.length === 0) {
|
|
|
+ this.$message.warning('没有可导入的数据')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.submitLoading = true
|
|
|
+ const data = {
|
|
|
+ salespersonName: this.importForm.salespersonName || '销冠',
|
|
|
+ scenario: this.importForm.scenario || '通用',
|
|
|
+ dialogs: this.previewList.map(item => ({
|
|
|
+ customer: item.customer,
|
|
|
+ sales: item.sales
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ batchImportCorpus(data).then(res => {
|
|
|
+ this.$message.success('导入成功!共 ' + (res.data && res.data.count ? res.data.count : this.previewList.length) + ' 条')
|
|
|
+ this.importVisible = false
|
|
|
+ this.previewList = []
|
|
|
+ this.getList()
|
|
|
+ }).catch(err => {
|
|
|
+ this.$message.error('导入失败: ' + (err.msg || err.message || '未知错误'))
|
|
|
+ }).finally(() => {
|
|
|
+ this.submitLoading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ handleAnalyze() {
|
|
|
+ this.$confirm('确定触发AI语料分析吗?分析可能需要几分钟。', '提示', { type: 'warning' }).then(() => {
|
|
|
+ analyzeCorpus().then(res => {
|
|
|
+ let d = res.data || {}
|
|
|
+ this.$message.success('分析完成: 总录入' + d.totalEntries + '条, 评分' + d.overallScore)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
</script>
|