Files
lucky_shop/pages_tool/ai-chat/index.vue

1201 lines
42 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="ai-chat-page">
<!-- 导航栏 -->
<view class="navbar">
<!-- 新增语言切换按钮左侧 -->
<button class="lang-switch-btn" @click="toggleLanguage">
{{ currentLang === 'zh' ? 'English' : '中文' }}
</button>
<!-- 中间标题居中显示 -->
<view class="navbar-title">{{ langConfig.navbarTitle }}</view>
<!-- 新增编辑昵称按钮 -->
<button class="edit-nickname-btn" @click="editNickname" aria-label="编辑昵称">
<text class="iconfont"></text>
</button>
</view>
<!-- 快捷功能入口 -->
<view class="quick-functions">
<button class="quick-btn expression-btn" @click="toggleExpressionPanel">
<text class="iconfont icon-expression">😊</text>
</button>
<button class="quick-btn" @click="openShoppingBag">
<text class="iconfont icon-bag">🛒</text>
</button>
<!-- 新增更换头像按钮 -->
<button class="quick-btn" @click="chooseUserAvatar">
<text class="iconfont icon-avatar">👤</text>
</button>
</view>
<!-- 更换头像弹窗 -->
<view class="avatar-modal" v-if="showAvatarModal">
<view class="avatar-modal-mask" @click="showAvatarModal = false"></view>
<view class="avatar-modal-content">
<view class="avatar-modal-header">
<text class="avatar-modal-title">{{ langConfig.changeAvatarTitle }}</text>
<button class="avatar-modal-close" @click="showAvatarModal = false">×</button>
</view>
<view class="avatar-modal-body">
<!-- 预览当前头像 -->
<view class="avatar-preview">
<image :src="userAvatarUrl || defaultAvatar" mode="aspectFill" class="preview-img"/>
</view>
<!-- 选择头像按钮 -->
<button class="avatar-select-btn" @click="pickAvatar">{{ langConfig.chooseFromAlbum }}</button>
<button class="avatar-select-btn camera-btn" @click="captureAvatar">{{ langConfig.takePhoto }}</button>
<button class="avatar-reset-btn" @click="resetAvatar" v-if="userAvatarUrl">{{ langConfig.resetAvatar }}</button>
</view>
</view>
</view>
<!-- 购物车面板点击购物袋显示 -->
<view class="cart-panel" v-if="showCartPanel">
<view class="cart-header">
<text class="cart-title">{{ langConfig.myCart }}</text>
</view>
<view class="cart-list">
<view
class="cart-item"
v-for="(product, idx) in cartProducts"
:key="idx"
@click="consultCartProduct(product)"
>
<image :src="product.imageUrl" mode="widthFix" class="cart-product-img"/>
<view class="cart-product-info">
<text class="cart-product-name">{{ product.name }}</text>
<text class="cart-product-spec">{{ product.spec }}</text>
<text class="cart-product-price">{{ langConfig.currency }}{{ product.price }}</text>
<text class="cart-product-count">×{{ product.count }}</text>
</view>
<text class="consult-icon">{{ langConfig.consult }}</text>
</view>
</view>
<view class="cart-footer">
<button class="cart-btn clear-cart" @click="clearCart">{{ langConfig.clearCart }}</button>
<button class="cart-btn checkout-cart" @click="checkoutCart">{{ langConfig.checkout }}</button>
</view>
</view>
<!-- 聊天内容区域 -->
<view class="chat-container">
<scroll-view
class="chat-scroll"
scroll-y
:scroll-top="scrollTop"
@scroll="onScroll"
scroll-with-animation
>
<view class="chat-messages">
<!-- AI消息项 -->
<view
class="message-item ai-message"
v-for="(msg, idx) in chatMessages"
:key="msg.id"
>
<!-- AI头像 -->
<view class="avatar ai-avatar">
<svg width="72" height="72" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="48" fill="#007aff" stroke="#e8f4ff" stroke-width="2"/>
<circle cx="50" cy="45" r="25" fill="#ffffff"/>
<circle cx="40" cy="40" r="4" fill="#007aff"/>
<circle cx="60" cy="40" r="4" fill="#007aff"/>
<path d="M35,55 Q50,60 65,55" fill="none" stroke="#007aff" stroke-width="2"/>
<path d="M50,55 L50,65" fill="none" stroke="#007aff" stroke-width="2"/>
</svg>
</view>
<!-- 聊天气泡 -->
<view class="message-bubble ai-bubble">
<text class="message-content">{{ msg.content }}</text>
<!-- 操作按钮 -->
<view class="message-actions" v-if="msg.showActions">
<button
class="action-btn"
@click="handleLike(msg)"
:class="{ active: msg.isLiked }"
>
<text class="iconfont">👍</text> {{ langConfig.useful }}
</button>
<button
class="action-btn"
@click="handleDislike(msg)"
:class="{ active: msg.isDisliked }"
>
<text class="iconfont">👎</text> {{ langConfig.useless }}
</button>
</view>
<text class="message-time">{{ formatTime(msg.timestamp) }}</text>
</view>
</view>
<!-- 用户消息项头像在气泡右侧 -->
<view
class="message-item user-message"
v-for="(msg, idx) in userMessages"
:key="msg.id"
>
<!-- 用户头像在右 -->
<view class="avatar user-avatar">
<image
:src="userAvatarUrl || defaultAvatar"
mode="aspectFill"
class="user-avatar-img"
/>
</view>
<!-- 聊天气泡在左 -->
<view class="message-bubble user-bubble">
<!-- 新增用户昵称 -->
<text class="user-nickname">{{ userNickname }}</text>
<text class="message-content">{{ msg.content }}</text>
<text class="message-time">{{ formatTime(msg.timestamp) }}</text>
</view>
</view>
<!-- AI加载中 -->
<view class="message-item ai-message" v-if="isLoading">
<view class="avatar ai-avatar">
<svg width="72" height="72" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="48" fill="#007aff" stroke="#e8f4ff" stroke-width="2"/>
<circle cx="50" cy="45" r="25" fill="#ffffff"/>
<circle cx="40" cy="40" r="4" fill="#007aff"/>
<circle cx="60" cy="40" r="4" fill="#007aff"/>
<path d="M35,55 Q50,60 65,55" fill="none" stroke="#007aff" stroke-width="2"/>
<path d="M50,55 L50,65" fill="none" stroke="#007aff" stroke-width="2"/>
</svg>
</view>
<view class="message-bubble ai-bubble loading">
<view class="loading-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 表情面板点击表情按钮显示 -->
<view class="expression-panel" v-if="showExpressionPanel">
<view class="expression-grid">
<text class="expression-item" @click="addExpression('😊')">😊</text>
<text class="expression-item" @click="addExpression('😂')">😂</text>
<text class="expression-item" @click="addExpression('👍')">👍</text>
<text class="expression-item" @click="addExpression(langConfig.thanks)">{{ langConfig.thanks }}</text>
<text class="expression-item" @click="addExpression('')"></text>
<text class="expression-item" @click="addExpression(langConfig.ok)">{{ langConfig.ok }}</text>
<text class="expression-item" @click="addExpression('👌')">👌</text>
<text class="expression-item" @click="addExpression(langConfig.trouble)">{{ langConfig.trouble }}</text>
<text class="expression-item" @click="addExpression('🙏')">🙏</text>
</view>
</view>
<!-- 底部输入区域 -->
<view class="input-area">
<textarea
class="input-box"
v-model="inputContent"
:placeholder="langConfig.inputPlaceholder"
@confirm="sendMessage"
:disabled="isLoading"
/>
<button
class="send-btn"
@click="sendMessage"
:disabled="!inputContent.trim() || isLoading"
>
<text class="iconfont icon-send"></text>
</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// 新增:语言配置
currentLang: 'zh', // 默认中文zh / 英文en
langConfigs: {
zh: {
// 导航/功能按钮
navbarTitle: 'AI智能客服',
// 头像弹窗
changeAvatarTitle: '更换头像',
chooseFromAlbum: '从相册选择',
takePhoto: '拍照上传',
resetAvatar: '重置默认头像',
// 购物车
myCart: '我的购物车',
currency: '¥',
consult: '💬 咨询',
clearCart: '清空购物车',
checkout: '结算',
// 聊天相关
useful: '有用',
useless: '无用',
inputPlaceholder: '请输入您的问题...',
sendBtn: '发送',
// 表情/通用文本
thanks: '谢谢',
ok: '好的',
trouble: '麻烦了',
// 系统提示/回复模板
systemTips: {
evaluateOpen: '打开了【评价客服】功能,您可以对服务进行打分和评价',
selfServiceOpen: '打开了【自助服务】功能,包含常见问题解答和自助办理入口',
cartOpen: '打开了【购物车】,您可以点击任意产品进行咨询',
cartClear: '购物车已清空',
cartCheckout: '发起购物车结算',
chatClear: '聊天记录已清空,有什么可以帮助您的吗?',
avatarSuccess: '头像更换成功',
avatarReset: '已重置为默认头像',
likeSuccess: '感谢您的认可!',
dislikeSuccess: '我们会继续改进!'
},
// AI回复模板
aiReplies: {
welcome: '您好!我是智能客服,很高兴为您服务~ 您可以咨询产品相关问题,也可以点击购物袋查看已选产品进行咨询。',
evaluateGood: '感谢您的认可!我们会继续努力提供更好的产品咨询服务~',
evaluateBad: '非常抱歉给您带来不好的体验,您可以告诉我具体问题,我会尽力解答!',
selfService: (item) => `您选择了【${item}】,正在为您提供相关服务...`,
cartProductReply: {
dryer: (p) => `您咨询的【${p.name}】相关信息:✨ 产品特点:• 鼓风再生技术节能30%• 露点稳定在-40℃以下• 全自动运行,无需人工值守💰 价格:¥${p.price}含13%增值税)📅 货期现货下单后48小时发货🔧 质保2年免费保修终身维护您想了解安装、参数还是报价详情`,
filter: (p) => `您咨询的【${p.name}】相关信息:✨ 产品特点:• 过滤精度可达0.01μm• 压差小,能耗低• 可更换滤芯,维护成本低💰 价格:¥${p.price}(含运费)📅 货期3-5个工作日🔧 适用介质:压缩空气、氮气、氧气您想了解滤芯寿命、安装方式还是批量采购折扣?`,
valve: (p) => `您咨询的【${p.name}】相关信息:✨ 产品特点:• 三偏心设计,密封性能好• 耐温范围:-20℃~450℃• 操作扭矩小,启闭轻便💰 价格:¥${p.price}(含安装配件)📅 货期7-10个工作日🔧 适用场景:化工、冶金、电力行业您想了解压力等级、材质还是定制服务?`
},
dryerReply: `关于干燥机的常见问题解答1. 干燥机露点不稳定?→ 检查再生风机是否正常吸附剂是否需要更换2. 能耗过高?→ 可调整再生周期或升级为变频控制3. 维护成本?→ 每年更换一次吸附剂成本约1200元您想了解具体哪方面的问题`,
filterReply: `关于过滤器的常见问题解答1. 滤芯更换周期?→ 一般6-8个月根据使用工况调整2. 压差过大?→ 及时更换滤芯检查进气杂质含量3. 过滤精度选择?→ 气动设备选0.01μm一般用气选1μm您想了解具体哪方面的问题`,
valveReply: `关于阀门的常见问题解答1. 密封泄漏?→ 检查阀座磨损情况及时更换密封件2. 操作卡顿?→ 加注专用润滑油检查阀杆是否变形3. 耐高温范围?→ 最高可达450℃特殊定制可到600℃您想了解具体哪方面的问题`,
generalReply: (content) => `您咨询的"${content}"我已收到如果是产品相关问题您可以1. 告诉我具体产品名称/型号2. 打开购物车点击对应产品咨询3. 描述您遇到的具体问题我会尽力为您解答~`,
cartCheckoutReply: (products) => `您购物车中有以下产品待结算:` +
products.map(p => `${p.name} ×${p.count} ¥${p.price}`).join('') +
`总计:¥${products.reduce((sum, p) => sum + p.price * p.count, 0)}您可以选择1. 在线支付2. 线下转账3. 申请报价单请告诉我您的选择~`,
aboutUs: '智能客服系统 v1.0专注于工业流体设备咨询服务~'
}
},
en: {
// 头像弹窗
changeAvatarTitle: 'Change Avatar',
chooseFromAlbum: 'Select from Album',
takePhoto: 'Take Photo',
resetAvatar: 'Reset to Default',
// 购物车
myCart: 'My Cart',
currency: '$',
consult: '💬 Consult',
clearCart: 'Clear Cart',
checkout: 'Checkout',
// 聊天相关
useful: 'Useful',
useless: 'Useless',
inputPlaceholder: 'Please enter your question...',
sendBtn: 'Send',
// 表情/通用文本
thanks: 'Thanks',
ok: 'OK',
trouble: 'Sorry to trouble you',
// 系统提示/回复模板
systemTips: {
evaluateOpen: 'Opened [Rate Service] function, you can rate and comment on the service',
selfServiceOpen: 'Opened [Self Service] function, including FAQ and self-service entry',
cartOpen: 'Opened [Shopping Cart], you can click any product to consult',
cartClear: 'Cart cleared',
cartCheckout: 'Initiated cart checkout',
chatClear: 'Chat history cleared, how can I help you?',
avatarSuccess: 'Avatar changed successfully',
avatarReset: 'Reset to default avatar',
likeSuccess: 'Thank you for your recognition!',
dislikeSuccess: 'We will continue to improve!'
},
// AI回复模板
aiReplies: {
welcome: 'Hello! I am an intelligent customer service, glad to serve you~ You can consult product-related questions, or click the shopping bag to view selected products for consultation.',
evaluateGood: 'Thank you for your recognition! We will continue to strive to provide better product consulting services~',
evaluateBad: 'We are very sorry for the bad experience. You can tell me the specific problem and I will try my best to answer it!',
selfService: (item) => `You selected [${item}], we are providing relevant services for you...`,
cartProductReply: {
dryer: (p) => `Information about [${p.name}] you consulted:✨ Product Features:• Blast regeneration technology, 30% energy saving• Dew point stable below -40℃• Fully automatic operation, no manual on duty💰 Price: $${p.price} (including 13% VAT)📅 Delivery: In stock, shipped within 48 hours after order🔧 Warranty: 2 years free warranty, lifetime maintenanceDo you want to know about installation, parameters or quotation details?`,
filter: (p) => `Information about [${p.name}] you consulted:✨ Product Features:• Filtration accuracy up to 0.01μm• Low pressure difference and energy consumption• Replaceable filter element, low maintenance cost💰 Price: $${p.price} (including shipping)📅 Delivery: 3-5 working days🔧 Applicable media: Compressed air, nitrogen, oxygenDo you want to know about filter element life, installation method or bulk purchase discount?`,
valve: (p) => `Information about [${p.name}] you consulted:✨ Product Features:• Triple eccentric design, good sealing performance• Temperature resistance range: -20℃~450℃• Small operating torque, easy to open and close💰 Price: $${p.price} (including installation accessories)📅 Delivery: 7-10 working days🔧 Applicable scenarios: Chemical, metallurgical, power industryDo you want to know about pressure rating, material or customization service?`
},
dryerReply: `FAQ about dryers:1. Unstable dryer dew point? → Check if the regeneration fan is normal and if the adsorbent needs replacement2. High energy consumption? → Adjust regeneration cycle or upgrade to frequency conversion control3. Maintenance cost? → Replace adsorbent once a year, cost about $1200Which specific aspect do you want to know about?`,
filterReply: `FAQ about filters:1. Filter element replacement cycle? → Generally 6-8 months, adjusted according to working conditions2. Excessive pressure difference? → Replace filter element in time and check inlet impurity content3. Filtration accuracy selection? → 0.01μm for pneumatic equipment, 1μm for general gas useWhich specific aspect do you want to know about?`,
valveReply: `FAQ about valves:1. Seal leakage? → Check valve seat wear and replace seal in time2. Operation jamming? → Add special lubricating oil and check if valve stem is deformed3. High temperature resistance range? → Up to 450℃, special customization up to 600℃Which specific aspect do you want to know about?`,
generalReply: (content) => `I have received your inquiry about "${content}"!If it is a product-related question, you can:1. Tell me the specific product name/model2. Open the shopping cart and click the corresponding product to consult3. Describe the specific problem you encounteredI will try my best to answer you~`,
cartCheckoutReply: (products) => `You have the following products to checkout in your cart:` +
products.map(p => `${p.name} ×${p.count} $${p.price}`).join('') +
`Total: $${products.reduce((sum, p) => sum + p.price * p.count, 0)}You can choose:1. Online payment2. Offline transfer3. Apply for quotationPlease tell me your choice~`,
aboutUs: 'Smart Customer Service System v1.0Focus on industrial fluid equipment consulting services~'
}
}
},
// 聊天数据
chatMessages: [],
userMessages: [],
inputContent: '',
isLoading: false,
scrollTop: 0,
// 功能面板控制
showExpressionPanel: false,
showCartPanel: false,
// 头像相关
showAvatarModal: false,
userAvatarUrl: '',
defaultAvatar: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNDgiIGZpbGw9IiNmNWY3ZmEiIHN0cm9rZT0iI2UwZTBlMCIgc3Ryb2tlLXdpZHRoPSIyIi8+PGNpcmNsZSBjeD0iNTAiIGN5PSIzNSIgcj0iMTUiIGZpbGw9IiNkOWUzZjAiLz48cGF0aCBkPSJNMjAsODAgUTUwLDYwIDgwLDgwIiBmaWxsPSIjZDllM2YwIi8+PC9zdmc+',
// 购物车产品数据
cartProducts: [
{id: 1,name: 'PFW无热再生压缩空气干燥机 / PFW Heatless Regeneration Compressed Air Dryer',spec: '型号PFW-06 压力1.6MPa / Model: PFW-06 Pressure: 1.6MPa',price: 3580,count: 1,imageUrl: 'https://p3-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/d2554fd9ddbb40a990951499a4c1fabe.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'dryer'},
{id: 2,name: 'PFG鼓风再生压缩空气干燥机 / PFG Blast Regeneration Compressed Air Dryer',spec: '型号PFG-100 处理量10m³/min / Model: PFG-100 Capacity: 10m³/min',price: 12800,count: 1,imageUrl: 'https://p11-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/d4dc4adb45464911b21764d4585d3602.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'dryer'},
{id: 3,name: 'PFY压缩热再生压缩空气干燥机 / PFY Compression Heat Regeneration Dryer',spec: '型号PFY-200 处理量20m³/min / Model: PFY-200 Capacity: 20m³/min',price: 18600,count: 1,imageUrl: 'https://p11-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/d1b86bda771d464fadb6bcc087c1ac4d.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'dryer'},
{id: 4,name: 'PFA组合式低露点干燥机 / PFA Combined Low Dew Point Dryer',spec: '型号PFA-300 露点:-70℃ / Model: PFA-300 Dew Point: -70℃',price: 28900,count: 1,imageUrl: 'https://p11-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/e419a257befb4209953dabc636ffb276.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'dryer'},
{id: 5,name: 'PFB微热再生压缩空气干燥机 / PFB Micro Heat Regeneration Dryer',spec: '型号PFB-150 处理量15m³/min / Model: PFB-150 Capacity: 15m³/min',price: 15600,count: 1,imageUrl: 'https://p26-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/1b0dbc17b2ef4b0bb646af100cff57dc.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'dryer'},
{id: 6,name: 'PFD冷冻式压缩空气干燥机 / PFD Refrigerated Compressed Air Dryer',spec: '型号PFD-080 处理量8m³/min / Model: PFD-080 Capacity: 8m³/min',price: 9800,count: 1,imageUrl: 'https://p26-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/d7d7ddd2ad714c62a61752fcaf5c0847.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'dryer'},
{id: 7,name: '高性能程控阀 / High Performance Programmed Valve',spec: '型号V-100 材质:铸铁 / Model: V-100 Material: Cast Iron',price: 2380,count: 1,imageUrl: 'https://p26-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/ac81fe92afee4243b0e1698bab2cbd68.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'valve'},
{id: 8,name: '三偏心蝶阀 / Triple Eccentric Butterfly Valve',spec: '型号D373H-16C 口径DN100 / Model: D373H-16C Caliber: DN100',price: 3580,count: 1,imageUrl: 'https://p26-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/0a4279a4ee034fc0b8f3c192fd3dcb2a.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'valve'},
{id: 9,name: '制氮专用角座阀 / Nitrogen Making Angle Seat Valve',spec: '型号JZ-20 材质不锈钢304 / Model: JZ-20 Material: Stainless Steel 304',price: 1860,count: 1,imageUrl: 'https://p3-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/d4a98659c8b743a59cf84953b4e67ce0.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'valve'},
{id: 10,name: '智能精控调节阀 / Intelligent Precision Control Valve',spec: '型号ZDLP-16 调节精度±1% / Model: ZDLP-16 Adjustment Accuracy: ±1%',price: 5680,count: 1,imageUrl: 'https://p3-flow-imagex-download-sign.byteimg.com/tos-cn-i-a9rns2rl98/96622539231f4bd4bacce9a2e62dca96.png~tplv-a9rns2rl98-resize-jpeg-v1.png',type: 'valve'},
],
// ====== 新增:昵称相关 ======
userNickname: '', // 当前昵称
defaultNicknames: { zh: '顾客', en: 'Customer' } // 默认昵称
};
},
computed: {
// 新增:当前语言配置(简化模板调用)
langConfig() {
return this.langConfigs[this.currentLang];
}
},
onLoad() {
// 初始化欢迎语(根据当前语言)
this.chatMessages.push({
id: 1,
content: this.langConfig.aiReplies.welcome,
timestamp: Date.now() - 60000,
showActions: true,
isLiked: false,
isDisliked: false
});
// 加载本地存储的头像和昵称
this.loadUserAvatar();
this.loadUserNickname(); // 👈 新增
this.scrollToBottom();
},
// 适配PC端挂载
mounted() {
this.loadUserAvatar();
this.loadUserNickname(); // 👈 新增
this.scrollToBottom();
// 监听窗口大小变化,保持样式一致
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.handleResize);
}
},
beforeDestroy() {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.handleResize);
}
},
methods: {
// ====== 新增:加载用户昵称 ======
loadUserNickname() {
let saved = null;
try {
// 兼容 uni-app 和 H5
if (typeof uni !== 'undefined' && uni.getStorageSync) {
saved = uni.getStorageSync('userNickname');
} else if (typeof localStorage !== 'undefined') {
saved = localStorage.getItem('userNickname');
}
} catch (e) {
console.warn('Failed to load user nickname:', e);
}
// 若无保存,则使用当前语言默认值
this.userNickname = saved || this.defaultNicknames[this.currentLang];
},
// ====== 新增:保存用户昵称 ======
saveUserNickname(nickname) {
try {
if (typeof uni !== 'undefined' && uni.setStorageSync) {
uni.setStorageSync('userNickname', nickname);
} else if (typeof localStorage !== 'undefined') {
localStorage.setItem('userNickname', nickname);
}
} catch (e) {
console.warn('Failed to save user nickname:', e);
}
},
// ====== 新增:编辑昵称弹窗 ======
editNickname() {
const confirmText = this.currentLang === 'zh' ? '确定' : 'OK';
const cancelText = this.currentLang === 'zh' ? '取消' : 'Cancel';
const title = this.currentLang === 'zh' ? '修改昵称' : 'Change Nickname';
uni.showModal({
title,
placeholderText: this.userNickname,
editable: true,
confirmText,
cancelText,
success: (res) => {
if (res.confirm && res.content?.trim()) {
const newNick = res.content.trim();
this.userNickname = newNick;
this.saveUserNickname(newNick);
}
}
});
},
// 原有方法:返回上一页
goBack() {
uni.navigateBack();
},
// 原有方法:滚动到底部
scrollToBottom() {
this.$nextTick(() => {
this.scrollTop = 999999;
});
},
// 原有方法:窗口大小变化处理
handleResize() {
this.$nextTick(this.scrollToBottom);
},
// 原有方法:滚动事件
onScroll(e) {
// 可扩展:判断是否在底部等
},
// 原有方法:切换语言
toggleLanguage() {
this.currentLang = this.currentLang === 'zh' ? 'en' : 'zh';
// 切换语言时,若用户未自定义昵称,则更新为对应默认值
if (!this.userNickname ||
this.userNickname === this.defaultNicknames.zh ||
this.userNickname === this.defaultNicknames.en) {
this.userNickname = this.defaultNicknames[this.currentLang];
}
// 保存语言设置
if (typeof uni !== 'undefined') {
uni.setStorageSync('appLang', this.currentLang);
}
},
// 原有方法:发送消息
sendMessage() {
const content = this.inputContent.trim();
if (!content || this.isLoading) return;
// 添加用户消息
const userMsg = {
id: Date.now(),
content,
timestamp: Date.now()
};
this.userMessages.push(userMsg);
this.inputContent = '';
this.showExpressionPanel = false;
// 滚动到底部
this.scrollToBottom();
// 模拟AI回复简化版实际可调用API
this.isLoading = true;
setTimeout(() => {
this.isLoading = false;
let aiReply = '';
// 匹配关键词回复
if (content.includes('干燥机') || content.includes('dryer')) {
aiReply = this.langConfig.aiReplies.dryerReply;
} else if (content.includes('过滤器') || content.includes('filter')) {
aiReply = this.langConfig.aiReplies.filterReply;
} else if (content.includes('阀门') || content.includes('valve')) {
aiReply = this.langConfig.aiReplies.valveReply;
} else if (content.includes('关于我们') || content.includes('about us')) {
aiReply = this.langConfig.aiReplies.aboutUs;
} else {
aiReply = this.langConfig.aiReplies.generalReply(content);
}
this.chatMessages.push({
id: Date.now() + 1,
content: aiReply,
timestamp: Date.now(),
showActions: true,
isLiked: false,
isDisliked: false
});
this.scrollToBottom();
}, 800);
},
// 原有方法:表情面板
toggleExpressionPanel() {
this.showExpressionPanel = !this.showExpressionPanel;
},
addExpression(text) {
this.inputContent += text;
this.showExpressionPanel = false;
},
// 原有方法:购物车
openShoppingBag() {
this.showCartPanel = true;
},
consultCartProduct(product) {
const msg = `我想咨询购物车中的【${product.name}`;
this.inputContent = msg;
this.showCartPanel = false;
},
clearCart() {
this.cartProducts = [];
uni.showToast({ title: this.langConfig.systemTips.cartClear, icon: 'none' });
this.showCartPanel = false;
},
checkoutCart() {
if (this.cartProducts.length === 0) return;
const reply = this.langConfig.aiReplies.cartCheckoutReply(this.cartProducts);
this.chatMessages.push({
id: Date.now(),
content: reply,
timestamp: Date.now(),
showActions: true,
isLiked: false,
isDisliked: false
});
this.showCartPanel = false;
this.scrollToBottom();
},
// 原有方法:评价与自助服务
openEvaluateService() {
uni.showToast({ title: this.langConfig.systemTips.evaluateOpen, icon: 'none' });
},
openSelfService() {
uni.showToast({ title: this.langConfig.systemTips.selfServiceOpen, icon: 'none' });
},
// 原有方法:消息反馈
handleLike(msg) {
if (msg.isDisliked) return;
msg.isLiked = !msg.isLiked;
msg.isDisliked = false;
if (msg.isLiked) {
uni.showToast({ title: this.langConfig.systemTips.likeSuccess, icon: 'success' });
}
},
handleDislike(msg) {
if (msg.isLiked) return;
msg.isDisliked = !msg.isDisliked;
msg.isLiked = false;
if (msg.isDisliked) {
uni.showToast({ title: this.langConfig.systemTips.dislikeSuccess, icon: 'none' });
}
},
// 原有方法:头像更换
chooseUserAvatar() {
this.showAvatarModal = true;
},
pickAvatar() {
uni.chooseImage({
count: 1,
success: (res) => {
this.userAvatarUrl = res.tempFilePaths[0];
this.saveUserAvatar(res.tempFilePaths[0]);
uni.showToast({ title: this.langConfig.systemTips.avatarSuccess, icon: 'success' });
}
});
this.showAvatarModal = false;
},
captureAvatar() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
this.userAvatarUrl = res.tempFilePaths[0];
this.saveUserAvatar(res.tempFilePaths[0]);
uni.showToast({ title: this.langConfig.systemTips.avatarSuccess, icon: 'success' });
}
});
this.showAvatarModal = false;
},
resetAvatar() {
this.userAvatarUrl = '';
this.saveUserAvatar('');
uni.showToast({ title: this.langConfig.systemTips.avatarReset, icon: 'none' });
this.showAvatarModal = false;
},
saveUserAvatar(url) {
try {
if (typeof uni !== 'undefined') {
uni.setStorageSync('userAvatar', url);
}
} catch (e) {
console.warn('Save avatar failed:', e);
}
},
loadUserAvatar() {
try {
if (typeof uni !== 'undefined') {
const saved = uni.getStorageSync('userAvatar');
if (saved) {
this.userAvatarUrl = saved;
}
}
} catch (e) {
console.warn('Load avatar failed:', e);
}
},
// 原有方法:时间格式化
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return this.currentLang === 'zh' ? '刚刚' : 'Just now';
if (minutes < 60) return `${minutes}${this.currentLang === 'zh' ? '分钟前' : 'm ago'}`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}${this.currentLang === 'zh' ? '小时前' : 'h ago'}`;
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${month}-${day} ${hour}:${minute}`;
}
}
};
</script>
<style scoped>
/* ========== 新增:用户昵称样式 ========== */
.user-nickname {
font-size: 12px;
color: #888;
opacity: 0.9;
margin-bottom: 4px;
display: block;
text-align: left;
}
/* ========== 原有样式保持不变 ========== */
.ai-chat-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
position: relative;
overflow: hidden;
}
.lang-switch-btn {
padding: 6px 12px;
background: rgba(0, 122, 255, 0.8);
color: white;
border: none;
border-radius: 20px;
font-size: 14px;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
.top-function-bar {
display: flex;
justify-content: center;
padding: 12px 0;
background: white;
border-bottom: 1px solid #eee;
gap: 30px;
}
.function-btn {
display: flex;
flex-direction: column;
align-items: center;
background: none;
border: none;
color: #666;
font-size: 12px;
}
.function-btn .icon {
font-size: 20px;
margin-bottom: 4px;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #007aff;
color: white;
font-size: 18px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.navbar-title {
flex: 1;
text-align: center;
margin: 0 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.edit-nickname-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
border: none;
color: white;
font-size: 16px;
margin-left: 10px;
}
.quick-functions {
display: flex;
justify-content: center;
padding: 12px 0;
background: white;
border-bottom: 1px solid #eee;
gap: 40px;
position: relative;
}
.quick-btn {
position: relative;
background: none;
border: none;
font-size: 24px;
color: #666;
}
.cart-badge {
position: absolute;
top: -6px;
right: -6px;
background: red;
color: white;
font-size: 10px;
padding: 2px 4px;
border-radius: 10px;
min-width: 14px;
text-align: center;
}
.avatar-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2000;
}
.avatar-modal-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
}
.avatar-modal-content {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: white;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding: 20px;
box-sizing: border-box;
}
.avatar-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.avatar-modal-title {
font-size: 18px;
font-weight: bold;
}
.avatar-modal-close {
background: none;
border: none;
font-size: 24px;
color: #999;
}
.avatar-preview {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.preview-img {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #eee;
}
.avatar-select-btn,
.avatar-reset-btn {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border: none;
border-radius: 8px;
background: #007aff;
color: white;
font-size: 16px;
}
.camera-btn {
background: #34c759;
}
.avatar-reset-btn {
background: #ff3b30;
}
.cart-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 1500;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
.cart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.cart-title {
font-size: 18px;
font-weight: bold;
}
.close-cart {
background: none;
border: none;
font-size: 24px;
color: #999;
}
.cart-list {
margin-bottom: 20px;
}
.cart-item {
display: flex;
padding: 12px;
border: 1px solid #eee;
border-radius: 12px;
margin-bottom: 12px;
cursor: pointer;
}
.cart-product-img {
width: 60px;
height: 60px;
border-radius: 8px;
margin-right: 12px;
object-fit: cover;
}
.cart-product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.cart-product-name {
font-weight: bold;
font-size: 14px;
margin-bottom: 4px;
}
.cart-product-spec {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.cart-product-price {
color: #ff3b30;
font-weight: bold;
}
.cart-product-count {
color: #999;
font-size: 12px;
}
.consult-icon {
color: #007aff;
font-size: 18px;
margin-left: 10px;
align-self: center;
}
.cart-footer {
display: flex;
gap: 12px;
}
.cart-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
color: white;
}
.clear-cart {
background: #ff3b30;
}
.checkout-cart {
background: #34c759;
}
.chat-container {
flex: 1;
padding: 12px;
overflow: hidden;
}
.chat-scroll {
height: 100%;
}
.chat-messages {
display: flex;
flex-direction: column;
}
.message-item {
display: flex;
margin-bottom: 16px;
max-width: 80%;
}
.ai-message {
flex-direction: row;
}
.user-message {
flex-direction: row-reverse;
align-self: flex-end;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin: 0 8px;
flex-shrink: 0;
}
.ai-avatar {
margin-right: 8px;
}
.user-avatar {
margin-left: 8px;
}
.avatar svg,
.avatar image {
width: 100%;
height: 100%;
display: block;
}
/* ======== 重点:美化 AI 气泡 ======== */
.message-bubble {
padding: 10px 16px;
border-radius: 16px;
position: relative;
max-width: 85%;
word-break: break-word;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all 0.2s ease;
font-size: 15px;
line-height: 1.5;
}
.ai-bubble {
background-color: white;
border: 1px solid #e0e0e0;
color: #333;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
}
.user-bubble {
background-color: #007aff;
color: white;
border: none;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.message-time {
font-size: 10px;
opacity: 0.7;
margin-top: 6px;
display: block;
text-align: right;
}
.message-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
.message-actions {
margin-top: 12px;
display: flex;
gap: 12px;
}
/* ✅ 修改点:让“有用/无用”按钮更扁平 */
.action-btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 12px;
background: white;
color: #666;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s ease;
height: 28px;
}
.action-btn.active {
background: #007aff;
color: white;
border-color: #007aff;
transform: scale(0.98);
}
.loading-dots {
display: flex;
gap: 4px;
justify-content: flex-start;
}
.dot {
width: 6px;
height: 6px;
background: #007aff;
border-radius: 50%;
animation: loading 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes loading {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.expression-panel {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: white;
border-radius: 12px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
}
.expression-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.expression-item {
font-size: 20px;
text-align: center;
padding: 8px;
border-radius: 8px;
background: #f5f7fa;
cursor: pointer;
}
.expression-item:hover {
background: #e0e6ed;
}
.input-area {
display: flex;
padding: 12px;
background: white;
border-top: 1px solid #eee;
gap: 10px;
align-items: center; /* 👈 关键:使子元素垂直居中 */
}
.input-box {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
font-size: 16px;
min-height: 20px;
max-height: 100px;
resize: none;
outline: none;
box-shadow: none;
/* 新增:防止文字过长撑开 */
overflow-y: auto;
/* 确保内容不会破坏布局 */
height: 44px; /* 👈 固定高度,与 send-btn 一致 */
}
.send-btn {
width: 44px;
height: 44px;
border-radius: 50%;
background: #007aff;
color: white;
border: none;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s ease;
}
/* 确保图标居中 */
.send-btn .iconfont {
font-size: 20px;
line-height: 1;
vertical-align: middle;
}
/* 响应式适配 */
@media (min-width: 768px) {
.ai-chat-page {
max-width: 800px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
border-radius: 12px;
overflow: hidden;
}
.lang-switch-btn {
margin-right: 10px;
position: absolute;
top: 10px;
right: 10px;
}
}
</style>