mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-12 04:16:45 +08:00
提示测试
This commit is contained in:
198
src/components/Course/SmartTagHint.vue
Normal file
198
src/components/Course/SmartTagHint.vue
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<div class="smart-tag-hint" v-if="showHint">
|
||||||
|
<div class="hint-content">
|
||||||
|
<div class="hint-header">
|
||||||
|
<i class="el-icon-light-rain"></i>
|
||||||
|
<span>智能标签建议</span>
|
||||||
|
<i class="el-icon-close" @click="closeHint"></i>
|
||||||
|
</div>
|
||||||
|
<div class="hint-body">
|
||||||
|
<!-- 基于分类的推荐 -->
|
||||||
|
<div v-if="categoryBasedTags.length > 0" class="tag-section">
|
||||||
|
<div class="section-title">基于内容分类的推荐</div>
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in categoryBasedTags"
|
||||||
|
:key="tag.id"
|
||||||
|
class="suggest-tag"
|
||||||
|
@click="applySuggestedTag(tag)"
|
||||||
|
type="info">
|
||||||
|
{{ tag.tagName }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基于课程名称的推荐 -->
|
||||||
|
<div v-if="nameBasedTags.length > 0" class="tag-section">
|
||||||
|
<div class="section-title">基于课程名称的推荐</div>
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in nameBasedTags"
|
||||||
|
:key="tag.id"
|
||||||
|
class="suggest-tag"
|
||||||
|
@click="applySuggestedTag(tag)"
|
||||||
|
type="info">
|
||||||
|
{{ tag.tagName }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 热门标签 -->
|
||||||
|
<div v-if="hotTags.length > 0" class="tag-section">
|
||||||
|
<div class="section-title">热门标签</div>
|
||||||
|
<el-tag
|
||||||
|
v-for="tag in hotTags"
|
||||||
|
:key="tag.id"
|
||||||
|
class="suggest-tag"
|
||||||
|
@click="applySuggestedTag(tag)"
|
||||||
|
type="warning">
|
||||||
|
{{ tag.tagName }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hint-footer">
|
||||||
|
<el-button size="mini" @click="dontShowAgain">不再显示</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
courseName: String,
|
||||||
|
sysTypeList: Array,
|
||||||
|
existingTags: Array,
|
||||||
|
maxTags: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showHint: false,
|
||||||
|
categoryBasedTags: [],
|
||||||
|
nameBasedTags: [],
|
||||||
|
hotTags: [],
|
||||||
|
hintDismissed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async showSmartHint() {
|
||||||
|
if (this.hintDismissed || this.existingTags.length >= this.maxTags) return;
|
||||||
|
|
||||||
|
// 获取智能推荐标签
|
||||||
|
await this.fetchSuggestedTags();
|
||||||
|
this.showHint = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchSuggestedTags() {
|
||||||
|
try {
|
||||||
|
// 基于分类获取推荐标签
|
||||||
|
if (this.sysTypeList.length > 0) {
|
||||||
|
const typeId = this.sysTypeList[this.sysTypeList.length - 1];
|
||||||
|
const { result } = await apiCourseTag.getTagsByCategory(typeId);
|
||||||
|
this.categoryBasedTags = result.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于课程名称关键词获取推荐
|
||||||
|
if (this.courseName) {
|
||||||
|
const keywords = this.extractKeywords(this.courseName);
|
||||||
|
if (keywords.length > 0) {
|
||||||
|
const { result } = await apiCourseTag.getTagsByKeywords(keywords);
|
||||||
|
this.nameBasedTags = result.slice(0, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取热门标签
|
||||||
|
const { result } = await apiCourseTag.getHotTags();
|
||||||
|
this.hotTags = result.slice(0, 3);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取推荐标签失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
extractKeywords(name) {
|
||||||
|
// 简单的关键词提取逻辑
|
||||||
|
return name.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ' ').split(' ')
|
||||||
|
.filter(word => word.length > 1).slice(0, 3);
|
||||||
|
},
|
||||||
|
|
||||||
|
applySuggestedTag(tag) {
|
||||||
|
this.$emit('tag-selected', tag);
|
||||||
|
this.showHint = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeHint() {
|
||||||
|
this.showHint = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
dontShowAgain() {
|
||||||
|
this.hintDismissed = true;
|
||||||
|
localStorage.setItem('tagHintDismissed', 'true');
|
||||||
|
this.closeHint();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.hintDismissed = localStorage.getItem('tagHintDismissed') === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.smart-tag-hint {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2000;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-content {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-body {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-tag {
|
||||||
|
margin: 2px 4px 2px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-tag:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-footer {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
:options="sysTypeListMap"></el-cascader>
|
:options="sysTypeListMap"></el-cascader>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="标签" required>
|
<el-form-item label="标签" required>
|
||||||
<courseTag ref="courseTag" :courseId="curCourseId" :sysTypeList="sysTypeList" :initialTags="courseTags" @change="handleTagsChange"></courseTag>
|
<courseTag ref="courseTag" :courseId="curCourseId" :sysTypeList="sysTypeList" :initialTags="courseTags" :courseName="courseInfo.name" @change="handleTagsChange"></courseTag>
|
||||||
<!-- 标签提示对话框 -->
|
<!-- 标签提示对话框 -->
|
||||||
<div v-if="tagTipDialogVisible && isFirstTime"
|
<div v-if="tagTipDialogVisible && isFirstTime"
|
||||||
class="tag-tip-dialog"
|
class="tag-tip-dialog"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
@keyup.enter.native="handleEnterKey"
|
@keyup.enter.native="handleEnterKey"
|
||||||
@keyup.delete.native="handleDeleteKey"
|
@keyup.delete.native="handleDeleteKey"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
ref="tagSelect"
|
ref="tagSelect"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -27,6 +28,18 @@
|
|||||||
:disabled="isTagDisabled(item)"
|
:disabled="isTagDisabled(item)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
|
<!-- 智能提示组件 -->
|
||||||
|
<smart-tag-hint
|
||||||
|
ref="smartHint"
|
||||||
|
:course-name="courseName"
|
||||||
|
:sys-type-list="sysTypeList"
|
||||||
|
:existing-tags="selectedTags"
|
||||||
|
:max-tags="maxTags"
|
||||||
|
@tag-selected="handleSmartTagSelect"
|
||||||
|
v-show="showSmartHint"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 添加标签计数显示 -->
|
<!-- 添加标签计数显示 -->
|
||||||
<div class="tag-count">
|
<div class="tag-count">
|
||||||
{{ selectedTags.length }}/5
|
{{ selectedTags.length }}/5
|
||||||
@@ -38,9 +51,17 @@
|
|||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import apiCourseTag from '@/api/modules/courseTag.js'
|
import apiCourseTag from '@/api/modules/courseTag.js'
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import SmartTagHint from './SmartTagHint.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
SmartTagHint
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
courseId:{
|
courseId:{
|
||||||
type:String,
|
type:String,
|
||||||
require:true,
|
require:true,
|
||||||
@@ -62,6 +83,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
showSmartHint: false,
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -138,11 +160,38 @@ export default {
|
|||||||
// 新增:处理输入框获得焦点事件
|
// 新增:处理输入框获得焦点事件
|
||||||
async handleFocus() {
|
async handleFocus() {
|
||||||
this.previousTags = [...this.selectedTags];
|
this.previousTags = [...this.selectedTags];
|
||||||
|
this.showSmartHint = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.$refs.smartHint) {
|
||||||
|
this.$refs.smartHint.showSmartHint();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
// 当输入框获得焦点时,加载默认的搜索结果
|
// 当输入框获得焦点时,加载默认的搜索结果
|
||||||
if (this.sysTypeList.length > 0) {
|
if (this.sysTypeList.length > 0) {
|
||||||
await this.doSearch('');
|
await this.doSearch('');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleBlur() {
|
||||||
|
// 延迟隐藏以避免点击建议标签时立即隐藏
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showSmartHint = false;
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
handleSmartTagSelect(tag) {
|
||||||
|
if (this.selectedTags.length >= this.maxTags) {
|
||||||
|
this.$message.warning(`最多只能添加${this.maxTags}个标签`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDuplicate = this.selectedTags.some(t => t.id === tag.id);
|
||||||
|
if (!isDuplicate) {
|
||||||
|
this.selectedTags = [...this.selectedTags, tag];
|
||||||
|
this.$emit('change', this.displayTags);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// 新增:重置标签状态的方法
|
// 新增:重置标签状态的方法
|
||||||
resetTagState() {
|
resetTagState() {
|
||||||
this.selectedTags = [];
|
this.selectedTags = [];
|
||||||
@@ -316,10 +365,24 @@ export default {
|
|||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
this.$message.info('无此标签,按回车键创建')
|
this.$message.info('无此标签,按回车键创建')
|
||||||
}
|
}
|
||||||
|
// 如果有查询词,显示智能匹配提示
|
||||||
|
if (query.trim()) {
|
||||||
|
this.showSmartMatching(query);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
showSmartMatching(query) {
|
||||||
|
// 在搜索结果中高亮匹配项
|
||||||
|
this.searchResults = this.searchResults.map(tag => {
|
||||||
|
return {
|
||||||
|
...tag,
|
||||||
|
isSmartMatch: tag.tagName.toLowerCase().includes(query.toLowerCase())
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user