fastGptRoleUpdate.vue 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279
  1. <template>
  2. <div class="app-container">
  3. <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  4. <el-form-item label="客服名称" prop="roleName">
  5. <el-input v-model="form.roleName" placeholder="请输入角色名" />
  6. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  7. <i class="el-icon-info"></i>
  8. AI角色名字
  9. </div>
  10. </el-form-item>
  11. <el-form-item label="角色类型" prop="roleType">
  12. <el-select v-model="form.roleType" placeholder="请选择类型" clearable size="small" style="width: 150px" >
  13. <el-option
  14. v-for="item in typeOptions"
  15. :key="item.dictValue"
  16. :label="item.dictLabel"
  17. :value="String(item.dictValue)"
  18. />
  19. </el-select>
  20. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  21. <i class="el-icon-info"></i>
  22. 选择角色类型:一般选择伴学助手,医生工作室,伴学助手无性别,无性别版本只会叫用户同学,其余的会叫用户先生女士
  23. </div>
  24. </el-form-item>
  25. <el-form-item label="渠道类型" prop="channelType">
  26. <el-select v-model="form.channelType" placeholder="请选择类型" clearable size="small" style="width: 150px" >
  27. <el-option
  28. v-for="item in channelOptions"
  29. :key="item.dictValue"
  30. :label="item.dictLabel"
  31. :value="String(item.dictLabel)"
  32. />
  33. </el-select>
  34. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  35. <i class="el-icon-info"></i>
  36. 选择进线渠道:只影响新客户进来的首次对话
  37. </div>
  38. </el-form-item>
  39. <el-form-item label="物流提醒" prop="logistics">
  40. <el-switch
  41. v-model="form.logistics"
  42. :active-value="1"
  43. :inactive-value="0"
  44. active-text="是"
  45. inactive-text="否"
  46. active-color="#13ce66"
  47. inactive-color="#ff4949"
  48. />
  49. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  50. <i class="el-icon-info"></i>
  51. AI是否主动发送物流信息,发货和到货的时候会提醒用户
  52. </div>
  53. </el-form-item>
  54. <el-form-item label="禁止夜间回复" prop="forbidStatus">
  55. <el-switch
  56. v-model="form.forbidStatus"
  57. :active-value="1"
  58. :inactive-value="0"
  59. active-text="是"
  60. inactive-text="否"
  61. active-color="#13ce66"
  62. inactive-color="#ff4949"
  63. />
  64. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  65. <i class="el-icon-info"></i>
  66. AI是否规定时间内不回复消息,默认为开启,在结束时间后再进行最后一条消息回复,关闭后一天24小时都进行回复
  67. </div>
  68. </el-form-item>
  69. <el-form-item label="AI是否发送新客先导课" prop="sendCourseStatus">
  70. <el-switch
  71. v-model="form.sendCourseStatus"
  72. :active-value="1"
  73. :inactive-value="0"
  74. active-text="是"
  75. inactive-text="否"
  76. active-color="#13ce66"
  77. inactive-color="#ff4949"
  78. />
  79. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  80. <i class="el-icon-info"></i>
  81. AI与客户聊天中是否发送新客先导课,默认关闭,开启后,与客户聊天会发送新客先导课
  82. </div>
  83. </el-form-item>
  84. <el-form-item label="新客先导课" prop="courseId">
  85. <el-popover
  86. placement="bottom"
  87. width="300"
  88. trigger="click"
  89. :visible="coursePopoverVisible"
  90. @show="onCoursePopoverShow"
  91. >
  92. <div class="course-select-popover">
  93. <el-input
  94. v-if="courseFilterable"
  95. placeholder="输入关键字过滤"
  96. v-model="courseFilterText"
  97. size="mini"
  98. clearable
  99. class="course-filter-input"
  100. >
  101. </el-input>
  102. <div class="course-list-container" :style="{ height: courseListHeight + 'px' }">
  103. <div
  104. ref="courseVirtualList"
  105. class="course-virtual-list"
  106. @scroll="handleCourseScroll"
  107. :style="{ height: courseListHeight + 'px', overflow: 'auto' }"
  108. >
  109. <div
  110. class="course-virtual-content"
  111. :style="{
  112. height: courseTotalHeight + 'px',
  113. position: 'relative',
  114. paddingTop: courseOffsetY + 'px'
  115. }"
  116. >
  117. <div
  118. v-for="(item, index) in courseVisibleItems"
  119. :key="`${item.videoId}-${index}`"
  120. :style="{
  121. height: courseItemHeight + 'px',
  122. display: 'flex',
  123. alignItems: 'center',
  124. padding: '0 10px'
  125. }"
  126. :class="[
  127. 'course-virtual-item',
  128. {
  129. 'is-selected': form.courseId === item.videoId,
  130. 'is-disabled': item.disabled
  131. }
  132. ]"
  133. @click="handleCourseItemClick(item)"
  134. >
  135. <span class="course-label" :title="item.title">{{ item.title }}</span>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. <el-button slot="reference" style="min-width: 150px; max-width: 300px;">
  142. {{ form.courseId ? getCourseTitle(form.courseId) : '请选择课程' }}
  143. <i class="el-icon-arrow-down el-icon--right"></i>
  144. </el-button>
  145. </el-popover>
  146. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  147. <i class="el-icon-info"></i>
  148. 选择课程:客户添加聊天后会根据聊天内容发送这节课程
  149. </div>
  150. </el-form-item>
  151. <el-form-item label="客服头像" prop="avatar">
  152. <ImageUpload v-model="form.avatar" type="image" :num="1" :width="150" :height="150" style="margin-top: 1%;" />
  153. </el-form-item>
  154. <el-form-item label="APPKey" >
  155. <el-input type="textarea" v-model="form.modeConfigJson.APPKey" placeholder="请输入APPKey(特定key)" />
  156. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  157. <i class="el-icon-info"></i>
  158. 根据管理员发送的APPKey填写
  159. </div>
  160. </el-form-item>
  161. <el-form-item label="提示词" >
  162. <el-input type="textarea" :rows="3" v-model="form.reminderWords" placeholder="请输入提示词" />
  163. <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
  164. <i class="el-icon-info"></i>
  165. 输入个人的介绍,并且在最后增加一个自我介绍(用于首次交流触发),模板可以找管理员获取
  166. </div>
  167. </el-form-item>
  168. <!-- <el-form-item label="标签人设" >
  169. <div v-if="tagsFormList.length > 0" style="display: flex; align-items: center; flex-wrap: wrap; width: 100%;">
  170. <div style="min-height: 40px; max-height: 200px; overflow-y: auto; width: 100%;">
  171. <div style="display: flex; flex-wrap: wrap; width: 100%;">
  172. <div v-for="(tagsForm, index) in tagsFormList" :key="tagsForm.id">
  173. <el-tag type="success"
  174. closable
  175. :disable-transitions="false"
  176. @click="handleEditRoleTag(tagsForm, index)"
  177. @close="handleCloseRoleTag(tagsForm, index)"
  178. style="margin: 3px;">
  179. {{ getTagNames(tagsForm.tagIds) }}
  180. </el-tag>
  181. </div>
  182. </div>
  183. </div>
  184. </div>
  185. <el-button
  186. size="mini"
  187. type="primary" plain
  188. icon="el-icon-circle-plus-outline"
  189. @click="handleAddTags(form.roleId,form.bindCorpId)"
  190. v-hasPermi="['fastGptRole:fastGptRole:edit']"
  191. >添加标签人设</el-button>
  192. </el-form-item> -->
  193. <!-- <el-form-item label="修改栏目" prop="contactInfo">
  194. <el-select v-model="contactInfo" multiple filterable placeholder="请选择修改栏目" clearable size="small" style="width: 50%" >
  195. <el-option
  196. v-for="item in externalInfoOptions"
  197. :key="item.dictValue"
  198. :label="item.dictLabel"
  199. :value="item.dictValue"
  200. />
  201. </el-select>
  202. </el-form-item> -->
  203. </el-form>
  204. <div slot="footer" class="dialog-footer" style="padding-bottom: 40px;">
  205. <el-button type="primary" @click="submitForm" style="float: right;margin-right: 20px;">确 定</el-button>
  206. <el-button @click="cancel" style="float: right;margin-right: 20px;">取 消</el-button>
  207. </div>
  208. <!-- 修改-->
  209. <el-dialog :title="changeTagsOpen.title" :visible.sync="changeTagsOpen.open" width="800px" append-to-body>
  210. <el-form ref="tagsForm" :model="tagsForm" :rules="tagsFormRules">
  211. <el-form-item label="选择标签" prop="tagIds">
  212. <div @click="handleChangeTags(changeTagsOpen.roleId)" style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 100%">
  213. <div style="min-height: 40px; max-height: 200px; overflow-y: auto;">
  214. <el-tag type="success"
  215. closable
  216. :disable-transitions="false"
  217. v-for="list in tagListFormIndex"
  218. :key="list.tagId"
  219. @close="handleCloseTag(list)"
  220. style="margin: 3px;"
  221. >{{list.name}}
  222. </el-tag>
  223. </div>
  224. </div>
  225. </el-form-item>
  226. <el-form-item label="提示词" prop="reminderWords">
  227. <el-input type="textarea" :rows="6" v-model="tagsForm.reminderWords" placeholder="请输入AI客服该标签的提示词" />
  228. </el-form-item>
  229. </el-form>
  230. <div slot="footer" class="dialog-footer">
  231. <el-button type="primary" @click="submitTagsForm(changeTagsOpen.roleId)">确 定</el-button>
  232. <el-button @click="cancelTags">取 消</el-button>
  233. </div>
  234. </el-dialog>
  235. <el-dialog :title="tagChange.title" :visible.sync="tagChange.open" width="800px" append-to-body>
  236. <div v-for="item in tagGroupList" :key="item.id" >
  237. <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
  238. <span class="name-background">{{ item.name }}</span>
  239. </div>
  240. <div class="tag-container">
  241. <a
  242. v-for="tagItem in item.tag"
  243. class="tag-box"
  244. @click="tagSelection(tagItem)"
  245. :class="{ 'tag-selected': tagItem.isSelected }"
  246. >
  247. {{ tagItem.name }}
  248. </a>
  249. </div>
  250. </div>
  251. <div slot="footer" class="dialog-footer">
  252. <el-button type="primary" @click="editTagSubmitForm(tagChange.roleId)">确 定</el-button>
  253. <el-button @click="editTagCancel(tagChange.roleId)">取消</el-button>
  254. </div>
  255. </el-dialog>
  256. <!-- 新增-->
  257. <el-dialog :title="changeTagsOpenAdd.title" :visible.sync="changeTagsOpenAdd.open" width="800px" append-to-body>
  258. <el-form ref="tagsFormAdd" :model="tagsFormAdd" :rules="tagsFormAddRules">
  259. <el-form-item label="选择标签" prop="tagIds">
  260. <div @click="handleChangeTagsAdd(changeTagsOpenAdd.roleId)" style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 100%">
  261. <div style="min-height: 40px; max-height: 200px; overflow-y: auto;">
  262. <el-tag type="success"
  263. closable
  264. :disable-transitions="false"
  265. v-for="list in tagListFormIndexAdd"
  266. :key="list.tagId"
  267. @close="handleCloseTag(list)"
  268. style="margin: 3px;"
  269. >{{list.name}}
  270. </el-tag>
  271. </div>
  272. </div>
  273. </el-form-item>
  274. <el-form-item label="提示词" prop="reminderWords">
  275. <el-input type="textarea" :rows="6" v-model="tagsFormAdd.reminderWords" placeholder="请输入AI客服该标签的提示词" />
  276. </el-form-item>
  277. </el-form>
  278. <div slot="footer" class="dialog-footer">
  279. <el-button type="primary" @click="submitAddTagsForm(changeTagsOpenAdd.roleId)">确 定</el-button>
  280. <el-button @click="cancelAddTags">取 消</el-button>
  281. </div>
  282. </el-dialog>
  283. <el-dialog :title="tagChangeAdd.title" :visible.sync="tagChangeAdd.open" width="800px" append-to-body>
  284. <div v-for="item in tagGroupList" :key="item.id" >
  285. <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
  286. <span class="name-background">{{ item.name }}</span>
  287. </div>
  288. <div class="tag-container">
  289. <a
  290. v-for="tagItem in item.tag"
  291. class="tag-box"
  292. @click="tagSelection(tagItem)"
  293. :class="{ 'tag-selected': tagItem.isSelected }"
  294. >
  295. {{ tagItem.name }}
  296. </a>
  297. </div>
  298. </div>
  299. <div slot="footer" class="dialog-footer">
  300. <el-button type="primary" @click="addTagSubmitForm(tagChangeAdd.roleId)">确 定</el-button>
  301. <el-button @click="addTagCancel(tagChangeAdd.roleId)">取消</el-button>
  302. </div>
  303. </el-dialog>
  304. </div>
  305. </template>
  306. <script>
  307. import { listFastGptRole, getFastGptRole, delFastGptRole, addFastGptRole, updateFastGptRole, exportFastGptRole,getAllRoleType,getAllCourseList } from "@/api/fastGpt/fastGptRole";
  308. import {allListTagGroup} from "@/api/qw/tagGroup";
  309. import {listTag} from "@/api/qw/tag";
  310. import {
  311. addFastGptRoleTag,
  312. delFastGptRoleTag,
  313. getListByRoleId,
  314. updateFastGptRoleTag
  315. } from "@/api/fastGpt/fastGptRoleTag";
  316. import source from "echarts/src/data/Source";
  317. import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
  318. export default {
  319. name: "fastGptRoleUpdate",
  320. components: {ImageUpload},
  321. data() {
  322. return {
  323. logisticsOptions: [
  324. { label: '是', value: 1 },
  325. { label: '否', value: 0 }
  326. ],
  327. // 遮罩层
  328. loading: true,
  329. // 导出遮罩层
  330. exportLoading: false,
  331. //AI客服类型
  332. typeOptions: [],
  333. // 课程选择相关数据
  334. coursePopoverVisible: false,
  335. courseFilterText: '',
  336. courseFilterable: true,
  337. courseListHeight: 300, // 课程列表高度
  338. courseItemHeight: 36, // 每个课程项的高度
  339. courseStartIndex: 0,
  340. courseEndIndex: 0,
  341. courseScrollTop: 0,
  342. courseVisibleItemCount: 0,
  343. courseFlattenedNodes: [], // 扁平化的课程数据
  344. courseNodeMap: new Map(), // 课程节点映射
  345. courseUpdateTimer: null,
  346. courseScrollRAF: null,
  347. courseFilterDebounced: null,
  348. //AI模型
  349. modeOptions: [],
  350. //渠道类型
  351. channelOptions: [],
  352. changeTagsOpen: {
  353. title : "",
  354. open :false,
  355. roleId : null,
  356. },
  357. changeTagsOpenAdd:{
  358. title : "",
  359. open :false,
  360. roleId : null,
  361. },
  362. //标签弹窗选择
  363. tagChange:{
  364. open:false,
  365. title:"",
  366. roleId:null,
  367. id:null,
  368. },
  369. //新增
  370. tagChangeAdd:{
  371. open:false,
  372. title:"",
  373. roleId:null,
  374. id:null,
  375. },
  376. //所有的标签组-标签
  377. tagGroupList:[],
  378. //所有的标签
  379. tagList:[],
  380. contactInfo:[],
  381. //已经选择的标签
  382. tagListFormIndex:[],
  383. //新的标签
  384. tagListFormIndexAdd:[],
  385. // uploadUrl:process.env.VUE_APP_BASE_API+"/chat/upload/uploadFile",
  386. uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
  387. // 选中数组
  388. ids: [],
  389. externalInfoOptions : [],
  390. // 非单个禁用
  391. single: true,
  392. // 非多个禁用
  393. multiple: true,
  394. // 显示搜索条件
  395. showSearch: true,
  396. // 总条数
  397. total: 0,
  398. // 应用表格数据
  399. fastGptRoleList: [],
  400. // 弹出层标题
  401. title: "",
  402. // 是否显示弹出层
  403. open: false,
  404. // 查询参数
  405. queryParams: {
  406. pageNum: 1,
  407. pageSize: 10,
  408. roleName: null,
  409. companyId: null,
  410. roleType: null,
  411. mode: null,
  412. kfId: null,
  413. kfUrl: null,
  414. avatar: null,
  415. kfMediaId: null,
  416. channelType: null,
  417. logistics: null,
  418. forbidStatus: null,
  419. sendCourseStatus: null,
  420. courseId: null,
  421. },
  422. // ... 其他数据
  423. courseListLoaded: false,
  424. // 表单参数
  425. form: {
  426. },
  427. //某一个标签组
  428. tagsForm:{},
  429. //新增
  430. tagsFormAdd:{},
  431. //当前role的所有标签组
  432. tagsFormList:[],
  433. // 表单校验
  434. rules: {
  435. roleName: [
  436. { required: true, message: "角色名称不能为空", trigger: "blur" }
  437. ],
  438. roleType: [
  439. { required: true, message: "角色类型不能为空", trigger: "change" }
  440. ],
  441. logistics: [
  442. { required: true, message: "物流提醒不能为空", trigger: "change" }
  443. ],
  444. forbidStatus: [
  445. { required: true, message: "禁止夜间回复不能为空", trigger: "change" }
  446. ],
  447. },
  448. tagsFormRules:{
  449. tagIds:[
  450. { required: true, message: "标签不能为空", trigger: "blur" }
  451. ],
  452. reminderWords:[
  453. { required: true, message: "提示词不能为空", trigger: "blur" }
  454. ]
  455. },
  456. tagsFormAddRules:{
  457. tagIds:[
  458. { required: true, message: "标签不能为空", trigger: "blur" }
  459. ],
  460. reminderWords:[
  461. { required: true, message: "提示词不能为空", trigger: "blur" }
  462. ]
  463. }
  464. };
  465. },
  466. watch: {
  467. tagListFormIndex: {
  468. handler(newList) {
  469. if (!Array.isArray(newList)) return; // 仅在 newList 为数组时继续
  470. // 确保 tagsForm.tagIds 是数组并清空它
  471. this.tagsForm.tagIds = [];
  472. // 遍历 newList,添加 tagId
  473. newList.forEach(tags => {
  474. if (Array.isArray(tags)) {
  475. // 如果 tags 是数组,遍历并添加
  476. tags.forEach(item => {
  477. if (item && item.tagId !== undefined) {
  478. this.tagsForm.tagIds.push(item.tagId);
  479. }
  480. });
  481. } else if (tags && tags.tagId !== undefined) {
  482. // 如果 tags 是单个对象,直接添加
  483. this.tagsForm.tagIds.push(tags.tagId);
  484. }
  485. });
  486. },
  487. deep: true
  488. },
  489. tagListFormIndexAdd: {
  490. handler(newList) {
  491. if (!Array.isArray(newList)) return; // 仅在 newList 为数组时继续
  492. // 确保 tagsForm.tagIds 是数组并清空它
  493. this.tagsFormAdd.tagIds = [];
  494. // 遍历 newList,添加 tagId
  495. newList.forEach(tags => {
  496. if (Array.isArray(tags)) {
  497. // 如果 tags 是数组,遍历并添加
  498. tags.forEach(item => {
  499. if (item && item.tagId !== undefined) {
  500. this.tagsFormAdd.tagIds.push(item.tagId);
  501. }
  502. });
  503. } else if (tags && tags.tagId !== undefined) {
  504. // 如果 tags 是单个对象,直接添加
  505. this.tagsFormAdd.tagIds.push(tags.tagId);
  506. }
  507. });
  508. },
  509. deep: true
  510. }
  511. },
  512. computed: {
  513. courseTotalHeight() {
  514. return this.courseVisibleNodes.length * this.courseItemHeight;
  515. },
  516. courseOffsetY() {
  517. return this.courseStartIndex * this.courseItemHeight;
  518. },
  519. courseVisibleNodes() {
  520. let nodes = this.courseFlattenedNodes;
  521. if (this.courseFilterText.trim()) {
  522. nodes = this.applyCourseFilter(nodes);
  523. }
  524. return nodes;
  525. },
  526. courseVisibleItems() {
  527. return this.courseVisibleNodes.slice(this.courseStartIndex, this.courseEndIndex);
  528. }
  529. },
  530. beforeDestroy() {
  531. // 清理定时器
  532. if (this.courseUpdateTimer) clearTimeout(this.courseUpdateTimer);
  533. if (this.courseScrollRAF) cancelAnimationFrame(this.courseScrollRAF);
  534. },
  535. async created() {
  536. this.handleUpdate();
  537. await this.loadCourseList();
  538. this.courseListLoaded = true
  539. // 初始化课程选择相关数据
  540. this.initCourseData();
  541. //客服类型
  542. // this.getDicts("chat_role_type").then((response) => {
  543. // this.typeOptions = response.data;
  544. // });
  545. //AI模型
  546. this.getDicts("chat_role_mode").then((response) => {
  547. this.modeOptions = response.data;
  548. });
  549. this.getDicts("sys_fastgpt_role_external_info").then((response) => {
  550. this.externalInfoOptions = response.data;
  551. });
  552. //渠道类型
  553. this.getDicts("sys_fastgpt_channel_type").then((response) => {
  554. this.channelOptions = response.data;
  555. });
  556. getAllRoleType().then(response => {
  557. this.typeOptions = response.data;
  558. });
  559. },
  560. mounted() {
  561. this.$nextTick(() => {
  562. // 确保 DOM 已渲染完成
  563. this.initScrollListener();
  564. });
  565. },
  566. methods: {
  567. // 初始化课程选择相关数据
  568. initCourseData() {
  569. this.courseVisibleItemCount = Math.ceil(this.courseListHeight / this.courseItemHeight) + 5;
  570. // 防抖函数
  571. this.handleCourseFilterDebounced = this.debounce(this.filterCourseTextChange, 300);
  572. },
  573. // 防抖函数
  574. debounce(func, wait) {
  575. let timeout;
  576. return function executedFunction(...args) {
  577. const later = () => {
  578. clearTimeout(timeout);
  579. func(...args);
  580. };
  581. clearTimeout(timeout);
  582. timeout = setTimeout(later, wait);
  583. };
  584. },
  585. // 处理课程数据
  586. processCourseData(data) {
  587. if (!Array.isArray(data)) {
  588. this.courseFlattenedNodes = [];
  589. this.courseNodeMap.clear();
  590. return;
  591. }
  592. const flattened = [];
  593. const nodeMap = new Map();
  594. data.forEach((item, index) => {
  595. const courseItem = {
  596. videoId: item.videoId && typeof item.videoId === 'string' ? item.videoId : String(item.videoId || Math.random()),
  597. title: item.title && typeof item.title === 'string' ? item.title : String(item.title || item.name || item.videoId || '未命名课程'),
  598. originalData: item,
  599. disabled: false
  600. };
  601. flattened.push(courseItem);
  602. nodeMap.set(courseItem.videoId, courseItem);
  603. });
  604. this.courseFlattenedNodes = flattened;
  605. this.courseNodeMap = nodeMap;
  606. },
  607. // 应用课程过滤
  608. applyCourseFilter(nodesToFilter) {
  609. const searchText = this.courseFilterText.toLowerCase().trim();
  610. if (!searchText) return nodesToFilter;
  611. return nodesToFilter.filter(node =>
  612. node.title.toLowerCase().includes(searchText)
  613. );
  614. },
  615. // 计算可见范围
  616. calculateCourseVisibleRange() {
  617. const containerHeight = this.courseListHeight;
  618. const totalItems = this.courseVisibleNodes.length;
  619. const newStartIndex = Math.floor(this.courseScrollTop / this.courseItemHeight);
  620. const visibleCountInViewport = Math.ceil(containerHeight / this.courseItemHeight);
  621. const buffer = 5;
  622. this.courseStartIndex = Math.max(0, newStartIndex - buffer);
  623. this.courseEndIndex = Math.min(totalItems, newStartIndex + visibleCountInViewport + buffer);
  624. },
  625. // 滚动处理
  626. handleCourseScroll(e) {
  627. const newScrollTop = e.target.scrollTop;
  628. if (newScrollTop !== this.courseScrollTop) {
  629. this.courseScrollTop = newScrollTop;
  630. if (!this.courseScrollRAF) {
  631. this.courseScrollRAF = requestAnimationFrame(() => {
  632. this.calculateCourseVisibleRange();
  633. this.courseScrollRAF = null;
  634. });
  635. }
  636. }
  637. },
  638. // 重置滚动
  639. resetCourseScroll() {
  640. this.courseScrollTop = 0;
  641. if (this.$refs.courseVirtualList) {
  642. this.$refs.courseVirtualList.scrollTop = 0;
  643. }
  644. this.$nextTick(() => {
  645. this.calculateCourseVisibleRange();
  646. });
  647. },
  648. // 课程过滤文本变化
  649. filterCourseTextChange() {
  650. this.resetCourseScroll();
  651. },
  652. // 课程项点击
  653. handleCourseItemClick(course) {
  654. if (course.disabled) return;
  655. this.form.courseId = course.videoId;
  656. this.coursePopoverVisible = false;
  657. },
  658. // 课程弹出框显示
  659. onCoursePopoverShow() {
  660. this.$nextTick(() => {
  661. this.resetCourseScroll();
  662. if (this.courseFilterable && this.$refs.courseVirtualList) {
  663. const inputEl = this.$refs.courseVirtualList.parentElement.querySelector('.course-filter-input input');
  664. if (inputEl) inputEl.focus();
  665. }
  666. });
  667. },
  668. // 初始化滚动监听器
  669. initScrollListener() {
  670. this.$nextTick(() => {
  671. const container = this.$refs.courseSelectContainer;
  672. if (container) {
  673. // 移除之前的事件监听器,避免重复绑定
  674. container.removeEventListener('scroll', this.handleScroll);
  675. // 添加新的滚动事件监听器
  676. container.addEventListener('scroll', this.handleScroll);
  677. // 添加初始调试信息
  678. console.log('滚动监听器已初始化');
  679. console.log('容器总高度:', container.scrollHeight);
  680. console.log('可见高度:', container.clientHeight);
  681. console.log('课程总数:', this.allCourseOptions.length);
  682. console.log('当前显示:', this.displayCourseOptions.length);
  683. }
  684. });
  685. },
  686. getCourseTitle(videoId) {
  687. if (!videoId) return '请选择课程';
  688. console.log(videoId);
  689. // 优先从处理后的课程数据中查找
  690. const course = this.courseNodeMap.get(String(videoId));
  691. if (course) {
  692. return course.title;
  693. }
  694. // 如果在虚拟滚动数据中找不到,尝试从原始数据中查找
  695. const originalCourse = this.allCourseOptions.find(item => item.videoId === videoId);
  696. return originalCourse ? originalCourse.title : '未找到对应课程';
  697. },
  698. // 滚动处理函数
  699. // 滚动处理函数 - 改进检测逻辑
  700. // 滚动处理函数 - 改进检测逻辑
  701. handleScroll(event) {
  702. const container = event.target;
  703. const { scrollTop, scrollHeight, clientHeight } = container;
  704. // 更精确的滚动到底部检测
  705. const threshold = 5; // 阈值调整为5px
  706. if (scrollTop + clientHeight >= scrollHeight - threshold &&
  707. !this.loadingMore &&
  708. this.hasMore) {
  709. this.loadMoreCourses();
  710. }
  711. },
  712. // 修复滚动加载方法,添加更详细的日志
  713. // 修复后的滚动加载方法
  714. async loadMoreCourses() {
  715. if (this.loadingMore || !this.hasMore) {
  716. console.log('加载条件不满足:', { loadingMore: this.loadingMore, hasMore: this.hasMore });
  717. return;
  718. }
  719. console.log('开始加载更多课程');
  720. this.loadingMore = true;
  721. try {
  722. // 短暂延迟以避免频繁请求
  723. await new Promise(resolve => setTimeout(resolve, 300));
  724. // 使用全部课程列表
  725. const sourceList = this.allCourseOptions;
  726. // 修复分页逻辑:从 (currentPage - 1) * pageSize 开始
  727. const start = (this.currentPage - 1) * this.pageSize;
  728. const end = start + this.pageSize;
  729. const moreCourses = sourceList.slice(start, end);
  730. console.log('加载的课程范围:', { start, end, count: moreCourses.length });
  731. if (moreCourses.length > 0) {
  732. // 添加新课程到显示列表(注意:使用 concat 或扩展运算符避免重复添加)
  733. this.displayCourseOptions = [...this.displayCourseOptions, ...moreCourses];
  734. this.currentPage++; // 递增页码
  735. // 检查是否还有更多课程
  736. this.hasMore = end < sourceList.length;
  737. console.log('更新后的课程列表长度:', this.displayCourseOptions.length, '还有更多:', this.hasMore);
  738. } else {
  739. // 没有更多课程时停止加载
  740. this.hasMore = false;
  741. console.log('没有更多课程可加载');
  742. }
  743. } catch (error) {
  744. console.error('加载更多课程失败:', error);
  745. this.hasMore = false;
  746. } finally {
  747. this.loadingMore = false;
  748. }
  749. },
  750. async loadCourseList() {
  751. try {
  752. const response = await getAllCourseList();
  753. if (Array.isArray(response.data)) {
  754. this.courseOptions = response.data
  755. .filter(item => item !== null && item !== undefined)
  756. .map(item => ({
  757. ...item,
  758. videoId: item.videoId && typeof item.videoId === 'string' ? item.videoId : String(item.videoId || Math.random()),
  759. title: item.title && typeof item.title === 'string' ? item.title : String(item.title || item.name || item.videoId || '未命名课程')
  760. }));
  761. this.allCourseOptions = [...this.courseOptions];
  762. // 重置分页参数 - 从第一页开始
  763. this.currentPage = 1;
  764. // 默认展示前100条
  765. this.displayCourseOptions = this.courseOptions.slice(0, this.pageSize);
  766. this.hasMore = this.courseOptions.length > this.pageSize;
  767. // 处理课程数据为虚拟滚动格式
  768. this.processCourseData(this.courseOptions);
  769. } else {
  770. console.warn('getAllCourseList 返回的数据不是数组格式:', response.data);
  771. this.courseOptions = [];
  772. this.displayCourseOptions = [];
  773. this.allCourseOptions = [];
  774. this.hasMore = false;
  775. this.courseFlattenedNodes = [];
  776. this.courseNodeMap.clear();
  777. }
  778. } catch (error) {
  779. console.error('课程列表加载失败:', error);
  780. this.courseOptions = [];
  781. this.displayCourseOptions = [];
  782. this.allCourseOptions = [];
  783. this.hasMore = false;
  784. this.courseFlattenedNodes = [];
  785. this.courseNodeMap.clear();
  786. }
  787. },
  788. /** 查询应用列表 */
  789. getList() {
  790. this.loading = true;
  791. listFastGptRole(this.queryParams).then(response => {
  792. this.fastGptRoleList = response.rows;
  793. this.total = response.total;
  794. this.loading = false;
  795. });
  796. },
  797. // 取消按钮
  798. cancel() {
  799. this.reset();
  800. window.location.replace('/fastGpt/fastGptRole')
  801. },
  802. cancelTags(){
  803. this.changeTagsOpen.open=false;
  804. },
  805. cancelAddTags(){
  806. this.changeTagsOpenAdd.open=false;
  807. },
  808. handleAvatarSuccess(res, file) {
  809. if(res.code==200){
  810. if(res.data.errcode!=0){
  811. this.msgError(res.data.errmsg);
  812. }
  813. else{
  814. //获取图片
  815. this.form.kfMediaId=res.data.media_id;
  816. this.form.avatar = res.ossUrl;
  817. this.$forceUpdate()
  818. }
  819. }
  820. else{
  821. this.msgError(res.msg);
  822. }
  823. },
  824. beforeAvatarUpload(file) {
  825. const isLt1M = file.size / 1024 / 1024 < 1;
  826. if (!isLt1M) {
  827. this.$message.error('上传图片大小不能超过 1MB!');
  828. }
  829. return isLt1M;
  830. },
  831. // 表单重置
  832. reset() {
  833. this.form = {
  834. roleId: null,
  835. roleName: null,
  836. companyId: null,
  837. createTime: null,
  838. updateTime: null,
  839. roleType: null,
  840. modeConfigJson:{APPKey:null,Key:null},
  841. mode: 2,
  842. kfId: null,
  843. kfUrl: null,
  844. avatar: null,
  845. kfMediaId: null,
  846. reminderWords: null,
  847. isTags:null,
  848. };
  849. this.resetForm("form");
  850. },
  851. tagRest(){
  852. this.tagListFormIndex=[];
  853. this.tagsForm={
  854. tagIds:null,
  855. reminderWords:null,
  856. };
  857. },
  858. tagAddRest(){
  859. this.tagListFormIndexAdd=[];
  860. this.tagsFormAdd={
  861. tagIds:null,
  862. reminderWords:null,
  863. };
  864. },
  865. /** 搜索按钮操作 */
  866. handleQuery() {
  867. this.queryParams.pageNum = 1;
  868. this.getList();
  869. },
  870. /** 重置按钮操作 */
  871. resetQuery() {
  872. this.resetForm("queryForm");
  873. this.handleQuery();
  874. },
  875. // 多选框选中数据
  876. handleSelectionChange(selection) {
  877. this.ids = selection.map(item => item.roleId)
  878. this.single = selection.length!==1
  879. this.multiple = !selection.length
  880. },
  881. /** 修改按钮操作 */
  882. handleUpdate() {
  883. var id=this.$route.params.command
  884. this.reset();
  885. getFastGptRole(id).then(response => {
  886. this.form = response.role;
  887. this.form.roleType = JSON.stringify(response.role.roleType);
  888. if(this.form.modeConfigJson!=null&&this.form.modeConfigJson!=""){
  889. this.form.modeConfigJson=JSON.parse(this.form.modeConfigJson)
  890. }else{
  891. this.form.modeConfigJson={APPKey:null}
  892. }
  893. if(this.form.contactInfo!=null){
  894. this.contactInfo = (this.form.contactInfo).split(",");
  895. }
  896. //含标签吗
  897. getListByRoleId(id).then(res => {
  898. this.tagsFormList=res.rows;
  899. })
  900. allListTagGroup({corpId:this.form.corpId}).then(response => {
  901. this.tagGroupList = response.rows;
  902. });
  903. //所有的标签
  904. listTag({corpId:this.form.corpId}).then(response => {
  905. this.tagList = response.rows;
  906. });
  907. this.title = "修改应用";
  908. });
  909. },
  910. getTagNames(tagIds) {
  911. // 确保 tagIds 是数组,如果是字符串则分割
  912. const idsArray = Array.isArray(tagIds) ? tagIds : tagIds.split(",");
  913. return idsArray.map(tagId => {
  914. const tag = this.tagList.find(list => list.tagId === tagId);
  915. return tag ? tag.name : tagId; // 返回标签名称或原 tagId
  916. }).join(', '); // 使用逗号和空格连接标签名称
  917. },
  918. //标签回复人设弹窗
  919. handleAddTags(value,bindCorpId) {
  920. if (bindCorpId==null){
  921. this.$message({
  922. message: '请先给AI客服-绑定企业 再添加标签【企微管理->员工管理->企微员工】',
  923. type: 'warning'
  924. });
  925. return;
  926. }
  927. this.changeTagsOpenAdd.title="创建标签回复人设";
  928. this.changeTagsOpenAdd.open=true;
  929. this.changeTagsOpenAdd.roleId=value;
  930. },
  931. //选择标签弹窗
  932. handleChangeTags(roleId){
  933. this.tagChange.open=true;
  934. this.tagChange.title='选择标签';
  935. this.tagChange.roleId=roleId;
  936. // 获取 tagListFormIndex 中的所有 tagId,用于快速查找
  937. const selectedTagIds = new Set(this.tagListFormIndex.map(tagItem => tagItem.tagId));
  938. for (let i = 0; i < this.tagGroupList.length; i++) {
  939. for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
  940. this.tagGroupList[i].tag[x].isSelected = selectedTagIds.has(this.tagGroupList[i].tag[x].tagId);
  941. }
  942. }
  943. },
  944. handleChangeTagsAdd(roleId){
  945. this.tagChangeAdd.open=true;
  946. this.tagChangeAdd.title='选择标签';
  947. this.tagChangeAdd.roleId=roleId;
  948. },
  949. //标签的选择
  950. tagSelection(row){
  951. row.isSelected= !row.isSelected;
  952. this.$forceUpdate();
  953. },
  954. //删除一些已经选择了的标签
  955. handleCloseTag(list){
  956. // 假设 list 对象具有一个 id 属性
  957. const ls = this.tagListFormIndex.findIndex(t => t.tagId === list.tagId);
  958. if (ls !== -1) {
  959. this.tagListFormIndex.splice(ls, 1);
  960. this.tagListFormIndex = [...this.tagListFormIndex];
  961. }
  962. },
  963. //删除一下已经存在的
  964. handleCloseRoleTag(row){
  965. delFastGptRoleTag(row.id).then(res => {
  966. getListByRoleId(row.roleId).then(res => {
  967. this.tagsFormList=res.rows;
  968. })
  969. })
  970. },
  971. //点击单个
  972. handleEditRoleTag(value, index) {
  973. this.changeTagsOpen.open = true;
  974. this.changeTagsOpen.title = "编辑标签回复人设";
  975. this.changeTagsOpen.roleId = value.roleId;
  976. this.tagsForm = value;
  977. // 过滤 tagList,获取匹配的标签
  978. const tagIdsArray = Array.isArray(value.tagIds) ? value.tagIds : value.tagIds.split(",");
  979. this.tagListFormIndex = this.tagList.filter(tag => tagIdsArray.includes(tag.tagId));
  980. },
  981. //选择标签
  982. editTagSubmitForm(){
  983. for (let i = 0; i < this.tagGroupList.length; i++) {
  984. for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
  985. if (this.tagGroupList[i].tag[x].isSelected === true) {
  986. if (!this.tagListFormIndex) {
  987. this.tagListFormIndex = [];
  988. }
  989. // 检查当前 tag 是否已经存在于 tagListFormIndex[index] 中
  990. let tagExists = this.tagListFormIndex.some(
  991. tag => tag.id === this.tagGroupList[i].tag[x].id
  992. );
  993. // 如果 tag 不存在于 tagListFormIndex[index] 中,则新增
  994. if (!tagExists) {
  995. this.tagListFormIndex.push(this.tagGroupList[i].tag[x]);
  996. }
  997. }
  998. }
  999. }
  1000. if (!this.tagListFormIndex || this.tagListFormIndex.length === 0) {
  1001. return this.$message('请选择标签');
  1002. }
  1003. this.tagChange.open = false;
  1004. },
  1005. //新增的标签
  1006. addTagSubmitForm(){
  1007. for (let i = 0; i < this.tagGroupList.length; i++) {
  1008. for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
  1009. if (this.tagGroupList[i].tag[x].isSelected === true) {
  1010. if (!this.tagListFormIndexAdd) {
  1011. this.tagListFormIndexAdd = [];
  1012. }
  1013. // 检查当前 tag 是否已经存在于 tagListFormIndex[index] 中
  1014. let tagExists = this.tagListFormIndexAdd.some(
  1015. tag => tag.id === this.tagGroupList[i].tag[x].id
  1016. );
  1017. // 如果 tag 不存在于 tagListFormIndex[index] 中,则新增
  1018. if (!tagExists) {
  1019. this.tagListFormIndexAdd.push(this.tagGroupList[i].tag[x]);
  1020. }
  1021. }
  1022. }
  1023. }
  1024. if (!this.tagListFormIndexAdd || this.tagListFormIndexAdd.length === 0) {
  1025. return this.$message('请选择标签');
  1026. }
  1027. this.tagChangeAdd.open = false;
  1028. },
  1029. //取消选择标签
  1030. editTagCancel(){
  1031. this.tagChange.open = false;
  1032. // this.tagRest();
  1033. },
  1034. addTagCancel(){
  1035. this.tagChangeAdd.open = false;
  1036. this.tagAddRest();
  1037. },
  1038. /** 提交按钮 */
  1039. submitForm() {
  1040. if(this.contactInfo!=null){
  1041. this.form.contactInfo= (this.contactInfo).toString()
  1042. }
  1043. this.$refs["form"].validate(valid => {
  1044. if (valid) {
  1045. this.form.modeConfigJson=JSON.stringify(this.form.modeConfigJson)
  1046. if (this.form.roleId != null) {
  1047. updateFastGptRole(this.form).then(response => {
  1048. this.msgSuccess("修改成功");
  1049. window.location.replace('/fastGpt/fastGptRole')
  1050. });
  1051. }
  1052. }
  1053. });
  1054. },
  1055. submitTagsForm(roleId){
  1056. this.$refs["tagsForm"].validate(valid => {
  1057. if (valid) {
  1058. this.tagsForm.roleId=roleId
  1059. this.tagsForm.tagIds=this.tagsForm.tagIds.join(",")
  1060. if (this.tagsForm.id!= null) {
  1061. updateFastGptRoleTag(this.tagsForm).then(response => {
  1062. this.msgSuccess("修改成功");
  1063. this.changeTagsOpen = false;
  1064. this.getList();
  1065. });
  1066. }else {
  1067. addFastGptRoleTag(this.tagsForm).then(response => {
  1068. this.msgSuccess("新增成功");
  1069. this.changeTagsOpen = false;
  1070. this.getList();
  1071. });
  1072. }
  1073. }
  1074. });
  1075. },
  1076. submitAddTagsForm(roleId){
  1077. this.$refs["tagsFormAdd"].validate(valid => {
  1078. if (valid) {
  1079. this.tagsFormAdd.roleId=roleId
  1080. this.tagsFormAdd.tagIds=this.tagsFormAdd.tagIds.join(",")
  1081. addFastGptRoleTag(this.tagsFormAdd).then(response => {
  1082. this.msgSuccess("新增成功");
  1083. this.changeTagsOpenAdd = false;
  1084. this.open=false;
  1085. this.handleUpdate();
  1086. });
  1087. }
  1088. });
  1089. },
  1090. /** 删除按钮操作 */
  1091. handleDelete(row) {
  1092. const roleIds = row.roleId || this.ids;
  1093. this.$confirm('是否确认删除应用编号为"' + roleIds + '"的数据项?', "警告", {
  1094. confirmButtonText: "确定",
  1095. cancelButtonText: "取消",
  1096. type: "warning"
  1097. }).then(function() {
  1098. return delFastGptRole(roleIds);
  1099. }).then(() => {
  1100. this.getList();
  1101. this.msgSuccess("删除成功");
  1102. }).catch(() => {});
  1103. },
  1104. /** 导出按钮操作 */
  1105. handleExport() {
  1106. const queryParams = this.queryParams;
  1107. this.$confirm('是否确认导出所有应用数据项?', "警告", {
  1108. confirmButtonText: "确定",
  1109. cancelButtonText: "取消",
  1110. type: "warning"
  1111. }).then(() => {
  1112. this.exportLoading = true;
  1113. return exportFastGptRole(queryParams);
  1114. }).then(response => {
  1115. this.download(response.msg);
  1116. this.exportLoading = false;
  1117. }).catch(() => {});
  1118. }
  1119. }
  1120. };
  1121. </script>
  1122. <style scoped>
  1123. /* CSS 样式 */
  1124. .tag-container {
  1125. display: flex;
  1126. flex-wrap: wrap; /* 超出宽度时自动换行 */
  1127. gap: 8px; /* 设置标签之间的间距 */
  1128. }
  1129. .name-background {
  1130. display: inline-block;
  1131. background-color: #abece6; /* 背景颜色 */
  1132. padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
  1133. border-radius: 4px; /* 可选:设置圆角 */
  1134. }
  1135. .tag-box {
  1136. padding: 8px 12px;
  1137. border: 1px solid #989797;
  1138. border-radius: 4px;
  1139. cursor: pointer;
  1140. display: inline-block;
  1141. }
  1142. .tag-selected {
  1143. background-color: #00bc98;
  1144. color: #fff;
  1145. border-color: #00bc98;
  1146. }
  1147. .el-tag + .el-tag {
  1148. margin-left: 10px;
  1149. }
  1150. .button-new-tag {
  1151. margin-left: 10px;
  1152. height: 32px;
  1153. line-height: 30px;
  1154. padding-top: 0;
  1155. padding-bottom: 0;
  1156. }
  1157. .input-new-tag {
  1158. width: 90px;
  1159. margin-left: 10px;
  1160. vertical-align: bottom;
  1161. }
  1162. .text-container {
  1163. max-height: 5em; /* 设置最大高度为6行,根据字体大小调整 */
  1164. overflow-y: auto; /* 内容超出时显示滚动条 */
  1165. line-height: 1.5em; /* 行高设置,确保每行高度一致 */
  1166. }
  1167. </style>