detail.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <view class="page">
  3. <NavBar title="门店详情" :showBack="true" />
  4. <!-- 加载 -->
  5. <view class="load-tip" v-if="loading">
  6. <view class="dot-row"><view class="pulse-dot"></view><view class="pulse-dot"></view><view class="pulse-dot"></view></view>
  7. </view>
  8. <!-- 错误 -->
  9. <view class="error-state" v-if="!loading && errorMsg">
  10. <view class="error-icon"></view>
  11. <text class="error-text">{{ errorMsg }}</text>
  12. <view class="retry-btn" @click="onRetry"><text>点击重试</text></view>
  13. </view>
  14. <scroll-view class="detail-scroll" scroll-y v-if="!loading && shop">
  15. <view class="detail-content">
  16. <view class="status-card">
  17. <view class="status-header">
  18. <text class="shop-name">{{ shop.name }}</text>
  19. <view class="shop-status" :class="getStatusClass(shop.status)">
  20. <view class="status-dot"></view>
  21. <text>{{ getStatusText(shop.status) }}</text>
  22. </view>
  23. </view>
  24. </view>
  25. <view class="info-card">
  26. <text class="section-title">基本信息</text>
  27. <view class="info-grid">
  28. <view class="info-item" v-for="f in infoFields" :key="f.label">
  29. <text class="label">{{ f.label }}</text>
  30. <text class="value">{{ shop[f.key] || '-' }}</text>
  31. </view>
  32. </view>
  33. </view>
  34. <view class="info-card">
  35. <text class="section-title">销售数据</text>
  36. <view class="sales-grid">
  37. <view class="sales-item">
  38. <text class="sales-value">¥{{ formatMoney(shop.todaySales) }}</text>
  39. <text class="sales-label">今日销售额</text>
  40. </view>
  41. <view class="sales-item">
  42. <text class="sales-value">{{ shop.todayOrders || 0 }}</text>
  43. <text class="sales-label">今日订单</text>
  44. </view>
  45. <view class="sales-item">
  46. <text class="sales-value">¥{{ formatMoney(shop.monthSales) }}</text>
  47. <text class="sales-label">本月销售额</text>
  48. </view>
  49. </view>
  50. </view>
  51. <view class="info-card" v-if="shop.devices && shop.devices.length > 0">
  52. <text class="section-title">门店设备 ({{ shop.devices.length }})</text>
  53. <view class="device-list">
  54. <view class="device-item" v-for="device in shop.devices" :key="device.id">
  55. <view class="device-info">
  56. <text class="device-name">{{ device.name }}</text>
  57. <text class="device-no">{{ device.deviceId }}</text>
  58. </view>
  59. <view class="device-status" :class="getDeviceStatusClass(device.status)">
  60. <view class="status-dot"></view>
  61. <text>{{ getDeviceStatusText(device.status) }}</text>
  62. </view>
  63. </view>
  64. </view>
  65. </view>
  66. <view class="info-card" v-else-if="!loading">
  67. <text class="section-title">门店设备</text>
  68. <text class="empty-hint">暂无设备</text>
  69. </view>
  70. </view>
  71. </scroll-view>
  72. </view>
  73. </template>
  74. <script setup lang="ts">
  75. import { ref } from 'vue';
  76. import { onLoad } from '@dcloudio/uni-app';
  77. import NavBar from '@/components/NavBar.vue';
  78. import { getShopDetail } from '@/api/shop';
  79. import { ShopStatusText, DeviceStatusText } from '@/utils/constants';
  80. import { formatMoney as formatMoneyUtil } from '@/utils/common';
  81. const shop = ref<any>(null);
  82. const loading = ref(true);
  83. const errorMsg = ref('');
  84. let lastId = '';
  85. const formatMoney = formatMoneyUtil;
  86. const infoFields = [
  87. { label: '门店地址', key: 'address' },
  88. { label: '联系电话', key: 'contactPhone' },
  89. { label: '负责人', key: 'contactName' },
  90. { label: '营业时间', key: 'businessHours' }
  91. ];
  92. const getStatusText = (status: number) => ShopStatusText[status] || '未知';
  93. const getDeviceStatusText = (status: number) => DeviceStatusText[status] || '未知';
  94. const getStatusClass = (status: number) => status === 1 ? 'active' : 'inactive';
  95. const getDeviceStatusClass = (status: number) => {
  96. if (status === 1) return 'online';
  97. if (status === 0) return 'offline';
  98. return 'maintenance';
  99. };
  100. const loadDetail = async (id?: string) => {
  101. if (!id) { errorMsg.value = '缺少门店参数'; loading.value = false; return; }
  102. lastId = id;
  103. loading.value = true;
  104. errorMsg.value = '';
  105. try {
  106. shop.value = await getShopDetail(id);
  107. } catch {
  108. errorMsg.value = '加载失败,请检查网络后重试';
  109. shop.value = null;
  110. } finally {
  111. loading.value = false;
  112. }
  113. };
  114. const onRetry = () => loadDetail(lastId);
  115. onLoad((options?: Record<string, string>) => loadDetail(options?.id));
  116. </script>
  117. <style lang="scss" scoped>
  118. .page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; }
  119. .detail-scroll { flex: 1; height: 0; }
  120. .detail-content { padding: $spacing-2 $spacing-3; }
  121. // ====== Loading ======
  122. .load-tip { display: flex; justify-content: center; padding: 200rpx 0; }
  123. .dot-row { display: flex; gap: $spacing-1; }
  124. .pulse-dot { width: 10rpx; height: 10rpx; background: $text-color-placeholder; border-radius: 50%; animation: pulse 1.2s ease-in-out infinite;
  125. &:nth-child(2) { animation-delay: 0.2s; } &:nth-child(3) { animation-delay: 0.4s; }
  126. }
  127. @keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
  128. // ====== Error ======
  129. .error-state { display: flex; flex-direction: column; align-items: center; padding: 200rpx $spacing-4;
  130. .error-icon { width: 80rpx; height: 80rpx; border-radius: 50%; background: $error-color-bg; margin-bottom: $spacing-3; position: relative;
  131. &::before { content: '!'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 40rpx; font-weight: 700; color: $error-color; line-height: 1; }
  132. }
  133. .error-text { font-size: $font-size-base; color: $text-color-secondary; margin-bottom: $spacing-4; }
  134. .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; } }
  135. }
  136. // ====== Cards ======
  137. .status-card {
  138. 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);
  139. .status-header { display: flex; justify-content: space-between; align-items: center;
  140. .shop-name { font-size: $font-size-lg; font-weight: 700; color: $text-color-primary; }
  141. .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;
  142. .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
  143. &.active { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
  144. &.inactive { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
  145. }
  146. }
  147. }
  148. .info-card {
  149. 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);
  150. .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; }
  151. .empty-hint { display: block; font-size: $font-size-sm; color: $text-color-muted; padding: $spacing-4 0; text-align: center; }
  152. }
  153. .info-grid .info-item { display: flex; justify-content: space-between; align-items: center; padding: $spacing-2 0;
  154. .label { font-size: $font-size-base; color: $text-color-muted; }
  155. .value { font-size: $font-size-base; color: $text-color-primary; text-align: right; max-width: 60%; }
  156. }
  157. .sales-grid { display: flex; padding: 16rpx 0; background: $bg-color-page; border-radius: $radius-sm;
  158. .sales-item { flex: 1; display: flex; flex-direction: column; align-items: center;
  159. .sales-value { font-size: $font-size-md; font-weight: 700; color: $text-color-primary; margin-bottom: $spacing-1; }
  160. .sales-label { font-size: $font-size-xs; color: $text-color-muted; }
  161. }
  162. }
  163. .device-list .device-item { display: flex; justify-content: space-between; align-items: center; padding: $spacing-2 0;
  164. & + .device-item { border-top: 1rpx solid $border-color-light; }
  165. .device-info {
  166. .device-name { display: block; font-size: $font-size-base; color: $text-color-primary; font-weight: 500; margin-bottom: 4rpx; }
  167. .device-no { font-size: 22rpx; color: $text-color-muted; }
  168. }
  169. .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;
  170. .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
  171. &.online { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
  172. &.offline { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
  173. &.maintenance { background: $warning-color-bg; color: $warning-color; .status-dot { background: $warning-color; } }
  174. }
  175. }
  176. </style>