广丰卷烟厂数采质量分析系统
perf: 1.优化储丝柜单柜卷包产量计算方式,支持未出料结束计算统计   2.新增储丝柜单柜卷包产量桑基图
已修改16个文件
470 ■■■■ 文件已修改
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/开发日志.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/hooks/common/echarts.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/router/elegant/imports.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/router/elegant/routes.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/router/elegant/transform.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/typings/elegant-router.d.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/analy/feed-match/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/analy/store-silk/index.vue 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkDetailVo.java
@@ -49,7 +49,17 @@
    private Date shiftEndTime;
    /**
     * è¯¥ç­æ¬¡å†…计算出的产量
     * è¯¥ç­æ¬¡å†…计算出的产量(作为记录时正数表示增加,负数表示剔除)
     */
    private Double output;
    /**
     * æ“ä½œç±»åž‹æè¿°ï¼ˆå¦‚ "班次累计", "扣除头部", "扣除尾部")
     */
    private String calcType;
    /**
     * å‘½ä¸­æ•°æ®çš„æ—¶é—´ç‚¹ï¼ˆç”¨äºŽè¿½æº¯æ•°æ®æ¥æºï¼‰
     */
    private Date hitTime;
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/domain/vo/StoreSilkInfoVo.java
@@ -106,7 +106,17 @@
    private List<StoreSilkDetailVo> rollerDetailList;
    /**
     * åŒ…装机产量明细
     * åŒ…装机产量明细(最终结果)
     */
    private List<StoreSilkDetailVo> packerDetailList;
    /**
     * å·æŽ¥æœºæ“ä½œè¿‡ç¨‹è®°å½•(增减明细)
     */
    private List<StoreSilkDetailVo> rollerRecordList;
    /**
     * åŒ…装机操作过程记录(增减明细)
     */
    private List<StoreSilkDetailVo> packerRecordList;
}
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/FeedmatchTimeDataServiceImpl.java
@@ -9,6 +9,7 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.qa.analy.domain.StoreSilkInfo;
import org.springframework.stereotype.Service;
import org.dromara.qa.analy.domain.bo.FeedmatchTimeDataBo;
import org.dromara.qa.analy.domain.vo.FeedmatchTimeDataVo;
@@ -74,7 +75,9 @@
    private LambdaQueryWrapper<FeedmatchTimeData> buildQueryWrapper(FeedmatchTimeDataBo bo) {
        Map<String, Object> params = bo.getParams();
        LambdaQueryWrapper<FeedmatchTimeData> lqw = Wrappers.lambdaQuery();
        lqw.eq(bo.getTime() != null, FeedmatchTimeData::getTime, bo.getTime());
        lqw.eq(bo.getShift() != null, FeedmatchTimeData::getShift, bo.getShift());
        lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
                FeedmatchTimeData::getTime, params.get("beginTime"), params.get("endTime"));
        lqw.eq(StringUtils.isNotBlank(bo.getKey()), FeedmatchTimeData::getKey, bo.getKey());
        return lqw;
    }
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
@@ -219,9 +219,12 @@
            // å·åŒ…产量统计
            Double rollerOutput = 0.0;
            Double packerOutput = 0.0;
            // æ˜Žç»†åˆ—表
            // æ˜Žç»†åˆ—表(仅存最终正数结果)
            List<StoreSilkDetailVo> rollerDetailList = new ArrayList<>();
            List<StoreSilkDetailVo> packerDetailList = new ArrayList<>();
            // æ“ä½œè®°å½•列表(存所有增减过程)
            List<StoreSilkDetailVo> rollerRecordList = new ArrayList<>();
            List<StoreSilkDetailVo> packerRecordList = new ArrayList<>();
            for (int s = 0; s < distShiftList.size(); s++) {
                MdShiftBo shiftBo = distShiftList.get(s);
@@ -347,6 +350,20 @@
                        // å…ˆæŠŠç»Ÿè®¡ç»“束时刻累计值加进来:current = Qty(calcEnd)
                        currentRollerOutput += rData.getQty();
                        // è®°å½•过程:班次截止累计
                        StoreSilkDetailVo endRecord = new StoreSilkDetailVo();
                        endRecord.setFsNum(fsNum.substring(2, 3));
                        endRecord.setSiloNum(containerNum);
                        endRecord.setPipeNum(channel);
                        endRecord.setEquNo(equNo);
                        endRecord.setShiftCode(shift);
                        endRecord.setShiftStartTime(calcStartDate);
                        endRecord.setShiftEndTime(calcEndDate);
                        endRecord.setOutput(rData.getQty());
                        endRecord.setCalcType("班次截止累计");
                        endRecord.setHitTime(rData.getTime());
                        rollerRecordList.add(endRecord);
                        // 2) æ‰£â€œå¤´â€ï¼šå¦‚果统计开始时刻晚于班次开始,则减去 Qty(calcStart)
                        if (calcStartDate.after(stimDate)) {
                            LocalDateTime calcStartLdt = LocalDateTime.ofInstant(calcStartDate.toInstant(), zone);
@@ -365,6 +382,20 @@
                            RollerTimeData rBeginData = rollerTimeDataMapper.selectOne(beginRlqw);
                            if (rBeginData != null) {
                                currentRollerOutput -= rBeginData.getQty();
                                // è®°å½•过程:扣除头部产量
                                StoreSilkDetailVo beginRecord = new StoreSilkDetailVo();
                                beginRecord.setFsNum(fsNum.substring(2, 3));
                                beginRecord.setSiloNum(containerNum);
                                beginRecord.setPipeNum(channel);
                                beginRecord.setEquNo(equNo);
                                beginRecord.setShiftCode(shift);
                                beginRecord.setShiftStartTime(calcStartDate);
                                beginRecord.setShiftEndTime(calcEndDate);
                                beginRecord.setOutput(-rBeginData.getQty()); // è´Ÿæ•°è¡¨ç¤ºæ‰£é™¤
                                beginRecord.setCalcType("扣除出料前累计");
                                beginRecord.setHitTime(rBeginData.getTime());
                                rollerRecordList.add(beginRecord);
                            }
                        }
                    }
@@ -401,6 +432,20 @@
                        // å…ˆæŠŠç»Ÿè®¡ç»“束时刻累计值加进来:current = Qty(calcEnd)
                        currentPackerOutput += pData.getTsQty();
                        // è®°å½•过程:班次截止累计
                        StoreSilkDetailVo endRecord = new StoreSilkDetailVo();
                        endRecord.setFsNum(fsNum.substring(2, 3));
                        endRecord.setSiloNum(containerNum);
                        endRecord.setPipeNum(channel);
                        endRecord.setEquNo(equNo);
                        endRecord.setShiftCode(shift);
                        endRecord.setShiftStartTime(calcStartDate);
                        endRecord.setShiftEndTime(calcEndDate);
                        endRecord.setOutput(pData.getTsQty());
                        endRecord.setCalcType("班次截止累计");
                        endRecord.setHitTime(pData.getTime());
                        packerRecordList.add(endRecord);
                        // 2) æ‰£â€œå¤´â€ï¼šå¦‚果统计开始时刻晚于班次开始,则减去 Qty(calcStart)
                        if (calcStartDate.after(stimDate)) {
                            LocalDateTime calcStartLdt = LocalDateTime.ofInstant(calcStartDate.toInstant(), zone);
@@ -419,6 +464,20 @@
                            PackerTimeData pBeginData = packerTimeDataMapper.selectOne(beginPlqw);
                            if (pBeginData != null) {
                                currentPackerOutput -= pBeginData.getTsQty();
                                // è®°å½•过程:扣除头部产量
                                StoreSilkDetailVo beginRecord = new StoreSilkDetailVo();
                                beginRecord.setFsNum(fsNum.substring(2, 3));
                                beginRecord.setSiloNum(containerNum);
                                beginRecord.setPipeNum(channel);
                                beginRecord.setEquNo(equNo);
                                beginRecord.setShiftCode(shift);
                                beginRecord.setShiftStartTime(calcStartDate);
                                beginRecord.setShiftEndTime(calcEndDate);
                                beginRecord.setOutput(-pBeginData.getTsQty()); // è´Ÿæ•°è¡¨ç¤ºæ‰£é™¤
                                beginRecord.setCalcType("扣除出料前累计");
                                beginRecord.setHitTime(pBeginData.getTime());
                                packerRecordList.add(beginRecord);
                            }
                        }
                    }
@@ -444,6 +503,8 @@
            storeSilkInfoVo.setPackerOutput(packerOutput);
            storeSilkInfoVo.setRollerDetailList(rollerDetailList);
            storeSilkInfoVo.setPackerDetailList(packerDetailList);
            storeSilkInfoVo.setRollerRecordList(rollerRecordList);
            storeSilkInfoVo.setPackerRecordList(packerRecordList);
        }
@@ -558,11 +619,44 @@
        LambdaQueryWrapper<StoreSilkInfo> lqw = Wrappers.lambdaQuery();
        lqw.orderByAsc(StoreSilkInfo::getId);
        lqw.like(StringUtils.isNotBlank(bo.getMaterialname()), StoreSilkInfo::getMaterialname, bo.getMaterialname());
        lqw.eq(StringUtils.isNotBlank(bo.getBatchcode()), StoreSilkInfo::getBatchcode, bo.getBatchcode());
        lqw.eq(bo.getActualstarttime() != null, StoreSilkInfo::getActualstarttime, bo.getActualstarttime());
        lqw.like(StringUtils.isNotBlank(bo.getBatchcode()), StoreSilkInfo::getBatchcode, bo.getBatchcode());
        if (bo.getActualstarttime() != null) {
            ZoneId zone = ZoneId.systemDefault();
            LocalDate day = bo.getActualstarttime().toInstant().atZone(zone).toLocalDate();
            Date dayStart = Date.from(day.atStartOfDay(zone).toInstant());
            Date nextDayStart = Date.from(day.plusDays(1).atStartOfDay(zone).toInstant());
            lqw.ge(StoreSilkInfo::getActualstarttime, dayStart);
            lqw.lt(StoreSilkInfo::getActualstarttime, nextDayStart);
        }
        lqw.eq(bo.getDistimebegin() != null, StoreSilkInfo::getDistimebegin, bo.getDistimebegin());
        lqw.eq(bo.getDistimeend() != null, StoreSilkInfo::getDistimeend, bo.getDistimeend());
        lqw.eq(StringUtils.isNotBlank(bo.getSiloid()), StoreSilkInfo::getSiloid, bo.getSiloid());
        if (StringUtils.isNotBlank(bo.getSiloid())) {
            // æ”¯æŒå¤šä¸ªæŸœå·æŸ¥è¯¢ï¼Œä»¥é€—号分隔
            String[] siloids = bo.getSiloid().split(",");
            lqw.and(wrapper -> {
                for (String val : siloids) {
                    val = val.trim();
                    if (StringUtils.isBlank(val)) {
                        continue;
                    }
                    String finalVal = val;
                    // ä½¿ç”¨ OR è¿žæŽ¥å¤šä¸ªæŸœå·æ¡ä»¶
                    wrapper.or(w -> {
                        try {
                            int num = Integer.parseInt(finalVal);
                            String padded = String.format("%02d", num);
                            // åŒ¹é… _1 æˆ– _01 (兼容不补零和补零的情况)
                            w.likeLeft(StoreSilkInfo::getSiloid, "_" + num)
                                    .or()
                                    .likeLeft(StoreSilkInfo::getSiloid, "_" + padded);
                        } catch (NumberFormatException e) {
                            // éžæ•°å­—则按原值匹配
                            w.eq(StoreSilkInfo::getSiloid, finalVal);
                        }
                    });
                }
            });
        }
        lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
                StoreSilkInfo::getDistimeend, params.get("beginTime"), params.get("endTime"));
        return lqw;
RuoYi-Vue-Plus/¿ª·¢ÈÕÖ¾.md
@@ -13,6 +13,8 @@
#### 20260302
- å¾…办事项
- [ ] ç›®å‰å‚¨ä¸æŸœä¿¡æ¯ä¿¡æ¯æ²¡æœ‰ä»Žè§†å›¾ä¸­èŽ·å–ï¼Œä½¿ç”¨oracle_store_silk表模拟->使用制丝车间真实视图数据源
- [ ] åˆ¶ä¸é›†æŽ§ï¼ˆOracle è§†å›¾ï¼‰ä¸­æ²¡æœ‰å‡ºæ–™ç»“束时间,以及实时重量是如何计算
- [ ] å–‚丝机对应关系,目前redis数据库命名规范3.4有4号机,实际车间没有4号机,需到现场校验对应关系
- [ ] å–‚丝机对应关系,fs开头储存的真实值是什么,目前测试数据使用的是 fs11 -> å‚¨ä¸æŸœå·
- [ ] ç¡®è®¤1台喂丝机对应的2台储丝柜是依次供丝还是可以并行供丝(<span style="color: red;">***并行供丝无法准确计算产量***</span>)
- [ ] ç¡®è®¤ å–‚丝机对应关系中(key->pipe01    value->1#机组对应的喂丝机和管道)value不会重复  (<span style="color: red;">***重复将导致计算逻辑出现错误***</span>)
ruoyi-plus-soybean/src/hooks/common/echarts.ts
@@ -1,7 +1,16 @@
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
import * as echarts from 'echarts/core';
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
import {
  BarChart,
  GaugeChart,
  LineChart,
  PictorialBarChart,
  PieChart,
  RadarChart,
  SankeyChart,
  ScatterChart
} from 'echarts/charts';
import type {
  BarSeriesOption,
  GaugeSeriesOption,
@@ -9,6 +18,7 @@
  PictorialBarSeriesOption,
  PieSeriesOption,
  RadarSeriesOption,
  SankeySeriesOption,
  ScatterSeriesOption
} from 'echarts/charts';
import {
@@ -40,6 +50,7 @@
  | PictorialBarSeriesOption
  | RadarSeriesOption
  | GaugeSeriesOption
  | SankeySeriesOption
  | TitleComponentOption
  | LegendComponentOption
  | TooltipComponentOption
@@ -63,6 +74,7 @@
  PictorialBarChart,
  RadarChart,
  GaugeChart,
  SankeyChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer
ruoyi-plus-soybean/src/router/elegant/imports.ts
@@ -38,6 +38,7 @@
  monitor_online: () => import("@/views/monitor/online/index.vue"),
  monitor_operlog: () => import("@/views/monitor/operlog/index.vue"),
  qm_batch: () => import("@/views/qm/batch/index.vue"),
  qm_std: () => import("@/views/qm/std/index.vue"),
  system_client: () => import("@/views/system/client/index.vue"),
  system_config: () => import("@/views/system/config/index.vue"),
  system_dept: () => import("@/views/system/dept/index.vue"),
ruoyi-plus-soybean/src/router/elegant/routes.ts
@@ -264,6 +264,15 @@
          title: 'qm_batch',
          i18nKey: 'route.qm_batch'
        }
      },
      {
        name: 'qm_std',
        path: '/qm/std',
        component: 'view.qm_std',
        meta: {
          title: 'qm_std',
          i18nKey: 'route.qm_std'
        }
      }
    ]
  },
ruoyi-plus-soybean/src/router/elegant/transform.ts
@@ -193,6 +193,7 @@
  "monitor_operlog": "/monitor/operlog",
  "qm": "/qm",
  "qm_batch": "/qm/batch",
  "qm_std": "/qm/std",
  "social-callback": "/social-callback",
  "system": "/system",
  "system_client": "/system/client",
ruoyi-plus-soybean/src/typings/api/analy.feed-match.api.d.ts
@@ -70,7 +70,7 @@
    /** feed match search params */
    type FeedMatchSearchParams = CommonType.RecordNullable<
      Pick<Api.Analy.FeedMatch, 'time' | 'key'> & Api.Common.CommonSearchParams
      Pick<Api.Analy.FeedMatch, 'time' | 'key' | 'shift'> & Api.Common.CommonSearchParams
    >;
    /** feed match operate params */
ruoyi-plus-soybean/src/typings/elegant-router.d.ts
@@ -47,6 +47,7 @@
    "monitor_operlog": "/monitor/operlog";
    "qm": "/qm";
    "qm_batch": "/qm/batch";
    "qm_std": "/qm/std";
    "social-callback": "/social-callback";
    "system": "/system";
    "system_client": "/system/client";
@@ -156,6 +157,7 @@
    | "monitor_online"
    | "monitor_operlog"
    | "qm_batch"
    | "qm_std"
    | "system_client"
    | "system_config"
    | "system_dept"
ruoyi-plus-soybean/src/views/analy/feed-match/index.vue
@@ -24,6 +24,7 @@
  pageSize: 10,
  time: null,
  key: null,
  shift: null,
  params: {}
});
ruoyi-plus-soybean/src/views/analy/feed-match/modules/feed-match-search.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { toRaw } from 'vue';
import { ref, toRaw } from 'vue';
import { jsonClone } from '@sa/utils';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
@@ -21,8 +21,30 @@
const defaultModel = jsonClone(toRaw(model.value));
function resetModel() {
  timeRange.value = null;
  Object.assign(model.value, defaultModel);
}
const timeRange = ref<[string, string] | null>(null);
function onTimeRangeUpdate(value: [string, string] | null) {
  const params = (model.value.params ||= {});
  const beginTime = value?.[0] ? String(value[0]).trim() : '';
  const endTime = value?.[1] ? String(value[1]).trim() : '';
  if (beginTime && endTime) {
    params.beginTime = beginTime;
    params.endTime = endTime;
  } else {
    delete params.beginTime;
    delete params.endTime;
  }
}
const shiftOptions = [
  { label: '早班', value: 1 },
  { label: '中班', value: 2 },
  { label: '晚班', value: 3 }
];
async function reset() {
  await restoreValidation();
@@ -42,18 +64,20 @@
      <NCollapseItem :title="$t('common.search')" name="analy-feed-match-search">
        <NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
          <NGrid responsive="screen" item-responsive>
            <NFormItemGi span="24 s:12 m:6" label="时间戳" label-width="auto" path="time" class="pr-24px">
            <NFormItemGi span="24 s:12 m:8 l:8 xl:8" label="时间区间" label-width="auto" path="params.beginTime" class="pr-24px">
              <NDatePicker
                v-model:formatted-value="model.time"
                type="datetime"
                v-model:formatted-value="timeRange"
                type="datetimerange"
                value-format="yyyy-MM-dd HH:mm:ss"
                clearable
                :default-time="['00:00:00', '23:59:59']"
                @update:formatted-value="onTimeRangeUpdate"
              />
            </NFormItemGi>
            <NFormItemGi span="24 s:12 m:6" label="班次+机台" label-width="auto" path="key" class="pr-24px">
              <NInput v-model:value="model.key" placeholder="请输入班次+机台" />
            <NFormItemGi span="24 s:12 m:8 l:8 xl:8" label="班次" label-width="auto" path="shift" class="pr-24px">
              <NSelect v-model:value="model.shift" placeholder="请选择班次" :options="shiftOptions" clearable />
            </NFormItemGi>
            <NFormItemGi :show-feedback="false" span="24" class="pr-24px">
            <NFormItemGi :show-feedback="false" span="24 s:12 m:8 l:8 xl:8" class="pr-24px">
              <NSpace class="w-full" justify="end">
                <NButton @click="reset">
                  <template #icon>
ruoyi-plus-soybean/src/views/analy/store-silk/index.vue
@@ -1,5 +1,5 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { NDivider } from 'naive-ui';
import { fetchBatchDeleteStoreSilk, fetchGetStoreSilkList } from '@/service/api/analy/store-silk';
import { useAppStore } from '@/store/modules/app';
@@ -11,6 +11,7 @@
import StoreSilkOperateDrawer from './modules/store-silk-operate-drawer.vue';
import StoreSilkSearch from './modules/store-silk-search.vue';
import StoreSilkDetail from './modules/store-silk-detail.vue';
import StoreSilkSankey from './modules/store-silk-sankey.vue';
defineOptions({
  name: 'StoreSilkList'
@@ -20,8 +21,32 @@
const { download } = useDownload();
const { hasAuth } = useAuth();
const selectedRollerDetailList = ref<any[]>([]);
const selectedPackerDetailList = ref<any[]>([]);
// æ¡‘基图start
const showSankey = ref(false);
const rollerRecordList = ref<any[]>([]);
const packerRecordList = ref<any[]>([]);
// æ¡‘基图end
const sankeyRollerBoxList = computed(() => {
  return (rollerRecordList.value || []).map(item => {
    const v = calcRollerBox((item as any).output);
    const output = v === null ? null : Number(v.toFixed(2));
    return { ...(item as any), output };
  });
});
const sankeyPackerBoxList = computed(() => {
  return (packerRecordList.value || []).map(item => {
    const v = calcPackerBox((item as any).output);
    const output = v === null ? null : Number(v.toFixed(2));
    return { ...(item as any), output };
  });
});
const searchParams = ref<Api.Analy.StoreSilkSearchParams>({
  pageNum: 1,
@@ -45,9 +70,33 @@
}
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
  useNaivePaginatedTable({
  useNaivePaginatedTable<any, any>({
    api: () => fetchGetStoreSilkList(searchParams.value),
    transform: response => defaultTransform(response),
    transform: response => {
      const respData = (response as any).data;
      const error = (response as any).error;
      if (!error) {
        const rows = Array.isArray(respData?.rows) ? respData.rows : [];
        const apiRollerRecordList = respData?.rollerRecordList;
        const apiPackerRecordList = respData?.packerRecordList;
        rollerRecordList.value =
          Array.isArray(apiRollerRecordList) && apiRollerRecordList.length > 0
            ? apiRollerRecordList
            : rows.flatMap((r: any) => (Array.isArray(r?.rollerDetailList) ? r.rollerDetailList : []));
        packerRecordList.value =
          Array.isArray(apiPackerRecordList) && apiPackerRecordList.length > 0
            ? apiPackerRecordList
            : rows.flatMap((r: any) => (Array.isArray(r?.packerDetailList) ? r.packerDetailList : []));
      } else {
        rollerRecordList.value = [];
        packerRecordList.value = [];
      }
      return defaultTransform(response as any);
    },
    onPaginationParamsChange: params => {
      searchParams.value.pageNum = params.page;
      searchParams.value.pageSize = params.pageSize;
@@ -64,19 +113,19 @@
        key: 'materialname',
        title: '牌号',
        align: 'center',
        minWidth: 120
        minWidth: 96
      },
      {
        key: 'batchcode',
        title: '批次号',
        align: 'center',
        minWidth: 130
        minWidth: 110
      },
      {
        key: 'rollerOutput',
        title: '卷接产量(ç®±)',
        align: 'center',
        minWidth: 120,
        minWidth: 112,
        render: row => {
          const v = calcRollerBox((row as any).rollerOutput);
          if (v === null) return '-';
@@ -84,10 +133,27 @@
        }
      },
      {
        key: 'rollerUnitCost',
        title: '卷接单耗',
        align: 'center',
        minWidth: 96,
        render: row => {
          const box = calcRollerBox((row as any).rollerOutput);
          if (box === null || box <= 0) return '-';
          const jobinput = Number((row as any).jobinput);
          const weight = Number((row as any).weight);
          const diff = jobinput - weight;
          if (!Number.isFinite(diff)) return '-';
          const val = diff / box;
          if (!Number.isFinite(val)) return '-';
          return val.toFixed(2);
        }
      },
      {
        key: 'packerOutput',
        title: '包装产量(ç®±)',
        align: 'center',
        minWidth: 120,
        minWidth: 112,
        render: row => {
          const v = calcPackerBox((row as any).packerOutput);
          if (v === null) return '-';
@@ -95,46 +161,63 @@
        }
      },
      {
        key: 'packerUnitCost',
        title: '包装单耗',
        align: 'center',
        minWidth: 96,
        render: row => {
          const box = calcPackerBox((row as any).packerOutput);
          if (box === null || box <= 0) return '-';
          const jobinput = Number((row as any).jobinput);
          const weight = Number((row as any).weight);
          const diff = jobinput - weight;
          if (!Number.isFinite(diff)) return '-';
          const val = diff / box;
          if (!Number.isFinite(val)) return '-';
          return val.toFixed(2);
        }
      },
      {
        key: 'actualstarttime',
        title: '投料日期',
        align: 'center',
        width: 180
        width: 160
      },
      {
        key: 'jobinput',
        title: '投料重量',
        align: 'center',
        minWidth: 120
        minWidth: 96
      },
      {
        key: 'weight',
        title: '储丝柜重量',
        align: 'center',
        minWidth: 120
        minWidth: 96
      },
      {
        key: 'distimebegin',
        title: '储丝柜出料开始时间',
        align: 'center',
        width: 180
        width: 160
      },
      {
        key: 'distimeend',
        title: '储丝柜出料结束时间',
        align: 'center',
        width: 180
        width: 160
      },
      {
        key: 'siloid',
        title: '柜子号(末位)',
        align: 'center',
        width: 160
        width: 120
      }
    ]
  });
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
  useTableOperate(data, 'id', getData);
  useTableOperate<any>(data, 'id', getData);
async function handleBatchDelete() {
  // request
@@ -194,12 +277,22 @@
          :loading="loading"
          :show-add="false"
          :show-delete="false"
          :show-export="hasAuth('analy:storeSilk:export')"
          :show-export="!showSankey && hasAuth('analy:storeSilk:export')"
          @add="handleAdd"
          @delete="handleBatchDelete"
          @export="handleExport"
          @refresh="getData"
        >
          <template #prefix>
            <NButton
              size="small"
              ghost
              :type="showSankey ? 'primary' : 'default'"
              @click="showSankey = !showSankey"
            >
              {{ showSankey ? '列表' : '桑基图' }}
            </NButton>
          </template>
          <template #suffix>
            <NPopover placement="bottom-end" trigger="click">
              <template #trigger>
@@ -221,8 +314,16 @@
          </template>
        </TableHeaderOperation>
      </template>
      <StoreSilkSankey
        v-if="showSankey"
        :roller-record-list="sankeyRollerBoxList"
        :packer-record-list="sankeyPackerBoxList"
        :rows="data"
        class="h-full"
      />
      <NDataTable
        :columns="columns"
        v-else
        :columns="columns as any"
        :data="data"
        :size="tableSize"
        :flex-height="!appStore.isMobile"
@@ -242,6 +343,7 @@
      />
    </NCard>
    <StoreSilkDetail
      v-if="!showSankey"
      :roller-detail-list="selectedRollerDetailList"
      :packer-detail-list="selectedPackerDetailList"
      class="h-[192px] overflow-hidden"
@@ -252,7 +354,6 @@
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
  padding-top: 4px;
  padding-bottom: 4px;
  padding: 4px 6px;
}
</style>
ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-detail.vue
@@ -55,38 +55,38 @@
    key: 'index',
    title: $t('common.index'),
    align: 'center',
    width: 64,
    width: 56,
    render: (_: any, index: number) => index + 1
  },
  {
    key: 'fsNum',
    title: '喂丝机号',
    align: 'center',
    width: 100
    width: 84
  },
  {
    key: 'siloNum',
    title: '储丝柜号',
    align: 'center',
    width: 100
    width: 84
  },
  {
    key: 'pipeNum',
    title: '管道号',
    align: 'center',
    width: 100
    width: 84
  },
  {
    key: 'equNo',
    title: '机台号',
    align: 'center',
    width: 100
    width: 72
  },
  {
    key: 'shiftCode',
    title: '班次代码',
    align: 'center',
    width: 100,
    width: 88,
    render: (row: StoreSilkDetailVo) => {
      const map: Record<string, string> = {
        '1': '早班',
@@ -100,19 +100,19 @@
    key: 'shiftStartTime',
    title: '班次开始时间',
    align: 'center',
    width: 160
    width: 148
  },
  {
    key: 'shiftEndTime',
    title: '班次结束时间',
    align: 'center',
    width: 160
    width: 148
  },
  {
    key: 'output',
    title: '产量(千支)',
    align: 'center',
    width: 120,
    width: 96,
    render: (row: StoreSilkDetailVo) => {
      if (row.output === null || row.output === undefined) return '-';
      const v = Number(row.output);
@@ -124,7 +124,7 @@
    key: 'outputBox',
    title: '产量(ç®±)',
    align: 'center',
    width: 100,
    width: 84,
    render: (row: StoreSilkDetailVo) => {
      const v = detailType.value === 'roller' ? calcRollerBox(row.output) : calcPackerBox(row.output);
      if (v === null) return '-';
@@ -170,4 +170,9 @@
:deep(.n-card__content) {
  padding: 8px 12px;
}
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
  padding: 4px 6px;
}
</style>
ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-search.vue
@@ -34,51 +34,78 @@
const dateRange = ref<[string, string] | null>(null);
onMounted(() => {
  if (model.value.params?.beginTime && model.value.params?.endTime) {
    dateRange.value = [model.value.params.beginTime as string, model.value.params.endTime as string];
  } else {
    dateRange.value = getDefaultRange();
function onDateRangeUpdate(value: [string, string] | null) {
    if (!model.value.params) {
      model.value.params = {};
    }
    model.value.params.beginTime = dateRange.value[0];
    model.value.params.endTime = dateRange.value[1];
  const beginTime = value?.[0] ? String(value[0]).trim() : '';
  const endTime = value?.[1] ? String(value[1]).trim() : '';
  if (beginTime && endTime) {
    model.value.params.beginTime = beginTime;
    model.value.params.endTime = endTime;
  } else {
    delete model.value.params.beginTime;
    delete model.value.params.endTime;
  }
}
function setDefaultRange() {
  const range = getDefaultRange();
  dateRange.value = range;
  onDateRangeUpdate(range);
}
onMounted(() => {
  if (!model.value.params) {
    model.value.params = {};
  }
  const beginTime = model.value.params.beginTime ? String(model.value.params.beginTime).trim() : '';
  const endTime = model.value.params.endTime ? String(model.value.params.endTime).trim() : '';
  if (beginTime && endTime) {
    dateRange.value = [beginTime, endTime];
  } else {
    setDefaultRange();
  }
});
watch(
  dateRange,
  val => {
    if (!model.value.params) {
      model.value.params = {};
    }
    if (val && val[0] && val[1]) {
      model.value.params.beginTime = val[0];
      model.value.params.endTime = val[1];
    } else {
      model.value.params.beginTime = null;
      model.value.params.endTime = null;
    }
  },
  { deep: true }
);
async function reset() {
  await restoreValidation();
  resetModel();
  dateRange.value = getDefaultRange();
  if (!model.value.params) {
    model.value.params = {};
  }
  model.value.params.beginTime = dateRange.value[0];
  model.value.params.endTime = dateRange.value[1];
  setDefaultRange();
  emit('search');
}
async function search() {
  await validate();
  emit('search');
}
const siloOptions = Array.from({ length: 12 }, (_, i) => ({
  label: `${i + 1}号储丝柜`,
  value: String(i + 1)
}));
const selectedSilos = ref<string[]>([]);
watch(
  () => model.value.siloid,
  val => {
    if (val) {
      selectedSilos.value = (val as string).split(',');
    } else {
      selectedSilos.value = [];
    }
  },
  { immediate: true }
);
function handleSiloChange(val: string[]) {
  selectedSilos.value = val;
  model.value.siloid = val.join(',');
}
</script>
@@ -95,10 +122,10 @@
              path="materialname"
              class="pr-24px"
            >
              <NInput v-model:value="model.materialname" placeholder="请输入牌号" />
              <NInput v-model:value="model.materialname" clearable placeholder="请输入牌号" />
            </NFormItemGi>
            <NFormItemGi span="24 s:12 m:8 l:8 xl:8" label="批次号" label-width="auto" path="batchcode" class="pr-24px">
              <NInput v-model:value="model.batchcode" placeholder="请输入批次号" />
              <NInput v-model:value="model.batchcode" clearable placeholder="请输入批次号" />
            </NFormItemGi>
            <NFormItemGi
              span="24 s:12 m:8 l:8 xl:8"
@@ -109,9 +136,10 @@
            >
              <NDatePicker
                v-model:formatted-value="model.actualstarttime"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                type="date"
                value-format="yyyy-MM-dd"
                clearable
                class="w-full"
              />
            </NFormItemGi>
            <NFormItemGi
@@ -127,6 +155,8 @@
                :default-time="['00:00:00', '23:59:59']"
                value-format="yyyy-MM-dd HH:mm:ss"
                clearable
                class="w-full"
                @update:formatted-value="onDateRangeUpdate"
              />
            </NFormItemGi>
            <NFormItemGi
@@ -136,7 +166,14 @@
              path="siloid"
              class="pr-24px"
            >
              <NInput v-model:value="model.siloid" placeholder="请输入柜子号(末位)" />
              <NSelect
                v-model:value="selectedSilos"
                multiple
                clearable
                :options="siloOptions"
                placeholder="请选择柜子号"
                @update:value="handleSiloChange"
              />
            </NFormItemGi>
            <NFormItemGi :show-feedback="false" span="24" class="pr-24px">
              <NSpace class="w-full" justify="end">