针对于条形码生成进行修改

This commit is contained in:
dxc
2026-02-02 16:43:35 +08:00
parent cf6a4a8957
commit 11a4e5f48a
4 changed files with 364 additions and 134 deletions

View File

@ -88,5 +88,5 @@ class StockBuy(db.Model):
# [新增] 返回全局打印ID及其格式化字符串
'global_print_id': self.global_print_id,
'global_print_id_str': f"{self.global_print_id:08d}" if self.global_print_id else ""
'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else ""
}

View File

@ -79,7 +79,7 @@ class BuyInboundService:
# ------------------------------------------------------------------
# 2. 自动生成 SKU (格式: 00000001)
# ------------------------------------------------------------------
generated_sku = f"{next_global_id:08d}"
generated_sku = str(next_global_id).zfill(10)
# ------------------------------------------------------------------
# 3. 条码逻辑处理 (核心修改)

View File

@ -26,12 +26,13 @@ class LabelPrintService:
LABEL_HEIGHT = int(LABEL_HEIGHT_MM * DOTS_PER_MM)
# 顶部留白: 10mm (120px) - 内容从这里开始
TOP_MARGIN_MM = 5
TOP_MARGIN_MM = 2
TOP_MARGIN_PX = int(TOP_MARGIN_MM * DOTS_PER_MM)
@staticmethod
def _get_font(size):
"""获取字体"""
# 尝试加载中文字体,否则乱码
font_names = ["simhei.ttf", "msyh.ttf", "SimHei.ttf", "arial.ttf"]
base_dirs = [os.getcwd(), os.path.dirname(__file__)]
@ -48,6 +49,10 @@ class LabelPrintService:
生成真实的条形码图片
"""
try:
# 确保内容字符串有效
if not content:
content = "0000000000"
# 使用 Code128 编码(通用性最强)
code128 = barcode.get('code128', content, writer=ImageWriter())
@ -85,31 +90,31 @@ class LabelPrintService:
sku_code = data.get('sku')
if not sku_code:
# 兜底逻辑:如果没有 SKU尝试用 serial_number 或 global_id
sku_code = data.get('serial_number') or str(data.get('global_print_id', '00000000')).zfill(8)
sku_code = data.get('serial_number') or str(data.get('global_print_id', '0000000000')).zfill(10)
# ==================== 绘制布局 ====================
# X 轴偏移 (5mm)
GLOBAL_OFFSET_X = int(5 * LabelPrintService.DOTS_PER_MM)
GLOBAL_OFFSET_X = int(2 * LabelPrintService.DOTS_PER_MM)
# Y 轴起始: 10mm 处
CURRENT_Y = LabelPrintService.TOP_MARGIN_PX
# --- A. 绘制真实条形码 ---
bc_w = int(28 * LabelPrintService.DOTS_PER_MM) # 条码宽约 28mm
bc_h = int(6 * LabelPrintService.DOTS_PER_MM) # 条码高约 6mm
# --- A. 绘制真实条形码 (内容为 SKU) ---
bc_w = int(35 * LabelPrintService.DOTS_PER_MM) # 条码宽约 28mm
bc_h = int(10 * LabelPrintService.DOTS_PER_MM) # 条码高约 6mm
# 生成并粘贴条码
bc_img = LabelPrintService._generate_barcode_image(sku_code, bc_w, bc_h)
# 粘贴位置: (X, Y)
img.paste(bc_img, (GLOBAL_OFFSET_X - 5, CURRENT_Y))
img.paste(bc_img, (GLOBAL_OFFSET_X - 10, CURRENT_Y))
# 更新 Y 坐标 (条码高度 + 间距)
CURRENT_Y += bc_h + 5
# --- B. 绘制文字 (统一加粗) ---
# 准备数据
# 准备数据 (名称、规格等)
name = str(data.get('material_name', '') or '-')
if len(name) > 8: name = name[:7] + ".."
@ -123,15 +128,43 @@ class LabelPrintService:
attr = f"{cat}/{typ}"
if len(attr) > 11: attr = attr[:10] + ".."
# 底部显示的文字SKU 编号
sku_str = f"NO.{sku_code}"
# -----------------------------------------------------------
# [修改点] 底部显示的文字:不再显示 NO.SKU而是显示 SN 或 BN
# -----------------------------------------------------------
bottom_text = ""
# 1. 优先检查前端是否传了明确的 print_no (打印值) 和 print_label (序/批)
if data.get('print_no'):
val = str(data.get('print_no'))
label_type = data.get('print_label', '')
if label_type == '':
bottom_text = f"SN: {val}"
elif label_type == '':
bottom_text = f"BN: {val}"
else:
bottom_text = f"NO: {val}"
# 2. 如果没有前端打印参数,检查数据中的序列号
elif data.get('serial_number'):
bottom_text = f"SN: {data.get('serial_number')}"
# 3. 检查数据中的批号
elif data.get('batch_number'):
bottom_text = f"BN: {data.get('batch_number')}"
# 4. 兜底 (如果什么都没有,才显示 SKU)
else:
bottom_text = f"NO: {sku_code}"
# -----------------------------------------------------------
lines = [
f"名: {name}",
f"规: {spec}",
f"库: {loc}",
f"属: {attr}",
f"{sku_str}"
f"{bottom_text}" # 这里放置计算好的 SN/BN 文字
]
line_height = 36 # 行高 (字号30 + 6间距)

View File

@ -27,11 +27,15 @@
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
<div class="col-group-title">基础信息</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
</el-col>
</el-row>
<div class="col-group-title" style="margin-top:10px">库存与商务</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in stockColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</el-popover>
@ -80,8 +84,12 @@
</template>
<template #default="scope" v-else-if="col.prop.includes('link')">
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
<el-icon><Link /></el-icon> 查看
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank"
:underline="false">
<el-icon>
<Link/>
</el-icon>
查看
</el-link>
</template>
@ -94,7 +102,10 @@
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="warning" size="default" @click="handlePrint(row)">
<el-icon><Printer /></el-icon> 打印
<el-icon>
<Printer/>
</el-icon>
打印
</el-button>
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
@ -132,7 +143,9 @@
<div class="form-card basic-card">
<div class="card-title">
<el-icon class="icon"><Box /></el-icon>
<el-icon class="icon">
<Box/>
</el-icon>
<span>1. 基础信息</span>
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索锁定物料)</span>
</div>
@ -179,11 +192,31 @@
<div class="read-only-grid">
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="名称">
<el-input v-model="form.material_name" disabled class="is-text-view"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="规格型号">
<el-input v-model="form.spec_model" disabled class="is-text-view"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="单位">
<el-input v-model="form.unit" disabled class="is-text-view"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="类别">
<el-input v-model="form.category" disabled class="is-text-view"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="类型">
<el-input v-model="form.material_type" disabled class="is-text-view"/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
@ -191,7 +224,9 @@
<div class="form-card inbound-card">
<div class="card-title">
<el-icon class="icon"><House /></el-icon>
<el-icon class="icon">
<House/>
</el-icon>
<span>2. 入库详情</span>
</div>
@ -201,24 +236,34 @@
<el-form-item label="编码/SKU" prop="sku">
<el-input
v-model="form.sku"
placeholder="系统自动生成 (0000000X)"
placeholder="系统自动生成 (000000000X)"
disabled
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="入库日期" prop="in_date">
<el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled />
<el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%"
disabled/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="条码" prop="barcode">
<el-input v-model="form.barcode" placeholder="扫描条码"/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="库位" prop="warehouse_location">
<el-input v-model="form.warehouse_location" placeholder="例如: A-01-02"/>
</el-form-item>
</el-col>
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: A-01-02" /></el-form-item></el-col>
</el-row>
<div class="identity-panel">
<el-row>
<el-col :span="24" style="margin-bottom: 8px;">
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" size="small" class="custom-radio-group">
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked"
size="small" class="custom-radio-group">
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
</el-radio-group>
@ -257,7 +302,8 @@
<el-row :gutter="20" style="margin-top: 10px;">
<el-col :span="6">
<el-form-item label="入库数量" prop="in_quantity">
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" />
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%"
class="strong-input"/>
</el-form-item>
</el-col>
@ -292,7 +338,11 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12"><el-form-item label="到货图片" prop="arrival_photo"><el-input v-model="form.arrival_photo" placeholder="输入图片 URL" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="到货图片" prop="arrival_photo">
<el-input v-model="form.arrival_photo" placeholder="输入图片 URL"/>
</el-form-item>
</el-col>
</el-row>
<div class="divider-text">商务与采购信息</div>
@ -314,15 +364,22 @@
</el-autocomplete>
</el-form-item>
</el-col>
<el-col :span="6"><el-form-item label="汇率"><el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
<el-col :span="6">
<el-form-item label="汇率">
<el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right"
style="width:100%"/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="含税单价" prop="unit_price">
<el-input-number v-model="form.unit_price" :precision="4" controls-position="right" style="width:100%" />
<el-input-number v-model="form.unit_price" :precision="4" controls-position="right"
style="width:100%"/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="总价">
<el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/>
<el-input-number v-model="form.total_price" :precision="2" disabled :controls="false"
style="width:100%" class="total-price-input"/>
</el-form-item>
</el-col>
</el-row>
@ -370,8 +427,16 @@
</el-row>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" placeholder="http://" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="http://" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="原始链接">
<el-input v-model="form.source_link" placeholder="http://"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详情链接">
<el-input v-model="form.detail_link" placeholder="http://"/>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
@ -411,7 +476,10 @@
<div class="dialog-footer">
<el-button @click="printVisible = false">取消</el-button>
<el-button type="primary" :loading="printing" @click="confirmPrint">
<el-icon><Printer /></el-icon> 确认打印
<el-icon>
<Printer/>
</el-icon>
确认打印
</el-button>
</div>
</template>
@ -554,7 +622,9 @@ const saveToHistory = (key: string, value: string) => {
list.unshift(value)
if (list.length > 20) list = list.slice(0, 20) // 最多存20条
localStorage.setItem(key, JSON.stringify(list))
} catch (e) { console.error('save history failed', e) }
} catch (e) {
console.error('save history failed', e)
}
}
// 获取历史 (String 类型)
@ -563,7 +633,9 @@ const getHistoryList = (key: string): any[] => {
const existing = localStorage.getItem(key)
const list = existing ? JSON.parse(existing) : []
return list.map((v: string) => ({value: v}))
} catch (e) { return [] }
} catch (e) {
return []
}
}
// 保存物料历史 (Object 类型)
@ -577,14 +649,17 @@ const saveMaterialHistory = (item: any) => {
list.unshift({...item, isHistory: true})
if (list.length > 10) list = list.slice(0, 10)
localStorage.setItem(key, JSON.stringify(list))
} catch (e) {}
} catch (e) {
}
}
const getMaterialHistory = () => {
try {
const existing = localStorage.getItem(HISTORY_KEYS.MATERIAL)
return existing ? JSON.parse(existing) : []
} catch (e) { return [] }
} catch (e) {
return []
}
}
@ -788,7 +863,9 @@ const fetchData = async () => {
const res: any = await getBuyList(queryParams)
tableData.value = res.data.items || []
total.value = res.data.total || 0
} finally { loading.value = false }
} finally {
loading.value = false
}
}
const handleCreate = () => {
@ -1011,9 +1088,20 @@ onMounted(() => fetchData())
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.left-tools { flex: 0 0 350px; }
.right-tools { display: flex; gap: 10px; align-items: center; }
.action-btn { font-weight: 500; }
.left-tools {
flex: 0 0 350px;
}
.right-tools {
display: flex;
gap: 10px;
align-items: center;
}
.action-btn {
font-weight: 500;
}
/* 表格美化 */
.modern-table {
@ -1021,18 +1109,47 @@ onMounted(() => fetchData())
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
:deep(.table-header-gray th) {
background-color: #f8f9fb !important;
color: #606266;
font-weight: 600;
height: 50px;
}
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; }
.money-text { font-family: 'Consolas', monospace; color: #303133; }
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
.sum-tag { margin-left: 4px; transform: scale(0.9); }
.tag-sn {
color: #409EFF;
font-weight: bold;
font-family: monospace;
}
.tag-bn {
color: #67C23A;
font-weight: bold;
font-family: monospace;
}
.money-text {
font-family: 'Consolas', monospace;
color: #303133;
}
.stock-num {
font-weight: bold;
color: #333;
font-size: 15px;
}
.avail-num {
font-weight: bold;
color: #67C23A;
font-size: 15px;
}
.sum-tag {
margin-left: 4px;
transform: scale(0.9);
}
/* 弹窗核心样式调整 */
:deep(.el-dialog__body) {
@ -1066,14 +1183,38 @@ onMounted(() => fetchData())
display: flex;
align-items: center;
}
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
.card-content { padding: 15px 20px; }
.card-title .icon {
margin-right: 8px;
font-size: 18px;
color: #409EFF;
}
.card-title .sub-title {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 10px;
}
.card-content {
padding: 15px 20px;
}
/* 基础信息卡片 */
.basic-card { border-left: 4px solid #409EFF; }
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
.basic-card {
border-left: 4px solid #409EFF;
}
.search-tip {
color: #909399;
font-size: 12px;
margin-left: 10px;
display: flex;
align-items: center;
gap: 4px;
}
.is-text-view :deep(.el-input__wrapper) {
box-shadow: none !important;
background-color: #f5f7fa;
@ -1081,10 +1222,17 @@ onMounted(() => fetchData())
border-radius: 0;
padding-left: 0;
}
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; font-size: 13px; }
.is-text-view :deep(.el-input__inner) {
color: #606266;
font-weight: 500;
font-size: 13px;
}
/* 入库信息卡片 */
.inbound-card { border-left: 4px solid #67C23A; }
.inbound-card {
border-left: 4px solid #67C23A;
}
/* 身份区域 (SN/BN) */
.identity-panel {
@ -1094,11 +1242,33 @@ onMounted(() => fetchData())
padding: 12px;
margin-bottom: 15px;
}
.custom-radio-group { margin-bottom: 10px; }
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
.custom-radio-group {
margin-bottom: 10px;
}
.locked-msg {
font-size: 12px;
color: #e6a23c;
margin-left: 15px;
}
.prefix-tag {
font-weight: bold;
font-size: 12px;
padding: 0 5px;
border-radius: 4px;
}
.prefix-tag.bn {
color: #67C23A;
background: #f0f9eb;
}
.prefix-tag.sn {
color: #409EFF;
background: #ecf5ff;
}
/* 分割线 */
.divider-text {
@ -1110,13 +1280,20 @@ onMounted(() => fetchData())
font-size: 13px;
font-weight: 500;
}
.divider-text::before, .divider-text::after {
content: '';
flex: 1;
border-bottom: 1px solid #ebeef5;
}
.divider-text::before { margin-right: 15px; }
.divider-text::after { margin-left: 15px; }
.divider-text::before {
margin-right: 15px;
}
.divider-text::after {
margin-left: 15px;
}
/* 底部按钮 */
.dialog-footer {
@ -1128,10 +1305,27 @@ onMounted(() => fetchData())
border-top: 1px solid #ebeef5;
}
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center;}
.opt-name { font-weight: bold; }
.opt-spec { color: #8492a6; font-size: 13px; margin-right: 10px; }
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
.option-item {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
}
.opt-name {
font-weight: bold;
}
.opt-spec {
color: #8492a6;
font-size: 13px;
margin-right: 10px;
}
.total-price-input :deep(.el-input__inner) {
color: #F56C6C;
font-weight: bold;
}
.preview-box {
min-height: 150px;
@ -1141,5 +1335,8 @@ onMounted(() => fetchData())
background: #f5f7fa;
border-radius: 4px;
}
.empty-preview { color: #909399; }
.empty-preview {
color: #909399;
}
</style>