|
|
@@ -0,0 +1,264 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <NavBar title="库存盘点" showBack />
|
|
|
+
|
|
|
+ <template v-if="!loading">
|
|
|
+ <!-- 设备信息 -->
|
|
|
+ <view class="device-card">
|
|
|
+ <view class="device-icon"></view>
|
|
|
+ <view class="device-info">
|
|
|
+ <text class="device-name">{{ deviceId }}</text>
|
|
|
+ <text class="device-addr" v-if="deviceName">{{ deviceName }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 商品清单 -->
|
|
|
+ <view class="inventory-section">
|
|
|
+ <view class="section-head">
|
|
|
+ <text class="section-title">盘点清单</text>
|
|
|
+ <text class="section-count">{{ inventoryList.length }} 种</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <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>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <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 class="item-info">
|
|
|
+ <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>
|
|
|
+ </view>
|
|
|
+ <view class="stock-badge" :class="getDiffClass(item, index)">
|
|
|
+ <text>{{ getDiffLabel(item, index) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="item-stock">
|
|
|
+ <view class="stock-col">
|
|
|
+ <text class="stock-label">标准库存</text>
|
|
|
+ <text class="stock-num">{{ item.standard_stock || item.standardStock || '-' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="stock-col">
|
|
|
+ <text class="stock-label">当前库存</text>
|
|
|
+ <text class="stock-num">{{ item.stock || 0 }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="audit-row">
|
|
|
+ <text class="audit-label">修正为</text>
|
|
|
+ <view class="qty-control">
|
|
|
+ <view class="qty-btn" @click="decrease(index)"><text>-</text></view>
|
|
|
+ <input class="qty-input" type="number" v-model.number="auditItems[index].newStock" />
|
|
|
+ <view class="qty-btn" @click="increase(index)"><text>+</text></view>
|
|
|
+ </view>
|
|
|
+ <view class="quick-btn" @click="setToCurrentStock(item, index)"><text>不变</text></view>
|
|
|
+ <view class="quick-btn" @click="setToStandard(item, index)"><text>设标</text></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部操作栏 -->
|
|
|
+ <view class="bottom-bar" v-if="inventoryList.length > 0">
|
|
|
+ <text class="total-label">修正 <text class="total-num">{{ changedCount }}</text> 项</text>
|
|
|
+ <view class="submit-btn" :class="{ disabled: submitting || changedCount === 0 }" @click="handleSubmit">
|
|
|
+ <view class="btn-spinner" v-if="submitting"></view>
|
|
|
+ <text>{{ submitting ? '提交中' : '提交盘点' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, onMounted } from 'vue';
|
|
|
+import NavBar from '@/components/NavBar.vue';
|
|
|
+import { getDeviceInventory, adjustStock } from '@/api/inventory';
|
|
|
+
|
|
|
+interface AuditInput {
|
|
|
+ productId: number | null;
|
|
|
+ productCode: string;
|
|
|
+ productName: string;
|
|
|
+ newStock: number;
|
|
|
+ originalStock: number;
|
|
|
+}
|
|
|
+
|
|
|
+const deviceId = ref('');
|
|
|
+const deviceName = ref('');
|
|
|
+const inventoryList = ref<any[]>([]);
|
|
|
+const auditItems = ref<AuditInput[]>([]);
|
|
|
+const loading = ref(true);
|
|
|
+const submitting = ref(false);
|
|
|
+
|
|
|
+const changedCount = computed(() =>
|
|
|
+ auditItems.value.filter(i => i.productId != null && i.newStock !== i.originalStock).length
|
|
|
+);
|
|
|
+
|
|
|
+function decrease(index: number) {
|
|
|
+ if (auditItems.value[index].newStock > 0) auditItems.value[index].newStock--;
|
|
|
+}
|
|
|
+function increase(index: number) {
|
|
|
+ if (auditItems.value[index].newStock < 9999) auditItems.value[index].newStock++;
|
|
|
+}
|
|
|
+function setToCurrentStock(item: any, index: number) {
|
|
|
+ auditItems.value[index].newStock = item.stock || 0;
|
|
|
+}
|
|
|
+function setToStandard(item: any, index: number) {
|
|
|
+ const std = item.standard_stock || item.standardStock || 0;
|
|
|
+ auditItems.value[index].newStock = std;
|
|
|
+}
|
|
|
+function getDiffLabel(item: any, index: number) {
|
|
|
+ if (!auditItems.value[index]) return '正常';
|
|
|
+ const cur = auditItems.value[index].newStock;
|
|
|
+ const std = item.standard_stock || item.standardStock || 0;
|
|
|
+ if (cur < std) return '偏少';
|
|
|
+ if (cur > std) return '偏多';
|
|
|
+ return '正常';
|
|
|
+}
|
|
|
+function getDiffClass(item: any, index: number) {
|
|
|
+ if (!auditItems.value[index]) return 'normal';
|
|
|
+ const cur = auditItems.value[index].newStock;
|
|
|
+ const std = item.standard_stock || item.standardStock || 0;
|
|
|
+ if (cur < std) return 'warning';
|
|
|
+ if (cur > std) return 'danger';
|
|
|
+ return 'normal';
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubmit() {
|
|
|
+ if (submitting.value || changedCount.value === 0) return;
|
|
|
+ const changes = auditItems.value.filter(i => i.productId != null && i.newStock !== i.originalStock);
|
|
|
+ if (!changes.length) { uni.showToast({ title: '无变更', icon: 'none' }); return; }
|
|
|
+
|
|
|
+ submitting.value = true;
|
|
|
+ let success = 0;
|
|
|
+ let fail = 0;
|
|
|
+ for (const item of changes) {
|
|
|
+ try {
|
|
|
+ await adjustStock({
|
|
|
+ deviceId: deviceId.value,
|
|
|
+ productId: item.productId!,
|
|
|
+ newStock: item.newStock,
|
|
|
+ remark: '小程序盘点修正'
|
|
|
+ });
|
|
|
+ success++;
|
|
|
+ } catch {
|
|
|
+ fail++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fail === 0) {
|
|
|
+ uni.showToast({ title: `盘点完成,修正${success}项`, icon: 'success' });
|
|
|
+ setTimeout(() => uni.navigateBack(), 800);
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: `成功${success}项,失败${fail}项`, icon: 'none' });
|
|
|
+ }
|
|
|
+ submitting.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ const pages = getCurrentPages();
|
|
|
+ const opts = (pages[pages.length - 1] as any)?.$page?.options || (pages[pages.length - 1] as any)?.options || {};
|
|
|
+ deviceId.value = opts.deviceId || '';
|
|
|
+ if (!deviceId.value) { uni.showToast({ title: '缺少设备ID', icon: 'none' }); setTimeout(() => uni.navigateBack(), 500); return; }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const data = await getDeviceInventory(deviceId.value);
|
|
|
+ deviceName.value = data?.name || '';
|
|
|
+ const list = (data?.inventoryList || data || []) as any[];
|
|
|
+ // 兼容两种返回格式
|
|
|
+ if (Array.isArray(data)) {
|
|
|
+ inventoryList.value = data;
|
|
|
+ } else if (data?.inventoryList) {
|
|
|
+ inventoryList.value = data.inventoryList;
|
|
|
+ deviceName.value = data.name || '';
|
|
|
+ } else {
|
|
|
+ inventoryList.value = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ auditItems.value = inventoryList.value.map((it: any) => ({
|
|
|
+ productId: it.product_id || it.productId || null,
|
|
|
+ productCode: it.product_code || it.productCode || '',
|
|
|
+ productName: it.product_name || it.productName || '',
|
|
|
+ newStock: it.stock || 0,
|
|
|
+ originalStock: it.stock || 0
|
|
|
+ }));
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '加载失败', icon: 'none' });
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.page { height: 100vh; background: $bg-color-page; display: flex; flex-direction: column; }
|
|
|
+
|
|
|
+.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-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-section { padding: 0 24rpx; flex: 1; height: 0; overflow-y: auto; }
|
|
|
+.section-head { display: flex; align-items: center; justify-content: space-between; padding: $spacing-3 0 $spacing-2; }
|
|
|
+.section-title { font-size: $font-size-md; font-weight: 700; color: $text-color-primary; }
|
|
|
+.section-count { font-size: $font-size-sm; color: $text-color-muted; }
|
|
|
+
|
|
|
+.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-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); }
|
|
|
+.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; }
|
|
|
+}
|
|
|
+.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; }
|
|
|
+}
|
|
|
+
|
|
|
+.item-stock { display: flex; gap: 16rpx; margin-bottom: 12rpx; padding: 12rpx 0; background: $bg-color-page; border-radius: $radius-sm; }
|
|
|
+.stock-col { flex: 1; display: flex; flex-direction: column; align-items: center; }
|
|
|
+.stock-label { font-size: 22rpx; color: $text-color-muted; margin-bottom: 4rpx; }
|
|
|
+.stock-num { font-size: $font-size-lg; font-weight: 700; color: $text-color-primary; }
|
|
|
+
|
|
|
+.audit-row { display: flex; align-items: center; background: $bg-color-page; border-radius: $radius-base; padding: 12rpx 16rpx; }
|
|
|
+.audit-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; }
|
|
|
+.quick-btn { margin-left: 12rpx; padding: 10rpx 20rpx; background: $bg-color-secondary; border-radius: $radius-sm;
|
|
|
+ text { font-size: 22rpx; color: $text-color-secondary; }
|
|
|
+ &:active { opacity: 0.7; }
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-bar { display: flex; align-items: center; gap: 16rpx; 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>
|