Forráskód Böngészése

推钢工操作台

zhangafei 3 hete
szülő
commit
f4f4fa1369

BIN
src/assets/images/billetHome/operator-head-bg.png


+ 12 - 0
src/router/routes/index.ts

@@ -155,6 +155,17 @@ export const carMonitor: AppRouteRecordRaw = {
     ignoreAuth: true,
   },
 };
+
+// 推钢室操作页面
+export const operatorPage: AppRouteRecordRaw = {
+  path: '/operator/:ccmNo',
+  name: 'OperatorPage',
+  component: () => import('/@/views/billet/operator/index.vue'),
+  meta: {
+    title: t('推钢室操作界面'),
+    ignoreAuth: true,
+  },
+};
 // Basic routing without permission
 export const basicRoutes = [
   LoginRoute,
@@ -173,4 +184,5 @@ export const basicRoutes = [
   billetHome,
   carBoard,
   carMonitor,
+  operatorPage,
 ];

+ 1 - 1
src/views/billet/Dashboard/components/OnDutyShipment.vue

@@ -48,7 +48,7 @@
   const emit = defineEmits(['jumpPage']);
 
   const goToDetial = () => {
-    emit('jumpPage', `/shippingBill`);
+    emit('jumpPage', `/shippingBill/` + props.ccmNo);
   };
 
   watch(

+ 4 - 3
src/views/billet/homePage/index.less

@@ -14,13 +14,14 @@
     background: #010c3a;
   }
 
-  .billetHomepage-header {
+  .ant-layout .billetHomepage-header {
     display: flex;
     align-items: flex-end;
     justify-content: flex-end;
     height: 100px;
-    background: transparent;
-    background-image: url(/@/assets/images/billetHome/header.png);
+    background: transparent !important;
+    background-image: url(/@/assets/images/billetHome/header.png) !important;
+    background-size: cover !important;
     position: relative;
 
     .clock-wrapper {

+ 505 - 0
src/views/billet/operator/components/car.vue

@@ -0,0 +1,505 @@
+<template>
+  <a-spin :spinning="isSpinning" wrapperClassName="car-info-spin">
+    <a-tabs v-model:activeKey="activeKey" type="card">
+      <a-tab-pane :key="item" :tab="'车位' + item" v-for="item in carPosition[ccmNo]">
+        <div class="car-info-wrapper">
+          <div class="licensePlate"
+            >装运车辆:{{ vehicleInfo['info' + item] && vehicleInfo['info' + item].licensePlate ? vehicleInfo['info' + item].licensePlate : '' }}</div
+          >
+          <print-car-info
+            :ref="printCarInfoRefs[`infoRef${item}`]"
+            :carPositon="item"
+            :ccm-no="ccmNo"
+            :info="vehicleInfo['info' + item]"
+            @change="handleChange"
+            @refresh="refreshCarList"
+          />
+          <div class="print">
+            <a-button
+              type="primary"
+              v-if="vehicleInfo['info' + item] && vehicleInfo['info' + item].id"
+              @click="() => openPrintModal(true, { record: vehicleInfo['info' + item] })"
+              >打印</a-button
+            >
+          </div>
+          <div class="change-type flex items-center" v-if="vehicleInfo['info' + item] && Object.keys(vehicleInfo['info' + item]).includes('btype')">
+            <span>{{ vehicleInfo['info' + item].ccmNo_dictText }}</span>
+            <span>/</span>
+            <a-switch
+              v-model:checked="vehicleInfo['info' + item].btype"
+              checked-value="0"
+              @change="(checked) => handleBtypeChange(checked, vehicleInfo['info' + item])"
+              checked-children="热坯"
+              un-checked-value="1"
+              un-checked-children="冷坯"
+            />
+          </div>
+        </div>
+      </a-tab-pane>
+      <!-- <a-tab-pane key="2" tab="车位2" force-render>Content of Tab Pane 2</a-tab-pane> -->
+    </a-tabs>
+    <div class="stack-divider flex justify-between items-center">
+      <div>{{ stackInfo.typeName || '' }}</div>
+      <div>
+        <a-button
+          type="primary"
+          v-if="vehicleInfo['info' + activeKey] && vehicleInfo['info' + activeKey].id && !vehicleInfo['info' + activeKey].outTime"
+          danger
+          @click="stackToCar"
+        >
+          堆垛装车
+        </a-button>
+        <a-button style="background-color: #f5f5f5; color: #838383" v-else disabled>堆垛装车</a-button>
+      </div>
+    </div>
+    <div class="stacking-wrapper selected-divider" id="operatorCC-stacking-wrapper">
+      <div class="selected-divider-row flex" :id="`stackLayer${pitem.value}`" v-for="pitem in stackingList" :key="pitem.value">
+        <!-- <a-divider orientation="left" class="stacking-list-divider">{{ pitem.label }}</a-divider> -->
+        <div class="p-layer">{{ pitem.value }}</div>
+        <div
+          v-for="item in stackingObj[pitem.value]"
+          class="p-stack-col stacking-list-row flex-1"
+          @click="handleStackClick(item)"
+          :class="{ 'selected-row': selectedAddressId.includes(item.id) }"
+        >
+          <div class="stack-col" :class="{ 'hemp-texture': !!item.steelBillet.length }">{{ item.heatNo }}</div>
+        </div>
+      </div>
+    </div>
+  </a-spin>
+  <!-- 打印 -->
+  <printModal @register="registerPrintModal" />
+</template>
+<script setup lang="ts">
+  import { ref, onMounted, onUnmounted, h } from 'vue';
+  import printCarInfo from './printCarInfo.vue';
+  import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+  import { list, edit } from '../../shippingBill/shippingBill.api';
+  import { getStackInfo, stackLoadSave } from '../../hotDelivery/hotDelivery.api';
+  import { getMachineConfig, MachineConfigType, destinationOptions } from '../../hotDelivery/common.data';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import printModal from '../../shippingBill/components/printModal.vue';
+  import { useModal } from '/@/components/Modal';
+  import { render } from '/@/utils/common/renderUtils';
+
+  // 注册打印modal
+  const [registerPrintModal, { openModal: openPrintModal }] = useModal();
+
+  const { createConfirm, createMessage } = useMessage();
+
+  const props = defineProps({
+    ccmNo: {
+      type: [String, Number],
+      default: '5',
+    },
+  });
+
+  const isSpinning = ref(false);
+
+  const activeKey = ref(1);
+  const carPosition = {
+    // '5': [1, 2],
+    '5': [1],
+    '6': [2, 3, 4],
+  };
+  // 车辆信息
+  const infoRef1 = ref();
+  const infoRef2 = ref();
+  const infoRef3 = ref();
+  const infoRef4 = ref();
+  const printCarInfoRefs = { infoRef1, infoRef2, infoRef3, infoRef4 };
+
+  // 5号机堆垛
+  const stackingList = ref<any[]>([]);
+  const stackingObj = ref<any>({});
+  const stackInfo = ref<any>({});
+
+  // 车位1车辆信息
+  // id: '',
+  // licensePlate: '',
+  // destination: '',
+  // typeConfigId: undefined,
+  // ccmNo: '',
+  // ccmNo_dictText: '',
+  // amountTotal: 0,
+  // steel: '',
+  // size: '',
+  // positionNum: '',
+  const vehicleInfo = ref<any>({
+    info1: {},
+    info2: {},
+    info3: {},
+    info4: {},
+  });
+
+  const getInfo = async () => {
+    // 获取车位1车辆信息
+    try {
+      isSpinning.value = true;
+      const fetchArr: any = [];
+      carPosition[props.ccmNo].forEach((item) => {
+        fetchArr.push(
+          list({
+            column: 'createTime',
+            order: 'desc',
+            pageNo: 1,
+            pageSize: 1,
+            positionNum: item,
+            ccmNo: props.ccmNo,
+          })
+        );
+      });
+
+      await Promise.all(fetchArr).then((res) => {
+        if (Array.isArray(res)) {
+          res.forEach((item, index) => {
+            if (item.records[0]) {
+              vehicleInfo.value[`info${index + 1}`] = {
+                ...item.records[0],
+                oldLicensePlate: item.records[0].licensePlate,
+                oldTypeConfigId: item.records[0].typeConfigId === '1024' ? undefined : item.records[0].typeConfigId,
+                typeConfigId: item.records[0].typeConfigId === '1024' ? undefined : item.records[0].typeConfigId,
+                oldBrandNum: item.records[0].brandNum,
+                oldSize: item.records[0].size,
+              };
+
+              setTimeout(
+                () => {
+                  printCarInfoRefs[`infoRef${index + 1}`] && printCarInfoRefs[`infoRef${index + 1}`].value[0].getTableList();
+                },
+                50 * (index + 1)
+              );
+            }
+          });
+        }
+      });
+
+      const machineConfig = await getMachineConfig(props.ccmNo)[MachineConfigType.STACKING];
+
+      stackInfo.value = machineConfig[0];
+      await getStackInfoList(machineConfig[0].id);
+    } catch (error) {
+      console.log(error);
+    } finally {
+      isSpinning.value = false;
+      start();
+    }
+  };
+
+  const { start, stop } = useTimeoutFn(getInfo, 10000);
+
+  // 获取堆垛机堆垛信息
+  // 获取当前堆垛信息
+  const getStackInfoList = async (typeConfigId) => {
+    const stackingInfo = await getStackInfo({ typeConfigId });
+    let layerObj = {};
+    if (stackingInfo.length) {
+      stackingInfo.forEach((item) => {
+        if (!layerObj[item.layer]) layerObj[item.layer] = [];
+
+        layerObj[item.layer].push({ ...item, steelBillet: item.billetNos ? item.billetNos.split(',') : [] });
+      });
+    }
+    stackingObj.value = layerObj;
+    stackingList.value = Object.keys(layerObj)
+      .sort((a, b) => Number(b) - Number(a))
+      .map((item) => ({ label: `第${item}层`, value: item }));
+
+    // nextTick(() => {
+    //   const element = document.getElementById(`operatorCC-stacking-wrapper`);
+    //   if (element) {
+    //     element.scrollIntoView({
+    //       behavior: 'smooth',
+    //       block: 'center',
+    //       inline: 'center',
+    //     });
+    //   }
+    // });
+  };
+
+  // 修改类型
+  function handleBtypeChange(v, record) {
+    stop();
+    createConfirm({
+      iconType: 'warning',
+      title: '确认修改',
+      width: '460px',
+      content: () => {
+        return h('div', { style: { fontSize: '16px' } }, [
+          h('span', null, `是否将车牌为`),
+          h('span', { style: { fontSize: '18px', color: '#d48806' } }, `${record.licensePlate}`),
+          h('span', null, `的`),
+          h('span', { style: { fontSize: '18px', color: v === '1' ? '#cd201f' : '#3b5999' } }, `${v === '1' ? ' 热坯 ' : ' 冷坯 '}`),
+          h('span', null, `改为`),
+          h('span', { style: { fontSize: '18px', color: v === '1' ? '#3b5999' : '#cd201f' } }, `${v === '1' ? ' 冷坯 ' : ' 热坯 '}`),
+          h('span', null, `?`),
+        ]);
+      },
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => {
+        return edit({ id: record.id, btype: v }).then(() => {
+          getInfo();
+        });
+      },
+      onCancel: () => {
+        start();
+        record.btype = v === '1' ? '0' : '1';
+      },
+    });
+  }
+
+  // 修改牌号,车号,目的地
+  const handleChange = (options) => {
+    const { type, value, carPositon } = options;
+    console.log(options, value, value === undefined);
+    if (value === undefined) return;
+    stop();
+    createConfirm({
+      iconType: 'warning',
+      title: '确认修改',
+      width: '460px',
+      content: () => {
+        let title = '',
+          content: any = '';
+        if (type === 'licensePlate') {
+          title = '是否修改车牌号为:';
+          content = value;
+        } else if (type === 'destination') {
+          title = '是否修改目的地为:';
+          const curDestination = destinationOptions[props.ccmNo].find((item) => item.value === value);
+          content = curDestination ? curDestination.label : '';
+        } else if (type === 'brandNum') {
+          title = '是否修改牌号为:';
+          content = render.renderDict(value, 'billet_spec');
+        } else if (type === 'size') {
+          title = '是否修改定尺为:';
+          content = value;
+        }
+        return h('div', { style: { fontSize: '16px' } }, [
+          h('span', null, title),
+          h('span', { style: { fontSize: '18px', color: '#d48806' } }, content),
+        ]);
+      },
+      onOk: () => {
+        let params = { ...vehicleInfo.value[`info${carPositon}`] };
+        if (type === 'destination') {
+          const curDestination = destinationOptions[props.ccmNo].find((item) => item.value === value);
+          params.destination = curDestination.label;
+        }
+        return edit(params).then(() => {
+          getInfo();
+        });
+      },
+      onCancel: () => {
+        start();
+        if (type === 'licensePlate') {
+          vehicleInfo.value[`info${carPositon}`].licensePlate = vehicleInfo.value[`info${carPositon}`].oldLicensePlate;
+        } else if (type === 'destination') {
+          vehicleInfo.value[`info${carPositon}`].typeConfigId = vehicleInfo.value[`info${carPositon}`].oldTypeConfigId;
+        } else if (type === 'brandNum') {
+          vehicleInfo.value[`info${carPositon}`].brandNum = vehicleInfo.value[`info${carPositon}`].oldBrandNum;
+        } else if (type === 'size') {
+          vehicleInfo.value[`info${carPositon}`].size = vehicleInfo.value[`info${carPositon}`].oldSize;
+        }
+      },
+    });
+  };
+
+  // 选择堆垛位置装车
+  const selectedAddressId = ref<any[]>([]);
+  const selectedAddress = ref<any[]>([]);
+
+  const getSelectedStack = () => {
+    return selectedAddress.value || [];
+  };
+
+  const handleStackClick = (v) => {
+    const index = selectedAddressId.value.indexOf(v.id);
+    if (index > -1) {
+      selectedAddressId.value.splice(index, 1);
+      selectedAddress.value.splice(index, 1);
+    } else {
+      selectedAddressId.value.push(v.id);
+      selectedAddress.value.push(v);
+    }
+  };
+  const stackToCar = async () => {
+    try {
+      const valiDesRes = validateCarDestination();
+      if (!valiDesRes) {
+        return;
+      }
+      const selectedStackList = selectedAddress.value.filter((item) => item.billetNos);
+      if (!selectedStackList.length) {
+        createMessage.error('请选择有钢坯的堆垛位置装车!');
+        return;
+      }
+
+      const curDestination =
+        destinationOptions[props.ccmNo].find((item) => item.value === vehicleInfo.value[`info${activeKey.value}`].typeConfigId) || {};
+
+      const params = {
+        storageBill: vehicleInfo.value[`info${activeKey.value}`] || {},
+        belongTable: stackInfo.value?.belongTable,
+        billetHotsendTypeConfigId: stackInfo.value.id,
+        stackingAndLoadingVehiclesList: selectedStackList,
+        destination: vehicleInfo.value[`info${activeKey.value}`].destination,
+        destinationId: vehicleInfo.value[`info${activeKey.value}`].typeConfigId,
+        destinationTable: curDestination.belongTable,
+
+        billetHotsend: {
+          ccmNo: props.ccmNo,
+          isUpd: false,
+        },
+      };
+      stop();
+      isSpinning.value = true;
+      await stackLoadSave(params);
+
+      getInfo();
+    } catch (error) {
+      isSpinning.value = false;
+    } finally {
+      selectedAddressId.value = [];
+      selectedAddress.value = [];
+    }
+  };
+
+  // 验证是否有目的地
+  const validateCarDestination = () => {
+    const curVehicleInfo = vehicleInfo.value[`info${activeKey.value}`];
+    if (!curVehicleInfo.typeConfigId || !curVehicleInfo.destination || curVehicleInfo.typeConfigId == 1024) {
+      createMessage.error('请先选择装运单库名!');
+      printCarInfoRefs[`infoRef${activeKey.value}`].value && printCarInfoRefs[`infoRef${activeKey.value}`].value[0].focusDestination();
+      return false;
+    }
+    return true;
+  };
+
+  onMounted(() => {
+    getInfo();
+  });
+
+  onUnmounted(() => {
+    stop();
+  });
+
+  const refreshCarList = () => {
+    stop();
+    getInfo();
+  };
+
+  defineExpose({
+    getSelectedStack,
+    getStackInfo: () => stackInfo.value,
+    getCurrentCar: () => vehicleInfo.value[`info${activeKey.value}`],
+    refreshCarList: refreshCarList,
+    valirDest: validateCarDestination,
+  });
+</script>
+<style lang="less" scoped>
+  @import '../../hotDelivery/components/metal.less';
+  .car-info-spin {
+    height: 100%;
+
+    :deep(.ant-spin-container) {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      overflow: hidden;
+    }
+  }
+
+  .car-info-wrapper {
+    position: relative;
+    padding: 30px 10px 10px;
+    border: 1px solid var(--op-border-color);
+    border-radius: 6px;
+
+    .licensePlate {
+      width: 190px;
+      background-color: #010c3a;
+      position: absolute;
+      top: -15px;
+      left: 30px;
+      font-size: 16px;
+      color: var(--op-text-color-fff);
+      text-align: center;
+    }
+
+    .print {
+      position: absolute;
+      right: 22px;
+      top: 40px;
+    }
+
+    .change-type {
+      position: absolute;
+      right: 22px;
+      top: -40px;
+      gap: 10px;
+      color: var(--op-text-color-fff);
+
+      .ant-switch {
+        background-color: #3b5999;
+      }
+
+      .ant-switch.ant-switch-checked {
+        background-color: #cd201f;
+      }
+    }
+  }
+
+  .stack-divider {
+    font-size: 16px;
+    margin-top: 10px;
+    color: var(--op-text-color-fff);
+  }
+
+  .stacking-wrapper {
+    flex: 1;
+    overflow: auto;
+    padding: 0;
+    margin-top: 6px;
+    padding-bottom: 20px;
+
+    .selected-divider-row {
+      margin-top: 5px;
+      gap: 3px;
+    }
+
+    &.selected-divider .selected-divider-row .stacking-list-row {
+      cursor: pointer;
+      height: 24px;
+      line-height: 20px;
+      border: 1px solid var(--op-border-color);
+      border-radius: 4px;
+      font-size: 20px;
+    }
+
+    .stacking-list-divider {
+      margin: 0;
+      color: var(--vxe-danger-color, #f56c6c);
+    }
+
+    .stack-col {
+      overflow: hidden;
+    }
+
+    .p-stack-col {
+      padding-right: 0 !important;
+      max-width: 11%;
+    }
+
+    .p-layer {
+      width: 20px;
+      margin-right: 10px;
+      font-size: 20px;
+      color: #f50;
+      font-family: 'Kingsoft_Cloud_Font';
+      line-height: 24px;
+      text-align: center;
+    }
+  }
+</style>

+ 287 - 0
src/views/billet/operator/components/headTop.vue

@@ -0,0 +1,287 @@
+<template>
+  <a-row class="head-top">
+    <a-col :span="3" class="head-no bg flex items-center justify-center"> 浇铸炉号:{{ info.billetHotsendChangeShift.heatNo }} </a-col>
+    <a-col :span="1" class="amount-no bg flex items-center justify-center">
+      <a-statistic title="支数" :value="info.currentCastingFurnaceAmount || 0" />
+    </a-col>
+    <a-col :span="1" class="amount-no bg flex items-center justify-center">
+      <a-statistic title="重量/t" :value="info.currentCastingFurnace || 0" />
+    </a-col>
+    <a-col :span="1" class="flex items-center justify-center">
+      <a-button type="primary" class="change-heat" :disabled="true" @click="switchHeatNo">换炉</a-button>
+    </a-col>
+    <a-col :span="2" class="current-shift flex items-center justify-center">当班统计:</a-col>
+    <a-col :span="1" class="flex items-center justify-center">
+      <a-statistic title="总支数" :value="info.billetHotsendChangeShift.shiftSum" />
+    </a-col>
+    <a-col :span="2" class="flex items-center justify-center">
+      <a-statistic title="总重量/t" :value="info.billetHotsendChangeShift.shiftProduct" />
+    </a-col>
+    <a-col :span="3" class="flex flex-col items-center justify-center" v-for="item of typeList">
+      <div class="type-item">
+        <span class="type-title" :style="{ color: item.titleColor }">{{ item.title }}:</span> {{ item.amount }}支 / {{ item.weight }}t
+      </div>
+      <div class="type-item" v-for="ele of item.dtlList">
+        <div class="type-item-dtl">
+          <span class="type-title">{{ Number(ele.size) / 1000 }}:</span> {{ ele.amount }}支 / {{ ele.weight }}t
+        </div>
+      </div>
+    </a-col>
+    <a-col :span="2" class="paihao flex items-center justify-center">当前牌号:<component :is="renderDictTag(info.brandNum, 'billet_spec')" /></a-col>
+    <a-col :span="2" class="current-shift flex items-center justify-center">
+      <a-button type="primary" danger @click="openPaihaoModal = true">切换牌号</a-button>
+    </a-col>
+  </a-row>
+  <a-modal
+    v-model:open="openPaihaoModal"
+    title="切换牌号"
+    centered
+    width="400px"
+    ok-text="确认"
+    :okButtonProps="{ loading: okLoading }"
+    cancel-text="取消"
+    @ok="switchSteelOne"
+    @cancel="
+      () => {
+        openPaihaoModal = false;
+        newBrandNum = '';
+      }
+    "
+  >
+    <div class="flex justify-center items-center" style="margin: 20px 0">
+      <div>选择牌号:</div>
+      <JSearchSelect type="list" style="width: 117px" v-model:value="newBrandNum" dict="billet_spec" placeholder="请选择" allowClear />
+    </div>
+  </a-modal>
+</template>
+<script setup lang="ts">
+  import { ref, h, onMounted, onUnmounted } from 'vue';
+  import AStatistic from 'ant-design-vue/lib/statistic/Statistic';
+  import { getOnDutyInfo, getTeamShift, getOnDutyDetail } from '../../Dashboard/dashboard.api';
+  import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+  import { switchSteel, furnaceChange } from '../operator.api';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue';
+  import { render } from '/@/utils/common/renderUtils';
+
+  const { createConfirm, createMessage } = useMessage();
+
+  const emits = defineEmits(['shiftChange']);
+
+  const props = defineProps({
+    ccmNo: {
+      type: String,
+      default: '5',
+    },
+  });
+
+  const info = ref({
+    brandNum: '',
+    billetHotsendChangeShift: {
+      heatNo: '',
+      shiftGroup: '',
+      shift: '',
+      wasteAmount: 0,
+      shiftProduct: 0,
+      shiftSum: 0,
+      wasteBlankOutput: 0,
+    },
+    currentCastingFurnaceAmount: null,
+    currentCastingFurnace: null,
+  });
+
+  // typeList
+  const typeList = ref<any>([
+    { title: '棒一', titleColor: '#f50', amount: 0, weight: 0, dtlList: [] },
+    { title: '热装', titleColor: '#f50', amount: 0, weight: 0, dtlList: [] },
+    { title: '堆垛', titleColor: '#b8ceff', amount: 0, weight: 0, dtlList: [] },
+  ]);
+
+  // 渲染字典标签
+  const renderDictTag = (value: string, dictCode: string) => {
+    return render.renderDict(value, dictCode);
+  };
+
+  // 获取班组班别
+  const getShiftInfo = () => {
+    emits('shiftChange', getTeamShift(info.value.billetHotsendChangeShift.shift, info.value.billetHotsendChangeShift.shiftGroup));
+  };
+
+  // 获取当前班次信息
+  const getInfo = async () => {
+    try {
+      const infoRes = await getOnDutyInfo({ ccmNo: props.ccmNo });
+      if (infoRes) {
+        info.value = infoRes;
+      }
+
+      // 获取明细
+      const dtlRes = await getOnDutyDetail({ ccmNo: props.ccmNo });
+      if (dtlRes) {
+        const {
+          hotSendSum,
+          hotChargeSum,
+          stackingSum,
+          hotSendTotalWeight,
+          hotChargeTotalWeight,
+          stackingTotalWeight,
+          hotSendDetailStatisticsList,
+          hotChargeDetailStatisticsList,
+          stackingDetailStatisticsList,
+        } = dtlRes;
+
+        let hotsendData: any = [],
+          hotchargeData: any = [],
+          stackingData: any = [];
+        if (Array.isArray(hotSendDetailStatisticsList)) {
+          hotsendData = hotSendDetailStatisticsList.map((item) => {
+            return { size: item.size, amount: item.amountTotal || 0, weight: item.blankOutput || 0 };
+          });
+        }
+        if (Array.isArray(hotChargeDetailStatisticsList)) {
+          hotchargeData = hotChargeDetailStatisticsList.map((item) => {
+            return { size: item.size, amount: item.amountTotal || 0, weight: item.blankOutput || 0 };
+          });
+        }
+        if (Array.isArray(stackingDetailStatisticsList)) {
+          stackingData = stackingDetailStatisticsList.map((item) => {
+            return { size: item.size, amount: item.amountTotal || 0, weight: item.blankOutput || 0 };
+          });
+        }
+
+        typeList.value = [
+          { title: '棒一', titleColor: '#f50', amount: hotSendSum || 0, weight: hotSendTotalWeight || 0, dtlList: hotsendData },
+          { title: '热装', titleColor: '#f50', amount: hotChargeSum || 0, weight: hotChargeTotalWeight || 0, dtlList: hotchargeData },
+          { title: '堆垛', titleColor: '#b8ceff', amount: stackingSum || 0, weight: stackingTotalWeight || 0, dtlList: stackingData },
+        ];
+
+        getShiftInfo();
+      }
+    } catch (error) {
+    } finally {
+      start();
+    }
+  };
+
+  const { start, stop } = useTimeoutFn(getInfo, 5000);
+
+  // 换炉
+  const switchHeatNo = async () => {
+    createConfirm({
+      iconType: 'warning',
+      title: '确认换炉',
+      width: '460px',
+      content: () => {
+        return h('div', { style: { fontSize: '16px' } }, [h('span', null, `是否换炉`), h('span', null, `?`)]);
+      },
+      onOk: () => {
+        return furnaceChange({ ccmNo: props.ccmNo, heatNo: info.value.billetHotsendChangeShift.heatNo }).then(() => {
+          getInfo();
+        });
+      },
+    });
+  };
+
+  // 切换牌号
+  const openPaihaoModal = ref(false);
+  const newBrandNum = ref('');
+  const okLoading = ref(false);
+  const switchSteelOne = async () => {
+    try {
+      if (newBrandNum.value == '') {
+        createMessage.error('请选择牌号');
+        return;
+      }
+      okLoading.value = true;
+      await switchSteel({
+        ccmNo: props.ccmNo,
+        brandNum: newBrandNum.value,
+      });
+
+      getInfo();
+      openPaihaoModal.value = false;
+      newBrandNum.value = '';
+      okLoading.value = false;
+    } catch (error) {
+      okLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    getInfo();
+  });
+
+  onUnmounted(() => {
+    stop();
+  });
+</script>
+<style lang="less" scoped>
+  .head-top {
+    height: 100px;
+    font-size: 16px;
+    color: var(--op-text-color-fff);
+    border: 1px solid var(--op-border-color);
+    border-radius: 6px;
+    background-color: #01396c;
+    overflow: hidden;
+
+    .ant-col {
+      height: 100px;
+      border-right: 1px solid var(--op-border-color);
+      overflow: auto;
+      padding: 10px;
+      font-size: 16px;
+
+      &:last-child {
+        border-right: none;
+      }
+    }
+
+    .head-no {
+      height: 100px;
+      font-size: 16px;
+    }
+    .bg {
+      background: #0085ff;
+    }
+
+    .ant-statistic {
+      :deep(.ant-statistic-title),
+      :deep(.ant-statistic-content-value-int),
+      :deep(.ant-statistic-content-value-decimal) {
+        color: var(--op-text-color-fff);
+        text-align: center;
+        font-size: 16px;
+      }
+
+      :deep(.ant-statistic-content-value-int),
+      :deep(.ant-statistic-content-value-decimal) {
+        font-size: 30px;
+      }
+
+      :deep(.ant-statistic-content) {
+        text-align: center;
+      }
+    }
+
+    .ant-btn {
+      height: 80px;
+      font-size: 18px;
+      padding: 4px 10px;
+    }
+
+    .paihao {
+      color: #f50;
+      font-size: 16px;
+    }
+
+    .type-item {
+      font-size: 16px;
+    }
+
+    .change-heat.is-disabled {
+      background: #d9d9d9;
+      color: #8a8a8a;
+    }
+  }
+</style>

+ 323 - 0
src/views/billet/operator/components/heatList.vue

@@ -0,0 +1,323 @@
+<template>
+  <div class="cc-heatList">
+    <div class="tips-title flex">
+      <div class="left-tip">当班浇铸炉次</div>
+      <div class="flex-1">支数(根) / 重量(t)</div>
+    </div>
+    <!--引用表格-->
+    <BasicTable @register="registerTable">
+      <!--操作栏-->
+      <template #action="{ record }">
+        <TableAction :actions="getTableAction(record)" />
+      </template>
+      <!--字段回显插槽-->
+      <!-- <template v-slot:bodyCell="{ column, record, index, text }"> </template> -->
+    </BasicTable>
+  </div>
+</template>
+
+<script lang="ts" name="billetLiftingBill" setup>
+  import { BasicTable, TableAction, ActionItem } from '/@/components/Table';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { columns } from '../operator.data';
+  import { queryHeatsActualsByCcmNo, addHotCharge, stackingUpAdd } from '../operator.api';
+  import { onMounted, onUnmounted } from 'vue';
+  import { mapTableTotalSummary } from '/@/utils/common/compUtils';
+  import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  const { createMessage } = useMessage();
+
+  const props = defineProps({
+    ccmNo: {
+      type: String,
+      default: '5',
+    },
+    carRef: {
+      type: Object,
+      default: () => {
+        return {};
+      },
+    },
+  });
+
+  //注册table数据
+  const { tableContext } = useListPage({
+    tableProps: {
+      api: queryHeatsActualsByCcmNo,
+      beforeFetch: (params) => {
+        return Object.assign(params, { ccmNo: props.ccmNo });
+      },
+      afterFetch: (data) => {
+        const length = data.length;
+        return data.map((item, index) => {
+          return {
+            ...item,
+            columnIndex: length - index,
+          };
+        });
+      },
+      columns,
+      showIndexColumn: false,
+      canResize: true,
+      striped: true,
+      showTableSetting: false,
+      pagination: false,
+      actionColumn: {
+        width: 120,
+        title: '操作',
+        fixed: 'right',
+      },
+      showActionColumn: true,
+      showSummary: true,
+      summaryFunc: onSummary,
+    },
+  });
+
+  /**
+   * 计算合计
+   * @param tableData
+   */
+  function onSummary(tableData: Recordable[]) {
+    // 可用工具方法自动计算合计
+    const totals = mapTableTotalSummary(tableData, [
+      'oneStrandNo',
+      'twoStrandNo',
+      'threeStrandNo',
+      'fourStrandNo',
+      'fiveStrandNo',
+      'sixStrandNo',
+      'sevenStrandNo',
+      'eightStrandNo',
+    ]);
+    // 直轧,热装,堆垛,总计
+    let directRollingTotalCount = 0,
+      directRollingTotalWeight = 0,
+      hotChargeTotalCount = 0,
+      hotChargeTotalWeight = 0,
+      stackingTotalCount = 0,
+      stackingTotalWeight = 0,
+      totalCount = 0,
+      totalWeight = 0;
+    try {
+      tableData.forEach((item) => {
+        const { hotSend, directRolling, hotCharge, stacking, totalInfo } = item;
+        // 直轧
+        if (hotSend) {
+          const obj = JSON.parse(hotSend);
+          directRollingTotalCount += Number(obj.hotSendTotalCount);
+          directRollingTotalWeight += parseFloat(obj.hotSendTotalWeight);
+        }
+        if (directRolling) {
+          const obj = JSON.parse(directRolling);
+          directRollingTotalCount += Number(obj.directRollingTotalCount);
+          directRollingTotalWeight += parseFloat(obj.directRollingTotalWeight);
+        }
+
+        // 热装
+        if (hotCharge) {
+          const obj = JSON.parse(hotCharge);
+          hotChargeTotalCount += Number(obj.hotChargeTotalCount);
+          hotChargeTotalWeight += parseFloat(obj.hotChargeTotalWeight);
+        }
+
+        // 堆垛
+        if (stacking) {
+          const obj = JSON.parse(stacking);
+          stackingTotalCount += Number(obj.stackingTotalCount);
+          stackingTotalWeight += parseFloat(obj.stackingTotalWeight);
+        }
+
+        // 总计
+        if (totalInfo) {
+          const obj = JSON.parse(totalInfo);
+          totalCount += Number(obj.totalCount);
+          totalWeight += parseFloat(obj.totalWeight);
+        }
+      });
+    } catch (error) {}
+
+    return [
+      {
+        ...totals,
+        directRolling: JSON.stringify({ directRollingTotalCount, directRollingTotalWeight: directRollingTotalWeight }),
+        hotCharge: JSON.stringify({ hotChargeTotalCount, hotChargeTotalWeight: hotChargeTotalWeight }),
+        stacking: JSON.stringify({ stackingTotalCount, stackingTotalWeight: stackingTotalWeight }),
+        totalInfo: JSON.stringify({ totalCount, totalWeight: totalWeight }),
+        columnIndex: '合计',
+      },
+    ];
+  }
+
+  const [registerTable, { reload, setLoading }] = tableContext;
+
+  const { start, stop } = useTimeoutFn(() => {
+    reload();
+    start();
+  }, 10000);
+
+  onMounted(() => {
+    start();
+  });
+
+  onUnmounted(() => {
+    stop();
+  });
+
+  // 热装
+  const hotCharge = async (record) => {
+    try {
+      const valiDesRes = props.carRef && props.carRef.valirDest ? props.carRef.valirDest() : true;
+      if (!valiDesRes) {
+        return;
+      }
+
+      const chargeInfo = props.carRef && props.carRef.getCurrentCar ? props.carRef.getCurrentCar() : {};
+      if (!chargeInfo.id) {
+        createMessage.error('获取车辆信息失败,请刷新页面重试~');
+        return;
+      }
+
+      const params = {
+        ccmNo: props.ccmNo,
+        heatNo: record.heatNo,
+        storageId: chargeInfo.id,
+      };
+
+      setLoading(true);
+      await addHotCharge(params);
+      stop();
+      reload();
+      props.carRef.refreshCarList && props.carRef.refreshCarList();
+    } catch (error) {
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 起垛
+  const doStack = async (record) => {
+    try {
+      const selectStackList = props.carRef && props.carRef.getSelectedStack ? props.carRef.getSelectedStack() : [];
+      const emptyAddress = selectStackList.filter((item) => !item.billetNos);
+      if (emptyAddress.length === 0) {
+        createMessage.error('请选择起垛位置!');
+        return;
+      }
+
+      // 堆垛信息
+      const stackInfo = props.carRef && props.carRef.getStackInfo ? props.carRef.getStackInfo() : {};
+      if (!stackInfo.id) {
+        createMessage.error('获取堆垛信息失败,请刷新页面重试~');
+        return;
+      }
+
+      let layerObj = {},
+        sortedStackList: any[] = [];
+      emptyAddress.forEach((item) => {
+        if (!layerObj[item.layer]) layerObj[item.layer] = [];
+        layerObj[item.layer].push(item);
+      });
+
+      // 根据层数和位置排序
+      Object.keys(layerObj)
+        .sort((a, b) => Number(a) - Number(b))
+        .forEach((item) => {
+          layerObj[item].sort((j, k) => Number(j.address) - Number(k.address)).forEach((item) => sortedStackList.push(item));
+        });
+
+      const params = {
+        ccmNo: props.ccmNo,
+        heatNo: record.heatNo,
+        billetHotsendTypeConfigId: stackInfo.id,
+        stackingAndLoadingVehiclesIds: sortedStackList.map((item) => item.id),
+      };
+
+      setLoading(true);
+      await stackingUpAdd(params);
+      stop();
+      reload();
+      props.carRef.refreshCarList && props.carRef.refreshCarList();
+    } catch (error) {
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  /**
+   * 操作栏
+   */
+  function getTableAction(record): ActionItem[] | undefined {
+    if (!record.operateStatus) return undefined;
+
+    const chargeInfo = props.carRef && props.carRef.getCurrentCar ? props.carRef.getCurrentCar() : {};
+    return [
+      {
+        label: '热装',
+        color: 'error',
+        disabled: !chargeInfo.id || !!chargeInfo.outTime,
+        type: 'primary',
+        onClick: () => {
+          hotCharge(record);
+        },
+      },
+      {
+        label: '起垛',
+        color: 'warning',
+        type: 'primary',
+        onClick: () => {
+          doStack(record);
+        },
+      },
+    ];
+  }
+</script>
+
+<style scoped lang="less">
+  .cc-heatList {
+    .tips-title {
+      color: #fff;
+      font-size: 18px;
+      text-align: center;
+      border: 1px solid #fff;
+      margin-bottom: 4px;
+
+      .left-tip {
+        width: 227px;
+        border-right: 1px solid #fff;
+      }
+    }
+
+    .jeecg-basic-table {
+      padding: 0;
+    }
+
+    :deep(.ant-table) {
+      .ant-table-header .ant-table-thead > tr > th {
+        font-size: 16px;
+        font-weight: 800;
+        color: #fff;
+      }
+
+      .ant-table-header .ant-table-thead > tr > th:nth-child(4),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(5),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(6),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(7),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(8),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(9),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(10),
+      .ant-table-header .ant-table-thead > tr > th:nth-child(11) {
+        color: #f50;
+      }
+
+      .ant-table-tbody {
+        font-size: 15px;
+        font-family: 'Kingsoft_Cloud_Font';
+      }
+
+      .ant-table-footer {
+        padding: 0;
+      }
+    }
+  }
+</style>

+ 259 - 0
src/views/billet/operator/components/printCarInfo.vue

@@ -0,0 +1,259 @@
+<template>
+  <section class="printContent">
+    <div class="ticket next-ticket">
+      <div class="flex" style="line-height: 32px">
+        <div class="flex-1">
+          库名:
+          <JSearchSelect
+            v-if="!info.destination && info.id"
+            type="list"
+            style="width: 160px"
+            v-model:value="info.typeConfigId"
+            :options="destinationOptions[ccmNo]"
+            placeholder="请选择"
+            allowClear
+            @change="(v) => handleSelectChange(v, 'destination')"
+            ref="destinationRef"
+          />
+          <span v-else>{{ info.destination }}</span>
+          <a-button style="margin-left: 20px" v-if="!info.outTime && info.id" type="primary" :loading="btnLoading" @click="sendCar">发车</a-button>
+        </div>
+        <div class="flex-1" style="text-align: center; font-size: 16px">{{ dayjs(info.arrivalTime).format('YYYY 年 MM 月 DD 日 HH 时 mm 分') }}</div>
+        <div class="flex-1" style="text-align: right; font-size: 16px"></div>
+      </div>
+      <a-descriptions layout="vertical" bordered :column="8" size="small">
+        <a-descriptions-item label="序号">
+          <div style="min-height: 80px; display: flex; align-items: center; justify-content: center"
+            ><span>{{ info.carAllNum }}</span></div
+          >
+        </a-descriptions-item>
+        <a-descriptions-item label="车号">
+          <!-- {{ info.licensePlate }} -->
+          <JSearchSelect
+            type="list"
+            v-if="!info.outTime && info.id"
+            style="width: 117px"
+            v-model:value="info.licensePlate"
+            dict="lg_car"
+            @change="(v) => handleSelectChange(v, 'licensePlate')"
+            placeholder="请选择"
+            allowClear
+          />
+          <span v-else>{{ info.licensePlate }}</span>
+        </a-descriptions-item>
+        <a-descriptions-item label="牌号">
+          <JSearchSelect
+            type="list"
+            v-if="!info.outTime && info.id"
+            style="width: 117px"
+            v-model:value="info.brandNum"
+            dict="billet_spec"
+            @change="(v) => handleSelectChange(v, 'brandNum')"
+            placeholder="请选择"
+            allowClear
+          />
+          <span v-else>
+            <component :is="renderDictTag(info.brandNum, 'billet_spec')" />
+          </span>
+        </a-descriptions-item>
+        <a-descriptions-item label="炉号">
+          <div v-for="item in headDtl" :key="item.id">{{ item.heatNo }} - {{ item.billetNos.length }}</div>
+        </a-descriptions-item>
+        <a-descriptions-item label="规格">
+          170 /
+          <JSearchSelect
+            class="size-input"
+            type="list"
+            v-if="!info.outTime && info.id"
+            style="width: 80px; text-align: left"
+            v-model:value="info.size"
+            dict="lg_dcgg"
+            @change="(v) => handleSelectChange(v, 'size')"
+            placeholder="请选择"
+            allowClear
+          />
+          <span v-else>
+            {{ info.size }}
+          </span>
+        </a-descriptions-item>
+        <a-descriptions-item label="名称"> 方坯 </a-descriptions-item>
+        <a-descriptions-item label="支数">
+          {{ info.amountTotal }}
+        </a-descriptions-item>
+        <a-descriptions-item label="重量(t)">
+          {{ weight > 0 ? weight.toFixed(4) : 0 }}
+        </a-descriptions-item>
+      </a-descriptions>
+    </div>
+  </section>
+</template>
+
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import dayjs from 'dayjs';
+  import { listShippingBill } from '../../shippingBill/shippingBill.api';
+  import { destinationOptions } from '../../hotDelivery/common.data';
+  import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue';
+  import { startCar } from '../../shippingBill/shippingBill.api';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { render } from '/@/utils/common/renderUtils';
+
+  const { createConfirm, createMessage } = useMessage();
+
+  const emits = defineEmits(['change', 'refresh']);
+
+  const props = defineProps({
+    ccmNo: {
+      type: [String, Number],
+      default: '5',
+    },
+    api: {
+      type: Function as PropType<(params: any) => Promise<any>>,
+      default: listShippingBill,
+    },
+    info: {
+      type: Object,
+      default: () => {},
+    },
+    carPositon: {
+      type: [Number, String],
+      default: 1,
+    },
+  });
+
+  const destinationRef = ref<any>(null);
+  const headDtl = ref<any[]>([]);
+  const weight = ref<number>(0);
+  const sizeInfo = ref<string[]>([]);
+  // 渲染字典标签
+  const renderDictTag = (value: string, dictCode: string) => {
+    return render.renderDict(value, dictCode);
+  };
+
+  const getTableList = async () => {
+    try {
+      const { id, ccmNo, heatNo, typeConfigId } = props.info;
+      const data = await props.api({
+        id,
+        ccmNo,
+        heatNo,
+        typeConfigId,
+      });
+
+      let newArr: any[] = [];
+      if (Array.isArray(data)) {
+        newArr = data;
+      } else {
+        for (let item in data) {
+          if (data[item] && data[item].length > 0) {
+            newArr = newArr.concat(data[item]);
+          }
+        }
+      }
+      let allWeight = 0;
+
+      let sizeArr: string[] = [];
+      // 数据结果按组批号分组
+      newArr = newArr.reduce((acc, cur) => {
+        const index = acc.findIndex((item) => cur.heatNo && item.heatNo === cur.heatNo);
+        if (index === -1) {
+          acc.push({
+            ...cur,
+            billetNos: [...cur.billetNo.split(',')],
+          });
+        } else {
+          acc[index].billetNos = acc[index].billetNos.concat(cur.billetNo.split(','));
+        }
+
+        if (cur.blankOutput) {
+          allWeight += Number(cur.blankOutput);
+        }
+        if (sizeArr.indexOf(cur.size) === -1) {
+          sizeArr.push(cur.size);
+        }
+        return acc;
+      }, []);
+
+      headDtl.value = newArr;
+      weight.value = allWeight;
+      sizeInfo.value = sizeArr;
+    } catch (error) {
+      console.log(error);
+    } finally {
+    }
+  };
+
+  const handleSelectChange = (v: any, type: string) => {
+    emits('change', { value: v, type, carPositon: props.carPositon });
+  };
+
+  // 发车
+  const btnLoading = ref(false);
+  const sendCar = async () => {
+    try {
+      if (!props.info.destination) {
+        createMessage.error('请选择库名');
+        return;
+      }
+
+      if (!props.info.licensePlate) {
+        createMessage.error('请选择车牌号');
+        return;
+      }
+
+      createConfirm({
+        iconType: 'warning',
+        title: '请确认是否发车?',
+        onOk: () => {
+          return startCar({ ...props.info }).then(() => {
+            emits('refresh');
+          });
+        },
+      });
+    } catch (error) {
+      console.log(error);
+    }
+  };
+
+  defineExpose({
+    getTableList,
+    focusDestination: () => {
+      // console.log('22222222222222222222', destinationRef.value);
+      // destinationRef.value && destinationRef.value.handleAsyncFocus();
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .printContent {
+    padding: 10px;
+    position: relative;
+    border: 1px solid #c5c5c5;
+    background: #01396c;
+    width: 100%;
+    border-radius: 4px;
+    color: #fff;
+
+    .ant-descriptions {
+      margin-top: 6px;
+      margin-bottom: 4px;
+    }
+
+    :deep(.ant-descriptions-view) {
+      .ant-descriptions-item-label,
+      .ant-descriptions-item-content {
+        border: 1px solid #bfbfbf;
+        font-size: 16px;
+        padding: 4px;
+        text-align: center;
+        height: 36px;
+        color: #fff;
+      }
+    }
+
+    .size-input {
+      :deep(.ant-select-selector) {
+        padding: 0 4px;
+      }
+    }
+  }
+</style>

+ 81 - 0
src/views/billet/operator/index.less

@@ -0,0 +1,81 @@
+.operator-app-container {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  background-image: url(/@/assets/images/billetHome/bg.png);
+  background-size: cover;
+  background-repeat: no-repeat;
+  display: flex;
+  position: relative;
+
+  .operator-large-layout {
+    width: 100%;
+    height: 100%;
+    background: #010c3a;
+  }
+
+  .ant-layout .operator-header {
+    display: flex;
+    align-items: flex-end;
+    justify-content: flex-end;
+    height: 100px;
+    background: transparent !important;
+    background-image: url(/@/assets/images/billetHome/operator-head-bg.png) !important;
+    background-repeat: no-repeat !important;
+    background-size: 100% 100% !important;
+    position: relative;
+
+    .shift-wrapper {
+      color: #6bd000;
+      font-size: 24px;
+      font-weight: 600;
+      line-height: 72px;
+      margin-right: 120px;
+    }
+
+    .clock-wrapper {
+      display: flex;
+      align-items: center;
+      font-family: 'Kingsoft_Cloud_Font';
+      font-weight: 400;
+      color: #ffffff;
+      font-size: 14px;
+
+      .week {
+        margin: 0 12px;
+      }
+
+      .time {
+        font-size: 20px;
+      }
+    }
+  }
+
+  .operator-content {
+    display: flex;
+    flex-direction: column;
+    margin: 10px;
+    overflow: hidden;
+    flex: 1;
+    font-family: 'Kingsoft_Cloud_Font';
+
+    .operator-content-wrapper {
+      margin-top: 10px;
+      flex: 1;
+      overflow: hidden;
+
+      .operator-content-left {
+        flex: 1 0 60%;
+        width: 60%;
+        padding-right: 20px;
+        overflow: hidden;
+      }
+
+      .operator-content-right {
+        flex: 1 0 40%;
+        width: 40%;
+        overflow: hidden;
+      }
+    }
+  }
+}

+ 78 - 0
src/views/billet/operator/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div
+    class="operator-app-container"
+    :style="{
+      '--op-border-color': '#d9d9d9',
+      '--op-text-color-fff': '#fff',
+    }"
+  >
+    <a-layout class="operator-large-layout">
+      <a-layout-header class="operator-header cover">
+        <div class="shift-wrapper"> {{ shiftText }} </div>
+        <div class="clock-wrapper">
+          <div class="date">{{ clockObj.date }}</div>
+          <div class="week">{{ clockObj.week }}</div>
+          <div class="time">{{ clockObj.time }}</div>
+        </div>
+      </a-layout-header>
+      <a-layout-content class="operator-content">
+        <head-top @shiftChange="(v) => (shiftText = v)" />
+        <div class="operator-content-wrapper flex">
+          <div class="operator-content-left">
+            <heat-list :ccmNo="ccmNo" :carRef="carRef" />
+          </div>
+          <div class="operator-content-right">
+            <car :ccmNo="ccmNo" ref="carRef" />
+          </div>
+        </div>
+      </a-layout-content>
+    </a-layout>
+  </div>
+</template>
+<script setup lang="ts" name="OperatorRoom">
+  import { onMounted, onUnmounted, ref } from 'vue';
+  import { getDateTimeWeek } from '/@/utils/dateUtil';
+  import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+  import headTop from './components/headTop.vue';
+  import car from './components/car.vue';
+  import heatList from './components/heatList.vue';
+  import { getMachineNum } from '../hotDelivery/common.data';
+
+  const ccmNo = getMachineNum();
+
+  const carRef = ref();
+  // 班次信息
+  const shiftText = ref('');
+  // 时钟
+  const clockObj = ref({
+    date: '',
+    week: '',
+    time: '',
+  });
+  const clock = () => {
+    try {
+      const dateStr = getDateTimeWeek().split(' ');
+      clockObj.value = {
+        date: dateStr[0],
+        time: dateStr[1],
+        week: dateStr[2],
+      };
+
+      start();
+    } catch (error) {}
+  };
+
+  const { start, stop } = useTimeoutFn(clock, 1000);
+
+  onMounted(() => {
+    start();
+  });
+
+  onUnmounted(() => {
+    stop();
+  });
+</script>
+<style lang="less" scoped>
+  @import '/@/assets/less/font.less';
+  @import './index.less';
+</style>

+ 39 - 0
src/views/billet/operator/operator.api.ts

@@ -0,0 +1,39 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  // 炉次信息
+  queryHeatsActualsByCcmNo = '/storageBill/queryHeatsActualsByCcmNo',
+  // 切换牌号
+  switchSteel = '/shiftConfiguration/shiftConfiguration/switchSteel',
+  // 换炉
+  furnaceChange = '/sys/billetHotsendBase/billetHotsendBase/furnaceChange',
+  // 热装
+  addHotCharge = '/billetHotsendBase/billetHotsendBase/addHotCharge',
+  // 起垛
+  stackingUpAdd = '/billet/stackingAndLoadingVehicles/stackingUpAdd',
+}
+
+// 炉次信息
+export const queryHeatsActualsByCcmNo = (params: any) => {
+  return defHttp.get({ url: Api.queryHeatsActualsByCcmNo, params });
+};
+
+// 切换牌号
+export const switchSteel = (params: any) => {
+  return defHttp.post({ url: Api.switchSteel, params });
+};
+
+// 换炉
+export const furnaceChange = (params: any) => {
+  return defHttp.post({ url: Api.furnaceChange, params });
+};
+
+// 热装
+export const addHotCharge = (params: any) => {
+  return defHttp.post({ url: Api.addHotCharge, params });
+};
+
+// 起垛
+export const stackingUpAdd = (params: any) => {
+  return defHttp.post({ url: Api.stackingUpAdd, params });
+};

+ 171 - 0
src/views/billet/operator/operator.data.ts

@@ -0,0 +1,171 @@
+import { h } from 'vue';
+import { BasicColumn } from '/@/components/Table';
+import { render } from '/@/utils/common/renderUtils';
+
+const renderNum = (num, weight, backArr?: boolean) => {
+  if (backArr === true) {
+    return [h('span', { style: { color: '#0085ff' } }, num), h('span', {}, '/' + weight.toFixed(2))];
+  }
+  return h('div', {}, [h('span', { style: { color: '#0085ff' } }, num), h('span', {}, '/' + weight.toFixed(2))]);
+};
+
+// 列表数据
+export const columns: BasicColumn[] = [
+  {
+    title: '序号',
+    align: 'center',
+    dataIndex: 'columnIndex',
+    width: 50,
+  },
+  {
+    title: '炉号',
+    align: 'center',
+    dataIndex: 'heatNo',
+    width: 90,
+  },
+  {
+    title: '牌号',
+    align: 'center',
+    width: 80,
+    dataIndex: 'brandNum',
+    customRender(opt) {
+      return render.renderDict(opt.record.brandNum, 'billet_spec');
+    },
+  },
+  {
+    title: '1流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'oneStrandNo',
+  },
+  {
+    title: '2流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'twoStrandNo',
+  },
+  {
+    title: '3流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'threeStrandNo',
+  },
+  {
+    title: '4流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'fourStrandNo',
+  },
+  {
+    title: '5流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'fiveStrandNo',
+  },
+  {
+    title: '6流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'sixStrandNo',
+  },
+  {
+    title: '7流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'sevenStrandNo',
+  },
+  {
+    title: '8流',
+    width: 50,
+    align: 'center',
+    dataIndex: 'eightStrandNo',
+  },
+  {
+    title: '棒一',
+    align: 'center',
+    dataIndex: 'directRolling',
+    customRender({ record }) {
+      const { hotSend, directRolling } = record;
+      try {
+        if (hotSend) {
+          const obj = JSON.parse(hotSend);
+          return obj.hotSendTotalCount + ' / ' + obj.hotSendTotalWeight.toFixed(2);
+        }
+        if (directRolling) {
+          const obj = JSON.parse(directRolling);
+          return obj.directRollingTotalCount + ' / ' + obj.directRollingTotalWeight.toFixed(2);
+        }
+      } catch (error) {}
+      return '';
+    },
+  },
+  {
+    title: '热装',
+    align: 'center',
+    dataIndex: 'hotCharge',
+    customRender({ record }) {
+      const { hotCharge } = record;
+      try {
+        if (hotCharge) {
+          const obj = JSON.parse(hotCharge);
+          // return obj.hotChargeTotalCount + ' / ' + obj.hotChargeTotalWeight.toFixed(2);
+          return renderNum(obj.hotChargeTotalCount, obj.hotChargeTotalWeight);
+        }
+      } catch (error) {}
+      return '';
+    },
+  },
+  {
+    title: '堆垛',
+    align: 'center',
+    dataIndex: 'stacking',
+    customRender({ record }) {
+      const { stacking } = record;
+      try {
+        if (stacking) {
+          const obj = JSON.parse(stacking);
+          // return obj.stackingTotalCount + ' / ' + obj.stackingTotalWeight.toFixed(2);
+          return renderNum(obj.stackingTotalCount, obj.stackingTotalWeight);
+        }
+      } catch (error) {}
+      return '';
+    },
+  },
+  {
+    title: '定尺',
+    align: 'center',
+    dataIndex: 'length',
+    customRender({ record }) {
+      const { length } = record;
+      try {
+        if (length) {
+          const obj = JSON.parse(length);
+          let lengthCC: any[] = [];
+          Object.keys(obj).forEach((key) => {
+            // lengthCC.push(h('div', {},  key + ' : ' + obj[key].lengthTotalCount + ' / ' + obj[key].lengthTotalWeight.toFixed(2)));
+            const lengthStlArr = renderNum(obj[key].lengthTotalCount, obj[key].lengthTotalWeight, true) as any[];
+            lengthCC.push(h('div', {}, [h('span', {}, Number(key) / 1000 + ' : '), ...lengthStlArr]));
+          });
+          return h('div', { style: { fontSize: '12px', textAlign: 'left' } }, lengthCC);
+        }
+      } catch (error) {}
+      return '';
+    },
+  },
+  {
+    title: '总计',
+    align: 'center',
+    dataIndex: 'totalInfo',
+    customRender({ record }) {
+      try {
+        const { totalInfo } = record;
+        if (totalInfo) {
+          const obj = JSON.parse(totalInfo);
+          // return obj.totalCount + ' / ' + obj.totalWeight;
+          return renderNum(obj.totalCount, obj.totalWeight);
+        }
+      } catch (error) {}
+      return '';
+    },
+  },
+];

+ 0 - 1
src/views/billet/shippingBill/components/hotCharging/stack.vue

@@ -408,7 +408,6 @@
         curDestination = orgDestinationList.value.find((item) => item.id === selectedStack.value.destination) || {};
       }
       const { heatNo, ccmNo, shift, shiftGroup } = record.value;
-      console.log('111111111111111111111', curDestination);
       const params = {
         storageBill: curCar || {},
         belongTable: curStack?.belongTable,

+ 8 - 1
src/views/billet/shippingBill/shippingBill.api.ts

@@ -36,7 +36,9 @@ enum Api {
   // 装运单,以车次为主
   listShippingBillDetails = '/storageBill/queryOnDutyStorageBillStatistics',
   // 移除钢坯
-  removeBillets = '/storageBill/deleteByAssemblyNumber',
+  removeBillets = 'storageBill/deleteByAssemblyNumber',
+  // 发车
+  startCar = '/storageBill/startCar',
 }
 
 /**
@@ -121,3 +123,8 @@ export const removeBillets = (params) => defHttp.delete({ url: Api.removeBillets
 export const edit = (params) => {
   return defHttp.put({ url: Api.edit, params }, { joinParamsToUrl: true });
 };
+
+// 发车
+export const startCar = (params) => {
+  return defHttp.post({ url: Api.startCar, params }, { joinParamsToUrl: true });
+};

+ 35 - 14
src/views/billet/steelRolling/commonTable.vue

@@ -1,29 +1,42 @@
 <template>
   <div class="common-table">
-    <a-descriptions bordered style="min-height: 72px" size="small" :label-style="{ fontWeight: 'bold', width: '70px' }">
+    <a-descriptions bordered style="min-height: 72px" size="small" :column="5" :label-style="{ fontWeight: 'bold', width: '70px' }">
       <template v-for="item in dtlList" :key="item.title">
         <a-descriptions-item
           :label-style="!item.title ? { display: 'none' } : {}"
           :label="item.title"
-          v-if="item.zjNo === 0 || (item.zjNo === 5 && line !== 'rollHeight') || (item.zjNo === 6 && line !== 'rollClubOne')"
+          v-if="
+            (item.zjNo === 0 || (item.zjNo === 5 && line !== 'rollHeight') || (item.zjNo === 6 && line !== 'rollClubOne')) && Number(item.nums) > 0
+          "
         >
-          <div class="flex">
-            <div class="flex nb-left">
-              <div class="flex nick">
-                <span class="total">总数:{{ item.nums }} 支</span>
+          <div :class="'wrapper-' + item.zjNo">
+            <div class="nb-left">
+              <div class="flex">
+                <div class="flex nick">
+                  <span class="total">总数:{{ item.nums }} 支</span>
+                </div>
+
+                <div class="flex nick">
+                  <span>重量:{{ item.blankOutput }}</span>
+                </div>
               </div>
 
               <div class="flex dtl-wrapper" v-if="item.statisticsDetailsList.length > 0">
                 <div>明细:</div>
-                <div class="dtl">
-                  <div class="flex dtl-item" v-for="ele in item.statisticsDetailsList">
-                    <span class="nums">{{ ele.size }}:&nbsp;&nbsp;&nbsp;{{ ele.nums }} 支</span>
+                <div class="dtl flex-1 flex flex-wrap">
+                  <div class="flex-dlt-item flex" v-for="ele in item.statisticsDetailsList">
+                    <div class="flex dtl-item bt-line">
+                      <span class="nums">{{ ele.size }}:&nbsp;&nbsp;&nbsp;{{ ele.nums }} 支</span>
+                    </div>
+                    <div class="flex dtl-item">
+                      <span class="nums bt-line">重量:{{ ele.blankOutput }} t</span>
+                    </div>
                   </div>
                 </div>
               </div>
             </div>
 
-            <div class="flex nb-right">
+            <!-- <div class="flex nb-right">
               <div class="flex nick">
                 <span>重量:{{ item.blankOutput }}</span>
               </div>
@@ -35,7 +48,7 @@
                   </div>
                 </div>
               </div>
-            </div>
+            </div> -->
           </div>
         </a-descriptions-item>
       </template>
@@ -228,6 +241,10 @@
     margin: 10px 10px 0;
     background-color: #fff;
 
+    .wrapper-0 {
+      min-width: 300px;
+    }
+
     .nb-left,
     .nb-right {
       flex-direction: column;
@@ -240,7 +257,7 @@
 
       .total {
         display: inline-block;
-        min-width: 100px;
+        min-width: 130px;
         margin-right: 30px;
       }
     }
@@ -251,14 +268,18 @@
 
     .dtl {
       display: flex;
-      flex-direction: column;
       color: rgba(0, 0, 0, 0.6);
+      gap: 8px 16px;
 
       .nums {
         display: inline-block;
-        min-width: 100px;
+        min-width: 130px;
         margin-right: 30px;
       }
+
+      .bt-line {
+        border-bottom: 1px solid #eee;
+      }
     }
   }
 </style>

+ 2 - 2
src/views/billet/storageAndTransportation/index.vue

@@ -101,7 +101,7 @@
 
   const { createMessage } = useMessage();
   //导入导出方法
-  const { handleExportXls } = useMethods();
+  const { handleExportXlsx } = useMethods();
 
   // 渲染字典标签
   const renderDictTag = (value: string, dictCode: string) => {
@@ -354,7 +354,7 @@
         return `${key}=${params[key]}`;
       });
 
-    return handleExportXls('储运中心', exportExcel() + (queryParams.length > 0 ? '?' + queryParams.join('&') : ''));
+    return handleExportXlsx('储运中心', exportExcel() + (queryParams.length > 0 ? '?' + queryParams.join('&') : ''));
   };
 
   onMounted(() => {