Merge remote-tracking branch 'origin/dev_新疆_新高电力' into dev_新疆_新高电力
已添加3个文件
已修改12个文件
855 ■■■■■ 文件已修改
PRODUCT.md 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/access.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/routerHelper.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/components/select-dialog.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/data.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/material-input-list.vue 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/process-form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/process-list.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/product-bom-list.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/product-form.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/product-list.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/route-configurator.vue 413 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
PRODUCT.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
# Product
<!-- impeccable:product-schema 1 -->
## Platform
web
## Users
- ç”Ÿäº§å·¥è‰ºã€è®¡åˆ’和车间管理人员在桌面端维护生产基础数据。
- ç”¨æˆ·éœ€è¦åœ¨è¾ƒå°‘培训的情况下完成工艺路线、产出产品、入库位置和工序产出的配置。
## Product Purpose
新高电力智造平台用于贯通产品、工艺、生产、质量和仓储业务。工艺路线配置成功的标准,是用户能明确知道每道工序产出什么,以及合格产出最终进入哪个仓库、库区和库位。
## Operating Context
- ä¸€æ¡å·¥è‰ºè·¯çº¿å¯å…³è”多个最终产品。
- æ¯ä¸ªæœ€ç»ˆäº§å“å¿…须配置报工对应的仓库、库区和库位。
- ä¸­é—´å·¥åºäº§å‡ºåŠæˆå“ï¼Œæœ€åŽä¸€é“工序必须产出路线绑定的全部成品。
- è·¯çº¿åœç”¨æ—¶æ‰å…è®¸ä¿®æ”¹æˆ–删除;配置完整后在列表中启用。
## Capabilities and Constraints
- æŠ€æœ¯æ ˆä¸º Vue 3、TypeScript、Vben Admin、Ant Design Vue å’Œ VXE Table。
- ä¿ç•™çŽ°æœ‰åŽç«¯æŽ¥å£å’Œä¸šåŠ¡æ ¡éªŒï¼Œä¸æ”¹å˜æ•°æ®æ¨¡åž‹ã€‚
- é¡µé¢åº”兼容亮色、暗色主题及常见桌面分辨率。
- å½“前事实来自用户明确需求和现有代码;是否需要移动端维护工艺路线尚未确认。
## Brand Commitments
- äº§å“åç§°ä¸ºâ€œæ–°é«˜ç”µåŠ›æ™ºé€ å¹³å°â€ã€‚
- æ²¿ç”¨å½“前系统的蓝色主色、Ant Design æŽ§ä»¶å’Œä¸­æ–‡ä¸šåŠ¡æœ¯è¯­ã€‚
## Evidence on Hand
- çŽ°æœ‰å·¥è‰ºè·¯çº¿é¡µé¢å’ŒæŽ¥å£ä½äºŽ `src/views/mes/pro/route` ä¸Ž `src/api/mes/pro/route`。
- åŽç«¯å·²å®žçŽ°å·¥è‰ºè·¯çº¿äº§å“ä»“ä½åŠå·¥åºäº§å‡ºå®Œæ•´æ€§æ ¡éªŒã€‚
- æ²¡æœ‰ç‹¬ç«‹ç”¨æˆ·ç ”究材料或可用性测试录像,不虚构量化效果。
## Product Principles
- ä¸šåŠ¡é¡ºåºå°±æ˜¯ç•Œé¢é¡ºåºï¼šå…ˆäº§å“å’Œå…¥åº“ä½ç½®ï¼Œå†å·¥åºå’Œäº§å‡ºï¼Œæœ€åŽæŒ‰äº§å“ä¸Žå·¥åºé…ç½®æŠ•æ–™ã€‚
- åœ¨ç”¨æˆ·æäº¤å‰è§£é‡Šè§„则,并在当前步骤就近暴露缺失项。
- é‡è¦çŠ¶æ€åŒæ—¶ä½¿ç”¨æ–‡å­—ã€å›¾æ ‡å’Œé¢œè‰²è¡¨è¾¾ã€‚
- é»˜è®¤æµç¨‹æœåŠ¡æ™®é€šä¸šåŠ¡ç”¨æˆ·ï¼Œé«˜çº§å­—æ®µä¸æŠ¢å ä¸»è¦æ“ä½œè·¯å¾„ã€‚
## Accessibility & Inclusion
- äº¤äº’目标应满足键盘操作和清晰焦点状态。
- æ­£æ–‡ä¸ŽæŽ§ä»¶æ–‡å­—保持可读对比度,不能仅依赖颜色区分完成状态。
src/router/access.ts
@@ -44,6 +44,10 @@
  const pageMap: ComponentRecordType = import.meta.glob([
    '../views/**/*.vue',
    '!../views/ai/**/*.vue',
    '!../views/dashboard/analytics/modules/ai-insights.vue',
    '!../views/mes/**/modules/ai/**/*.vue',
    '!../views/mes/qc/components/ai-*.vue',
    '!../views/crm/saleQuotation/modules/ocr-upload.vue',
  ]);
  const accessStore = useAccessStore();
src/router/routes/index.ts
@@ -2,11 +2,12 @@
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { isAiFeatureEnabled } from '#/utils/features';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
const dynamicRouteFiles = import.meta.glob([
  './modules/**/*.ts',
  '!./modules/ai.ts',
], {
  eager: true,
});
@@ -39,10 +40,12 @@
// add by èŠ‹è‰¿ï¼šfrom https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45
const componentKeys: string[] = Object.keys(
  import.meta.glob('../../views/**/*.vue'),
  import.meta.glob([
    '../../views/**/*.vue',
    '!../../views/**/modules/**',
    '!../../views/ai/**',
  ]),
)
  .filter((item) => !item.includes('/modules/'))
  .filter((item) => isAiFeatureEnabled || !item.includes('/views/ai/'))
  .map((v) => {
    const path = v.replace('../../views/', '/');
    return path.endsWith('.vue') ? path.slice(0, -4) : path;
src/utils/routerHelper.ts
@@ -5,7 +5,14 @@
import { defineAsyncComponent } from 'vue';
const modules = import.meta.glob('../views/**/*.{vue,tsx}');
const modules = import.meta.glob([
  '../views/**/*.{vue,tsx}',
  '!../views/ai/**',
  '!../views/dashboard/analytics/modules/ai-insights.vue',
  '!../views/mes/**/modules/ai/**',
  '!../views/mes/qc/components/ai-*.vue',
  '!../views/crm/saleQuotation/modules/ocr-upload.vue',
]);
/**
 * æ³¨å†Œä¸€ä¸ªå¼‚步组件
src/views/mes/md/item/components/select-dialog.vue
@@ -3,11 +3,11 @@
  import type { MesMdItemApi } from "#/api/mes/md/item";
  import type { MesMdItemTypeApi } from "#/api/mes/md/item/type";
  import { nextTick, ref } from "vue";
  import { computed, nextTick, ref } from "vue";
  import { CommonStatusEnum } from "@vben/constants";
  import { Card, message, Modal } from "ant-design-vue";
  import { Alert, Card, message, Modal, Tag } from "ant-design-vue";
  import { useVbenVxeGrid } from "#/adapter/vxe-table";
  import { getItemPage } from "#/api/mes/md/item";
@@ -28,6 +28,13 @@
  const preSelectedIds = ref<number[]>([]); // é¢„选物料编号列表
  const productIds = ref<number[]>([]); // é™å®šå¯é€‰çš„物料编号列表(为空则不限制)
  const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // ç‰©æ–™åˆ†ç±»æ ‘
  const limitedToProducts = computed(() => productIds.value.length > 0);
  const dialogTitle = computed(() =>
    limitedToProducts.value ? "路线产出选择" : "物料产品选择",
  );
  const tableTitle = computed(() =>
    limitedToProducts.value ? "已配置的产出产品" : "物料产品列表",
  );
  /** èŽ·å–å½“å‰è¡¨æ ¼æ•°æ® */
  function getTableRows() {
@@ -140,7 +147,6 @@
            };
            // é™å®šå¯é€‰ç‰©æ–™èŒƒå›´
            if (productIds.value.length > 0) {
              params.pageSize = 1000;
              params.ids = productIds.value.join(',');
            }
            const result = await getItemPage(params as never);
@@ -219,21 +225,34 @@
<template>
  <Modal v-model:open="open"
         title="物料产品选择"
         :title="dialogTitle"
         width="80%"
         :destroy-on-close="true"
         @ok="handleConfirm"
         @cancel="closeModal">
    <div class="flex h-[60vh] w-full">
      <Card
        v-if="!limitedToProducts"
        class="mr-4 h-full w-1/5"
        :body-style="{ height: '100%', overflow: 'hidden' }"
      >
        <MdItemTypeTree ref="typeTreeRef"
                        @node-click="handleItemTypeNodeClick" />
      </Card>
      <div class="w-4/5">
        <Grid table-title="物料产品列表" />
      <div :class="limitedToProducts ? 'w-full' : 'w-4/5'">
        <Alert
          v-if="limitedToProducts"
          class="mb-3"
          message="仅可选择本路线已配置的产出产品"
          description="如需增加可选项,请返回“产出产品及入库仓”步骤先添加产出。"
          show-icon
          type="info"
        >
          <template #action>
            <Tag color="blue">{{ productIds.length }} ä¸ªäº§å‡º</Tag>
          </template>
        </Alert>
        <Grid :table-title="tableTitle" />
      </div>
    </div>
  </Modal>
src/views/mes/pro/route/data.ts
@@ -220,6 +220,17 @@
      rules: 'selectRequired',
    },
    {
      fieldName: 'outputItemIds',
      label: '本工序产出',
      component: markRaw(MdItemSelect),
      componentProps: {
        multiple: true,
        productIds,
        placeholder: '可留空;形成半成品时选择,末道工序选择全部成品',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'linkType',
      label: '与下道工序关系',
      component: 'Select',
@@ -256,15 +267,14 @@
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'outputItemIds',
      label: '产出产品',
      component: markRaw(MdItemSelect),
      fieldName: 'backflushFlag',
      label: '报工自动扣料',
      component: 'Switch',
      componentProps: {
        multiple: true,
        productIds,
        placeholder: '中间工序选半成品,末道工序选全部成品',
        checkedChildren: '自动',
        unCheckedChildren: '手动',
      },
      formItemClass: 'col-span-2',
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'prepareTime',
@@ -308,6 +318,7 @@
    { field: 'sort', title: '序号', width: 70, align: 'center', fixed: 'left' },
    { field: 'processCode', title: '工序编码', width: 140, fixed: 'left' },
    { field: 'processName', title: '工序名称', width: 140, fixed: 'left' },
    { field: 'outputItemNames', title: '本工序产出', minWidth: 190 },
    { field: 'nextProcessName', title: '下一道工序', width: 140 },
    {
      field: 'linkType',
@@ -336,9 +347,17 @@
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'backflushFlag',
      title: '自动扣料',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    { field: 'prepareTime', title: '准备时间(分)', width: 110 },
    { field: 'waitTime', title: '等待时间(分)', width: 110 },
    { field: 'outputItemNames', title: '产出产品', minWidth: 180 },
    {
      field: 'colorCode',
      title: '甘特图颜色',
@@ -358,14 +377,14 @@
/** å·¥è‰ºè·¯çº¿äº§å“åˆ—表字段 */
export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] {
  return [
    { field: 'itemCode', title: '产品物料编码', width: 150 },
    { field: 'itemName', title: '产品物料名称', width: 150 },
    { field: 'itemCode', title: '产出编码', width: 150 },
    { field: 'itemName', title: '产出名称', width: 150 },
    { field: 'specification', title: '规格型号', width: 150 },
    { field: 'unitName', title: '单位', width: 80 },
    { field: 'quantity', title: '生产数量', width: 100 },
    { field: 'warehouseName', title: '报工仓库', width: 130 },
    { field: 'locationName', title: '库区', width: 120 },
    { field: 'areaName', title: '库位', width: 120 },
    { field: 'warehouseName', title: '报工入库仓', width: 140 },
    { field: 'locationName', title: '入库库区', width: 120 },
    { field: 'areaName', title: '入库库位', width: 120 },
    {
      field: 'productionTime',
      title: '生产用时',
@@ -385,8 +404,8 @@
/** å·¥è‰ºè·¯çº¿äº§å“ BOM åˆ—表字段 */
export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] {
  return [
    { field: 'itemCode', title: 'BOM ç‰©æ–™ç¼–码', width: 150 },
    { field: 'itemName', title: 'BOM ç‰©æ–™åç§°', width: 150 },
    { field: 'itemCode', title: '投料编码', width: 150 },
    { field: 'itemName', title: '投料名称', width: 150 },
    { field: 'specification', title: '规格型号', width: 150 },
    { field: 'unitName', title: '单位', width: 80 },
    { field: 'quantity', title: '用料比例', width: 100 },
src/views/mes/pro/route/index.vue
@@ -8,7 +8,7 @@
import { getDictLabel } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tooltip } from 'ant-design-vue';
import { Alert, Button, message, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -23,7 +23,7 @@
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import Form from './modules/route-configurator.vue';
const { hasAccessByCodes } = useAccess();
const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ˜¯å¦å¯åˆ‡æ¢çŠ¶æ€
@@ -77,7 +77,9 @@
): Promise<boolean | undefined> {
  try {
    await confirm(
      `确认要将"${row.name}"工艺路线切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}】吗?`,
      newStatus === CommonStatusEnum.ENABLE
        ? `确认启用“${row.name}”吗?系统将检查产出入库位置、工序产出和工序投料是否完整。`
        : `确认要将“${row.name}”工艺路线切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}】吗?`,
    );
  } catch {
    return false;
@@ -139,6 +141,13 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Alert
      class="mb-3"
      message="路线配置顺序"
      description="基本信息 â†’ äº§å‡ºäº§å“åŠå…¥åº“仓 â†’ å·¥åºåŠå„工序产出 â†’ å·¥åºæŠ•æ–™ â†’ æ ¡éªŒå¯ç”¨"
      show-icon
      type="info"
    />
    <Grid table-title="工艺路线列表">
      <template #toolbar-tools>
        <TableAction
src/views/mes/pro/route/modules/material-input-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,145 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { computed, ref, watch } from 'vue';
import { Alert, Button, Empty, Tag } from 'ant-design-vue';
import { getRouteProductBomList } from '#/api/mes/pro/route/productbom';
import ProductBomList from './product-bom-list.vue';
const props = defineProps<{
  formType: FormType;
  processes: MesProRouteProcessApi.RouteProcess[];
  products: MesProRouteProductApi.RouteProduct[];
  routeId: number;
}>();
const emit = defineEmits<{
  change: [list: MesProRouteProductBomApi.RouteProductBom[]];
}>();
const activeProductId = ref<number>();
const allBomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]);
const activeProduct = computed(() =>
  props.products.find((item) => item.itemId === activeProductId.value),
);
function materialCount(productId?: number) {
  if (!productId) return 0;
  return allBomList.value.filter((item) => item.productId === productId).length;
}
async function loadAll() {
  allBomList.value = (await getRouteProductBomList({ routeId: props.routeId })) || [];
  emit('change', allBomList.value);
}
watch(
  () => props.products.map((item) => item.itemId).filter(Boolean),
  (productIds) => {
    if (!productIds.length) {
      activeProductId.value = undefined;
      return;
    }
    if (!activeProductId.value || !productIds.includes(activeProductId.value)) {
      activeProductId.value = productIds[0];
    }
  },
  { immediate: true },
);
watch(
  () => props.routeId,
  (value) => value && loadAll(),
  { immediate: true },
);
</script>
<template>
  <Alert
    class="mb-4"
    message="按产出产品配置每道工序需要投入的物料"
    description="先选择产出产品,再切换到具体工序添加投料物料和用料比例。开启“报工自动扣料”的工序,会在报工时按这里的配置自动扣减库存。"
    show-icon
    type="info"
  />
  <Empty
    v-if="!products.length || !processes.length"
    :description="!products.length ? '请先完成产出产品配置' : '请先完成工序配置'"
  />
  <div v-else class="material-layout">
    <aside class="product-selector" aria-label="选择要配置投料的产出产品">
      <div class="selector-heading">
        <strong>产出产品</strong>
        <span>{{ products.length }} ä¸ª</span>
      </div>
      <Button
        v-for="product in products"
        :key="product.itemId"
        :class="['product-button', { active: product.itemId === activeProductId }]"
        type="text"
        @click="activeProductId = product.itemId"
      >
        <span class="product-copy">
          <strong>{{ product.itemName }}</strong>
          <small>{{ product.itemCode }}</small>
        </span>
        <Tag :color="materialCount(product.itemId) ? 'success' : 'warning'">
          {{ materialCount(product.itemId) ? `${materialCount(product.itemId)} æ¡` : '未配置' }}
        </Tag>
      </Button>
    </aside>
    <section v-if="activeProduct" class="material-detail">
      <header class="material-detail-header">
        <div>
          <h3>{{ activeProduct.itemName }} Â· å·¥åºæŠ•æ–™</h3>
          <p>依次切换工序,维护该产品在该工序实际消耗的物料。</p>
        </div>
        <Tag color="blue">{{ processes.length }} é“工序</Tag>
      </header>
      <ProductBomList
        :form-type="formType"
        :product-id="activeProduct.itemId!"
        :product-name="activeProduct.itemName"
        :route-id="routeId"
        @change="loadAll"
      />
    </section>
  </div>
</template>
<style scoped>
.material-layout { display: grid; grid-template-columns: minmax(210px, 260px) minmax(0, 1fr); gap: 18px; }
.product-selector { overflow: hidden; align-self: start; border: 1px solid rgb(226 232 240); border-radius: 12px; background: rgb(248 250 252); }
.selector-heading { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid rgb(226 232 240); color: rgb(15 23 42); }
.selector-heading span { color: rgb(100 116 139); font-size: 12px; }
.product-button { display: flex; width: 100%; height: auto; min-height: 62px; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border-radius: 0; text-align: left; }
.product-button + .product-button { border-top: 1px solid rgb(226 232 240); }
.product-button.active { background: rgb(219 234 254); color: rgb(29 78 216); }
.product-copy { display: grid; min-width: 0; gap: 2px; }
.product-copy strong, .product-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.product-copy small { color: rgb(100 116 139); }
.material-detail { min-width: 0; padding: 18px; border: 1px solid rgb(226 232 240); border-radius: 12px; }
.material-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.material-detail-header h3 { margin: 0; color: rgb(15 23 42); font-size: 16px; font-weight: 650; }
.material-detail-header p { margin: 4px 0 0; color: rgb(100 116 139); font-size: 13px; }
:global(.dark) .product-selector, :global(.dark) .material-detail { border-color: rgb(51 65 85); background: rgb(15 23 42); }
:global(.dark) .selector-heading, :global(.dark) .product-button + .product-button { border-color: rgb(51 65 85); color: rgb(241 245 249); }
:global(.dark) .product-button.active { background: rgb(30 64 175 / 35%); color: rgb(147 197 253); }
:global(.dark) .material-detail-header h3 { color: rgb(241 245 249); }
@media (max-width: 900px) {
  .material-layout { grid-template-columns: 1fr; }
  .product-selector { display: flex; overflow-x: auto; }
  .selector-heading { display: none; }
  .product-button { min-width: 210px; border-top: 0 !important; border-right: 1px solid rgb(226 232 240); }
}
</style>
src/views/mes/pro/route/modules/process-form.vue
@@ -6,7 +6,7 @@
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { Alert, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getProcessSimpleList } from '#/api/mes/pro/process';
@@ -121,6 +121,13 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Alert
      class="mx-4 mb-4"
      message="按实际生产节点配置产出"
      description="中间工序没有独立产出时可留空;形成半成品时选择对应半成品,末道工序必须选择全部成品。如开启报工自动扣料,报工时会按工序投料配置扣减库存。"
      show-icon
      type="info"
    />
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pro/route/modules/process-list.vue
@@ -4,11 +4,11 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, Tag } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -25,8 +25,15 @@
  routeId: number;
}>();
const isEditable = ref(props.formType !== 'detail'); // æ˜¯å¦å¯ç¼–辑
const emit = defineEmits<{
  change: [list: MesProRouteProcessApi.RouteProcess[]];
}>();
const isEditable = computed(() => props.formType !== 'detail'); // æ˜¯å¦å¯ç¼–辑
const list = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥è‰ºè·¯çº¿å·¥åºåˆ—表
const sortedList = computed(() =>
  [...list.value].sort((a, b) => (a.sort || 0) - (b.sort || 0)),
);
const [ProcessFormModal, processFormModalApi] = useVbenModal({
  connectedComponent: ProcessForm,
@@ -53,6 +60,7 @@
  try {
    list.value = await getRouteProcessListByRoute(props.routeId);
    gridApi.setGridOptions({ data: list.value });
    emit('change', list.value);
  } finally {
    gridApi.setLoading(false);
  }
@@ -89,6 +97,16 @@
<template>
  <ProcessFormModal @success="getList" />
  <div v-if="sortedList.length" class="mb-4 flex flex-wrap items-center gap-2 rounded-lg bg-slate-50 p-3 dark:bg-slate-900">
    <template v-for="(item, index) in sortedList" :key="item.id">
      <div class="flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-2 dark:border-slate-700 dark:bg-slate-800">
        <span class="text-xs font-semibold text-blue-600">{{ item.sort }}</span>
        <span class="font-medium">{{ item.processName }}</span>
        <Tag v-if="index === sortedList.length - 1" color="blue">末道</Tag>
      </div>
      <span v-if="index < sortedList.length - 1" class="text-slate-400">→</span>
    </template>
  </div>
  <div v-if="isEditable" class="mb-3 flex items-center justify-start">
    <TableAction
      :actions="[
@@ -100,7 +118,7 @@
      ]"
    />
  </div>
  <Grid class="w-full" table-title="组成工序">
  <Grid class="w-full" table-title="工序顺序及各工序产出">
    <template #colorCode="{ row }">
      <div v-if="row.colorCode" class="flex items-center justify-center gap-1">
        <div
src/views/mes/pro/route/modules/product-bom-list.vue
@@ -1,9 +1,10 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -21,10 +22,17 @@
import BomForm from './bom-form.vue';
const props = defineProps<{
  formType: FormType;
  productId: number;
  productName?: string;
  routeId: number;
}>();
const emit = defineEmits<{
  change: [];
}>();
const isEditable = computed(() => props.formType !== 'detail');
const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥åºåˆ—表(用于 Tab)
const activeProcessId = ref<number>(); // å½“前选中的工序 Tab
@@ -112,6 +120,12 @@
  await deleteRouteProductBom(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['BOM ç‰©æ–™']));
  await getList();
  emit('change');
}
async function handleSaved() {
  await getList();
  emit('change');
}
watch(
@@ -126,7 +140,7 @@
</script>
<template>
  <BomFormModal @success="getList" />
  <BomFormModal @success="handleSaved" />
  <Tabs v-model:active-key="activeProcessId" @change="getList">
    <TabPane
      v-for="item in processList"
@@ -134,11 +148,11 @@
      :tab="item.processName"
    />
  </Tabs>
  <div class="mb-3 flex items-center justify-start">
  <div v-if="isEditable" class="mb-3 flex items-center justify-start">
    <TableAction
      :actions="[
        {
          label: $t('ui.actionTitle.create', ['BOM ç‰©æ–™']),
          label: $t('ui.actionTitle.create', ['投料物料']),
          type: 'primary',
          disabled: !activeProcessId,
          onClick: handleCreate,
@@ -146,21 +160,23 @@
      ]"
    />
  </div>
  <Grid class="w-full" table-title="产品 BOM">
  <Grid class="w-full" table-title="当前工序投料">
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: $t('common.edit'),
            type: 'link',
            ifShow: () => isEditable,
            onClick: handleEdit.bind(null, row),
          },
          {
            label: $t('common.delete'),
            type: 'link',
            danger: true,
            ifShow: () => isEditable,
            popConfirm: {
              title: $t('ui.actionMessage.deleteConfirm', ['BOM ç‰©æ–™']),
              title: $t('ui.actionMessage.deleteConfirm', ['投料物料']),
              confirm: handleDelete.bind(null, row),
            },
          },
src/views/mes/pro/route/modules/product-form.vue
@@ -5,7 +5,7 @@
import { useVbenModal } from '@vben/common-ui';
import { Divider, message } from 'ant-design-vue';
import { Alert, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -16,7 +16,6 @@
import { $t } from '#/locales';
import { useRouteProductFormSchema } from '../data';
import ProductBomList from './product-bom-list.vue';
const emit = defineEmits(['success']);
const formData = ref<MesProRouteProductApi.RouteProduct>();
@@ -64,14 +63,10 @@
    try {
      if (formData.value?.id) {
        await updateRouteProduct(data);
        // ç”¨æœ€æ–°è¡¨å•值同步 formData,确保产品 BOM å­è¡¨ç»‘定的 itemId ä¸Žè¡¨å•一致
        formData.value = { ...formData.value, ...data };
      } else {
        // æ–°å¢žæˆåŠŸåŽåˆ‡æ¢åˆ°ç¼–è¾‘æ¨¡å¼ï¼Œæ–¹ä¾¿ç»§ç»­ç»´æŠ¤äº§å“ BOM
        const id = await createRouteProduct(data);
        formData.value = { ...data, id };
        await formApi.setFieldValue('id', id);
        await createRouteProduct(data);
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
@@ -109,17 +104,13 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Alert
      class="mx-4 mb-4"
      message="这里维护产出及其报工入库位置"
      description="投料物料将在路线配置的“工序投料”步骤中按产品、按工序统一维护。"
      show-icon
      type="info"
    />
    <Form class="mx-4" />
    <!-- ç¼–辑模式下展示产品 BOM å­è¡¨ï¼Œæ–°å¢žæ¨¡å¼ä¸‹éšè— -->
    <template v-if="formData?.id && formData?.itemId">
      <Divider class="!my-3" orientation="left">产品 BOM é…ç½®</Divider>
      <div class="mx-4">
        <ProductBomList
          :route-id="formData.routeId!"
          :product-id="formData.itemId"
          :product-name="formData.itemName"
        />
      </div>
    </template>
  </Modal>
</template>
src/views/mes/pro/route/modules/product-list.vue
@@ -4,7 +4,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
@@ -27,7 +27,11 @@
  routeId: number;
}>();
const isEditable = ref(props.formType !== 'detail'); // æ˜¯å¦å¯ç¼–辑
const emit = defineEmits<{
  change: [list: MesProRouteProductApi.RouteProduct[]];
}>();
const isEditable = computed(() => props.formType !== 'detail'); // æ˜¯å¦å¯ç¼–辑
const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // å·¥è‰ºè·¯çº¿äº§å“åˆ—表
const [ProductFormModal, productFormModalApi] = useVbenModal({
@@ -55,6 +59,7 @@
  try {
    list.value = await getRouteProductListByRoute(props.routeId);
    gridApi.setGridOptions({ data: list.value });
    emit('change', list.value);
  } finally {
    gridApi.setLoading(false);
  }
@@ -94,14 +99,14 @@
    <TableAction
      :actions="[
        {
          label: $t('ui.actionTitle.create', ['关联产品']),
          label: $t('ui.actionTitle.create', ['产出产品']),
          type: 'primary',
          onClick: handleCreate,
        },
      ]"
    />
  </div>
  <Grid class="w-full" table-title="关联产品">
  <Grid class="w-full" table-title="产出产品及报工入库位置">
    <template #productionTime="{ row }">
      <span v-if="row.productionTime">
        {{ row.productionTime }}
src/views/mes/pro/route/modules/route-configurator.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,413 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesProRouteApi } from '#/api/mes/pro/route';
import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Alert, Button, Empty, message, Steps, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route';
import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process';
import { getRouteProductListByRoute } from '#/api/mes/pro/route/product';
import { getRouteProductBomList } from '#/api/mes/pro/route/productbom';
import { useFormSchema } from '../data';
import MaterialInputList from './material-input-list.vue';
import ProcessList from './process-list.vue';
import ProductList from './product-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesProRouteApi.Route>();
const activeStep = ref(0);
const productList = ref<MesProRouteProductApi.RouteProduct[]>([]);
const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]);
const bomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]);
const isDetail = computed(() => formType.value === 'detail');
const isNew = computed(() => !formData.value?.id);
const routeId = computed(() => formData.value?.id);
const sortedProcessList = computed(() =>
  [...processList.value].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)),
);
const title = computed(() =>
  isDetail.value ? '查看工艺路线' : isNew.value ? '新建工艺路线' : '编辑工艺路线',
);
const stepItems = [
  { title: '基本信息', description: '路线名称与说明' },
  { title: '产出与入库', description: '产品及报工仓位' },
  { title: '工序与产出', description: '顺序和半成品/成品' },
  { title: '工序投料', description: '按产品与工序配置' },
  { title: '启用前检查', description: '集中检查缺失项' },
];
const productsWithWarehouse = computed(() =>
  productList.value.filter((item) => item.warehouseId && item.locationId && item.areaId).length,
);
const processesWithOutput = computed(() =>
  processList.value.filter((item) => item.outputItemIds?.length).length,
);
const lastProcess = computed(() => sortedProcessList.value.at(-1));
const configuredProductIds = computed(
  () => new Set(bomList.value.map((item) => item.productId).filter(Boolean)),
);
const productsWithBom = computed(() => configuredProductIds.value.size);
const reviewIssues = computed(() => {
  const issues: Array<{ step: number; text: string }> = [];
  if (!productList.value.length) issues.push({ step: 1, text: '还没有配置产出产品' });
  const missingWarehouse = productList.value.filter(
    (item) => !item.warehouseId || !item.locationId || !item.areaId,
  );
  if (missingWarehouse.length) {
    issues.push({
      step: 1,
      text: `${missingWarehouse.length} ä¸ªäº§å‡ºäº§å“æœªé…ç½®å®Œæ•´ä»“库、库区和库位`,
    });
  }
  if (!processList.value.length) issues.push({ step: 2, text: '还没有配置生产工序' });
  if (processList.value.length && !processList.value.some((item) => item.keyFlag)) {
    issues.push({ step: 2, text: '未设置关键工序' });
  }
  if (lastProcess.value && !lastProcess.value.outputItemIds?.length) {
    issues.push({
      step: 2,
      text: `末道工序“${lastProcess.value.processName || lastProcess.value.processCode || lastProcess.value.sort}”必须配置成品产出`,
    });
  }
  const productIds = new Set(productList.value.map((item) => item.itemId));
  const invalidOutputCount = processList.value.filter((process) =>
    process.outputItemIds?.some((itemId) => !productIds.has(itemId)),
  ).length;
  if (invalidOutputCount) {
    issues.push({
      step: 2,
      text: `${invalidOutputCount} é“工序的产出未包含在路线产出产品中`,
    });
  }
  const noBomProducts = productList.value.filter(
    (item) => item.itemId && !configuredProductIds.value.has(item.itemId),
  );
  if (noBomProducts.length) {
    issues.push({ step: 3, text: `${noBomProducts.length} ä¸ªäº§å“è¿˜æ²¡æœ‰é…ç½®ä»»ä½•工序投料` });
  }
  return issues;
});
const primaryText = computed(() => {
  if (activeStep.value === stepItems.length - 1) return isDetail.value ? '关闭' : '完成配置';
  if (activeStep.value === 0 && isNew.value) return '保存并配置产出';
  return '下一步';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
async function loadConfiguration() {
  if (!routeId.value) return;
  const [products, processes, boms] = await Promise.all([
    getRouteProductListByRoute(routeId.value),
    getRouteProcessListByRoute(routeId.value),
    getRouteProductBomList({ routeId: routeId.value }),
  ]);
  productList.value = products || [];
  processList.value = processes || [];
  bomList.value = boms || [];
}
async function saveBaseInformation() {
  const { valid } = await formApi.validate();
  if (!valid) return false;
  modalApi.lock();
  try {
    const data = (await formApi.getValues()) as MesProRouteApi.Route;
    if (!routeId.value) {
      const id = await createRoute(data);
      formData.value = { ...data, id };
      await formApi.setFieldValue('id', id);
      formType.value = 'update';
      message.success('基本信息已保存,请继续配置产出产品和入库位置');
    } else {
      await updateRoute({ ...data, id: routeId.value });
      formData.value = { ...formData.value, ...data, id: routeId.value };
    }
    emit('success');
    return true;
  } finally {
    modalApi.unlock();
  }
}
function validateProducts() {
  if (!productList.value.length) {
    message.warning('请至少添加一个产出产品,再配置工序');
    return false;
  }
  if (productsWithWarehouse.value !== productList.value.length) {
    message.warning('请为每个产出产品配置完整的仓库、库区和库位');
    return false;
  }
  return true;
}
function validateProcesses() {
  if (!processList.value.length) {
    message.warning('请至少添加一道生产工序');
    return false;
  }
  return true;
}
function validateBom() {
  if (productsWithBom.value !== productList.value.length) {
    message.warning('每个产出产品至少要配置一条工序投料');
    return false;
  }
  return true;
}
async function goToStep(target: number) {
  if (target < 0 || target >= stepItems.length) return;
  if (target <= activeStep.value || isDetail.value) {
    activeStep.value = target;
    return;
  }
  if (!(await saveBaseInformation())) return;
  await loadConfiguration();
  if (target >= 2 && !validateProducts()) return void (activeStep.value = 1);
  if (target >= 3 && !validateProcesses()) return void (activeStep.value = 2);
  if (target >= 4 && !validateBom()) return void (activeStep.value = 3);
  activeStep.value = target;
}
async function handlePrimaryAction() {
  if (activeStep.value === stepItems.length - 1) {
    if (!isDetail.value && reviewIssues.value.length) {
      message.warning('仍有缺失配置,请根据检查结果补充后再完成');
      return;
    }
    await modalApi.close();
    return;
  }
  await goToStep(activeStep.value + 1);
}
const [Modal, modalApi] = useVbenModal({
  showConfirmButton: false,
  cancelText: '关闭',
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      productList.value = [];
      processList.value = [];
      bomList.value = [];
      activeStep.value = 0;
      return;
    }
    formApi.setState({ schema: useFormSchema(formApi) });
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(isDetail.value);
    if (!data.id) return;
    modalApi.lock();
    try {
      formData.value = await getRoute(data.id);
      await formApi.setValues(formData.value);
      await loadConfiguration();
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="title" class="w-[94vw] max-w-[1560px]">
    <div class="route-config-shell">
      <header class="route-config-header">
        <div class="min-w-0">
          <div class="flex flex-wrap items-center gap-2">
            <h2 class="m-0 text-lg font-semibold text-gray-900 dark:text-gray-100">
              {{ formData?.name || '新工艺路线' }}
            </h2>
            <Tag :color="isDetail ? 'default' : 'processing'">
              {{ isDetail ? '只读查看' : '配置中' }}
            </Tag>
          </div>
          <p class="mb-0 mt-1 text-sm text-gray-600 dark:text-gray-300">
            å·¥åºæŠ•料依赖产出产品和工序,因此在工序编排完成后集中配置。
          </p>
        </div>
        <div class="route-config-summary" aria-label="当前配置概览">
          <span>产出 {{ productList.length }}</span>
          <span>工序 {{ processList.length }}</span>
          <span>投料 {{ bomList.length }}</span>
        </div>
      </header>
      <Steps
        :current="activeStep"
        :items="stepItems"
        class="route-config-steps"
        responsive
        size="small"
        type="navigation"
        @change="goToStep"
      />
      <main class="route-config-content">
        <section v-show="activeStep === 0" class="route-step-panel">
          <div class="route-step-copy">
            <IconifyIcon icon="lucide:route" />
            <div>
              <h3>先定义这条路线是什么</h3>
              <p>编码用于系统识别,名称和说明帮助现场人员快速选择。</p>
            </div>
          </div>
          <Form class="route-base-form" />
        </section>
        <section v-if="routeId" v-show="activeStep === 1" class="route-step-panel">
          <Alert
            class="mb-4"
            message="先维护所有可能的工序产出"
            description="实际形成的中间半成品和末道工序的成品都要加入,并分别指定报工后进入的仓库、库区和库位。"
            show-icon
            type="info"
          />
          <ProductList
            :form-type="formType"
            :route-id="routeId"
            @change="productList = $event"
          />
        </section>
        <section v-if="routeId" v-show="activeStep === 2" class="route-step-panel">
          <Alert
            class="mb-4"
            message="按实际生产顺序编排工序"
            description="中间工序没有独立产出时可留空;形成半成品时选择对应半成品,最后一道工序必须选择路线绑定的全部成品。"
            show-icon
            type="info"
          />
          <ProcessList
            :form-type="formType"
            :route-id="routeId"
            @change="processList = $event"
          />
        </section>
        <section v-if="routeId" v-show="activeStep === 3" class="route-step-panel">
          <MaterialInputList
            :form-type="formType"
            :processes="sortedProcessList"
            :products="productList"
            :route-id="routeId"
            @change="bomList = $event"
          />
        </section>
        <section v-show="activeStep === 4" class="route-step-panel">
          <div class="review-heading">
            <div>
              <h3>启用前检查</h3>
              <p>确认产出、仓位、工序和投料形成闭环后,再回到列表启用路线。</p>
            </div>
            <Tag :color="reviewIssues.length ? 'warning' : 'success'">
              {{ reviewIssues.length ? `${reviewIssues.length} é¡¹å¾…处理` : '配置完整' }}
            </Tag>
          </div>
          <div class="review-metrics">
            <div><strong>{{ productsWithWarehouse }}/{{ productList.length }}</strong><span>产品仓位完整</span></div>
            <div><strong>{{ processesWithOutput }}</strong><span>配置产出的工序</span></div>
            <div><strong>{{ productsWithBom }}/{{ productList.length }}</strong><span>产品已配置投料</span></div>
          </div>
          <div v-if="reviewIssues.length" class="issue-list" role="status">
            <button
              v-for="issue in reviewIssues"
              :key="issue.text"
              class="issue-item"
              type="button"
              @click="activeStep = issue.step"
            >
              <IconifyIcon icon="lucide:circle-alert" />
              <span>{{ issue.text }}</span>
              <IconifyIcon class="ml-auto" icon="lucide:chevron-right" />
            </button>
          </div>
          <Empty v-else description="所有必需配置均已完成">
            <template #image>
              <div class="review-success-icon"><IconifyIcon icon="lucide:badge-check" /></div>
            </template>
          </Empty>
        </section>
      </main>
    </div>
    <template #prepend-footer>
      <div class="flex flex-auto items-center justify-between gap-3">
        <Button v-if="activeStep > 0" @click="activeStep--">
          <IconifyIcon class="mr-1" icon="lucide:arrow-left" />上一步
        </Button>
        <span v-else class="text-sm text-gray-500">新建路线默认停用,配置完整后再启用</span>
        <Button type="primary" @click="handlePrimaryAction">
          {{ primaryText }}
          <IconifyIcon
            v-if="activeStep < stepItems.length - 1"
            class="ml-1"
            icon="lucide:arrow-right"
          />
        </Button>
      </div>
    </template>
  </Modal>
</template>
<style scoped>
.route-config-shell { min-height: min(720px, 72vh); background: rgb(248 250 252); color: rgb(15 23 42); }
.route-config-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 24px 14px; background: white; border-bottom: 1px solid rgb(226 232 240); }
.route-config-summary { display: flex; flex-shrink: 0; gap: 8px; }
.route-config-summary span { padding: 5px 10px; border-radius: 8px; background: rgb(239 246 255); color: rgb(29 78 216); font-size: 12px; font-weight: 600; }
.route-config-steps { padding: 12px 20px 0; background: white; }
.route-config-content { padding: 20px 24px 28px; }
.route-step-panel { min-height: 460px; padding: 22px; border-radius: 14px; background: white; box-shadow: 0 10px 28px rgb(15 23 42 / 6%); }
.route-step-copy, .review-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 22px; }
.route-step-copy { justify-content: flex-start; }
.route-step-copy > svg { width: 38px; height: 38px; padding: 9px; border-radius: 10px; background: rgb(219 234 254); color: rgb(29 78 216); }
.route-step-copy h3, .review-heading h3 { margin: 0; color: rgb(15 23 42); font-size: 17px; font-weight: 650; }
.route-step-copy p, .review-heading p { margin: 4px 0 0; color: rgb(71 85 105); font-size: 14px; }
.route-base-form { max-width: 1040px; margin: 34px auto 0; }
.review-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 24px 0; }
.review-metrics > div { display: flex; align-items: baseline; gap: 10px; padding: 18px; border-radius: 12px; background: rgb(248 250 252); }
.review-metrics strong { color: rgb(29 78 216); font-size: 24px; }
.review-metrics span { color: rgb(71 85 105); font-size: 13px; }
.issue-list { display: grid; gap: 10px; }
.issue-item { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid rgb(253 230 138); border-radius: 10px; background: rgb(255 251 235); color: rgb(146 64 14); cursor: pointer; text-align: left; transition: background-color 180ms ease, border-color 180ms ease; }
.issue-item:hover, .issue-item:focus-visible { border-color: rgb(245 158 11); background: rgb(254 243 199); outline: none; }
.review-success-icon { display: grid; width: 72px; height: 72px; place-items: center; margin: 0 auto; border-radius: 50%; background: rgb(220 252 231); color: rgb(22 163 74); font-size: 38px; }
:global(.dark) .route-config-shell { background: rgb(15 23 42); color: rgb(226 232 240); }
:global(.dark) .route-config-header, :global(.dark) .route-config-steps, :global(.dark) .route-step-panel { background: rgb(30 41 59); border-color: rgb(51 65 85); }
:global(.dark) .route-step-copy h3, :global(.dark) .review-heading h3 { color: rgb(241 245 249); }
:global(.dark) .route-step-copy p, :global(.dark) .review-heading p, :global(.dark) .review-metrics span { color: rgb(203 213 225); }
:global(.dark) .review-metrics > div { background: rgb(15 23 42); }
@media (max-width: 900px) {
  .route-config-header { align-items: flex-start; flex-direction: column; }
  .review-metrics { grid-template-columns: 1fr; }
  .route-step-panel { padding: 16px; }
}
</style>
src/views/mes/process-design/route/index.vue
@@ -8,7 +8,7 @@
import { getDictLabel } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tooltip } from 'ant-design-vue';
import { Alert, Button, message, Tooltip } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -24,7 +24,8 @@
import ImportModalComponent from '#/components/import-modal';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
// å·¥è‰ºè®¾è®¡ä¸Žç”Ÿäº§åŸºç¡€æ•°æ®å…±ç”¨åŒä¸€ä¸ªé…ç½®å™¨ï¼Œé¿å…ä¸¤å¥—页面能力不一致。
import Form from '#/views/mes/pro/route/modules/route-configurator.vue';
const { hasAccessByCodes } = useAccess();
const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ˜¯å¦å¯åˆ‡æ¢çŠ¶æ€
@@ -78,7 +79,9 @@
): Promise<boolean | undefined> {
  try {
    await confirm(
      `确认要将"${row.name}"工艺路线切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}】吗?`,
      newStatus === CommonStatusEnum.ENABLE
        ? `确认启用“${row.name}”吗?系统将检查产出入库位置、工序产出和工序投料是否完整。`
        : `确认要将“${row.name}”工艺路线切换为【${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}】吗?`,
    );
  } catch {
    return false;
@@ -140,6 +143,13 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Alert
      class="mb-3"
      message="路线配置顺序"
      description="基本信息 â†’ äº§å‡ºäº§å“åŠå…¥åº“仓 â†’ å·¥åºåŠå„工序产出 â†’ å·¥åºæŠ•æ–™ â†’ æ ¡éªŒå¯ç”¨"
      show-icon
      type="info"
    />
    <Grid table-title="工艺路线列表">
      <template #toolbar-tools>
        <TableAction