boss vor 1 Woche
Ursprung
Commit
4ee48ccf48
3 geänderte Dateien mit 252 neuen und 36 gelöschten Zeilen
  1. 2 1
      package.json
  2. 1 1
      src/api/workflow/lobster.js
  3. 249 34
      src/views/lobster/sales-corpus/index.vue

+ 2 - 1
package.json

@@ -89,7 +89,8 @@
     "vue2-ace-editor": "0.0.15",
     "vue2-ace-editor": "0.0.15",
     "vuedraggable": "^2.20.0",
     "vuedraggable": "^2.20.0",
     "vuex": "3.1.0",
     "vuex": "3.1.0",
-    "wangeditor": "^4.6.13"
+    "wangeditor": "^4.6.13",
+    "xlsx": "^0.18.5"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@vue/cli-plugin-babel": "4.4.4",
     "@vue/cli-plugin-babel": "4.4.4",

+ 1 - 1
src/api/workflow/lobster.js

@@ -122,7 +122,7 @@ export function addCorpusDialog(data) {
 }
 }
 
 
 export function batchImportCorpus(data) {
 export function batchImportCorpus(data) {
-  return request({ url: '/workflow/lobster/sales-corpus/batch-import', method: 'post', data })
+  return request({ url: '/workflow/lobster/sales-corpus/batch-import', method: 'post', data, timeout: 60000 })
 }
 }
 
 
 export function analyzeCorpus() {
 export function analyzeCorpus() {

+ 249 - 34
src/views/lobster/sales-corpus/index.vue

@@ -1,13 +1,6 @@
 <template>
 <template>
   <div class="app-container">
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
     <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-form-item label="场景" prop="scenario">
         <el-select v-model="queryParams.scenario" placeholder="请选择场景" clearable size="small">
         <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" />
           <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>
       <el-form-item label="状态" prop="status">
       <el-form-item label="状态" prop="status">
         <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
         <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-select>
       </el-form-item>
       </el-form-item>
       <el-form-item>
       <el-form-item>
@@ -24,58 +17,280 @@
       </el-form-item>
       </el-form-item>
     </el-form>
     </el-form>
     <el-row :gutter="10" class="mb8">
     <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>
       <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>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
     </el-row>
     <el-table border v-loading="loading" :data="list">
     <el-table border v-loading="loading" :data="list">
       <el-table-column label="ID" align="center" prop="id" width="60" />
       <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="scenario" width="100" />
       <el-table-column label="状态" align="center" prop="status" width="80">
       <el-table-column label="状态" align="center" prop="status" width="80">
         <template slot-scope="scope">
         <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>
         </template>
       </el-table-column>
       </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>
     </el-table>
     <pagination v-show="total>0" :total="total" :page.sync="queryParams.page" :limit.sync="queryParams.size" @pagination="getList" />
     <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: 你们这个产品多少钱?&#10;b: 咱们这个产品根据配置不同价格也不一样,您主要想用在哪个场景呢?&#10;&#10;a: 我主要是想用在客服场景&#10;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>
-        <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>
       </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>
     </el-dialog>
   </div>
   </div>
 </template>
 </template>
 <script>
 <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 {
 export default {
   name: 'SalesCorpus',
   name: 'SalesCorpus',
   data() {
   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: {
   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 || [] }) },
     getScenarios() { getCorpusScenarios().then(res => { this.scenarios = res.data || [] }) },
     handleQuery() { this.queryParams.page = 1; this.getList() },
     handleQuery() { this.queryParams.page = 1; this.getList() },
     resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
     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>
 </script>