feat(product): 添加产品推荐功能并优化相关页面

- 新增产品推荐页面,提供个性化的产品推荐服务
- 优化首页布局,增加产品推荐入口
- 调整聊天组件,支持产品推荐功能
- 优化样式文件,统一主题颜色和样式
- 更新路由配置,添加产品推荐路由
This commit is contained in:
陈昱达
2025-06-30 13:27:29 +08:00
parent ac240892dd
commit 8cb956ba6d
7 changed files with 254 additions and 64 deletions

View File

@@ -4,7 +4,7 @@
@import './utils.scss'; @import './utils.scss';
@import './transition.scss'; @import './transition.scss';
@import './public.scss'; @import './public.scss';
@import "./loading.scss"; @import './loading.scss';
* { * {
margin: 0; margin: 0;
@@ -33,7 +33,7 @@ a:hover {
outline: none; outline: none;
text-decoration: none; text-decoration: none;
} }
::-webkit-scrollbar{ ::-webkit-scrollbar {
display: none; display: none;
} }
// 宽度设置 // 宽度设置

View File

@@ -1,61 +1,62 @@
body{ body {
background-color: #EEEEEE!important; background-color: #eeeeee !important;
} }
.van-cell-group--inset{ .van-cell-group--inset {
margin:0 10px!important; margin: 0 10px !important;
} }
.van-cell-group__title--inset{ .van-cell-group__title--inset {
padding:15px 15px 5px 15px!important; padding: 15px 15px 5px 15px !important;
} }
.van-toast--text { .van-toast--text {
max-width: 70vw !important; max-width: 70vw !important;
min-width: 60vw !important; min-width: 60vw !important;
} }
.button-group-container{ .button-group-container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
#app .van-col{ #app .van-col {
margin: 5px 0; margin: 5px 0;
} }
#app .button-group-container{ #app .button-group-container {
margin: 5px!important; margin: 5px !important;
.van-button{ .van-button {
margin: unset; margin: unset;
} }
} }
#app .van-button + .van-button{ #app .van-button + .van-button {
margin-left: 5px; margin-left: 5px;
} }
#app .van-cell__title{ #app .van-cell__title {
color: #646566; color: #646566;
} }
.flexPrice {
.flexPrice{ display: flex;
display: flex; justify-content: space-between; justify-items: center; align-items: center; width: 100% justify-content: space-between;
justify-items: center;
align-items: center;
width: 100%;
} }
.van-tag+.van-tag{margin-left: 5px;} .van-tag + .van-tag {
.speakLoadingToast{ margin-left: 5px;
z-index: 2100!important; }
.speakLoadingToast {
z-index: 2100 !important;
width: 200px; width: 200px;
} }
.gs-color-g {
.gs-color-g{
color: #009960; color: #009960;
} }
.gs-bg-color-g{ .gs-bg-color-g {
background: #009960; background: #009960;
} }
.chat-icon {
.chat-icon{
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-left: 0!important; margin-left: 0 !important;
margin-right: 2px!important; margin-right: 2px !important;
} }

View File

@@ -23,4 +23,12 @@ export default [
title: '产品对比', title: '产品对比',
}, },
}, },
{
path: '/recommend',
name: 'recommend',
component: () => import('@/views/recommend/index.vue'),
meta: {
title: '产品推荐',
},
},
] ]

View File

@@ -90,6 +90,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
chatData: {
type: Object,
default: () => ({}),
},
}, },
data() { data() {
return { return {
@@ -220,6 +224,12 @@ export default {
conversationId: this.conversationId, conversationId: this.conversationId,
productName: this.productName, productName: this.productName,
} }
// 如果有自定义参数
if (this.chatData) {
for (let k in this.chatData) {
params[k] = this.chatData[k]
}
}
if (this.$route.query.compareId) { if (this.$route.query.compareId) {
params.compareResult = JSON.parse(sessionStorage.getItem('results')) params.compareResult = JSON.parse(sessionStorage.getItem('results'))
} }

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="home-container"> <div class="home-container">
<van-swipe class="my-swipe"> <van-swipe class="my-swipe">
<van-swipe-item class="item"> <van-swipe-item class="item" v-for="swiper in swiper">
<div v-for="item in navigationItems" @click="jumpPage(item)"> <div v-for="item in swiper" @click="jumpPage(item)">
<div class="icon-contact mt20"> <div class="icon-contact mt20">
<svg-icon :icon-class="item.icon" class-name="icon "></svg-icon> <svg-icon :icon-class="item.icon" class-name="icon "></svg-icon>
</div> </div>
@@ -39,10 +39,14 @@ export default {
}, },
data() { data() {
return { return {
navigationItems: [ swiper: [
[
{ title: 'AI智能助手', icon: 'product', path: '/chatPage' }, { title: 'AI智能助手', icon: 'product', path: '/chatPage' },
{ title: 'AI客服助手', icon: 'sale', path: '/customer' },
{ title: '产品对比', icon: 'earth', path: '/comparison' }, { title: '产品对比', icon: 'earth', path: '/comparison' },
{ title: '产品推荐', icon: 'product', path: '/recommend' },
],
[{ title: 'AI客服助手', icon: 'sale', path: '/customer' }],
], ],
list: [ list: [
{ {
@@ -63,6 +67,17 @@ export default {
} }
</script> </script>
<style lang="scss">
// 主题颜色定义
$primary-color: #2e5ca9;
$primary-text-color: #f6aa21;
$primary-trans-color: #87a2d0;
.van-nav-bar__text,
.van-nav-bar .van-icon {
color: $primary-color !important;
}
</style>
<style scoped lang="scss"> <style scoped lang="scss">
// 主题颜色定义 // 主题颜色定义
$primary-color: #2e5ca9; $primary-color: #2e5ca9;

View File

@@ -0,0 +1,157 @@
<template>
<div class="chat-page">
<van-nav-bar title="产品推荐" left-text="返回" left-arrow @click-left="$router.history.go(-1)" />
<main class="chat-main">
<div class="chat-content">
<div class="message-area" ref="messageArea" @scroll="handleScroll">
<!-- -->
<messageComponent
:messagesList="messages"
:is-deep="isDeep"
:is-search="isSearching"
:think-ok="isThink"
@setProductName="setProductName"
></messageComponent>
</div>
</div>
<!-- 滚动到顶部按钮 -->
<div class="button-container" style="background: #fff">
<van-icon name="upgrade" size="2em" @click="scrollToTop" />
</div>
</main>
<chatMessage
:messages.sync="messages"
:messageStatus.sync="messageStatus"
:is-deep.sync="isDeep"
:conversation-id.sync="conversationId"
:is-searching.sync="isSearching"
:product-name.sync="productName"
:autoScrollEnabled.sync="autoScrollEnabled"
:chatData="{ action: 'product_recommand' }"
@getIsThink="getIsThink"
></chatMessage>
</div>
</template>
<script>
import { Icon, NavBar } from 'vant'
import SvgIcon from '@/components/svg-icon/index.vue'
import HotProducts from '@/views/AI/components/HotProducts.vue'
import chatMessage from '@/views/AI/components/chat.vue'
import messageComponent from '@/views/AI/components/message.vue'
export default {
components: {
SvgIcon,
messageComponent,
[Icon.name]: Icon,
[NavBar.name]: NavBar,
HotProducts,
chatMessage,
},
data() {
return {
hotList: [],
productName: '',
conversationId: '',
messageStatus: 'stop',
isThink: null,
messages: [
{
type: 'bot',
text: '欢迎使用产品推荐智能助手,我将推荐符合您的产品',
},
],
isSearching: false,
isDeep: false,
autoScrollEnabled: true,
scrollPosition: 0,
}
},
methods: {
getIsThink(e) {
this.isThink = e
},
getHotProducts(e) {
console.log(e)
this.hotList = e
},
scrollToTop() {
const messageArea = this.$refs.messageArea
if (messageArea) {
messageArea.scrollTop = 0
}
},
scrollToBottom() {
if (!this.autoScrollEnabled) return
this.$nextTick(() => {
const messageArea = this.$refs.messageArea
if (messageArea) {
messageArea.scrollTop = messageArea.scrollHeight
}
})
},
setProductName(e) {
this.productName = e
},
handleScroll() {
const messageArea = this.$refs.messageArea
if (!messageArea) return
const threshold = 10
const isAtBottom = messageArea.scrollHeight - messageArea.clientHeight <= messageArea.scrollTop + threshold
this.autoScrollEnabled = isAtBottom
this.scrollPosition = messageArea.scrollTop
},
},
watch: {
messages: {
handler() {
this.$nextTick(() => this.scrollToBottom())
},
deep: true,
},
},
}
</script>
<style lang="scss" scoped>
$primary-color: #2e5ca9;
$primary-text-color: #f6aa21;
$primary-trans-color: rgba(135, 162, 208, 0.5);
.chat-page {
display: flex;
flex-direction: column;
height: 100vh;
//-webkit-user-select: none;
//-moz-user-select: none;
//-ms-user-select: none;
//user-select: none;
.chat-main {
flex: 1;
overflow-y: auto;
padding: 10px;
background: #f7f8fa;
position: relative;
.button-container {
position: fixed;
bottom: 150px;
right: 10px;
border-radius: 50%;
}
.chat-content {
height: 100%;
.message-area {
height: 100%;
overflow-y: auto;
transition: all 0.2s ease-in-out;
}
}
}
}
</style>

View File

@@ -1,9 +1,9 @@
const autoprefixer = require('autoprefixer'); const autoprefixer = require('autoprefixer')
const pxtoviewport = require('postcss-px-to-viewport'); const pxtoviewport = require('postcss-px-to-viewport')
const path = require('path'); const path = require('path')
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, dir); return path.join(__dirname, dir)
} }
module.exports = { module.exports = {
@@ -12,7 +12,7 @@ module.exports = {
outputDir: 'dist', outputDir: 'dist',
productionSourceMap: false, productionSourceMap: false,
devServer: { devServer: {
https: false, https: true,
host: '0.0.0.0', host: '0.0.0.0',
disableHostCheck: true, disableHostCheck: true,
}, },
@@ -23,26 +23,25 @@ module.exports = {
postcssOptions: { postcssOptions: {
plugins: [ plugins: [
autoprefixer(), // 自动加浏览器前缀 autoprefixer(), // 自动加浏览器前缀
pxtoviewport({ // px 转 viewport pxtoviewport({
// px 转 viewport
viewportWidth: 375, viewportWidth: 375,
selectorBlackList: ['van-circle__layer'] // 排除vant circle组件样式转换 selectorBlackList: ['van-circle__layer'], // 排除vant circle组件样式转换
}) }),
] ],
} },
} },
} },
}, },
chainWebpack: (config) => { chainWebpack: (config) => {
// 设置路径别名 // 设置路径别名
config.resolve.alias config.resolve.alias.set('@', resolve('src')).set('@utils', resolve('src/assets/js/utils'))
.set('@', resolve('src'))
.set('@utils', resolve('src/assets/js/utils'));
// 移除 prefetch 插件(减少初始加载体积) // 移除 prefetch 插件(减少初始加载体积)
config.plugins.delete('prefetch'); config.plugins.delete('prefetch')
// SVG 图标配置 // SVG 图标配置
config.module.rule('svg').exclude.add(resolve('src/icons')).end(); config.module.rule('svg').exclude.add(resolve('src/icons')).end()
config.module config.module
.rule('icons') .rule('icons')
.test(/\.svg$/) .test(/\.svg$/)
@@ -52,7 +51,7 @@ module.exports = {
.loader('svg-sprite-loader') .loader('svg-sprite-loader')
.options({ .options({
symbolId: 'icon-[name]', symbolId: 'icon-[name]',
}); })
// 处理 .mjs 文件(用于 mermaid 等模块) // 处理 .mjs 文件(用于 mermaid 等模块)
config.module config.module
@@ -61,7 +60,7 @@ module.exports = {
.include.add(/node_modules/) .include.add(/node_modules/)
.end() .end()
.use('babel-loader') .use('babel-loader')
.loader('babel-loader'); .loader('babel-loader')
// 添加对现代 JS 的支持(如 ?? 和 ?. // 添加对现代 JS 的支持(如 ?? 和 ?.
config.module config.module
@@ -71,19 +70,19 @@ module.exports = {
.loader('babel-loader') .loader('babel-loader')
.options({ .options({
presets: ['@babel/preset-env'], presets: ['@babel/preset-env'],
}); })
}, },
configureWebpack: (config) => { configureWebpack: (config) => {
// 强制使用 CommonJS 模块加载 mermaid // 强制使用 CommonJS 模块加载 mermaid
config.resolve = config.resolve || {}; config.resolve = config.resolve || {}
config.resolve.alias = config.resolve.alias || {}; config.resolve.alias = config.resolve.alias || {}
// config.resolve.alias.mermaid$ = path.resolve( // config.resolve.alias.mermaid$ = path.resolve(
// __dirname, // __dirname,
// './node_modules/mermaid/dist/mermaid.common.js' // './node_modules/mermaid/dist/mermaid.common.js'
// ); // );
// Webpack devtool 设置 // Webpack devtool 设置
config.devtool = 'source-map'; config.devtool = 'source-map'
// 打包性能提示设置 // 打包性能提示设置
config.performance = { config.performance = {
@@ -91,8 +90,8 @@ module.exports = {
maxEntrypointSize: 7168000, // 7MB maxEntrypointSize: 7168000, // 7MB
maxAssetSize: 7168000, // 7MB maxAssetSize: 7168000, // 7MB
assetFilter: function (assetFilename) { assetFilter: function (assetFilename) {
return assetFilename.endsWith('.js'); return assetFilename.endsWith('.js')
}, },
}; }
}, },
}; }