|
|
@@ -72,6 +72,95 @@
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
|
|
|
+ <el-form-item label="AI是否发送新客先导课" prop="sendCourseStatus">
|
|
|
+ <el-switch
|
|
|
+ v-model="form.sendCourseStatus"
|
|
|
+ :active-value="1"
|
|
|
+ :inactive-value="0"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ active-color="#13ce66"
|
|
|
+ inactive-color="#ff4949"
|
|
|
+ />
|
|
|
+ <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
|
|
|
+ <i class="el-icon-info"></i>
|
|
|
+ AI与客户聊天中是否发送新客先导课,默认关闭,开启后,与客户聊天会发送新客先导课
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="新客先导课" prop="courseId">
|
|
|
+ <el-popover
|
|
|
+ placement="bottom"
|
|
|
+ width="300"
|
|
|
+ trigger="click"
|
|
|
+ :visible="coursePopoverVisible"
|
|
|
+ @show="onCoursePopoverShow"
|
|
|
+ >
|
|
|
+ <div class="course-select-popover">
|
|
|
+ <el-input
|
|
|
+ v-if="courseFilterable"
|
|
|
+ placeholder="输入关键字过滤"
|
|
|
+ v-model="courseFilterText"
|
|
|
+ size="mini"
|
|
|
+ clearable
|
|
|
+ class="course-filter-input"
|
|
|
+ >
|
|
|
+ </el-input>
|
|
|
+
|
|
|
+ <div class="course-list-container" :style="{ height: courseListHeight + 'px' }">
|
|
|
+ <div
|
|
|
+ ref="courseVirtualList"
|
|
|
+ class="course-virtual-list"
|
|
|
+ @scroll="handleCourseScroll"
|
|
|
+ :style="{ height: courseListHeight + 'px', overflow: 'auto' }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="course-virtual-content"
|
|
|
+ :style="{
|
|
|
+ height: courseTotalHeight + 'px',
|
|
|
+ position: 'relative',
|
|
|
+ paddingTop: courseOffsetY + 'px'
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in courseVisibleItems"
|
|
|
+ :key="`${item.videoId}-${index}`"
|
|
|
+ :style="{
|
|
|
+ height: courseItemHeight + 'px',
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ padding: '0 10px'
|
|
|
+ }"
|
|
|
+ :class="[
|
|
|
+ 'course-virtual-item',
|
|
|
+ {
|
|
|
+ 'is-selected': form.courseId === item.videoId,
|
|
|
+ 'is-disabled': item.disabled
|
|
|
+ }
|
|
|
+ ]"
|
|
|
+ @click="handleCourseItemClick(item)"
|
|
|
+ >
|
|
|
+ <span class="course-label" :title="item.title">{{ item.title }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-button slot="reference" style="min-width: 150px; max-width: 300px;">
|
|
|
+ {{ form.courseId ? getCourseTitle(form.courseId) : '请选择课程' }}
|
|
|
+ <i class="el-icon-arrow-down el-icon--right"></i>
|
|
|
+ </el-button>
|
|
|
+ </el-popover>
|
|
|
+
|
|
|
+ <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
|
|
|
+ <i class="el-icon-info"></i>
|
|
|
+ 选择课程:客户添加聊天后会根据聊天内容发送这节课程
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
<el-form-item label="客服头像" prop="avatar">
|
|
|
<ImageUpload v-model="form.avatar" type="image" :num="1" :width="150" :height="150" style="margin-top: 1%;" />
|
|
|
</el-form-item>
|
|
|
@@ -237,7 +326,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { listFastGptRole, getFastGptRole, delFastGptRole, addFastGptRole, updateFastGptRole, exportFastGptRole,getAllRoleType } from "@/api/fastGpt/fastGptRole";
|
|
|
+import { listFastGptRole, getFastGptRole, delFastGptRole, addFastGptRole, updateFastGptRole, exportFastGptRole,getAllRoleType,getAllCourseList } from "@/api/fastGpt/fastGptRole";
|
|
|
import {allListTagGroup} from "@/api/qw/tagGroup";
|
|
|
import {listTag} from "@/api/qw/tag";
|
|
|
import {
|
|
|
@@ -264,6 +353,23 @@ export default {
|
|
|
exportLoading: false,
|
|
|
//AI客服类型
|
|
|
typeOptions: [],
|
|
|
+
|
|
|
+ // 课程选择相关数据
|
|
|
+ coursePopoverVisible: false,
|
|
|
+ courseFilterText: '',
|
|
|
+ courseFilterable: true,
|
|
|
+ courseListHeight: 300, // 课程列表高度
|
|
|
+ courseItemHeight: 36, // 每个课程项的高度
|
|
|
+ courseStartIndex: 0,
|
|
|
+ courseEndIndex: 0,
|
|
|
+ courseScrollTop: 0,
|
|
|
+ courseVisibleItemCount: 0,
|
|
|
+ courseFlattenedNodes: [], // 扁平化的课程数据
|
|
|
+ courseNodeMap: new Map(), // 课程节点映射
|
|
|
+ courseUpdateTimer: null,
|
|
|
+ courseScrollRAF: null,
|
|
|
+ courseFilterDebounced: null,
|
|
|
+
|
|
|
//AI模型
|
|
|
modeOptions: [],
|
|
|
//渠道类型
|
|
|
@@ -337,7 +443,11 @@ export default {
|
|
|
channelType: null,
|
|
|
logistics: null,
|
|
|
forbidStatus: null,
|
|
|
+ sendCourseStatus: null,
|
|
|
+ courseId: null,
|
|
|
},
|
|
|
+ // ... 其他数据
|
|
|
+ courseListLoaded: false,
|
|
|
// 表单参数
|
|
|
form: {
|
|
|
},
|
|
|
@@ -434,9 +544,35 @@ export default {
|
|
|
deep: true
|
|
|
}
|
|
|
},
|
|
|
- created() {
|
|
|
-
|
|
|
+ computed: {
|
|
|
+ courseTotalHeight() {
|
|
|
+ return this.courseVisibleNodes.length * this.courseItemHeight;
|
|
|
+ },
|
|
|
+ courseOffsetY() {
|
|
|
+ return this.courseStartIndex * this.courseItemHeight;
|
|
|
+ },
|
|
|
+ courseVisibleNodes() {
|
|
|
+ let nodes = this.courseFlattenedNodes;
|
|
|
+ if (this.courseFilterText.trim()) {
|
|
|
+ nodes = this.applyCourseFilter(nodes);
|
|
|
+ }
|
|
|
+ return nodes;
|
|
|
+ },
|
|
|
+ courseVisibleItems() {
|
|
|
+ return this.courseVisibleNodes.slice(this.courseStartIndex, this.courseEndIndex);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 清理定时器
|
|
|
+ if (this.courseUpdateTimer) clearTimeout(this.courseUpdateTimer);
|
|
|
+ if (this.courseScrollRAF) cancelAnimationFrame(this.courseScrollRAF);
|
|
|
+ },
|
|
|
+ async created() {
|
|
|
this.handleUpdate();
|
|
|
+ await this.loadCourseList();
|
|
|
+ this.courseListLoaded = true
|
|
|
+ // 初始化课程选择相关数据
|
|
|
+ this.initCourseData();
|
|
|
//客服类型
|
|
|
// this.getDicts("chat_role_type").then((response) => {
|
|
|
// this.typeOptions = response.data;
|
|
|
@@ -457,7 +593,269 @@ export default {
|
|
|
this.typeOptions = response.data;
|
|
|
});
|
|
|
},
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ // 确保 DOM 已渲染完成
|
|
|
+ this.initScrollListener();
|
|
|
+ });
|
|
|
+ },
|
|
|
methods: {
|
|
|
+ // 初始化课程选择相关数据
|
|
|
+ initCourseData() {
|
|
|
+ this.courseVisibleItemCount = Math.ceil(this.courseListHeight / this.courseItemHeight) + 5;
|
|
|
+
|
|
|
+ // 防抖函数
|
|
|
+ this.handleCourseFilterDebounced = this.debounce(this.filterCourseTextChange, 300);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 防抖函数
|
|
|
+ debounce(func, wait) {
|
|
|
+ let timeout;
|
|
|
+ return function executedFunction(...args) {
|
|
|
+ const later = () => {
|
|
|
+ clearTimeout(timeout);
|
|
|
+ func(...args);
|
|
|
+ };
|
|
|
+ clearTimeout(timeout);
|
|
|
+ timeout = setTimeout(later, wait);
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理课程数据
|
|
|
+ processCourseData(data) {
|
|
|
+ if (!Array.isArray(data)) {
|
|
|
+ this.courseFlattenedNodes = [];
|
|
|
+ this.courseNodeMap.clear();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const flattened = [];
|
|
|
+ const nodeMap = new Map();
|
|
|
+
|
|
|
+ data.forEach((item, index) => {
|
|
|
+ const courseItem = {
|
|
|
+ videoId: item.videoId && typeof item.videoId === 'string' ? item.videoId : String(item.videoId || Math.random()),
|
|
|
+ title: item.title && typeof item.title === 'string' ? item.title : String(item.title || item.name || item.videoId || '未命名课程'),
|
|
|
+ originalData: item,
|
|
|
+ disabled: false
|
|
|
+ };
|
|
|
+
|
|
|
+ flattened.push(courseItem);
|
|
|
+ nodeMap.set(courseItem.videoId, courseItem);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.courseFlattenedNodes = flattened;
|
|
|
+ this.courseNodeMap = nodeMap;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 应用课程过滤
|
|
|
+ applyCourseFilter(nodesToFilter) {
|
|
|
+ const searchText = this.courseFilterText.toLowerCase().trim();
|
|
|
+ if (!searchText) return nodesToFilter;
|
|
|
+
|
|
|
+ return nodesToFilter.filter(node =>
|
|
|
+ node.title.toLowerCase().includes(searchText)
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ // 计算可见范围
|
|
|
+ calculateCourseVisibleRange() {
|
|
|
+ const containerHeight = this.courseListHeight;
|
|
|
+ const totalItems = this.courseVisibleNodes.length;
|
|
|
+
|
|
|
+ const newStartIndex = Math.floor(this.courseScrollTop / this.courseItemHeight);
|
|
|
+ const visibleCountInViewport = Math.ceil(containerHeight / this.courseItemHeight);
|
|
|
+
|
|
|
+ const buffer = 5;
|
|
|
+ this.courseStartIndex = Math.max(0, newStartIndex - buffer);
|
|
|
+ this.courseEndIndex = Math.min(totalItems, newStartIndex + visibleCountInViewport + buffer);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 滚动处理
|
|
|
+ handleCourseScroll(e) {
|
|
|
+ const newScrollTop = e.target.scrollTop;
|
|
|
+ if (newScrollTop !== this.courseScrollTop) {
|
|
|
+ this.courseScrollTop = newScrollTop;
|
|
|
+ if (!this.courseScrollRAF) {
|
|
|
+ this.courseScrollRAF = requestAnimationFrame(() => {
|
|
|
+ this.calculateCourseVisibleRange();
|
|
|
+ this.courseScrollRAF = null;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重置滚动
|
|
|
+ resetCourseScroll() {
|
|
|
+ this.courseScrollTop = 0;
|
|
|
+ if (this.$refs.courseVirtualList) {
|
|
|
+ this.$refs.courseVirtualList.scrollTop = 0;
|
|
|
+ }
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.calculateCourseVisibleRange();
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 课程过滤文本变化
|
|
|
+ filterCourseTextChange() {
|
|
|
+ this.resetCourseScroll();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 课程项点击
|
|
|
+ handleCourseItemClick(course) {
|
|
|
+ if (course.disabled) return;
|
|
|
+ this.form.courseId = course.videoId;
|
|
|
+ this.coursePopoverVisible = false;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 课程弹出框显示
|
|
|
+ onCoursePopoverShow() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.resetCourseScroll();
|
|
|
+ if (this.courseFilterable && this.$refs.courseVirtualList) {
|
|
|
+ const inputEl = this.$refs.courseVirtualList.parentElement.querySelector('.course-filter-input input');
|
|
|
+ if (inputEl) inputEl.focus();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 初始化滚动监听器
|
|
|
+ initScrollListener() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const container = this.$refs.courseSelectContainer;
|
|
|
+ if (container) {
|
|
|
+ // 移除之前的事件监听器,避免重复绑定
|
|
|
+ container.removeEventListener('scroll', this.handleScroll);
|
|
|
+
|
|
|
+ // 添加新的滚动事件监听器
|
|
|
+ container.addEventListener('scroll', this.handleScroll);
|
|
|
+
|
|
|
+ // 添加初始调试信息
|
|
|
+ console.log('滚动监听器已初始化');
|
|
|
+ console.log('容器总高度:', container.scrollHeight);
|
|
|
+ console.log('可见高度:', container.clientHeight);
|
|
|
+ console.log('课程总数:', this.allCourseOptions.length);
|
|
|
+ console.log('当前显示:', this.displayCourseOptions.length);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ getCourseTitle(videoId) {
|
|
|
+ if (!videoId) return '请选择课程';
|
|
|
+
|
|
|
+ console.log(videoId);
|
|
|
+ // 优先从处理后的课程数据中查找
|
|
|
+ const course = this.courseNodeMap.get(String(videoId));
|
|
|
+ if (course) {
|
|
|
+ return course.title;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果在虚拟滚动数据中找不到,尝试从原始数据中查找
|
|
|
+ const originalCourse = this.allCourseOptions.find(item => item.videoId === videoId);
|
|
|
+ return originalCourse ? originalCourse.title : '未找到对应课程';
|
|
|
+ },
|
|
|
+ // 滚动处理函数
|
|
|
+ // 滚动处理函数 - 改进检测逻辑
|
|
|
+ // 滚动处理函数 - 改进检测逻辑
|
|
|
+ handleScroll(event) {
|
|
|
+ const container = event.target;
|
|
|
+ const { scrollTop, scrollHeight, clientHeight } = container;
|
|
|
+
|
|
|
+ // 更精确的滚动到底部检测
|
|
|
+ const threshold = 5; // 阈值调整为5px
|
|
|
+ if (scrollTop + clientHeight >= scrollHeight - threshold &&
|
|
|
+ !this.loadingMore &&
|
|
|
+ this.hasMore) {
|
|
|
+ this.loadMoreCourses();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修复滚动加载方法,添加更详细的日志
|
|
|
+ // 修复后的滚动加载方法
|
|
|
+ async loadMoreCourses() {
|
|
|
+ if (this.loadingMore || !this.hasMore) {
|
|
|
+ console.log('加载条件不满足:', { loadingMore: this.loadingMore, hasMore: this.hasMore });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('开始加载更多课程');
|
|
|
+ this.loadingMore = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 短暂延迟以避免频繁请求
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
+
|
|
|
+ // 使用全部课程列表
|
|
|
+ const sourceList = this.allCourseOptions;
|
|
|
+
|
|
|
+ // 修复分页逻辑:从 (currentPage - 1) * pageSize 开始
|
|
|
+ const start = (this.currentPage - 1) * this.pageSize;
|
|
|
+ const end = start + this.pageSize;
|
|
|
+ const moreCourses = sourceList.slice(start, end);
|
|
|
+
|
|
|
+ console.log('加载的课程范围:', { start, end, count: moreCourses.length });
|
|
|
+
|
|
|
+ if (moreCourses.length > 0) {
|
|
|
+ // 添加新课程到显示列表(注意:使用 concat 或扩展运算符避免重复添加)
|
|
|
+ this.displayCourseOptions = [...this.displayCourseOptions, ...moreCourses];
|
|
|
+ this.currentPage++; // 递增页码
|
|
|
+ // 检查是否还有更多课程
|
|
|
+ this.hasMore = end < sourceList.length;
|
|
|
+ console.log('更新后的课程列表长度:', this.displayCourseOptions.length, '还有更多:', this.hasMore);
|
|
|
+ } else {
|
|
|
+ // 没有更多课程时停止加载
|
|
|
+ this.hasMore = false;
|
|
|
+ console.log('没有更多课程可加载');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载更多课程失败:', error);
|
|
|
+ this.hasMore = false;
|
|
|
+ } finally {
|
|
|
+ this.loadingMore = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async loadCourseList() {
|
|
|
+ try {
|
|
|
+ const response = await getAllCourseList();
|
|
|
+ if (Array.isArray(response.data)) {
|
|
|
+ this.courseOptions = response.data
|
|
|
+ .filter(item => item !== null && item !== undefined)
|
|
|
+ .map(item => ({
|
|
|
+ ...item,
|
|
|
+ videoId: item.videoId && typeof item.videoId === 'string' ? item.videoId : String(item.videoId || Math.random()),
|
|
|
+ title: item.title && typeof item.title === 'string' ? item.title : String(item.title || item.name || item.videoId || '未命名课程')
|
|
|
+ }));
|
|
|
+
|
|
|
+ this.allCourseOptions = [...this.courseOptions];
|
|
|
+ // 重置分页参数 - 从第一页开始
|
|
|
+ this.currentPage = 1;
|
|
|
+ // 默认展示前100条
|
|
|
+ this.displayCourseOptions = this.courseOptions.slice(0, this.pageSize);
|
|
|
+ this.hasMore = this.courseOptions.length > this.pageSize;
|
|
|
+
|
|
|
+ // 处理课程数据为虚拟滚动格式
|
|
|
+ this.processCourseData(this.courseOptions);
|
|
|
+ } else {
|
|
|
+ console.warn('getAllCourseList 返回的数据不是数组格式:', response.data);
|
|
|
+ this.courseOptions = [];
|
|
|
+ this.displayCourseOptions = [];
|
|
|
+ this.allCourseOptions = [];
|
|
|
+ this.hasMore = false;
|
|
|
+ this.courseFlattenedNodes = [];
|
|
|
+ this.courseNodeMap.clear();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('课程列表加载失败:', error);
|
|
|
+ this.courseOptions = [];
|
|
|
+ this.displayCourseOptions = [];
|
|
|
+ this.allCourseOptions = [];
|
|
|
+ this.hasMore = false;
|
|
|
+ this.courseFlattenedNodes = [];
|
|
|
+ this.courseNodeMap.clear();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
/** 查询应用列表 */
|
|
|
getList() {
|
|
|
this.loading = true;
|