|
@@ -2,108 +2,67 @@
|
|
|
<view class="page">
|
|
<view class="page">
|
|
|
<NavBar title="补货操作" showBack />
|
|
<NavBar title="补货操作" showBack />
|
|
|
|
|
|
|
|
- <view class="loading-container" v-if="loading">
|
|
|
|
|
- <view class="loading-spinner"></view>
|
|
|
|
|
- <text class="loading-text">加载设备信息...</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>
|
|
|
|
|
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
- <!-- 补货单提示 -->
|
|
|
|
|
<view class="order-banner" v-if="orderNo">
|
|
<view class="order-banner" v-if="orderNo">
|
|
|
<text class="order-banner-text">补货单 {{ orderNo }} · 数量已按计划预填</text>
|
|
<text class="order-banner-text">补货单 {{ orderNo }} · 数量已按计划预填</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 设备信息 -->
|
|
|
|
|
- <view class="device-section">
|
|
|
|
|
- <view class="device-card">
|
|
|
|
|
- <view class="device-header">
|
|
|
|
|
- <view class="device-icon"></view>
|
|
|
|
|
- <view class="device-info">
|
|
|
|
|
- <text class="device-name">{{ deviceInfo.name || deviceInfo.deviceId }}</text>
|
|
|
|
|
- <text class="device-id" v-if="deviceInfo.address">{{ deviceInfo.address }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <view class="device-card">
|
|
|
|
|
+ <view class="device-icon"></view>
|
|
|
|
|
+ <view class="device-info">
|
|
|
|
|
+ <text class="device-name">{{ deviceInfo.name || deviceInfo.deviceId }}</text>
|
|
|
|
|
+ <text class="device-addr" v-if="deviceInfo.address">{{ deviceInfo.address }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 商品库存清单 -->
|
|
|
|
|
<view class="inventory-section">
|
|
<view class="inventory-section">
|
|
|
- <view class="section-header">
|
|
|
|
|
|
|
+ <view class="section-head">
|
|
|
<text class="section-title">商品库存清单</text>
|
|
<text class="section-title">商品库存清单</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 空状态 -->
|
|
|
|
|
<view class="empty-state" v-if="inventoryList.length === 0">
|
|
<view class="empty-state" v-if="inventoryList.length === 0">
|
|
|
|
|
+ <view class="empty-icon-box"><view class="empty-doc-icon"></view></view>
|
|
|
<text class="empty-text">暂无商品库存数据</text>
|
|
<text class="empty-text">暂无商品库存数据</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 商品列表 -->
|
|
|
|
|
- <view
|
|
|
|
|
- class="inventory-card"
|
|
|
|
|
- v-for="(item, index) in inventoryList"
|
|
|
|
|
- :key="index"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="item-main">
|
|
|
|
|
- <image
|
|
|
|
|
- v-if="item.product_image"
|
|
|
|
|
- :src="item.product_image"
|
|
|
|
|
- class="item-img"
|
|
|
|
|
- mode="aspectFill"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <view class="inventory-card" v-for="(item, index) in inventoryList" :key="index">
|
|
|
|
|
+ <view class="item-top">
|
|
|
|
|
+ <image v-if="item.product_image" :src="item.product_image" class="item-img" mode="aspectFill" />
|
|
|
<view v-else class="item-img-placeholder"><text>无图</text></view>
|
|
<view v-else class="item-img-placeholder"><text>无图</text></view>
|
|
|
<view class="item-info">
|
|
<view class="item-info">
|
|
|
<text class="item-name">{{ item.product_name || item.productName || '未知商品' }}</text>
|
|
<text class="item-name">{{ item.product_name || item.productName || '未知商品' }}</text>
|
|
|
<text class="item-code" v-if="item.product_code || item.productCode">{{ item.product_code || item.productCode }}</text>
|
|
<text class="item-code" v-if="item.product_code || item.productCode">{{ item.product_code || item.productCode }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view :class="['stock-badge', getStockStatus(item)]">
|
|
|
|
|
- {{ getStockStatusText(item) }}
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <view class="stock-badge" :class="getStockStatus(item)">{{ getStockStatusText(item) }}</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<view class="item-stock">
|
|
<view class="item-stock">
|
|
|
- <view class="stock-row">
|
|
|
|
|
- <text class="stock-label">标准库存</text>
|
|
|
|
|
- <text class="stock-value muted">{{ item.warning_threshold || item.warningThreshold || '-' }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="stock-row">
|
|
|
|
|
- <text class="stock-label">剩余库存</text>
|
|
|
|
|
- <text :class="['stock-value', { 'low-stock': isLowStock(item), 'out-of-stock': isOutOfStock(item) }]">
|
|
|
|
|
- {{ item.stock || 0 }}
|
|
|
|
|
- </text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <text class="stock-label">标准库存 {{ item.warning_threshold || item.warningThreshold || '-' }}</text>
|
|
|
|
|
+ <text class="stock-label">剩余库存 <text :class="{ warn: isLowStock(item), danger: isOutOfStock(item) }">{{ item.stock || 0 }}</text></text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 补货输入 -->
|
|
|
|
|
- <view class="replenish-input-section">
|
|
|
|
|
- <text class="input-label">补货数量</text>
|
|
|
|
|
- <view class="quantity-control">
|
|
|
|
|
- <view class="qty-btn" @click="decrease(item, index)">-</view>
|
|
|
|
|
- <input
|
|
|
|
|
- class="qty-input"
|
|
|
|
|
- type="number"
|
|
|
|
|
- v-model="replenishItems[index].quantity"
|
|
|
|
|
- @blur="validateQuantity(index)"
|
|
|
|
|
- />
|
|
|
|
|
- <view class="qty-btn" @click="increase(item, index)">+</view>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="suggest-btn" @click="suggestQuantity(item, index)">
|
|
|
|
|
- 一键补满
|
|
|
|
|
|
|
+ <view class="replenish-row">
|
|
|
|
|
+ <text class="replenish-label">补货数量</text>
|
|
|
|
|
+ <view class="qty-control">
|
|
|
|
|
+ <view class="qty-btn" @click="decrease(item, index)"><text>-</text></view>
|
|
|
|
|
+ <input class="qty-input" type="number" v-model="replenishItems[index].quantity" @blur="validateQuantity(index)" />
|
|
|
|
|
+ <view class="qty-btn" @click="increase(item, index)"><text>+</text></view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <view class="fill-btn" @click="suggestQuantity(item, index)"><text>一键补满</text></view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 底部提交 -->
|
|
|
|
|
- <view class="bottom-actions" v-if="inventoryList.length > 0">
|
|
|
|
|
- <view class="total-info">
|
|
|
|
|
- <text class="total-label">共补货</text>
|
|
|
|
|
- <text class="total-value">{{ totalReplenishCount }}</text>
|
|
|
|
|
- <text class="total-label">件商品</text>
|
|
|
|
|
|
|
+ <view class="bottom-bar" v-if="inventoryList.length > 0">
|
|
|
|
|
+ <text class="total-label">共补货 <text class="total-num">{{ totalReplenishCount }}</text> 件</text>
|
|
|
|
|
+ <view class="submit-btn" :class="{ disabled: submitting || totalReplenishCount === 0 }" @click="handleSubmit">
|
|
|
|
|
+ <view class="btn-spinner" v-if="submitting"></view>
|
|
|
|
|
+ <text>{{ submitting ? '提交中' : '确认补货' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <button class="submit-btn" :loading="submitting" :disabled="submitting || totalReplenishCount === 0" @click="handleSubmit">
|
|
|
|
|
- <text v-if="!submitting">确认补货</text>
|
|
|
|
|
- <text v-else>提交中...</text>
|
|
|
|
|
- </button>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
</view>
|
|
</view>
|
|
@@ -114,21 +73,6 @@ import { ref, computed, onMounted } from 'vue';
|
|
|
import NavBar from '@/components/NavBar.vue';
|
|
import NavBar from '@/components/NavBar.vue';
|
|
|
import { getDeviceInventory, replenishStock } from '@/api/replenish';
|
|
import { getDeviceInventory, replenishStock } from '@/api/replenish';
|
|
|
|
|
|
|
|
-interface InventoryItem {
|
|
|
|
|
- product_id?: number;
|
|
|
|
|
- productId?: number;
|
|
|
|
|
- product_code?: string;
|
|
|
|
|
- productCode?: string;
|
|
|
|
|
- product_name?: string;
|
|
|
|
|
- productName?: string;
|
|
|
|
|
- stock?: number;
|
|
|
|
|
- warning_threshold?: number;
|
|
|
|
|
- warningThreshold?: number;
|
|
|
|
|
- shelf_num?: number;
|
|
|
|
|
- shelfNum?: number;
|
|
|
|
|
- position?: string;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
interface ReplenishInput {
|
|
interface ReplenishInput {
|
|
|
productId: number;
|
|
productId: number;
|
|
|
productCode?: string;
|
|
productCode?: string;
|
|
@@ -140,464 +84,166 @@ interface ReplenishInput {
|
|
|
|
|
|
|
|
const deviceId = ref('');
|
|
const deviceId = ref('');
|
|
|
const deviceInfo = ref<any>({});
|
|
const deviceInfo = ref<any>({});
|
|
|
-const inventoryList = ref<InventoryItem[]>([]);
|
|
|
|
|
|
|
+const inventoryList = ref<any[]>([]);
|
|
|
const replenishItems = ref<ReplenishInput[]>([]);
|
|
const replenishItems = ref<ReplenishInput[]>([]);
|
|
|
const loading = ref(true);
|
|
const loading = ref(true);
|
|
|
const submitting = ref(false);
|
|
const submitting = ref(false);
|
|
|
|
|
+const orderId = ref('');
|
|
|
|
|
+const orderNo = ref('');
|
|
|
|
|
|
|
|
-const totalReplenishCount = computed(() => {
|
|
|
|
|
- return replenishItems.value.reduce((sum, item) => sum + (item.quantity || 0), 0);
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const isLowStock = (item: any): boolean => {
|
|
|
|
|
- const stock = item.stock || 0;
|
|
|
|
|
- const threshold = item.warning_threshold || item.warningThreshold || 5;
|
|
|
|
|
- return stock > 0 && stock <= threshold;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const isOutOfStock = (item: any): boolean => {
|
|
|
|
|
- return (item.stock || 0) === 0;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const getStockStatus = (item: any): string => {
|
|
|
|
|
- if (isOutOfStock(item)) return 'danger';
|
|
|
|
|
- if (isLowStock(item)) return 'warning';
|
|
|
|
|
- return 'normal';
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const getStockStatusText = (item: any): string => {
|
|
|
|
|
- if ((item.stock || 0) === 0) return '缺货';
|
|
|
|
|
- const threshold = item.warning_threshold || item.warningThreshold || 5;
|
|
|
|
|
- if ((item.stock || 0) <= threshold) return '低库存';
|
|
|
|
|
- return '正常';
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const decrease = (item: any, index: number) => {
|
|
|
|
|
- if (replenishItems.value[index].quantity > 0) {
|
|
|
|
|
- replenishItems.value[index].quantity--;
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const MAX_QUANTITY = 9999;
|
|
|
|
|
|
|
+const totalReplenishCount = computed(() => replenishItems.value.reduce((s, i) => s + (i.quantity || 0), 0));
|
|
|
|
|
|
|
|
-const increase = (item: any, index: number) => {
|
|
|
|
|
- if (replenishItems.value[index].quantity < MAX_QUANTITY) {
|
|
|
|
|
- replenishItems.value[index].quantity++;
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
|
|
+const isLowStock = (item: any) => { const s = item.stock || 0; const t = item.warning_threshold || item.warningThreshold || 5; return s > 0 && s <= t; };
|
|
|
|
|
+const isOutOfStock = (item: any) => (item.stock || 0) === 0;
|
|
|
|
|
+const getStockStatus = (item: any) => isOutOfStock(item) ? 'danger' : isLowStock(item) ? 'warning' : 'normal';
|
|
|
|
|
+const getStockStatusText = (item: any) => (item.stock || 0) === 0 ? '缺货' : (item.stock || 0) <= (item.warning_threshold || item.warningThreshold || 5) ? '低库存' : '正常';
|
|
|
|
|
|
|
|
-const suggestQuantity = (item: any, index: number) => {
|
|
|
|
|
- const threshold = item.warning_threshold || item.warningThreshold || 5;
|
|
|
|
|
- const currentStock = item.stock || 0;
|
|
|
|
|
- // 建议补满到预警阈值的3倍(即建议库存水平)
|
|
|
|
|
- const suggestLevel = threshold * 3;
|
|
|
|
|
- const suggest = Math.max(suggestLevel - currentStock, 0);
|
|
|
|
|
- replenishItems.value[index].quantity = suggest;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const validateQuantity = (index: number) => {
|
|
|
|
|
- if (replenishItems.value[index].quantity < 0) {
|
|
|
|
|
- replenishItems.value[index].quantity = 0;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const decrease = (_: any, i: number) => { if (replenishItems.value[i].quantity > 0) replenishItems.value[i].quantity--; };
|
|
|
|
|
+const increase = (_: any, i: number) => { if (replenishItems.value[i].quantity < 9999) replenishItems.value[i].quantity++; };
|
|
|
|
|
+const suggestQuantity = (item: any, i: number) => {
|
|
|
|
|
+ const std = item.warning_threshold || item.warningThreshold || 0;
|
|
|
|
|
+ const cur = item.stock || 0;
|
|
|
|
|
+ replenishItems.value[i].quantity = cur >= std ? 0 : std - cur;
|
|
|
};
|
|
};
|
|
|
|
|
+const validateQuantity = (i: number) => { if (replenishItems.value[i].quantity < 0) replenishItems.value[i].quantity = 0; };
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
const handleSubmit = async () => {
|
|
|
- if (totalReplenishCount.value === 0) {
|
|
|
|
|
- uni.showToast({ title: '请至少补货一件商品', icon: 'none' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 过滤掉数量为0的项
|
|
|
|
|
- const items = replenishItems.value
|
|
|
|
|
- .filter(item => item.quantity > 0)
|
|
|
|
|
- .map(item => ({
|
|
|
|
|
- productId: item.productId,
|
|
|
|
|
- productCode: item.productCode,
|
|
|
|
|
- productName: item.productName,
|
|
|
|
|
- quantity: item.quantity,
|
|
|
|
|
- shelfNum: item.shelfNum,
|
|
|
|
|
- position: item.position
|
|
|
|
|
- }));
|
|
|
|
|
-
|
|
|
|
|
- if (items.length === 0) {
|
|
|
|
|
- uni.showToast({ title: '请至少补货一件商品', icon: 'none' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (submitting.value || totalReplenishCount.value === 0) return;
|
|
|
|
|
+ const items = replenishItems.value.filter(i => i.quantity > 0).map(i => ({
|
|
|
|
|
+ productId: i.productId, productCode: i.productCode, productName: i.productName,
|
|
|
|
|
+ quantity: i.quantity, shelfNum: i.shelfNum, position: i.position
|
|
|
|
|
+ }));
|
|
|
|
|
+ if (!items.length) { uni.showToast({ title: '请至少补货一件', icon: 'none' }); return; }
|
|
|
|
|
|
|
|
submitting.value = true;
|
|
submitting.value = true;
|
|
|
try {
|
|
try {
|
|
|
const payload: any = { deviceId: deviceId.value, items };
|
|
const payload: any = { deviceId: deviceId.value, items };
|
|
|
if (orderId.value) payload.orderId = orderId.value;
|
|
if (orderId.value) payload.orderId = orderId.value;
|
|
|
-
|
|
|
|
|
const result = await replenishStock(payload);
|
|
const result = await replenishStock(payload);
|
|
|
-
|
|
|
|
|
uni.removeStorageSync('pendingReplenishOrder');
|
|
uni.removeStorageSync('pendingReplenishOrder');
|
|
|
uni.showToast({ title: `补货完成,成功${result.success}项`, icon: 'success' });
|
|
uni.showToast({ title: `补货完成,成功${result.success}项`, icon: 'success' });
|
|
|
- setTimeout(() => {
|
|
|
|
|
- uni.navigateBack();
|
|
|
|
|
- }, 800);
|
|
|
|
|
- } catch (error: any) {
|
|
|
|
|
- uni.showToast({ title: error.message || '补货失败', icon: 'none' });
|
|
|
|
|
- } finally {
|
|
|
|
|
- submitting.value = false;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ setTimeout(() => uni.navigateBack(), 800);
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ uni.showToast({ title: e.message || '补货失败', icon: 'none' });
|
|
|
|
|
+ } finally { submitting.value = false; }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const orderId = ref('');
|
|
|
|
|
-const orderNo = ref('');
|
|
|
|
|
-
|
|
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
const pages = getCurrentPages();
|
|
const pages = getCurrentPages();
|
|
|
- const currentPage = pages[pages.length - 1];
|
|
|
|
|
- const options = (currentPage as any).$page?.options || (currentPage as any).options || {};
|
|
|
|
|
- deviceId.value = options.deviceId || '';
|
|
|
|
|
- orderId.value = options.orderId || '';
|
|
|
|
|
-
|
|
|
|
|
- if (!deviceId.value) {
|
|
|
|
|
- uni.showToast({ title: '缺少设备ID', icon: 'none' });
|
|
|
|
|
- setTimeout(() => uni.navigateBack(), 500);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const opts = (pages[pages.length - 1] as any)?.$page?.options || (pages[pages.length - 1] as any)?.options || {};
|
|
|
|
|
+ deviceId.value = opts.deviceId || '';
|
|
|
|
|
+ orderId.value = opts.orderId || '';
|
|
|
|
|
+ if (!deviceId.value) { uni.showToast({ title: '缺少设备ID', icon: 'none' }); setTimeout(() => uni.navigateBack(), 500); return; }
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const data = await getDeviceInventory(deviceId.value);
|
|
const data = await getDeviceInventory(deviceId.value);
|
|
|
deviceInfo.value = data;
|
|
deviceInfo.value = data;
|
|
|
-
|
|
|
|
|
const list = data.inventoryList || [];
|
|
const list = data.inventoryList || [];
|
|
|
inventoryList.value = list;
|
|
inventoryList.value = list;
|
|
|
|
|
|
|
|
- // 加载补货单预填数据
|
|
|
|
|
- const orderStr = uni.getStorageSync('pendingReplenishOrder');
|
|
|
|
|
let orderItems: any[] = [];
|
|
let orderItems: any[] = [];
|
|
|
|
|
+ const orderStr = uni.getStorageSync('pendingReplenishOrder');
|
|
|
if (orderStr) {
|
|
if (orderStr) {
|
|
|
- try {
|
|
|
|
|
- const orderData = JSON.parse(orderStr);
|
|
|
|
|
- if (orderData.orderId === orderId.value) {
|
|
|
|
|
- orderNo.value = orderData.orderNo || '';
|
|
|
|
|
- orderItems = orderData.items || [];
|
|
|
|
|
- }
|
|
|
|
|
- } catch { /* ignore parse error */ }
|
|
|
|
|
|
|
+ try { const od = JSON.parse(orderStr); if (od.orderId === orderId.value) { orderNo.value = od.orderNo || ''; orderItems = od.items || []; } } catch { }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 初始化补货输入,匹配补货单预填数量
|
|
|
|
|
- replenishItems.value = list
|
|
|
|
|
- .filter((item: any) => (item.product_id || item.productId))
|
|
|
|
|
- .map((item: any) => {
|
|
|
|
|
- const code = item.product_code || item.productCode || '';
|
|
|
|
|
- const matched = orderItems.find((oi: any) => oi.productCode === code);
|
|
|
|
|
- return {
|
|
|
|
|
- productId: item.product_id || item.productId,
|
|
|
|
|
- productCode: code,
|
|
|
|
|
- productName: item.product_name || item.productName || '',
|
|
|
|
|
- quantity: matched ? (matched.plannedQuantity || 0) : 0,
|
|
|
|
|
- shelfNum: item.shelf_num || item.shelfNum || null,
|
|
|
|
|
- position: item.position || null
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error: any) {
|
|
|
|
|
- uni.showToast({ title: error.message || '加载失败', icon: 'none' });
|
|
|
|
|
- } finally {
|
|
|
|
|
- loading.value = false;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ replenishItems.value = list.filter((it: any) => it.product_id || it.productId).map((it: any) => {
|
|
|
|
|
+ const code = it.product_code || it.productCode || '';
|
|
|
|
|
+ const m = orderItems.find((oi: any) => oi.productCode === code);
|
|
|
|
|
+ return {
|
|
|
|
|
+ productId: it.product_id || it.productId, productCode: code,
|
|
|
|
|
+ productName: it.product_name || it.productName || '',
|
|
|
|
|
+ quantity: m ? (m.plannedQuantity || 0) : 0,
|
|
|
|
|
+ shelfNum: it.shelf_num || it.shelfNum || null, position: it.position || null
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ uni.showToast({ title: e.message || '加载失败', icon: 'none' });
|
|
|
|
|
+ } finally { loading.value = false; }
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
-.page {
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- background: $bg-color-page;
|
|
|
|
|
- padding-bottom: 200rpx;
|
|
|
|
|
|
|
+.page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; }
|
|
|
|
|
+
|
|
|
|
|
+.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; } }
|
|
|
|
|
|
|
|
-/* 补货单提示 */
|
|
|
|
|
-.order-banner { margin: $spacing-2 $spacing-3; padding: $spacing-2 $spacing-3; background: $primary-color-bg; border-radius: $radius-sm; border-left: 4rpx solid $primary-color; }
|
|
|
|
|
|
|
+.order-banner { margin: $spacing-2 $spacing-3; padding: $spacing-2 $spacing-3; background: $primary-color-bg; border-radius: $radius-sm; }
|
|
|
.order-banner-text { font-size: $font-size-sm; color: $primary-color-dark; }
|
|
.order-banner-text { font-size: $font-size-sm; color: $primary-color-dark; }
|
|
|
|
|
|
|
|
-/* 加载 */
|
|
|
|
|
-.loading-container {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 100rpx 0;
|
|
|
|
|
-
|
|
|
|
|
- .loading-text {
|
|
|
|
|
- margin-top: 16rpx;
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: $text-color-muted;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// ====== Device ======
|
|
|
|
|
+.device-card { display: flex; align-items: center; margin: 16rpx 24rpx; padding: $spacing-3; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); }
|
|
|
|
|
+.device-icon { width: 48rpx; height: 48rpx; background: $success-color-bg; border-radius: $radius-base; margin-right: 16rpx; flex-shrink: 0; position: relative;
|
|
|
|
|
+ &::before { content: ''; position: absolute; top: 8rpx; left: 50%; transform: translateX(-50%); width: 20rpx; height: 16rpx; border: 2.5rpx solid $success-color; border-radius: 4rpx; }
|
|
|
|
|
+ &::after { content: ''; position: absolute; bottom: 4rpx; left: 50%; transform: translateX(-50%); width: 12rpx; height: 5rpx; background: $success-color; border-radius: 0 0 3rpx 3rpx; }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-/* 设备信息 */
|
|
|
|
|
-.device-section {
|
|
|
|
|
- padding: 16rpx 24rpx 0;
|
|
|
|
|
-
|
|
|
|
|
- .device-card {
|
|
|
|
|
- background: $bg-color-card;
|
|
|
|
|
- border: 1rpx solid $border-color;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
-
|
|
|
|
|
- .device-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-
|
|
|
|
|
- .device-icon {
|
|
|
|
|
- width: 48rpx;
|
|
|
|
|
- height: 48rpx;
|
|
|
|
|
- background: $success-color-bg;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- margin-right: 16rpx;
|
|
|
|
|
- position: relative;
|
|
|
|
|
-
|
|
|
|
|
- &::before {
|
|
|
|
|
- content: '';
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 8rpx;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translateX(-50%);
|
|
|
|
|
- width: 20rpx;
|
|
|
|
|
- height: 16rpx;
|
|
|
|
|
- border: 3rpx solid $primary-color;
|
|
|
|
|
- border-radius: 4rpx;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &::after {
|
|
|
|
|
- content: '';
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 4rpx;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translateX(-50%);
|
|
|
|
|
- width: 12rpx;
|
|
|
|
|
- height: 6rpx;
|
|
|
|
|
- background: $primary-color;
|
|
|
|
|
- border-radius: 0 0 4rpx 4rpx;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .device-info {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
-
|
|
|
|
|
- .device-name {
|
|
|
|
|
- display: block;
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: $text-color-primary;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .device-id {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: $text-color-tertiary;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.device-info { flex: 1; min-width: 0; }
|
|
|
|
|
+.device-name { display: block; font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
|
|
|
|
|
+.device-addr { font-size: $font-size-sm; color: $text-color-muted; }
|
|
|
|
|
+
|
|
|
|
|
+// ====== Inventory ======
|
|
|
|
|
+.inventory-section { padding: 0 24rpx; flex: 1; height: 0; overflow-y: auto; }
|
|
|
|
|
+.section-head { padding: $spacing-3 0 $spacing-2; }
|
|
|
|
|
+.section-title { font-size: $font-size-md; font-weight: 700; color: $text-color-primary; }
|
|
|
|
|
+
|
|
|
|
|
+.empty-state { display: flex; flex-direction: column; align-items: center; padding: 80rpx 0; }
|
|
|
|
|
+.empty-icon-box { width: 96rpx; height: 96rpx; background: $bg-color-secondary; border-radius: $radius-lg; display: flex; align-items: center; justify-content: center; margin-bottom: $spacing-2; }
|
|
|
|
|
+.empty-doc-icon { width: 40rpx; height: 48rpx; border: 2.5rpx solid $text-color-placeholder; border-radius: 4rpx; position: relative;
|
|
|
|
|
+ &::after { content: ''; position: absolute; top: 14rpx; left: 8rpx; right: 8rpx; height: 2.5rpx; background: $text-color-placeholder; border-radius: 1rpx; box-shadow: 0 8rpx 0 $text-color-placeholder, 0 16rpx 0 $text-color-placeholder; }
|
|
|
}
|
|
}
|
|
|
|
|
+.empty-text { font-size: $font-size-base; color: $text-color-muted; }
|
|
|
|
|
|
|
|
-/* 库存区域 */
|
|
|
|
|
-.inventory-section {
|
|
|
|
|
- padding: 0 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.inventory-card { margin-bottom: 16rpx; padding: $spacing-3; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); }
|
|
|
|
|
|
|
|
-.section-header {
|
|
|
|
|
- padding: 24rpx 0 16rpx;
|
|
|
|
|
-
|
|
|
|
|
- .section-title {
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: $text-color-primary;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.item-top { display: flex; align-items: center; margin-bottom: 12rpx; }
|
|
|
|
|
+.item-img { width: 72rpx; height: 72rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 12rpx; }
|
|
|
|
|
+.item-img-placeholder { width: 72rpx; height: 72rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 12rpx; display: flex; align-items: center; justify-content: center;
|
|
|
|
|
+ text { font-size: 20rpx; color: $text-color-muted; }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.empty-state {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 60rpx 0;
|
|
|
|
|
-
|
|
|
|
|
- .empty-text {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: $text-color-muted;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.item-info { flex: 1; min-width: 0; overflow: hidden; }
|
|
|
|
|
+.item-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
+.item-code { font-size: $font-size-xs; color: $text-color-muted; }
|
|
|
|
|
+
|
|
|
|
|
+.stock-badge { padding: 4rpx 12rpx; border-radius: $radius-sm; font-size: 22rpx; font-weight: 500; flex-shrink: 0; margin-left: 8rpx;
|
|
|
|
|
+ &.normal { background: $success-color-bg; color: $success-color; }
|
|
|
|
|
+ &.warning { background: $warning-color-bg; color: $warning-color; }
|
|
|
|
|
+ &.danger { background: $error-color-bg; color: $error-color; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 商品卡片 */
|
|
|
|
|
-.inventory-card {
|
|
|
|
|
- background: $bg-color-card;
|
|
|
|
|
- border: 1rpx solid $border-color;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
- margin-bottom: 16rpx;
|
|
|
|
|
-
|
|
|
|
|
- .item-main { display: flex; align-items: center; margin-bottom: 16rpx; }
|
|
|
|
|
- .item-img { width: 80rpx; height: 80rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 16rpx; }
|
|
|
|
|
- .item-img-placeholder { width: 80rpx; height: 80rpx; border-radius: $radius-sm; background: $bg-color-page; flex-shrink: 0; margin-right: 16rpx; display: flex; align-items: center; justify-content: center;
|
|
|
|
|
- text { font-size: 20rpx; color: $text-color-muted; }
|
|
|
|
|
- }
|
|
|
|
|
- .item-info { flex: 1; min-width: 0; overflow: hidden; }
|
|
|
|
|
- .item-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
- .item-code { font-size: 22rpx; color: $text-color-muted; }
|
|
|
|
|
-
|
|
|
|
|
- .stock-badge { padding: 4rpx 12rpx; border-radius: $radius-sm; font-size: 20rpx; font-weight: 500; flex-shrink: 0; margin-left: 12rpx;
|
|
|
|
|
- &.normal { background: $success-color-bg; color: $success-color; }
|
|
|
|
|
- &.warning { background: $warning-color-bg; color: $warning-color; }
|
|
|
|
|
- &.danger { background: $error-color-bg; color: $error-color; }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .item-stock {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 24rpx;
|
|
|
|
|
- margin-bottom: 16rpx;
|
|
|
|
|
-
|
|
|
|
|
- .stock-row {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-
|
|
|
|
|
- .stock-label {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: $text-color-muted;
|
|
|
|
|
- margin-right: 8rpx;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .stock-value {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: $text-color-primary;
|
|
|
|
|
-
|
|
|
|
|
- &.muted {
|
|
|
|
|
- color: $text-color-muted;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &.low-stock {
|
|
|
|
|
- color: $warning-color;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &.out-of-stock {
|
|
|
|
|
- color: $error-color;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .replenish-input-section {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- background: $bg-color-page;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- padding: 16rpx;
|
|
|
|
|
-
|
|
|
|
|
- .input-label {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: $text-color-tertiary;
|
|
|
|
|
- margin-right: 16rpx;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .quantity-control {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- border: 1rpx solid $border-color;
|
|
|
|
|
- border-radius: 8rpx;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-
|
|
|
|
|
- .qty-btn {
|
|
|
|
|
- width: 56rpx;
|
|
|
|
|
- height: 56rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: $text-color-tertiary;
|
|
|
|
|
- background: $bg-color-card;
|
|
|
|
|
-
|
|
|
|
|
- &:active {
|
|
|
|
|
- background: $bg-color-secondary;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .qty-input {
|
|
|
|
|
- width: 80rpx;
|
|
|
|
|
- height: 56rpx;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: $text-color-primary;
|
|
|
|
|
- border-left: 1rpx solid $border-color;
|
|
|
|
|
- border-right: 1rpx solid $border-color;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .suggest-btn {
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
- padding: 10rpx 20rpx;
|
|
|
|
|
- background: $success-color-bg;
|
|
|
|
|
- border-radius: 8rpx;
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: $primary-color;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
-
|
|
|
|
|
- &:active {
|
|
|
|
|
- background: $success-color-bg;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.item-stock { display: flex; gap: 24rpx; margin-bottom: 12rpx; }
|
|
|
|
|
+.stock-label { font-size: $font-size-sm; color: $text-color-muted;
|
|
|
|
|
+ text { font-weight: 600; }
|
|
|
|
|
+ .warn { color: $warning-color; }
|
|
|
|
|
+ .danger { color: $error-color; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* 底部操作 */
|
|
|
|
|
-.bottom-actions {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- background: $bg-color-card;
|
|
|
|
|
- border-top: 1rpx solid $border-color;
|
|
|
|
|
- padding: 20rpx 24rpx;
|
|
|
|
|
- padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
|
|
|
|
|
- padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 20rpx;
|
|
|
|
|
-
|
|
|
|
|
- .total-info {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-
|
|
|
|
|
- .total-label {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: $text-color-tertiary;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .total-value {
|
|
|
|
|
- font-size: 36rpx;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: $primary-color;
|
|
|
|
|
- margin: 0 8rpx;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .submit-btn {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- height: 88rpx;
|
|
|
|
|
- line-height: 88rpx;
|
|
|
|
|
- padding: 0;
|
|
|
|
|
- background: $primary-color;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
-
|
|
|
|
|
- &::after {
|
|
|
|
|
- border: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &:active {
|
|
|
|
|
- background: $primary-color-dark;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.replenish-row { display: flex; align-items: center; background: $bg-color-page; border-radius: $radius-base; padding: 12rpx 16rpx; }
|
|
|
|
|
+.replenish-label { font-size: $font-size-sm; color: $text-color-muted; margin-right: 12rpx; }
|
|
|
|
|
+.qty-control { display: flex; align-items: center; border: 1rpx solid $border-color; border-radius: $radius-sm; overflow: hidden; }
|
|
|
|
|
+.qty-btn { width: 52rpx; height: 52rpx; display: flex; align-items: center; justify-content: center; background: $bg-color-card;
|
|
|
|
|
+ text { font-size: 28rpx; color: $text-color-secondary; line-height: 1; }
|
|
|
|
|
+ &:active { background: $bg-color-secondary; }
|
|
|
|
|
+}
|
|
|
|
|
+.qty-input { width: 72rpx; height: 52rpx; text-align: center; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; border-left: 1rpx solid $border-color; border-right: 1rpx solid $border-color; }
|
|
|
|
|
+.fill-btn { margin-left: auto; padding: 10rpx 24rpx; background: $success-color-bg; border-radius: $radius-sm;
|
|
|
|
|
+ text { font-size: 22rpx; color: $success-color; font-weight: 500; }
|
|
|
|
|
+ &:active { opacity: 0.7; }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- &[disabled] {
|
|
|
|
|
- opacity: 0.6;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// ====== Bottom ======
|
|
|
|
|
+.bottom-bar { display: flex; align-items: center; gap: 20rpx; padding: 20rpx 24rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: $bg-color-card; border-top: 1rpx solid $border-color-light; }
|
|
|
|
|
+.total-label { font-size: $font-size-sm; color: $text-color-muted; flex-shrink: 0; }
|
|
|
|
|
+.total-num { font-size: $font-size-xl; font-weight: 700; color: $primary-color-dark; margin: 0 4rpx; }
|
|
|
|
|
+.submit-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 12rpx; height: 80rpx; background: $primary-color; border-radius: $radius-base;
|
|
|
|
|
+ text { font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
|
|
|
|
|
+ &:active { opacity: 0.8; }
|
|
|
|
|
+ &.disabled { opacity: 0.5; pointer-events: none; }
|
|
|
}
|
|
}
|
|
|
|
|
+.btn-spinner { width: 28rpx; height: 28rpx; border: 3rpx solid rgba(30, 41, 59, 0.2); border-top-color: $text-color-primary; border-radius: 50%; animation: spin 0.7s linear infinite; }
|
|
|
|
|
+@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
</style>
|
|
</style>
|