Просмотр исходного кода

feat(mallApp): 新增商城消息入口

- 增加消息 tab 与消息列表页面,支持查看最近聊天门店并进入会话

- 首页搜索栏与客服入口替换为 SVG 图标,顶部消息入口跳转消息列表

- 商品详情发起客服会话时兼容 account 作为门店标识
shizhongqi 4 дней назад
Родитель
Сommit
3324bec694

+ 1 - 0
mallApp/src/assets/svg-icons/general/customerService.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="1785746702311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2993" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M139.6 372.4v232.7H46.5V372.4h93.1m0-46.6H46.5c-25.6 0-46.5 21-46.5 46.6v232.7c0 25.6 20.9 46.5 46.5 46.5h93.1c25.6 0 46.5-20.9 46.5-46.5V372.4c0.1-25.6-20.9-46.6-46.5-46.6zM977.5 372.4v232.7h-93.1V372.4h93.1m0-46.6h-93.1c-25.6 0-46.5 20.9-46.5 46.5V605c0 25.6 20.9 46.5 46.5 46.5h93.1c25.6 0 46.5-20.9 46.5-46.5V372.4c0-25.6-20.9-46.6-46.5-46.6zM512 791.3c51.3 0 93.1 41.8 93.1 93.1s-41.8 93.1-93.1 93.1-93.1-41.8-93.1-93.1 41.8-93.1 93.1-93.1m0-46.6c-77.1 0-139.6 62.5-139.6 139.6S434.9 1024 512 1024s139.6-62.5 139.6-139.6S589.1 744.7 512 744.7zM418.9 325.8h-93.1v93.1h93.1v-93.1zM698.2 325.8h-93.1v93.1h93.1v-93.1zM672.3 558.5c-32.3 55.4-91.7 93.1-160.3 93.1s-128-37.7-160.3-93.1h-52.8c35.9 82.1 117.7 139.6 213.1 139.6s177.1-57.5 213.1-139.6h-52.8z" fill="#515151" p-id="2994"></path><path d="M117.5 325.8C175.2 163.4 330 46.5 512 46.5s336.8 116.8 394.5 279.3h49.6C896.8 137 720.4 0 512 0S127.2 137 67.9 325.8h49.6zM886.7 651.6c-48.2 96.6-132.4 171.8-235.1 208.3v49.6C780.4 869 884.9 774 938.4 651.6h-51.7z" fill="#515151" p-id="2995"></path></svg>

+ 1 - 0
mallApp/src/assets/svg-icons/general/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="1785746241908" class="icon" viewBox="0 0 1080 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1975" xmlns:xlink="http://www.w3.org/1999/xlink" width="210.9375" height="200"><path d="M207.303111 458.638222a64.284444 64.284444 0 1 0 128.455111 0 64.284444 64.284444 0 0 0-128.455111 0z m253.895111 0a64.284444 64.284444 0 1 0 128.455111 0 64.284444 64.284444 0 0 0-128.455111 0z m253.838222 0a64.284444 64.284444 0 1 0 128.512 0 64.284444 64.284444 0 0 0-128.512 0z m288.654223-180.053333a458.467556 458.467556 0 0 0-111.786667-145.692445C794.396444 47.217778 665.144889 0.341333 527.928889 0.341333c-137.272889 0-266.524444 47.104-363.861333 132.551111A456.817778 456.817778 0 0 0 52.280889 278.584889a412.046222 412.046222 0 0 0-41.358222 180.053333 419.271111 419.271111 0 0 0 57.685333 210.659556c32.995556 56.490667 79.644444 106.951111 135.623111 146.944v151.836444a36.693333 36.693333 0 0 0 54.954667 31.687111l160.654222-92.728889c35.384889 6.712889 71.850667 10.069333 108.145778 10.069334 137.329778 0 266.581333-47.160889 363.918222-132.551111a456.817778 456.817778 0 0 0 111.786667-145.749334 412.046222 412.046222 0 0 0 41.358222-180.053333 409.998222 409.998222 0 0 0-41.358222-180.167111z m-475.704889 565.248a509.952 509.952 0 0 1-106.780445-11.264 36.579556 36.579556 0 0 0-25.941333 4.096l-117.76 68.096V797.013333a36.977778 36.977778 0 0 0-16.497778-30.663111c-112.469333-73.671111-176.981333-185.799111-176.981333-307.768889 0-212.366222 199.111111-385.024 443.960889-385.024 244.736 0 444.017778 172.657778 444.017778 385.080889 0 212.366222-199.281778 385.251556-444.017778 385.251556z" fill="#000000" p-id="1976"></path></svg>

+ 8 - 8
mallApp/src/components/home/bannerSwiper.vue

@@ -21,7 +21,7 @@
         <image class="home-banner-img" :src="item.imgUrl" mode="aspectFill" @click="onBannerTap(index)" />
       </swiper-item>
     </swiper>
-    <view v-if="showService" class="service-float" @click="goChat">🎧</view>
+    <view v-if="showService" class="service-float" @click="goChat"><zui-svg-icon icon="general-customerService" :width="24" :height="24" style="margin-top: 2upx"></zui-svg-icon></view>
   </view>
 </template>
 
@@ -101,13 +101,13 @@ export default {
 }
 .service-float {
   position: absolute;
-  right: 20upx;
-  bottom: 20upx;
-  width: 76upx;
-  height: 76upx;
-  line-height: 76upx;
-  text-align: center;
-  font-size: 34upx;
+  right: 10upx;
+  bottom: 10upx;
+  width: 70upx;
+  height: 70upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
   border-radius: 50%;
   background: rgba(255, 255, 255, 0.92);
   box-shadow: 0 4upx 16upx rgba(0, 0, 0, 0.15);

+ 16 - 9
mallApp/src/components/home/topNav.vue

@@ -8,14 +8,14 @@
   <view v-if="data && data.enabled == 1" class="home-top-nav">
     <view class="search-row" @click="goSearch">
       <view class="search-input">
-        <text class="search-icon">🔍</text>
+        <view class="search-icon"><zui-svg-icon icon="general-search" :width="24" :height="24" color="#999"></zui-svg-icon></view>
         <text class="search-ph">{{ data.placeholder || '搜索鲜花、花束、绿植等' }}</text>
         <view
           class="search-btn"
           :style="{ background: data.searchBtnColor || '#FF6B88', color: data.searchFontColor || '#FFFFFF' }"
         >搜索</view>
       </view>
-      <view class="message-icon" @click.stop="goMessageCenter">💬</view>
+      <view class="message-icon" @click.stop="goMessageCenter"><zui-svg-icon icon="general-message" :width="24" :height="24" color="#999"></zui-svg-icon></view>
     </view>
   </view>
 </template>
@@ -42,9 +42,11 @@ export default {
     goSearch() {
       this.pageTo({ url: `/pages/home/category?account=${this.account}&hdId=${this.hdId}` })
     },
-    /** 消息中心入口:功能暂未开放,点击提示用户 */
+    /**
+     * 消息中心入口:跳转 消息列表(pages/message/index)
+     */
     goMessageCenter() {
-      uni.showToast({ title: '功能开放中', icon: 'none' })
+      uni.navigateTo({ url: '/pages/message/index' })
     }
   }
 }
@@ -69,8 +71,13 @@ export default {
   align-items: center;
 }
 .search-icon {
-  font-size: 26upx;
-  margin-right: 8upx;
+  flex-shrink: 0;
+  width: 48upx;
+  height: 48upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  line-height: 1;
 }
 .search-ph {
   flex: 1;
@@ -92,9 +99,9 @@ export default {
 }
 .message-icon {
   flex-shrink: 0;
-  width: 68upx;
-  height: 68upx;
-  line-height: 68upx;
+  width: 48upx;
+  height: 48upx;
+  line-height: 50upx;
   text-align: center;
   font-size: 32upx;
   margin-left: 12upx;

+ 18 - 3
mallApp/src/pages.json

@@ -5,7 +5,8 @@
             "^sprite-icon$": "@/components/sprite-icon/index.vue"
         }
     },
-    "pages": [{ "path": "pages/home/recent", "style": { "navigationBarTitleText": "我的花店","enablePullDownRefresh":true} },
+    "pages": [
+        { "path": "pages/home/recent", "style": { "navigationBarTitleText": "我的花店","enablePullDownRefresh":true} },
         { "path": "pages/home/index", "style": { "navigationStyle": "custom", "navigationBarTitleText": "首页" } },
         { "path": "pages/home/category", "style": { "navigationBarTitleText": "花束" } },
         { "path": "pages/home/shop-category", "style": { "navigationBarTitleText": "分类" } },
@@ -22,9 +23,11 @@
         { "path": "common/redirect", "style": { "navigationBarTitleText": "加载中..." } },
         { "path": "common/webview", "style": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" } },
         { "path": "pages/class/index", "style": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" } },
-        { "path": "pages/class/paySuccess", "style": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" } }
+        { "path": "pages/class/paySuccess", "style": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" } },
+        { "path": "pages/home/message", "style": { "navigationBarTitleText": "消息", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "enablePullDownRefresh": true } }
     ],
-    "subPackages": [        {
+    "subPackages": [
+        {
             "root": "pages/user",
             "pages": [
                 { "path": "register", "style": { "navigationBarTitleText": "注册" } },
@@ -157,6 +160,12 @@
                 { "path": "list", "style": { "navigationBarTitleText": "充值记录", "enablePullDownRefresh": false } },
                 { "path": "detail", "style": { "navigationBarTitleText": "充值详情" } }
             ]
+        },
+        {
+            "root": "pages/message",
+            "pages": [
+                { "path": "index", "style": { "navigationBarTitleText": "消息列表", "enablePullDownRefresh": false } }
+            ]
         }
     ],
     "globalStyle": {
@@ -182,6 +191,12 @@
                 "selectedIconPath": "static/tabbar/order-active.png",
                 "text": "订单"
             },
+            {
+                "pagePath": "pages/home/message",
+                "iconPath": "static/tabbar/order.png",
+                "selectedIconPath": "static/tabbar/order-active.png",
+                "text": "消息"
+            },
             {
                 "pagePath": "pages/home/user",
                 "iconPath": "static/tabbar/mine.png",

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

@@ -348,35 +348,6 @@ export default {
     },
     // 初始化聊天页面数据
     initializeChatPage(params) {
-      // 校验数据 --- 旧版本(做对比,没有对比就没有伤害)
-      // if (isNaN(params.hdId) || params.hdId <= 0) {
-      //   this.$msg("hdId数据异常");
-      //   return;
-      // }
-      // if (isNaN(params.account) || params.account <= 0) {
-      //   this.$msg("account数据异常");
-      //   return;
-      // }
-      // if (isNaN(params.shopId) || params.shopId <= 0) {
-      //   this.$msg("shopId数据异常");
-      //   return;
-      // }
-      // if (isNaN(params.goodsId) || params.goodsId <= 0) {
-      //   this.$msg("goodsId数据异常");
-      //   return;
-      // }
-      // if (isNaN(params.categoryId) || params.categoryId <= 0) {
-      //   this.$msg("categoryId数据异常");
-      //   return;
-      // }
-      // if (isNaN(params.name) || params.name == '') {
-      //   this.$msg("name数据异常");
-      //   return;
-      // }
-      // if (params.avatar == '') {
-      //   this.$msg("avatar数据异常");
-      //   return;
-      // }
       // 参数校验
       const rules = [
         { key: 'hdId', type: 'positiveNumber' },
@@ -410,7 +381,6 @@ export default {
       this.hdId = params.hdId || 0;
       this.account = params.account || 0;
       if (params.shopId || params.goodsId) {
-        // console.log("---------- params: ", params);
         this.shopInfo = {
           ...params
         };
@@ -583,7 +553,7 @@ export default {
         this.$msg("客户数据异常,请稍后再试");
         return;
       }
-      if (isNaN(parseInt(this.shopInfo.shopId))) {
+      if (isNaN(parseInt(this.shopInfo.shopId || this.shopInfo.account))) { // 门店id或账号(account 就是 shopId)
         this.$msg("门店数据异常,请稍后再试");
         return;
       }
@@ -592,7 +562,7 @@ export default {
         userId: this.loginInfo.id,
         customId: this.customId,
         //staffId: 0,
-        shopId: this.shopInfo.shopId,
+        shopId: this.shopInfo.shopId || this.shopInfo.account, // 门店id或账号(account 就是 shopId)
         name: this.loginInfo.name,
         avatar: this.loginInfo.avatar,
         chatPerson: this.shopInfo.name,

+ 295 - 0
mallApp/src/pages/home/message.vue

@@ -0,0 +1,295 @@
+<!--
+  商城「消息」Tab 页:聊天会话列表
+  谁用:底部 tabBar「消息」入口
+  解决:展示用户近期聊天门店列表,点击后进入 chatPage 会话详情
+-->
+<template>
+  <view class="app-main app-content">
+    <view class="list-wrap">
+      <block v-if="$util.isEmpty(list.data) === false">
+        <block v-for="(item, index) in list.data" :key="index">
+          <tui-list-cell :arrow="true" @click="handleItemClick(item)">
+            <view class="user-card">
+              <view class="user-avatar">
+                <image
+                  :src="item.avatar || getDefaultImage()"
+                  class="avatar-img"
+                  mode="aspectFill"
+                />
+              </view>
+              <view class="user-info">
+                <view class="user-main">
+                  <!-- 首店用商户名/手机号兜底,避免显示无意义的「首店」 -->
+                  <view class="user-name">{{
+                    item.shopName != "首店"
+                      ? item.shopName
+                      : item.merchantName || item.mobile
+                  }}</view>
+                </view>
+                <view class="user-meta">
+                  <view class="visit-time">
+                    {{ formatChatTime(item.chatTime) }}
+                  </view>
+                </view>
+              </view>
+              <view v-if="item.unReadNum > 0" class="unread-badge">
+                {{ item.unReadNum > 9 ? '9+' : item.unReadNum }}
+              </view>
+            </view>
+          </tui-list-cell>
+        </block>
+      </block>
+      <block v-else>
+        <app-wrapper-empty :is-empty="true" />
+      </block>
+    </view>
+  </view>
+</template>
+<script>
+/**
+ * 消息 Tab:拉取最近聊天门店列表,并通过 eventChannel 打开聊天详情页
+ */
+import TuiListCell from "@/components/plugin/list-cell";
+import AppWrapperEmpty from "@/components/app-wrapper-empty";
+import { list } from "@/mixins";
+import { getLatestUsers } from "@/api/chat";
+import { mapGetters } from "vuex";
+export default {
+  name: "MessageIndex",
+  components: {
+    TuiListCell,
+    AppWrapperEmpty
+  },
+  mixins: [list],
+  data() {
+    // 预计算常用日期值,减少 formatChatTime 中重复 new Date
+    const now = new Date();
+    const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
+    const formatDateToString = (date) => {
+      const y = date.getFullYear();
+      const m = String(date.getMonth() + 1).padStart(2, '0');
+      const d = String(date.getDate()).padStart(2, '0');
+      return `${y}-${m}-${d}`;
+    };
+
+    return {
+      shopAvatar: '',
+      currentYear: now.getFullYear(),
+      todayStr: formatDateToString(now),
+      yesterdayStr: formatDateToString(yesterday),
+    };
+  },
+  created() {
+    // 进入页面时刷新日期基准,避免跨天后「今天/昨天」判断错误
+    this.updateDateStrings();
+  },
+  onPullDownRefresh() {
+    this.resetList();
+    this.getMyShopList().then(() => {
+      uni.stopPullDownRefresh();
+    });
+  },
+  onReachBottom() {
+    if (!this.list.finished) {
+      this.getMyShopList();
+    }
+  },
+  computed: {
+    ...mapGetters(["getLoginInfo"]),
+  },
+  onLoad() {
+    // 当前登录用户头像:用于进入聊天页时作为自己的头像
+    this.shopAvatar = this.getLoginInfo.avatar != ''
+      ? this.getLoginInfo.avatar
+      : this.$constant.imgUrl + '/hhb_small.png?x-oss-process=image/resize,m_fill,h_130,w_130';
+  },
+  onShow() {
+    // Tab 页保活,每次切回消息 Tab 都刷新会话列表与未读数
+    this.getMyShopList();
+  },
+  methods: {
+    /**
+     * 点击会话项:本地清未读,经 eventChannel 打开 chatPage
+     * @param {Object} item 列表项,含 customId/id/shopName/unReadNum 等
+     */
+    handleItemClick(item) {
+      if (item.unReadNum > 0) {
+        item.unReadNum = 0;
+      }
+
+      const params = {
+        customId: item.customId,
+        shopId: item.id,
+        name: this.getLoginInfo.name,
+        avatar: this.getLoginInfo.avatar,
+        chatPerson: item.shopName != "首店" ? item.shopName : item.merchantName || item.mobile,
+      };
+      uni.navigateTo({
+        url: '/pages/chat/chatPage',
+        success: function(res) {
+          res.eventChannel.emit('acceptDataFromOpenerPage', params)
+        }
+      });
+    },
+    init() {
+    },
+    /**
+     * 拉取最近聊天门店列表
+     * @returns {Promise} 接口 Promise,供下拉刷新 await
+     */
+    getMyShopList() {
+      return getLatestUsers({}).then(res => {
+        if (res.code == 1) {
+          this.list.data = res.data;
+          this.list.finished = true;
+        }
+      }).finally(() => {
+        this.list.loading = false;
+      });
+    },
+    /** 默认头像:接口未返回 avatar 时的占位图 */
+    getDefaultImage() {
+      return this.$constant.imgUrl + '/retail/default-img.png';
+    },
+    /**
+     * 会话时间展示规则:今天仅时分;昨天加「昨天」前缀;今年 MM-DD HH:mm;跨年完整串
+     * @param {string} chatTime 后端返回的 "YYYY-MM-DD HH:mm" 字符串
+     * @returns {string} 格式化后的展示文案
+     */
+    formatChatTime(chatTime) {
+      if (!chatTime) return '';
+
+      try {
+        const spaceIndex = chatTime.indexOf(' ');
+        const dateStr = chatTime.substring(0, spaceIndex);
+        const timeStr = chatTime.substring(spaceIndex + 1);
+        const year = parseInt(dateStr.substring(0, 4));
+        const chatDateStr = dateStr;
+
+        if (chatDateStr === this.todayStr) {
+          return timeStr;
+        } else if (chatDateStr === this.yesterdayStr) {
+          return `昨天 ${timeStr}`;
+        } else if (year === this.currentYear) {
+          return chatTime.substring(5, 16);
+        } else {
+          return chatTime;
+        }
+      } catch (error) {
+        console.error('时间格式化失败:', error);
+        return chatTime;
+      }
+    },
+    /** 刷新 today/yesterday/currentYear,供跨日场景使用 */
+    updateDateStrings() {
+      const now = new Date();
+      const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
+      const formatDateToString = (date) => {
+        const y = date.getFullYear();
+        const m = String(date.getMonth() + 1).padStart(2, '0');
+        const d = String(date.getDate()).padStart(2, '0');
+        return `${y}-${m}-${d}`;
+      };
+
+      this.todayStr = formatDateToString(now);
+      this.yesterdayStr = formatDateToString(yesterday);
+      this.currentYear = now.getFullYear();
+    },
+  }
+};
+</script>
+<style lang="scss" scoped>
+.app-content {
+  padding-bottom: 20upx;
+}
+.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;
+  }
+
+  .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);
+  }
+
+  .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;
+  }
+}
+
+.unread-badge {
+  position: absolute;
+  top: 50%;
+  right: 60upx;
+  transform: translateY(-50%);
+  background-color: #ff4d4f;
+  color: #ffffff;
+  font-size: 28upx;
+  min-width: 42upx;
+  height: 42upx;
+  border-radius: 21upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 10upx;
+  box-sizing: border-box;
+}
+
+::v-deep .tui-list-cell.tui-cell-arrow:before {
+  z-index: 10;
+}
+</style>

+ 295 - 0
mallApp/src/pages/message/index.vue

@@ -0,0 +1,295 @@
+<!--
+  商城「消息」Tab 页:聊天会话列表
+  谁用:底部 tabBar「消息」入口
+  解决:展示用户近期聊天门店列表,点击后进入 chatPage 会话详情
+-->
+<template>
+  <view class="app-main app-content">
+    <view class="list-wrap">
+      <block v-if="$util.isEmpty(list.data) === false">
+        <block v-for="(item, index) in list.data" :key="index">
+          <tui-list-cell :arrow="true" @click="handleItemClick(item)">
+            <view class="user-card">
+              <view class="user-avatar">
+                <image
+                  :src="item.avatar || getDefaultImage()"
+                  class="avatar-img"
+                  mode="aspectFill"
+                />
+              </view>
+              <view class="user-info">
+                <view class="user-main">
+                  <!-- 首店用商户名/手机号兜底,避免显示无意义的「首店」 -->
+                  <view class="user-name">{{
+                    item.shopName != "首店"
+                      ? item.shopName
+                      : item.merchantName || item.mobile
+                  }}</view>
+                </view>
+                <view class="user-meta">
+                  <view class="visit-time">
+                    {{ formatChatTime(item.chatTime) }}
+                  </view>
+                </view>
+              </view>
+              <view v-if="item.unReadNum > 0" class="unread-badge">
+                {{ item.unReadNum > 9 ? '9+' : item.unReadNum }}
+              </view>
+            </view>
+          </tui-list-cell>
+        </block>
+      </block>
+      <block v-else>
+        <app-wrapper-empty :is-empty="true" />
+      </block>
+    </view>
+  </view>
+</template>
+<script>
+/**
+ * 消息 Tab:拉取最近聊天门店列表,并通过 eventChannel 打开聊天详情页
+ */
+import TuiListCell from "@/components/plugin/list-cell";
+import AppWrapperEmpty from "@/components/app-wrapper-empty";
+import { list } from "@/mixins";
+import { getLatestUsers } from "@/api/chat";
+import { mapGetters } from "vuex";
+export default {
+  name: "MessageIndex",
+  components: {
+    TuiListCell,
+    AppWrapperEmpty
+  },
+  mixins: [list],
+  data() {
+    // 预计算常用日期值,减少 formatChatTime 中重复 new Date
+    const now = new Date();
+    const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
+    const formatDateToString = (date) => {
+      const y = date.getFullYear();
+      const m = String(date.getMonth() + 1).padStart(2, '0');
+      const d = String(date.getDate()).padStart(2, '0');
+      return `${y}-${m}-${d}`;
+    };
+
+    return {
+      shopAvatar: '',
+      currentYear: now.getFullYear(),
+      todayStr: formatDateToString(now),
+      yesterdayStr: formatDateToString(yesterday),
+    };
+  },
+  created() {
+    // 进入页面时刷新日期基准,避免跨天后「今天/昨天」判断错误
+    this.updateDateStrings();
+  },
+  onPullDownRefresh() {
+    this.resetList();
+    this.getMyShopList().then(() => {
+      uni.stopPullDownRefresh();
+    });
+  },
+  onReachBottom() {
+    if (!this.list.finished) {
+      this.getMyShopList();
+    }
+  },
+  computed: {
+    ...mapGetters(["getLoginInfo"]),
+  },
+  onLoad() {
+    // 当前登录用户头像:用于进入聊天页时作为自己的头像
+    this.shopAvatar = this.getLoginInfo.avatar != ''
+      ? this.getLoginInfo.avatar
+      : this.$constant.imgUrl + '/hhb_small.png?x-oss-process=image/resize,m_fill,h_130,w_130';
+  },
+  onShow() {
+    // Tab 页保活,每次切回消息 Tab 都刷新会话列表与未读数
+    this.getMyShopList();
+  },
+  methods: {
+    /**
+     * 点击会话项:本地清未读,经 eventChannel 打开 chatPage
+     * @param {Object} item 列表项,含 customId/id/shopName/unReadNum 等
+     */
+    handleItemClick(item) {
+      if (item.unReadNum > 0) {
+        item.unReadNum = 0;
+      }
+
+      const params = {
+        customId: item.customId,
+        shopId: item.id,
+        name: this.getLoginInfo.name,
+        avatar: this.getLoginInfo.avatar,
+        chatPerson: item.shopName != "首店" ? item.shopName : item.merchantName || item.mobile,
+      };
+      uni.navigateTo({
+        url: '/pages/chat/chatPage',
+        success: function(res) {
+          res.eventChannel.emit('acceptDataFromOpenerPage', params)
+        }
+      });
+    },
+    init() {
+    },
+    /**
+     * 拉取最近聊天门店列表
+     * @returns {Promise} 接口 Promise,供下拉刷新 await
+     */
+    getMyShopList() {
+      return getLatestUsers({}).then(res => {
+        if (res.code == 1) {
+          this.list.data = res.data;
+          this.list.finished = true;
+        }
+      }).finally(() => {
+        this.list.loading = false;
+      });
+    },
+    /** 默认头像:接口未返回 avatar 时的占位图 */
+    getDefaultImage() {
+      return this.$constant.imgUrl + '/retail/default-img.png';
+    },
+    /**
+     * 会话时间展示规则:今天仅时分;昨天加「昨天」前缀;今年 MM-DD HH:mm;跨年完整串
+     * @param {string} chatTime 后端返回的 "YYYY-MM-DD HH:mm" 字符串
+     * @returns {string} 格式化后的展示文案
+     */
+    formatChatTime(chatTime) {
+      if (!chatTime) return '';
+
+      try {
+        const spaceIndex = chatTime.indexOf(' ');
+        const dateStr = chatTime.substring(0, spaceIndex);
+        const timeStr = chatTime.substring(spaceIndex + 1);
+        const year = parseInt(dateStr.substring(0, 4));
+        const chatDateStr = dateStr;
+
+        if (chatDateStr === this.todayStr) {
+          return timeStr;
+        } else if (chatDateStr === this.yesterdayStr) {
+          return `昨天 ${timeStr}`;
+        } else if (year === this.currentYear) {
+          return chatTime.substring(5, 16);
+        } else {
+          return chatTime;
+        }
+      } catch (error) {
+        console.error('时间格式化失败:', error);
+        return chatTime;
+      }
+    },
+    /** 刷新 today/yesterday/currentYear,供跨日场景使用 */
+    updateDateStrings() {
+      const now = new Date();
+      const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
+      const formatDateToString = (date) => {
+        const y = date.getFullYear();
+        const m = String(date.getMonth() + 1).padStart(2, '0');
+        const d = String(date.getDate()).padStart(2, '0');
+        return `${y}-${m}-${d}`;
+      };
+
+      this.todayStr = formatDateToString(now);
+      this.yesterdayStr = formatDateToString(yesterday);
+      this.currentYear = now.getFullYear();
+    },
+  }
+};
+</script>
+<style lang="scss" scoped>
+.app-content {
+  padding-bottom: 20upx;
+}
+.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;
+  }
+
+  .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);
+  }
+
+  .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;
+  }
+}
+
+.unread-badge {
+  position: absolute;
+  top: 50%;
+  right: 60upx;
+  transform: translateY(-50%);
+  background-color: #ff4d4f;
+  color: #ffffff;
+  font-size: 28upx;
+  min-width: 42upx;
+  height: 42upx;
+  border-radius: 21upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 10upx;
+  box-sizing: border-box;
+}
+
+::v-deep .tui-list-cell.tui-cell-arrow:before {
+  z-index: 10;
+}
+</style>

+ 13 - 4
mallApp/src/static/svg-icons-lib.js

@@ -6,7 +6,7 @@
  *
  * !!! DO NOT MODIFY MANUALLY !!!
  *
- * @datetime 2026/8/1 20:10:20
+ * @datetime 2026/8/3 16:49:07
  *
  */
 
@@ -22,13 +22,21 @@ const collections = {
         "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200.195\" height=\"200\" class=\"icon\" viewBox=\"0 0 1025 1024\"><path fill=\"#22ac38\" d=\"m585.24 512.064 425.152-416.256a49.41 49.41 0 0 0 0-70.976 52.096 52.096 0 0 0-72.512 0l-425.152 416.32L87.576 24.832a52.096 52.096 0 0 0-72.512 0 49.41 49.41 0 0 0 0 70.976l425.152 416.32-425.152 416.32a49.41 49.41 0 0 0 0 70.976 52.61 52.61 0 0 0 72.512 0l425.152-416.32 425.152 416.32a52.61 52.61 0 0 0 72.512 0 49.41 49.41 0 0 0 0-70.976z\"/></svg>",
         1
       ],
+      "general-customerservice": [
+        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" class=\"icon\" viewBox=\"0 0 1024 1024\"><path fill=\"#515151\" d=\"M139.6 372.4v232.7H46.5V372.4zm0-46.6H46.5c-25.6 0-46.5 21-46.5 46.6v232.7c0 25.6 20.9 46.5 46.5 46.5h93.1c25.6 0 46.5-20.9 46.5-46.5V372.4c.1-25.6-20.9-46.6-46.5-46.6m837.9 46.6v232.7h-93.1V372.4zm0-46.6h-93.1c-25.6 0-46.5 20.9-46.5 46.5V605c0 25.6 20.9 46.5 46.5 46.5h93.1c25.6 0 46.5-20.9 46.5-46.5V372.4c0-25.6-20.9-46.6-46.5-46.6M512 791.3c51.3 0 93.1 41.8 93.1 93.1s-41.8 93.1-93.1 93.1-93.1-41.8-93.1-93.1 41.8-93.1 93.1-93.1m0-46.6c-77.1 0-139.6 62.5-139.6 139.6S434.9 1024 512 1024s139.6-62.5 139.6-139.6S589.1 744.7 512 744.7m-93.1-418.9h-93.1v93.1h93.1zm279.3 0h-93.1v93.1h93.1zm-25.9 232.7c-32.3 55.4-91.7 93.1-160.3 93.1s-128-37.7-160.3-93.1h-52.8c35.9 82.1 117.7 139.6 213.1 139.6s177.1-57.5 213.1-139.6z\"/><path fill=\"#515151\" d=\"M117.5 325.8C175.2 163.4 330 46.5 512 46.5s336.8 116.8 394.5 279.3h49.6C896.8 137 720.4 0 512 0S127.2 137 67.9 325.8zm769.2 325.8c-48.2 96.6-132.4 171.8-235.1 208.3v49.6C780.4 869 884.9 774 938.4 651.6z\"/></svg>",
+        2
+      ],
       "general-image": [
         "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" class=\"icon\" viewBox=\"0 0 1024 1024\"><path fill=\"#22ac38\" d=\"M984.615 196.923c0-43.323-35.446-78.77-78.769-78.77H118.154c-43.323 0-78.77 35.447-78.77 78.77v630.154c0 43.323 35.447 78.77 78.77 78.77h787.692c43.323 0 78.77-35.447 78.77-78.77zm-204.8 551.385H208.738c-23.63 0-37.415-25.6-25.6-45.293l173.293-301.292c7.877-13.785 25.6-13.785 33.477 0l104.369 179.2c7.877 11.815 25.6 13.785 33.477 1.97L612.43 460.8c7.877-11.815 25.6-11.815 33.477 0l155.569 248.123c11.815 17.723 0 39.385-21.662 39.385m-51.2-354.462c-43.323 0-78.769-35.446-78.769-78.77s35.446-78.768 78.77-78.768 78.769 35.446 78.769 78.769-35.447 78.77-78.77 78.77\"/></svg>",
         1
       ],
+      "general-message": [
+        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"210.938\" height=\"200\" class=\"icon\" viewBox=\"0 0 1080 1024\"><path fill=\"#000\" d=\"M207.303 458.638a64.284 64.284 0 1 0 128.455 0 64.284 64.284 0 0 0-128.455 0m253.895 0a64.284 64.284 0 1 0 128.455 0 64.284 64.284 0 0 0-128.455 0m253.838 0a64.284 64.284 0 1 0 128.512 0 64.284 64.284 0 0 0-128.512 0m288.655-180.053a458.5 458.5 0 0 0-111.787-145.693C794.396 47.218 665.144.342 527.929.342c-137.273 0-266.525 47.103-363.861 132.55A456.8 456.8 0 0 0 52.28 278.585a412.05 412.05 0 0 0-41.358 180.053 419.27 419.27 0 0 0 57.685 210.66c32.996 56.49 79.644 106.95 135.623 146.944v151.836a36.693 36.693 0 0 0 54.955 31.687l160.654-92.729c35.385 6.713 71.85 10.07 108.146 10.07 137.33 0 266.581-47.161 363.918-132.551a456.8 456.8 0 0 0 111.787-145.75 412.05 412.05 0 0 0 41.358-180.053 410 410 0 0 0-41.358-180.167M527.986 843.833a510 510 0 0 1-106.78-11.264 36.58 36.58 0 0 0-25.942 4.096l-117.76 68.096V797.013a36.98 36.98 0 0 0-16.498-30.663C148.536 692.68 84.025 580.551 84.025 458.581c0-212.366 199.111-385.024 443.96-385.024 244.737 0 444.019 172.658 444.019 385.081 0 212.366-199.282 385.252-444.018 385.252z\"/></svg>",
+        3
+      ],
       "general-notice-horn": [
         "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><path fill=\"#ff4d7d\" d=\"M4 12h4l6-6v20l-6-6H4c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2m18-2c2 2 2 12 0 14l-2-2c1-1 1-9 0-10zm4-2c3 3 3 14 0 17l-2-2c2-2 2-11 0-13z\"/></svg>",
-        2
+        4
       ],
       "general-search": [
         "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"200\" class=\"icon\" viewBox=\"0 0 1024 1024\"><path fill=\"#000\" d=\"M482.133 724.846a252.952 252.952 0 1 1 252.953-252.953 253.32 253.32 0 0 1-252.953 252.953m0-457.143a204.19 204.19 0 1 0 204.19 204.19 204.31 204.31 0 0 0-204.19-203.703z\"/><path fill=\"#000\" d=\"m615.948 660.724 34.487-34.487L787.749 763.55l-34.475 34.487z\"/></svg>",
@@ -39,8 +47,9 @@ const collections = {
     "$_colorPalette": [
       "#fff",
       "#22ac38",
-      "#ff4d7d",
-      "#000"
+      "#515151",
+      "#000",
+      "#ff4d7d"
     ]
   },
 }