Browse Source

添加客服功能

lex-wxl 1 week ago
parent
commit
31dc099a03

+ 2 - 2
miniprogram/api/login.ts

@@ -119,9 +119,9 @@ export const api_userPaymentOrderRefundPayment = (data: any) => {
 };
 
 /** AI客服消息发送 */
-export const api_cozeAgent = (data: { message: string; userId: string }) => {
+export const api_cozeAgent = (data: { message: string; userId?: string, openId?: string }) => {
   return request({
-    url: `/open/coze/agent`,
+    url: `/edu-app/open/coze/agent`,
     method: "post",
     noToken: true,
     data

+ 2 - 2
miniprogram/pages/chat/chat.json

@@ -1,6 +1,6 @@
 {
+  "navigationStyle": "custom",
   "usingComponents": {
     "navigation-bar": "/components/navigation-bar/navigation-bar"
-  },
-  "navigationBarTitleText": "音乐数字AI客服"
+  }
 }

+ 172 - 99
miniprogram/pages/chat/chat.less

@@ -1,5 +1,15 @@
+/* 背景图 - 使用image组件实现 */
+.bg-image {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: -1;
+}
+
 page {
-  background: linear-gradient(180deg, #E3F2FD 0%, #E8F5E9 50%, #FFF3E0 100%);
+  background: linear-gradient(180deg, #E8F4FC 0%, #F5F9FC 100%);
   height: 100%;
 }
 
@@ -7,7 +17,6 @@ page {
   display: flex;
   flex-direction: column;
   height: calc(100vh - 88px);
-  // margin-top: 88px;
   box-sizing: border-box;
 }
 
@@ -17,12 +26,12 @@ page {
 }
 
 .message-list {
-  padding: 16px;
+  padding: 24rpx 24rpx;
 }
 
 .message-row {
   display: flex;
-  margin-bottom: 16px;
+  margin-bottom: 32rpx;
   align-items: flex-start;
 }
 
@@ -34,10 +43,10 @@ page {
   justify-content: flex-end;
 }
 
-/* AI头像 - 蓝色机器人 */
+/* AI头像 */
 .avatar {
-  width: 40px;
-  height: 40px;
+  width: 80rpx;
+  height: 80rpx;
   border-radius: 50%;
   flex-shrink: 0;
   display: flex;
@@ -47,34 +56,34 @@ page {
 }
 
 .ai-avatar {
-  margin-right: 10px;
+  margin-right: 24rpx;
   background: linear-gradient(135deg, #42A5F5 0%, #64B5F6 100%);
-  border: 2px solid #fff;
-  box-shadow: 0 2px 8px rgba(66, 165, 245, 0.3);
+  border: 4rpx solid #fff;
+  box-shadow: 0 4rpx 16rpx rgba(66, 165, 245, 0.3);
 }
 
 .ai-avatar-image {
-  width: 70%;
-  height: 70%;
+  width: 100%;
+  height: 100%;
   object-fit: contain;
 }
 
+.avatar-fallback {
+  color: #fff;
+  font-size: 24rpx;
+  font-weight: 600;
+}
+
 .user-avatar {
-  margin-left: 10px;
+  margin-left: 24rpx;
   background: linear-gradient(135deg, #66BB6A 0%, #81C784 100%);
-  border: 2px solid #fff;
-  box-shadow: 0 2px 8px rgba(102, 187, 106, 0.3);
+  border: 4rpx solid #fff;
+  box-shadow: 0 4rpx 16rpx rgba(102, 187, 106, 0.3);
 }
 
 .avatar-text {
   color: #fff;
-  font-size: 12px;
-  font-weight: 600;
-}
-
-.avatar-fallback {
-  color: #fff;
-  font-size: 12px;
+  font-size: 24rpx;
   font-weight: 600;
 }
 
@@ -84,69 +93,105 @@ page {
 
 /* AI消息气泡 - 白色 */
 .bubble {
-  padding: 12px 16px;
-  border-radius: 18px;
+  padding: 20rpx 24rpx;
+  border-radius: 24rpx;
   word-wrap: break-word;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.08);
 }
 
 .ai-bubble {
   background-color: #FFFFFF;
-  border-bottom-left-radius: 4px;
 }
 
-/* 用户消息气泡 - 蓝 */
+/* 用户消息气泡 - 微信蓝 */
 .user-bubble {
-  background: linear-gradient(135deg, #2196F3 0%, #42A5F5 100%);
+  background-color: #0F85FF;
   color: #FFFFFF;
-  border-bottom-right-radius: 4px;
 }
 
 .message-text {
-  font-size: 15px;
-  line-height: 1.5;
-  color: inherit;
+  font-size: 30rpx;
+  line-height: 44rpx;
+  color: rgba(19, 20, 21, 1);
+}
+
+.user-bubble .message-text {
+  color: #FFFFFF;
+}
+
+/* FAQ卡片单独显示时的样式(不带头像) */
+.faq-row {
+  justify-content: center;
 }
 
-/* FAQ卡片 - 白色圆角 */
+.faq-content-full {
+  max-width: 100%;
+  width: 100%;
+}
+
+/* FAQ卡片 - 参考蓝湖 block_2 */
 .faq-card {
-  background: rgba(255, 255, 255, 0.9);
-  border-radius: 16px;
-  padding: 14px 12px;
-  margin-top: 8px;
-  width: 280px;
-  backdrop-filter: blur(10px);
-  border: 1px solid rgba(255, 255, 255, 0.8);
-  box-shadow: 0 4px 16px rgba(33, 150, 243, 0.1);
+  background-color: rgba(255, 255, 255, 0.5);
+  border-radius: 32rpx;
+  padding: 26rpx 24rpx;
+  border: 2rpx solid rgba(255, 255, 255, 1);
+  box-shadow: 0 8rpx 40rpx rgba(118, 167, 206, 0.1);
 }
 
 .faq-header {
-  margin-bottom: 10px;
-  padding-left: 2px;
+  display: flex;
+  align-items: center;
+  margin-bottom: 26rpx;
 }
 
-.faq-title {
-  font-size: 15px;
+.faq-header-img {
+  width: 138rpx;
+  height: 32rpx;
+  margin-right: 16rpx;
+}
+
+.faq-header-icon {
+  width: 44rpx;
+  height: 44rpx;
+  background: linear-gradient(135deg, #2196F3 0%, #42A5F5 100%);
+  border-radius: 8rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+
+.faq-header-icon-text {
+  color: #fff;
+  font-size: 22rpx;
   font-weight: 700;
-  color: #2196F3;
-  letter-spacing: 0.5px;
 }
 
+.faq-title {
+  font-size: 30rpx;
+  font-weight: 500;
+  color: rgba(19, 20, 21, 1);
+}
+
+/* FAQ问题列表 */
 .faq-list {
   display: flex;
   flex-direction: column;
-  gap: 8px;
 }
 
+/* FAQ问题项 - 参考蓝湖 list-items 药丸形状 */
 .faq-item {
   display: flex;
   align-items: center;
-  padding: 10px 12px;
+  padding: 22rpx 24rpx;
   background-color: #FFFFFF;
-  border-radius: 10px;
-  box-shadow: 0 1px 3px rgba(0,0,0,0.04);
-  cursor: pointer;
-  transition: all 0.2s ease;
+  border-radius: 132rpx;
+  margin-bottom: 16rpx;
+}
+
+.faq-item:last-child {
+  margin-bottom: 0;
 }
 
 .faq-item:active {
@@ -154,118 +199,147 @@ page {
   background-color: #F5F9FC;
 }
 
+/* 问题图标图片 */
+.faq-icon-img {
+  width: 32rpx;
+  height: 32rpx;
+  margin-right: 24rpx;
+  flex-shrink: 0;
+}
+
+/* 问题图标 */
 .faq-icon {
-  width: 18px;
-  height: 18px;
+  width: 32rpx;
+  height: 32rpx;
   background: linear-gradient(135deg, #2196F3 0%, #42A5F5 100%);
-  border-radius: 4px;
+  border-radius: 8rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-right: 10px;
+  margin-right: 24rpx;
   flex-shrink: 0;
 }
 
 .faq-icon-text {
   color: #fff;
-  font-size: 10px;
+  font-size: 18rpx;
   font-weight: 700;
 }
 
 .faq-text {
   flex: 1;
-  font-size: 13px;
-  color: #333;
-  font-weight: 500;
+  font-size: 28rpx;
+  color: rgba(19, 20, 21, 1);
+  font-weight: 400;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+/* 箭头图片 */
+.faq-arrow-img {
+  width: 24rpx;
+  height: 16rpx;
+  margin-left: 8rpx;
+  flex-shrink: 0;
 }
 
 .faq-arrow {
-  font-size: 14px;
-  color: #CCC;
-  margin-left: 4px;
+  font-size: 24rpx;
+  color: rgba(19, 20, 21, 0.3);
+  margin-left: 8rpx;
 }
 
 /* 用户图片 */
 .user-image {
-  max-width: 180px;
-  border-radius: 12px;
+  width: 360rpx;
+  border-radius: 24rpx;
   overflow: hidden;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.1);
 }
 
 .user-image image {
   width: 100%;
-  max-height: 180px;
+  height: 360rpx;
   display: block;
 }
 
-/* 输入区域 - 圆角灰色 */
+/* 输入区域 - 参考蓝湖 box_10 */
 .input-area {
-  background-color: #FFFFFF;
-  border-top: 1px solid rgba(0,0,0,0.04);
-  padding: 10px 16px;
-  padding-bottom: calc(10px + env(safe-area-inset-bottom));
+  padding: 20rpx 24rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
 }
 
 .input-container {
   display: flex;
   align-items: center;
-  background-color: #F5F7FA;
-  border-radius: 24px;
-  padding: 6px 10px;
 }
 
 .input-wrapper {
   flex: 1;
   display: flex;
   align-items: center;
-  padding: 0 4px;
+  background-color: #FFFFFF;
+  border-radius: 86rpx;
+  padding: 12rpx 24rpx;
+  border: 2rpx solid rgba(255, 255, 255, 1);
 }
 
 .voice-btn {
-  width: 30px;
-  height: 30px;
+  width: 48rpx;
+  height: 48rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-right: 4px;
+  margin-right: 12rpx;
 }
 
 .voice-icon {
-  font-size: 16px;
+  font-size: 32rpx;
   color: #666;
 }
 
+.voice-icon-img {
+  width: 48rpx;
+  height: 48rpx;
+  margin-right: 12rpx;
+}
+
 .chat-input {
   flex: 1;
-  height: 34px;
-  font-size: 15px;
-  color: #333;
+  height: 48rpx;
+  font-size: 30rpx;
+  color: rgba(19, 20, 21, 1);
   background: transparent;
 }
 
 .placeholder {
-  color: #AAA;
-  font-size: 15px;
+  color: rgba(19, 20, 21, 0.3);
+  font-size: 30rpx;
 }
 
+/* 发送按钮 - 参考蓝湖 image-wrapper_2 */
 .image-btn {
-  width: 34px;
-  height: 34px;
+  width: 96rpx;
+  height: 96rpx;
   display: flex;
   align-items: center;
   justify-content: center;
   background-color: #FFFFFF;
-  border-radius: 50%;
-  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
-  margin-left: 4px;
+  border-radius: 86rpx;
+  margin-left: 12rpx;
 }
 
 .image-icon {
-  font-size: 18px;
+  font-size: 48rpx;
   color: #666;
 }
 
+.image-icon-img {
+  width: 48rpx;
+  height: 48rpx;
+}
+
 /* 加载中状态 */
 .loading-row {
   align-items: center;
@@ -273,21 +347,20 @@ page {
 
 .loading-bubble {
   background-color: #FFFFFF;
-  border-radius: 16px;
-  padding: 12px 18px;
-  border-bottom-left-radius: 4px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+  border-radius: 24rpx;
+  padding: 20rpx 36rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
 }
 
 .loading-dots {
   display: flex;
   align-items: center;
-  gap: 5px;
+  gap: 10rpx;
 }
 
 .dot {
-  width: 7px;
-  height: 7px;
+  width: 14rpx;
+  height: 14rpx;
   background: linear-gradient(135deg, #2196F3 0%, #42A5F5 100%);
   border-radius: 50%;
   animation: bounce 1.4s ease-in-out infinite both;

+ 20 - 2
miniprogram/pages/chat/chat.ts

@@ -30,6 +30,7 @@ Page({
     scrollToMessage: '',
     userAvatar: '',
     isLoading: false,
+    avatarError: false,
   },
 
   onLoad() {
@@ -87,6 +88,11 @@ Page({
     });
   },
 
+  // 头像加载失败
+  onAvatarError() {
+    this.setData({ avatarError: true });
+  },
+
   // 输入框变化
   onInput(e: WechatMiniprogram.Input) {
     this.setData({
@@ -197,7 +203,7 @@ Page({
 
     // 获取用户ID(优先使用openid,其次使用token中的用户ID)
     const userInfo = wx.getStorageSync('userInfo') || {};
-    const userId = userInfo.openId || userInfo.userId || wx.getStorageSync('openid') || '';
+    const userId = userInfo.openId || userInfo.userId;
 
     // 构建消息内容(图片发送固定提示语)
     const messageContent = contentType === 'image' ? '我发送了一张图片' : content;
@@ -206,7 +212,8 @@ Page({
       // 调用客服消息接口
       const res: any = await api_cozeAgent({
         message: messageContent,
-        userId: userId
+        userId: userId,
+        openId: wx.getStorageSync('openId')
       });
 
       if (res.code === 200 && res.data) {
@@ -269,5 +276,16 @@ Page({
         this.addReplyMessage(faq.answer);
       }, 500);
     }
+  },
+
+  // 点击图片预览
+  onImageTap(e: any) {
+    const url = e.currentTarget.dataset.url;
+    if (url) {
+      wx.previewImage({
+        urls: [url],
+        current: url
+      });
+    }
   }
 });

+ 28 - 22
miniprogram/pages/chat/chat.wxml

@@ -1,32 +1,39 @@
 <navigation-bar title="音乐数字AI客服" back-btn="{{true}}" bg-color="#E8F4FC"></navigation-bar>
 
+<!-- 背景图 -->
+<image class="bg-image" src="./images/bg.png" mode="aspectFill"></image>
+
 <view class="chat-container">
   <!-- 聊天消息列表 -->
   <scroll-view class="chat-scroll" scroll-y scroll-into-view="{{scrollToMessage}}" enhanced show-scrollbar="{{false}}">
     <view class="message-list">
       <block wx:for="{{messages}}" wx:key="id">
-        <!-- AI消息 -->
-        <view wx:if="{{item.type === 'ai'}}" class="message-row ai-row">
+        <!-- AI文本消息(带头像) -->
+        <view wx:if="{{item.type === 'ai' && item.contentType === 'text'}}" class="message-row ai-row">
+          <!-- AI头像 -->
           <view class="avatar ai-avatar">
-            <image class="ai-avatar-image" src="https://oss.dayaedu.com/ktyq/ai-avatar.png" mode="aspectFill" binderror="onAvatarError"></image>
+            <image class="ai-avatar-image" src="./images/ai-avatar.png" mode="aspectFill" binderror="onAvatarError" wx:if="{{!avatarError}}"></image>
+            <text wx:if="{{avatarError}}" class="avatar-fallback">AI</text>
           </view>
           <view class="message-content">
-            <!-- 文本消息 -->
-            <view wx:if="{{item.contentType === 'text'}}" class="bubble ai-bubble">
+            <view class="bubble ai-bubble">
               <text class="message-text">{{item.content}}</text>
             </view>
-            <!-- 常见问题卡片 -->
-            <view wx:if="{{item.contentType === 'faq'}}" class="faq-card">
+          </view>
+        </view>
+
+        <!-- 常见问题卡片(不带头像) -->
+        <view wx:if="{{item.type === 'ai' && item.contentType === 'faq'}}" class="message-row faq-row">
+          <view class="message-content faq-content-full">
+            <view class="faq-card">
               <view class="faq-header">
-                <text class="faq-title">常见问题</text>
+                <image class="faq-header-img" src="./images/faq-header.png" mode="aspectFit"></image>
               </view>
               <view class="faq-list">
                 <view wx:for="{{item.questions}}" wx:key="index" wx:for-item="question" class="faq-item" data-index="{{index}}" bindtap="onFaqTap">
-                  <view class="faq-icon">
-                    <text class="faq-icon-text">#</text>
-                  </view>
+                  <image class="faq-icon-img" src="./images/faq-icon.png" mode="aspectFit"></image>
                   <text class="faq-text">{{question}}</text>
-                  <text class="faq-arrow">›</text>
+                  <image class="faq-arrow-img" src="./images/arrow-right.png" mode="aspectFit"></image>
                 </view>
               </view>
             </view>
@@ -40,19 +47,20 @@
               <text class="message-text">{{item.content}}</text>
             </view>
             <view wx:if="{{item.contentType === 'image'}}" class="user-image">
-              <image src="{{item.content}}" mode="aspectFit" show-menu-by-longpress></image>
+              <image src="{{item.content}}" mode="aspectFill" bindtap="onImageTap" data-url="{{item.content}}"></image>
             </view>
           </view>
           <view class="avatar user-avatar">
-            <image wx:if="{{userAvatar}}" src="{{userAvatar}}" mode="aspectFill"></image>
-            <text wx:else class="avatar-text">我</text>
+            <image wx:if="{{userAvatar}}" src="{{userAvatar}}" mode="widthFix"></image>
+            <image wx:else src="./images/user-avatar.png" mode="widthFix"></image>
           </view>
         </view>
 
         <!-- AI回复(用户问题答案) -->
         <view wx:if="{{item.type === 'reply'}}" class="message-row ai-row">
           <view class="avatar ai-avatar">
-            <image class="ai-avatar-image" src="https://oss.dayaedu.com/ktyq/ai-avatar.png" mode="aspectFill"></image>
+            <image class="ai-avatar-image" src="./images/ai-avatar.png" mode="aspectFill" binderror="onAvatarError" wx:if="{{!avatarError}}"></image>
+            <text wx:if="{{avatarError}}" class="avatar-fallback">AI</text>
           </view>
           <view class="message-content">
             <view class="bubble ai-bubble">
@@ -65,7 +73,7 @@
       <!-- 加载中状态 -->
       <view wx:if="{{isLoading}}" class="message-row ai-row loading-row">
         <view class="avatar ai-avatar">
-          <image class="ai-avatar-image" src="https://oss.dayaedu.com/ktyq/ai-avatar.png" mode="aspectFill"></image>
+          <image class="ai-avatar-image" src="./images/ai-avatar.png" mode="aspectFill"></image>
         </view>
         <view class="loading-bubble">
           <view class="loading-dots">
@@ -84,9 +92,7 @@
   <view class="input-area">
     <view class="input-container">
       <view class="input-wrapper">
-        <view class="voice-btn">
-          <text class="voice-icon">🎤</text>
-        </view>
+        <!-- <image class="voice-icon-img" src="./images/mic-icon.png" mode="aspectFit"></image> -->
         <input
           class="chat-input"
           placeholder="请输入您的问题..."
@@ -98,8 +104,8 @@
         />
       </view>
       <view class="image-btn" bindtap="chooseImage">
-        <text class="image-icon">🖼️</text>
+        <image class="image-icon-img" src="./images/send-btn.png" mode="aspectFit"></image>
       </view>
     </view>
   </view>
-</view>
+</view>

BIN
miniprogram/pages/chat/images/ai-avatar.png


BIN
miniprogram/pages/chat/images/arrow-right.png


BIN
miniprogram/pages/chat/images/bg.png


BIN
miniprogram/pages/chat/images/faq-header.png


BIN
miniprogram/pages/chat/images/faq-icon.png


BIN
miniprogram/pages/chat/images/mic-icon.png


BIN
miniprogram/pages/chat/images/send-btn.png


BIN
miniprogram/pages/chat/images/user-avatar.png


+ 38 - 35
project.config.json

@@ -1,46 +1,49 @@
 {
-  "description": "项目配置文件",
+  "appid": "wx524b61f5bec60655",
+  "projectname": "orchestra-music-activation",
+  "description": "音乐数字教育激活小程序",
   "miniprogramRoot": "miniprogram/",
   "compileType": "miniprogram",
   "setting": {
-    "useCompilerPlugins": [
-      "typescript",
-      "less"
-    ],
+    "urlCheck": true,
+    "es6": true,
+    "enhance": true,
+    "postcss": true,
+    "preloadBackgroundData": false,
+    "minified": true,
+    "newFeature": false,
+    "coverView": true,
+    "nodeModules": false,
+    "autoAudits": false,
+    "showShadowRootInWxmlPanel": true,
+    "scopeDataCheck": false,
+    "uglifyFileName": false,
+    "checkInvalidKey": true,
+    "checkSiteMap": true,
+    "uploadWithSourceMap": true,
+    "compileHotReLoad": true,
+    "lazyloadPlaceholderEnable": false,
+    "useMultiFrameRuntime": true,
+    "useApiHook": true,
+    "useApiHostHook": true,
     "babelSetting": {
       "ignore": [],
       "disablePlugins": [],
       "outputPath": ""
     },
-    "coverView": false,
-    "postcss": false,
-    "minified": true,
-    "enhance": true,
-    "showShadowRootInWxmlPanel": false,
-    "packNpmManually": true,
-    "packNpmRelationList": [
-      {
-        "packageJsonPath": "./package.json",
-        "miniprogramNpmDistDir": "./miniprogram/"
-      }
-    ],
-    "ignoreUploadUnusedFiles": true,
-    "compileHotReLoad": false,
-    "skylineRenderEnable": true,
-    "es6": true
-  },
-  "simulatorType": "wechat",
-  "simulatorPluginLibVersion": {},
-  "condition": {},
-  "srcMiniprogramRoot": "miniprogram/",
-  "editorSetting": {
-    "tabIndent": "insertSpaces",
-    "tabSize": 2
-  },
-  "libVersion": "2.32.3",
-  "packOptions": {
-    "ignore": [],
-    "include": []
+    "useIsolateContext": true,
+    "userConfirmedBundleSwitch": false,
+    "packNpmManually": false,
+    "packNpmRelationList": [],
+    "minifyWXSS": true,
+    "disableUseStrict": false,
+    "minifyWXML": true,
+    "showES6CompileOption": false,
+    "useCompilerPlugins": [
+      "typescript",
+      "less"
+    ]
   },
-  "appid": "wx524b61f5bec60655"
+  "libVersion": "3.6.6",
+  "condition": {}
 }