gaoluyang
2 天以前 bbc93d7a5c22bf006502f2515e2200cdfe8f6a62
双奇点app:
1.扫码改成可连续扫码,加一个导出excel功能
已添加1个文件
已修改4个文件
4482 ■■■■ 文件已修改
package.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/receiptManagement.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/components/ScanListPopup.vue 625 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yarn.lock 3832 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -79,7 +79,8 @@
    "tslib": "^2.7.0",
    "uview-plus": "^3.4.62",
    "vue": "3.4.21",
    "vue-i18n": "^9.14.2"
    "vue-i18n": "^9.14.2",
    "xlsx": "^0.18.5"
  },
  "devDependencies": {
    "@dcloudio/types": "^3.4.14",
src/api/inventoryManagement/receiptManagement.js
@@ -97,3 +97,13 @@
  });
}
// å¯¼å‡ºæ‰«ç åˆ—表
export function exportScanList(query) {
  return request({
    url: "/stockin/exportTwo",
    method: "post",
    data: query,
    responseType: "blob",
  });
}
src/pages/components/ScanListPopup.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,625 @@
<template>
    <uni-popup
        ref="popupRef"
        type="center"
        :round="20"
        @close="handleClose"
        @open="handleOpen"
    >
        <view class="scan-list-modal">
            <!-- å¤´éƒ¨ -->
            <view class="modal-header">
                <text class="modal-title">扫码列表 ({{ scanList.length }})</text>
                <view class="close-btn" @click="handleClose">
                    <up-icon name="close" size="20" color="#999"></up-icon>
                </view>
            </view>
            <!-- æ“ä½œæŒ‰é’® -->
            <view class="modal-actions">
                <up-button
                    type="primary"
                    size="small"
                    @click="startScan"
                    :loading="isScanning"
                >
                    {{ isScanning ? '扫码中...' : '继续扫码' }}
                </up-button>
                <up-button
                    type="success"
                    size="small"
                    @click="exportToCSV"
                >
                    å¯¼å‡ºè¡¨æ ¼
                </up-button>
                <up-button
                    type="error"
                    size="small"
                    @click="clearList"
                    :disabled="scanList.length === 0"
                >
                    æ¸…空列表
                </up-button>
            </view>
            <!-- åˆ—表内容 -->
            <view class="modal-content">
                <view v-if="scanList.length === 0" class="empty-state">
                    <text class="empty-text">暂无扫码记录</text>
                    <text class="empty-hint">点击"继续扫码"开始扫码</text>
                </view>
                <scroll-view v-else scroll-y class="list-container">
                    <view
                        v-for="(item, index) in scanList"
                        :key="index"
                        class="list-item"
                    >
                        <view class="item-header">
                            <text class="item-index">#{{ index + 1 }}</text>
                            <text class="item-time">{{ item.scanTime }}</text>
                            <view class="item-delete" @click="deleteItem(index)">
                                <up-icon name="trash" size="16" color="#f56c6c"></up-icon>
                            </view>
                        </view>
                        <view class="item-content">
                            <!-- äº§å“å›¾ç‰‡ -->
                            <view v-if="item.type == 2 && item.url" class="item-image-row">
                                <text class="item-label">产品图片:</text>
                                <image :src="baseUrl + item.url" class="product-image" mode="aspectFit"></image>
                            </view>
                            <view class="item-row">
                                <text class="item-label">产品名称:</text>
                                <text class="item-value">{{ item.productCategory || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">产品高度:</text>
                                <text class="item-value">{{ item.specificationModelUnit || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">单价(美元)/件:</text>
                                <text class="item-value">{{ item.taxInclusiveUnitPrice || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">入库数量/件:</text>
                                <text class="item-value">{{ item.inboundNum || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">每件数量/支:</text>
                                <text class="item-value">{{ item.boxNum || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">纸箱规格:</text>
                                <text class="item-value">{{ item.cartonSpecifications || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">入库人:</text>
                                <text class="item-value">{{ item.createBy || '-' }}</text>
                            </view>
                            <view class="item-row">
                                <text class="item-label">入库时间:</text>
                                <text class="item-value">{{ item.inboundDate || '-' }}</text>
                            </view>
                        </view>
                    </view>
                </scroll-view>
            </view>
        </view>
    </uni-popup>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { stockinDetail, detailManagementByCustom, exportScanList } from '@/api/inventoryManagement/receiptManagement'
import config from '@/config'
import { getToken } from '@/utils/auth'
import request from '@/utils/request'
const baseUrl = config.imgUrl
const emit = defineEmits(['scan', 'close']);
// å¼¹çª—显示状态
const popupRef = ref();
const isScanning = ref(false);
// æ‰«ç åˆ—表数据
const scanList = reactive([]);
// æ‰“开弹窗
const open = async () => {
    popupRef.value.open();
    // æ‰“开后自动开始扫码
    startScan();
};
// å¼€å§‹æ‰«ç 
const startScan = () => {
    isScanning.value = true;
    uni.scanCode({
        onlyFromCamera: true,
        scanType: ['barCode', 'qrCode'],
        success: async (res) => {
            isScanning.value = false;
            console.log('扫码结果:', res);
            await handleScanResult(res.result || '');
        },
        fail: (res) => {
            isScanning.value = false;
            if (res.errMsg && !res.errMsg.includes('cancel')) {
                uni.showToast({
                    title: '扫码失败',
                    icon: 'none',
                    duration: 1500
                });
            }
        }
    });
};
// å¤„理扫码结果
const handleScanResult = async (barcode) => {
    if (!barcode || barcode.indexOf(',') === -1) {
        uni.showToast({
            title: "请扫描正确的二维码",
            icon: 'none',
            duration: 1500
        });
        return;
    }
    let barcodeList = barcode.split(",");
    let barcodeId = barcodeList[0];
    let type = barcodeList[1];
    let detailApi = null;
    if (type == 1) {
        detailApi = stockinDetail;
    } else if (type == 2) {
        detailApi = detailManagementByCustom;
    }
    if (!detailApi) {
        uni.showToast({
            title: "请扫描正确的二维码",
            icon: 'none',
            duration: 1500
        });
        return;
    }
    // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨
    const existingIndex = scanList.findIndex(item => item.barcodeId === barcodeId && item.type === type);
    if (existingIndex !== -1) {
        uni.showToast({
            title: "该产品已扫码,请勿重复",
            icon: 'none',
            duration: 1500
        });
        return;
    }
    try {
        uni.showLoading({
            title: '获取产品信息中...',
            mask: true
        });
        const resp = await detailApi({ id: barcodeId });
        uni.hideLoading();
        if (resp.code != 200) {
            uni.showToast({
                title: resp.msg,
                icon: "none",
                duration: 1500
            });
            return;
        }
        if (!resp.data) {
            uni.showToast({
                title: '商品不存在',
                icon: "none",
                duration: 1500
            });
            return;
        }
        // æ·»åŠ åˆ°åˆ—è¡¨
        const scanTime = new Date().toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        });
        scanList.push({
            barcodeId: barcodeId,
            type: type,
            scanTime: scanTime,
            ...resp.data,
            specificationModelUnit: (resp.data.specificationModel || '') + (resp.data.unit || '')
        });
        uni.showToast({
            title: '扫码成功',
            icon: 'success',
            duration: 1500
        });
        // è§¦å‘扫码事件
        emit('scan', resp.data);
    } catch (error) {
        uni.hideLoading();
        uni.showToast({
            title: "获取数据失败",
            icon: 'none',
            duration: 1500
        });
    }
};
// åˆ é™¤åˆ—表项
const deleteItem = (index) => {
    uni.showModal({
        title: '提示',
        content: '确定要删除这条记录吗?',
        success: (res) => {
            if (res.confirm) {
                scanList.splice(index, 1);
                uni.showToast({
                    title: '已删除',
                    icon: 'success',
                    duration: 1000
                });
            }
        }
    });
};
// æ¸…空列表
const clearList = () => {
    uni.showModal({
        title: '提示',
        content: '确定要清空所有记录吗?',
        success: (res) => {
            if (res.confirm) {
                scanList.splice(0, scanList.length);
                uni.showToast({
                    title: '已清空',
                    icon: 'success',
                    duration: 1000
                });
            }
        }
    });
};
// å¯¼å‡ºExcel(使用 uni.downloadFile + uni.saveFile)
const exportToCSV = async () => {
    try {
        if (scanList.length === 0) {
            uni.showToast({
                title: '列表为空,无法导出',
                icon: 'none',
                duration: 1500
            });
            return;
        }
        uni.showLoading({
            title: '导出中...',
            mask: true
        });
        // å‡†å¤‡å¯¼å‡ºå‚æ•° - ä¼ é€’完整的扫码数据数组
        const exportData = scanList.map(item => ({
            barcodeId: item.barcodeId,
            type: item.type,
            scanTime: item.scanTime,
            productCategory: item.productCategory,
            specificationModel: item.specificationModel,
            unit: item.unit,
            specificationModelUnit: item.specificationModelUnit,
            taxInclusiveUnitPrice: item.taxInclusiveUnitPrice,
            inboundNum: item.inboundNum,
            boxNum: item.boxNum,
            cartonSpecifications: item.cartonSpecifications,
            createBy: item.createBy,
            inboundDate: item.inboundDate,
            url: item.url
        }));
        const fileName = `产品扫码记录_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_${Date.now()}.xlsx`;
        // 1. è°ƒç”¨åŽç«¯å¯¼å‡ºæŽ¥å£ï¼Œä½¿ç”¨å°è£…好的 request,拿到 msg(文件相对路径或文件名)
        // æ³¨æ„ï¼šè¿™é‡Œå‡è®¾åŽç«¯è¿”回 { code: 200, msg: 'xxx.xlsx' } ç»“æž„
        // â˜… è¿™é‡Œå¿…须走业务后端 baseUrl,不要用文件服务器 imgUrl,否则会被 nginx æ‹’绝(405)
        const apiBaseUrl = config.baseUrl.replace(/\/+$/, '');
        const resp = await request({
            url: '/stockin/exportTwoSave',
            method: 'POST',
            data: exportData,
            baseUrl: apiBaseUrl
        });
        const code = resp.code;
        const msg = resp.msg;
        console.log('exportTwoSave è¿”回:', resp);
        if (code !== 200) {
            uni.hideLoading();
            uni.showToast({
                title: msg || '导出失败',
                icon: 'none',
                duration: 2000
            });
            return;
        }
        // 2. ç”¨æ–‡ä»¶æœåŠ¡å‰ç¼€ + msg æ‹¼æˆå®Œæ•´ä¸‹è½½åœ°å€
        // åŽç«¯è¯´æ˜Žï¼šéœ€è¦ç”¨ http://114.132.189.42:9044/javaWork/product-inventory-management/file/prod/ å’Œ msg æ‹¼èµ·æ¥
        const fileServerBase = 'http://114.132.189.42:9044/javaWork/product-inventory-management/file/prod/';
        if (!msg) {
            uni.hideLoading();
            uni.showToast({
                title: '导出失败,返回的文件名为空',
                icon: 'none',
                duration: 2000
            });
            return;
        }
        const downloadUrl = encodeURI(fileServerBase.replace(/\/+$/, '/') + msg);
        console.log('最终下载地址:', downloadUrl);
        // 3. ä½¿ç”¨ downloadFile + saveFile ä¸‹è½½å¹¶æŒä¹…化,再打开
        uni.downloadFile({
            url: downloadUrl,
            success: (downloadRes) => {
                console.log('downloadFile statusCode:', downloadRes.statusCode);
                if (downloadRes.statusCode !== 200) {
                    uni.hideLoading();
                    uni.showToast({
                        title: `下载失败,状态码: ${downloadRes.statusCode}`,
                        icon: 'none',
                        duration: 2000
                    });
                    return;
                }
                uni.saveFile({
                    tempFilePath: downloadRes.tempFilePath,
                    success: (saveRes) => {
                        uni.hideLoading();
                        const savedPath = saveRes.savedFilePath;
                        uni.showToast({
                            title: 'Excel已下载',
                            icon: 'success',
                            duration: 3000
                        });
                        setTimeout(() => {
                            uni.openDocument({
                                filePath: savedPath,
                                success: () => console.log('文件打开成功'),
                                fail: (err) => console.log('文件已保存,路径:', savedPath, err)
                            });
                        }, 300);
                    },
                    fail: (err) => {
                        uni.hideLoading();
                        console.error('保存文件失败:', err);
                        uni.showToast({
                            title: `保存文件失败: ${err.errMsg || '未知错误'}`,
                            icon: 'none',
                            duration: 3000
                        });
                    }
                });
            },
            fail: (err) => {
                uni.hideLoading();
                console.error('下载失败:', err);
                uni.showToast({
                    title: `下载失败: ${err.errMsg || '未知错误'}`,
                    icon: 'none',
                    duration: 3000
                });
            }
        });
    } catch (error) {
        uni.hideLoading();
        console.error('导出失败:', error);
        uni.showToast({
            title: error.message || '导出失败,请重试',
            icon: 'none',
            duration: 2000
        });
    }
};
// å¼¹çª—打开事件
const handleOpen = () => {
    // å¼¹çª—打开时的处理
};
// å…³é—­å¼¹çª—
const handleClose = () => {
    popupRef.value.close();
    emit('close');
};
// æš´éœ²æ–¹æ³•
defineExpose({
    open,
    scanList
});
</script>
<style scoped lang="scss">
.scan-list-modal {
    background: #ffffff;
    border-radius: 20px;
    max-height: 80vh;
    width: 90vw;
    max-width: 600px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
}
.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px 20px 16px 20px;
    border-bottom: 1px solid #f0f0f0;
}
.modal-title {
    font-size: 18px;
    font-weight: 600;
    color: #333;
}
.close-btn {
    padding: 4px;
    &:active {
        opacity: 0.7;
    }
}
.modal-actions {
    display: flex;
    gap: 10px;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    flex-wrap: wrap;
}
.modal-content {
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
.empty-state {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 60px 20px;
    color: #999;
}
.empty-text {
    margin-top: 16px;
    font-size: 16px;
    color: #999;
}
.empty-hint {
    margin-top: 8px;
    font-size: 14px;
    color: #ccc;
}
.list-container {
    flex: 1;
    padding: 12px 20px;
    max-height: 50vh;
}
.list-item {
    background: #f8f9fa;
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 12px;
    border: 1px solid #e9ecef;
    &:last-child {
        margin-bottom: 0;
    }
}
.item-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    padding-bottom: 12px;
    border-bottom: 1px solid #e9ecef;
}
.item-index {
    font-size: 14px;
    font-weight: 600;
    color: #2979ff;
}
.item-time {
    font-size: 12px;
    color: #999;
    flex: 1;
    margin-left: 12px;
}
.item-delete {
    padding: 4px;
    cursor: pointer;
    &:active {
        opacity: 0.7;
    }
}
.item-content {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.item-row {
    display: flex;
    align-items: flex-start;
    font-size: 14px;
}
.item-label {
    color: #666;
    min-width: 100px;
    flex-shrink: 0;
}
.item-value {
    color: #333;
    flex: 1;
    word-break: break-all;
}
.item-image-row {
    display: flex;
    align-items: flex-start;
    margin-bottom: 8px;
    flex-direction: column;
}
.product-image {
    width: 200rpx;
    height: 200rpx;
    border-radius: 8rpx;
    background-color: #f5f5f5;
    margin-top: 8px;
    border: 1px solid #e9ecef;
}
</style>
src/pages/index.vue
@@ -46,6 +46,7 @@
            </view>
        </view>
        <GoodsDetailPopup ref="refGoodsDetailPopup"></GoodsDetailPopup>
        <ScanListPopup ref="refScanListPopup"></ScanListPopup>
    </view>
</template>
@@ -56,6 +57,7 @@
import modal from "@/plugins/modal";
import useUserStore from "@/store/modules/user";
import GoodsDetailPopup from './components/GoodsDetailPopup.vue';
import ScanListPopup from './components/ScanListPopup.vue';
const userStore = useUserStore()
const factoryId = ref('');
@@ -402,7 +404,7 @@
            });
            break
        case '产品扫码':
            scanQRCode()
            openScanListPopup()
            break
        default:
            uni.showToast({
@@ -480,6 +482,14 @@
//谈框相关
const refGoodsDetailPopup = ref(null)
const refScanListPopup = ref(null)
//打开扫码列表弹窗
const openScanListPopup = () => {
    if (refScanListPopup.value) {
        refScanListPopup.value.open()
    }
}
//查看详情
yarn.lock
ÎļþÌ«´ó