shizhongqi 11 месяцев назад
Родитель
Сommit
b55ad70402

+ 0 - 0
hdApp/src/admin/chat/chatPage.vue


+ 356 - 0
hdApp/src/admin/chat/list.vue

@@ -0,0 +1,356 @@
+<template>
+  <view class="app-main app-content">
+    <view class="input-wrap_box">
+      <view class="search-container">
+        <AppSearchModule placeholder="这里搜索" v-model="searchText" @input="searchFn" />
+      </view>
+      <button class="admin-button-com middle blue" @click="addCustom()">添加客户</button>
+    </view>
+    <!-- <view class="app-tabs">
+      <app-tabs :tabs="tabs" :isFixed="false" :top="50" :currentTab="tabIndex" :height="100" @change="change" itemWidth="25%" :isAdd="false" />
+    </view> -->
+
+    <view class="list-wrap">
+      <block v-if="$util.isEmpty(list.data) === false">
+        <block v-for="(item, index) in list.data" :key="item.id || index">
+          <tui-list-cell :arrow="true" @click="pageTo({ url: '/admin/chat/chatPage', query: { id: item.id } })">
+            <view class="user-card">
+              <view class="user-avatar">
+                <image :src="item.avatar" class="avatar-img" mode="aspectFill" />
+              </view>
+              <view class="user-info">
+                <view class="user-main">
+                  <view class="user-name">{{ item.name }}</view>
+                  <view class="right-info">
+                    <view v-if="!$util.isEmpty(item.memberName)" class="member-icon">
+                      <image
+                        v-if="item.member == 1"
+                        class="member-badge-icon"
+                        style="width: 76upx; height: 76upx"
+                        src="@/static/member-icons/member-1.svg"
+                        mode="aspectFit"
+                      ></image>
+                      <image
+                        v-else-if="item.member == 2"
+                        class="member-badge-icon"
+                        style="width: 74upx; height: 74upx"
+                        src="@/static/member-icons/member-2.svg"
+                        mode="aspectFit"
+                      ></image>
+                      <image
+                        v-else-if="item.member == 3"
+                        class="member-badge-icon"
+                        style="width: 73upx; height: 73upx"
+                        src="@/static/member-icons/member-3.svg"
+                        mode="aspectFit"
+                      ></image>
+                      <image
+                        v-else-if="item.member == 4"
+                        class="member-badge-icon"
+                        style="width: 82upx; height: 82upx"
+                        src="@/static/member-icons/member-4.svg"
+                        mode="aspectFit"
+                      ></image>
+                      <image
+                        v-else-if="item.member == 5"
+                        class="member-badge-icon"
+                        style="width: 77upx; height: 77upx"
+                        src="@/static/member-icons/member-5.svg"
+                        mode="aspectFit"
+                      ></image>
+                    </view>
+                  </view>
+                </view>
+                <view class="user-meta">
+                  <view class="visit-time">
+                    {{ item.visitTime.substr(2, 11) }}
+
+                    <text v-if="Number(item.balance) > 0" class="positive">余额{{ parseFloat(item.balance) }}</text>
+                    <text v-if="Number(item.balance) < 0" class="negative">-{{ parseFloat(Math.abs(item.balance)) }}</text>
+
+                    <block v-if="item.passStatus == 1">
+                      <text v-if="item.overTimeUnExpend && item.overTimeUnExpend == 10000" style="margin-left: 20upx; color: green">没有下过单</text>
+                      <text v-if="item.overTimeUnExpend && item.overTimeUnExpend == 7" style="margin-left: 20upx; color: green">超1周未下单</text>
+                      <text v-if="item.overTimeUnExpend && item.overTimeUnExpend == 30" style="margin-left: 20upx; color: green">超1个月未下单</text>
+                      <text v-if="item.overTimeUnExpend && item.overTimeUnExpend == 180" style="margin-left: 20upx; color: green">超半年未下单</text>
+                      <text v-if="item.overTimeUnExpend && item.overTimeUnExpend == 365" style="margin-left: 20upx; color: green">超1年未下单</text>
+                    </block>
+                    <block v-else>
+                      <text style="font-size: 26upx; margin-left: 20upx; color: #3385ff; font-weight: bold">待审核</text>
+                    </block>
+                  </view>
+                </view>
+              </view>
+            </view>
+          </tui-list-cell>
+        </block>
+      </block>
+      <block v-else>
+        <app-wrapper-empty :is-empty="true" />
+      </block>
+    </view>
+
+    <NotLogin></NotLogin>
+  </view>
+</template>
+<script>
+import AppTabs from "@/components/plugin/tabs";
+import TuiListCell from "@/components/plugin/list-cell";
+import AppWrapperEmpty from "@/components/app-wrapper-empty";
+import AppSearchModule from "@/components/module/app-search";
+import { list } from "@/mixins";
+import { getList } from "@/api/member";
+import NotLogin from "@/components/not-login";
+export default {
+  name: "memebr",
+  components: {
+    AppTabs,
+    TuiListCell,
+    AppWrapperEmpty,
+    NotLogin,
+    AppSearchModule
+  },
+  mixins: [list],
+  data() {
+    return {
+      tabIndex: 0,
+      tabs: [
+        {
+          name: "全部",
+          value: 0,
+          type:0
+        },
+        {
+          name: "余额",
+          value: 0,
+          type:2
+        },
+        {
+          name: "消费",
+          value: 0,
+          type:1
+        },
+        {
+          name: "生日",
+          value: 0,
+          type:3
+        }
+      ],
+      customType:0,
+      searchStyle:0,
+      searchText:''
+    };
+  },
+  onPullDownRefresh() {
+    this.resetList();
+    this.getMyCustomList().then(res => {
+      uni.stopPullDownRefresh();
+    });
+  },
+  onReachBottom() {
+    if (!this.list.finished) {
+      this.getMyCustomList().then(res => {
+        uni.stopPullDownRefresh();
+      });
+    } else {
+      uni.stopPullDownRefresh();
+    }
+  },
+  methods: {
+    change(e) {
+      if (this.tabIndex == e.index) {
+        return false;
+      }
+      this.tabIndex = e.index;
+      const tab = this.tabs[this.tabIndex];
+      this.customType = tab.type;
+      this.resetList();
+      this.getMyCustomList();
+    },
+    addCustom() {
+      this.pageTo({ url: '/admin/custom/addCustom' });
+    },
+    init() {
+      this.getMyCustomList();
+    },
+    searchFn() {
+      this.resetList();
+      this.getMyCustomList();
+    },
+    getMyCustomList() {
+      return getList({ page: this.list.page, type: this.customType, searchText: this.searchText, searchStyle: this.searchStyle }).then(res => {
+        if (res.code == 1) {
+          this.completes(res);
+        }
+      });
+    }
+  }
+};
+</script>
+<style lang="scss" scoped>
+.app-content {
+  padding-top: 100upx;
+  padding-bottom: 20upx;
+}
+.list-wrap {
+  .list {
+    background-color: #fff;
+    margin-bottom: 20upx;
+  }
+}
+.app-tabs {
+  position: fixed;
+  @media screen and (max-width: 1100px) {
+    width: 100%;
+  }
+  z-index: 9;
+}
+.tabs-wrap {
+  position: fixed;
+  .tabs-left {
+    width: 80%;
+  }
+  .tabs-right {
+    width: 19%;
+    border-left: $borderColor;
+    @include disFlex(center, center);
+  }
+}
+.input-wrap_box {
+  background-color: #fff;
+  position: fixed;
+  top: 0;
+  z-index: 10;
+  width: 100%;
+  height: 100upx;
+  display: flex;
+  align-items: center;
+  padding: 0 20upx;
+  gap: 20upx;
+
+  .admin-button-com {
+    flex-shrink: 0;
+    min-width: 120upx;
+  }
+
+  .search-container {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+  }
+}
+
+.user-card {
+  display: flex;
+  align-items: center;
+  padding: 12upx 0;
+  width: 100%;
+  position: relative;
+  box-sizing: border-box;
+
+  .user-avatar {
+    width: 80upx;
+    height: 80upx;
+    border-radius: 50%;
+    overflow: hidden;
+    margin-left: 16upx;
+    margin-right: 16upx;
+    flex-shrink: 0; /* 防止头像被挤压 */
+
+    .avatar-img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  .user-info {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    height: 80upx;
+    padding-right: 150upx; /* 减小右侧内边距,给名字留出更多空间 */
+    min-width: 0; /* 确保flex子元素可以正确缩小 */
+  }
+
+  .user-main {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 4upx;
+    width: 100%;
+  }
+
+  .user-name {
+    flex: 1;
+    font-size: 34upx;
+    color: #333;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    margin-right: 10upx;
+    max-width: calc(100% - 50upx); /* 进一步增加名字显示的最大宽度 */
+  }
+
+  .right-info {
+    position: absolute;
+    right: 50upx;
+    top: 8upx;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-end;
+    gap: 4upx;
+    width: 80upx; /* 进一步减小宽度以给名字留出更多空间 */
+    z-index: 1;
+    text-align: right;
+  }
+
+  .positive {
+    color: #3385ff;
+    margin-left: 20upx;
+    font-weight: bold;
+  }
+
+  .negative {
+    color: #ff0000;
+    margin-left: 20upx;
+    font-weight: bold;
+  }
+
+  .user-meta {
+    display: flex;
+    align-items: center;
+    font-size: 26upx;
+    color: #9397a4;
+    width: 495upx;
+    margin-top: 8upx;
+  }
+
+  .visit-time {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-size: 26upx;
+    width: 495upx;
+  }
+
+  .member-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+  }
+
+  .member-badge-icon {
+    width: 80upx;
+    height: 80upx;
+    flex-shrink: 0;
+    filter: drop-shadow(0 2upx 6upx rgba(0, 0, 0, 0.3));
+  }
+}
+
+// 确保箭头正常显示
+::v-deep .tui-list-cell.tui-cell-arrow:before {
+  z-index: 10;
+}
+</style>

+ 1 - 0
hdApp/src/admin/home/workbench.vue

@@ -12,6 +12,7 @@
         <view class="today-overview-card">
           <view class="today-header">
             <view class="today-title">今日概况</view>
+            <zui-svg-icon icon="home-message" color="#FFFFFF" :width="26" :height="26" @click="pageTo({url:'/admin/chat/list'})" />
           </view>
           
           <view class="today-income-section" @click="pageTo({url:'/admin/stat/kdIncome'})">

+ 1 - 0
hdApp/src/assets/svg-icons/home/message.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756353563597" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="36986" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 68.267c-243.95 0-443.733 169.813-443.733 382.293l0.068 6.639c2.833 143.445 97.724 270.438 241.408 333.79l4.779 2.048V921.6l0.102 2.611c1.894 26.505 32.768 41.063 54.545 24.662l154.42-116.156 3.003-0.068C763.87 826.044 955.733 658.79 955.733 450.56c0-212.48-199.782-382.293-443.733-382.293z m0 68.266c208.486 0 375.467 141.927 375.467 314.027S720.487 764.587 512 764.587l-3.157 0.153a34.133 34.133 0 0 0-17.357 6.707L382.788 853.18v-83.251A34.133 34.133 0 0 0 360.5 737.91C224.94 687.735 136.533 575.317 136.533 450.56c0-172.1 166.98-314.027 375.467-314.027z" fill="#2C2C2C" p-id="36987"></path><path d="M290.133 477.867a34.133 34.133 0 0 1 30.345 18.5l-1.621-2.424c38.126 58.778 95.897 92.826 165.512 94.72l6.161 0.086v68.266c-93.867 0-173.687-44.373-225.382-120.541l-3.567-5.36 0.17-0.119a34.133 34.133 0 0 1 28.382-53.128z" fill="#2C2C2C" p-id="36988"></path></svg>

+ 3 - 1
hdApp/src/pages.json

@@ -325,7 +325,9 @@
 		{
 			"root": "admin/chat",
 			"pages": [
-				{ "path": "index", "style": { "navigationBarTitleText": "客服", "enablePullDownRefresh": true } }
+				{ "path": "index", "style": { "navigationBarTitleText": "客服", "enablePullDownRefresh": true } },
+				{ "path": "list", "style": { "navigationBarTitleText": "聊天客户", "enablePullDownRefresh": true } },
+				{ "path": "chatPage", "style": { "navigationBarTitleText": "聊天" } }
 			]
 		},
 		{

+ 7 - 2
hdApp/src/static/svg-icons-lib.js

@@ -6,7 +6,7 @@
  *
  * !!! DO NOT MODIFY MANUALLY !!!
  *
- * @datetime 2025/7/30 21:22:48
+ * @datetime 2025/8/28 12:02:37
  *
  */
 
@@ -77,12 +77,17 @@ const collections = {
       "general-top-line": [
         "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" class=\"icon\" viewBox=\"0 0 1024 1024\"><path fill=\"#444\" d=\"M298.667 170.667h426.666V256H298.667zm211.731 130.137 60.943 60.943-181.02 181.02-60.943-60.943z\"/><path fill=\"#444\" d=\"m449.711 361.731 60.944-60.943 181.019 181.02-60.943 60.943z\"/><path fill=\"#444\" d=\"M469.333 384h85.334v469.333h-85.334z\"/></svg>",
         1
+      ],
+      "home-message": [
+        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" class=\"icon\" viewBox=\"0 0 1024 1024\"><path fill=\"#2c2c2c\" d=\"M512 68.267c-243.95 0-443.733 169.813-443.733 382.293l.068 6.639c2.833 143.445 97.724 270.438 241.408 333.79l4.779 2.048V921.6l.102 2.611c1.894 26.505 32.768 41.063 54.545 24.662l154.42-116.156 3.003-.068C763.87 826.044 955.733 658.79 955.733 450.56c0-212.48-199.782-382.293-443.733-382.293m0 68.266c208.486 0 375.467 141.927 375.467 314.027S720.487 764.587 512 764.587l-3.157.153a34.13 34.13 0 0 0-17.357 6.707L382.788 853.18v-83.251A34.13 34.13 0 0 0 360.5 737.91c-135.56-50.175-223.967-162.593-223.967-287.35 0-172.1 166.98-314.027 375.467-314.027\"/><path fill=\"#2c2c2c\" d=\"M290.133 477.867a34.13 34.13 0 0 1 30.345 18.5l-1.621-2.424c38.126 58.778 95.897 92.826 165.512 94.72l6.161.086v68.266c-93.867 0-173.687-44.373-225.382-120.541l-3.567-5.36.17-.119a34.133 34.133 0 0 1 28.382-53.128\"/></svg>",
+        2
       ]
     },
     "currentColor": "",
     "$_colorPalette": [
       "#22ac38",
-      "#444"
+      "#444",
+      "#2c2c2c"
     ]
   },
 }

+ 9 - 2
mallApp/src/api/category/index.js

@@ -1,19 +1,26 @@
 import https from '@/plugins/luch-request_0.0.7/request'
 
-/** *
+/**
  * 获取分类 m
  */
 export const getClass = data => {
 	return https.get('/category/list', data)
 }
 
-/** *
+/**
  * 获取分类的商品 m
  */
 export const getList = data => {
 	return https.get('/category/goods-list', data)
 }
 
+/**
+ * 获取无价格分类的商品
+ */
+export const getNonPriceList = data => {
+	return https.get('/category/non-price-goods-list', data)
+}
+
 // =====================  B 端 ========================
 
 /** *

+ 4 - 2
mallApp/src/pages.json

@@ -53,7 +53,8 @@
         {
             "root": "pages/goods",
             "pages": [
-                { "path": "detail", "style": { "navigationBarTitleText": "详情" } }
+                { "path": "detail", "style": { "navigationBarTitleText": "详情" } },
+                { "path": "picGoods", "style": { "navigationBarTitleText": "图集" } }
             ]
         },
         {
@@ -104,7 +105,8 @@
         {
             "root": "pages/chat",
             "pages": [
-                { "path": "index", "style": { "navigationBarTitleText": "客服", "enablePullDownRefresh": true } }
+                { "path": "index", "style": { "navigationBarTitleText": "客服", "enablePullDownRefresh": true } },
+                { "path": "chatPage", "style": { "navigationBarTitleText": "聊天" } }
             ]
         },
         {

+ 722 - 0
mallApp/src/pages/chat/chatPage.vue

@@ -0,0 +1,722 @@
+<template>
+  <view class="chat-container">
+    <!-- 连接状态指示器 -->
+    <view class="connection-status" v-if="!isConnected">
+      <text class="status-text">{{
+        reconnectCount > 0 ? "重连中..." : "连接中..."
+      }}</text>
+    </view>
+
+    <!-- 聊天消息列表 -->
+    <scroll-view
+      class="message-list"
+      :scroll-top="scrollTop"
+      scroll-y
+      :scroll-into-view="scrollIntoView"
+      scroll-with-animation
+    >
+      <view
+        class="message-item"
+        v-for="(message, index) in messageList"
+        :key="index"
+        :id="'message-' + index"
+      >
+        <!-- 左侧消息(商家/其他用户) -->
+        <view class="message-left" v-if="!message.isMine">
+          <view class="avatar-wrapper">
+            <image class="avatar" :src="message.avatar" mode="aspectFill" />
+          </view>
+          <view class="message-content-wrapper">
+            <view class="username" v-if="isMultiUser">{{
+              message.username
+            }}</view>
+            <view class="message-bubble left-bubble">
+              <text class="message-text">{{ message.content }}</text>
+            </view>
+            <view class="message-time">{{ message.time }}</view>
+          </view>
+        </view>
+
+        <!-- 右侧消息(我的) -->
+        <view class="message-right" v-else>
+          <view class="message-content-wrapper">
+            <view class="username align-right" v-if="isMultiUser">{{
+              message.username
+            }}</view>
+            <view class="message-bubble right-bubble">
+              <text class="message-text">{{ message.content }}</text>
+            </view>
+            <view class="message-time align-right">{{ message.time }}</view>
+          </view>
+          <view class="avatar-wrapper">
+            <image class="avatar" :src="message.avatar" mode="aspectFill" />
+          </view>
+        </view>
+      </view>
+
+      <!-- 空状态 -->
+      <view class="empty-state" v-if="messageList.length === 0">
+        <text class="empty-text">暂无聊天记录</text>
+      </view>
+    </scroll-view>
+
+    <!-- 输入区域 -->
+    <view class="input-area">
+      <view class="input-wrapper">
+        <input
+          class="message-input"
+          v-model="inputText"
+          placeholder="请输入消息..."
+          :placeholder-style="placeholderStyle"
+          @confirm="sendMessage"
+          confirm-type="send"
+        />
+        <button
+          class="send-button"
+          :class="{ 'send-active': inputText.trim() }"
+          @click="sendMessage"
+          :disabled="!inputText.trim()"
+        >
+          发送
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      inputText: "",
+      scrollTop: 0,
+      scrollIntoView: "",
+      placeholderStyle: "color: #ccc; font-size: 28upx;",
+      // 是否多人聊天
+      isMultiUser: false,
+      // 当前用户信息
+      currentUser: {
+        customId: 0,
+        shopId: 0,
+        id: 0,
+        username: "",
+        avatar: "",
+      },
+      // 聊天消息列表
+      messageList: [],
+      // WebSocket相关
+      socket: null,
+      isConnected: false,
+      room: "",
+      reconnectCount: 0,
+      maxReconnectCount: 5,
+    };
+  },
+  onLoad(options) {
+    console.log("---------- options: ", options);
+    // 根据传入参数判断是否多人聊天
+    if (options.isMultiUser) {
+      this.isMultiUser = options.isMultiUser === "true";
+    }
+
+    // 如果传入了用户信息,更新当前用户
+    if (options.customId) {
+      this.currentUser.customId = options.customId;
+    }
+    if (options.shopId) {
+      this.currentUser.shopId = options.shopId;
+    }
+    if (options.name) {
+      this.currentUser.username = options.name;
+    }
+    if (options.account) { // 1. 如果是门店,则使用shopId
+      this.currentUser.id = options.account;
+    }
+    if (options.avatar) {
+      this.currentUser.avatar = options.avatar;
+    }
+
+    // 获取房间号
+    if (options.room) {
+      this.room = options.room;
+    } else {
+      if (this.currentUser.customId && this.currentUser.shopId) {
+        this.room = this.currentUser.customId + "_" + this.currentUser.shopId;
+      } else {
+        // 如果没有传入房间号,使用默认值或生成一个
+        this.room = "default_room";
+      }
+    }
+    console.log("---------- this.room: ", this.room);
+
+    // 加载聊天记录
+    this.loadChatHistory();
+
+    // 连接WebSocket
+    this.connectWebSocket();
+  },
+  onReady() {
+    // 页面加载完成后滚动到底部
+    this.$nextTick(() => {
+      this.scrollToBottom();
+    });
+  },
+  onUnload() {
+    // 页面卸载时关闭WebSocket连接
+    this.disconnectWebSocket();
+  },
+  onHide() {
+    // 页面隐藏时可以选择性地关闭连接(根据业务需求)
+    // this.disconnectWebSocket()
+  },
+  onShow() {
+    // 页面显示时重新连接(如果之前断开了)
+    // if (!this.isConnected && this.room) {
+    //   this.connectWebSocket();
+    // }
+  },
+  methods: {
+    init(){},
+    // 加载聊天历史记录
+    loadChatHistory() {
+      // 在实际应用中,这里应该从服务器加载历史聊天记录
+      // 可以通过API请求获取该房间的历史消息
+
+      // 示例:
+      // uni.request({
+      //   url: `${API_BASE_URL}/chat/history`,
+      //   data: {
+      //     room: this.room,
+      //     userId: this.currentUser.id
+      //   },
+      //   success: (res) => {
+      //     if (res.data && res.data.messages) {
+      //       this.messageList = res.data.messages
+      //       this.$nextTick(() => {
+      //         this.scrollToBottom()
+      //       })
+      //     }
+      //   }
+      // })
+
+      // 临时保留一些示例数据用于开发测试
+      if (process.env.NODE_ENV === "development") {
+        const sampleMessages = [
+          {
+            id: "msg_001",
+            content: "您好,请问有什么可以帮助您的吗?",
+            time: "14:30",
+            isMine: false,
+            username: "花店客服",
+            avatar: "https://cdn.uviewui.com/uview/demo/user/2.jpg",
+          },
+          {
+            id: "msg_002",
+            content: "我想订购一束玫瑰花",
+            time: "14:31",
+            isMine: true,
+            username: "我",
+            avatar: "https://cdn.uviewui.com/uview/demo/user/1.jpg",
+          },
+        ];
+        this.messageList = sampleMessages;
+      }
+    },
+
+    // 获取主机地址
+    getHost() {
+      // 在实际项目中,这里应该返回你的WebSocket服务器地址
+      // 可以从配置文件中读取
+
+      // #ifdef H5
+      return window.location.host;
+      // #endif
+
+      // #ifdef MP-WEIXIN || APP-PLUS
+      // 小程序和App环境下,需要配置具体的服务器地址
+      return "192.168.10.57:8080"; // 替换为实际的服务器地址
+      // #endif
+    },
+
+    // 连接WebSocket
+    connectWebSocket() {
+      if (!this.room) {
+        console.error("房间号不能为空");
+        uni.showToast({
+          title: "房间号不能为空",
+          icon: "none",
+        });
+        return;
+      }
+
+      try {
+        // 构建WebSocket URL
+        const wsUrl = `ws://${this.getHost()}/room?room=${this.room}`;
+
+        // 创建WebSocket连接
+        this.socket = uni.connectSocket({
+          url: wsUrl,
+          success: () => {
+            console.log("WebSocket连接创建成功");
+          },
+          fail: (error) => {
+            console.error("WebSocket连接创建失败:", error);
+            this.handleConnectionError();
+          },
+        });
+
+        // 监听WebSocket连接打开
+        this.socket.onOpen(() => {
+          console.log("WebSocket连接已打开");
+          this.isConnected = true;
+          this.reconnectCount = 0;
+
+          uni.showToast({
+            title: "连接成功",
+            icon: "success",
+            duration: 1500,
+          });
+        });
+
+        // 监听WebSocket消息
+        this.socket.onMessage((event) => {
+          this.handleWebSocketMessage(event.data);
+        });
+
+        // 监听WebSocket连接关闭
+        this.socket.onClose(() => {
+          console.log("WebSocket连接已关闭");
+          this.isConnected = false;
+
+          // 尝试重连
+          if (this.reconnectCount < this.maxReconnectCount) {
+            setTimeout(() => {
+              this.reconnectWebSocket();
+            }, 3000);
+          }
+        });
+
+        // 监听WebSocket错误
+        this.socket.onError((error) => {
+          console.error("WebSocket连接错误:", error);
+          this.isConnected = false;
+          this.handleConnectionError();
+        });
+      } catch (error) {
+        console.error("创建WebSocket连接异常:", error);
+        this.handleConnectionError();
+      }
+    },
+
+    // 断开WebSocket连接
+    disconnectWebSocket() {
+      if (this.socket) {
+        this.socket.close();
+        this.socket = null;
+        this.isConnected = false;
+      }
+    },
+
+    // 重连WebSocket
+    reconnectWebSocket() {
+      if (this.reconnectCount < this.maxReconnectCount) {
+        this.reconnectCount++;
+        console.log(`正在尝试第${this.reconnectCount}次重连...`);
+
+        uni.showToast({
+          title: `重连中(${this.reconnectCount}/${this.maxReconnectCount})`,
+          icon: "loading",
+          duration: 1500,
+        });
+
+        this.connectWebSocket();
+      } else {
+        uni.showToast({
+          title: "连接失败,请检查网络",
+          icon: "none",
+          duration: 3000,
+        });
+      }
+    },
+
+    // 处理WebSocket消息
+    handleWebSocketMessage(data) {
+      console.log("---------- data: ", data);
+      try {
+        // 尝试解析JSON,如果失败则认为是纯文本消息
+        let messageData;
+        try {
+          const temp = JSON.parse(data);
+          //const msg = temp.message;
+          console.log("---------- temp: ", temp);
+          const msg = temp.message ? temp.message : temp; // TODO 测试兼容
+          messageData = JSON.parse(msg);
+        } catch (parseError) {
+          // 如果不是JSON格式,可能是纯文本消息,暂时忽略或创建简单消息对象
+          console.log("收到非JSON格式消息:", data);
+          try {
+            messageData = JSON.parse(data);
+          } catch (error) {
+            console.error("解析WebSocket消息失败:", error, data);
+          }
+        }
+
+        console.log("---------- messageData: ", messageData);
+        console.log("---------- this.currentUser: ", this.currentUser);
+        // 检查是否是自己发送的消息,避免重复显示
+        const isMyMessage =
+          messageData.username === this.currentUser.username &&
+          messageData.userId === this.currentUser.id;
+
+        // 如果是自己发送的消息,不重复添加(因为发送时已经添加了)
+        if (isMyMessage) {
+          console.log("忽略自己发送的消息回显");
+          return;
+        }
+
+        // 创建消息对象
+        const newMessage = {
+          id: "msg_" + Date.now() + "_" + Math.random(),
+          content: messageData.message || messageData || "",
+          time: this.getCurrentTime(),
+          isMine: false, // 既然不是自己的消息,就标记为false
+          username: messageData.name || messageData.username || "未知用户",
+          avatar:
+            messageData.avatar ||
+            "https://cdn.uviewui.com/uview/demo/user/2.jpg",
+        };
+
+        console.log("---------- newMessage: ", newMessage);
+        // 添加到消息列表
+        this.messageList.push(newMessage);
+
+        // 滚动到底部
+        this.$nextTick(() => {
+          this.scrollToBottom();
+        });
+      } catch (error) {
+        console.error("解析WebSocket消息失败:", error, data);
+      }
+    },
+
+    // 发送消息
+    sendMessage() {
+      if (!this.inputText.trim()) {
+        return;
+      }
+
+      if (!this.isConnected) {
+        uni.showToast({
+          title: "WebSocket未连接",
+          icon: "none",
+        });
+        return;
+      }
+
+      const messageContent = this.inputText.trim();
+
+      try {
+        // 立即在本地显示发送的消息(右侧)
+        const myMessage = {
+          id: "msg_" + Date.now() + "_local",
+          content: messageContent,
+          time: this.getCurrentTime(),
+          isMine: true,
+          username: this.currentUser.username,
+          avatar: this.currentUser.avatar,
+        };
+
+        // 添加到消息列表
+        this.messageList.push(myMessage);
+
+        // 清空输入框
+        this.inputText = "";
+
+        // 滚动到底部
+        this.$nextTick(() => {
+          this.scrollToBottom();
+        });
+
+        // 构建要发送的消息数据
+        const messageData = {
+          message: messageContent,
+          name: this.currentUser.username,
+          username: this.currentUser.username,
+          userId: this.currentUser.id,
+          avatar: this.currentUser.avatar,
+          timestamp: Date.now(),
+        };
+
+        const json = JSON.stringify(messageData);
+        console.log("send ---------- json: ", json);
+        // 发送到WebSocket服务器
+        this.socket.send({
+          data: json,
+          success: () => {
+            console.log("消息发送成功");
+          },
+          fail: (error) => {
+            console.error("消息发送失败:", error);
+            uni.showToast({
+              title: "发送失败",
+              icon: "none",
+            });
+            // 发送失败时,可以考虑移除刚添加的消息或标记为失败
+          },
+        });
+      } catch (error) {
+        console.error("发送消息异常:", error);
+        uni.showToast({
+          title: "发送异常",
+          icon: "none",
+        });
+      }
+    },
+
+    // 处理连接错误
+    handleConnectionError() {
+      uni.showToast({
+        title: "连接失败",
+        icon: "none",
+        duration: 2000,
+      });
+    },
+
+    // 滚动到底部
+    scrollToBottom() {
+      if (this.messageList.length > 0) {
+        this.scrollIntoView = "message-" + (this.messageList.length - 1);
+      }
+    },
+
+    // 获取当前时间
+    getCurrentTime() {
+      const now = new Date();
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      return `${hours}:${minutes}`;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.chat-container {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background-color: #f5f5f5;
+}
+
+.connection-status {
+  background-color: #ff9800;
+  color: #ffffff;
+  text-align: center;
+  padding: 16upx;
+  font-size: 24upx;
+
+  .status-text {
+    color: #ffffff;
+  }
+}
+
+.message-list {
+  flex: 1;
+  padding: 20upx;
+  overflow-y: auto;
+}
+
+.message-item {
+  margin-bottom: 30upx;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.message-left {
+  display: flex;
+  justify-content: flex-start;
+  align-items: flex-start;
+}
+
+.message-right {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-start;
+}
+
+.avatar-wrapper {
+  flex-shrink: 0;
+  margin: 0 20upx;
+}
+
+.avatar {
+  width: 80upx;
+  height: 80upx;
+  border-radius: 50%;
+  background-color: #e0e0e0;
+}
+
+.message-content-wrapper {
+  max-width: 520upx;
+  min-width: 100upx;
+}
+
+.username {
+  font-size: 24upx;
+  color: $fontColor3;
+  margin-bottom: 8upx;
+
+  &.align-right {
+    text-align: right;
+  }
+}
+
+.message-bubble {
+  padding: 20upx 24upx;
+  border-radius: 16upx;
+  position: relative;
+  word-wrap: break-word;
+  word-break: break-all;
+
+  &.left-bubble {
+    background-color: #ffffff;
+    margin-left: 0;
+    border-top-left-radius: 4upx;
+
+    &::before {
+      content: "";
+      position: absolute;
+      left: -12upx;
+      top: 20upx;
+      width: 0;
+      height: 0;
+      border: 6upx solid transparent;
+      border-right-color: #ffffff;
+    }
+  }
+
+  &.right-bubble {
+    background-color: rgb(27, 193, 66);
+    margin-right: 0;
+    border-top-right-radius: 4upx;
+
+    &::before {
+      content: "";
+      position: absolute;
+      right: -12upx;
+      top: 20upx;
+      width: 0;
+      height: 0;
+      border: 6upx solid transparent;
+      border-left-color: rgb(27, 193, 66);
+    }
+
+    .message-text {
+      color: #ffffff;
+    }
+  }
+}
+
+.message-text {
+  font-size: 28upx;
+  line-height: 1.4;
+  color: $fontColorMain;
+}
+
+.message-time {
+  font-size: 20upx;
+  color: $fontColor3;
+  margin-top: 8upx;
+
+  &.align-right {
+    text-align: right;
+  }
+}
+
+.empty-state {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 300upx;
+}
+
+.empty-text {
+  font-size: 28upx;
+  color: $fontColor3;
+}
+
+.input-area {
+  background-color: #ffffff;
+  padding: 20upx;
+  border-top: 1upx solid $borderColor;
+  // 适配安全区域
+  padding-bottom: calc(20upx + env(safe-area-inset-bottom));
+}
+
+.input-wrapper {
+  display: flex;
+  align-items: center;
+  background-color: #f8f8f8;
+  border-radius: 50upx;
+  padding: 16upx 20upx;
+}
+
+.message-input {
+  flex: 1;
+  font-size: 28upx;
+  border: none;
+  background-color: transparent;
+  outline: none;
+
+  &::placeholder {
+    color: #ccc;
+  }
+}
+
+.send-button {
+  margin-left: 20upx;
+  padding: 12upx 32upx;
+  background-color: $fontColor4;
+  color: #ffffff;
+  border: none;
+  border-radius: 50upx;
+  font-size: 26upx;
+  transition: all 0.3s ease;
+
+  &.send-active {
+    background-color: $mainColor;
+  }
+
+  &:disabled {
+    background-color: $fontColor4;
+    color: #ffffff;
+  }
+
+  &::after {
+    border: none;
+  }
+}
+
+// 适配不同设备
+/* #ifdef H5 */
+.chat-container {
+  height: calc(100vh - 44px); // 减去导航栏高度
+}
+/* #endif */
+
+/* #ifdef MP-WEIXIN */
+.input-area {
+  // 微信小程序底部安全区域
+  padding-bottom: calc(20upx + env(safe-area-inset-bottom));
+}
+/* #endif */
+
+/* #ifdef APP-PLUS */
+.input-area {
+  // App端底部安全区域
+  padding-bottom: calc(20upx + env(safe-area-inset-bottom));
+}
+/* #endif */
+</style>

+ 31 - 12
mallApp/src/pages/goods/components/buy-foot.vue

@@ -1,31 +1,50 @@
 <template>
-	<div class="app-footer">
-        <div class="btn-left">
-            <div class="btn-wrap">
+	<view class="app-footer">
+        <view class="btn-left" v-if="priceType == 1">
+            <view class="btn-wrap">
                 <i class="iconfont icondianpu"></i>
-                <div class="btn-text">店铺</div>
-            </div>
-            <div class="btn-wrap">
+                <view class="btn-text">店铺</view>
+            </view>
+            <view class="btn-wrap">
                 <i class="iconfont iconxiaoxi"></i>
-                <div class="btn-text">咨询</div>
-            </div>
-        </div>
+                <view class="btn-text" @click="goChat">咨询</view>
+            </view>
+        </view>
+        <view class="btn-text" v-else-if="priceType == 0">
+            <view class="btn-wrap">
+                <i class="iconfont iconxiaoxi"></i>
+                <view class="btn-text" @click="goChat">咨询</view>
+            </view>
+        </view>
         <div class="btn-right">
-            <button class="button-com red" @click="buy">购买</button>
+            <button v-if="priceType == 1" class="button-com red" @click="buy">购买</button>
+            <button v-else class="button-com red" @click.stop="goChat">询价</button>
         </div>
-    </div>
+    </view>
 </template>
 
 <script>
 export default {
     name: 'buy-foot',
-    props:['stock'],
+    props: {
+        stock: {
+            type: Number,
+            default: 0
+        },
+        priceType: {
+            type: Number,
+            default: 1
+        }
+    },
 	methods: {
 		buy() {
 			this.$emit('buy')
 		},
         goShop(){
             this.$emit('goShop')
+        },
+        goChat(){
+            this.$emit('goChat')
         }
 	}
 }

+ 9 - 2
mallApp/src/pages/goods/detail.vue

@@ -10,7 +10,7 @@
       :list="imgList"
     />
     <view class="shop-intro">
-      <view class="app-price app-bold">
+      <view class="app-price app-bold" v-if="data.priceType == 1">
         <text>¥</text>
         <text class="app-size-34">{{
           data.price ? parseFloat(data.price) : 0
@@ -84,7 +84,7 @@
       @change="changeNum"
       @close="hidePopup"
     />
-    <buy-foot @buy="showPopup" @goShop="goToShop" :stock="data.stock" />
+    <buy-foot @buy="showPopup" @goShop="goToShop" :stock="data.stock" :priceType="data.priceType" @goChat="goChat" />
   </scroll-view>
 </template>
 <script>
@@ -204,6 +204,13 @@ export default {
       this.showShare = true;
       // #endif
     },
+    goChat(){
+      const account = uni.getStorageSync('account') || this.account || (this.shopUser && this.shopUser.account) || '';
+      this.$util.pageTo({
+        url: "/pages/chat/chatPage",
+        query: { account }
+      });
+    }
   },
 };
 </script>

+ 646 - 0
mallApp/src/pages/goods/picGoods.vue

@@ -0,0 +1,646 @@
+<template>
+    <view class="app-main app-content">
+  
+          <view class="c_header" :class="myClass">
+              <view class="header_inside">
+                  <image :src="shopImg"></image>
+                  <view class="shop_info">
+                      <view class="shop_title">{{shopName}}</view>
+                  </view>
+              </view>
+          </view>
+  
+          <!-- 搜索框和花材绿植按钮区域 -->
+          <view class="search-wrap">
+              <view class="search-bar">
+                  <app-search-module 
+                      v-model="searchText" 
+                      :placeholder="isSearching ? '正在搜索...' : '搜索商品名称'" 
+                      @input="searchFn" 
+                      :height="66"
+                      :fontSize="28"
+                  />
+              </view>
+              <view class="buy-material-container">
+                  <button class="buy-material-btn" @click="buyProduct()">
+                      <view class="btn-content">
+                          <text class="btn-text">花材绿植</text>
+                      </view>
+                  </button>
+              </view>
+          </view>
+          <!-- 搜索状态提示 -->
+          <view v-if="isSearching" class="search-loading">
+              <text class="loading-text">正在搜索...</text>
+          </view>
+  
+      <scroll-view scroll-y scroll-with-animation class="tab-view" :scroll-top="scrollTop">
+        <view v-for="(item,index) in tabbar" :key="index" class="tab-bar-item" :class="[!isSearchMode && currentTab==index ? 'active' : '']" :data-current="index" @tap.stop="swichNav($event ,item)" >
+          <text>{{item.categoryName}}</text>
+        </view>
+        <view style="height:150upx"></view>
+      </scroll-view>
+      
+      <!-- 搜索结果展示区域 -->
+      <scroll-view v-if="isSearchMode" scroll-y class="right-box" @scrolltolower="reachBottom">
+        <view class="page-view" v-if="!$util.isEmpty(list.data)" >
+          <view class="goods-list" v-for="(items) in list.data" :key="items.id" @click="toBuy(items)" >
+            <view class="img-blo">
+              <image :src="items.smallCover" mode="aspectFill" ></image>
+            </view>
+            <view class="goods-det">
+              <view class="goods-det-top">
+                <view class="goods-name">{{ items.name }}</view>
+                <view class="goods-sales"></view>
+              </view>
+              <view class="goods-price">已售 {{ items.actualSold?parseInt(items.actualSold):0 }}</view>
+              <view class="buy-btn">查看</view>
+            </view>
+          </view>
+        </view>
+        <block v-else>
+          <app-wrapper-empty 
+            :title="getEmptyTitle()" 
+            :is-empty="$util.isEmpty(list.data)" 
+          />
+        </block>
+      </scroll-view>
+  
+      <!-- 分类商品展示区域 -->
+      <block v-for="(item,index) in tabbar" :key="index" >
+        <scroll-view scroll-y class="right-box" @scrolltolower="reachBottom" v-if="!isSearchMode && currentTab==index" >
+          <view class="page-view" v-if="!$util.isEmpty(list.data)" >
+            <view class="goods-list" v-for="(items) in list.data" :key="items.id" @click="toBuy(items)" >
+              <view class="img-blo">
+                <image :src="items.smallCover" mode="aspectFill" ></image>
+              </view>
+              <view class="goods-det">
+                <view class="goods-det-top">
+                  <view class="goods-name">{{ items.name }}</view>
+                  <view class="goods-sales"></view>
+                </view>
+                <view class="goods-price">已售 {{ items.actualSold?parseInt(items.actualSold):0 }}</view>
+                <view class="buy-btn">查看</view>
+              </view>
+            </view>
+          </view>
+          <block v-else>
+            <app-wrapper-empty 
+              :title="getEmptyTitle()" 
+              :is-empty="$util.isEmpty(list.data)" 
+            />
+          </block>
+        </scroll-view>
+      </block>
+  
+      <wangCg :loginStyle.sync="globalLoginStyle" @goToLogin="loginTo()"></wangCg>
+    </view>
+  </template>
+  <script>
+  import { mapGetters } from 'vuex'
+  import AppImg from '@/components/app-img'
+  import wangCg from "@/components/wangCg"
+  import AppWrapperEmpty from '@/components/app-wrapper-empty'
+  import AppSearchModule from '@/components/item/module/app-search'
+  import { getClass, getNonPriceList } from '@/api/category'
+  import { list,share } from '@/mixins'
+  import { getInfo } from "@/api/shop"
+  export default {
+    name: 'category',
+    components: {
+      AppWrapperEmpty,
+      AppImg,
+      wangCg,
+      AppSearchModule
+    },
+    mixins: [list, share],
+    data () {
+      return {
+        tabbar: [],
+        height: 0,
+        currentTab: 0,
+        scrollTop: 0,
+        form: {
+          categoryId: ''
+        },
+        currentTabIndex: 1,
+        myClass: 'fadeIn',
+              shopName:'',
+              shopImg:'',
+        hdId:0,
+        searchText: '', // 搜索文本
+        searchTimer: null, // 搜索防抖定时器
+        isSearchMode: false, // 是否为搜索模式
+        isSearching: false, // 是否正在搜索
+        lastSearchText: '' // 上次搜索的文本,用于优化
+      }
+    },
+      computed: {
+      ...mapGetters({ loginInfo:"getLoginInfo" })
+      },
+    onShareAppMessage(res) {
+          return {
+              title: '花束花礼',
+              desc: "",
+              path: "pages/home/category?account="+this.option.account,
+          }
+      },
+      onShow(){
+          if(!this.$util.isEmpty(this.loginInfo)){
+              this.globalLoginStyle = 1
+          }else{
+              this.globalLoginStyle = 0
+          }
+    },
+    onUnload() {
+      // 清理搜索定时器
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer);
+        this.searchTimer = null;
+      }
+    },
+  
+      watch: {
+          loginInfo: {
+              deep: true,
+              handler: function(newVal) {
+                  if(!this.$util.isEmpty(newVal)){
+                      this.globalLoginStyle = 1
+                  }else{
+                      this.globalLoginStyle = 0
+                  }
+              },
+              immediate:true
+          }
+      },
+    methods: {
+      toBuy(item){
+        let account = this.option.account?this.option.account:0
+        this.$util.pageTo({url:"/pages/goods/detail?id="+item.id+'&hdId='+this.hdId+'&categoryId='+this.form.categoryId+'&account='+account})
+      },
+      buyProduct(){
+        let account = this.option.account?this.option.account:0
+        this.$util.pageTo({url:"/pages/item/item?account="+account+'&hdId='+this.hdId,type:2})
+      },
+      loginTo() {
+        let account = this.option.account?this.option.account:0
+        this.$util.pageTo({url:"/pages/login/index?needBack=1&account="+account})
+      },
+      init(){
+        this.hdId = this.option.hdId ? this.option.hdId : 0
+        this.classInit()
+              getInfo({hdId:this.hdId}).then(res=>{
+                  if(res.code == 1){
+                      if(res.data.info && res.data.info.merchantName){
+                          let sjName = res.data.info.merchantName
+                          let shopName = res.data.info.shopName ? res.data.info.shopName : '首店'
+                          let name = shopName == '首店' ? sjName : sjName+' '+shopName
+                          this.shopName = name
+                          this.shopImg = res.data.info.avatar
+                      }
+  
+                      if(res.data.hd && Number(res.data.hd.id) > 0){
+                          if(Number(this.hdId) == 0){
+                              this.hdId = res.data.hd.id
+                          }
+                      }
+  
+                  }
+              })
+      },
+      // 搜索输入处理(防抖)
+      searchFn() {
+        console.log('searchFn----------', this.searchText, 'lastSearchText:', this.lastSearchText)
+        
+        // 清除之前的定时器
+        if (this.searchTimer) {
+          clearTimeout(this.searchTimer)
+          this.searchTimer = null
+        }
+        
+        // 如果搜索文本为空
+        if (this.$util.isEmpty(this.searchText)) {
+          // 如果之前也是空文本(首次点击或焦点清空),跳过搜索
+          if (this.$util.isEmpty(this.lastSearchText)) {
+            console.log('搜索文本为空且之前也为空,跳过搜索')
+            return
+          }
+          // 如果之前有搜索文本,现在清空了,则恢复分类筛选
+          this.clearSearch()
+          return
+        }
+        
+        // 如果搜索文本与上次相同,不重复搜索
+        if (this.searchText === this.lastSearchText) {
+          console.log('搜索文本未变化,跳过搜索')
+          return
+        }
+        
+        // 设置防抖定时器,1秒后执行搜索
+        this.searchTimer = setTimeout(() => {
+          this.performSearch()
+        }, 1000)
+      },
+      
+      // 执行搜索
+      performSearch() {
+        if (this.$util.isEmpty(this.searchText)) {
+          return
+        }
+        
+        console.log('执行搜索:', this.searchText)
+        
+        this.isSearchMode = true
+        this.isSearching = true
+        this.lastSearchText = this.searchText
+        this.resetList()
+        this.searchGoods()
+      },
+      
+      // 清空搜索,恢复分类筛选
+      clearSearch() {
+        console.log('清空搜索,恢复分类筛选')
+        this.isSearchMode = false
+        this.isSearching = false
+        this.lastSearchText = ''
+        
+        // 确保搜索框内容被清空
+        this.$nextTick(() => {
+          this.searchText = ''
+        })
+        
+        this.resetList()
+        this.getGoodList()
+      },
+      
+      // 搜索商品
+      searchGoods() {
+        return getNonPriceList({ 
+          searchText: this.searchText, 
+          page: this.list.page, 
+          pageSize: 10 
+        }).then(res => {
+          this.isSearching = false
+          this.completes(res)
+          console.log('搜索成功,数据已加载')
+        }).catch(err => {
+          this.isSearching = false
+          console.error('搜索失败:', err)
+          this.$msg('搜索失败,请重试')
+        })
+      },
+      reachBottom () {
+        console.log('reachBottom----------', this.list.finished)
+        if (this.list.finished == false && !this.isSearching) {
+          // 根据是否为搜索模式决定调用哪个方法
+          if (this.isSearchMode) {
+            this.searchGoods().then(res => {
+              uni.stopPullDownRefresh()
+            });
+          } else {
+            this.getGoodList().then(res => {
+              uni.stopPullDownRefresh()
+            });
+          }
+        } else {
+          uni.stopPullDownRefresh()
+        }
+      },
+      async classInit () {
+        this.getMyClass() .then(res => {
+          this.getGoodList()
+        })
+      },
+      // 分类
+      getMyClass () {
+        return getClass().then(res => {
+          if(res.code && res.code == 1){
+            this.form.categoryId = res.data[0].id
+            this.tabbar = res.data
+          }
+        })
+      },
+      // 列表
+      getGoodList () {
+        return getNonPriceList({ ...this.form, page: this.list.page,pageSize:10 }) .then(res => {
+            // 处理商品列表数据,completes方法来自list混入
+            // 该方法会处理分页数据,更新list对象的data、page、finished等属性
+            // res包含接口返回的商品列表数据,completes方法会将数据添加到this.list.data中
+            this.completes(res)
+        })
+      },
+      // 点击标题切换当前页时改变样式
+      swichNav (e, item) {
+        let cur = e.currentTarget.dataset.current
+        if (this.currentTab == cur && !this.isSearchMode) {
+          return false
+        } else {
+          this.currentTab = cur
+          this.form.categoryId = item.id
+          
+          // 切换分类时清空搜索状态
+          this.clearSearchState()
+          this.resetList()
+          this.getGoodList()
+        }
+      },
+      // 判断当前滚动超过一屏时,设置tab标题滚动条。
+      checkCor () {
+        let that = this
+        // 这里计算按照实际情况进行修改,动态数据要进行动态分析
+        // 思路:窗体高度/单个分类高度 200upx 转px计算 =>得到一屏幕所显示的个数,结合后台传回分类总数进行计算
+        // 数据很多可以多次if判断然后进行滚动距离计算即可
+        if (that.currentTab > 7) {
+          that.scrollTop = 500
+        } else {
+          that.scrollTop = 0
+        }
+      },
+      tabClick (index) {
+        this.currentTabIndex = index
+      },
+      
+      // 清空搜索状态(不触发数据重新加载)
+      clearSearchState() {
+        // 清除定时器
+        if (this.searchTimer) {
+          clearTimeout(this.searchTimer)
+          this.searchTimer = null
+        }
+        
+        // 重置搜索相关状态
+        this.searchText = ''
+        this.isSearchMode = false
+        this.isSearching = false
+        this.lastSearchText = ''
+        
+        // 确保搜索框内容被清空(强制更新)
+        // this.$nextTick(() => {
+        //   this.searchText = ''
+        // })
+      },
+      
+      // 获取空状态提示文字
+      getEmptyTitle() {
+        if (this.isSearchMode && this.searchText) {
+          return `未找到"${this.searchText}"相关商品`
+        }
+        return '暂无商品'
+      }
+    },
+    
+    // 组件销毁时清理定时器
+    beforeDestroy() {
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+        this.searchTimer = null
+      }
+    }
+  }
+  </script>
+  
+  <style lang="scss" scoped>
+  .app-content {
+    background-color: #fff;
+  
+  .c_header{
+      width: 100vw;
+      padding: 0 15upx;
+      box-sizing: border-box;
+      background: linear-gradient(to bottom,#3385FF, #fff);
+      &>.header_inside{
+        position: relative;
+        height: 124upx;
+        margin-top: 15upx;
+        background: #fff;
+        box-shadow: 0upx 1px 21px 0upx rgba(161, 166, 173, 0.4);
+        border-radius: 20upx;
+        padding: 35upx 30upx 30upx 30upx;
+        box-sizing: border-box;
+        &>image{
+          height: 60upx;
+          width: 60upx;
+          display: inline-block;
+          border-radius: 10upx;
+          background-color: #eee;
+          vertical-align: top;
+        }
+        &>.shop_info{
+          vertical-align: top;
+          display: inline-block;
+          height: 100%;
+          margin-left: 20upx;
+          &>.shop_title{
+            margin-top:0upx;
+            font-size:40upx;
+            font-weight: 600;
+            color: #060606;
+            width: 580upx;
+            overflow: hidden;
+            white-space: nowrap;
+          }
+        }
+      }
+    }
+  
+    .fadeIn{
+      height:180upx;
+      overflow: hidden;
+      animation-name: fadeInFrames;
+      animation-duration:.3s;
+    }
+  
+  }
+  
+  page {
+    background: #fff;
+  }
+  
+  ::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+    color: transparent;
+  }
+  
+  .tab-view {
+    ::-webkit-scrollbar {
+      width: 0;
+      height: 0;
+      color: transparent;
+    }
+    width: 200upx;
+    position: fixed;
+    left: 0;
+    z-index: 10;
+    height: calc(100vh - 160upx); /* 调整高度以适应搜索框 */
+    background-color: #f0f2f6;
+    .tab-bar-item {
+      width: 200upx;
+      height: 110upx;
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 28upx;
+      color: #444;
+      font-weight: 400;
+    }
+    .active {
+      position: relative;
+      color: $mainColor;
+      font-size: 26upx;
+      background: #fff;
+    }
+  
+    .active::before {
+      content: "";
+      position: absolute;
+      border-left: 8upx solid $mainColor;
+      height: 100%;
+      left: 0;
+    }
+  }
+  .right-box {
+    width: 100%;
+    overflow: auto;
+    height: calc(100vh - 280upx); /* 调整高度以适应搜索框 */
+    padding-left: 220upx;
+    box-sizing: border-box;
+    .page-view {
+      width: 100%;
+      overflow: hidden;
+      box-sizing: border-box;
+      margin-bottom:60upx;
+      .goods-list {
+        @include disFlex(flex-start, flex-start);
+        margin: 40upx 0;
+        .img-blo {
+          width: 200upx;
+          height: 200upx;
+          margin-right: 20upx;
+          image{
+            width: 200upx;
+            height: 200upx;
+          }
+        }
+        .goods-det {
+          padding-top: 18upx;
+          font-size: 28upx;
+          position: relative;
+          width: calc(100% - 240upx);
+          .goods-det-top {
+            .goods-name {
+              font-size: 32upx;
+              margin-bottom: 10upx;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              display: -webkit-box;
+              -webkit-line-clamp: 2;
+              line-clamp: 2;
+              -webkit-box-orient: vertical;
+              word-break: break-all;
+            }
+          }
+          .goods-price {
+            font-size: 22upx;
+            margin-top: 110upx;
+            color: #676767;
+          }
+        }
+      }
+    }
+  }
+  
+  /* 搜索框样式 */
+  .search-wrap {
+      padding: 15upx 30upx 25upx 30upx;
+      background: linear-gradient(to bottom, #3385FF, #fff);
+      display: flex;
+      align-items: center;
+      gap: 20upx;
+  }
+  
+  .search-bar {
+      flex: 1;
+      background-color: #fff;
+      border-radius: 30upx;
+      padding: 2upx;
+      box-shadow: 0upx 2px 8px 0upx rgba(0, 0, 0, 0.1);
+      position: relative;
+  }
+  
+  .search-loading {
+      padding: 10upx 30upx;
+      text-align: center;
+      background-color: #f5f5f5;
+      
+      .loading-text {
+          font-size: 24upx;
+          color: #666;
+      }
+  }
+  
+  .buy-material-container {
+      flex-shrink: 0;
+  }
+  
+  .buy-material-btn {
+      position: relative;
+      background: linear-gradient(135deg, #4caf50, #66bb6a, #81c784);
+      border: none;
+      border-radius: 50upx;
+      padding: 0;
+      height: 80upx;
+      min-width: 160upx;
+      box-shadow: 0 8upx 20upx rgba(76, 175, 80, 0.3);
+      overflow: hidden;
+  
+  
+      &:active {
+          box-shadow: 0 4upx 10upx rgba(76, 175, 80, 0.4);
+      }
+  
+      .btn-content {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          height: 100%;
+          position: relative;
+          z-index: 1;
+      }
+  
+      .btn-text {
+          color: white;
+          font-size: 28upx;
+          font-weight: bold;
+          text-shadow: 0 2upx 4upx rgba(0, 0, 0, 0.2);
+          margin: 0 8upx;
+      }
+  }
+  
+  @keyframes fadeInFrames {
+      0% {
+          opacity: 0;
+          transform: translateY(0);
+          height: 0;
+      }
+      100% {
+          opacity: 1;
+          height: 180upx;
+      }
+  }
+  
+  .buy-btn {
+    color: red;
+    border: 1upx solid red;
+    border-radius: 10upx;
+    width: 100upx;
+    text-align: center;
+    padding: 5upx 0;
+    font-size: 25upx;
+    position: absolute;
+    right: 30upx;
+    top: 150upx;
+  }
+  </style>
+  

+ 6 - 2
mallApp/src/pages/home/recent.vue

@@ -34,6 +34,9 @@
                     <view class="popup-item" @click="toPay(item)">
                       <text>付款</text>
                     </view>
+                    <view class="popup-item" @click="getLevelChange(item)">
+                      <text>等级变动</text>
+                    </view>
                     <view class="popup-item" @click="hideMoreOptions()">
                       <text>取消</text>
                     </view>
@@ -51,7 +54,7 @@
                 </view>
                 <view class="box_4 flex-col"></view>
                 <view class="box_9">
-                  <view class="tag_2" @click="getLevelChange(item)"> <text>等级变动</text> </view>
+                  <view class="tag_2" @click="pageTo({ url: '/pages/chat/chatPage', query: {customId: loginInfo.id, shopId: item.shopId, name: item.name, account: item.shopId, avatar: item.smallAvatar } })"> <text>联系商家</text> </view>
                   <view class="tag_2" @click="getBalanceChange(item)"> <text>余额变动</text> </view>
                   <view class="tag_2" @click="getSettleList(item)"> <text>结账记录</text> </view>
                   <view class="tag_2" @click="getBuyList(item)"> <text>买花记录</text> </view>
@@ -203,7 +206,8 @@ export default {
       this.pageTo({ url:'/pages/home/category?id='+item.id+'&account='+item.shopId})
     },
     tj(item){
-      this.pageTo({ url:'/pages/home/mall?id='+item.id+'&account='+item.shopId})
+      //this.pageTo({ url:'/pages/goods/mall?id='+item.id+'&account='+item.shopId})
+      this.pageTo({ url:'/pages/goods/picGoods?id='+item.id+'&account='+item.shopId})
     },
     init () {
       this.getMyHdList()