home.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. <template>
  2. <view class="page">
  3. <view class="yellow-header">
  4. <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
  5. <view class="header-content">
  6. <image class="header-logout" src="/static/icon-logout.svg" @click="handleLogout" />
  7. <text class="header-title">补货管理</text>
  8. </view>
  9. </view>
  10. <view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
  11. <!-- 加载 -->
  12. <view class="load-tip" v-if="loading">
  13. <view class="dot-row"><view class="pulse-dot"></view><view class="pulse-dot"></view><view class="pulse-dot"></view></view>
  14. </view>
  15. <!-- 设备列表 -->
  16. <scroll-view class="device-scroll" scroll-y v-else>
  17. <view class="empty-state" v-if="deviceList.length === 0">
  18. <view class="empty-icon-box"><view class="empty-device-icon"></view></view>
  19. <text class="empty-text">暂无绑定的设备</text>
  20. <text class="empty-hint">请联系管理员绑定设备后开始补货</text>
  21. </view>
  22. <view class="device-card" v-for="(device, index) in deviceList" :key="device.deviceId || index" @click="goToOperation(device)">
  23. <view class="card-left">
  24. <view class="card-icon"></view>
  25. </view>
  26. <view class="card-main">
  27. <text class="card-name">{{ device.deviceId }} - {{ device.name || '' }}</text>
  28. <view class="card-stats">
  29. <view class="cs-item">
  30. <text class="cs-label">标准</text>
  31. <text class="cs-value">{{ device.standardStock || 0 }}</text>
  32. </view>
  33. <view class="cs-item">
  34. <text class="cs-label">剩余</text>
  35. <text class="cs-value" :class="{ warn: device.remainingStock < device.standardStock }">{{ device.remainingStock || 0 }}</text>
  36. </view>
  37. <view class="cs-item" v-if="device.pendingOrderCount > 0">
  38. <text class="cs-label">待补</text>
  39. <text class="cs-value accent">{{ device.pendingOrderCount }}</text>
  40. </view>
  41. </view>
  42. </view>
  43. <view class="card-badge" :class="device.remainingStock < device.standardStock ? 'warn' : 'normal'">
  44. <text>{{ device.remainingStock < device.standardStock ? '待补货' : '正常' }}</text>
  45. </view>
  46. <view class="card-audit" @click.stop="goToAudit(device)"><text>盘点</text></view>
  47. </view>
  48. </scroll-view>
  49. <!-- 扫码浮钮 -->
  50. <view class="scan-fab" @click="handleScan">
  51. <view class="scan-icon"></view>
  52. </view>
  53. </view>
  54. </template>
  55. <script setup lang="ts">
  56. import { ref, onMounted } from 'vue';
  57. import { onShow } from '@dcloudio/uni-app';
  58. import { getDeviceList } from '@/api/replenish';
  59. import { clearAuth } from '@/utils/auth';
  60. const statusBarHeight = ref(0);
  61. const deviceList = ref<any[]>([]);
  62. const loading = ref(true);
  63. const loadData = async () => {
  64. loading.value = true;
  65. try {
  66. const devices = await getDeviceList();
  67. deviceList.value = devices || [];
  68. } catch { /* ignore */ }
  69. finally { loading.value = false; }
  70. };
  71. const goToOperation = (device: any) => {
  72. uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${device.deviceId}` });
  73. };
  74. const goToAudit = (device: any) => {
  75. uni.navigateTo({ url: `/pages/replenish/audit?deviceId=${device.deviceId}` });
  76. };
  77. const handleScan = () => {
  78. uni.scanCode({
  79. onlyFromCamera: true,
  80. scanType: ['qrCode', 'barCode'],
  81. success: (res: any) => {
  82. // 与 haha-mp 一致的 deviceId 提取逻辑
  83. let deviceId = '';
  84. try {
  85. const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
  86. const match = res.result.match(urlPattern);
  87. if (match && match[1]) {
  88. deviceId = match[1];
  89. } else {
  90. try {
  91. const qrData = JSON.parse(res.result);
  92. if (qrData.deviceId) deviceId = qrData.deviceId;
  93. } catch (e) {
  94. deviceId = res.result;
  95. }
  96. }
  97. if (!deviceId) throw new Error('无法解析设备ID');
  98. } catch (error) {
  99. uni.showToast({ title: '二维码格式错误', icon: 'none' });
  100. return;
  101. }
  102. uni.showActionSheet({
  103. itemList: ['设备补货', '库存盘点'],
  104. success: (sheetRes) => {
  105. if (sheetRes.tapIndex === 0) {
  106. uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${deviceId}` });
  107. } else if (sheetRes.tapIndex === 1) {
  108. uni.navigateTo({ url: `/pages/replenish/audit?deviceId=${deviceId}` });
  109. }
  110. }
  111. });
  112. },
  113. fail: () => { /* 用户取消扫码 */ }
  114. });
  115. };
  116. const handleLogout = () => {
  117. uni.showModal({
  118. title: '退出登录',
  119. content: '确定要退出登录吗?',
  120. success: (res) => {
  121. if (res.confirm) { clearAuth(); uni.reLaunch({ url: '/pages/login/login' }); }
  122. }
  123. });
  124. };
  125. onMounted(() => {
  126. const info = uni.getSystemInfoSync();
  127. statusBarHeight.value = info.statusBarHeight || 20;
  128. });
  129. onShow(async () => { await loadData(); });
  130. </script>
  131. <style lang="scss" scoped>
  132. .page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; overflow: hidden; }
  133. // ====== Header ======
  134. .yellow-header { position: fixed; top: 0; left: 0; right: 0; z-index: 999; background: $primary-color; padding-top: constant(safe-area-inset-top); padding-top: env(safe-area-inset-top); }
  135. .header-content { display: flex; align-items: center; justify-content: center; height: 44px; padding: 0 16rpx; position: relative; }
  136. .header-title { font-size: $font-size-xl; font-weight: 700; color: $text-color-primary; }
  137. .header-logout { position: absolute; left: 24rpx; width: 40rpx; height: 40rpx; &:active { opacity: 0.7; } }
  138. .header-placeholder { width: 100%; }
  139. // ====== Loading ======
  140. .load-tip { display: flex; justify-content: center; padding: 200rpx 0; }
  141. .dot-row { display: flex; gap: $spacing-1; }
  142. .pulse-dot { width: 10rpx; height: 10rpx; background: $text-color-placeholder; border-radius: 50%; animation: pulse 1.2s ease-in-out infinite;
  143. &:nth-child(2) { animation-delay: 0.2s; } &:nth-child(3) { animation-delay: 0.4s; }
  144. }
  145. @keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
  146. // ====== Device ======
  147. .device-scroll { flex: 1; height: 0; }
  148. .empty-state { display: flex; flex-direction: column; align-items: center; padding: 160rpx 0;
  149. .empty-icon-box { width: 120rpx; height: 120rpx; background: $bg-color-secondary; border-radius: $radius-xl; display: flex; align-items: center; justify-content: center; margin-bottom: $spacing-3; }
  150. .empty-device-icon { width: 44rpx; height: 36rpx; border: 3rpx solid $text-color-placeholder; border-radius: 6rpx; position: relative;
  151. &::after { content: ''; position: absolute; bottom: -8rpx; left: 50%; transform: translateX(-50%); width: 22rpx; height: 6rpx; background: $text-color-placeholder; border-radius: 0 0 4rpx 4rpx; }
  152. }
  153. .empty-text { font-size: $font-size-base; color: $text-color-secondary; margin-bottom: $spacing-1; }
  154. .empty-hint { font-size: $font-size-sm; color: $text-color-muted; }
  155. }
  156. .device-card { display: flex; align-items: center; gap: 16rpx; margin: 16rpx 24rpx; padding: 24rpx; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
  157. &:active { opacity: 0.7; }
  158. }
  159. .card-left { flex-shrink: 0; }
  160. .card-icon { width: 56rpx; height: 56rpx; background: $success-color-bg; border-radius: $radius-base; position: relative;
  161. &::before { content: ''; position: absolute; top: 10rpx; left: 50%; transform: translateX(-50%); width: 24rpx; height: 18rpx; border: 2.5rpx solid $success-color; border-radius: 4rpx; }
  162. &::after { content: ''; position: absolute; bottom: 6rpx; left: 50%; transform: translateX(-50%); width: 14rpx; height: 5rpx; background: $success-color; border-radius: 0 0 3rpx 3rpx; }
  163. }
  164. .card-main { flex: 1; min-width: 0; }
  165. .card-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; margin-bottom: 10rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  166. .card-stats { display: flex; gap: 24rpx; }
  167. .cs-item { display: flex; align-items: baseline; gap: 6rpx; }
  168. .cs-label { font-size: 22rpx; color: $text-color-muted; }
  169. .cs-value { font-size: $font-size-base; font-weight: 700; color: $text-color-primary;
  170. &.warn { color: $warning-color; }
  171. &.accent { color: $accent-color; }
  172. }
  173. .card-badge { padding: 8rpx 16rpx; border-radius: $radius-sm; font-size: 22rpx; font-weight: 500; flex-shrink: 0;
  174. &.normal { background: $success-color-bg; color: $success-color; }
  175. &.warn { background: $warning-color-bg; color: $warning-color; }
  176. }
  177. .card-audit { margin-left: 12rpx; padding: 8rpx 16rpx; border-radius: $radius-sm; background: $info-color-bg; font-size: 22rpx; color: $info-color; font-weight: 500; flex-shrink: 0;
  178. &:active { opacity: 0.7; }
  179. }
  180. .scan-fab { position: fixed; left: 50%; bottom: calc(120rpx + env(safe-area-inset-bottom)); transform: translateX(-50%); width: 128rpx; height: 128rpx; border-radius: 50%; background: $primary-color; box-shadow: 0 12rpx 36rpx rgba(255,193,7,0.4); display: flex; align-items: center; justify-content: center; z-index: 100;
  181. &:active { transform: translateX(-50%) scale(0.92); background: $primary-color-dark; }
  182. }
  183. .scan-icon { width: 56rpx; height: 56rpx; position: relative;
  184. &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border: 5rpx solid $text-color-primary; border-radius: 10rpx; }
  185. &::after { content: ''; position: absolute; top: 50%; left: 8rpx; right: 8rpx; height: 3rpx; background: $text-color-primary; border-radius: 2rpx; transform: translateY(-50%); }
  186. }
  187. </style>