<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>
|