feat(mes): 添加条码相关组件和逻辑

新增条码格式枚举、条码生成组件及其相关 API,支持条码的创建、查看和配置功能。实现了条码的自动生成逻辑,并优化了条码配置管理界面,提升用户体验。

- 新增 Barcode 组件用于条码展示
- 实现条码生成和下载功能
- 添加条码配置管理功能
pull/871/MERGE
YunaiV 2026-03-06 00:09:23 +08:00
parent e275ef417e
commit c12d7616f2
6 changed files with 729 additions and 1 deletions

View File

@ -53,6 +53,7 @@
"element-plus": "2.11.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsbarcode": "^3.12.3",
"jsencrypt": "^3.3.2",
"jsoneditor": "^10.1.3",
"lodash-es": "^4.17.21",

View File

@ -92,6 +92,9 @@ importers:
highlight.js:
specifier: ^11.9.0
version: 11.10.0
jsbarcode:
specifier: ^3.12.3
version: 3.12.3
jsencrypt:
specifier: ^3.3.2
version: 3.3.2
@ -3669,6 +3672,9 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
jsbarcode@3.12.3:
resolution: {integrity: sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==, tarball: https://registry.npmmirror.com/jsbarcode/-/jsbarcode-3.12.3.tgz}
jsencrypt@3.3.2:
resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
@ -4603,7 +4609,7 @@ packages:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, tarball: https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@ -9023,6 +9029,8 @@ snapshots:
dependencies:
argparse: 2.0.1
jsbarcode@3.12.3: {}
jsencrypt@3.3.2: {}
jsesc@3.0.2: {}

View File

@ -0,0 +1,148 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="条码格式" prop="format">
<el-select v-model="formData.format" placeholder="请选择条码格式" class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_FORMAT)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-select v-model="formData.bizType" placeholder="请选择业务类型" class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_BIZ_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<!-- TODO @AI需要根据 bizType使用不同业务的 select -->
<el-form-item label="业务编号" prop="bizId">
<el-input-number v-model="formData.bizId" :min="1" class="!w-240px" />
</el-form-item>
<!-- TODO @AIbizCodebizName 根据上面的 select 进行设置必填后端校验也加下 -->
<el-form-item label="业务编码" prop="bizCode">
<el-input v-model="formData.bizCode" placeholder="请输入业务编码" />
</el-form-item>
<el-form-item label="业务名称" prop="bizName">
<el-input v-model="formData.bizName" placeholder="请输入业务名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { BarcodeApi, BarcodeVO } from '@/api/mes/wm/barcode'
defineOptions({ name: 'BarcodeForm' })
// TODO @AI /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/system/user/UserForm.vue
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref({
id: undefined,
format: undefined,
bizType: undefined,
bizId: undefined,
bizCode: '',
bizName: '',
status: 0,
remark: ''
})
const formRules = reactive({
format: [{ required: true, message: '条码格式不能为空', trigger: 'change' }],
bizType: [{ required: true, message: '业务类型不能为空', trigger: 'change' }],
bizId: [{ required: true, message: '业务编号不能为空', trigger: 'blur' }],
bizCode: [{ required: true, message: '业务编码不能为空', trigger: 'blur' }]
})
const formRef = ref()
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'create' ? '新增条码' : '修改条码'
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
formData.value = await BarcodeApi.getBarcode(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
try {
const data = formData.value as unknown as BarcodeVO
if (formType.value === 'create') {
await BarcodeApi.createBarcode(data)
message.success(t('common.createSuccess'))
} else {
await BarcodeApi.updateBarcode(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
format: undefined,
bizType: undefined,
bizId: undefined,
bizCode: '',
bizName: '',
status: 0, // TODO @AIcommonstatusenum
remark: ''
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,281 @@
<template>
<!--
TODO @AI挪到 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/mes/wm/barcode/components 改名为 BarcodeDetail
TODO @AI参数有 2 1一种是目前这种 BarcodeData2在加一种是 bizId + bizType 组合然后去加载 BarcodeData
// TODO @AIBarcodeData 使 BarcodeVO
-->
<Dialog title="查看条码" v-model="dialogVisible" width="500px" :close-on-click-modal="false">
<div class="barcode-view-container">
<!-- 条码显示区域 -->
<div class="barcode-display">
<div v-if="barcodeData.content" class="barcode-wrapper">
<!-- TODO @AI二维码不够大 -->
<Barcode
ref="barcodeRef"
:content="barcodeData.content"
:format="barcodeData.format"
:width="300"
:height="150"
/>
</div>
<el-empty v-else description="暂无条码数据" />
</div>
<!-- 条码详细信息 -->
<!-- TODO @AI统一左对齐目前貌似没左对齐 -->
<el-descriptions :column="1" border class="barcode-info">
<el-descriptions-item label="条码格式" label-align="center" align="left">
<!-- TODO @AI不用 String -->
<dict-tag :type="DICT_TYPE.MES_BARCODE_FORMAT" :value="String(barcodeData.format)" />
</el-descriptions-item>
<el-descriptions-item label="业务类型" label-align="center" align="left">
<!-- TODO @AI不用 String -->
<dict-tag :type="DICT_TYPE.MES_BARCODE_BIZ_TYPE" :value="String(barcodeData.bizType)" />
</el-descriptions-item>
<el-descriptions-item label="条码内容" label-align="center" align="left">
<el-tooltip :content="barcodeData.content" placement="top">
<span class="content-text">{{ barcodeData.content }}</span>
</el-tooltip>
</el-descriptions-item>
<el-descriptions-item label="业务编码" label-align="center" align="left">
{{ barcodeData.bizCode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="业务名称" label-align="center" align="left">
{{ barcodeData.bizName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态" label-align="center" align="left">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="String(barcodeData.status)" />
</el-descriptions-item>
<el-descriptions-item label="创建时间" label-align="center" align="left">
{{ formatDate(barcodeData.createTime) }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 底部操作按钮 -->
<!-- TODO @AI如果没二维码的情况需要支持生成 -->
<template #footer>
<el-button type="primary" @click="handlePrint">
<Icon icon="ep:printer" class="mr-5px" /> 打印
</el-button>
<el-button @click="handleDownload">
<Icon icon="ep:download" class="mr-5px" /> 下载
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useMessage } from '@/hooks/web/useMessage'
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import { Barcode } from './components'
defineOptions({ name: 'BarcodeViewDialog' })
interface BarcodeData {
id?: number
format?: number
bizType?: number
content: string
bizCode?: string
bizName?: string
status?: number
createTime?: string
}
const message = useMessage()
const dialogVisible = ref(false)
const barcodeRef = ref<InstanceType<typeof Barcode>>()
const barcodeData = ref<BarcodeData>({
format: undefined,
bizType: undefined,
content: '',
bizCode: '',
bizName: '',
status: undefined,
createTime: ''
})
/** 打开弹窗 */
const open = (row: BarcodeData) => {
dialogVisible.value = true
barcodeData.value = { ...row }
}
defineExpose({ open })
/** 打印条码 */
// TODO @AI
const handlePrint = () => {
if (!barcodeRef.value) {
message.warning('条码组件未加载')
return
}
const base64 = barcodeRef.value.getImageBase64?.()
if (!base64) {
message.warning('条码生成失败,无法打印')
return
}
//
const printWindow = window.open('', '_blank')
if (!printWindow) {
message.error('无法打开打印窗口,请检查浏览器设置')
return
}
try {
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>打印条码</title>
<style>
* { margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; padding: 20px; }
.print-container { text-align: center; }
.barcode-img { max-width: 100%; margin: 20px 0; }
.info { margin-top: 20px; text-align: left; font-size: 12px; }
.info p { margin: 5px 0; }
@media print {
body { padding: 0; }
.print-container { padding: 20px; }
}
</style>
</head>
<body>
<div class="print-container">
<img src="${base64}" class="barcode-img" alt="条码" />
<div class="info">
<p><strong>业务编码:</strong> ${escapeHtml(barcodeData.value.bizCode || '')}</p>
<p><strong>业务名称:</strong> ${escapeHtml(barcodeData.value.bizName || '')}</p>
<p><strong>条码内容:</strong> ${escapeHtml(barcodeData.value.content || '')}</p>
</div>
</div>
</body>
</html>`
printWindow.document.write(html)
printWindow.document.close()
//
printWindow.onload = () => {
setTimeout(() => {
printWindow.print()
}, 500)
}
} catch (error) {
console.error('打印失败:', error)
message.error('打印失败,请重试')
}
}
/** 下载条码 */
// TODO @AIdownload
const handleDownload = () => {
if (!barcodeRef.value) {
message.warning('条码组件未加载')
return
}
const base64 = barcodeRef.value.getImageBase64?.()
if (!base64) {
message.warning('条码生成失败,无法下载')
return
}
try {
// base64 Blob
const arr = base64.split(',')
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
const bstr = atob(arr[1])
const n = bstr.length
const u8arr = new Uint8Array(n)
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i)
}
const blob = new Blob([u8arr], { type: mime })
//
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `barcode_${barcodeData.value.bizCode || 'unknown'}_${Date.now()}.png`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
message.success('下载成功')
} catch (error) {
console.error('下载失败:', error)
message.error('下载失败,请重试')
}
}
/** HTML 转义函数,防止 XSS */
// TODO @AI
const escapeHtml = (text: string): string => {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, (char) => map[char])
}
// TODO @AI css unocss
</script>
<style scoped lang="scss">
.barcode-view-container {
.barcode-display {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
margin-bottom: 20px;
.barcode-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
}
.barcode-info {
margin-top: 0;
:deep(.el-descriptions__body) {
background-color: #fff;
}
:deep(.el-descriptions-item__label) {
font-weight: 500;
color: #606266;
background-color: #f5f7fa;
}
:deep(.el-descriptions-item__content) {
color: #303133;
}
}
.content-text {
display: inline-block;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
}
</style>

View File

@ -0,0 +1,29 @@
// TODO @AI迁移到 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/mes/utils/constants.ts
/**
*
*/
export enum BarcodeFormatEnum {
QR_CODE = 1,
EAN13 = 2,
CODE39 = 3,
UPC_A = 4
}
/**
*
*/
// TODO @AI拿到需要的地方貌似就一次性的
export const BARCODE_FORMAT_MAP: Record<BarcodeFormatEnum, string> = {
[BarcodeFormatEnum.QR_CODE]: 'QR_CODE',
[BarcodeFormatEnum.EAN13]: 'EAN13',
[BarcodeFormatEnum.CODE39]: 'CODE39',
[BarcodeFormatEnum.UPC_A]: 'UPC_A'
}
/**
*
*/
// TODO @AI去掉拿到需要的地方
export const isValidBarcodeFormat = (format: number): boolean => {
return Object.values(BarcodeFormatEnum).includes(format)
}

View File

@ -0,0 +1,261 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="业务类型" prop="bizType">
<el-select
v-model="queryParams.bizType"
placeholder="请选择业务类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_BIZ_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="业务编码" prop="bizCode">
<el-input
v-model="queryParams.bizCode"
placeholder="请输入业务编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<!-- TODO @AI前后端筛选额外增加 bizName -->
<el-form-item label="条码内容" prop="content">
<el-input
v-model="queryParams.content"
placeholder="请输入条码内容"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['mes:wm-barcode:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDelete()"
:disabled="!selectedIds.length"
v-hasPermi="['mes:wm-barcode:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
<el-button
type="success"
plain
@click="handleConfig"
v-hasPermi="['mes:wm-barcode-config:query']"
>
<Icon icon="ep:setting" class="mr-5px" /> 条码设置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="条码" align="center" width="150">
<template #default="scope">
<!-- TODO @AI改成操作有个查看点击开 -->
<div class="barcode-preview" @click="handleView(scope.row)">
<Barcode
v-if="scope.row.content"
:content="scope.row.content"
:format="scope.row.format"
:width="120"
:height="60"
/>
</div>
</template>
</el-table-column>
<el-table-column label="条码格式" align="center" prop="format">
<template #default="scope">
<!-- TODO @AIMES_BARCODE_FORMAT => MES_WM_BARCODE_FORMAT -->
<dict-tag :type="DICT_TYPE.MES_BARCODE_FORMAT" :value="scope.row.format" />
</template>
</el-table-column>
<el-table-column label="业务类型" align="center" prop="bizType">
<!-- TODO @AIMES_BARCODE_BIZ_TYPE => MES_WM_BARCODE_BIZ_TYPE -->
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_BARCODE_BIZ_TYPE" :value="scope.row.bizType" />
</template>
</el-table-column>
<el-table-column label="条码内容" align="center" prop="content" show-overflow-tooltip />
<el-table-column label="业务编码" align="center" prop="bizCode" />
<el-table-column label="业务名称" align="center" prop="bizName" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180px" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="handleView(scope.row)"
v-hasPermi="['mes:wm-barcode:query']"
>
查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['mes:wm-barcode:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['mes:wm-barcode:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<BarcodeForm ref="formRef" @success="getList" />
<!-- 查看弹窗 -->
<BarcodeViewDialog ref="viewDialogRef" />
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { BarcodeApi } from '@/api/mes/wm/barcode'
import { Barcode } from './components'
import BarcodeForm from './BarcodeForm.vue'
import BarcodeViewDialog from './BarcodeViewDialog.vue'
defineOptions({ name: 'MesWmBarcode' })
const message = useMessage()
const { t } = useI18n()
const { push } = useRouter()
const loading = ref(true)
const list = ref([])
const total = ref(0)
const selectedIds = ref<number[]>([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
bizType: undefined,
bizCode: undefined,
content: undefined
})
const queryFormRef = ref()
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await BarcodeApi.getBarcodePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: any[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id?: number) => {
const ids = id ? [id] : selectedIds.value
try {
await message.delConfirm()
await Promise.all(ids.map((id) => BarcodeApi.deleteBarcode(id)))
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 查看条码 */
const viewDialogRef = ref()
const handleView = (row: any) => {
viewDialogRef.value.open(row)
}
/** 条码设置 */
const handleConfig = () => {
// TODO @AI name便
push('/mes/wm/barcode/config')
}
onMounted(() => {
getList()
})
// TODO @AI scss 使 unocss
</script>
<style scoped lang="scss">
.barcode-preview {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&:hover {
opacity: 0.8;
}
}
</style>