样品库存和出入库展示

besure_bit
ck-chenkang 1 month ago
parent 9a589d531a
commit ff0b41820e

@ -4,7 +4,9 @@ export interface BitWmsInboundReqVO {
productId: number productId: number
warehouseId: number warehouseId: number
areaId?: number areaId?: number
locationId?: number
count: number count: number
unitId: number
supplierId?: number supplierId?: number
remark?: string remark?: string
} }

@ -1,5 +1,12 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="92%" :fullscreen="false" top="4vh"> <Dialog
v-model="dialogVisible"
:title="dialogTitle"
width="96%"
:fullscreen="false"
:initial-fullscreen="true"
top="2vh"
>
<div class="hiprint-preview"> <div class="hiprint-preview">
<div class="hiprint-toolbar"> <div class="hiprint-toolbar">
<div class="hiprint-paper"> <div class="hiprint-paper">
@ -306,6 +313,10 @@ defineExpose({ open })
<style scoped lang="scss"> <style scoped lang="scss">
.hiprint-preview { .hiprint-preview {
width: 100%; width: 100%;
height: calc(100vh - 84px);
display: flex;
flex-direction: column;
min-height: 0;
} }
.hiprint-toolbar { .hiprint-toolbar {
@ -313,6 +324,7 @@ defineExpose({ open })
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 12px; margin-bottom: 12px;
flex-shrink: 0;
} }
.hiprint-paper { .hiprint-paper {
@ -348,7 +360,8 @@ defineExpose({ open })
} }
.hiprint-body { .hiprint-body {
height: 75vh; flex: 1;
min-height: 0;
display: grid; display: grid;
grid-template-columns: 220px 1fr 300px; grid-template-columns: 220px 1fr 300px;
gap: 12px; gap: 12px;

@ -202,7 +202,7 @@ const clear = () => {
<template> <template>
<div <div
:class="prefixCls" :class="prefixCls"
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px" class="fixed right-0 top-[45%] z-10 h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
@click="drawer = true" @click="drawer = true"
> >
<Icon color="#fff" icon="ep:setting" /> <Icon color="#fff" icon="ep:setting" />

@ -8,35 +8,153 @@
<script setup lang="ts"> <script setup lang="ts">
import HiprintPreviewDialog from '@/components/HiprintPreviewDialog/index.vue' import HiprintPreviewDialog from '@/components/HiprintPreviewDialog/index.vue'
import { ConfigApi } from '@/api/mes/printconfig'
import { defaultElementTypeProvider, hiwebSocket, hiprint } from 'vue-plugin-hiprint'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
title: string title: string
buttonText?: string buttonText?: string
businessScene?: string
printData?: Record<string, any> printData?: Record<string, any>
templateJson?: Record<string, any> templateJson?: Record<string, any> | string
loadLabel?: () => Promise<{ printData: Record<string, any>; templateJson?: Record<string, any> }> loadLabel?: () => Promise<{ printData: Record<string, any>; templateJson?: Record<string, any> | string }>
}>(), }>(),
{ {
buttonText: '打印' buttonText: '打印'
} }
) )
const message = useMessage()
const printDialogRef = ref() const printDialogRef = ref()
const loading = ref(false) const loading = ref(false)
let hiprintInited = false
const clientHostStorageKey = 'hiprint-client-host'
const defaultClientHost = 'http://192.168.2.58:17521'
const getHiwebSocket = () => {
return ((hiwebSocket as any) || (window as any).hiwebSocket) as any
}
const ensureHiprintInit = () => {
if (hiprintInited) {
return
}
hiprint.init({
host: localStorage.getItem(clientHostStorageKey) || defaultClientHost,
providers: [defaultElementTypeProvider()]
})
hiprintInited = true
}
const normalizeTemplateJson = (templateJson?: Record<string, any> | string) => {
if (!templateJson) {
return undefined
}
if (typeof templateJson !== 'string') {
return templateJson
}
try {
return JSON.parse(templateJson)
} catch (error) {
message.error('打印模板格式异常')
return undefined
}
}
const getPaperSize = (templateJson?: Record<string, any>) => {
const panel = templateJson?.panels?.[0]
if (panel?.width && panel?.height) {
return {
width: Number(panel.width),
height: Number(panel.height)
}
}
return { width: 80, height: 60 }
}
const ensureClientConnected = async () => {
ensureHiprintInit()
const socket = getHiwebSocket()
if (!socket) {
message.warning('打印客户端未初始化')
return false
}
if (socket.opened) {
return true
}
if (typeof socket.start !== 'function') {
message.warning('打印客户端未提供连接方法')
return false
}
return await new Promise<boolean>((resolve) => {
socket.stop?.()
socket.start((status: boolean) => {
if (!status) {
message.warning('未连接到本地打印客户端,请先确认 electron-hiprint 已启动')
}
resolve(Boolean(status))
})
})
}
const silentPrint = async (printData: Record<string, any>, templateJson?: Record<string, any>) => {
if (!templateJson) {
message.warning('未找到可用打印模板')
return
}
const pageRes = await ConfigApi.getConfigPage({
pageNo: 1,
pageSize: 100,
businessScenario: props.businessScene
})
const configs = (pageRes?.list || []).filter((item: any) => item.isEnabled !== false)
if (!configs.length) {
message.warning('未找到匹配业务场景「' + props.businessScene + '」的打印机配置')
return
}
const printerName = configs[0].systemPrinterName
if (!printerName) {
message.warning('匹配到的打印机配置未设置系统打印机')
return
}
const connected = await ensureClientConnected()
if (!connected) {
return
}
const hiprintTmpl = new hiprint.PrintTemplate({ template: templateJson })
hiprintTmpl.on('printSuccess', () => {
message.success('打印已发送到: ' + printerName)
})
hiprintTmpl.on('printError', (error: any) => {
message.error(error?.msg || error?.message || '打印失败')
})
try {
hiprintTmpl.print2(printData, {
printer: printerName,
title: props.title
})
} catch (error: any) {
message.error(error?.message || '打印失败')
}
}
const openPrint = async () => { const openPrint = async () => {
loading.value = true loading.value = true
try { try {
const label = props.loadLabel ? await props.loadLabel() : undefined const label = props.loadLabel ? await props.loadLabel() : undefined
const printData = label?.printData || props.printData || {} const printData = label?.printData || props.printData || {}
const templateJson = label?.templateJson || props.templateJson const templateJson = normalizeTemplateJson(label?.templateJson || props.templateJson)
if (props.businessScene) {
await silentPrint(printData, templateJson)
return
}
printDialogRef.value?.open({ printDialogRef.value?.open({
title: props.title, title: props.title,
printData, printData,
templateJson, templateJson,
withDefaultQrcodeLayout: !templateJson, withDefaultQrcodeLayout: !templateJson,
paperSize: { width: 80, height: 60 } paperSize: getPaperSize(templateJson)
}) })
} finally { } finally {
loading.value = false loading.value = false

@ -10,33 +10,38 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="供应商" prop="supplierId"> <el-form-item label="库位" prop="locationId">
<el-select v-model="formData.supplierId" placeholder="请选择供应商" filterable clearable class="!w-full"> <el-select
<el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /> v-model="formData.locationId"
placeholder="请选择库位"
filterable
clearable
class="!w-full"
@change="handleLocationChange"
>
<el-option
v-for="item in filteredLocationList"
:key="item.id"
:label="formatLocationLabel(item)"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="12">
<el-form-item label="仓库" prop="warehouseId"> <el-form-item label="数量" prop="count">
<el-select v-model="formData.warehouseId" placeholder="请选择仓库" filterable class="!w-full" @change="handleWarehouseChange"> <el-input-number v-model="formData.count" :min="1" :precision="0" class="!w-full" />
<el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="12">
<el-form-item label="库区" prop="areaId"> <el-form-item label="单位" prop="unitId">
<el-select v-model="formData.areaId" placeholder="请选择库区" filterable clearable class="!w-full"> <el-select v-model="formData.unitId" placeholder="请选择单位" filterable class="!w-full">
<el-option v-for="item in filteredAreaList" :key="item.id" :label="item.areaName" :value="item.id" /> <el-option v-for="item in sampleUnitOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8">
<el-form-item label="数量" prop="count">
<el-input-number v-model="formData.count" :min="1" :precision="0" class="!w-full" />
</el-form-item>
</el-col>
</el-row> </el-row>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" /> <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
@ -55,48 +60,117 @@
import { BitWmsApi } from '@/api/erp/bitwms' import { BitWmsApi } from '@/api/erp/bitwms'
import { WarehouseApi } from '@/api/erp/stock/warehouse' import { WarehouseApi } from '@/api/erp/stock/warehouse'
import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea' import { WarehouseAreaApi } from '@/api/erp/stock/warehousearea'
import { SupplierApi } from '@/api/erp/purchase/supplier' import { WarehouseLocationApi } from '@/api/erp/stock/warehouselocation'
defineOptions({ name: 'ErpBitWmsIn' }) defineOptions({ name: 'ErpBitWmsIn' })
const DEFAULT_WAREHOUSE_NAME = '样品仓'
const DEFAULT_AREA_NAME = '必硕三楼'
const message = useMessage() const message = useMessage()
const formRef = ref() const formRef = ref()
const submitLoading = ref(false) const submitLoading = ref(false)
const sampleList = ref<any[]>([]) const sampleList = ref<any[]>([])
const warehouseList = ref<any[]>([]) const warehouseList = ref<any[]>([])
const areaList = ref<any[]>([]) const areaList = ref<any[]>([])
const supplierList = ref<any[]>([]) const locationList = ref<any[]>([])
const formData = reactive<any>({ const formData = reactive<any>({
productId: undefined, productId: undefined,
warehouseId: undefined, warehouseId: undefined,
areaId: undefined, areaId: undefined,
locationId: undefined,
count: 1, count: 1,
supplierId: undefined, unitId: undefined,
remark: undefined remark: undefined
}) })
const formRules = reactive({ const formRules = reactive({
productId: [{ required: true, message: '请选择样品', trigger: 'change' }], productId: [{ required: true, message: '请选择样品', trigger: 'change' }],
warehouseId: [{ required: true, message: '请选择仓库', trigger: 'change' }], locationId: [{ required: true, message: '请选择库位', trigger: 'change' }],
count: [{ required: true, message: '请输入数量', trigger: 'blur' }] count: [{ required: true, message: '请输入数量', trigger: 'blur' }],
unitId: [{ required: true, message: '请选择单位', trigger: 'change' }]
}) })
const filteredAreaList = computed(() => { const selectedSample = computed(() => sampleList.value.find((item) => item.id === formData.productId))
if (!formData.warehouseId) return areaList.value const sampleUnitOptions = computed(() => {
return areaList.value.filter((item) => item.warehouseId === formData.warehouseId) const sample = selectedSample.value
if (!sample?.unitId) return []
return [{ id: sample.unitId, name: sample.unitName || '默认单位' }]
})
const filteredLocationList = computed(() => {
if (!formData.warehouseId || !formData.areaId) return []
return locationList.value.filter((item) => {
const matchedWarehouse = item.warehouseId === formData.warehouseId
const matchedArea = item.areaId === formData.areaId
return matchedWarehouse && matchedArea
})
}) })
const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}` const formatSampleLabel = (item: any) => `${item.barCode || item.code || '-'} / ${item.name || '-'}`
const handleWarehouseChange = () => { const formatLocationLabel = (item: any) => `${item.code || '-'} / ${item.name || '-'}`
formData.areaId = undefined const applyDefaultWarehouseArea = () => {
const warehouse = warehouseList.value.find((item) => item.name === DEFAULT_WAREHOUSE_NAME)
if (!warehouse) {
formData.warehouseId = undefined
formData.areaId = undefined
return
}
formData.warehouseId = warehouse.id
const area = areaList.value.find(
(item) => item.warehouseId === warehouse.id && item.areaName === DEFAULT_AREA_NAME
)
formData.areaId = area?.id
}
const handleLocationChange = (locationId?: number) => {
const location = locationList.value.find((item) => item.id === locationId)
if (!location) return
formData.warehouseId = location.warehouseId
formData.areaId = location.areaId
}
const handleSampleChange = () => {
formData.unitId = selectedSample.value?.unitId
}
const resetInnerFormData = () => {
formData.productId = undefined
formData.locationId = undefined
formData.count = 1
formData.unitId = undefined
formData.remark = undefined
applyDefaultWarehouseArea()
} }
const resetForm = () => { const resetForm = () => {
formRef.value?.resetFields() formRef.value?.resetFields()
formData.count = 1 resetInnerFormData()
} }
const buildSubmitData = () => ({
productId: formData.productId,
locationId: formData.locationId,
warehouseId: formData.warehouseId,
areaId: formData.areaId,
count: formData.count,
unitId: formData.unitId,
remark: formData.remark
})
watch(
() => formData.productId,
() => handleSampleChange()
)
watch(
() => [formData.warehouseId, formData.areaId],
() => {
if (
formData.locationId &&
!filteredLocationList.value.some((item) => item.id === formData.locationId)
) {
formData.locationId = undefined
}
}
)
const submitForm = async () => { const submitForm = async () => {
await formRef.value.validate() await formRef.value.validate()
submitLoading.value = true submitLoading.value = true
try { try {
await BitWmsApi.inbound(formData) await BitWmsApi.inbound(buildSubmitData())
message.success('入库成功') message.success('入库成功')
resetForm() resetForm()
} finally { } finally {
@ -105,15 +179,16 @@ const submitForm = async () => {
} }
onMounted(async () => { onMounted(async () => {
const [samples, warehouses, areas, suppliers] = await Promise.all([ const [samples, warehouses, areas, locations] = await Promise.all([
BitWmsApi.getSampleSimpleList(), BitWmsApi.getSampleSimpleList(),
WarehouseApi.getWarehouseSimpleList(), WarehouseApi.getWarehouseSimpleList(),
WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }), WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }),
SupplierApi.getSupplierSimpleList() WarehouseLocationApi.getWarehouseLocationPage({ pageNo: 1, pageSize: 100 })
]) ])
sampleList.value = samples || [] sampleList.value = samples || []
warehouseList.value = warehouses || [] warehouseList.value = warehouses || []
areaList.value = areas.list || [] areaList.value = areas.list || []
supplierList.value = suppliers || [] locationList.value = locations.list || []
resetInnerFormData()
}) })
</script> </script>

@ -138,7 +138,27 @@ const resetQuery = () => {
const openForm = async (type: string, id?: number) => { const openForm = async (type: string, id?: number) => {
formVisible.value = true formVisible.value = true
await nextTick() await nextTick()
formRef.value.open(type, id) formRef.value.open(type, id, type === 'create'
? {
defaultWarehouseName: '样品仓',
defaultAreaName: '必硕三楼',
defaultData: {
isCode: false,
allowProductMix: true,
allowBatchMix: true
},
hideAutoCodeSwitch: true,
hiddenFields: [
'areaSize',
'maxLoadWeight',
'positionX',
'positionY',
'positionZ',
'allowProductMix',
'allowBatchMix'
]
}
: undefined)
} }
const loadLocationLabel = async (id: number) => await BitWmsApi.getLocationLabel(id) const loadLocationLabel = async (id: number) => await BitWmsApi.getLocationLabel(id)

@ -1,23 +1,69 @@
<template> <template>
<ContentWrap> <ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto"> <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="auto"
>
<el-form-item label="样品" prop="productId"> <el-form-item label="样品" prop="productId">
<el-select v-model="queryParams.productId" placeholder="请选择样品" clearable filterable class="!w-240px"> <el-select
<el-option v-for="item in sampleList" :key="item.id" :label="formatSampleLabel(item)" :value="item.id" /> v-model="queryParams.productId"
placeholder="请选择样品"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in sampleList"
:key="item.id"
:label="formatSampleLabel(item)"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="仓库" prop="warehouseId"> <el-form-item label="仓库" prop="warehouseId">
<el-select v-model="queryParams.warehouseId" placeholder="请选择仓库" clearable filterable class="!w-220px" @change="handleWarehouseChange"> <el-select
<el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" /> v-model="queryParams.warehouseId"
placeholder="请选择仓库"
clearable
filterable
class="!w-220px"
@change="handleWarehouseChange"
>
<el-option
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="库区" prop="areaId"> <el-form-item label="库区" prop="areaId">
<el-select v-model="queryParams.areaId" placeholder="请选择库区" clearable filterable class="!w-220px"> <el-select
<el-option v-for="item in filteredAreaList" :key="item.id" :label="item.areaName" :value="item.id" /> v-model="queryParams.areaId"
placeholder="请选择库区"
clearable
filterable
class="!w-220px"
>
<el-option
v-for="item in filteredAreaList"
:key="item.id"
:label="item.areaName"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="业务单号" prop="bizNo"> <el-form-item label="业务单号" prop="bizNo">
<el-input v-model="queryParams.bizNo" placeholder="请输入业务单号" clearable class="!w-220px" @keyup.enter="handleQuery" /> <el-input
v-model="queryParams.bizNo"
placeholder="请输入业务单号"
clearable
class="!w-220px"
@keyup.enter="handleQuery"
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button> <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
@ -27,30 +73,51 @@
</ContentWrap> </ContentWrap>
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id"> <el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column label="业务单号" align="center" prop="bizNo" min-width="170" /> <el-table-column label="业务单号" align="center" prop="bizNo" min-width="170" />
<el-table-column label="方向" align="center" prop="bizDirection" width="100"> <el-table-column label="方向" align="center" prop="bizDirection" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.bizDirection" :type="getDirectionTagType(row.bizDirection)">{{ row.bizDirection }}</el-tag> <el-tag v-if="row.bizDirection" :type="getDirectionTagType(row.bizDirection)">{{
row.bizDirection
}}</el-tag>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="样品名称" align="left" prop="productName" min-width="180" /> <el-table-column label="样品名称" align="left" prop="productName" min-width="180" />
<el-table-column label="分类" align="center" prop="categoryName" min-width="120" /> <!-- <el-table-column label="分类" align="center" prop="categoryName" min-width="120" /> -->
<el-table-column label="仓库" align="center" prop="warehouseName" min-width="140" /> <el-table-column label="仓库" align="center" prop="warehouseName" min-width="140" />
<el-table-column label="库区" align="center" prop="areaName" min-width="120" /> <el-table-column label="库区" align="center" prop="areaName" min-width="120" />
<el-table-column label="变动数量" align="center" prop="count" min-width="120"> <el-table-column label="变动数量" align="center" prop="count" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<span :class="getDirectionClass(row.bizDirection)">{{ formatCount(row.count, row.unitName, true, row.bizDirection) }}</span> <span :class="getDirectionClass(row.bizDirection)">{{
formatCount(row.count, row.unitName, true, row.bizDirection)
}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="结存" align="center" prop="totalCount" min-width="120"> <el-table-column label="结存" align="center" prop="totalCount" min-width="120">
<template #default="{ row }">{{ formatCount(row.totalCount, row.unitName) }}</template> <template #default="{ row }">{{ formatCount(row.totalCount, row.unitName) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作人" align="center" prop="creatorName" min-width="110" /> <el-table-column label="操作人" align="center" prop="creatorName" min-width="110" />
<el-table-column label="操作时间" align="center" prop="recordTime" :formatter="dateFormatter" width="180" /> <el-table-column
label="操作时间"
align="center"
prop="recordTime"
:formatter="dateFormatter"
width="180"
/>
</el-table> </el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" /> <Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap> </ContentWrap>
</template> </template>
@ -78,7 +145,8 @@ const queryParams = reactive<any>({
areaId: undefined, areaId: undefined,
bizNo: undefined, bizNo: undefined,
isSample: true, isSample: true,
bizUnit: 'BIT' bizUnit: 'BIT',
bitWmsOnly: true
}) })
const filteredAreaList = computed(() => { const filteredAreaList = computed(() => {
if (!queryParams.warehouseId) return areaList.value if (!queryParams.warehouseId) return areaList.value
@ -103,7 +171,12 @@ const formatNumber = (value: number | string | undefined) => {
const num = Number(value) const num = Number(value)
return Number.isFinite(num) ? num.toLocaleString() : String(value) return Number.isFinite(num) ? num.toLocaleString() : String(value)
} }
const formatCount = (value: number | string | undefined, unit?: string, withSign = false, direction?: string) => { const formatCount = (
value: number | string | undefined,
unit?: string,
withSign = false,
direction?: string
) => {
const unitText = unit ? ` ${unit}` : '' const unitText = unit ? ` ${unit}` : ''
if (!withSign) return `${formatNumber(value)}${unitText}` if (!withSign) return `${formatNumber(value)}${unitText}`
const num = Number(value) const num = Number(value)
@ -114,7 +187,11 @@ const formatCount = (value: number | string | undefined, unit?: string, withSign
return `${formatNumber(value)}${unitText}` return `${formatNumber(value)}${unitText}`
} }
const buildQueryParams = () => const buildQueryParams = () =>
Object.fromEntries(Object.entries(queryParams).filter(([, value]) => value !== undefined && value !== null && value !== '')) Object.fromEntries(
Object.entries(queryParams).filter(
([, value]) => value !== undefined && value !== null && value !== ''
)
)
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
@ -136,6 +213,7 @@ const resetQuery = () => {
queryFormRef.value.resetFields() queryFormRef.value.resetFields()
queryParams.isSample = true queryParams.isSample = true
queryParams.bizUnit = 'BIT' queryParams.bizUnit = 'BIT'
queryParams.bitWmsOnly = true
handleQuery() handleQuery()
} }

@ -1,19 +1,59 @@
<template> <template>
<ContentWrap> <ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto"> <el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="auto"
>
<el-form-item label="样品" prop="productId"> <el-form-item label="样品" prop="productId">
<el-select v-model="queryParams.productId" placeholder="请选择样品" clearable filterable class="!w-240px"> <el-select
<el-option v-for="item in sampleList" :key="item.id" :label="formatSampleLabel(item)" :value="item.id" /> v-model="queryParams.productId"
placeholder="请选择样品"
clearable
filterable
class="!w-240px"
>
<el-option
v-for="item in sampleList"
:key="item.id"
:label="formatSampleLabel(item)"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="仓库" prop="warehouseId"> <el-form-item label="仓库" prop="warehouseId">
<el-select v-model="queryParams.warehouseId" placeholder="请选择仓库" clearable filterable class="!w-220px" @change="handleWarehouseChange"> <el-select
<el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" /> v-model="queryParams.warehouseId"
placeholder="请选择仓库"
clearable
filterable
class="!w-220px"
@change="handleWarehouseChange"
>
<el-option
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="库区" prop="areaId"> <el-form-item label="库区" prop="areaId">
<el-select v-model="queryParams.areaId" placeholder="请选择库区" clearable filterable class="!w-220px"> <el-select
<el-option v-for="item in filteredAreaList" :key="item.id" :label="item.areaName" :value="item.id" /> v-model="queryParams.areaId"
placeholder="请选择库区"
clearable
filterable
class="!w-220px"
>
<el-option
v-for="item in filteredAreaList"
:key="item.id"
:label="item.areaName"
:value="item.id"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -24,23 +64,34 @@
</ContentWrap> </ContentWrap>
<ContentWrap> <ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id"> <el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column label="样品码" align="center" prop="barCode" min-width="150" /> <el-table-column label="样品码" align="center" prop="barCode" min-width="150" />
<el-table-column label="样品名称" align="left" min-width="180"> <el-table-column label="样品名称" align="left" min-width="180">
<template #default="{ row }">{{ row.name || row.productName || '-' }}</template> <template #default="{ row }">{{ row.name || row.productName || '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column label="样品分类" align="center" prop="categoryName" min-width="130" /> <!-- <el-table-column label="样品分类" align="center" prop="categoryName" min-width="130" /> -->
<el-table-column label="仓库" align="center" prop="warehouseName" min-width="140" /> <el-table-column label="仓库" align="center" prop="warehouseName" min-width="140" />
<el-table-column label="库区" align="center" prop="areaName" min-width="120" /> <el-table-column label="库区" align="center" prop="areaName" min-width="120" />
<el-table-column label="库存" align="center" prop="count" min-width="120"> <el-table-column label="库存" align="center" prop="count" min-width="120">
<template #default="{ row }">{{ formatCount(row.count) }}</template> <template #default="{ row }">{{ formatCount(row.count) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="单位" align="center" prop="unitName" width="90" /> <el-table-column label="单位" align="center" prop="unitName" width="90" />
<el-table-column label="库存展示" align="center" prop="stockDisplay" min-width="200"> <!-- <el-table-column label="库存展示" align="center" prop="stockDisplay" min-width="200">
<template #default="{ row }">{{ row.stockDisplay || '-' }}</template> <template #default="{ row }">{{ row.stockDisplay || '-' }}</template>
</el-table-column> </el-table-column> -->
</el-table> </el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" /> <Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap> </ContentWrap>
</template> </template>
@ -80,7 +131,11 @@ const formatCount = (value: number | string | undefined) => {
return Number.isFinite(num) ? num.toLocaleString() : String(value) return Number.isFinite(num) ? num.toLocaleString() : String(value)
} }
const buildQueryParams = () => const buildQueryParams = () =>
Object.fromEntries(Object.entries(queryParams).filter(([, value]) => value !== undefined && value !== null && value !== '')) Object.fromEntries(
Object.entries(queryParams).filter(
([, value]) => value !== undefined && value !== null && value !== ''
)
)
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {

@ -47,10 +47,10 @@
<el-col :span="8"> <el-col :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.code')" prop="code"> <el-form-item :label="t('ErpStock.WarehouseLocation.code')" prop="code">
<el-row :gutter="10" class="!w-full"> <el-row :gutter="10" class="!w-full">
<el-col :span="18"> <el-col :span="hideAutoCodeSwitch ? 24 : 18">
<el-input v-model="formData.code" :placeholder="t('ErpStock.WarehouseLocation.placeholderCode')" :disabled="formData.isCode == true || formType === 'update'" /> <el-input v-model="formData.code" :placeholder="t('ErpStock.WarehouseLocation.placeholderCode')" :disabled="formData.isCode == true || formType === 'update'" />
</el-col> </el-col>
<el-col :span="6"> <el-col v-if="!hideAutoCodeSwitch" :span="6">
<el-switch v-model="formData.isCode" :disabled="formType === 'update'" /> <el-switch v-model="formData.isCode" :disabled="formType === 'update'" />
</el-col> </el-col>
</el-row> </el-row>
@ -63,7 +63,7 @@
<el-input v-model="formData.name" :placeholder="t('ErpStock.WarehouseLocation.placeholderName')" class="!w-full" /> <el-input v-model="formData.name" :placeholder="t('ErpStock.WarehouseLocation.placeholderName')" class="!w-full" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col v-if="!isFieldHidden('areaSize')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.areaSize')" prop="areaSize"> <el-form-item :label="t('ErpStock.WarehouseLocation.areaSize')" prop="areaSize">
<el-input-number <el-input-number
v-model="formData.areaSize" v-model="formData.areaSize"
@ -74,7 +74,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col v-if="!isFieldHidden('maxLoadWeight')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.maxLoadWeight')" prop="maxLoadWeight"> <el-form-item :label="t('ErpStock.WarehouseLocation.maxLoadWeight')" prop="maxLoadWeight">
<el-input-number <el-input-number
v-model="formData.maxLoadWeight" v-model="formData.maxLoadWeight"
@ -87,7 +87,7 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col v-if="!isFieldHidden('positionX')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.positionX')" prop="positionX"> <el-form-item :label="t('ErpStock.WarehouseLocation.positionX')" prop="positionX">
<el-input-number <el-input-number
v-model="formData.positionX" v-model="formData.positionX"
@ -97,7 +97,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col v-if="!isFieldHidden('positionY')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.positionY')" prop="positionY"> <el-form-item :label="t('ErpStock.WarehouseLocation.positionY')" prop="positionY">
<el-input-number <el-input-number
v-model="formData.positionY" v-model="formData.positionY"
@ -107,7 +107,7 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col v-if="!isFieldHidden('positionZ')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.positionZ')" prop="positionZ"> <el-form-item :label="t('ErpStock.WarehouseLocation.positionZ')" prop="positionZ">
<el-input-number <el-input-number
v-model="formData.positionZ" v-model="formData.positionZ"
@ -119,12 +119,12 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col v-if="!isFieldHidden('allowProductMix')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.allowProductMix')" prop="allowProductMix"> <el-form-item :label="t('ErpStock.WarehouseLocation.allowProductMix')" prop="allowProductMix">
<el-switch v-model="formData.allowProductMix" /> <el-switch v-model="formData.allowProductMix" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col v-if="!isFieldHidden('allowBatchMix')" :span="8">
<el-form-item :label="t('ErpStock.WarehouseLocation.allowBatchMix')" prop="allowBatchMix"> <el-form-item :label="t('ErpStock.WarehouseLocation.allowBatchMix')" prop="allowBatchMix">
<el-switch v-model="formData.allowBatchMix" /> <el-switch v-model="formData.allowBatchMix" />
</el-form-item> </el-form-item>
@ -154,6 +154,14 @@ import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'WarehouseLocationForm' }) defineOptions({ name: 'WarehouseLocationForm' })
type WarehouseLocationOpenOptions = {
defaultWarehouseName?: string
defaultAreaName?: string
defaultData?: Record<string, any>
hideAutoCodeSwitch?: boolean
hiddenFields?: string[]
}
const { t } = useI18n() const { t } = useI18n()
const message = useMessage() const message = useMessage()
@ -176,9 +184,21 @@ const formData = ref({
status: undefined, status: undefined,
isCode: true isCode: true
}) })
const validateCode = (_rule: any, value: any, callback: any) => {
if (formData.value.isCode) {
callback()
return
}
if (value === undefined || value === null || String(value).trim() === '') {
callback(new Error('请输入库位编码'))
return
}
callback()
}
const formRules = reactive({ const formRules = reactive({
warehouseId: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorWarehouseIdRequired'), trigger: 'blur' }], warehouseId: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorWarehouseIdRequired'), trigger: 'blur' }],
areaId: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAreaIdRequired'), trigger: 'blur' }], areaId: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAreaIdRequired'), trigger: 'blur' }],
code: [{ validator: validateCode, trigger: ['blur', 'change'] }],
name: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorNameRequired'), trigger: 'blur' }], name: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorNameRequired'), trigger: 'blur' }],
allowProductMix: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAllowProductMixRequired'), trigger: 'blur' }], allowProductMix: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAllowProductMixRequired'), trigger: 'blur' }],
allowBatchMix: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAllowBatchMixRequired'), trigger: 'blur' }], allowBatchMix: [{ required: true, message: t('ErpStock.WarehouseLocation.validatorAllowBatchMixRequired'), trigger: 'blur' }],
@ -187,6 +207,8 @@ const formRules = reactive({
const formRef = ref() const formRef = ref()
const warehouseList = ref<any[]>([]) const warehouseList = ref<any[]>([])
const areaList = ref<any[]>([]) const areaList = ref<any[]>([])
const hideAutoCodeSwitch = ref(false)
const hiddenFields = ref<string[]>([])
const filteredAreaList = computed(() => { const filteredAreaList = computed(() => {
if (!formData.value.warehouseId) return areaList.value if (!formData.value.warehouseId) return areaList.value
@ -197,9 +219,32 @@ const handleWarehouseChange = () => {
formData.value.areaId = undefined formData.value.areaId = undefined
} }
const open = async (type: string, id?: number) => { const isFieldHidden = (field: string) => hiddenFields.value.includes(field)
const applyDefaultWarehouseArea = (options?: WarehouseLocationOpenOptions) => {
if (!options) return
const defaultWarehouse = options.defaultWarehouseName
? warehouseList.value.find((item) => item.name === options.defaultWarehouseName)
: undefined
if (defaultWarehouse) {
formData.value.warehouseId = defaultWarehouse.id
}
const candidateAreaList = formData.value.warehouseId
? areaList.value.filter((item) => item.warehouseId === formData.value.warehouseId)
: areaList.value
const defaultArea = options.defaultAreaName
? candidateAreaList.find((item) => item.areaName === options.defaultAreaName)
: undefined
if (defaultArea) {
formData.value.areaId = defaultArea.id
}
}
const open = async (type: string, id?: number, options?: WarehouseLocationOpenOptions) => {
dialogTitle.value = t('action.' + type) dialogTitle.value = t('action.' + type)
formType.value = type formType.value = type
hideAutoCodeSwitch.value = Boolean(options?.hideAutoCodeSwitch)
hiddenFields.value = options?.hiddenFields ?? []
resetForm() resetForm()
warehouseList.value = await WarehouseApi.getWarehouseSimpleList() warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
const areaData = await WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 }) const areaData = await WarehouseAreaApi.getWarehouseAreaPage({ pageNo: 1, pageSize: 100 })
@ -211,6 +256,12 @@ const open = async (type: string, id?: number) => {
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
} else {
formData.value = {
...formData.value,
...(options?.defaultData ?? {})
}
applyDefaultWarehouseArea(options)
} }
} }
defineExpose({ open }) defineExpose({ open })

@ -1,5 +1,12 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="92%" :fullscreen="false" top="4vh"> <Dialog
v-model="dialogVisible"
:title="dialogTitle"
width="96%"
:fullscreen="false"
:initial-fullscreen="true"
top="2vh"
>
<div class="hiprint-preview"> <div class="hiprint-preview">
<div class="hiprint-toolbar"> <div class="hiprint-toolbar">
<div class="hiprint-paper"> <div class="hiprint-paper">
@ -167,6 +174,11 @@ type BarcodeElementItem = {
label: string label: string
} }
type BarcodeFieldItem = BarcodeElementItem & {
field: string
type: 'image' | 'text'
}
type BarcodeElementGroup = { type BarcodeElementGroup = {
value: string | number value: string | number
label: string label: string
@ -175,6 +187,37 @@ type BarcodeElementGroup = {
const barcodeGroups = ref<BarcodeElementGroup[]>([]) const barcodeGroups = ref<BarcodeElementGroup[]>([])
const barcodeDictData = ref<any[]>([]) const barcodeDictData = ref<any[]>([])
const barcodeFieldLabelMap: Record<string, string> = {
qrcodeUrl: '二维码',
name: '名称',
code: '编码',
sampleCategory: '样品分类',
customerName: '客户名称'
}
const getBarcodeFieldLabel = (field: string, fallback: string) => barcodeFieldLabelMap[field] || fallback || field
const parseBarcodeFields = (item: any): BarcodeFieldItem[] => {
const parts = String(item.remark || '')
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
const qrcodeField = parts[0] || ''
const nameField = parts[1] || ''
const codeField = parts[2] || parts[1] || ''
const baseFields: BarcodeFieldItem[] = [
{ tid: `barcodeModule.${item.value}_qrcode`, label: '二维码', field: qrcodeField, type: 'image' },
{ tid: `barcodeModule.${item.value}_name`, label: getBarcodeFieldLabel(nameField, '名称'), field: nameField, type: 'text' },
{ tid: `barcodeModule.${item.value}_code`, label: getBarcodeFieldLabel(codeField, '编码'), field: codeField, type: 'text' }
]
const extraFields = parts.slice(3).map((field: string, index: number) => ({
tid: `barcodeModule.${item.value}_field_${index + 3}`,
label: getBarcodeFieldLabel(field, field),
field,
type: 'text' as const
}))
return [...baseFields, ...extraFields]
}
const loadBarcodeDictData = async () => { const loadBarcodeDictData = async () => {
try { try {
@ -184,11 +227,7 @@ const loadBarcodeDictData = async () => {
barcodeGroups.value = filtered.map((item: any) => ({ barcodeGroups.value = filtered.map((item: any) => ({
value: item.value, value: item.value,
label: item.label, label: item.label,
elements: [ elements: parseBarcodeFields(item).map(({ tid, label }) => ({ tid, label }))
{ tid: `barcodeModule.${item.value}_qrcode`, label: '二维码' },
{ tid: `barcodeModule.${item.value}_name`, label: '名称' },
{ tid: `barcodeModule.${item.value}_code`, label: '编码' }
]
})) }))
} catch (e) { } catch (e) {
console.error('加载条码字典数据失败', e) console.error('加载条码字典数据失败', e)
@ -201,55 +240,39 @@ const barcodeProvider = function () {
if (!barcodeDictData.value.length) return if (!barcodeDictData.value.length) return
const groups: any[] = [] const groups: any[] = []
barcodeDictData.value.forEach((item: any) => { barcodeDictData.value.forEach((item: any) => {
const remark = item.remark || '' const fields = parseBarcodeFields(item)
const parts = remark.split(',').map((s: string) => s.trim())
const qrcodeField = parts[0] || ''
const nameField = parts[1] || ''
const codeField = parts[2] || parts[1] || ''
groups.push( groups.push(
new hiprint.PrintElementTypeGroup(item.label, [ new hiprint.PrintElementTypeGroup(item.label, fields.map((fieldItem) => {
{ if (fieldItem.type === 'image') {
tid: `barcodeModule.${item.value}_qrcode`, return {
title: '二维码', tid: fieldItem.tid,
type: 'image', title: fieldItem.label,
options: { type: 'image',
field: qrcodeField, options: {
src: qrCodeImg, field: fieldItem.field,
testData: qrCodeImg, src: qrCodeImg,
width: 80, testData: qrCodeImg,
height: 80, width: 80,
title: '二维码' height: 80,
title: fieldItem.label
}
} }
}, }
{ return {
tid: `barcodeModule.${item.value}_name`, tid: fieldItem.tid,
title: '名称', title: fieldItem.label,
type: 'text',
options: {
field: nameField,
testData: '',
title: '名称',
fontSize: 10,
textAlign: 'center',
width: 80,
height: 16
}
},
{
tid: `barcodeModule.${item.value}_code`,
title: '编码',
type: 'text', type: 'text',
options: { options: {
field: codeField, field: fieldItem.field,
testData: '', testData: '',
title: '编码', title: fieldItem.label,
fontSize: 10, fontSize: 10,
textAlign: 'center', textAlign: 'center',
width: 80, width: 80,
height: 16 height: 16
} }
} }
]) }))
) )
}) })
context.addPrintElementTypes('barcodeModule', groups) context.addPrintElementTypes('barcodeModule', groups)
@ -644,6 +667,10 @@ watch(selectedIconColor, () => {
<style scoped lang="scss"> <style scoped lang="scss">
.hiprint-preview { .hiprint-preview {
width: 100%; width: 100%;
height: calc(100vh - 84px);
display: flex;
flex-direction: column;
min-height: 0;
} }
.hiprint-toolbar { .hiprint-toolbar {
@ -651,6 +678,7 @@ watch(selectedIconColor, () => {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 12px; margin-bottom: 12px;
flex-shrink: 0;
} }
.hiprint-paper { .hiprint-paper {
@ -686,7 +714,8 @@ watch(selectedIconColor, () => {
} }
.hiprint-body { .hiprint-body {
height: 75vh; flex: 1;
min-height: 0;
display: grid; display: grid;
grid-template-columns: 220px 1fr 400px; grid-template-columns: 220px 1fr 400px;
gap: 12px; gap: 12px;

Loading…
Cancel
Save