Parcourir la source

fix: 门店列表/详情、设备详情、我的页面打磨

【门店列表】
- Page重置修复 + isFirstLoad骨架屏 + 错误重试态
- 卡片stats背景改用$bg-color-page替代#FAFAFA
- Loading spinner活动部分改为$primary-color琥珀色
- Shop编号改为显示地址缩略

【门店详情】
- 移除死代码(ShopStatusColor/DeviceStatusColor/getStatusColor等)
- onLoad替代getCurrentPages获取路由参数
- navigateBack→内联错误重试态
- 加载pulse dots + 错误图标+重试
- #FAFAFA→设计token + 设备列表空态提示
- 间距对齐8rpx基准

【设备详情】
- status颜色修正: online→$success-color(绿), offline→灰, maintenance→$warning-color
- 模态遮罩纯黑 rgba(0,0,0,0.5)→rgba(30,30,30,0.5)

【我的】
- min-height→height+flex column 布局修正
- 移除.icon-lock死CSS(20行)
- 间距4rpx→$spacing-1(8rpx) / 100rpx→96rpx

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline il y a 1 jour
Parent
commit
2b78a2029e

+ 11 - 11
haha-admin-mp/src/pages/device/detail.vue

@@ -336,20 +336,20 @@ onMounted(() => {
       
       &.online {
         background: $success-color-bg;
-        color: $primary-color;
-        .status-dot { background: $primary-color; }
+        color: $success-color;
+        .status-dot { background: $success-color; }
       }
-      
+
       &.offline {
-        background: $accent-color-bg;
-        color: $accent-color;
-        .status-dot { background: $accent-color; }
+        background: $bg-color-secondary;
+        color: $text-color-muted;
+        .status-dot { background: $text-color-placeholder; }
       }
-      
+
       &.maintenance {
-        background: $primary-color-bg;
-        color: $primary-color;
-        .status-dot { background: $primary-color; }
+        background: $warning-color-bg;
+        color: $warning-color;
+        .status-dot { background: $warning-color; }
       }
     }
   }
@@ -771,7 +771,7 @@ onMounted(() => {
   left: 0;
   right: 0;
   bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
+  background: rgba(30, 30, 30, 0.5);
   z-index: 100;
   display: flex;
   align-items: center;

+ 6 - 24
haha-admin-mp/src/pages/my/my.vue

@@ -303,7 +303,9 @@ onMounted(() => {
 $font-stack: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', system-ui, sans-serif;
 
 .page {
-  min-height: 100vh;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
   width: 100vw;
   overflow-x: hidden;
   background: $bg-color-page;
@@ -381,7 +383,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue',
       font-size: $font-size-lg;
       font-weight: 700;
       color: $text-color-primary;
-      margin-bottom: 4rpx;
+      margin-bottom: $spacing-1;
     }
 
     .user-role {
@@ -627,26 +629,6 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue',
     }
   }
 
-  .icon-lock {
-    width: 16rpx;
-    height: 20rpx;
-    background: $primary-color;
-    border-radius: $radius-xs;
-    position: relative;
-
-    &::before {
-      content: '';
-      position: absolute;
-      top: -6rpx;
-      left: 50%;
-      transform: translateX(-50%);
-      width: 10rpx;
-      height: 8rpx;
-      border: 3rpx solid $primary-color;
-      border-bottom: none;
-      border-radius: 8rpx 8rpx 0 0;
-    }
-  }
 }
 
 .menu-content {
@@ -680,8 +662,8 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue',
 
   .logout-btn {
     width: 100%;
-    height: 100rpx;
-    line-height: 100rpx;
+    height: 96rpx;
+    line-height: 96rpx;
     padding: 0;
     background: $error-color-bg;
     border-radius: $radius-md;

+ 138 - 251
haha-admin-mp/src/pages/shop/detail.vue

@@ -2,312 +2,199 @@
   <view class="page">
     <NavBar title="门店详情" :showBack="true" />
 
-    <view class="detail-content" v-if="shop">
-      <view class="status-card">
-        <view class="status-header">
-          <text class="shop-name">{{ shop.name }}</text>
-          <view class="shop-status" :class="getStatusClass(shop.status)">
-            <view class="status-dot"></view>
-            <text>{{ getStatusText(shop.status) }}</text>
+    <!-- 加载 -->
+    <view class="load-tip" v-if="loading">
+      <view class="dot-row"><view class="pulse-dot"></view><view class="pulse-dot"></view><view class="pulse-dot"></view></view>
+    </view>
+
+    <!-- 错误 -->
+    <view class="error-state" v-if="!loading && errorMsg">
+      <view class="error-icon"></view>
+      <text class="error-text">{{ errorMsg }}</text>
+      <view class="retry-btn" @click="onRetry"><text>点击重试</text></view>
+    </view>
+
+    <scroll-view class="detail-scroll" scroll-y v-if="!loading && shop">
+      <view class="detail-content">
+        <view class="status-card">
+          <view class="status-header">
+            <text class="shop-name">{{ shop.name }}</text>
+            <view class="shop-status" :class="getStatusClass(shop.status)">
+              <view class="status-dot"></view>
+              <text>{{ getStatusText(shop.status) }}</text>
+            </view>
           </view>
         </view>
-      </view>
 
-      <view class="info-card">
-        <text class="section-title">基本信息</text>
-        <view class="info-grid">
-          <view class="info-item">
-            <text class="label">门店地址</text>
-            <text class="value">{{ shop.address }}</text>
-          </view>
-          <view class="info-item">
-            <text class="label">联系电话</text>
-            <text class="value">{{ shop.contactPhone }}</text>
-          </view>
-          <view class="info-item">
-            <text class="label">负责人</text>
-            <text class="value">{{ shop.contactName }}</text>
-          </view>
-          <view class="info-item">
-            <text class="label">营业时间</text>
-            <text class="value">{{ shop.businessHours }}</text>
+        <view class="info-card">
+          <text class="section-title">基本信息</text>
+          <view class="info-grid">
+            <view class="info-item" v-for="f in infoFields" :key="f.label">
+              <text class="label">{{ f.label }}</text>
+              <text class="value">{{ shop[f.key] || '-' }}</text>
+            </view>
           </view>
         </view>
-      </view>
 
-      <view class="info-card">
-        <text class="section-title">销售数据</text>
-        <view class="sales-grid">
-          <view class="sales-item">
-            <text class="sales-value">¥{{ formatMoney(shop.todaySales) }}</text>
-            <text class="sales-label">今日销售额</text>
-          </view>
-          <view class="sales-item">
-            <text class="sales-value">{{ shop.todayOrders }}</text>
-            <text class="sales-label">今日订单</text>
-          </view>
-          <view class="sales-item">
-            <text class="sales-value">¥{{ formatMoney(shop.monthSales) }}</text>
-            <text class="sales-label">本月销售额</text>
+        <view class="info-card">
+          <text class="section-title">销售数据</text>
+          <view class="sales-grid">
+            <view class="sales-item">
+              <text class="sales-value">¥{{ formatMoney(shop.todaySales) }}</text>
+              <text class="sales-label">今日销售额</text>
+            </view>
+            <view class="sales-item">
+              <text class="sales-value">{{ shop.todayOrders || 0 }}</text>
+              <text class="sales-label">今日订单</text>
+            </view>
+            <view class="sales-item">
+              <text class="sales-value">¥{{ formatMoney(shop.monthSales) }}</text>
+              <text class="sales-label">本月销售额</text>
+            </view>
           </view>
         </view>
-      </view>
 
-      <view class="info-card" v-if="shop.devices && shop.devices.length > 0">
-        <text class="section-title">门店设备</text>
-        <view class="device-list">
-          <view class="device-item" v-for="device in shop.devices" :key="device.id">
-            <view class="device-info">
-              <text class="device-name">{{ device.name }}</text>
-              <text class="device-no">{{ device.deviceId }}</text>
-            </view>
-            <view class="device-status" :class="getDeviceStatusClass(device.status)">
-              <view class="status-dot"></view>
-              <text>{{ getDeviceStatusText(device.status) }}</text>
+        <view class="info-card" v-if="shop.devices && shop.devices.length > 0">
+          <text class="section-title">门店设备 ({{ shop.devices.length }})</text>
+          <view class="device-list">
+            <view class="device-item" v-for="device in shop.devices" :key="device.id">
+              <view class="device-info">
+                <text class="device-name">{{ device.name }}</text>
+                <text class="device-no">{{ device.deviceId }}</text>
+              </view>
+              <view class="device-status" :class="getDeviceStatusClass(device.status)">
+                <view class="status-dot"></view>
+                <text>{{ getDeviceStatusText(device.status) }}</text>
+              </view>
             </view>
           </view>
         </view>
+
+        <view class="info-card" v-else-if="!loading">
+          <text class="section-title">门店设备</text>
+          <text class="empty-hint">暂无设备</text>
+        </view>
       </view>
-    </view>
+    </scroll-view>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
 import NavBar from '@/components/NavBar.vue';
 import { getShopDetail } from '@/api/shop';
-import { ShopStatusText, ShopStatusColor } from '@/utils/constants';
-import { DeviceStatusText, DeviceStatusColor } from '@/utils/constants';
-import { formatMoney as formatMoneyUtil, showToast, navigateBack } from '@/utils/common';
+import { ShopStatusText, DeviceStatusText } from '@/utils/constants';
+import { formatMoney as formatMoneyUtil } from '@/utils/common';
 
 const shop = ref<any>(null);
+const loading = ref(true);
+const errorMsg = ref('');
+let lastId = '';
+
 const formatMoney = formatMoneyUtil;
 
+const infoFields = [
+  { label: '门店地址', key: 'address' },
+  { label: '联系电话', key: 'contactPhone' },
+  { label: '负责人', key: 'contactName' },
+  { label: '营业时间', key: 'businessHours' }
+];
+
 const getStatusText = (status: number) => ShopStatusText[status] || '未知';
-const getStatusColor = (status: number) => ShopStatusColor[status] || '$text-color-muted';
 const getDeviceStatusText = (status: number) => DeviceStatusText[status] || '未知';
-const getDeviceStatusColor = (status: number) => DeviceStatusColor[status] || '$text-color-muted';
-
-const getStatusClass = (status: number) => {
-  if (status === 1) return 'active';
-  return 'inactive';
-};
 
+const getStatusClass = (status: number) => status === 1 ? 'active' : 'inactive';
 const getDeviceStatusClass = (status: number) => {
   if (status === 1) return 'online';
   if (status === 0) return 'offline';
   return 'maintenance';
 };
 
-const loadDetail = async () => {
-  const pages = getCurrentPages();
-  const currentPage = pages[pages.length - 1];
-  const id = (currentPage as any).options?.id;
-  
-  if (!id) {
-    showToast('门店ID不存在');
-    navigateBack();
-    return;
-  }
-  
+const loadDetail = async (id?: string) => {
+  if (!id) { errorMsg.value = '缺少门店参数'; loading.value = false; return; }
+  lastId = id;
+  loading.value = true;
+  errorMsg.value = '';
   try {
     shop.value = await getShopDetail(id);
-  } catch (error) {
-    showToast('加载门店详情失败');
-    navigateBack();
+  } catch {
+    errorMsg.value = '加载失败,请检查网络后重试';
+    shop.value = null;
+  } finally {
+    loading.value = false;
   }
 };
 
-onMounted(() => {
-  loadDetail();
-});
+const onRetry = () => loadDetail(lastId);
+
+onLoad((options?: Record<string, string>) => loadDetail(options?.id));
 </script>
 
 <style lang="scss" scoped>
-.page {
-  min-height: 100vh;
-  background: $bg-color-page;
+.page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; }
+.detail-scroll { flex: 1; height: 0; }
+.detail-content { padding: $spacing-2 $spacing-3; }
+
+// ====== Loading ======
+.load-tip { display: flex; justify-content: center; padding: 200rpx 0; }
+.dot-row { display: flex; gap: $spacing-1; }
+.pulse-dot { width: 10rpx; height: 10rpx; background: $text-color-placeholder; border-radius: 50%; animation: pulse 1.2s ease-in-out infinite;
+  &:nth-child(2) { animation-delay: 0.2s; } &:nth-child(3) { animation-delay: 0.4s; }
 }
+@keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
 
-.detail-content {
-  padding: $spacing-2 $spacing-3;
+// ====== Error ======
+.error-state { display: flex; flex-direction: column; align-items: center; padding: 200rpx $spacing-4;
+  .error-icon { width: 80rpx; height: 80rpx; border-radius: 50%; background: $error-color-bg; margin-bottom: $spacing-3; position: relative;
+    &::before { content: '!'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 40rpx; font-weight: 700; color: $error-color; line-height: 1; }
+  }
+  .error-text { font-size: $font-size-base; color: $text-color-secondary; margin-bottom: $spacing-4; }
+  .retry-btn { padding: $spacing-2 $spacing-6; background: $primary-color; border-radius: $radius-full; text { font-size: $font-size-base; color: $text-color-primary; font-weight: 500; } }
 }
 
+// ====== Cards ======
 .status-card {
-  background: $bg-color-card;
-  border-radius: $radius-lg;
-  padding: 28rpx $spacing-4;
-  margin-bottom: $spacing-2;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
-
-  .status-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-
-    .shop-name {
-      font-size: $font-size-xl;
-      font-weight: 700;
-      color: $text-color-primary;
-      line-height: 1.3;
-    }
-
-    .shop-status {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      gap: 8rpx;
-      padding: 8rpx 18rpx;
-      font-size: 22rpx;
-      font-weight: 500;
-      border-radius: $radius-full;
-      flex-shrink: 0;
-
-      .status-dot {
-        width: 10rpx;
-        height: 10rpx;
-        border-radius: 50%;
-      }
-
-      &.active {
-        background: $success-color-bg;
-        color: $success-color;
-        .status-dot { background: $success-color; }
-      }
-
-      &.inactive {
-        background: $bg-color-secondary;
-        color: $text-color-muted;
-        .status-dot { background: $text-color-placeholder; }
-      }
+  padding: $spacing-4; margin-bottom: $spacing-2; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+  .status-header { display: flex; justify-content: space-between; align-items: center;
+    .shop-name { font-size: $font-size-lg; font-weight: 700; color: $text-color-primary; }
+    .shop-status { display: flex; align-items: center; gap: 8rpx; padding: 6rpx 16rpx; border-radius: $radius-full; font-size: 22rpx; font-weight: 500; flex-shrink: 0;
+      .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
+      &.active { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
+      &.inactive { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
     }
   }
 }
 
 .info-card {
-  background: $bg-color-card;
-  border-radius: $radius-lg;
-  padding: 24rpx $spacing-4;
-  margin-bottom: $spacing-2;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
-
-  .section-title {
-    display: block;
-    font-size: $font-size-base;
-    font-weight: 600;
-    color: $text-color-primary;
-    margin-bottom: 20rpx;
-  }
-
-  .info-grid {
-    .info-item {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 14rpx 0;
+  padding: $spacing-3 $spacing-4; margin-bottom: $spacing-2; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+  .section-title { display: block; font-size: $font-size-md; font-weight: 600; color: $text-color-primary; margin-bottom: 16rpx; padding-bottom: $spacing-2; border-bottom: 1rpx solid $border-color-light; }
+  .empty-hint { display: block; font-size: $font-size-sm; color: $text-color-muted; padding: $spacing-4 0; text-align: center; }
+}
 
-      .label {
-        font-size: $font-size-base;
-        color: $text-color-muted;
-      }
+.info-grid .info-item { display: flex; justify-content: space-between; align-items: center; padding: $spacing-2 0;
+  .label { font-size: $font-size-base; color: $text-color-muted; }
+  .value { font-size: $font-size-base; color: $text-color-primary; text-align: right; max-width: 60%; }
+}
 
-      .value {
-        font-size: $font-size-base;
-        color: $text-color-primary;
-        text-align: right;
-        max-width: 60%;
-      }
-    }
+.sales-grid { display: flex; padding: 16rpx 0; background: $bg-color-page; border-radius: $radius-sm;
+  .sales-item { flex: 1; display: flex; flex-direction: column; align-items: center;
+    .sales-value { font-size: $font-size-md; font-weight: 700; color: $text-color-primary; margin-bottom: $spacing-1; }
+    .sales-label { font-size: $font-size-xs; color: $text-color-muted; }
   }
+}
 
-  .sales-grid {
-    display: flex;
-    background: #FAFAFA;
-    border-radius: $radius-base;
-    padding: 20rpx 0;
-
-    .sales-item {
-      flex: 1;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      justify-content: center;
-
-      .sales-value {
-        font-size: $font-size-md;
-        font-weight: 700;
-        color: $text-color-primary;
-        margin-bottom: 8rpx;
-      }
-
-      .sales-label {
-        font-size: $font-size-xs;
-        color: $text-color-muted;
-      }
-    }
+.device-list .device-item { display: flex; justify-content: space-between; align-items: center; padding: $spacing-2 0;
+  & + .device-item { border-top: 1rpx solid $border-color-light; }
+  .device-info {
+    .device-name { display: block; font-size: $font-size-base; color: $text-color-primary; font-weight: 500; margin-bottom: 4rpx; }
+    .device-no { font-size: 22rpx; color: $text-color-muted; }
   }
-
-  .device-list {
-    .device-item {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 18rpx 0;
-
-      & + .device-item {
-        border-top: 1rpx solid #FAFAFA;
-      }
-
-      .device-info {
-        .device-name {
-          display: block;
-          font-size: $font-size-base;
-          color: $text-color-primary;
-          font-weight: 500;
-          margin-bottom: 4rpx;
-        }
-
-        .device-no {
-          font-size: 22rpx;
-          color: $text-color-muted;
-        }
-      }
-
-      .device-status {
-        display: flex;
-        align-items: center;
-        gap: 6rpx;
-        padding: 6rpx 14rpx;
-        border-radius: $radius-full;
-        font-size: 20rpx;
-        font-weight: 500;
-        flex-shrink: 0;
-
-        .status-dot {
-          width: 10rpx;
-          height: 10rpx;
-          border-radius: 50%;
-        }
-
-        &.online {
-          background: $success-color-bg;
-          color: $success-color;
-          .status-dot { background: $success-color; }
-        }
-
-        &.offline {
-          background: $bg-color-secondary;
-          color: $text-color-muted;
-          .status-dot { background: $text-color-placeholder; }
-        }
-
-        &.maintenance {
-          background: $warning-color-bg;
-          color: $warning-color;
-          .status-dot { background: $warning-color; }
-        }
-      }
-    }
+  .device-status { display: flex; align-items: center; gap: 6rpx; padding: 6rpx 14rpx; border-radius: $radius-full; font-size: 20rpx; font-weight: 500; flex-shrink: 0;
+    .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
+    &.online { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
+    &.offline { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
+    &.maintenance { background: $warning-color-bg; color: $warning-color; .status-dot { background: $warning-color; } }
   }
 }
 </style>

+ 119 - 239
haha-admin-mp/src/pages/shop/list.vue

@@ -2,22 +2,30 @@
   <view class="page">
     <NavBar title="门店管理" :showBack="true" />
 
-    <scroll-view
-      class="shop-scroll"
-      scroll-y
-      @scrolltolower="loadMore"
-    >
-      <view class="shop-list">
-        <view
-          class="shop-card"
-          v-for="shop in shopList"
-          :key="shop.id"
-          @click="goDetail(shop.id)"
-        >
+    <scroll-view class="shop-scroll" scroll-y @scrolltolower="loadMore">
+      <!-- 骨架屏 -->
+      <view class="skeleton-list" v-if="isFirstLoad">
+        <view class="skeleton-card" v-for="i in 3" :key="i">
+          <view class="sk-row"><view class="sk-box w-48"></view><view class="sk-box w-16"></view></view>
+          <view class="sk-row"><view class="sk-box w-36"></view><view class="sk-box w-24"></view></view>
+          <view class="sk-row sk-stats"><view class="sk-box w-16"></view><view class="sk-box w-20"></view><view class="sk-box w-20"></view></view>
+        </view>
+      </view>
+
+      <!-- 错误状态 -->
+      <view class="error-state" v-if="errorMsg && !isFirstLoad">
+        <view class="error-icon"></view>
+        <text class="error-text">{{ errorMsg }}</text>
+        <view class="retry-btn" @click="onRetry"><text>点击重试</text></view>
+      </view>
+
+      <!-- 门店列表 -->
+      <view class="shop-list" v-if="!isFirstLoad && !errorMsg">
+        <view class="shop-card" v-for="shop in shopList" :key="shop.id" @click="goDetail(shop.id)">
           <view class="card-header">
             <view class="shop-info">
               <text class="shop-name">{{ shop.name }}</text>
-              <text class="shop-no">{{ shop.id }}</text>
+              <text class="shop-no">{{ shop.address ? shop.address.slice(0, 20) : '' }}</text>
             </view>
             <view class="status-badge" :class="getStatusClass(shop.status)">
               <view class="status-dot"></view>
@@ -26,19 +34,15 @@
           </view>
 
           <view class="card-body">
-            <view class="info-row">
-              <text class="info-label">地址</text>
-              <text class="info-text">{{ shop.address }}</text>
-            </view>
-            <view class="info-row">
+            <view class="info-row" v-if="shop.contactName || shop.contactPhone">
               <text class="info-label">联系人</text>
-              <text class="info-text">{{ shop.contactName }}  {{ shop.contactPhone }}</text>
+              <text class="info-text">{{ shop.contactName || '' }} {{ shop.contactPhone || '' }}</text>
             </view>
           </view>
 
           <view class="card-stats">
             <view class="stat-item">
-              <text class="stat-value">{{ shop.deviceCount }}</text>
+              <text class="stat-value">{{ shop.deviceCount || 0 }}</text>
               <text class="stat-label">设备数</text>
             </view>
             <view class="stat-item">
@@ -53,17 +57,18 @@
         </view>
       </view>
 
-      <view class="loading-more" v-if="loading">
+      <!-- 加载更多 -->
+      <view class="loading-more" v-if="isLoadingMore">
         <view class="loading-spinner"></view>
-        <text>加载中...</text>
-      </view>
-
-      <view class="no-more" v-if="!hasMore && shopList.length > 0">
-        <text>没有更多了</text>
+        <text>加载中</text>
       </view>
+      <view class="no-more" v-if="!hasMore && shopList.length > 0 && !isFirstLoad">没有更多了</view>
 
-      <view class="empty-state" v-if="!loading && shopList.length === 0">
-        <view class="empty-icon"></view>
+      <!-- 空状态 -->
+      <view class="empty-state" v-if="!isFirstLoad && !errorMsg && shopList.length === 0">
+        <view class="empty-icon-box">
+          <view class="empty-shop-icon"></view>
+        </view>
         <text class="empty-title">暂无门店</text>
         <text class="empty-desc">门店数据将在此处展示</text>
       </view>
@@ -73,281 +78,156 @@
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { logger } from '@/utils/logger';
 import NavBar from '@/components/NavBar.vue';
 import { getShopList } from '@/api/shop';
-import { ShopStatusText, ShopStatusColor } from '@/utils/constants';
+import { ShopStatusText } from '@/utils/constants';
 import { formatMoney as formatMoneyUtil } from '@/utils/common';
 
 const shopList = ref<any[]>([]);
-const loading = ref(false);
+const isFirstLoad = ref(true);
+const isLoadingMore = ref(false);
 const hasMore = ref(true);
 const page = ref(1);
-const pageSize = 10;
+const errorMsg = ref('');
 
 const formatMoney = formatMoneyUtil;
 
 const getStatusText = (status: number) => ShopStatusText[status] || '未知';
-
-const getStatusClass = (status: number) => {
-  if (status === 1) return 'active';
-  return 'inactive';
-};
+const getStatusClass = (status: number) => status === 1 ? 'active' : 'inactive';
 
 const loadShops = async () => {
-  loading.value = true;
+  errorMsg.value = '';
   try {
-    const res = await getShopList({
-      page: page.value,
-      pageSize
-    });
-    
+    const res = await getShopList({ page: page.value, pageSize: 10 });
+    if (!res) return;
+
     if (page.value === 1) {
-      shopList.value = res.list;
+      shopList.value = res.list || [];
     } else {
-      shopList.value = [...shopList.value, ...res.list];
+      shopList.value = [...shopList.value, ...(res.list || [])];
+    }
+    hasMore.value = shopList.value.length < (res.total || 0);
+  } catch (e: any) {
+    if (page.value === 1) {
+      errorMsg.value = e?.message || '加载失败,请检查网络后重试';
+      shopList.value = [];
     }
-    
-    hasMore.value = shopList.value.length < res.total;
-  } catch (error) {
-    logger.warn('加载门店列表失败', error);
   } finally {
-    loading.value = false;
+    isFirstLoad.value = false;
+    isLoadingMore.value = false;
   }
 };
 
 const loadMore = () => {
-  if (loading.value || !hasMore.value) return;
+  if (isLoadingMore.value || !hasMore.value || isFirstLoad.value) return;
+  isLoadingMore.value = true;
   page.value++;
   loadShops();
 };
 
+const onRetry = () => {
+  page.value = 1;
+  hasMore.value = true;
+  isFirstLoad.value = true;
+  loadShops();
+};
+
 const goDetail = (shopId: string) => {
   uni.navigateTo({ url: `/pages/shop/detail?id=${shopId}` });
 };
 
-onMounted(() => {
-  loadShops();
-});
+onMounted(() => loadShops());
 </script>
 
 <style lang="scss" scoped>
 .page {
-  min-height: 100vh;
+  height: 100vh;
   background: $bg-color-page;
   display: flex;
   flex-direction: column;
 }
+.shop-scroll { flex: 1; height: 0; }
+.shop-list { padding: $spacing-2 $spacing-3; }
 
-.shop-scroll {
-  flex: 1;
-  height: 0;
+// ====== Skeleton ======
+.skeleton-list { padding: $spacing-2 $spacing-3; }
+.skeleton-card {
+  padding: $spacing-3;
+  margin-bottom: $spacing-2;
+  background: $bg-color-card;
+  border-radius: $radius-md;
+  .sk-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: $spacing-2; }
+  .sk-stats { justify-content: space-around; padding-top: $spacing-2; border-top: 1rpx solid $border-color-light; }
+  .sk-box { height: 28rpx; background: $bg-color-page; border-radius: $radius-xs; }
+  .w-16 { width: 16%; } .w-20 { width: 20%; } .w-24 { width: 24%; } .w-36 { width: 36%; } .w-48 { width: 48%; }
 }
 
-.shop-list {
-  padding: $spacing-2 $spacing-3;
+// ====== Error ======
+.error-state {
+  display: flex; flex-direction: column; align-items: center; padding: 160rpx $spacing-4;
+  .error-icon {
+    width: 80rpx; height: 80rpx; border-radius: 50%; background: $error-color-bg; margin-bottom: $spacing-3; position: relative;
+    &::before { content: '!'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 40rpx; font-weight: 700; color: $error-color; line-height: 1; }
+  }
+  .error-text { font-size: $font-size-base; color: $text-color-secondary; margin-bottom: $spacing-4; }
+  .retry-btn { padding: $spacing-2 $spacing-6; background: $primary-color; border-radius: $radius-full; text { font-size: $font-size-base; color: $text-color-primary; font-weight: 500; } }
 }
 
+// ====== Card ======
 .shop-card {
-  background: $bg-color-card;
-  border-radius: $radius-lg;
   margin-bottom: $spacing-2;
-  overflow: hidden;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
-  transition: transform 0.15s;
-
-  &:active {
-    transform: scale(0.985);
-  }
+  padding: $spacing-3;
+  background: $bg-color-card;
+  border-radius: $radius-md;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+  &:active { opacity: 0.7; }
 }
 
 .card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  padding: $spacing-3 $spacing-3 $spacing-2;
-
+  display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12rpx;
   .shop-info {
     flex: 1;
-
-    .shop-name {
-      display: block;
-      font-size: $font-size-lg;
-      font-weight: 600;
-      color: $text-color-primary;
-      margin-bottom: 4rpx;
-      line-height: 1.3;
-    }
-
-    .shop-no {
-      font-size: $font-size-xs;
-      color: $text-color-muted;
-    }
+    .shop-name { display: block; font-size: $font-size-md; font-weight: 600; color: $text-color-primary; margin-bottom: 4rpx; }
+    .shop-no { font-size: 22rpx; color: $text-color-muted; }
   }
-
   .status-badge {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 6rpx;
-    padding: 6rpx 14rpx;
-    border-radius: $radius-full;
-    font-size: 20rpx;
-    font-weight: 500;
-    flex-shrink: 0;
-
-    .status-dot {
-      width: 10rpx;
-      height: 10rpx;
-      border-radius: 50%;
-    }
-
-    &.active {
-      background: $success-color-bg;
-      color: $success-color;
-      .status-dot { background: $success-color; }
-    }
-
-    &.inactive {
-      background: $bg-color-secondary;
-      color: $text-color-muted;
-      .status-dot { background: $text-color-placeholder; }
-    }
+    display: flex; align-items: center; gap: 6rpx; padding: 6rpx 14rpx; border-radius: $radius-full; font-size: 20rpx; font-weight: 500; flex-shrink: 0;
+    .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
+    &.active { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
+    &.inactive { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
   }
 }
 
 .card-body {
-  padding: 0 $spacing-3 $spacing-2;
-
-  .info-row {
-    display: flex;
-    align-items: flex-start;
-    margin-bottom: 10rpx;
-
-    &:last-child {
-      margin-bottom: 0;
-    }
-
-    .info-label {
-      font-size: 24rpx;
-      color: $text-color-muted;
-      flex-shrink: 0;
-      margin-right: $spacing-1;
-    }
-
-    .info-text {
-      font-size: 26rpx;
-      color: $text-color-secondary;
-      line-height: 1.5;
-    }
+  margin-bottom: 4rpx;
+  .info-row { display: flex; align-items: flex-start; margin-bottom: 6rpx;
+    .info-label { font-size: 24rpx; color: $text-color-muted; flex-shrink: 0; margin-right: $spacing-1; }
+    .info-text { font-size: 24rpx; color: $text-color-secondary; }
   }
 }
 
 .card-stats {
-  display: flex;
-  align-items: center;
-  background: #FAFAFA;
-  padding: 16rpx 8rpx;
-
-  .stat-item {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-
-    .stat-value {
-      font-size: 26rpx;
-      font-weight: 600;
-      color: $text-color-primary;
-      margin-bottom: 4rpx;
-    }
-
-    .stat-label {
-      font-size: 20rpx;
-      color: $text-color-muted;
-    }
-  }
-}
-
-.loading-more {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 12rpx;
-  padding: 40rpx;
-  color: $text-color-muted;
-  font-size: 24rpx;
-
-  .loading-spinner {
-    width: 32rpx;
-    height: 32rpx;
-    border: 3rpx solid $border-color;
-    border-top-color: $text-color-muted;
-    border-radius: 50%;
-    animation: spin 0.8s linear infinite;
+  display: flex; align-items: center; padding: 16rpx 8rpx; margin-top: 8rpx; background: $bg-color-page; border-radius: $radius-sm;
+  .stat-item { flex: 1; display: flex; flex-direction: column; align-items: center;
+    .stat-value { font-size: 26rpx; font-weight: 600; color: $text-color-primary; margin-bottom: 4rpx; }
+    .stat-label { font-size: 20rpx; color: $text-color-muted; }
   }
 }
 
-@keyframes spin {
-  to { transform: rotate(360deg); }
+// ====== Loading / No more ======
+.loading-more { display: flex; align-items: center; justify-content: center; gap: $spacing-1; padding: $spacing-4; color: $text-color-muted; font-size: $font-size-sm;
+  .loading-spinner { width: 32rpx; height: 32rpx; border: 3rpx solid $border-color; border-top-color: $primary-color; border-radius: 50%; animation: spin 0.8s linear infinite; }
 }
-
-.no-more {
-  text-align: center;
-  padding: 40rpx;
-  font-size: 24rpx;
-  color: $text-color-placeholder;
-}
-
-.empty-state {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 120rpx 0 80rpx;
-
-  .empty-icon {
-    width: 100rpx;
-    height: 100rpx;
-    border-radius: 50%;
-    background: $bg-color-secondary;
-    margin-bottom: 24rpx;
-    position: relative;
-
-    &::before,
-    &::after {
-      content: '';
-      position: absolute;
-      left: 50%;
-      transform: translateX(-50%);
-      background: $text-color-placeholder;
-    }
-
-    &::before {
-      top: 32rpx;
-      width: 28rpx;
-      height: 28rpx;
-      border-radius: 50%;
-    }
-
-    &::after {
-      top: 68rpx;
-      width: 44rpx;
-      height: 6rpx;
-      border-radius: 3rpx;
-    }
-  }
-
-  .empty-title {
-    font-size: $font-size-base;
-    font-weight: 500;
-    color: $text-color-secondary;
-    margin-bottom: 8rpx;
-  }
-
-  .empty-desc {
-    font-size: $font-size-sm;
-    color: $text-color-muted;
+@keyframes spin { to { transform: rotate(360deg); } }
+.no-more { text-align: center; padding: $spacing-4; font-size: $font-size-sm; color: $text-color-placeholder; }
+
+// ====== Empty ======
+.empty-state { display: flex; flex-direction: column; align-items: center; padding: 160rpx $spacing-4;
+  .empty-icon-box { width: 120rpx; height: 120rpx; border-radius: $radius-xl; background: $bg-color-secondary; display: flex; align-items: center; justify-content: center; margin-bottom: $spacing-3; }
+  .empty-shop-icon { width: 44rpx; height: 44rpx; border: 2.5rpx solid $text-color-placeholder; border-radius: 8rpx; position: relative;
+    &::after { content: ''; position: absolute; bottom: 6rpx; left: 50%; transform: translateX(-50%); width: 20rpx; height: 6rpx; background: $text-color-placeholder; border-radius: 3rpx 3rpx 0 0; }
   }
+  .empty-title { font-size: $font-size-base; font-weight: 500; color: $text-color-secondary; margin-bottom: $spacing-1; }
+  .empty-desc { font-size: $font-size-sm; color: $text-color-muted; }
 }
 </style>