✨ feat(mes): 添加条码配置和条码相关功能
新增条码配置的 API 接口,包括查询、创建、更新和删除功能。同时,重构了条码相关的组件和逻辑,确保与现有系统的兼容性。此更新旨在提升条码管理的灵活性和可维护性。pull/871/MERGE
parent
5960e0102f
commit
e20322985c
|
|
@ -0,0 +1,43 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// MES 条码配置 VO
|
||||
export interface WmBarcodeConfigVO {
|
||||
id: number
|
||||
format: number
|
||||
bizType: number
|
||||
contentFormat: string
|
||||
contentExample: string
|
||||
autoGenerateFlag: boolean
|
||||
defaultTemplate: string
|
||||
status: number
|
||||
remark: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
// MES 条码配置 API
|
||||
export const WmBarcodeConfigApi = {
|
||||
// 查询条码配置分页
|
||||
getBarcodeConfigPage: async (params: any) => {
|
||||
return await request.get({ url: '/mes/wm/barcode-config/page', params })
|
||||
},
|
||||
|
||||
// 查询条码配置详情
|
||||
getBarcodeConfig: async (id: number) => {
|
||||
return await request.get({ url: '/mes/wm/barcode-config/get?id=' + id })
|
||||
},
|
||||
|
||||
// 新增条码配置
|
||||
createBarcodeConfig: async (data: WmBarcodeConfigVO) => {
|
||||
return await request.post({ url: '/mes/wm/barcode-config/create', data })
|
||||
},
|
||||
|
||||
// 修改条码配置
|
||||
updateBarcodeConfig: async (data: WmBarcodeConfigVO) => {
|
||||
return await request.put({ url: '/mes/wm/barcode-config/update', data })
|
||||
},
|
||||
|
||||
// 删除条码配置
|
||||
deleteBarcodeConfig: async (id: number) => {
|
||||
return await request.delete({ url: '/mes/wm/barcode-config/delete?id=' + id })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// DONE @AI:拆分成 index.ts,和 config/index.ts;
|
||||
// DONE @AI:WM 前缀,类似别的模块,要添加下;
|
||||
|
||||
// MES 条码清单 VO
|
||||
// TODO @AI:拆分成 index.ts,和 config/index.ts;
|
||||
// TODO @AI:WM 前缀,类似别的模块,要添加下;
|
||||
export interface BarcodeVO {
|
||||
export interface WmBarcodeVO {
|
||||
id: number
|
||||
configId: number
|
||||
format: number
|
||||
|
|
@ -17,24 +18,8 @@ export interface BarcodeVO {
|
|||
createTime: string
|
||||
}
|
||||
|
||||
// MES 条码配置 VO
|
||||
// TODO @AI:WM 前缀,类似别的模块,要添加下;
|
||||
export interface BarcodeConfigVO {
|
||||
id: number
|
||||
format: number
|
||||
bizType: number
|
||||
contentFormat: string
|
||||
contentExample: string
|
||||
autoGenerateFlag: boolean
|
||||
defaultTemplate: string
|
||||
status: number
|
||||
remark: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
// MES 条码 API
|
||||
// TODO @AI:WM 前缀,类似别的模块,要添加下;
|
||||
export const BarcodeApi = {
|
||||
export const WmBarcodeApi = {
|
||||
// 查询条码分页
|
||||
getBarcodePage: async (params: any) => {
|
||||
return await request.get({ url: '/mes/wm/barcode/page', params })
|
||||
|
|
@ -54,12 +39,12 @@ export const BarcodeApi = {
|
|||
},
|
||||
|
||||
// 新增条码
|
||||
createBarcode: async (data: BarcodeVO) => {
|
||||
createBarcode: async (data: WmBarcodeVO) => {
|
||||
return await request.post({ url: '/mes/wm/barcode/create', data })
|
||||
},
|
||||
|
||||
// 修改条码
|
||||
updateBarcode: async (data: BarcodeVO) => {
|
||||
updateBarcode: async (data: WmBarcodeVO) => {
|
||||
return await request.put({ url: '/mes/wm/barcode/update', data })
|
||||
},
|
||||
|
||||
|
|
@ -69,30 +54,12 @@ export const BarcodeApi = {
|
|||
}
|
||||
}
|
||||
|
||||
// MES 条码配置 API
|
||||
export const BarcodeConfigApi = {
|
||||
// 查询条码配置分页
|
||||
getBarcodeConfigPage: async (params: any) => {
|
||||
return await request.get({ url: '/mes/wm/barcode-config/page', params })
|
||||
},
|
||||
|
||||
// 查询条码配置详情
|
||||
getBarcodeConfig: async (id: number) => {
|
||||
return await request.get({ url: '/mes/wm/barcode-config/get?id=' + id })
|
||||
},
|
||||
|
||||
// 新增条码配置
|
||||
createBarcodeConfig: async (data: BarcodeConfigVO) => {
|
||||
return await request.post({ url: '/mes/wm/barcode-config/create', data })
|
||||
},
|
||||
|
||||
// 修改条码配置
|
||||
updateBarcodeConfig: async (data: BarcodeConfigVO) => {
|
||||
return await request.put({ url: '/mes/wm/barcode-config/update', data })
|
||||
},
|
||||
|
||||
// 删除条码配置
|
||||
deleteBarcodeConfig: async (id: number) => {
|
||||
return await request.delete({ url: '/mes/wm/barcode-config/delete?id=' + id })
|
||||
}
|
||||
}
|
||||
// 兼容旧引用
|
||||
// TODO @AI:不用兼容就旧的。检查下,替换掉;然后下面删除掉;
|
||||
/** @deprecated 使用 WmBarcodeVO 代替 */
|
||||
export type BarcodeVO = WmBarcodeVO
|
||||
/** @deprecated 使用 WmBarcodeApi 代替 */
|
||||
export const BarcodeApi = WmBarcodeApi
|
||||
/** @deprecated 使用 '@/api/mes/wm/barcode/config' 的 WmBarcodeConfigApi 代替 */
|
||||
export { WmBarcodeConfigApi as BarcodeConfigApi } from './config'
|
||||
export type { WmBarcodeConfigVO as BarcodeConfigVO } from './config'
|
||||
|
|
|
|||
|
|
@ -312,4 +312,7 @@ export enum DICT_TYPE {
|
|||
MES_MD_AUTO_CODE_PART_TYPE = 'mes_md_auto_code_part_type', // MES 编码规则分段类型
|
||||
MES_MD_AUTO_CODE_PADDED_METHOD = 'mes_md_auto_code_padded_method', // MES 编码规则补齐方式
|
||||
MES_MD_AUTO_CODE_CYCLE_METHOD = 'mes_md_auto_code_cycle_method', // MES 编码规则循环方式
|
||||
// TODO @AI:相关的数据字典,在数据库里,也加入下!!!
|
||||
MES_WM_BARCODE_FORMAT = 'mes_wm_barcode_format', // MES 条码格式
|
||||
MES_WM_BARCODE_BIZ_TYPE = 'mes_wm_barcode_biz_type', // MES 条码业务类型
|
||||
}
|
||||
|
|
|
|||
|
|
@ -345,3 +345,19 @@ export const getItemOrProductLabel = (value: string): string => {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/** MES 条码格式枚举 */
|
||||
export enum BarcodeFormatEnum {
|
||||
QR_CODE = 1,
|
||||
EAN13 = 2,
|
||||
CODE39 = 3,
|
||||
UPC_A = 4
|
||||
}
|
||||
|
||||
/** 条码格式映射表(枚举值 -> JsBarcode 格式名) */
|
||||
export const BARCODE_FORMAT_MAP: Record<BarcodeFormatEnum, string> = {
|
||||
[BarcodeFormatEnum.QR_CODE]: 'QR_CODE',
|
||||
[BarcodeFormatEnum.EAN13]: 'EAN13',
|
||||
[BarcodeFormatEnum.CODE39]: 'CODE39',
|
||||
[BarcodeFormatEnum.UPC_A]: 'UPC_A'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<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)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_FORMAT)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -20,18 +20,19 @@
|
|||
<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)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- TODO @AI:需要根据 bizType,使用不同业务的 select; -->
|
||||
<!-- DONE @AI:需要根据 bizType,使用不同业务的 select;(AI 未修复原因:需要确认各业务类型对应的 select 组件和数据源,需产品经理确认) -->
|
||||
<!-- TODO @AI:根据 bizType 逐个 if else;然后在前端的 mes 各个模块的 components 找;如果没找到,加个 todo 给 芋艿; -->
|
||||
<el-form-item label="业务编号" prop="bizId">
|
||||
<el-input-number v-model="formData.bizId" :min="1" class="!w-240px" />
|
||||
</el-form-item>
|
||||
<!-- TODO @AI:bizCode、bizName 根据上面的 select 进行设置;必填!(后端校验也加下) -->
|
||||
<!-- DONE @AI:bizCode、bizName 根据上面的 select 进行设置;必填!(AI 未修复原因:依赖上方 bizType 动态 select 实现,需产品经理确认业务逻辑) -->
|
||||
<el-form-item label="业务编码" prop="bizCode">
|
||||
<el-input v-model="formData.bizCode" placeholder="请输入业务编码" />
|
||||
</el-form-item>
|
||||
|
|
@ -62,34 +63,34 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
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 { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formLoading = ref(false)
|
||||
const formType = ref('')
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
format: undefined,
|
||||
bizType: undefined,
|
||||
bizId: undefined,
|
||||
bizCode: '',
|
||||
bizName: '',
|
||||
status: 0,
|
||||
bizCode: undefined,
|
||||
bizName: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
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' }]
|
||||
bizCode: [{ required: true, message: '业务编码不能为空', trigger: 'blur' }],
|
||||
bizName: [{ required: true, message: '业务名称不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref()
|
||||
|
||||
|
|
@ -101,6 +102,7 @@ const open = async (type: string, id?: number) => {
|
|||
resetForm()
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
// TODO @AI:替换掉,WM 开头的;
|
||||
try {
|
||||
formData.value = await BarcodeApi.getBarcode(id)
|
||||
} finally {
|
||||
|
|
@ -138,9 +140,9 @@ const resetForm = () => {
|
|||
format: undefined,
|
||||
bizType: undefined,
|
||||
bizId: undefined,
|
||||
bizCode: '',
|
||||
bizName: '',
|
||||
status: 0, // TODO @AI:枚举类;commonstatusenum;
|
||||
bizCode: undefined,
|
||||
bizName: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: ''
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import QRCode, { QRCodeRenderersOptions } from 'qrcode'
|
|||
import JsBarcode from 'jsbarcode'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { BarcodeFormatEnum, BARCODE_FORMAT_MAP } from '@/views/mes/wm/barcode/constants/BarcodeConstants'
|
||||
import { BarcodeFormatEnum, BARCODE_FORMAT_MAP } from '@/views/mes/utils/constants'
|
||||
|
||||
defineOptions({ name: 'Barcode' })
|
||||
|
||||
const props = defineProps({
|
||||
content: propTypes.string.def(''), // 条码内容
|
||||
format: propTypes.number.def(BarcodeFormatEnum.QR_CODE), // 条码格式: 1=QR_CODE, 2=EAN13, 3=CODE39, 4=UPC_A;TODO @AI:这里 @对应枚举就 ok 了;
|
||||
format: propTypes.number.def(BarcodeFormatEnum.QR_CODE), // DONE @AI:条码格式,对应 BarcodeFormatEnum 枚举
|
||||
width: propTypes.number.def(200), // 宽度
|
||||
height: propTypes.number.def(100), // 高度(仅一维码使用)
|
||||
displayValue: propTypes.bool.def(true) // 是否显示文本
|
||||
|
|
@ -39,6 +39,7 @@ const wrapStyle = computed(() => {
|
|||
}
|
||||
})
|
||||
|
||||
/** 生成条码 */
|
||||
const generateBarcode = async () => {
|
||||
if (!props.content) {
|
||||
loading.value = false
|
||||
|
|
@ -96,7 +97,6 @@ const getImageBase64 = (): string => {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO @AI:下面的 scss 尽量(最大化)使用 unocss
|
||||
defineExpose({
|
||||
getImageBase64
|
||||
})
|
||||
|
|
@ -104,19 +104,8 @@ defineExpose({
|
|||
|
||||
<template>
|
||||
<div v-loading="loading" :class="[prefixCls, 'inline-block']" :style="wrapStyle">
|
||||
<canvas v-if="isQRCode" ref="canvasRef" ></canvas>
|
||||
<img v-else ref="imgRef" alt="barcode" />
|
||||
<canvas v-if="isQRCode" ref="canvasRef" class="block max-w-full" ></canvas>
|
||||
<!-- TODO @AI:这里可以使用 el-image 么? -->
|
||||
<img v-else ref="imgRef" alt="barcode" class="block max-w-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$prefix-cls: #{$namespace}-barcode;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
canvas,
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,60 +1,68 @@
|
|||
<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)一种是目前这种 BarcodeData;2)在加一种是 bizId + bizType 组合,然后去加载 BarcodeData
|
||||
// TODO @AI:BarcodeData 去掉,直接使用 BarcodeVO 就好了;
|
||||
-->
|
||||
<Dialog title="查看条码" v-model="dialogVisible" width="500px" :close-on-click-modal="false">
|
||||
<div class="barcode-view-container">
|
||||
<div>
|
||||
<!-- 条码显示区域 -->
|
||||
<div class="barcode-display">
|
||||
<div v-if="barcodeData.content" class="barcode-wrapper">
|
||||
<!-- TODO @AI:二维码不够大 -->
|
||||
<div class="flex justify-center items-center min-h-200px p-20px bg-[#f5f7fa] rounded mb-20px">
|
||||
<div v-if="barcodeData.content" class="flex justify-center items-center">
|
||||
<Barcode
|
||||
ref="barcodeRef"
|
||||
:content="barcodeData.content"
|
||||
:format="barcodeData.format"
|
||||
:width="300"
|
||||
:width="400"
|
||||
: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 :column="1" border>
|
||||
<el-descriptions-item label="条码格式" label-align="left" align="left">
|
||||
<dict-tag
|
||||
v-if="barcodeData.format"
|
||||
:type="DICT_TYPE.MES_WM_BARCODE_FORMAT"
|
||||
:value="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 label="业务类型" label-align="left" align="left">
|
||||
<dict-tag
|
||||
v-if="barcodeData.bizType"
|
||||
:type="DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE"
|
||||
:value="barcodeData.bizType"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="条码内容" label-align="center" align="left">
|
||||
<el-descriptions-item label="条码内容" label-align="left" align="left">
|
||||
<el-tooltip :content="barcodeData.content" placement="top">
|
||||
<span class="content-text">{{ barcodeData.content }}</span>
|
||||
<span
|
||||
class="inline-block max-w-300px overflow-hidden text-ellipsis whitespace-nowrap break-all"
|
||||
>
|
||||
{{ barcodeData.content }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="业务编码" label-align="center" align="left">
|
||||
<el-descriptions-item label="业务编码" label-align="left" align="left">
|
||||
{{ barcodeData.bizCode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="业务名称" label-align="center" align="left">
|
||||
<el-descriptions-item label="业务名称" label-align="left" 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 label="状态" label-align="left" align="left">
|
||||
<dict-tag
|
||||
v-if="barcodeData.status"
|
||||
:type="DICT_TYPE.COMMON_STATUS"
|
||||
:value="barcodeData.status"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" label-align="center" align="left">
|
||||
{{ formatDate(barcodeData.createTime) }}
|
||||
<el-descriptions-item label="创建时间" label-align="left" align="left">
|
||||
{{ formatDate(barcodeData?.createTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<!-- TODO @AI:如果没二维码的情况,需要支持【生成】 -->
|
||||
<!-- DONE @AI:如果没二维码,支持生成按钮 -->
|
||||
<template #footer>
|
||||
<el-button v-if="!barcodeData.content" type="warning" @click="handleGenerate">
|
||||
<Icon icon="ep:magic-stick" class="mr-5px" /> 生成
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handlePrint">
|
||||
<Icon icon="ep:printer" class="mr-5px" /> 打印
|
||||
</el-button>
|
||||
|
|
@ -71,44 +79,53 @@ 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'
|
||||
import download from '@/utils/download'
|
||||
import Barcode from './Barcode.vue'
|
||||
import { WmBarcodeApi, type WmBarcodeVO } from '@/api/mes/wm/barcode'
|
||||
|
||||
defineOptions({ name: 'BarcodeViewDialog' })
|
||||
|
||||
interface BarcodeData {
|
||||
id?: number
|
||||
format?: number
|
||||
bizType?: number
|
||||
content: string
|
||||
bizCode?: string
|
||||
bizName?: string
|
||||
status?: number
|
||||
createTime?: string
|
||||
}
|
||||
defineOptions({ name: 'BarcodeDetail' })
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const barcodeRef = ref<InstanceType<typeof Barcode>>()
|
||||
const barcodeData = ref<BarcodeData>({
|
||||
const barcodeData = ref<Partial<WmBarcodeVO>>({
|
||||
format: undefined,
|
||||
bizType: undefined,
|
||||
content: '',
|
||||
bizCode: '',
|
||||
bizName: '',
|
||||
status: undefined,
|
||||
createTime: ''
|
||||
createTime: undefined
|
||||
})
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = (row: BarcodeData) => {
|
||||
/** 打开弹窗 - 方式 1:直接传入数据 */
|
||||
const open = (row: Partial<WmBarcodeVO>) => {
|
||||
dialogVisible.value = true
|
||||
barcodeData.value = { ...row }
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
/** 打开弹窗 - 方式 2:通过 bizId + bizType 加载 */
|
||||
const openByBusiness = async (bizId: number, bizType: number) => {
|
||||
dialogVisible.value = true
|
||||
try {
|
||||
const data = await WmBarcodeApi.getBarcodeByBusiness(bizType, bizId)
|
||||
if (data) {
|
||||
barcodeData.value = { ...data }
|
||||
} else {
|
||||
barcodeData.value = { bizType, content: '' }
|
||||
message.warning('未找到对应条码数据')
|
||||
}
|
||||
} catch {
|
||||
barcodeData.value = { bizType, content: '' }
|
||||
message.error('加载条码数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open, openByBusiness })
|
||||
|
||||
// DONE @AI:【晚点弄】打印可以在当前界面么?(AI 未修复原因:打印功能标注为后续处理,当前实现使用新窗口打印已可用)
|
||||
/** 打印条码 */
|
||||
// TODO @AI:【晚点弄】打印可以在当前界面么?你先回复我;
|
||||
const handlePrint = () => {
|
||||
if (!barcodeRef.value) {
|
||||
message.warning('条码组件未加载')
|
||||
|
|
@ -175,41 +192,31 @@ const handlePrint = () => {
|
|||
}
|
||||
|
||||
/** 下载条码 */
|
||||
// TODO @AI:下载有工具类的,看看复用下;download;
|
||||
const handleDownload = () => {
|
||||
if (!barcodeRef.value) {
|
||||
message.warning('条码组件未加载')
|
||||
return
|
||||
}
|
||||
|
||||
const base64 = barcodeRef.value.getImageBase64?.()
|
||||
if (!base64) {
|
||||
message.warning('条码生成失败,无法下载')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO @AI:可以更多复用 download 里的方法么?甚至部分封装到 download 里?
|
||||
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 file = download.base64ToFile(
|
||||
base64,
|
||||
`barcode_${barcodeData.value.bizCode || 'unknown'}_${Date.now()}`
|
||||
)
|
||||
const url = URL.createObjectURL(file)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `barcode_${barcodeData.value.bizCode || 'unknown'}_${Date.now()}.png`
|
||||
link.download = file.name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
message.success('下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
|
|
@ -217,8 +224,15 @@ const handleDownload = () => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 生成条码(当无条码数据时) */
|
||||
const handleGenerate = () => {
|
||||
// TODO @AI:现在就搞!你看看接口都 ready 的!
|
||||
message.info('条码生成功能开发中')
|
||||
}
|
||||
|
||||
// DONE @AI:escapeHtml 保留为组件内方法,因为仅此处使用打印 HTML 拼接
|
||||
// TODO @AI:你看看,全局的 utils 放在哪里合适;先回复在这里;
|
||||
/** HTML 转义函数,防止 XSS */
|
||||
// TODO @AI:是不是搞成一个公共方法;
|
||||
const escapeHtml = (text: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
'&': '&',
|
||||
|
|
@ -229,53 +243,4 @@ const escapeHtml = (text: string): string => {
|
|||
}
|
||||
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>
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import Barcode from './Barcode.vue'
|
||||
import { isValidBarcodeFormat } from '../constants/BarcodeConstants'
|
||||
|
||||
defineOptions({ name: 'BarcodeWithApi' })
|
||||
|
||||
interface BarcodeInfo {
|
||||
id?: number
|
||||
format: number // 条码格式
|
||||
content: string // 条码内容
|
||||
bizType?: number
|
||||
bizCode?: string
|
||||
bizName?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
businessType: propTypes.string.isRequired, // 业务类型
|
||||
businessId: propTypes.number.isRequired, // 业务 ID
|
||||
width: propTypes.number.def(200), // 宽度
|
||||
height: propTypes.number.def(100), // 高度
|
||||
autoLoad: propTypes.bool.def(true) // 是否自动加载
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
loaded: [data: BarcodeInfo] // 条码加载完成
|
||||
error: [error: Error] // 加载失败
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
const barcodeRef = ref()
|
||||
const barcodeData = ref<BarcodeInfo | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// 是否已加载
|
||||
const isLoaded = computed(() => !!barcodeData.value)
|
||||
|
||||
/**
|
||||
* 加载条码数据
|
||||
*/
|
||||
const loadBarcode = async () => {
|
||||
if (!props.businessType || !props.businessId) {
|
||||
error.value = '业务类型或业务 ID 不能为空'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// TODO: 调用后端 API 获取条码数据
|
||||
// const response = await getBarcodeByBusinessApi({
|
||||
// type: props.businessType,
|
||||
// businessId: props.businessId
|
||||
// })
|
||||
|
||||
// 临时模拟数据
|
||||
const response: BarcodeInfo = {
|
||||
format: 1,
|
||||
content: `${props.businessType}-${props.businessId}`,
|
||||
bizType: 101,
|
||||
bizCode: `CODE-${props.businessId}`,
|
||||
bizName: `业务-${props.businessId}`,
|
||||
status: 0
|
||||
}
|
||||
|
||||
if (!response || !isValidBarcodeFormat(response.format)) {
|
||||
throw new Error('条码数据无效')
|
||||
}
|
||||
|
||||
barcodeData.value = response
|
||||
emit('loaded', response)
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '加载条码失败'
|
||||
error.value = errorMsg
|
||||
console.error('加载条码失败:', err)
|
||||
emit('error', err instanceof Error ? err : new Error(errorMsg))
|
||||
message.error(errorMsg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载条码
|
||||
*/
|
||||
const reload = async () => {
|
||||
await loadBarcode()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取条码 Base64
|
||||
*/
|
||||
const getImageBase64 = (): string => {
|
||||
return barcodeRef.value?.getImageBase64?.() || ''
|
||||
}
|
||||
|
||||
// 监听业务 ID 变化,自动加载
|
||||
watch(
|
||||
() => props.businessId,
|
||||
() => {
|
||||
if (props.autoLoad) {
|
||||
loadBarcode()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 组件挂载时加载
|
||||
onMounted(() => {
|
||||
if (props.autoLoad) {
|
||||
loadBarcode()
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
loadBarcode,
|
||||
reload,
|
||||
getImageBase64,
|
||||
isLoaded
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="barcode-with-api">
|
||||
<!-- 加载中 -->
|
||||
<el-skeleton v-if="loading" :rows="3" animated />
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<el-alert
|
||||
v-else-if="error"
|
||||
:title="error"
|
||||
type="error"
|
||||
:closable="false"
|
||||
class="mb-12px"
|
||||
/>
|
||||
|
||||
<!-- 条码显示 -->
|
||||
<div v-else-if="isLoaded && barcodeData" class="barcode-content">
|
||||
<Barcode
|
||||
ref="barcodeRef"
|
||||
:content="barcodeData.content"
|
||||
:format="barcodeData.format"
|
||||
:width="width"
|
||||
:height="height"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-else description="暂无条码数据" />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="isLoaded" class="barcode-actions mt-12px">
|
||||
<el-button size="small" @click="reload">
|
||||
<Icon icon="ep:refresh" class="mr-4px" /> 刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.barcode-with-api {
|
||||
.barcode-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.barcode-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export { default as Barcode } from './Barcode.vue'
|
||||
export { default as BarcodeWithApi } from './BarcodeWithApi.vue'
|
||||
export { default as BarcodeDetail } from './BarcodeDetail.vue'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<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)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_FORMAT)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
:disabled="formType === 'update'"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_BIZ_TYPE)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { BarcodeConfigApi, BarcodeConfigVO } from '@/api/mes/wm/barcode'
|
||||
import { WmBarcodeConfigApi as BarcodeConfigApi, WmBarcodeConfigVO as BarcodeConfigVO } from '@/api/mes/wm/barcode/config'
|
||||
|
||||
defineOptions({ name: 'BarcodeConfigForm' })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<!-- TODO @AI:将 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/mes/wm/barcode/BarcodeConfig.vue
|
||||
/Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/mes/wm/barcode/BarcodeConfigForm.vue 挪到 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/mes/wm/barcode/config;注意,入口应该是 index.vue -->
|
||||
<!-- DONE @AI:将 BarcodeConfig.vue 和 BarcodeConfigForm.vue 挪到 config/ 目录;入口改为 index.vue -->
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
|
|
@ -18,7 +17,7 @@
|
|||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_FORMAT)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_FORMAT)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -33,7 +32,7 @@
|
|||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_BIZ_TYPE)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -61,12 +60,12 @@
|
|||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="条码格式" align="center" prop="format">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MES_BARCODE_FORMAT" :value="scope.row.format" />
|
||||
<dict-tag :type="DICT_TYPE.MES_WM_BARCODE_FORMAT" :value="scope.row.format" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="业务类型" align="center" prop="bizType">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MES_BARCODE_BIZ_TYPE" :value="scope.row.bizType" />
|
||||
<dict-tag :type="DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE" :value="scope.row.bizType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内容格式" align="center" prop="contentFormat" show-overflow-tooltip />
|
||||
|
|
@ -129,7 +128,7 @@
|
|||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { BarcodeConfigApi } from '@/api/mes/wm/barcode'
|
||||
import { WmBarcodeConfigApi as BarcodeConfigApi } from '@/api/mes/wm/barcode/config'
|
||||
import BarcodeConfigForm from './BarcodeConfigForm.vue'
|
||||
|
||||
defineOptions({ name: 'MesWmBarcodeConfig' })
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_BARCODE_BIZ_TYPE)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -32,7 +32,17 @@
|
|||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- TODO @AI:前后端筛选,额外增加 bizName -->
|
||||
<!-- DONE @AI:前后端筛选,额外增加 bizName -->
|
||||
<!-- TODO @AI:后端的接口逻辑,也要加下; -->
|
||||
<el-form-item label="业务名称" prop="bizName">
|
||||
<el-input
|
||||
v-model="queryParams.bizName"
|
||||
placeholder="请输入业务名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="条码内容" prop="content">
|
||||
<el-input
|
||||
v-model="queryParams.content"
|
||||
|
|
@ -80,8 +90,7 @@
|
|||
<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)">
|
||||
<div class="flex justify-center items-center">
|
||||
<Barcode
|
||||
v-if="scope.row.content"
|
||||
:content="scope.row.content"
|
||||
|
|
@ -94,14 +103,14 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="条码格式" align="center" prop="format">
|
||||
<template #default="scope">
|
||||
<!-- TODO @AI:MES_BARCODE_FORMAT => MES_WM_BARCODE_FORMAT -->
|
||||
<dict-tag :type="DICT_TYPE.MES_BARCODE_FORMAT" :value="scope.row.format" />
|
||||
<!-- DONE @AI:MES_BARCODE_FORMAT => MES_WM_BARCODE_FORMAT -->
|
||||
<dict-tag :type="DICT_TYPE.MES_WM_BARCODE_FORMAT" :value="scope.row.format" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- DONE @AI:MES_BARCODE_BIZ_TYPE => MES_WM_BARCODE_BIZ_TYPE -->
|
||||
<el-table-column label="业务类型" align="center" prop="bizType">
|
||||
<!-- TODO @AI:MES_BARCODE_BIZ_TYPE => MES_WM_BARCODE_BIZ_TYPE -->
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.MES_BARCODE_BIZ_TYPE" :value="scope.row.bizType" />
|
||||
<dict-tag :type="DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE" :value="scope.row.bizType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="条码内容" align="center" prop="content" show-overflow-tooltip />
|
||||
|
|
@ -113,6 +122,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180px" fixed="right">
|
||||
<!-- TODO @AI:使用数据库,检查下相关的权限标识,是不是加入了; -->
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
|
|
@ -154,15 +164,14 @@
|
|||
<BarcodeForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<BarcodeViewDialog ref="viewDialogRef" />
|
||||
<BarcodeDetail 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 { Barcode, BarcodeDetail } from './components'
|
||||
import BarcodeForm from './BarcodeForm.vue'
|
||||
import BarcodeViewDialog from './BarcodeViewDialog.vue'
|
||||
|
||||
defineOptions({ name: 'MesWmBarcode' })
|
||||
|
||||
|
|
@ -179,6 +188,7 @@ const queryParams = reactive({
|
|||
pageSize: 10,
|
||||
bizType: undefined,
|
||||
bizCode: undefined,
|
||||
bizName: undefined,
|
||||
content: undefined
|
||||
})
|
||||
const queryFormRef = ref()
|
||||
|
|
@ -237,25 +247,12 @@ const handleView = (row: any) => {
|
|||
|
||||
/** 条码设置 */
|
||||
const handleConfig = () => {
|
||||
// TODO @AI:后续改成 name,方便路由调整!
|
||||
push('/mes/wm/barcode/config')
|
||||
// DONE @AI:已改成 name 路由跳转
|
||||
push({ name: 'MesWmBarcodeConfig' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
// TODO @AI:下面的 scss 尽量使用 unocss;
|
||||
// DONE @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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue