liu
2026-10-23 077d36ebdf699961b9e66dd21e13b509240f8f79
feat: 完善设备数采与标签打印功能
已修改15个文件
273 ■■■■■ 文件已修改
.env.production 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/telemetry/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/template/data.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/template/modules/form.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/components/printer-label.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/itemreceipt/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/outsourcereceipt/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productreceipt/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnissue/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/transfer/modules/line-list.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production
@@ -2,10 +2,10 @@
VITE_BASE=/
# 请求路径
VITE_BASE_URL=http://1.95.78.124:9006
# 接口地址
VITE_GLOB_API_URL=http://1.95.78.124:9006/admin-api
# 请求路径(留空表示同源,由前端按当前访问域名端口拼接)
VITE_BASE_URL=
# 接口地址(相对路径,跟随当前访问域名端口,经 nginx 反代到后端)
VITE_GLOB_API_URL=/admin-api
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
@@ -25,4 +25,4 @@
VITE_ARCHIVER=true
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=true
VITE_APP_CAPTCHA_ENABLE=true
company.config.ts
@@ -21,7 +21,7 @@
    name: '新高数字工厂控制中心',
    logo: '/logos/xgdl.svg',
    favicon: '/logos/xgdl.svg',
    apiUrl: 'http://1.95.78.124:9006/admin-api',
    apiUrl: '/admin-api',
  },
  JHHG: {
    name: '宁夏九泓化工',
src/packages/constants/src/biz-mes-enum.ts
@@ -154,6 +154,8 @@
  MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
  MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
  PD_PROJECT_CODE: 'PD_PROJECT_CODE',
  PD_PROCESS_PARAM_CODE: 'PD_PROCESS_PARAM_CODE',
  PD_PROCESS_PARAM_TEMPLATE_CODE: 'PD_PROCESS_PARAM_TEMPLATE_CODE',
  PD_TASK_CODE: 'PD_TASK_CODE',
  PD_VERSION_CODE: 'PD_VERSION_CODE',
  PD_DOCUMENT_AUDIT_CODE: 'PD_DOCUMENT_AUDIT_CODE',
src/views/mes/dv/telemetry/index.vue
@@ -104,14 +104,14 @@
    }
  }
  /** 切换实时自动刷新(每 30 秒) */
  /** 切换自动刷新(每 30 分钟) */
  function toggleAutoRefresh(checked: boolean | string | number) {
    if (checked) {
      refreshTimer = setInterval(() => {
        if (activeTab.value === 'latest') {
          latestGridApi.query();
        }
      }, 30_000);
      }, 1_800_000);
    } else if (refreshTimer) {
      clearInterval(refreshTimer);
      refreshTimer = undefined;
@@ -135,8 +135,8 @@
        </Button>
        <Switch
          v-model:checked="autoRefresh"
          checked-children="实时自动刷新"
          un-checked-children="实时自动刷新"
          checked-children="自动刷新(30分钟)"
          un-checked-children="自动刷新(30分钟)"
          @change="toggleAutoRefresh"
        />
        <span v-if="lastUpdateTime" class="text-sm text-gray-500">
src/views/mes/process-design/template/data.ts
@@ -1,17 +1,25 @@
import type { VbenFormSchema } from "#/adapter/form";
import type { VbenFormApi, VbenFormSchema } from "#/adapter/form";
import type { VxeTableGridOptions } from "#/adapter/vxe-table";
import type { MesPdTemplateApi } from "#/api/mes/pro/processdesign/template";
import { DICT_TYPE } from "@vben/constants";
import { h } from "vue";
import { DICT_TYPE, MesAutoCodeRuleCode } from "@vben/constants";
import { getDictOptions } from "@vben/hooks";
import { Button } from "ant-design-vue";
import { z } from "#/adapter/form";
import { generateAutoCode } from "#/api/mes/md/autocode/record";
/** 表单类型 */
export type FormType = "create" | "detail" | "update";
/** 新增/修改工艺参数模板的表单 */
export function useFormSchema(): VbenFormSchema[] {
export function useFormSchema(
  formType: FormType,
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: "id",
@@ -27,9 +35,26 @@
      component: "Input",
      componentProps: {
        maxLength: 64,
        placeholder: "模板编码自动生成",
        disabled: true,
        placeholder: "请输入模板编码,留空将自动生成",
      },
      rules: z.string().max(64).optional(),
      suffix:
        formType === "detail"
          ? undefined
          : () =>
              h(
                Button,
                {
                  type: "default",
                  onClick: async () => {
                    const code = await generateAutoCode(
                      MesAutoCodeRuleCode.PD_PROCESS_PARAM_TEMPLATE_CODE,
                    );
                    await formApi?.setFieldValue("templateCode", code);
                  },
                },
                { default: () => "自动生成" },
              ),
    },
    {
      fieldName: "templateName",
src/views/mes/process-design/template/modules/form.vue
@@ -118,9 +118,9 @@
        selectedDetails.value = [];
        return;
      }
      formApi.setState({ schema: useFormSchema() });
      const data = modalApi.getData<{ formType: FormType; id?: number }>();
      formType.value = data.formType;
      formApi.setState({ schema: useFormSchema(formType.value, formApi) });
      formApi.setDisabled(formType.value === "detail");
      modalApi.setState({ showConfirmButton: formType.value !== "detail" });
      if (!data?.id) {
src/views/wls/barcode/components/printer-label.vue
@@ -1,30 +1,220 @@
<script lang="ts" setup>
import type { MesWmBarcodeApi } from '#/api/mes/wm/barcode';
import { ref } from 'vue';
import { BarcodeBizTypeEnum } from '@vben/constants';
import { Button, message } from 'ant-design-vue';
import {
  getBarcodeByBusiness,
  renderBarcodeImage,
} from '#/api/mes/wm/barcode';
defineOptions({ name: 'WlsPrinterLabel' });
withDefaults(
const props = withDefaults(
  defineProps<{
    bizCode?: string;
    bizId?: number;
    bizType?: string;
    bizName?: string;
    bizType?: number | string;
    labelText?: string;
  }>(),
  {
    bizCode: undefined,
    bizId: undefined,
    bizName: undefined,
    bizType: undefined,
    labelText: '标签打印',
  },
);
function handlePrint() {
  message.warning('标签打印功能暂未实现,敬请期待');
const printing = ref(false);
function resolveBizType(value?: number | string) {
  if (typeof value === 'number') {
    return value;
  }
  const normalized = value?.trim().toUpperCase();
  if (!normalized) {
    return undefined;
  }
  // ITEM_BATCH 是部分入库页面沿用的旧别名,实际仍对应“批次”。
  if (normalized === 'ITEM_BATCH') {
    return BarcodeBizTypeEnum.BATCH;
  }
  const numericValue = Number(normalized);
  if (Number.isInteger(numericValue)) {
    return numericValue;
  }
  const enumValue =
    BarcodeBizTypeEnum[
      normalized as keyof typeof BarcodeBizTypeEnum
    ];
  return typeof enumValue === 'number' ? enumValue : undefined;
}
function getBizTypeLabel(bizType: number) {
  const labels: Partial<Record<BarcodeBizTypeEnum, string>> = {
    [BarcodeBizTypeEnum.AREA]: '库位',
    [BarcodeBizTypeEnum.BATCH]: '批次',
    [BarcodeBizTypeEnum.CLIENT]: '客户',
    [BarcodeBizTypeEnum.ITEM]: '物料',
    [BarcodeBizTypeEnum.LOCATION]: '库区',
    [BarcodeBizTypeEnum.MACHINERY]: '设备',
    [BarcodeBizTypeEnum.PACKAGE]: '装箱单',
    [BarcodeBizTypeEnum.PROCARD]: '流转卡',
    [BarcodeBizTypeEnum.SN]: 'SN 码',
    [BarcodeBizTypeEnum.STOCK]: '库存',
    [BarcodeBizTypeEnum.TOOL]: '工具',
    [BarcodeBizTypeEnum.USER]: '人员',
    [BarcodeBizTypeEnum.VENDOR]: '供应商',
    [BarcodeBizTypeEnum.WAREHOUSE]: '仓库',
    [BarcodeBizTypeEnum.WORKORDER]: '工单',
    [BarcodeBizTypeEnum.WORKSHOP]: '车间',
    [BarcodeBizTypeEnum.WORKSTATION]: '工作站',
  };
  return labels[bizType as BarcodeBizTypeEnum] || '业务';
}
function escapeHtml(value?: null | string) {
  return (value || '')
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}
async function getBarcodeForPrint(bizType: number) {
  const barcode = await getBarcodeByBusiness(bizType, props.bizId!);
  if (barcode?.content) {
    return barcode;
  }
  if (!props.bizCode) {
    throw new Error('缺少业务编码,无法打印标签');
  }
  // 历史批次可能未配置条码规则。此时使用业务编码即时生成 CODE128,
  // 只用于本次打印,不写入条码表,避免“条码配置不存在”阻断业务。
  return {
    bizCode: props.bizCode,
    bizId: props.bizId,
    bizName: props.bizName || props.bizCode,
    bizType,
    content: props.bizCode,
  } satisfies MesWmBarcodeApi.Barcode;
}
function writePrintDocument(
  printWindow: Window,
  barcode: MesWmBarcodeApi.Barcode,
  imageBase64: string,
  bizType: number,
) {
  const typeLabel = getBizTypeLabel(bizType);
  const isQrCode = barcode.format === 1;
  printWindow.document.open();
  printWindow.document.write(`<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>${escapeHtml(typeLabel)}标签</title>
  <style>
    @page { size: 80mm 55mm; margin: 0; }
    * { box-sizing: border-box; }
    html, body { width: 80mm; height: 55mm; margin: 0; color: #000; font-family: "Microsoft YaHei", Arial, sans-serif; }
    .label { width: 80mm; height: 55mm; padding: 3mm; border: .35mm solid #000; display: grid; grid-template-columns: ${isQrCode ? '31mm 1fr' : '1fr'}; gap: 3mm; overflow: hidden; }
    .barcode { display: flex; align-items: center; justify-content: center; min-width: 0; }
    .barcode img { display: block; max-width: 100%; max-height: ${isQrCode ? '38mm' : '22mm'}; object-fit: contain; }
    .info { min-width: 0; display: flex; flex-direction: column; justify-content: center; }
    .title { margin-bottom: 2mm; padding-bottom: 1.5mm; border-bottom: .3mm solid #000; font-size: 15pt; font-weight: 700; text-align: center; }
    .row { margin: .8mm 0; font-size: 9.5pt; line-height: 1.35; overflow-wrap: anywhere; }
    .key { font-weight: 700; }
    .content { margin-top: 1mm; font-family: Consolas, monospace; font-size: 7.5pt; text-align: center; overflow-wrap: anywhere; }
    ${isQrCode ? '' : '.label { grid-template-rows: auto 1fr; } .barcode { order: 2; } .info { order: 1; } .title { margin-bottom: 1mm; } .row { display: inline-block; margin-right: 4mm; }'}
    @media screen { body { margin: 8px; } }
  </style>
</head>
<body>
  <main class="label">
    <section class="barcode">
      <img src="${imageBase64}" alt="${escapeHtml(typeLabel)}条码" />
    </section>
    <section class="info">
      <div class="title">${escapeHtml(typeLabel)}标签</div>
      <div class="row"><span class="key">业务编码:</span>${escapeHtml(barcode.bizCode || props.bizCode)}</div>
      <div class="row"><span class="key">业务名称:</span>${escapeHtml(barcode.bizName || props.bizName || '-')}</div>
      <div class="content">${escapeHtml(barcode.content)}</div>
    </section>
  </main>
  <script>
    window.addEventListener('load', function () {
      window.focus();
      window.onafterprint = function () { window.close(); };
      setTimeout(function () { window.print(); }, 150);
    });
  <\/script>
</body>
</html>`);
  printWindow.document.close();
}
async function handlePrint() {
  if (printing.value) {
    return;
  }
  const bizType = resolveBizType(props.bizType);
  if (!props.bizId || !bizType) {
    message.warning('缺少业务类型或业务编号,无法打印标签');
    return;
  }
  // 在用户点击事件中同步打开窗口,避免异步请求结束后被浏览器拦截。
  const printWindow = window.open('', '_blank', 'width=900,height=650');
  if (!printWindow) {
    message.error('无法打开打印窗口,请允许浏览器弹出窗口后重试');
    return;
  }
  printWindow.opener = null;
  printWindow.document.write(
    '<!doctype html><html><head><meta charset="UTF-8"><title>生成标签</title></head><body style="font-family:Microsoft YaHei;padding:24px">正在生成标签,请稍候……</body></html>',
  );
  printWindow.document.close();
  printing.value = true;
  try {
    const barcode = await getBarcodeForPrint(bizType);
    const isQrCode = barcode.format === 1;
    const imageBase64 = await renderBarcodeImage(
      barcode.content!,
      barcode.format,
      isQrCode ? 360 : 720,
      isQrCode ? 360 : 220,
    );
    if (!imageBase64?.startsWith('data:image/png;base64,')) {
      throw new Error('服务端未返回有效的标签图片');
    }
    if (!printWindow.closed) {
      writePrintDocument(printWindow, barcode, imageBase64, bizType);
    }
  } catch (error) {
    if (!printWindow.closed) {
      printWindow.close();
    }
    const errorMessage =
      error instanceof Error ? error.message : '标签打印失败,请稍后重试';
    message.error(errorMessage);
  } finally {
    printing.value = false;
  }
}
</script>
<template>
  <Button type="link" @click="handlePrint">
  <Button :loading="printing" type="link" @click="handlePrint">
    {{ labelText }}
  </Button>
</template>
src/views/wls/itemreceipt/modules/line-list.vue
@@ -232,6 +232,7 @@
            v-if="isStock"
            :biz-code="row.batchCode"
            :biz-id="row.batchId"
            :biz-name="row.itemName"
            biz-type="ITEM_BATCH"
          />
        </div>
src/views/wls/outsourcereceipt/modules/line-list.vue
@@ -227,6 +227,7 @@
            v-if="isStock"
            :biz-code="row.batchCode"
            :biz-id="row.batchId"
            :biz-name="row.itemName"
            biz-type="BATCH"
          />
        </div>
src/views/wls/productreceipt/modules/line-list.vue
@@ -244,6 +244,7 @@
            v-if="isStock"
            :biz-code="row.batchCode"
            :biz-id="row.batchId"
            :biz-name="row.itemName"
            biz-type="BATCH"
          />
        </div>
src/views/wls/returnissue/modules/line-list.vue
@@ -240,6 +240,7 @@
            v-if="isStock"
            :biz-code="row.batchCode"
            :biz-id="row.batchId"
            :biz-name="row.itemName"
            biz-type="BATCH"
          />
        </div>
src/views/wls/returnsales/modules/line-list.vue
@@ -242,6 +242,7 @@
            v-if="isStock"
            :biz-code="row.batchCode"
            :biz-id="row.batchId"
            :biz-name="row.itemName"
            biz-type="BATCH"
          />
        </div>
src/views/wls/returnvendor/modules/line-list.vue
@@ -228,6 +228,7 @@
            v-if="isStock"
            :biz-code="row.batchCode"
            :biz-id="row.batchId"
            :biz-name="row.itemName"
            biz-type="BATCH"
          />
        </div>
src/views/wls/transfer/modules/line-list.vue
@@ -220,6 +220,7 @@
          v-if="isStock"
          :biz-code="row.batchCode"
          :biz-id="row.batchId"
          :biz-name="row.itemName"
          biz-type="BATCH"
        />
      </template>
vite.config.ts
@@ -20,7 +20,12 @@
  };
  if (!isDev && appApiUrl) {
    defineVars['import.meta.env.VITE_GLOB_API_URL'] = JSON.stringify(appApiUrl);
    defineVars['import.meta.env.VITE_BASE_URL'] = JSON.stringify(new URL(appApiUrl).origin);
    // 支持相对路径(如 /admin-api):无 origin 时留空,前端各处以同源方式拼接
    try {
      defineVars['import.meta.env.VITE_BASE_URL'] = JSON.stringify(new URL(appApiUrl).origin);
    } catch {
      defineVars['import.meta.env.VITE_BASE_URL'] = JSON.stringify('');
    }
  }
  return {