Compare commits

8 Commits

20 changed files with 198 additions and 172 deletions

Binary file not shown.

0
deploy_code.sh Executable file → Normal file
View File

0
deploy_full.sh Executable file → Normal file
View File

View File

@ -309,8 +309,13 @@ def delete_buy(id):
try: try:
material_name = BuyInboundService.delete_inbound(id) material_name = BuyInboundService.delete_inbound(id)
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name}) return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
except ValueError as ve:
# 捕获业务拦截的报错,返回友好的 msg
return jsonify({"code": 400, "msg": str(ve)})
except Exception as e: except Exception as e:
return jsonify({"code": 500, "msg": str(e)}), 500 import traceback
traceback.print_exc() # 在控制台打印真实错误堆栈
return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View File

@ -185,9 +185,12 @@ def delete(id):
try: try:
material_name = ProductInboundService.delete_inbound(id) material_name = ProductInboundService.delete_inbound(id)
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name}) return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
except ValueError as ve:
return jsonify({"code": 400, "msg": str(ve)})
except Exception as e: except Exception as e:
import traceback
traceback.print_exc() traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500 return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
@inbound_product_bp.route('/<int:id>/history', methods=['GET']) @inbound_product_bp.route('/<int:id>/history', methods=['GET'])
@permission_required('inbound_product') @permission_required('inbound_product')

View File

@ -180,9 +180,12 @@ def delete_semi(id):
try: try:
material_name = SemiInboundService.delete_inbound(id) material_name = SemiInboundService.delete_inbound(id)
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name}) return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
except ValueError as ve:
return jsonify({"code": 400, "msg": str(ve)})
except Exception as e: except Exception as e:
import traceback
traceback.print_exc() traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500 return jsonify({"code": 500, "msg": f"服务器内部错误详情: {str(e)}"}), 500
@inbound_semi_bp.route('/<int:id>/history', methods=['GET']) @inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
@permission_required('inbound_semi') @permission_required('inbound_semi')

View File

@ -39,7 +39,7 @@ class SysUser(db.Model):
前端需要的是 '张三(zhangsan)' 前端需要的是 '张三(zhangsan)'
""" """
raw_name = self.username raw_name = self.username
display_name = raw_name real_name = ''
account_id = raw_name account_id = raw_name
# 解析存储格式: Name/ID # 解析存储格式: Name/ID
@ -51,11 +51,15 @@ class SysUser(db.Model):
display_name = f"{real_name}({acc_id})" display_name = f"{real_name}({acc_id})"
# 单独提取账号ID (如果前端需要单独用) # 单独提取账号ID (如果前端需要单独用)
account_id = acc_id account_id = acc_id
else:
display_name = raw_name
return { return {
'id': self.id, 'id': self.id,
'username': display_name, # 列表显示: 张三(zhangsan01) 'username': display_name, # 列表显示: 张三(zhangsan01)
'raw_username': self.username, # 原始数据 'raw_username': self.username, # 原始数据
'real_name': real_name, # 真实姓名: 张三
'display_name': display_name, # 显示名: 张三(zhangsan01)
'account_id': account_id, # 纯账号ID: zhangsan01 'account_id': account_id, # 纯账号ID: zhangsan01
'email': self.email, 'email': self.email,
'department': self.department, 'department': self.department,

View File

@ -124,7 +124,8 @@ class AuthService:
identity=user_id, identity=user_id,
additional_claims={ additional_claims={
'role': user_role, 'role': user_role,
'username': account_id 'username': account_id,
'display_name': user_info.get('display_name', account_id)
} }
) )
@ -153,11 +154,19 @@ class AuthService:
user_id = decoded.get('sub') user_id = decoded.get('sub')
role = decoded.get('role') role = decoded.get('role')
username = decoded.get('username') username = decoded.get('username')
display_name = decoded.get('display_name')
if not user_id: if not user_id:
raise ValueError("无效的 refresh_token") raise ValueError("无效的 refresh_token")
# 重新查询数据库获取用户的 display_name避免刷新后丢失
from app.models.system import SysUser
user = SysUser.query.get(user_id)
if user:
user_info = user.to_dict()
display_name = user_info.get('display_name', username)
else:
display_name = username
# 生成新的 access_token # 生成新的 access_token
new_access_token = create_access_token( new_access_token = create_access_token(
identity=user_id, identity=user_id,

View File

@ -4,6 +4,7 @@ from app.models.inbound.buy import StockBuy
from app.models.base import MaterialBase from app.models.base import MaterialBase
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
from sqlalchemy.exc import IntegrityError
import traceback import traceback
import json import json
@ -280,11 +281,14 @@ class BuyInboundService:
try: try:
stock = StockBuy.query.get(stock_id) stock = StockBuy.query.get(stock_id)
if not stock: raise ValueError("记录不存在") if not stock: raise ValueError("记录不存在")
# 提前获取物料名称用于审计日志 # 提前获取物料名称用于审计日志(通过外键关系 base.name 获取)
material_name = stock.material_name material_name = stock.base.name if stock.base else '未知物料'
db.session.delete(stock) db.session.delete(stock)
db.session.commit() db.session.commit()
return material_name return material_name
except IntegrityError:
db.session.rollback()
raise ValueError("该入库单已被出库、盘点或借还等业务关联,为保证账目完整,禁止删除!")
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
raise e raise e

View File

@ -4,6 +4,7 @@ from app.models.base import MaterialBase
from app.models.outbound import TransOutbound from app.models.outbound import TransOutbound
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
from sqlalchemy.exc import IntegrityError
import traceback import traceback
import json import json
@ -252,12 +253,15 @@ class ProductInboundService:
try: try:
stock = StockProduct.query.get(stock_id) stock = StockProduct.query.get(stock_id)
if stock: if stock:
# 提前获取物料名称用于审计日志 # 提前获取物料名称用于审计日志(通过外键关系 base.name 获取)
material_name = stock.material_name material_name = stock.base.name if stock.base else '未知物料'
db.session.delete(stock) db.session.delete(stock)
db.session.commit() db.session.commit()
return material_name return material_name
return None return None
except IntegrityError:
db.session.rollback()
raise ValueError("该入库单已被出库、盘点或借还等业务关联,为保证账目完整,禁止删除!")
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
raise e raise e

View File

@ -4,6 +4,7 @@ from app.models.base import MaterialBase
from app.models.outbound import TransOutbound from app.models.outbound import TransOutbound
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
from sqlalchemy.exc import IntegrityError
import traceback import traceback
import json import json
@ -341,11 +342,14 @@ class SemiInboundService:
stock = StockSemi.query.get(stock_id) stock = StockSemi.query.get(stock_id)
if not stock: if not stock:
raise ValueError("记录不存在") raise ValueError("记录不存在")
# 提前获取物料名称用于审计日志 # 提前获取物料名称用于审计日志(通过外键关系 base.name 获取)
material_name = stock.material_name material_name = stock.base.name if stock.base else '未知物料'
db.session.delete(stock) db.session.delete(stock)
db.session.commit() db.session.commit()
return material_name return material_name
except IntegrityError:
db.session.rollback()
raise ValueError("该入库单已被出库、盘点或借还等业务关联,为保证账目完整,禁止删除!")
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
raise e raise e

View File

@ -1,6 +1,7 @@
from app.models.system import SysMenu, SysElement, SysRolePermission from app.models.system import SysMenu, SysElement, SysRolePermission
from app.extensions import db from app.extensions import db
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import func
class PermissionService: class PermissionService:

View File

@ -205,6 +205,17 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
username = claims.get('username', '') username = claims.get('username', '')
display_name = claims.get('display_name', '') display_name = claims.get('display_name', '')
# 兜底:如果 display_name 为空,查询数据库获取
if not display_name and user_id:
try:
from app.models.system import SysUser
user = SysUser.query.get(user_id)
if user:
user_info = user.to_dict()
display_name = user_info.get('display_name', username)
except Exception:
pass
# 获取IP # 获取IP
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or '' ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
if ip_address and ',' in ip_address: if ip_address and ',' in ip_address:

View File

@ -176,7 +176,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer"> <footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag"> <span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon> <el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.5(3.24BOM表修改版 当前版本:V3.6(3.25审计导致的入库修改错误
</span> </span>
</footer> </footer>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div v-if="userStore.hasPermission('scrap_list:view')" class="app-container">
<div class="filter-container"> <div class="filter-container">
<el-input v-model="sku" placeholder="SKU" style="width: 200px" @keyup.enter="fetchData" /> <el-input v-model="sku" placeholder="SKU" style="width: 200px" @keyup.enter="fetchData" />
<el-date-picker <el-date-picker
@ -66,6 +66,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { getScrapRecords } from '@/api/scrap' import { getScrapRecords } from '@/api/scrap'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const list = ref<any[]>([]) const list = ref<any[]>([])
const total = ref(0) const total = ref(0)

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div v-if="userStore.hasPermission('outbound_list:view')" class="app-container">
<div class="filter-container"> <div class="filter-container">
<el-input <el-input
v-model="listQuery.keyword" v-model="listQuery.keyword"

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div v-if="userStore.hasPermission('stock_adjustment:view')" class="app-container">
<!-- 筛选条件 --> <!-- 筛选条件 -->
<div class="filter-container"> <div class="filter-container">
<el-input v-model="keyword" placeholder="搜索单号/SKU/物料名称" style="width: 220px" @keyup.enter="fetchData" clearable /> <el-input v-model="keyword" placeholder="搜索单号/SKU/物料名称" style="width: 220px" @keyup.enter="fetchData" clearable />

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div v-if="userStore.hasPermission('inbound_summary:view')" class="app-container">
<div class="filter-container"> <div class="filter-container">
<el-input <el-input
v-model="listQuery.keyword" v-model="listQuery.keyword"
@ -101,6 +101,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { getInboundSummaryList } from '@/api/inbound/inbound_summary' import { getInboundSummaryList } from '@/api/inbound/inbound_summary'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const list = ref([]) const list = ref([])
const total = ref(0) const total = ref(0)

View File

@ -386,7 +386,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { getAllStock } from '@/api/inbound/stock' import { getStockList } from '@/api/inbound/stock'
import QrScanner from '@/components/QrScanner/index.vue' import QrScanner from '@/components/QrScanner/index.vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue' import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
@ -456,7 +456,8 @@ let countdownTimer: any = null
// ★ 新增: 防呆确认弹窗显示状态 // ★ 新增: 防呆确认弹窗显示状态
const showConfirmDialog = ref(false) const showConfirmDialog = ref(false)
const allData = ref<StockItem[]>([]) // ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★
const tableData = ref<StockItem[]>([])
const scannedMap = ref<Map<string, number>>(new Map()) const scannedMap = ref<Map<string, number>>(new Map())
const borrowedQuantities = ref<Record<string, number>>({}) const borrowedQuantities = ref<Record<string, number>>({})
@ -465,63 +466,11 @@ const currentSessionId = ref<string>('')
const varianceLoading = ref(false) const varianceLoading = ref(false)
const filterType = ref('all')
const searchKeyword = ref('')
const currentItem = ref<StockItem | null>(null) const currentItem = ref<StockItem | null>(null)
const inputQty = ref<number | undefined>(undefined) const inputQty = ref<number | undefined>(undefined)
const inputRemark = ref('') const inputRemark = ref('')
const qtyInputRef = ref() const qtyInputRef = ref()
// ★ 新增: 静默刷新数据不弹loading
const syncData = async () => {
try {
// 仅刷新差异列表数据不显示loading
const res = await getAllStock()
if (!res) return
const processItem = (item: any, type: string) => {
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
if (stock <= 0) return
const name = item.material_name || item.product_name || item.name || '未知物品'
const uuid = item.uuid || item.sku || ''
const isScanned = scannedMap.value.has(uuid)
return {
...item,
name: name,
standard: item.spec_model || item.standard || item.model || '',
sku: item.sku || '',
uuid: uuid,
bar_code: item.bar_code || item.barcode || '',
qty_stock: stock,
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
scanned: isScanned,
uniqueKey: `${type}_${item.id}`,
source_table: typeToSourceTable(type),
stock_id: item.id
}
}
const list: StockItem[] = []
if (res.materials) res.materials.forEach((i: any) => { const item = processItem(i, 'material'); if (item) list.push(item) })
if (res.semis) res.semis.forEach((i: any) => { const item = processItem(i, 'semi'); if (item) list.push(item) })
if (res.products) res.products.forEach((i: any) => { const item = processItem(i, 'product'); if (item) list.push(item) })
// ★ 强制按 SKU 数字+字符串升序排序
list.sort((a, b) => {
const skuA = a.sku || '';
const skuB = b.sku || '';
return skuA.localeCompare(skuB, undefined, { numeric: true, sensitivity: 'base' });
});
// 静默更新数据不触发loading
allData.value = list
await fetchBorrowedQuantities(list)
} catch (e) {
// 静默失败,不弹错误
}
}
const api = { const api = {
getDrafts: (sessionId?: string) => request({ getDrafts: (sessionId?: string) => request({
url: '/v1/inbound/stock/draft/list', url: '/v1/inbound/stock/draft/list',
@ -632,7 +581,7 @@ const doStartNewSession = async () => {
const res: any = await api.startNewSession() const res: any = await api.startNewSession()
currentSessionId.value = res.session_id || '' currentSessionId.value = res.session_id || ''
scannedMap.value.clear() scannedMap.value.clear()
await loadData() tableData.value = [] // 清空已扫码列表
isSessionActive.value = true isSessionActive.value = true
// ★ 标记当前阶段为 scanning扫码中 // ★ 标记当前阶段为 scanning扫码中
localStorage.setItem('stocktake_phase', 'scanning') localStorage.setItem('stocktake_phase', 'scanning')
@ -690,8 +639,8 @@ const resumeSession = async () => {
}) })
scannedMap.value = map scannedMap.value = map
// 加载完整库存数据 // 清空本地列表,用户扫码时会实时添加
await loadData() tableData.value = []
// ★ 智能路由:根据本地记忆的阶段决定下一步 // ★ 智能路由:根据本地记忆的阶段决定下一步
const phase = localStorage.getItem('stocktake_phase') const phase = localStorage.getItem('stocktake_phase')
@ -724,59 +673,8 @@ const returnToScan = () => {
ElMessage.info('继续扫码,发现漏扫的物料') ElMessage.info('继续扫码,发现漏扫的物料')
} }
// ★★★ 核心修改:数据加载映射修复 ★★★ // ★★★ 核心修改:扫码成功后实时查询后端匹配 ★★★
const loadData = async () => { const onScanSuccess = async (code: string) => {
loading.value = true
try {
const res = await getAllStock()
const list: StockItem[] = []
const processItem = (item: any, type: string) => {
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
if (stock <= 0) return
const name = item.material_name || item.product_name || item.name || '未知物品'
const uuid = item.uuid || item.sku || ''
const isScanned = scannedMap.value.has(uuid)
list.push({
...item,
name: name,
// ★ 修复点:优先读取 spec_model因为数据库是这个字段
standard: item.spec_model || item.standard || item.model || '',
sku: item.sku || '',
batch_no: item.batch_no || item.batch_number || '',
serial_number: item.serial_number || '',
uuid: uuid,
bar_code: item.bar_code || item.barcode || '',
qty_stock: stock,
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
scanned: isScanned,
uniqueKey: `${type}_${item.id}`,
source_table: typeToSourceTable(type),
stock_id: item.id
})
}
if (res.materials) res.materials.forEach((i: any) => processItem(i, 'material'))
if (res.semis) res.semis.forEach((i: any) => processItem(i, 'semi'))
if (res.products) res.products.forEach((i: any) => processItem(i, 'product'))
// ★ 强制按 SKU 数字+字符串升序排序
list.sort((a, b) => {
const skuA = a.sku || '';
const skuB = b.sku || '';
return skuA.localeCompare(skuB, undefined, { numeric: true, sensitivity: 'base' });
});
allData.value = list
await fetchBorrowedQuantities(list)
} catch (e) {
ElMessage.error('数据加载失败')
} finally { loading.value = false }
}
const onScanSuccess = (code: string) => {
if (!code || loading.value) return if (!code || loading.value) return
const trimCode = code.trim() const trimCode = code.trim()
@ -790,19 +688,61 @@ const onScanSuccess = (code: string) => {
return return
} }
const item = allData.value.find(i => i.uuid === trimCode || i.bar_code === trimCode) // 实时查询后端匹配
loading.value = true
try {
const res: any = await getStockList({
page: 1,
pageSize: 10,
keyword: trimCode
})
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
ElMessage.error(`未找到该物料库存: ${trimCode}`)
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
return
}
// 查找匹配的物料
const foundItem = res.data.list.find((i: any) =>
i.uuid === trimCode || i.sku === trimCode || i.barcode === trimCode || i.bar_code === trimCode
)
if (!foundItem) {
ElMessage.error(`未找到该物料库存: ${trimCode}`)
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
return
}
if (item) {
if (navigator.vibrate) navigator.vibrate(100) if (navigator.vibrate) navigator.vibrate(100)
// ★★★ 核心修改:扫码成功后立即关闭全屏扫码,弹出填数对话框 // 关闭全屏扫码,弹出填数对话框
showCamera.value = false showCamera.value = false
// 无论是否多批次,都弹出对话框让用户确认数量 // 处理数据格式
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
const type = foundItem.stock_type || foundItem.type || 'material'
const item: StockItem = {
...foundItem,
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
sku: foundItem.sku || '',
uuid: foundItem.uuid || foundItem.sku || '',
bar_code: foundItem.bar_code || foundItem.barcode || '',
qty_stock: stock,
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0,
scanned: true,
uniqueKey: `${type}_${foundItem.id}`,
source_table: typeToSourceTable(type),
stock_id: foundItem.id
}
openQtyDialog(item) openQtyDialog(item)
} else { } catch (e) {
ElMessage.error(`不在库条码: ${trimCode}`) ElMessage.error('查询库存失败')
if (navigator.vibrate) navigator.vibrate([200, 50, 200]) console.error(e)
} finally {
loading.value = false
} }
} }
@ -820,21 +760,61 @@ const closeScanner = () => {
showCamera.value = false showCamera.value = false
} }
// 手动输入条码 // 手动输入条码 - 实时查询后端
const handleManualInput = async () => { const handleManualInput = async () => {
const code = barcodeInput.value.trim() const code = barcodeInput.value.trim()
if (!code) return if (!code) return
if (code.length < 3) {
ElMessage.warning('输入内容过短,请输入完整条码')
return
}
loading.value = true loading.value = true
try { try {
const item = allData.value.find(i => i.uuid === code || i.bar_code === code) const res: any = await getStockList({
page: 1,
pageSize: 10,
keyword: code
})
if (item) { if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
if (navigator.vibrate) navigator.vibrate(100) ElMessage.error(`未找到该物料库存: ${code}`)
openQtyDialog(item) return
} else {
ElMessage.error(`不在库条码: ${code}`)
} }
const foundItem = res.data.list.find((i: any) =>
i.uuid === code || i.sku === code || i.barcode === code || i.bar_code === code
)
if (!foundItem) {
ElMessage.error(`未找到该物料库存: ${code}`)
return
}
if (navigator.vibrate) navigator.vibrate(100)
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
const type = foundItem.stock_type || foundItem.type || 'material'
const item: StockItem = {
...foundItem,
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
sku: foundItem.sku || '',
uuid: foundItem.uuid || foundItem.sku || '',
bar_code: foundItem.bar_code || foundItem.barcode || '',
qty_stock: stock,
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0,
scanned: true,
uniqueKey: `${type}_${foundItem.id}`,
source_table: typeToSourceTable(type),
stock_id: foundItem.id
}
openQtyDialog(item)
} catch (e) {
ElMessage.error('查询库存失败')
console.error(e)
} finally { } finally {
loading.value = false loading.value = false
} }
@ -854,23 +834,25 @@ const openQtyDialog = (item: StockItem) => {
const handleManualConfirm = () => { const handleManualConfirm = () => {
if (!currentItem.value) return if (!currentItem.value) return
const val = inputQty.value === undefined ? 0 : inputQty.value const val = inputQty.value === undefined ? 0 : inputQty.value
const remark = inputRemark.value
// ★★★ 乐观更新:立即本地更新 UI不等待后端响应 ★★★ // ★★★ 更新已扫码物料列表 ★★★
currentItem.value.scanned = true currentItem.value.scanned = true
currentItem.value.qty_actual = val currentItem.value.qty_actual = val
scannedMap.value.set(currentItem.value.uuid, val) scannedMap.value.set(currentItem.value.uuid, val)
// 保存备注用于异步提交 // 检查是否已存在于 tableData如果存在则更新否则添加
const remark = inputRemark.value const existingIndex = tableData.value.findIndex(i => i.uuid === currentItem.value!.uuid)
if (existingIndex >= 0) {
tableData.value[existingIndex] = { ...currentItem.value }
} else {
tableData.value.push({ ...currentItem.value })
}
showQtyDialog.value = false showQtyDialog.value = false
// 重置备注
inputRemark.value = '' inputRemark.value = ''
ElMessage.success(`已记录实盘: ${val}`) ElMessage.success(`已记录实盘: ${val}`)
// ★★★ 取消自动弹摄像头,把开启摄像头的控制权完全交还给用户
// 用户需主动点击才能开启摄像头
// ★★★ 异步保存到后端,不阻塞 UIfire-and-forget★★★ // ★★★ 异步保存到后端,不阻塞 UIfire-and-forget★★★
syncToBackend(currentItem.value.uuid, val, remark) syncToBackend(currentItem.value.uuid, val, remark)
} }
@ -888,10 +870,17 @@ const syncToBackend = (uuid: string, quantity: number, remark: string) => {
} }
const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => { const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => {
// ★★★ 保留原有逻辑用于兼容性,但不再使用 ★★★
item.scanned = true item.scanned = true
item.qty_actual = quantity item.qty_actual = quantity
scannedMap.value.set(item.uuid, quantity) scannedMap.value.set(item.uuid, quantity)
// 更新 tableData
const existingIndex = tableData.value.findIndex(i => i.uuid === item.uuid)
if (existingIndex >= 0) {
tableData.value[existingIndex] = { ...item }
} else {
tableData.value.push({ ...item })
}
} }
const closeOverlays = () => { const closeOverlays = () => {
@ -935,34 +924,17 @@ const exportToExcel = async () => {
} }
} }
const filteredList = computed(() => {
let result = allData.value
if (filterType.value === 'scanned') result = result.filter(i => i.scanned)
else if (filterType.value === 'missing') result = result.filter(i => !i.scanned)
if (searchKeyword.value) {
const kw = searchKeyword.value.toLowerCase()
result = result.filter(i =>
i.name.toLowerCase().includes(kw) ||
i.uuid.includes(kw) ||
(i.sku && i.sku.toLowerCase().includes(kw)) ||
(i.batch_no && i.batch_no.toLowerCase().includes(kw))
)
}
return result
})
const stats = computed(() => { const stats = computed(() => {
const total = allData.value.length const total = tableData.value.length
const scanned = allData.value.filter(i => i.scanned).length const scanned = tableData.value.filter(i => i.scanned).length
const varianceItems = allData.value.filter(i => const varianceItems = tableData.value.filter(i =>
!i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)) !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))
).length ).length
return { total, scanned, varianceItems } return { total, scanned, varianceItems }
}) })
const varianceList = computed(() => { const varianceList = computed(() => {
return allData.value return tableData.value
.filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))) .filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)))
.map(i => ({ .map(i => ({
...i, ...i,

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div v-if="userStore.hasPermission('transaction_records:view')" class="app-container">
<div class="filter-container"> <div class="filter-container">
<el-radio-group v-model="status" @change="fetchData" style="margin-right: 20px"> <el-radio-group v-model="status" @change="fetchData" style="margin-right: 20px">
<el-radio-button label="all">全部</el-radio-button> <el-radio-button label="all">全部</el-radio-button>