4ec5774fa25119744bf534266d6a09df33cb8fc6..41d885b2b3f731650328813da002be9b050d5805
2026-01-30 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
41d885 对比 | 目录
2026-01-30 spring
fix: 生成货架时,输入100行,100列,页面生成会卡死(建议货架生成时输入数量限制小一些,以及不能输入负数、小数这些,只能正整数)
2b3eb9 对比 | 目录
2026-01-30 zhangwencui
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
7f53e6 对比 | 目录
2026-01-30 zhangwencui
安全生产样式调整
e2235a 对比 | 目录
2026-01-30 huminmin
生产工单列表增加工单类型
ad2e4b 对比 | 目录
2026-01-30 huminmin
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
b0c3db 对比 | 目录
2026-01-30 huminmin
生产工单列表增加工单类型
e3758b 对比 | 目录
2026-01-30 spring
fix: 查看计量器具台账有计量器具名称,新增时没有对应的输入框
16072e 对比 | 目录
2026-01-30 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
8ac303 对比 | 目录
2026-01-30 gaoluyang
进销存升级 1.备件管理未做分页
fd7dbd 对比 | 目录
2026-01-30 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
39724c 对比 | 目录
2026-01-30 spring
fix: 大屏初始化样式自适应问题
a3828e 对比 | 目录
2026-01-30 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
a275d7 对比 | 目录
2026-01-30 gaoluyang
进销存升级 1.部分输入框(有效日期、检定周期)输入做下限制(大于0的整数数字)
d5e65b 对比 | 目录
2026-01-30 zhangwencui
安全培训考核搜索条件修改
f44afe 对比 | 目录
2026-01-30 zhangwencui
安全培训考核模块
0714f4 对比 | 目录
2026-01-30 spring
fix: 原材料、过程、出厂检验分配检验员后,其他用户编辑、提交按钮需置灰。只有分配的检验账户登录后才能编辑、提交操作。
3576bc 对比 | 目录
2026-01-30 spring
fix: 生产核算样式调整
4d787e 对比 | 目录
2026-01-30 spring
fix: 预计运行时间编辑后还是回显原来的时间
e2db88 对比 | 目录
2026-01-30 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
28d2e9 对比 | 目录
2026-01-30 spring
fix: 绑定关系中操作栏点击删除按钮,删除不了(未调接口)
7ebe2e 对比 | 目录
2026-01-30 huminmin
生成核算增加日期搜索
2cd733 对比 | 目录
2026-01-30 huminmin
生成核算增加日期搜索
e05905 对比 | 目录
2026-01-30 huminmin
生成核算增加日期搜索
3dadba 对比 | 目录
2026-01-30 huminmin
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
b46d3f 对比 | 目录
2026-01-30 huminmin
生成核算增加日期搜索
21ca75 对比 | 目录
2026-01-30 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
728d7c 对比 | 目录
2026-01-30 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
2222e2 对比 | 目录
2026-01-30 spring
fix: 完成生产数据分析页面
8a3bfd 对比 | 目录
2026-01-30 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
febea6 对比 | 目录
2026-01-30 gaoluyang
进销存升级 1.会计核算页面样式优化
582354 对比 | 目录
2026-01-30 huminmin
生成核算增加日期搜索
6de8af 对比 | 目录
2026-01-30 zss
不合格管理的编辑按钮去掉
11934f 对比 | 目录
已添加19个文件
已修改27个文件
4583 ■■■■■ 文件已修改
src/api/fileManagement/bookshelf.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/safeProduction/safetyTrainingAssessment.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/png/blue@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/png/circlePink@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/png/green@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/png/pink@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/png/yellow@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Echarts/echarts.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useChartBackground.js 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/formDia.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/bookshelf/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/accounting/index.vue 511 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/metricBinding/index.vue 76 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/metricMaintenance/index.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue 351 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/index.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/accidentReportingRecord/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/dangerInvestigation/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/emergencyPlanReview/index.vue 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/detail.vue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/index.vue 435 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fileManagement/bookshelf.js
@@ -102,14 +102,15 @@
/**
 * åˆ é™¤è´§æž¶
 * @description æ ¹æ®è´§æž¶ID删除指定的货架记录
 * @param {string|number} id è´§æž¶ID
 * @description æ ¹æ®è´§æž¶ID删除指定的货架记录,后端要求传入 ID æ•°ç»„(支持批量)
 * @param {Array<string|number>} data è´§æž¶ID数组
 * @returns {Promise} è¿”回删除结果
 */
export function deleteShelf(id) {
export function deleteShelf(data) {
  return request({
    url: `/warehouse/goodsShelves/delete/${id}`,
    url: `/warehouse/goodsShelves/delete/`,
    method: "delete",
    data,
  });
}
src/api/safeProduction/safetyTrainingAssessment.js
@@ -67,4 +67,46 @@
        method: 'delete',
        data: ids
    })
}
}
// ç­¾åˆ°
export function safeTrainingSign(query) {
    return request({
        url: '/safeTraining/sign',
        method: 'post',
        data: query
    })
}
// æŸ¥è¯¢è¯¦æƒ…
export function safeTrainingGet(query) {
    return request({
        url: '/safeTraining/getSafeTraining',
        method: 'get',
        params: query
    })
}
// æäº¤
export function safeTrainingSave(query) {
    return request({
        url: '/safeTraining/saveSafeTraining',
        method: 'post',
        data: query
    })
}
export function safeTrainingDetailListPage(query) {
  return request({
    url: "/safeTrainingDetails/page",
    method: "get",
    params: query,
  });
}
// å¯¼å‡º
export function safeTrainingDetailExport(query) {
    return request({
        url: '/safeTrainingDetails/export',
        method: 'post',
        data: query,
        responseType: 'blob'
    })
}
src/assets/icons/png/blue@2x.png
src/assets/icons/png/circlePink@2x.png
src/assets/icons/png/green@2x.png
src/assets/icons/png/pink@2x.png
src/assets/icons/png/yellow@2x.png
src/components/Echarts/echarts.vue
@@ -6,8 +6,10 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watchEffect } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
const emit = defineEmits(['finished'])
// Props
const props = defineProps({
@@ -91,6 +93,47 @@
// Refs
const chartRef = ref(null)
let chartInstance = null
let finishedHandler = null
let initTimer = null
let initAttempts = 0
function clearInitTimer() {
  if (initTimer) {
    clearTimeout(initTimer)
    initTimer = null
  }
}
function isContainerReady() {
  const el = chartRef.value
  if (!el) return false
  // offsetWidth/offsetHeight æ›´è´´è¿‘真实布局(为 0 å¾€å¾€ä»£è¡¨è¿˜æ²¡å¸ƒå±€/不可见)
  return el.offsetWidth > 0 && el.offsetHeight > 0
}
function initChartWhenReady() {
  clearInitTimer()
  initAttempts += 1
  if (!isContainerReady()) {
    // ç­‰å®¹å™¨çœŸæ­£æœ‰å°ºå¯¸ï¼ˆé¿å…é¦–屏初始化偏移/空白,热更新后才正常的情况)
    // æœ€å¤šé‡è¯•约 3 ç§’,避免无限循环
    if (initAttempts < 60) {
      initTimer = setTimeout(initChartWhenReady, 50)
    }
    return
  }
  if (chartInstance) return
  chartInstance = echarts.init(chartRef.value)
  finishedHandler = () => emit('finished')
  chartInstance.on('finished', finishedHandler)
  renderChart()
  // setOption åŽè¡¥ä¸€æ¬¡ resize,确保首屏尺寸正确
  nextTick(() => {
    if (chartInstance) chartInstance.resize()
  })
}
// Methods
function generateChart(option) {
@@ -139,26 +182,38 @@
// Lifecycle hooks
onMounted(() => {
  chartInstance = echarts.init(chartRef.value)
  renderChart()
  initAttempts = 0
  initChartWhenReady()
  window.addEventListener('resize', windowResizeListener)
})
onBeforeUnmount(() => {
  if (chartInstance) {
    window.removeEventListener('resize', windowResizeListener)
    if (finishedHandler) {
      chartInstance.off('finished', finishedHandler)
      finishedHandler = null
    }
    chartInstance.dispose()
    chartInstance = null
  }
  clearInitTimer()
})
// Watch all reactive props that affect the chart
watch(
    () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap],
    () => {
      if (chartInstance) {
        renderChart()
      // å¦‚果首屏还没初始化成功,等待容器 ready åŽå†æ¸²æŸ“
      if (!chartInstance) {
        initChartWhenReady()
        return
      }
      renderChart()
      // æ•°æ®å˜åŒ–后补一次 resize,避免布局变化导致的偏移
      nextTick(() => {
        if (chartInstance) chartInstance.resize()
      })
    },
    { deep: true, immediate: true }
)
src/hooks/useChartBackground.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
/**
 * å›¾è¡¨èƒŒæ™¯ä½ç½®è°ƒæ•´ composable
 * @param {Object} options é…ç½®é€‰é¡¹
 * @param {Ref} options.wrapperRef - å›¾è¡¨å®¹å™¨çš„ ref
 * @param {Ref} options.backgroundRef - èƒŒæ™¯å…ƒç´ çš„ ref
 * @param {String} options.left - èƒŒæ™¯ left ä½ç½®ï¼Œå¦‚ '25%' æˆ– '50%',默认 '50%'
 * @param {String} options.top - èƒŒæ™¯ top ä½ç½®ï¼Œå¦‚ '50%',默认 '50%'
 * @param {String} options.offsetX - X è½´åç§»å€¼ï¼Œå¦‚ '-51.5%' æˆ– '-50%',默认 '-50%'
 * @param {String} options.offsetY - Y è½´åç§»å€¼ï¼Œå¦‚ '-39%' æˆ– '-50%',默认 '-50%'
 * @param {Ref} options.watchData - å¯é€‰ï¼Œç›‘听的数据变化,数据变化时重新调整位置
 * @returns {Function} adjustBackgroundPosition - æ‰‹åŠ¨è°ƒæ•´èƒŒæ™¯ä½ç½®çš„æ–¹æ³•
 */
export function useChartBackground(options = {}) {
  const {
    wrapperRef,
    backgroundRef,
    left = '50%',
    top = '50%',
    offsetX = '-50%',
    offsetY = '-50%',
    watchData = null
  } = options
  let resizeObserver = null
  let intersectionObserver = null
  let retryTimers = []
  const clearRetryTimers = () => {
    if (!retryTimers.length) return
    retryTimers.forEach((t) => clearTimeout(t))
    retryTimers = []
  }
  // è°ƒæ•´èƒŒæ™¯ä½ç½®
  const adjustBackgroundPosition = () => {
    nextTick(() => {
      if (!wrapperRef?.value || !backgroundRef?.value) {
        return
      }
      // åˆå§‹åŒ–阶段经常出现:容器尚未可见/尺寸为 0(非全屏、tab、动画等)
      // è¿™ç§æƒ…况下先不对齐,等 ResizeObserver / IntersectionObserver å†è§¦å‘
      const rect = wrapperRef.value.getBoundingClientRect()
      if (!rect.width || !rect.height) return
      const background = backgroundRef.value
      // ä½¿ç”¨ç™¾åˆ†æ¯”定位 + transform å¾®è°ƒï¼ˆè¿™æ˜¯æœ€å¯é çš„æ–¹å¼ï¼‰
      background.style.left = left
      background.style.top = top
      background.style.transform = `translate(${offsetX}, ${offsetY})`
    })
  }
  // åˆå§‹åŒ–阶段多次“补偿对齐”,覆盖 Echarts é¦–次渲染/动画造成的延迟布局
  const scheduleKickAlign = () => {
    clearRetryTimers()
    ;[0, 60, 180, 360, 800].forEach((ms) => {
      retryTimers.push(
        setTimeout(() => {
          adjustBackgroundPosition()
        }, ms)
      )
    })
  }
  // çª—口 resize å¤„理
  const resizeHandler = () => {
    adjustBackgroundPosition()
  }
  // å¦‚果提供了 watchData,监听数据变化(需要在 setup é˜¶æ®µåˆ›å»ºï¼‰
  if (watchData) {
    watch(watchData, () => {
      adjustBackgroundPosition()
    }, { deep: true })
  }
  // åˆå§‹åŒ–
  const init = () => {
    // ç›‘听窗口 resize
    window.addEventListener('resize', resizeHandler)
    // ä½¿ç”¨ ResizeObserver ç›‘听容器尺寸变化
    nextTick(() => {
      if (wrapperRef?.value && window.ResizeObserver) {
        resizeObserver = new ResizeObserver(() => {
          adjustBackgroundPosition()
        })
        resizeObserver.observe(wrapperRef.value)
      }
      // ç›‘听“从不可见到可见”,解决初始化时未对齐但热更新又正常的问题
      if (wrapperRef?.value && window.IntersectionObserver) {
        intersectionObserver = new IntersectionObserver(
          (entries) => {
            const entry = entries?.[0]
            if (entry?.isIntersecting) {
              scheduleKickAlign()
            }
          },
          { threshold: 0.01 }
        )
        intersectionObserver.observe(wrapperRef.value)
      }
      // åˆå§‹åŒ–多次补偿对齐,确保图表渲染完成
      scheduleKickAlign()
    })
  }
  // æ¸…理
  const cleanup = () => {
    window.removeEventListener('resize', resizeHandler)
    clearRetryTimers()
    if (resizeObserver) {
      resizeObserver.disconnect()
      resizeObserver = null
    }
    if (intersectionObserver) {
      intersectionObserver.disconnect()
      intersectionObserver = null
    }
  }
  return {
    adjustBackgroundPosition,
    init,
    cleanup
  }
}
src/views/equipmentManagement/ledger/Form.vue
@@ -254,6 +254,12 @@
    form.taxRate = data.taxRate;
    form.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal;
    form.createTime = data.createTime;
    // é¢„计运行时间:后端返回后转为 YYYY-MM-DD ä»¥ä¾¿æ—¥æœŸé€‰æ‹©å™¨æ­£ç¡®å±•示
    if (data.planRuntimeTime) {
      form.planRuntimeTime = dayjs(data.planRuntimeTime).format('YYYY-MM-DD');
    } else {
      form.planRuntimeTime = undefined;
    }
  }
};
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -51,11 +51,14 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="有效期:" prop="valid">
                        <el-form-item label="有效日期(天):" prop="valid">
                            <el-input
                                v-model="form.valid"
                                placeholder="请输入"
                                type="number"
                                placeholder="请输入有效期天数"
                                clearable
                                :min="1"
                                @input="handleValidInput"
                            >
                                <template #append>日</template>
                            </el-input>
@@ -152,7 +155,32 @@
    rules: {
        code: [{required: true, message: "请输入", trigger: "blur"}],
        name: [{required: true, message: "请输入", trigger: "blur"}],
        valid: [{required: true, message: "请输入", trigger: "blur"}],
        valid: [
            {required: true, message: "请输入", trigger: "blur"},
            {
                validator: (rule, value, callback) => {
                    if (value === '' || value === null || value === undefined) {
                        callback();
                        return;
                    }
                    const numValue = Number(value);
                    if (isNaN(numValue)) {
                        callback(new Error('请输入有效的数字'));
                        return;
                    }
                    if (numValue <= 0) {
                        callback(new Error('只能输入正数'));
                        return;
                    }
                    if (!Number.isInteger(numValue)) {
                        callback(new Error('请输入整数'));
                        return;
                    }
                    callback();
                },
                trigger: 'blur'
            }
        ],
        recordDate: [{required: true, message: "请选择", trigger: "change"}],
        userId: [{required: true, message: "请选择", trigger: "change"}],
        entryDate: [{required: true, message: "请选择", trigger: "change"}],
@@ -233,6 +261,27 @@
    }
}
// å¤„理有效日期输入,只允许正整数
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
        form.value.valid = '';
        return;
    }
    // è½¬æ¢ä¸ºå­—符串并移除所有非数字字符(包括负号、小数点等)
    const numStr = String(value).replace(/[^0-9]/g, '');
    if (numStr === '') {
        form.value.valid = '';
        return;
    }
    const numValue = parseInt(numStr, 10);
    // ç¡®ä¿æ˜¯æ­£æ•´æ•°ï¼ˆå¤§äºŽ0)
    if (numValue > 0 && !isNaN(numValue)) {
        form.value.valid = numValue;
    } else {
        form.value.valid = '';
    }
}
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
src/views/equipmentManagement/measurementEquipment/components/formDia.vue
@@ -15,11 +15,20 @@
                ref="formRef"
            >
                <el-row :gutter="30">
                    <el-col :span="24">
                    <el-col :span="12">
                        <el-form-item label="出厂编号:" prop="code">
                            <el-input
                                v-model="form.code"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="计量器具名称:" prop="name">
                            <el-input
                                v-model="form.name"
                                placeholder="请输入计量器具名称"
                                clearable
                            />
                        </el-form-item>
@@ -74,8 +83,11 @@
            <el-form-item label="有效日期(天):" prop="valid">
              <el-input
                  v-model="form.valid"
                  type="number"
                  placeholder="请输入有效期天数"
                  clearable
                  :min="1"
                  @input="handleValidInput"
              >
              <template #append>日</template>
              </el-input>
@@ -171,6 +183,7 @@
const data = reactive({
    form: {
        code: "",
    name: "",
    instationLocation: "",
    mostDate:"",
        model: "",
@@ -184,6 +197,7 @@
    },
    rules: {
        code: [{required: true, message: "请输入", trigger: "blur"}],
    name: [{ required: true, message: "请输入", trigger: "blur" }],
        model: [{required: true, message: "请输入", trigger: "blur"}],
        validDate: [{required: true, message: "请输入", trigger: "blur"}],
        nextDate: [{required: true, message: "请选择", trigger: "change"}],
@@ -192,7 +206,32 @@
    instationLocation: [{required: true, message: "请输入", trigger: "blur"}],
    mostDate: [{required: true, message: "请选择", trigger: "change"}],
    cycle: [{required: true, message: "请选择", trigger: "blur"}],
    valid: [{required: true, message: "请输入", trigger: "blur"}],
    valid: [
      {required: true, message: "请输入", trigger: "blur"},
      {
        validator: (rule, value, callback) => {
          if (value === '' || value === null || value === undefined) {
            callback();
            return;
          }
          const numValue = Number(value);
          if (isNaN(numValue)) {
            callback(new Error('请输入有效的数字'));
            return;
          }
          if (numValue <= 0) {
            callback(new Error('只能输入正数'));
            return;
          }
          if (!Number.isInteger(numValue)) {
            callback(new Error('请输入整数'));
            return;
          }
          callback();
        },
        trigger: 'blur'
      }
    ],
    unit: [{required: true, message: "请输入", trigger: "blur"}],
    }
})
@@ -254,6 +293,27 @@
    }
}
// å¤„理有效日期输入,只允许正整数
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
        form.value.valid = '';
        return;
    }
    // è½¬æ¢ä¸ºå­—符串并移除所有非数字字符(包括负号、小数点等)
    const numStr = String(value).replace(/[^0-9]/g, '');
    if (numStr === '') {
        form.value.valid = '';
        return;
    }
    const numValue = parseInt(numStr, 10);
    // ç¡®ä¿æ˜¯æ­£æ•´æ•°ï¼ˆå¤§äºŽ0)
    if (numValue > 0 && !isNaN(numValue)) {
        form.value.valid = numValue;
    } else {
        form.value.valid = '';
    }
}
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -82,6 +82,12 @@
    minWidth:150,
    align:"center"
    },
  {
    label: "计量器具名称",
    prop: "name",
    width: '160px',
    align: "center",
  },
    {
        label: "安装位置",
        prop: "instationLocation",
src/views/equipmentManagement/spareParts/index.vue
@@ -38,7 +38,7 @@
        </el-table-column>
        <el-table-column prop="price" label="ä»·æ ¼" width="140"></el-table-column>
        <el-table-column prop="quantity" label="数量" width="140"></el-table-column>
        <el-table-column prop="description" label="描述" width="150"></el-table-column>
        <el-table-column prop="description" label="描述"></el-table-column>
        <el-table-column label="操作" width="150" fixed="right" align="center">
          <template #default="{ row }">
            <el-button
@@ -60,6 +60,18 @@
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="pagination.current"
          v-model:page-size="pagination.size"
          :page-sizes="[10, 20, 50, 100]"
          :total="pagination.total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </div>
    <el-dialog title="分类管理" v-model="dialogVisible" width="60%">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
@@ -147,6 +159,12 @@
const queryParams = reactive({
  name: ''
});
// åˆ†é¡µå‚æ•°
const pagination = reactive({
  current: 1,
  size: 10,
  total: 0
});
// è¡¨å•数据
const form = reactive({
  id:'',
@@ -215,7 +233,10 @@
const fetchListData = async () => {
  loading.value = true;
  try {
    const params = {};
    const params = {
      current: pagination.current,
      size: pagination.size
    };
    if (queryParams.name) {
      params.name = queryParams.name;
    }
@@ -223,6 +244,7 @@
    if (res.code === 200) {
      renderTableData.value = res.data.records || [];
      categories.value = res.data.records || [];
      pagination.total = res.data.total || 0;
    }
  } catch (error) {
        loading.value = false;
@@ -233,12 +255,27 @@
// æŸ¥è¯¢
const handleQuery = () => {
  pagination.current = 1;
  fetchListData();
}
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  queryParams.name = '';
  pagination.current = 1;
  fetchListData();
}
// åˆ†é¡µå¤§å°æ”¹å˜
const handleSizeChange = (size) => {
  pagination.size = size;
  pagination.current = 1;
  fetchListData();
}
// å½“前页改变
const handleCurrentChange = (current) => {
  pagination.current = current;
  fetchListData();
}
@@ -373,6 +410,13 @@
  margin-top: unset;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
  padding: 16px 0;
}
.el-table__header-wrapper th {
  background-color: #f5f7fa;
  font-weight: 600;
src/views/fileManagement/bookshelf/index.vue
@@ -135,7 +135,7 @@
        <el-col class="search_thing" :span="24">
          <div class="search_label"><span class="required-span">* </span>货架层数:</div>
          <div class="search_input">
            <el-input v-model="shelves.row" size="small"></el-input>
            <el-input-number v-model="shelves.row" size="small" :min="1" :max="10" :precision="0" :step="1" controls-position="right" style="width: 100%"></el-input-number>
          </div>
        </el-col>
      </el-row>
@@ -143,7 +143,7 @@
        <el-col class="search_thing" :span="24">
          <div class="search_label"><span class="required-span">* </span>货架列数:</div>
          <div class="search_input">
            <el-input v-model="shelves.col" size="small"></el-input>
            <el-input-number v-model="shelves.col" size="small" :min="1" :max="10" :precision="0" :step="1" controls-position="right" style="width: 100%"></el-input-number>
          </div>
        </el-col>
      </el-row>
@@ -287,6 +287,16 @@
    ElMessage.error('请填写货架列数')
    return
  }
  const rowNum = Number(shelves.row)
  const colNum = Number(shelves.col)
  if (rowNum < 1 || colNum < 1 || rowNum > 10 || colNum > 10) {
    ElMessage.error('货架层数和列数需为1-10的整数')
    return
  }
  if (!Number.isInteger(rowNum) || !Number.isInteger(colNum)) {
    ElMessage.error('货架层数和列数不能为小数')
    return
  }
  upLoadShelves.value = true
  
  if (currentEdit.value && currentEdit.value.id) {
@@ -294,8 +304,8 @@
    updateShelf({
      id: currentEdit.value.id,
      name: shelves.name,
      row: Number(shelves.row),
      col: Number(shelves.col),
      row: rowNum,
      col: colNum,
      warehouseId: entity.warehouseId
    }).then(res => {
      upLoadShelves.value = false
@@ -311,11 +321,10 @@
    
  } else {
    // æ–°å¢ž
    // è¿™é‡Œéœ€è¦æ›¿æ¢ä¸ºå®žé™…çš„API调用
      addShelf({
    addShelf({
      name: shelves.name,
      row: Number(shelves.row),
      col: Number(shelves.col),
      row: rowNum,
      col: colNum,
      warehouseId: entity.warehouseId
    }).then(res => {
      upLoadShelves.value = false
@@ -341,16 +350,14 @@
    type: "warning"
  }).then(() => {
    if (level == 1) {
      // åˆ é™¤ä»“库
      // åˆ é™¤ä»“库(接口要求传 ID æ•°ç»„)
      deleteWarehouse([row.id]).then(res => {
        ElMessage.success('删除成功')
        selectList()
      })
    } else {
      // åˆ é™¤è´§æž¶
      deleteShelf({
        id: row.id
      }).then(res => {
      // åˆ é™¤è´§æž¶ï¼ˆæŽ¥å£åŒæ ·è¦æ±‚ä¼  ID æ•°ç»„)
      deleteShelf([row.id]).then(res => {
        ElMessage.success('删除成功')
        selectList()
      })
src/views/financialManagement/accounting/index.vue
@@ -1,7 +1,7 @@
<template>
  <div style="padding: 20px;">
    <!-- é¡µé¢æ ‡é¢˜å’Œç­›é€‰æ¡ä»¶ -->
    <div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;">
    <div class="w-full md:w-auto flex items-center gap-3">
      <el-form :inline="true">
        <el-form-item label="年份">
          <el-date-picker
@@ -31,94 +31,133 @@
    <main class="container mx-auto px-4 pb-10">
      <!-- å›ºå®šèµ„产指标卡片 -->
      <div class="grid-container">
      <div class="kpi-grid">
        <!-- è®¾å¤‡æ€»æ•° -->
        <el-card class="bg2">
          <p>设备总数</p>
          <h3>
            {{ assetInfo.totalEquipment }}
          </h3>
        </el-card>
        <div class="kpi-card">
          <div class="kpi-left">
            <span class="kpi-dot kpi-dot-blue"></span>
            <span class="kpi-title">设备总数</span>
            <div class="kpi-value">{{ assetInfo.totalEquipment }}个</div>
          </div>
          <div class="kpi-icon-wrap kpi-icon-blue">
            <img :src="iconBlue" alt="" class="kpi-icon" />
          </div>
        </div>
        <!-- èµ„产原值 -->
        <el-card class="bg3">
          <p>资产原值</p>
          <h3>
            Â¥{{ formatCurrency(assetInfo.totalOriginalValue) }}
          </h3>
        </el-card>
        <div class="kpi-card">
          <div class="kpi-left">
            <span class="kpi-dot kpi-dot-orange"></span>
            <span class="kpi-title">资产原值</span>
            <div class="kpi-value">Â¥{{ formatCurrency(assetInfo.totalOriginalValue) }}</div>
          </div>
          <div class="kpi-icon-wrap kpi-icon-orange">
            <img :src="iconWalletOrange" alt="" class="kpi-icon" />
          </div>
        </div>
        <!-- ç´¯è®¡æŠ˜æ—§ -->
        <el-card class="bg4">
          <p>累计折旧</p>
          <h3>
            Â¥{{ formatCurrency(assetInfo.totalDepreciation) }}
          </h3>
        </el-card>
        <div class="kpi-card">
          <div class="kpi-left">
            <span class="kpi-dot kpi-dot-green"></span>
            <span class="kpi-title">累计折旧</span>
            <div class="kpi-value">Â¥{{ formatCurrency(assetInfo.totalDepreciation) }}</div>
          </div>
          <div class="kpi-icon-wrap kpi-icon-green">
            <img :src="iconGreen" alt="" class="kpi-icon" />
          </div>
        </div>
        <!-- åº“存资产 -->
        <div class="kpi-card">
          <div class="kpi-left">
            <span class="kpi-dot kpi-dot-pink"></span>
            <span class="kpi-title">库存资产</span>
            <div class="kpi-value">Â¥{{ formatCurrency(assetInfo.inventoryValue) }}</div>
          </div>
          <div class="kpi-icon-wrap kpi-icon-pink">
            <img :src="iconPink" alt="" class="kpi-icon" />
          </div>
        </div>
        <!-- å‡€å€¼ -->
        <el-card class="bg5">
          <p>净值</p>
          <h3>
            Â¥{{ formatCurrency(assetInfo.totalNetValue) }}
          </h3>
        </el-card>
        <div class="kpi-card">
          <div class="kpi-left">
            <span class="kpi-dot kpi-dot-yellow"></span>
            <span class="kpi-title">净值</span>
            <div class="kpi-value">Â¥{{ formatCurrency(assetInfo.totalNetValue) }}</div>
          </div>
          <div class="kpi-icon-wrap kpi-icon-yellow">
            <img :src="iconYellow" alt="" class="kpi-icon" />
          </div>
        </div>
        <!-- è´Ÿå€º -->
        <el-card class="bg2">
          <p>负债</p>
          <h3>
            Â¥{{ formatCurrency(assetInfo.debt) }}
          </h3>
        </el-card>
        <!-- åº“存资产 -->
        <el-card class="bg3">
          <p>库存资产</p>
          <h3>
            Â¥{{ formatCurrency(assetInfo.inventoryValue) }}
          </h3>
        </el-card>
        <div class="kpi-card">
          <div class="kpi-left">
            <span class="kpi-dot kpi-dot-red"></span>
            <span class="kpi-title">负债</span>
            <div class="kpi-value">Â¥{{ formatCurrency(assetInfo.debt) }}</div>
          </div>
          <div class="kpi-icon-wrap kpi-icon-red">
            <img :src="iconWalletRed" alt="" class="kpi-icon" />
          </div>
        </div>
      </div>
      <!-- å›ºå®šèµ„产统计图表 -->
      <div class="grid-layout">
        <!-- æŒ‰è®¾å¤‡ç±»åž‹ç»Ÿè®¡ -->
        <el-card style="margin-bottom: 20px;">
      <div class="chart-row">
        <!-- è®¾å¤‡ç±»åž‹åˆ†å¸ƒ -->
        <el-card class="chart-card">
          <h2 class="section-title">设备类型分布</h2>
          <div class="echarts">
            <Echarts
          <div class="chart-content">
            <div class="pie-wrap">
              <Echarts
                :legend="typeDistributionLegend"
                :chartStyle="chartStylePie"
                :series="typeDistributionSeries"
                :tooltip="pieTooltip"
                style="height: 260px; width: 35%;">
              <div class="chart-num">
                <span style="font-size: 22px;">设备类型</span>
                <span style="font-size: 36px; font-weight: 500; font-family: 'MyCustomFont', sans-serif;">{{ deviceTypeTotalCount }}</span>
                style="height: 260px; width: 100%;"
              />
            </div>
            <div class="type-cards">
              <div class="type-card" v-for="(item, index) in typeDistributionData" :key="index">
                <span class="type-name">{{ item.name }}</span>
                <span class="type-count">{{ item.count }}</span>
              </div>
            </Echarts>
            </div>
          </div>
        </el-card>
        <!-- è®¾å¤‡é‡‘额分析 -->
        <el-card class="chart-card">
          <h2 class="section-title">设备金额分析</h2>
          <div class="bar-chart-wrap">
            <Echarts
                ref="chart"
                :chartStyle="chartStyle"
                :grid="grid"
                :legend="lineLegend"
                :series="typeDistributionLineSeries"
                :tooltip="tooltip"
                :xAxis="xAxis"
                :yAxis="yAxis"
                style="height: 260px; width: 64%;"></Echarts>
              ref="barChart"
              :chartStyle="chartStyle"
              :grid="grid"
              :legend="lineLegend"
              :series="typeDistributionBarSeries"
              :tooltip="tooltip"
              :xAxis="xAxis"
              :yAxis="yAxisBar"
              style="height: 260px; width: 100%;"
            />
          </div>
        </el-card>
      </div>
      <!-- è®¾å¤‡å°è´¦è¡¨æ ¼ -->
      <el-card style="margin-bottom: 20px;">
      <!-- è®¾å¤‡æ•°æ®è¡¨ -->
      <el-card class="table-card">
        <h2 class="section-title">设备数据表</h2>
        <el-table
          :data="equipmentList"
          stripe
          style="width: 100%"
          :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
        >
          <el-table-column type="index" label="序号" width="60" :index="(index) => (pagination.currentPage - 1) * pagination.pageSize + index + 1" />
          <el-table-column prop="deviceName" label="设备名称" width="250" />
          <el-table-column prop="deviceModel" label="型号规格" min-width="150" />
          <el-table-column prop="deviceModel" label="规格型号" min-width="150" />
          <el-table-column prop="supplierName" label="供应商" min-width="120" />
          <el-table-column prop="unit" label="单位" width="120" />
          <el-table-column prop="number" label="数量" width="120" />
@@ -161,9 +200,14 @@
import { ref, computed, onMounted, reactive } from 'vue';
import 'element-plus/dist/index.css';
import Echarts from "@/components/Echarts/echarts.vue";
import { getLedgerPage } from "@/api/equipmentManagement/ledger";
import { getAccountingTotal, getDeviceTypeDistribution, getCalculateDepreciation } from "@/api/financialManagement/accounting";
import dayjs from "dayjs";
import iconBlue from '@/assets/icons/png/blue@2x.png';
import iconWalletOrange from '@/assets/icons/png/walletOrange@2x.png';
import iconGreen from '@/assets/icons/png/green@2x.png';
import iconPink from '@/assets/icons/png/pink@2x.png';
import iconYellow from '@/assets/icons/png/yellow@2x.png';
import iconWalletRed from '@/assets/icons/png/walletRed@2x.png';
// ç­›é€‰æ¡ä»¶
const dateRange = ref(null);
@@ -258,30 +302,16 @@
  height: '100%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
};
const pieColors = ['#F04864', '#FACC14', '#8543E0', '#1890FF', '#13C2C2', '#2FC25B']; // å¯æ ¹æ®å®žé™…调整
const pieColors = ['#165DFF', '#14C9C9', '#8543E0', '#1890FF', '#13C2C2', '#2FC25B']; // å¯æ ¹æ®å®žé™…调整
// é¥¼å›¾æ•°æ®
const typeDistributionData = ref([]);
const departmentDistributionData = ref([]);
// é¥¼å›¾å›¾ä¾‹
// é¥¼å›¾å›¾ä¾‹ï¼ˆæ‚¬åœæ˜¾ç¤ºåç§°+占比,图例放下方卡片展示)
const typeDistributionLegend = computed(() => ({
  show: true,
  top: 'center',
  left: '60%',
  orient: 'vertical',
  icon: 'circle',
  data: typeDistributionData.value.map(item => item.name),
  formatter: function(name) {
    const item = typeDistributionData.value.find(i => i.name === name);
    if (!item) return name;
    return `${name} | ${item.count} å° | ${item.amount}`;
  },
  textStyle: {
    color: '#333',
    fontSize: 14,
    lineHeight: 26,
  }
  show: false,
  data: typeDistributionData.value.map(item => item.name)
}));
@@ -289,16 +319,14 @@
const typeDistributionSeries = computed(() => [
  {
    type: 'pie',
    radius: ['50%', '65%'],
    center: ['25%', '50%'],
    radius: ['0%', '65%'],
    center: ['50%', '45%'],
    avoidLabelOverlap: false,
    itemStyle: {
      borderColor: '#fff',
      borderWidth: 2
    },
    label: {
      show: false
    },
    label: { show: false },
    data: typeDistributionData.value,
    color: pieColors
  }
@@ -306,23 +334,35 @@
// æŠ˜çº¿å›¾æ•°æ®
const typeDistributionLineSeries = ref([]);
// æŸ±çŠ¶å›¾æ•°æ®ï¼ˆè®¾å¤‡é‡‘é¢åˆ†æžï¼‰
const typeDistributionBarSeries = computed(() => [
  {
    name: '销售额',
    type: 'bar',
    data: typeDistributionData.value.map(item => (item.amountNum != null ? item.amountNum : 0)),
    itemStyle: { color: '#13C2C2' }
  }
]);
// æŸ±çж图 Y è½´
const yAxisBar = [
  {
    type: 'value',
    name: '销售额(万元)',
    position: 'left',
    min: 0,
    nameTextStyle: { color: '#000', fontSize: 14 },
    splitLine: { lineStyle: { color: '#f0f0f0' } }
  }
];
// é¥¼å›¾æç¤ºæ¡†
// é¥¼å›¾æç¤ºæ¡†ï¼ˆå›¾å†…样式:名称 + å æ¯”)
const pieTooltip = reactive({
  trigger: 'item',
  formatter: function(params) {
    // æ£€æŸ¥æ•°æ®æ˜¯å¦å­˜åœ¨
    if (!params.data) return params.name;
    // æ‹¼æŽ¥å®Œæ•´å†…容
    return `
      <div>
        <div style="color:${params.color};font-size:16px;">●</div>
        <div>${params.name}</div>
        <div>数量:${params.data.count} å°</div>
        <div>金额:${params.data.amount}</div>
      </div>
    `;
    const pct = params.percent != null ? params.percent.toFixed(0) : 0;
    return `${params.name} ${pct}%`;
  }
});
@@ -373,7 +413,8 @@
          name: item.type || '',
          value: Number(item.count || 0),
          count: Number(item.count || 0),
          amount: `Â¥${formatCurrency(item.amount || 0)}`
          amount: `Â¥${formatCurrency(item.amount || 0)}`,
          amountNum: Number(item.amount || 0)
        }));
      } else if (data.categories && data.categories.length > 0) {
        // å¦‚果没有 details,使用 categories、countData å’Œ amountData æž„建
@@ -381,7 +422,8 @@
          name: category,
          value: Number(data.countData[index] || 0),
          count: Number(data.countData[index] || 0),
          amount: `Â¥${formatCurrency(data.amountData[index] || 0)}`
          amount: `Â¥${formatCurrency(data.amountData[index] || 0)}`,
          amountNum: Number(data.amountData[index] || 0)
        }));
      } else {
        typeDistributionData.value = [];
@@ -478,131 +520,222 @@
</script>
<style scoped lang="scss">
/* åŸºç¡€æ ·å¼è¡¥å…… */
:root {
  --el-color-primary: #4f46e5;
  --el-color-primary: #1890ff;
}
.el-card {
  position: relative;
  border-radius: 12px;
  padding: 14px 10px 10px 10px;
  box-shadow: 0 2px 8px #eee;
/* é¡µé¢èƒŒæ™¯ */
main {
  background: #f5f5f5;
  padding: 0;
  margin: 0 -20px;
  padding: 0 20px 20px;
}
/* KPI å¡ç‰‡ç½‘æ ¼ */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-bottom: 20px;
}
@media (max-width: 1024px) {
  .kpi-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (max-width: 640px) {
  .kpi-grid {
    grid-template-columns: 1fr;
  }
}
/* KPI å¡ç‰‡ - ç™½åº•、圆角、阴影 */
.kpi-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #fff;
  border-radius: 8px;
  padding: 16px 20px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  min-height: 100px;
}
.kpi-left {
  flex: 1;
  min-width: 0;
}
.kpi-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
}
.kpi-dot-blue { background: #1890ff; }
.kpi-dot-orange { background: #fa8c16; }
.kpi-dot-green { background: #52c41a; }
.kpi-dot-pink { background: #eb2f96; }
.kpi-dot-yellow { background: #facc14; }
.kpi-dot-red { background: #f5222d; }
.kpi-title {
  font-size: 14px;
  color: #333;
  vertical-align: middle;
}
.kpi-value {
  font-size: 24px;
  font-weight: 600;
  color: #333;
  margin-top: 8px;
  line-height: 1.2;
}
/* å³ä¾§å›¾æ ‡æ–¹å— */
.kpi-icon-wrap {
  width: 48px;
  height: 48px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.kpi-icon-blue { background: #e6f7ff; }
.kpi-icon-orange { background: #fff7e6; }
.kpi-icon-green { background: #f6ffed; }
.kpi-icon-pink { background: #fff0f6; }
.kpi-icon-yellow { background: #fffbe6; }
.kpi-icon-red { background: #fff1f0; }
.kpi-icon {
  width: 28px;
  height: 28px;
  object-fit: contain;
}
/* å›¾è¡¨åŒºåŸŸä¸¤åˆ— */
.chart-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-bottom: 20px;
}
@media (max-width: 1024px) {
  .chart-row {
    grid-template-columns: 1fr;
  }
}
.chart-card {
  border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  :deep(.el-card__body) {
    padding: 10px 20px !important;
  }
  &.bg1 {
    background: url(@/assets/icons/png/1.png) no-repeat 100% 100% !important;
  }
  &.bg2 {
    background: url(@/assets/icons/png/2.png) no-repeat 100% 100% !important;
  }
  &.bg3 {
    background: url(@/assets/icons/png/3.png) no-repeat 100% 100% !important;
  }
  &.bg4 {
    background: url(@/assets/icons/png/4.png) no-repeat 100% 100% !important;
  }
  &.bg5 {
    background: url(@/assets/icons/png/5.png) no-repeat 100% 100% !important;
    padding: 16px 20px;
  }
}
.grid-container {
  /* grid å®¹å™¨åŸºç¡€æ ·å¼ */
  display: grid;
  gap: 1rem; /* gap-4 å¯¹åº” 1rem (16px) */
  margin-bottom: 2rem; /* mb-8 å¯¹åº” 2rem (32px) */
  p {
    font-size: 22px;
    margin-top: 0px;
    color: #fff;
  }
  h3 {
    font-size: 36px;
    font-weight: 500;
    font-family: 'MyCustomFont', sans-serif;
    margin: 10px 0;
    color: #fff;
  }
}
/* ç§»åŠ¨ç«¯é»˜è®¤æ ·å¼ (grid-cols-1) */
.grid-container {
  grid-template-columns: repeat(1, minmax(0, 1fr));
}
/* å°å±å¹•及以上 (sm:grid-cols-2) */
@media (min-width: 640px) {
  .grid-container {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
/* å¤§å±å¹•及以上 (lg:grid-cols-6) */
@media (min-width: 1024px) {
  .grid-container {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}
/* å¡ç‰‡æ‚¬åœæ•ˆæžœå¢žå¼º */
.el-card:hover {
  transform: translateY(-2px);
}
.echarts {
.chart-content {
  display: flex;
  justify-content: space-between;
  flex-direction: column;
  align-items: center;
}
/* å›¾è¡¨å®¹å™¨æ ·å¼ */
.el-chart {
.pie-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  height: 260px;
}
.type-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  margin-top: 12px;
}
.type-card {
  background: #fafafa;
  border-radius: 6px;
  padding: 8px 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 80px;
}
.type-name {
  font-size: 12px;
  color: #666;
}
.type-count {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  margin-top: 4px;
}
.bar-chart-wrap {
  width: 100%;
  height: 260px;
}
/* åŒºå—标题 - å·¦ä¾§è“è‰²ç«–线 */
.section-title {
  position: relative;
  font-size: 18px;
  color: #333;
  padding-left: 10px;
  margin-bottom: 10px;
  padding-left: 12px;
  margin-bottom: 16px;
  font-weight: 700;
}
.section-title::before {
  position: absolute;
  left: 0;
  top: 0px;
  top: 2px;
  content: '';
  width: 4px;
  height: 18px;
  background-color: #002FA7;
  background: #1890ff;
  border-radius: 2px;
}
.chart-num {
  position: absolute;
  z-index: 3;
  top: 92px;
  left: 92px;
  display: flex;
  flex-direction: column;
  justify-content: center;
/* è¡¨æ ¼å¡ç‰‡ */
.table-card {
  border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  :deep(.el-card__body) {
    padding: 16px 20px;
  }
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: center;
  justify-content: flex-end;
  align-items: center;
}
:deep(.el-pagination) {
  --el-pagination-button-bg-color: #fff;
}
:deep(.el-pager li.is-active) {
  background: #1890ff;
}
</style>
src/views/productionManagement/productStructure/Detail/index.vue
@@ -115,7 +115,7 @@
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                              :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -264,15 +264,20 @@
    const productData = row[0];
    //  æœ€å¤–层组件中,与当前产品相同的产品只能有一个
    const isTopLevel = dataValue.dataList.some(item => (item as any).tempId === dataValue.currentRowName);
    const isTopLevel = dataValue.dataList.some(
      item => (item as any).tempId === dataValue.currentRowName
    );
    if (isTopLevel) {
      if (productData.productName === tableData[0].productName &&
        productData.model === tableData[0].model) {
      if (
        productData.productName === tableData[0].productName &&
        productData.model === tableData[0].model
      ) {
        //  æŸ¥æ‰¾æ˜¯å¦å·²ç»æœ‰å…¶ä»–顶层行已经是这个产品
        const hasOther = dataValue.dataList.some(item =>
          (item as any).tempId !== dataValue.currentRowName &&
          (item as any).productName === tableData[0].productName &&
          (item as any).model === tableData[0].model
        const hasOther = dataValue.dataList.some(
          item =>
            (item as any).tempId !== dataValue.currentRowName &&
            (item as any).productName === tableData[0].productName &&
            (item as any).model === tableData[0].model
        );
        if (hasOther) {
          ElMessage.warning("最外层和当前产品一样的一级只能有一个");
@@ -390,7 +395,7 @@
    }
  };
  const removeItem = (tempId:string) => {
  const removeItem = (tempId: string) => {
    // å…ˆå°è¯•从顶层删除
    const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
    if (topIndex !== -1) {
src/views/productionManagement/productionCosting/index.vue
@@ -1,15 +1,32 @@
<template>
    <div class="app-container">
        <div class="content-layout">
        <el-row :gutter="16" class="content-row">
            <!-- å·¦ä¾§å°è´¦ + é¡¶éƒ¨ç­›é€‰ -->
            <div class="left-panel">
            <el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8" class="left-col">
                <div class="left-panel">
                <div class="left-header">
                    <!-- <div class="left-title">生产台账</div> -->
                    <el-radio-group v-model="dateType" size="small" @change="handleDateTypeChange">
                        <el-radio-button label="day">日</el-radio-button>
                        <el-radio-button label="month">月</el-radio-button>
                    </el-radio-group>
          <el-form :model="searchForm" inline>
            <el-form-item prop="dateType">
              <el-radio-group v-model="searchForm.dateType" size="small" @change="handleDateTypeChange">
                <el-radio-button label="day">日</el-radio-button>
                <el-radio-button label="month">月</el-radio-button>
              </el-radio-group>
            </el-form-item>
            <el-form-item label="日期:" prop="dateRange">
              <el-date-picker
                  v-model="searchForm.dateRange"
                  :type="searchForm.dateType === 'day' ? 'date' : 'daterange'"
                  range-separator="至"
                  start-placeholder="开始日期"
                  end-placeholder="结束日期"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                  style="width: 200px"
                  @change="handleDateRangeChange"
              />
            </el-form-item>
          </el-form>
                </div>
                <PIMTable
                    rowKey="id"
@@ -17,28 +34,33 @@
                    :tableData="leftTableData"
                    :tableLoading="tableLoading"
          :page="page"
          :height="200"
          @row-click="handleLeftRowClick"
          @pagination="pagination"
        ></PIMTable>
            </div>
                </div>
            </el-col>
            <!-- å³ä¾§æ˜Žç»†ï¼ˆåŽŸæœ‰å†…å®¹ï¼‰ -->
            <div class="right-panel">
                <div class="header-filters">
                        <el-button @click="handleOut" class="ml10">导出</el-button>
                    </div>
            <!-- å³ä¾§æ˜Žç»† -->
            <el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16" class="right-col">
                <div class="right-panel">
                    <el-form inline>
                        <el-form-item>
                            <el-button type="primary" @click="handleOut">导出</el-button>
                        </el-form-item>
                    </el-form>
                    <PIMTable
                        rowKey="id"
                        :column="tableColumn"
                        :tableData="tableData"
                        :page="page1"
                        :tableLoading="tableLoading"
                        :tableLoading="tableLoading1"
                        style="margin-right: 20px;"
                        @pagination="pagination1"
                    ></PIMTable>
            </div>
        </div>
                </div>
            </el-col>
        </el-row>
    </div>
</template>
@@ -52,7 +74,7 @@
const tableColumn = ref([
    {
        label: "生产日期",
        prop: "scheduleDate",
        prop: "schedulingDate",
    minWidth: 100,
    },
    {
@@ -130,7 +152,10 @@
        label: "合格率",
        prop: "outputRate",
    minWidth: 100,
    formatData: (val) => {
      if (val == null || val === '') return '-'
      return parseFloat(val).toFixed(2)
    },
    },
]);
@@ -139,7 +164,6 @@
const tableLoading1 = ref(false);
const leftTableData = ref([]);
// æ—¥ / æœˆ åˆ‡æ¢ï¼ˆé»˜è®¤æŒ‰æ—¥ï¼‰
const dateType = ref("day");
const page = reactive({
    current: 1,
    size: 100,
@@ -156,12 +180,11 @@
    searchForm: {
        schedulingUserName: "",
        salesContractNo: "",
        entryDate: [
            dayjs().format("YYYY-MM-DD"),
            dayjs().add(1, "day").format("YYYY-MM-DD"),
        ], // å½•入日期
        entryDateStart: dayjs().format("YYYY-MM-DD"),
        entryDateEnd: dayjs().add(1, "day").format("YYYY-MM-DD"),
    dateType: "day",
    dateRange: dayjs().format("YYYY-MM-DD"),
        entryDate: dayjs().format("YYYY-MM-DD"),
        entryDateStart: undefined,
        entryDateEnd: undefined,
    },
});
const { searchForm } = toRefs(data);
@@ -178,28 +201,36 @@
    getList1();
};
const changeDaterange = (value) => {
const handleDateRangeChange = (value) => {
    if (value) {
        searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
        searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
    if (searchForm.value.dateType === "day") {
      searchForm.value.entryDate = value;
    } else {
      searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
      searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
    }
    } else {
        searchForm.value.entryDate = undefined;
        searchForm.value.entryDateStart = undefined;
        searchForm.value.entryDateEnd = undefined;
    }
    handleQuery();
  reloadData()
};
const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    params.dateType = dateType.value;
    params.entryDate = undefined
  salesLedgerProductionAccountingList(params).then((res) => {
        tableLoading.value = false;
        const records = res.data.records || [];
    leftTableData.value = records;
        page.total = res.data.total || 0;
    });
    }).finally(() => {
    tableLoading.value = false;
  })
};
@@ -207,10 +238,11 @@
  tableLoading1.value = true;
  const params = { ...page1, ...searchForm.value };
  salesLedgerProductionAccountingListProductionDetails(params).then((res) => {
    tableLoading1.value = false;
    tableData.value = res.data.records || [];;
    page1.total = res.data.total || 0;
  });
  }).finally(() => {
    tableLoading1.value = false;
  })
};
// æž„建左侧汇总台账(按生产人汇总产量、工资等)
@@ -237,12 +269,26 @@
};
// å·¦ä¾§æ—¥/月切换
const handleDateTypeChange = () => {
const handleDateTypeChange = (value) => {
    // è¿™é‡Œåªä½œä¸ºç­›é€‰æ¡ä»¶çš„一部分,直接重新查询列表
  page.current = 1;
    getList();
  handleQuery()
  if (value === "day") {
    searchForm.value.entryDate = dayjs().format("YYYY-MM-DD");
    searchForm.value.dateRange = searchForm.value.entryDate
  } else {
    searchForm.value.entryDateStart = dayjs().startOf("month").format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs().endOf("month").format("YYYY-MM-DD");
    searchForm.value.dateRange = [searchForm.value.entryDateStart, searchForm.value.entryDateEnd]
  }
  reloadData()
};
const reloadData = () => {
  page.current = 1;
  page1.current = 1;
  getList();
  tableData.value = []
}
// ç‚¹å‡»å·¦ä¾§è¡Œï¼Œåˆ·å³ä¾§æ˜Žç»†ï¼ˆæŒ‰ç”Ÿäº§äººè¿‡æ»¤ï¼‰
const handleLeftRowClick = (row) => {
@@ -279,31 +325,27 @@
</script>
<style scoped lang="scss">
.content-layout {
  display: flex;
  flex-direction: column;
  gap: 16px;
.content-row {
  width: 100%;
}
.left-panel {
  flex: 0 0 50%;
  display: flex;
  flex-direction: column;
  gap: 10px;
.content-row .left-col,
.content-row .right-col {
  margin-bottom: 16px;
}
.left-panel,
.right-panel {
  flex: 0 0 50%;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}
.left-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}
.left-title {
src/views/productionManagement/workOrder/index.vue
@@ -231,6 +231,11 @@
  const tableColumn = ref([
    {
      label: "工单类型",
      prop: "workOrderType",
      width: "80",
    },
    {
      label: "工单编号",
      prop: "workOrderNo",
      width: "140",
src/views/qualityManagement/finalInspection/index.vue
@@ -62,7 +62,7 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import InspectionFormDia from "@/views/qualityManagement/finalInspection/components/inspectionFormDia.vue";
import FormDia from "@/views/qualityManagement/finalInspection/components/formDia.vue";
import {ElMessageBox} from "element-plus";
@@ -75,6 +75,7 @@
import FilesDia from "@/views/qualityManagement/finalInspection/components/filesDia.vue";
import dayjs from "dayjs";
import {userListNoPage} from "@/api/system/user.js";
import useUserStore from "@/store/modules/user";
const data = reactive({
  searchForm: {
@@ -159,7 +160,13 @@
          openForm("edit", row);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                    // å·²æäº¤åˆ™ç¦ç”¨
                    if (row.inspectState == 1) return true;
                    // å¦‚果检验员有值,只有当前登录用户能编辑
                    if (row.checkName) {
                        return row.checkName !== userStore.nickName;
                    }
                    return false;
                }
      },
      {
@@ -176,7 +183,13 @@
                    submit(row.id);
                },
                disabled: (row) => {
                    return row.inspectState == 1;
                    // å·²æäº¤åˆ™ç¦ç”¨
                    if (row.inspectState == 1) return true;
                    // å¦‚果检验员有值,只有当前登录用户能提交
                    if (row.checkName) {
                        return row.checkName !== userStore.nickName;
                    }
                    return false;
                }
            },
            {
@@ -216,6 +229,7 @@
const filesDia = ref()
const inspectionFormDia = ref()
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const userList = ref([]);
const form = ref({
    checkName: ""
src/views/qualityManagement/metricBinding/index.vue
@@ -1,7 +1,9 @@
<template>
  <div class="app-container metric-binding">
    <!-- å·¦ä¾§ï¼šæ£€æµ‹æ ‡å‡†åˆ—表(只读) -->
    <div class="left-panel">
    <el-row :gutter="16" class="metric-binding-row">
      <!-- å·¦ä¾§ï¼šæ£€æµ‹æ ‡å‡†åˆ—表 -->
      <el-col :xs="24" :sm="24" :md="12" :lg="14" :xl="14" class="left-col">
        <div class="panel left-panel">
      <PIMTable
        rowKey="id"
        :column="standardColumns"
@@ -72,10 +74,12 @@
          </el-select>
        </template>
      </PIMTable>
    </div>
        </div>
      </el-col>
    <!-- å³ä¾§ï¼šç»‘定列表 -->
    <div class="right-panel">
      <!-- å³ä¾§ï¼šç»‘定列表 -->
      <el-col :xs="24" :sm="24" :md="12" :lg="10" :xl="10" class="right-col">
        <div class="panel right-panel">
      <div class="right-header">
        <div class="title">绑定关系</div>
        <div class="desc" v-if="currentStandard">
@@ -108,7 +112,9 @@
          </template>
        </el-table-column>
      </el-table>
    </div>
        </div>
      </el-col>
    </el-row>
    <!-- æ·»åŠ ç»‘å®šå¼¹æ¡† -->
    <el-dialog
@@ -375,15 +381,21 @@
}
const handleUnbind = async (row) => {
  if (!row?.id) return
  const id = row?.id ?? row?.qualityTestStandardBindingId
  if (id == null || id === '') return
  try {
    await ElMessageBox.confirm('确认删除该绑定?', '提示', { type: 'warning' })
  } catch {
    return
  }
  await qualityTestStandardBindingDel([row.qualityTestStandardBindingId])
  proxy.$message.success('删除成功')
  loadBindingList()
  try {
    await qualityTestStandardBindingDel([id])
    proxy.$message.success('删除成功')
    loadBindingList()
  } catch (err) {
    console.error('删除绑定失败:', err)
    proxy.$message?.error(err?.message || '删除失败')
  }
}
const handleBatchUnbind = async () => {
@@ -391,15 +403,26 @@
    proxy.$message.warning('请选择数据')
    return
  }
  const ids = bindingSelectedRows.value.map((i) => i.qualityTestStandardBindingId)
  const ids = bindingSelectedRows.value
    .map((i) => i?.id ?? i?.qualityTestStandardBindingId)
    .filter((id) => id != null && id !== '')
  if (!ids.length) {
    proxy.$message.warning('选中数据缺少有效 id')
    return
  }
  try {
    await ElMessageBox.confirm('选中的内容将被删除,是否确认删除?', '删除提示', { type: 'warning' })
  } catch {
    return
  }
  await qualityTestStandardBindingDel(ids)
  proxy.$message.success('删除成功')
  loadBindingList()
  try {
    await qualityTestStandardBindingDel(ids)
    proxy.$message.success('删除成功')
    loadBindingList()
  } catch (err) {
    console.error('批量删除绑定失败:', err)
    proxy.$message?.error(err?.message || '删除失败')
  }
}
onMounted(() => {
@@ -410,16 +433,29 @@
<style scoped>
.metric-binding {
  display: flex;
  gap: 16px;
  padding: 0;
}
.metric-binding-row {
  width: 100%;
}
.metric-binding-row .left-col,
.metric-binding-row .right-col {
  margin-bottom: 16px;
}
.metric-binding-row .panel {
  background: #ffffff;
  padding: 16px;
  box-sizing: border-box;
  height: 100%;
  min-height: 400px;
}
.left-panel,
.right-panel {
  flex: 1;
  background: #ffffff;
  padding: 16px;
  box-sizing: border-box;
  height: 100%;
}
.toolbar {
src/views/qualityManagement/metricMaintenance/index.vue
@@ -1,7 +1,9 @@
<template>
  <div class="app-container metric-maintenance">
    <!-- å·¦ä¾§ï¼šæ£€æµ‹æ ‡å‡†åˆ—表 -->
    <div class="left-panel">
    <el-row :gutter="16" class="metric-maintenance-row">
      <!-- å·¦ä¾§ï¼šæ£€æµ‹æ ‡å‡†åˆ—表 -->
      <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="left-col">
        <div class="left-panel">
      <div class="toolbar">
        <div class="toolbar-left"></div>
        <div class="toolbar-right">
@@ -82,10 +84,12 @@
          </el-select>
        </template>
      </PIMTable>
    </div>
        </div>
      </el-col>
    <!-- å³ä¾§ï¼šæ ‡å‡†å‚数列表 -->
    <div class="right-panel">
      <!-- å³ä¾§ï¼šæ ‡å‡†å‚数列表 -->
      <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="right-col">
        <div class="right-panel">
      <div class="right-header">
        <div class="title">标准参数</div>
        <div class="desc" v-if="currentStandard">
@@ -132,7 +136,9 @@
          </template>
        </el-table-column>
      </el-table>
    </div>
        </div>
      </el-col>
    </el-row>
    <!-- æ–°å¢ž / ç¼–辑检测标准 -->
    <StandardFormDialog
@@ -693,39 +699,31 @@
<style scoped>
.metric-maintenance {
  display: flex;
  gap: 16px;
  min-width: 0; /* å…è®¸ flex å­å…ƒç´ æ”¶ç¼© */
  padding: 0;
  min-width: 0;
}
.metric-maintenance-row {
  width: 100%;
}
.metric-maintenance-row .left-col,
.metric-maintenance-row .right-col {
  margin-bottom: 16px;
}
.left-panel,
.right-panel {
  flex: 1;
  min-width: 0; /* å…è®¸ flex å­å…ƒç´ æ”¶ç¼© */
  min-width: 0;
  background: #ffffff;
  padding: 16px;
  box-sizing: border-box;
  overflow: hidden; /* é˜²æ­¢å†…容溢出 */
}
/* ä½Žåˆ†è¾¨çŽ‡é€‚é… */
@media (max-width: 1400px) {
  .metric-maintenance {
    flex-direction: column;
  }
  .left-panel,
  .right-panel {
    width: 100%;
    min-width: 0;
  }
  overflow: hidden;
  height: 100%;
  min-height: 400px;
}
@media (max-width: 768px) {
  .metric-maintenance {
    gap: 12px;
  }
  .left-panel,
  .right-panel {
    padding: 12px;
src/views/qualityManagement/nonconformingManagement/index.vue
@@ -177,16 +177,8 @@
    label: "操作",
    align: "center",
    fixed: "right",
    width: 120,
    width: 100,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        },
        disabled: (row) => row.inspectState === 1,
      },
      {
        name: "处理",
        type: "text",
src/views/qualityManagement/processInspection/index.vue
@@ -62,7 +62,7 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import InspectionFormDia from "@/views/qualityManagement/processInspection/components/inspectionFormDia.vue";
import FormDia from "@/views/qualityManagement/processInspection/components/formDia.vue";
import {ElMessageBox} from "element-plus";
@@ -75,6 +75,7 @@
import FilesDia from "@/views/qualityManagement/processInspection/components/filesDia.vue";
import dayjs from "dayjs";
import {userListNoPage} from "@/api/system/user.js";
import useUserStore from "@/store/modules/user";
const data = reactive({
  searchForm: {
@@ -164,7 +165,13 @@
          openForm("edit", row);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                    // å·²æäº¤åˆ™ç¦ç”¨
                    if (row.inspectState == 1) return true;
                    // å¦‚果检验员有值,只有当前登录用户能编辑
                    if (row.checkName) {
                        return row.checkName !== userStore.nickName;
                    }
                    return false;
                }
      },
      {
@@ -181,7 +188,13 @@
                    submit(row.id);
                },
                disabled: (row) => {
                    return row.inspectState == 1;
                    // å·²æäº¤åˆ™ç¦ç”¨
                    if (row.inspectState == 1) return true;
                    // å¦‚果检验员有值,只有当前登录用户能提交
                    if (row.checkName) {
                        return row.checkName !== userStore.nickName;
                    }
                    return false;
                }
            },
            {
@@ -226,6 +239,7 @@
const filesDia = ref()
const inspectionFormDia = ref()
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const changeDaterange = (value) => {
  searchForm.value.entryDateStart = undefined;
  searchForm.value.entryDateEnd = undefined;
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -64,7 +64,7 @@
<script setup>
import {Search} from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import InspectionFormDia from "@/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue";
import FormDia from "@/views/qualityManagement/rawMaterialInspection/components/formDia.vue";
import {ElMessageBox} from "element-plus";
@@ -77,6 +77,7 @@
import FilesDia from "@/views/qualityManagement/rawMaterialInspection/components/filesDia.vue";
import dayjs from "dayjs";
import {userListNoPage} from "@/api/system/user.js";
import useUserStore from "@/store/modules/user";
const data = reactive({
  searchForm: {
@@ -166,7 +167,13 @@
          openForm("edit", row);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                    // å·²æäº¤åˆ™ç¦ç”¨
                    if (row.inspectState == 1) return true;
                    // å¦‚果检验员有值,只有当前登录用户能编辑
                    if (row.checkName) {
                        return row.checkName !== userStore.nickName;
                    }
                    return false;
                }
      },
      {
@@ -183,7 +190,13 @@
          submit(row.id);
        },
                disabled: (row) => {
                    return row.inspectState == 1;
                    // å·²æäº¤åˆ™ç¦ç”¨
                    if (row.inspectState == 1) return true;
                    // å¦‚果检验员有值,只有当前登录用户能提交
                    if (row.checkName) {
                        return row.checkName !== userStore.nickName;
                    }
                    return false;
                }
      },
      {
@@ -228,6 +241,7 @@
const filesDia = ref()
const inspectionFormDia = ref()
const {proxy} = getCurrentInstance()
const userStore = useUserStore()
const changeDaterange = (value) => {
  searchForm.value.entryDateStart = undefined;
  searchForm.value.entryDateEnd = undefined;
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -3,8 +3,8 @@
    <PanelHeader title="采购品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -22,11 +22,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -164,6 +168,18 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  rawMaterialPurchaseAmountRatio()
    .then((res) => {
@@ -191,6 +207,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -218,9 +239,6 @@
.pie-background {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -229,5 +247,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -3,8 +3,8 @@
    <PanelHeader title="销售品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
@@ -21,11 +21,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -137,6 +141,17 @@
const cardItems = ref([])
// ä½¿ç”¨å°è£…的背景位置调整方法(与其他文件保持一致)
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: pieDatas // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  productSalesAnalysis()
    .then((res) => {
@@ -162,6 +177,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -2,8 +2,8 @@
  <div>
    <PanelHeader title="产品大类" />
    <div class="panel-item-customers">
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -21,10 +21,15 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import { productCategoryDistribution } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const chart = ref(null)
// æ•°æ®åˆ—表(来自接口)
const dataList = ref([])
@@ -170,6 +175,15 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法,可自定义偏移值
const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  offsetX: '-51.5%', // X è½´åç§»ï¼Œå¯åŠ¨æ€è°ƒæ•´
  offsetY: '-39%',   // Y è½´åç§»ï¼Œå¯åŠ¨æ€è°ƒæ•´
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const loadData = async () => {
  try {
    const res = await productCategoryDistribution()
@@ -182,6 +196,8 @@
    }))
    landLegend.data = dataList.value.map((d) => d.name)
    landSeries.value[0].data = dataList.value
    // æ•°æ®åŠ è½½å®ŒæˆåŽè°ƒæ•´èƒŒæ™¯ä½ç½®
    adjustBackgroundPosition()
  } catch (e) {
    console.error('获取产品大类分布失败:', e)
    dataList.value = []
@@ -190,8 +206,14 @@
  }
}
onMounted(() => {
  loadData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -212,9 +234,6 @@
.pie-background {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-51.5%, -39%);
  width: 360px;
  height: 360px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -223,5 +242,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* é»˜è®¤å±…中,会在 JS ä¸­åŠ¨æ€è°ƒæ•´ */
  left: 50%;
  top: 50%;
  transform: translate(-51.5%, -39%);
}
</style>
src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue
@@ -10,8 +10,8 @@
        />
      </div>
      <!-- <CarouselCards :items="cardItems" :visible-count="3" /> -->
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -29,11 +29,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import ProductTypeSwitch from './ProductTypeSwitch.vue'
import { expenseCompositionAnalysis } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -185,6 +189,18 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  expenseCompositionAnalysis({ type: amountType.value })
    .then((res) => {
@@ -216,6 +232,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -251,9 +272,6 @@
.pie-background {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -262,5 +280,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
}
</style>
src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,306 @@
<template>
  <div class="carousel-cards">
    <button
      v-if="canScrollLeft"
      class="nav-button nav-button-left"
      @click="scrollLeftFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="左箭头" />
    </button>
    <div
      class="cards-container"
      :style="{ '--visible-count': visibleCount }"
      ref="cardsContainerRef"
    >
      <div
        v-for="(item, index) in items"
        :key="index"
        class="card-item"
      >
        <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
        <div class="card-title">
          <div class="card-label">{{ item.label }}</div>
          <div class="card-value">
            <span class="value-number">{{ item.value }}</span>
            <span class="value-unit">{{ item.unit }}</span>
          </div>
          <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate">
            <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span>
          </div>
        </div>
      </div>
    </div>
    <button
      v-if="canScrollRight"
      class="nav-button nav-button-right"
      @click="scrollRightFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="右箭头" />
    </button>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
const props = defineProps({
  items: {
    type: Array,
    default: () => [],
    validator: (value) => {
      return value.every(item =>
        item && typeof item.label !== 'undefined' &&
        typeof item.value !== 'undefined' &&
        typeof item.unit !== 'undefined'
      )
    }
  },
  visibleCount: {
    type: Number,
    default: 3
  }
})
const cardsContainerRef = ref(null)
const currentScrollLeft = ref(0)
const maxScrollLeft = ref(0)
// æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘左滚动
const canScrollLeft = computed(() => {
  return currentScrollLeft.value > 0
})
// æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘右滚动
const canScrollRight = computed(() => {
  return currentScrollLeft.value < maxScrollLeft.value
})
// æ›´æ–°æ»šåŠ¨çŠ¶æ€
const updateScrollState = () => {
  const container = cardsContainerRef.value
  if (!container) return
  currentScrollLeft.value = container.scrollLeft
  maxScrollLeft.value = container.scrollWidth - container.clientWidth
}
// å‘左滚动
const scrollLeftFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: -scrollDistance,
    behavior: 'smooth'
  })
  // å»¶è¿Ÿæ›´æ–°çŠ¶æ€ï¼Œç­‰å¾…æ»šåŠ¨åŠ¨ç”»å®Œæˆ
  setTimeout(() => {
    updateScrollState()
  }, 300)
}
// å‘右滚动
const scrollRightFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: scrollDistance,
    behavior: 'smooth'
  })
  // å»¶è¿Ÿæ›´æ–°çŠ¶æ€ï¼Œç­‰å¾…æ»šåŠ¨åŠ¨ç”»å®Œæˆ
  setTimeout(() => {
    updateScrollState()
  }, 300)
}
// ç›‘听 items å˜åŒ–,更新滚动状态
watch(() => props.items, () => {
  nextTick(() => {
    updateScrollState()
  })
}, { deep: true })
onMounted(() => {
  nextTick(() => {
    updateScrollState()
    // ç›‘听滚动事件
    const container = cardsContainerRef.value
    if (container) {
      container.addEventListener('scroll', updateScrollState)
    }
  })
})
onBeforeUnmount(() => {
  // æ¸…理滚动事件监听器
  const container = cardsContainerRef.value
  if (container) {
    container.removeEventListener('scroll', updateScrollState)
  }
})
</script>
<style scoped>
.carousel-cards {
  width: 100%;
  overflow: hidden;
  position: relative;
  display: flex;
  align-items: center;
}
.cards-container {
  display: flex;
  gap: 12px;
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE and Edge */
  padding-bottom: 4px;
  scroll-behavior: smooth;
}
.cards-container::-webkit-scrollbar {
  display: none; /* Chrome, Safari, Opera */
}
.nav-button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  background: rgba(26, 88, 176, 0.6);
  border: 1px solid rgba(26, 88, 176, 0.8);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: all 0.3s ease;
  padding: 0;
}
.nav-button:hover {
  background: rgba(26, 88, 176, 0.8);
  transform: translateY(-50%) scale(1.1);
}
.nav-button-left {
  left: -16px;
}
.nav-button-left img {
  width: 16px;
  height: 16px;
  transform: rotate(180deg);
}
.nav-button-right {
  right: -16px;
}
.nav-button-right img {
  width: 16px;
  height: 16px;
}
.card-item {
  flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  display: flex;
  align-items: center;
  background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
  border-radius: 8px 8px 8px 8px;
  padding: 12px 16px;
  transition: all 0.3s ease;
}
.card-item:hover {
  transform: translateY(-2px);
}
.card-icon {
  width: 80px;
  height: 60px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  flex-shrink: 0;
  margin-right: 12px;
}
.card-title {
  display: flex;
  align-items: flex-start;
  flex-direction: column;
  flex: 1;
}
.card-label {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
}
.card-value {
  display: flex;
  align-items: baseline;
  gap: 4px;
}
.card-rate {
  margin-top: 4px;
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
}
.rate-label {
  opacity: 0.85;
}
.rate-value {
  font-weight: 500;
}
.value-number {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  line-height: 1;
}
.value-unit {
  font-size: 14px;
  color: #FFFFFF;
  font-weight: 400;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<template>
  <el-radio-group
    v-model="currentValue"
    class="date-type-switch"
    @change="handleChange"
  >
    <el-radio-button :label="1">周</el-radio-button>
    <el-radio-button :label="2">月</el-radio-button>
    <el-radio-button :label="3">季度</el-radio-button>
  </el-radio-group>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"周"
  },
})
const emit = defineEmits(['update:modelValue', 'change'])
const currentValue = ref(props.modelValue)
// ç›‘听外部值变化
watch(
  () => props.modelValue,
  (newVal) => {
    currentValue.value = newVal
  }
)
// å¤„理值变化
const handleChange = (value) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>
<style scoped>
.date-type-switch {
  display: inline-flex;
}
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
}
/* æœ€åŽä¸€ä¸ªæŒ‰é’®å³ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<template>
  <div class="panel-header">
    <span class="panel-title">{{ title }}</span>
  </div>
</template>
<script setup>
defineProps({
  title: {
    type: String,
    required: true,
    default: ''
  }
})
</script>
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
<template>
  <el-radio-group
    v-model="currentValue"
    class="product-type-switch"
    @change="handleChange"
  >
    <el-radio-button :label="1">原材料</el-radio-button>
    <el-radio-button :label="3">半成品</el-radio-button>
    <el-radio-button :label="2">成品</el-radio-button>
  </el-radio-group>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"原材料"
  },
})
const emit = defineEmits(['update:modelValue', 'change'])
const currentValue = ref(props.modelValue)
watch(
  () => props.modelValue,
  (newVal) => {
    currentValue.value = newVal
  }
)
const handleChange = (value) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>
<style scoped>
.product-type-switch {
  display: inline-flex;
}
.product-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
}
.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
}
.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  box-shadow: none;
}
.product-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,351 @@
<template>
  <div>
    <PanelHeader title="生产订单完成进度" />
    <div class="main-panel">
      <div class="panel-item-customers">
        <CarouselCards :items="cardItems" :visible-count="4" />
        <div
          class="progress-table-container"
          ref="progressTableRef"
          style="margin-top: 0px;"
          @scroll="handleTableScroll"
        >
          <table class="progress-table">
            <thead>
              <tr>
                <th>生产订单号</th>
                <th>产品名称</th>
                <th>规格</th>
                <th>需求数量</th>
                <th>完成数量</th>
                <th>完成进度</th>
              </tr>
            </thead>
            <tbody>
              <tr
                v-for="(item, index) in progressTableData"
                :key="index"
                :ref="(el) => setRowRef(el, index)"
                :class="{ 'row-under-header': isRowUnderHeader(index) }"
              >
                <td>{{ item.npsNo || '-' }}</td>
                <td>{{ item.productCategory || '-' }}</td>
                <td>{{ item.specificationModel || '-' }}</td>
                <td>{{ item.quantity || 0 }}</td>
                <td>{{ item.completeQuantity || 0 }}</td>
                <td>
                  <el-progress
                    :percentage="calculateProgress(item)"
                    :color="progressColor(calculateProgress(item))"
                    :status="calculateProgress(item) >= 100 ? 'success' : ''"
                    :stroke-width="8"
                  />
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { getProgressStatistics } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
const progressTableRef = ref(null)
const progressTableScrollTimer = ref(null)
const tableScrollTimeout = ref(null)
const tableRowRefs = ref([])
const rowsUnderHeader = ref(new Set())
// è®¢å•统计对象
const orderStatisticsObject = ref({
  totalOrderCount: 0,
  uncompletedOrderCount: 0,
  partialCompletedOrderCount: 0,
  completedOrderCount: 0,
})
// è½®æ’­å¡ç‰‡æ•°æ®ï¼ˆç”± orderStatisticsObject åŒæ­¥ï¼‰
const cardItems = ref([])
// ç”Ÿäº§è®¢å•完成进度表格数据
const progressTableData = ref([])
// è®¡ç®—完成进度百分比
const calculateProgress = (item) => {
  if (!item) return 0
  if (item.completionStatus !== undefined && item.completionStatus !== null) {
    const percentage = Number(item.completionStatus)
    if (isNaN(percentage)) return 0
    return Math.min(Math.max(Math.round(percentage), 0), 100)
  }
  if (!item.quantity || item.quantity === 0) return 0
  const percentage = ((item.completeQuantity || 0) / item.quantity) * 100
  return Math.min(Math.max(Math.round(percentage), 0), 100)
}
// æ ¹æ®è¿›åº¦ç™¾åˆ†æ¯”返回颜色
const progressColor = (percentage) => {
  const p = percentage || 0
  if (p < 30) return '#f56c6c'
  if (p < 50) return '#e6a23c'
  if (p < 80) return '#409eff'
  return '#67c23a'
}
const setRowRef = (el, index) => {
  if (el) {
    tableRowRefs.value[index] = el
  }
}
const isRowUnderHeader = (index) => rowsUnderHeader.value.has(index)
const handleTableScroll = () => {
  const tableContainer = progressTableRef.value
  if (!tableContainer) return
  const thead = tableContainer.querySelector('thead')
  if (!thead) return
  const theadHeight = thead.offsetHeight
  const containerRect = tableContainer.getBoundingClientRect()
  const containerTop = containerRect.top
  const theadBottom = containerTop + theadHeight
  rowsUnderHeader.value.clear()
  tableRowRefs.value.forEach((row, index) => {
    if (row) {
      const rowRect = row.getBoundingClientRect()
      const rowTop = rowRect.top
      const rowBottom = rowRect.bottom
      if (rowTop < theadBottom && rowBottom > containerTop) {
        rowsUnderHeader.value.add(index)
      }
    }
  })
  if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
  tableScrollTimeout.value = setTimeout(() => {
    rowsUnderHeader.value.clear()
  }, 150)
}
const initProgressTableScroll = () => {
  const tableContainer = progressTableRef.value
  if (!tableContainer) return
  if (progressTableScrollTimer.value) {
    cancelAnimationFrame(progressTableScrollTimer.value)
    progressTableScrollTimer.value = null
  }
  if (tableContainer._pauseTimer) {
    clearInterval(tableContainer._pauseTimer)
    tableContainer._pauseTimer = null
  }
  const tbody = tableContainer.querySelector('tbody')
  if (!tbody) return
  const originalCount = progressTableData.value.length
  const allRows = Array.from(tbody.querySelectorAll('tr'))
  if (allRows.length > originalCount) {
    for (let i = originalCount; i < allRows.length; i++) {
      allRows[i].remove()
    }
  }
  const scrollItems = Array.from(tbody.querySelectorAll('tr'))
  if (scrollItems.length === 0) return
  const originalItemCount = scrollItems.length
  const thead = tableContainer.querySelector('thead')
  const theadHeight = thead ? thead.offsetHeight : 40
  const containerHeight = tableContainer.clientHeight
  const visibleHeight = containerHeight - theadHeight
  const itemHeight = scrollItems[0]?.offsetHeight || 40
  const totalContentHeight = itemHeight * originalItemCount
  if (totalContentHeight <= visibleHeight) return
  const cloneCount = Math.ceil(visibleHeight / itemHeight) + 2
  for (let i = 0; i < cloneCount; i++) {
    const clone = scrollItems[i % originalItemCount].cloneNode(true)
    tbody.appendChild(clone)
  }
  let scrollPosition = 0
  const scrollSpeed = 1.5
  const pauseTime = 3000
  let isPaused = false
  let lastTimestamp = 0
  function scrollAnimation(timestamp) {
    if (!lastTimestamp) lastTimestamp = timestamp
    const deltaTime = timestamp - lastTimestamp
    lastTimestamp = timestamp
    if (!isPaused) {
      scrollPosition += scrollSpeed * (deltaTime / 16)
      const maxScroll = itemHeight * originalItemCount
      if (scrollPosition >= maxScroll) {
        scrollPosition = 0
        tableContainer.scrollTop = 0
      } else {
        tableContainer.scrollTop = scrollPosition
      }
    }
    progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
  }
  progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
  const pauseTimer = setInterval(() => {
    isPaused = !isPaused
  }, pauseTime)
  tableContainer._pauseTimer = pauseTimer
}
const progressStatisticsInfo = () => {
  getProgressStatistics()
    .then((res) => {
      if (!res || !res.data) return
      const obj = {
        totalOrderCount: res.data.totalOrderCount || 0,
        uncompletedOrderCount: res.data.uncompletedOrderCount || 0,
        partialCompletedOrderCount: res.data.partialCompletedOrderCount || 0,
        completedOrderCount: res.data.completedOrderCount || 0,
      }
      orderStatisticsObject.value = obj
      cardItems.value = [
        { label: '总订单数', value: obj.totalOrderCount, unit: 'ä»¶' },
        { label: '未完成订单数', value: obj.uncompletedOrderCount, unit: 'ä»¶' },
        { label: '部分完成订单数', value: obj.partialCompletedOrderCount, unit: 'ä»¶' },
        { label: '已完成订单数', value: obj.completedOrderCount, unit: 'ä»¶' },
      ]
      progressTableData.value = res.data.completedOrderDetails || []
      tableRowRefs.value = []
      rowsUnderHeader.value.clear()
      nextTick(() => {
        initProgressTableScroll()
      })
    })
    .catch((err) => {
      console.error('获取生产订单完成进度统计失败:', err)
    })
}
onMounted(() => {
  progressStatisticsInfo()
})
onBeforeUnmount(() => {
  if (progressTableScrollTimer.value) {
    cancelAnimationFrame(progressTableScrollTimer.value)
  }
  if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
  const tableContainer = progressTableRef.value
  if (tableContainer?._pauseTimer) {
    clearInterval(tableContainer._pauseTimer)
  }
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 428px;
}
.progress-table-container {
  height: 320px;
  overflow-y: auto;
  overflow-x: hidden;
  margin-top: 10px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.progress-table-container::-webkit-scrollbar {
  display: none;
}
.progress-table {
  width: 100%;
  border-collapse: collapse;
  color: #b8c8e0;
  font-size: 12px;
  table-layout: fixed;
}
.progress-table thead {
  position: sticky;
  top: 0;
  background-color: rgba(26, 88, 176, 0.9);
  z-index: 10;
}
.progress-table th {
  padding: 8px 6px;
  text-align: left;
  font-weight: 500;
  border-bottom: 1px solid rgba(184, 200, 224, 0.3);
  color: #b8c8e0;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.progress-table th:nth-child(1) {
  width: 15%;
}
.progress-table th:nth-child(2) {
  width: 15%;
}
.progress-table th:nth-child(3) {
  width: 15%;
}
.progress-table th:nth-child(4) {
  width: 12%;
}
.progress-table th:nth-child(5) {
  width: 12%;
}
.progress-table th:nth-child(6) {
  width: 31%;
}
.progress-table td {
  padding: 8px 6px;
  border-bottom: 1px solid rgba(184, 200, 224, 0.1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  transition: opacity 0.3s ease;
}
.progress-table tbody tr:hover {
  background-color: rgba(184, 200, 224, 0.1);
}
.progress-table tbody tr.row-under-header {
  opacity: 0.5;
}
.progress-table :deep(.el-progress) {
  width: 100%;
}
.progress-table :deep(.el-progress-bar__outer) {
  background-color: rgba(184, 200, 224, 0.2);
}
.progress-table :deep(.el-progress__text) {
  color: #b8c8e0;
  font-size: 11px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
<template>
  <div>
    <!-- è®¾å¤‡ç»Ÿè®¡ -->
    <div class="equipment-stats">
      <div class="equipment-header">
        <img
          src="@/assets/BI/shujutongjiicon@2x.png"
          alt="图标"
          class="equipment-icon"
        />
        <span class="equipment-title">投入产出分析</span>
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="lineLegend"
        :series="lineSeries"
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import { productInOutAnalysis } from '@/api/viewIndex.js'
const chartStyle = { width: '100%', height: '100%' }
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  top: '16%',
  containLabel: true,
}
const lineLegend = {
  show: true,
  top: '2%',
  left: 'center',
  itemGap: 24,
  itemWidth: 12,
  itemHeight: 12,
  textStyle: { color: '#B8C8E0', fontSize: 14 },
  data: [
    { name: '出库', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
    { name: '入库', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
  ],
}
const xAxis1 = ref([
  {
    type: 'category',
    data: [],
    axisTick: { show: false },
    axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
  },
])
const yAxis1 = [
  {
    type: 'value',
    name: '单位: ä»¶',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
    axisLine: { show: false },
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { lineStyle: { color: '#B8C8E0' } },
  },
]
const lineSeries = ref([
  {
    name: '出库',
    type: 'line',
    smooth: false,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
        { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
      ]),
    },
    data: [],
    emphasis: { focus: 'series' },
  },
  {
    name: '入库',
    type: 'line',
    smooth: false,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
        { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
      ]),
    },
    data: [],
    emphasis: { focus: 'series' },
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'line' },
  borderWidth: 1,
  textStyle: { fontSize: 12 },
  formatter(params) {
    let result = params[0].axisValue + '<br/>'
    params.forEach((item) => {
      result += `${item.marker} ${item.seriesName}: ${item.value} ä»¶<br/>`
    })
    return result
  },
}
const fetchData = () => {
  productInOutAnalysis({ type: 1 })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
        xAxis1.value[0].data = list.map((d) => d.date)
        lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0)
        lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0)
      }
    })
    .catch((err) => {
      console.error('获取投入产出分析失败:', err)
    })
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 0 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.equipment-header {
  font-weight: 500;
  font-size: 21px;
  display: flex;
  border-bottom: 1px solid;
  border-image: linear-gradient(
      270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%
    )
    1;
  padding-bottom: 2px;
}
.equipment-title {
  font-weight: 500;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 50px;
}
.equipment-icon {
  width: 50px;
  height: 50px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
<template>
  <div>
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div
        v-for="item in statItems"
        :key="item.name"
        class="stat-card"
      >
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">{{ item.name }}</span>
          <span class="card-value">{{ item.value }}</span>
          <div class="card-compare" :class="compareClass(Number(item.rate))">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(item.rate) }}</span>
            <span class="compare-icon">{{ Number(item.rate) >= 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
const statItems = ref([])
const formatPercent = (val) => {
  const num = Number(val) || 0
  return `${num.toFixed(2)}%`
}
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const fetchData = () => {
  salesPurchaseStorageProductCount()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        statItems.value = res.data.map((item) => ({
          name: item.name,
          value: item.value,
          rate: item.rate,
        }))
      }
    })
    .catch((err) => {
      console.error('获取销售/采购/储存产品数失败:', err)
    })
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.stats-cards {
  display: flex;
  gap: 30px;
}
.stat-card {
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.card-value {
  font-weight: 500;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.card-label {
  font-weight: 400;
  font-size: 19px;
  color: rgba(208, 231, 255, 0.7);
}
.card-compare {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
}
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
}
.compare-value {
  font-weight: 600;
}
.compare-icon {
  font-size: 14px;
  position: relative;
  top: -1px; /* è½»å¾®ä¸Šç§»ï¼Œè®©ç®­å¤´ä¸Žæ–‡å­—垂直居中对齐 */
}
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
<template>
  <div>
    <PanelHeader title="在制品统计分析" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="chart-wrapper">
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
          :grid="grid"
          :legend="workInProcessBarLegend"
          :series="workInProcessBarSeries"
          :tooltip="tooltip"
          :xAxis="workInProcessXAxis"
          :yAxis="workInProcessYAxis"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          style="height: 100%"
        />
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { getWorkInProcessTurnover } from '@/api/viewIndex.js'
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡å¯¹è±¡
const workInProcessStatistics = ref({
  totalQuantity: 0,
  avgTurnoverDays: 0,
  turnoverEfficiency: 0,
})
// è½®æ’­å¡ç‰‡æ•°æ®ï¼ˆç”± workInProcessStatistics åŒæ­¥ï¼‰
const cardItems = ref([])
const chartStyle = {
  width: '100%',
  height: '100%',
}
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  containLabel: true,
}
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`
    })
    return result
  },
}
// åœ¨åˆ¶å“å·¥åºæŸ±çŠ¶å›¾é…ç½®
const workInProcessXAxis = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: [],
  },
])
const workInProcessYAxis = [
  {
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    name: '',
  },
]
const workInProcessBarLegend = {
  show: false,
  textStyle: { color: '#B8C8E0' },
  data: [],
}
const workInProcessBarSeries = ref([
  {
    name: '在制品数量',
    type: 'bar',
    barWidth: 25,
    barGap: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
        { offset: 1, color: 'rgba(0,164,237,0)' },
        { offset: 0, color: '#4EE4FF' },
        ],
      },
    },
    label: {
      show: true,
      position: 'top',
      color: '#B8C8E0',
    },
    data: [],
  },
])
const workInProcessTurnoverInfo = () => {
  getWorkInProcessTurnover()
    .then((res) => {
      if (!res || !res.data) return
      const stats = {
        totalQuantity: res.data.totalOrderCount || 0,
        avgTurnoverDays: res.data.averageTurnoverDays || 0,
        turnoverEfficiency: res.data.turnoverEfficiency || 0,
      }
      workInProcessStatistics.value = stats
      cardItems.value = [
        { label: '总在制数量', value: stats.totalQuantity, unit: 'ä»¶' },
        { label: '平均周转天数', value: stats.avgTurnoverDays, unit: '天' },
        { label: '周转效率', value: stats.turnoverEfficiency, unit: '%' },
      ]
      if (res.data.processDetails && Array.isArray(res.data.processDetails)) {
        workInProcessXAxis.value[0].data = res.data.processDetails
      } else {
        workInProcessXAxis.value[0].data = []
      }
      if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) {
        workInProcessBarSeries.value[0].data = res.data.processQuantityDetails
      } else {
        workInProcessBarSeries.value[0].data = []
      }
    })
    .catch((err) => {
      console.error('获取在制品周转统计失败:', err)
    })
}
onMounted(() => {
  workInProcessTurnoverInfo()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.chart-wrapper {
  height: 70%;
  flex: 1;
  min-height: 200px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,227 @@
<template>
  <div>
    <PanelHeader title="工序产出分析" />
    <div class="main-panel panel-item-customers">
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
          :legend="pieLegend"
          :series="pieSeries"
          :tooltip="pieTooltip"
          :color="pieColors"
          :options="pieOptions"
          style="height: 320px"
        />
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const dateType = ref(1) // 1=周 2=月 3=季度
function array2obj(array, key) {
  const resObj = {}
  for (let i = 0; i < array.length; i++) {
    resObj[array[i][key]] = array[i]
  }
  return resObj
}
const chartStyle = {
  width: '100%',
  height: '100%',
}
const echartsRef = ref(null)
const pieDatas = ref([])
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
const pieLegend = computed(() => {
  const data = pieDatas.value.map((d, idx) => ({
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      color: pieColors[idx % pieColors.length],
    },
  }))
  return {
    orient: 'vertical',
    top: 'center',
    left: '52%',
    itemGap: 30,
    data: data,
    formatter: function (name) {
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: '#82baff',
          fontSize: 12,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 0],
        },
        title: {
          fontSize: 12,
          padding: [0, 0, 0, 0],
        },
      },
    },
  }
})
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
}
const pieSeries = computed(() => [
  {
    name: '产品销售金额分析',
    type: 'pie',
    radius: '60%',
    center: ['25%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderWidth: 2,
    },
    label: {
      show: false
    },
    minAngle: 15,
    data: pieDatas.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
      return Math.random() * 200
    },
  },
])
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: pieDatas // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  productSalesAnalysis()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
        pieDatas.value = items.map((item) => ({
          name: item.name,
          value: parseFloat(item.value) || 0,
          rate: item.rate,
        }))
      }
    })
    .catch((err) => {
      console.error('获取产品销售金额分析失败:', err)
    })
}
const handleDateTypeChange = () => {
  fetchData()
}
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  background: transparent;
}
.pie-background {
  position: absolute;
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
}
</style>
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
<template>
  <div>
    <PanelHeader title="生产核算分析" />
    <div class="main-panel panel-item-customers">
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="barLegend"
        :series="chartSeries"
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { qualityStatistics } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import Echarts from '@/components/Echarts/echarts.vue'
const dateType = ref(1) // 1=周 2=月 3=季度
const chartStyle = {
  width: '100%',
  height: '140%',
}
const grid = { left: '10%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  data: ['产量', '工资', '合格率'],
}
// æŸ±çŠ¶å›¾ï¼šäº§é‡ã€å·¥èµ„ï¼›æŠ˜çº¿å›¾ï¼šåˆæ ¼çŽ‡ï¼ˆç»¿è‰²ï¼‰
const chartSeries = ref([
  {
    name: '产量',
    type: 'bar',
    barWidth: 20,
    barGap: '40%',
    yAxisIndex: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
        ],
      },
    },
    data: [],
  },
  {
    name: '工资',
    type: 'bar',
    barGap: '40%',
    barWidth: 20,
    yAxisIndex: 1,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
        ],
      },
    },
    data: [],
  },
  {
    name: '合格率',
    type: 'line',
    yAxisIndex: 2,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    data: [],
    emphasis: { focus: 'series' },
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      let unit = 'ä»¶'
      if (item.seriesName === '合格率') unit = '%'
      else if (item.seriesName === '工资') unit = '元'
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    })
    return result
  },
}
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: '产量(ä»¶)', position: 'left', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  { type: 'value', name: '工资(元)', position: 'left', offset: 50, axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
    type: 'value',
    name: '合格率(%)',
    position: 'right',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
  },
]
const handleDateTypeChange = () => {
  fetchData()
}
const fetchData = () => {
  qualityStatistics()
    .then((res) => {
      if (!res?.data?.item || !Array.isArray(res.data.item)) return
      const items = res.data.item
      xAxis1.value[0].data = items.map((d) => d.date)
      // äº§é‡ï¼šå‡ºåŽ‚æ•°
      chartSeries.value[0].data = items.map((d) => Number(d.factoryNum) || 0)
      // å·¥èµ„:暂无单独接口,用 0 å ä½ï¼ŒåŽç»­å¯æŽ¥å·¥èµ„接口
      chartSeries.value[1].data = items.map(() => 0)
      // åˆæ ¼çŽ‡ï¼šå‡ºåŽ‚æ•°/过程数*100(无单独接口时用此占位)
      chartSeries.value[2].data = items.map((d) => {
        const processNum = Number(d.processNum) || 0
        const factoryNum = Number(d.factoryNum) || 0
        if (processNum <= 0) return 0
        return Math.min(100, Math.round((factoryNum / processNum) * 100))
      })
    })
    .catch((err) => {
      console.error('获取产量、工资与合格率数据失败:', err)
    })
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/right-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
<template>
  <div>
    <PanelHeader title="工单执行效率分析" />
    <div class="main-panel panel-item-customers">
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="barLegend"
        :series="chartSeries"
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { qualityStatistics } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
const chartStyle = {
  width: '100%',
  height: '160%',
}
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  data: ['开工', '完成', '良品率'],
}
// æŸ±çŠ¶å›¾ï¼šå¼€å·¥ã€å®Œæˆï¼›æŠ˜çº¿å›¾ï¼šè‰¯å“çŽ‡ï¼ˆé¢œè‰² rgba(90, 216, 166, 1))
const chartSeries = ref([
  {
    name: '开工',
    type: 'bar',
    barWidth: 20,
    barGap: '40%',
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
        ],
      },
    },
    data: [],
  },
  {
    name: '完成',
    type: 'bar',
    barGap: '40%',
    barWidth: 20,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
        ],
      },
    },
    data: [],
  },
  {
    name: '良品率',
    type: 'line',
    yAxisIndex: 1,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    data: [],
    emphasis: { focus: 'series' },
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      const unit = item.seriesName === '良品率' ? '%' : 'ä»¶'
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    })
    return result
  },
}
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: 'ä»¶', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
    type: 'value',
    name: '良品率(%)',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
  },
]
const fetchData = () => {
  qualityStatistics()
    .then((res) => {
      if (!res?.data?.item || !Array.isArray(res.data.item)) return
      const items = res.data.item
      xAxis1.value[0].data = items.map((d) => d.date)
      // å¼€å·¥ï¼šè¿‡ç¨‹æ£€éªŒæ•°
      chartSeries.value[0].data = items.map((d) => Number(d.processNum) || 0)
      // å®Œæˆï¼šå‡ºåŽ‚æ•°
      chartSeries.value[1].data = items.map((d) => Number(d.factoryNum) || 0)
      // è‰¯å“çŽ‡ï¼šå‡ºåŽ‚æ•°/过程数*100(无单独接口时用此占位)
      chartSeries.value[2].data = items.map((d) => {
        const processNum = Number(d.processNum) || 0
        const factoryNum = Number(d.factoryNum) || 0
        if (processNum <= 0) return 0
        return Math.min(100, Math.round((factoryNum / processNum) * 100))
      })
    })
    .catch((err) => {
      console.error('获取开工与良品率数据失败:', err)
    })
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
</style>
src/views/reportAnalysis/productionAnalysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,292 @@
<template>
  <div class="scale-container">
    <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
    <!-- å…¨å±æŒ‰é’® - ç§»åŠ¨åˆ°å·¦ä¸Šè§’ -->
    <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
      <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
      </svg>
      <svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
      </svg>
    </button>
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="factory-name">生产数据分析</div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
    <div class="dashboard-content">
      <!-- å·¦ä¾§åŒºåŸŸ -->
      <div class="left-panel">
        <LeftTop />
        <LeftBottom />
      </div>
      <!-- ä¸­é—´åŒºåŸŸ -->
      <div class="center-panel">
        <CenterTop />
        <CenterCenter/>
        <CenterBottom />
      </div>
      <!-- å³ä¾§åŒºåŸŸ -->
      <div class="right-panel">
        <RightTop />
        <RightBottom />
      </div>
    </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
import RightTop from './components/right-top.vue'
import RightBottom from './components/right-bottom.vue'
import useUserStore from '@/store/modules/user'
import LeftTop from './components/left-top.vue'
import CenterTop from './components/center-top.vue'
import CenterBottom from './components/center-bottom.vue'
// å…¨å±ç›¸å…³çŠ¶æ€
const isFullscreen = ref(false);
// ç¼©æ”¾æ¯”例
const scaleRatio = ref(1)
// è®¾è®¡å°ºå¯¸ï¼ˆåŸºå‡†å°ºå¯¸ï¼‰- æ ¹æ®å®žé™…设计稿调整
const designWidth = 1920
const designHeight = 1080
// ç”¨æˆ·store
const userStore = useUserStore()
// è®¡ç®—缩放比例
const calculateScale = () => {
  const container = document.querySelector('.scale-container')
  if (!container) return
  // èŽ·å–å®¹å™¨çš„å®žé™…å°ºå¯¸
  const rect = container.getBoundingClientRect?.()
  const containerWidth = container.clientWidth || rect?.width || window.innerWidth
  const containerHeight = container.clientHeight || rect?.height || window.innerHeight
  // è®¡ç®—宽高缩放比例,取较小值以保证内容完整显示(等比缩放)
  const scaleX = containerWidth / designWidth
  const scaleY = containerHeight / designHeight
  scaleRatio.value = Math.min(scaleX, scaleY)
}
// çª—口大小变化处理
const handleResize = () => {
  // å»¶è¿Ÿæ‰§è¡Œï¼Œç¡®ä¿DOM更新完成
  setTimeout(() => {
    calculateScale()
  }, 100)
}
// å…¨å±åŠŸèƒ½å®žçŽ° - é’ˆå¯¹scale-container元素
const toggleFullscreen = () => {
  const element = document.querySelector('.scale-container')
  if (!element) return
  if (!isFullscreen.value) {
    if (element.requestFullscreen) {
      element.requestFullscreen()
    } else if (element.webkitRequestFullscreen) {
      element.webkitRequestFullscreen()
    } else if (element.msRequestFullscreen) {
      element.msRequestFullscreen()
    }
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen()
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen()
    }
  }
}
// ç›‘听全屏变化事件
const handleFullscreenChange = () => {
  const fullscreenElement = document.fullscreenElement ||
                           document.webkitFullscreenElement ||
                           document.msFullscreenElement
  isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
  // å…¨å±çŠ¶æ€å˜åŒ–æ—¶ï¼Œå»¶è¿Ÿé‡æ–°è®¡ç®—ç¼©æ”¾æ¯”ä¾‹ï¼ˆç¡®ä¿DOM更新完成)
  setTimeout(() => {
    calculateScale()
  }, 200)
}
// ç”Ÿå‘½å‘¨æœŸé’©å­
onMounted(() => {
  // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化
  nextTick(() => {
    // è®¡ç®—初始缩放比例
    calculateScale()
  })
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
})
onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
  // ç§»é™¤æˆ‘们添加的autofit动态调整监听器
  if (window._autofitUpdateHandler) {
    window.removeEventListener('resize', window._autofitUpdateHandler)
    delete window._autofitUpdateHandler
  }
  // å…³é—­autofit
  autofit.off()
})
</script>
<style scoped>
/* å¤–部缩放容器 - å æ®æ•´ä¸ªè§†å£ */
.scale-container {
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
overflow: hidden;
}
/* å†…部内容区域 - å›ºå®šè®¾è®¡å°ºå¯¸ */
.data-dashboard {
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-origin: center center;
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
.scale-container:fullscreen {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
/* Webkit浏览器前缀 */
.scale-container:-webkit-full-screen {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
/* MS浏览器前缀 */
.scale-container:-ms-fullscreen {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
.dashboard-header {
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
position: absolute;
}
.fullscreen-btn {
position: absolute;
top: 10px;
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px;
color: #00d4ff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
z-index: 10000;
}
.fullscreen-btn:hover {
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
}
.dashboard-content {
position: relative;
z-index: 1;
display: flex;
gap: 30px;
padding: 0 30px;
height: calc(100% - 86px);
overflow: hidden;
}
/* ç¡®ä¿å„面板能够正确显示 */
.left-panel, .center-panel, .right-panel {
overflow: hidden;
}
.left-panel,
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 24px;
width: 520px;
}
.center-panel {
flex: 1.5;
display: flex;
flex-direction: column;
gap: 20px;
}
</style>
src/views/safeProduction/accidentReportingRecord/index.vue
@@ -52,7 +52,8 @@
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="140px">
               label-position="top"
               label-width="150px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="事故编号"
src/views/safeProduction/dangerInvestigation/index.vue
@@ -267,10 +267,11 @@
          </el-col>
        </el-row>
      </el-form>
      <div v-if="operationType === 'edit2' || operationType === 'edit3'"
           class="classtitle">隐患详情</div>
      <el-descriptions :column="2"
                       style="margin-bottom: 20px;"
                       v-if="operationType === 'edit2' || operationType === 'edit3'"
                       title="隐患详情"
                       border>
        <el-descriptions-item label="隐患编号">
          <span class="detail-title">{{ form.hiddenCode }}</span>
@@ -306,10 +307,12 @@
          <span class="detail-title">{{ form.rectifyTime }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div class="classtitle"
           v-if="operationType === 'edit3'"
           style="margin-top: 40px;">整改详情</div>
      <el-descriptions :column="2"
                       style="margin-bottom: 20px;"
                       v-if="operationType === 'edit3'"
                       title="整改详情"
                       border>
        <el-descriptions-item label="整改具体措施"
                              :span="2">
@@ -319,6 +322,9 @@
          <span class="detail-title">{{ form2.rectifyActualTime }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div class="classtitle"
           v-if="operationType === 'edit2' || operationType === 'edit3'"
           style="margin-top: 40px;margin-bottom: 30px;">验收情况</div>
      <el-form :model="form2"
               v-if="operationType === 'edit2'"
               label-width="140px"
@@ -1272,4 +1278,12 @@
      page-break-after: avoid;
    }
  }
  .classtitle {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-left: 4px solid #409eff;
    padding-left: 12px;
    margin-bottom: 12px;
  }
</style>
src/views/safeProduction/emergencyPlanReview/index.vue
@@ -52,17 +52,18 @@
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="120px">
               label-position="top"
               label-width="150px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="应急预案编码"
            <el-form-item label="应急预案编码:"
                          prop="planCode">
              <el-input v-model="form.planCode"
                        placeholder="请输入应急预案编码" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="应急预案名称"
            <el-form-item label="应急预案名称:"
                          prop="planName">
              <el-input v-model="form.planName"
                        placeholder="请输入应急预案名称" />
@@ -99,7 +100,7 @@
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="预案类型"
            <el-form-item label="预案类型:"
                          prop="planType">
              <el-select v-model="form.planType"
                         placeholder="请选择预案类型"
@@ -112,14 +113,14 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="备注"
            <el-form-item label="备注:"
                          prop="remark">
              <el-input v-model="form.remark"
                        placeholder="请输入备注" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="适用范围"
        <el-form-item label="适用范围:"
                      prop="applyScope">
          <el-checkbox-group v-model="form.applyScope">
            <el-checkbox label="all">全体员工</el-checkbox>
@@ -129,7 +130,7 @@
            <el-checkbox label="tech">技术部门</el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="应急处置步骤"
        <el-form-item label="应急处置步骤:"
                      prop="execSteps">
          <div class="exec-steps-container"
               style="width:100%">
@@ -216,7 +217,7 @@
              <div v-for="(step, index) in JSON.parse(currentKnowledge.execSteps)"
                   :key="index"
                   class="exec-step-view">
                <span class="step-number">{{ index + 1 }}.</span>
                <!-- <span class="step-number">{{ index + 1 }}.</span> -->
                <span class="step-title">{{ step.step }}:</span>
                <span>{{ step.description }}</span>
              </div>
@@ -764,45 +765,83 @@
  .exec-steps-container {
    border: 1px solid #e4e7ed;
    border-radius: 4px;
    padding: 15px;
    border-radius: 8px;
    padding: 20px;
    background-color: #f9fafc;
    margin-top: 10px;
  }
  .exec-step-item {
    margin-bottom: 10px;
    padding: 10px;
    margin-bottom: 12px;
    padding: 12px;
    background-color: #ffffff;
    border: 1px solid #e4e7ed;
    border-radius: 4px;
    border-radius: 6px;
    transition: all 0.3s ease;
  }
  .exec-step-item:hover {
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    border-color: #c6e2ff;
  }
  .step-header {
    display: flex;
    align-items: flex-start;
    flex-direction: column;
    gap: 8px;
  }
  .exec-step-view {
    margin-bottom: 8px;
    padding-left: 20px;
    margin-bottom: 16px;
    padding-left: 24px;
    position: relative;
    line-height: 1.6;
  }
  .exec-step-view::before {
    content: "";
    position: absolute;
    left: 10px;
    top: 20px;
    bottom: -16px;
    width: 2px;
    background-color: #eaeaea;
  }
  .exec-step-view:last-child::before {
    display: none;
  }
  .step-number {
    position: absolute;
    left: 0;
    top: 0;
    width: 20px;
    height: 20px;
    line-height: 20px;
    text-align: center;
    font-weight: bold;
    color: #409eff;
    color: #ffffff;
    background-color: #409eff;
    border-radius: 50%;
    font-size: 12px;
    z-index: 1;
  }
  .step-title {
    font-weight: bold;
    margin-right: 5px;
    font-weight: 600;
    margin-right: 8px;
    color: #395a9c;
  }
  .no-data {
    color: #909399;
    font-style: italic;
    text-align: center;
    padding: 20px;
    background-color: #f8f9fa;
    border-radius: 4px;
    margin-top: 10px;
  }
</style>
src/views/safeProduction/safetyTrainingAssessment/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,325 @@
<template>
  <div class="app-container">
    <PageHeader content="培训记录">
    </PageHeader>
    <div class="search_form">
      <div class="search_item">
        <span class="search_title">人员名称:</span>
        <el-input v-model="searchForm.searchText"
                  style="width: 240px"
                  placeholder="输入客户名称搜索"
                  @change="searchName"
                  clearable
                  prefix-icon="Search" />
        <el-button type="primary"
                   @click="searchName"
                   style="margin-left: 10px">搜索</el-button>
      </div>
      <div class="search_item">
        <span class="search_title">年份:</span>
        <el-date-picker v-model="searchForm.invoiceDate"
                        type="year"
                        @change="searchDate"
                        placeholder="选择年">
        </el-date-picker>
        <el-button type="primary"
                   @click="searchDate"
                   style="margin-left: 10px">搜索</el-button>
        <el-button type="primary"
                   @click="exportData"
                   style="margin-left: 20px;margin-right: 20px">导出</el-button>
      </div>
    </div>
    <div style="display: flex">
      <div class="table_list">
        <el-table :data="tableData"
                  border
                  v-loading="tableLoading"
                  :row-key="(row) => row.id"
                  @row-click="rowClickMethod"
                  height="calc(100vh - 18.5em)">
          <el-table-column align="center"
                           label="序号"
                           type="index"
                           width="60" />
          <el-table-column label="名称"
                           prop="nickName"
                           show-overflow-tooltip
                           width="200" />
          <el-table-column label="所属部门"
                           prop="deptNames"
                           show-overflow-tooltip
                           width="200" />
          <el-table-column label="联系方式"
                           prop="phonenumber"
                           show-overflow-tooltip
                           width="200" />
          <!-- <el-table-column label="email"
                           prop="email"
                           show-overflow-tooltip
                           width="200" /> -->
          <!-- <el-table-column label="应收金额(元)"
                           prop="unReceiptPaymentAmount"
                           show-overflow-tooltip
                           width="200">
            <template #default="{ row, column }">
              <el-text type="danger">
                {{ formattedNumber(row, column, row.unReceiptPaymentAmount) }}
              </el-text>
            </template>
          </el-table-column> -->
        </el-table>
        <pagination v-show="total > 0"
                    :total="total"
                    layout="total, sizes, prev, pager, next, jumper"
                    :page="page.current"
                    :limit="page.size"
                    @pagination="paginationChange" />
      </div>
      <div class="table_list">
        <el-table :data="receiptRecord"
                  border
                  :row-key="(row) => row.id"
                  height="calc(100vh - 18.5em)">
          <el-table-column align="center"
                           label="序号"
                           type="index"
                           width="60" />
          <el-table-column label="培训日期"
                           prop="trainingDate"
                           show-overflow-tooltip />
          <el-table-column label="培训内容"
                           prop="trainingContent"
                           show-overflow-tooltip />
          <el-table-column label="培训课时"
                           prop="classHour"
                           show-overflow-tooltip />
          <el-table-column label="考核结果"
                           prop="examinationResults"
                           show-overflow-tooltip>
            <template #default="{ row }">
              <el-tag :type="row.examinationResults === '合格' ? 'success' : 'danger'">
                {{ row.examinationResults }}
              </el-tag>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </div>
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import { invoiceLedgerSalesAccount } from "@/api/salesManagement/invoiceLedger.js";
  import { customerInteractions } from "@/api/salesManagement/receiptPayment.js";
  import Pagination from "@/components/PIMTable/Pagination.vue";
  import {
    safeTrainingDetailListPage,
    safeTrainingDetailExport,
  } from "@/api/safeProduction/safetyTrainingAssessment.js";
  import { userListNoPage } from "@/api/system/user.js";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const receiptRecord = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
  });
  const recordPage = reactive({
    current: 1,
    size: 100,
  });
  const total = ref(0);
  const recordTotal = ref(0);
  const data = reactive({
    searchForm: {
      searchText: "",
      invoiceDate: "",
    },
  });
  const customerId = ref("");
  const { searchForm } = toRefs(data);
  const originReceiptRecord = ref([]);
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const tableDataCopy = ref([]);
  const getList = () => {
    tableLoading.value = true;
    userListNoPage({}).then(res => {
      console.log("res", res.data);
      tableData.value = res.data;
      tableDataCopy.value = res.data;
      if (tableData.value.length > 0) {
        customerId.value = tableData.value[0].userId;
        receiptPaymentList(customerId.value);
        tableLoading.value = false;
      }
    });
  };
  const exportData = () => {
    safeTrainingDetailExport({
      userId: customerId.value,
    })
      .then(res => {
        // åˆ›å»ºBlob对象
        const blob = new Blob([res], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        // åˆ›å»ºä¸‹è½½é“¾æŽ¥
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = `记录详情_${tableData.value[0].nickName}.docx`;
        // æ¨¡æ‹Ÿç‚¹å‡»ä¸‹è½½
        document.body.appendChild(link);
        link.click();
        // æ¸…理临时对象
        setTimeout(() => {
          document.body.removeChild(link);
          window.URL.revokeObjectURL(url);
        }, 100);
        ElMessage.success("导出成功");
      })
      .catch(err => {
        console.error("导出失败:", err);
        ElMessage.error("导出失败,请重试");
      });
  };
  const formattedNumber = (row, column, cellValue) => {
    return parseFloat(cellValue).toFixed(2);
  };
  // ä¸»è¡¨åˆè®¡æ–¹æ³•
  const summarizeMainTable = param => {
    return proxy.summarizeTable(
      param,
      ["invoiceTotal", "receiptPaymentAmount", "unReceiptPaymentAmount"],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
    );
  };
  // å­è¡¨åˆè®¡æ–¹æ³•
  const summarizeMainTable1 = param => {
    var summarizeTable = proxy.summarizeTable(
      param,
      ["invoiceAmount", "receiptAmount", "unReceiptAmount"],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
    );
    // å–最后一行数据;
    if (receiptRecord.value?.length > 0) {
      const index = tableData.value.findIndex(
        item => item.id == customerId.value
      );
      summarizeTable[summarizeTable.length - 1] =
        tableData.value[index].unReceiptPaymentAmount.toFixed(2);
    } else {
      summarizeTable[summarizeTable.length - 1] = 0.0;
    }
    return summarizeTable;
  };
  const goBack = () => {
    proxy.$router.push({
      path: "/safeProduction/safetyTrainingAssessment",
    });
  };
  const searchName = () => {
    tableData.value = tableDataCopy.value;
    if (searchForm.value.searchText) {
      tableData.value = tableData.value.filter(item =>
        item.nickName.includes(searchForm.value.searchText)
      );
      customerId.value = tableData.value[0].userId;
    }
    receiptPaymentList(customerId.value);
  };
  const dateFormat = (date, format = "yyyy-MM-dd") => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return format.replace("yyyy", year).replace("MM", month).replace("dd", day);
  };
  const searchDate = () => {
    receiptRecord.value = originReceiptRecordCopy.value;
    console.log("searchForm.value.invoiceDate", searchForm.value.invoiceDate);
    if (searchForm.value.invoiceDate) {
      const year = dateFormat(searchForm.value.invoiceDate, "yyyy");
      receiptRecord.value = receiptRecord.value.filter(item => {
        console.log("item.trainingDate", item.trainingDate);
        return item.trainingDate.includes(year);
      });
    }
  };
  const originReceiptRecordCopy = ref([]);
  const receiptPaymentList = id => {
    const param = {
      userId: id,
    };
    console.log("param", param);
    safeTrainingDetailListPage(param).then(res => {
      originReceiptRecord.value = res.data.records;
      handlePagination({ page: 1, limit: recordPage.size });
      recordTotal.value = res.data.length;
    });
  };
  // æ±‡æ¬¾è®°å½•列表分页
  const recordPaginationChange = pagination => {
    handlePagination(pagination);
  };
  const rowClickMethod = row => {
    customerId.value = row.userId;
    receiptPaymentList(customerId.value);
  };
  const handlePagination = ({ page, limit }) => {
    recordPage.current = page;
    recordPage.size = limit;
    const start = (page - 1) * limit;
    const end = start + limit;
    receiptRecord.value = originReceiptRecord.value.slice(start, end);
    originReceiptRecordCopy.value = originReceiptRecord.value.slice(start, end);
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped lang="scss">
  .table_list {
    width: 50%;
  }
  .search_back {
    cursor: pointer;
    color: #0f497e;
  }
  .search_item {
    width: 50%;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    margin-right: 20px;
  }
</style>
src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -2,23 +2,14 @@
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">培训名称:</span>
        <el-input v-model="searchForm.name"
                  style="width: 240px"
                  placeholder="请输入培训名称搜索"
                  @change="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title ml10">培训类型:</span>
        <el-select v-model="searchForm.type"
                   clearable
                   @change="handleQuery"
                   style="width: 240px">
          <el-option v-for="item in knowledgeTypeOptions"
                     :key="item.value"
                     :label="item.label"
                     :value="item.value" />
        </el-select>
        <span class="search_title">培训日期:</span>
        <el-date-picker v-model="searchForm.trainingDate"
                        value-format="YYYY-MM-DD"
                        format="YYYY-MM-DD"
                        @change="handleQuery"
                        type="date"
                        placeholder="请选择"
                        clearable />
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">
@@ -28,12 +19,24 @@
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增培训</el-button>
        <el-button type="primary"
                   @click="opendetail">培训记录</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-tabs v-model="searchForm.state"
               @tab-click="tabhandleQuery">
        <el-tab-pane label="未开始"
                     :name="0"></el-tab-pane>
        <el-tab-pane label="进行中"
                     :name="1"></el-tab-pane>
        <el-tab-pane label="已结束"
                     :name="2"></el-tab-pane>
      </el-tabs>
      <!-- state    çŠ¶æ€(0:未开始1:进行中;2:已结束)     -->
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
@@ -53,7 +56,8 @@
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="120px">
               label-position="top"
               label-width="150px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="培训日期"
@@ -82,6 +86,7 @@
                          prop="openingTime">
              <el-time-picker v-model="form.openingTime"
                              placeholder="请选择"
                              style="width: 100%"
                              value-format="HH:mm:ss"
                              format="HH:mm:ss"
                              clearable />
@@ -92,6 +97,7 @@
                          prop="endTime">
              <el-time-picker v-model="form.endTime"
                              placeholder="请选择"
                              style="width: 100%"
                              value-format="HH:mm:ss"
                              format="HH:mm:ss"
                              clearable />
@@ -135,6 +141,8 @@
            <el-form-item label="课程学分"
                          prop="projectCredits">
              <el-input v-model="form.projectCredits"
                        type="number"
                        min="0"
                        placeholder="请输入课程学分" />
            </el-form-item>
          </el-col>
@@ -181,51 +189,171 @@
    </el-dialog>
    <!-- æŸ¥çœ‹çŸ¥è¯†è¯¦æƒ…弹窗 -->
    <el-dialog v-model="viewDialogVisible"
               title="培训详情"
               title="结果明细"
               width="900px"
               :close-on-click-modal="false">
      <div class="knowledge-detail">
        <el-descriptions :column="2"
                         border>
          <el-descriptions-item label="培训名称"
                                :span="2">
            <span class="detail-title">{{ currentKnowledge.name }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="培训编码">
            {{ currentKnowledge.code }}
          </el-descriptions-item>
          <el-descriptions-item label="培训类型">
            <el-tag type="info">
              <!-- {{ getTypeLabel(currentKnowledge.type) }} -->
        <div class="classtitle">课程详情</div>
        <el-descriptions size="mini"
                         border
                         :column="3">
          <el-descriptions-item label="课程编号:">{{ currentKnowledge.courseCode }}</el-descriptions-item>
          <el-descriptions-item label="培训内容:">{{ currentKnowledge.trainingContent }}</el-descriptions-item>
          <el-descriptions-item label="状态:">
            <el-tag :type="currentKnowledge.status === 0 ? 'success' : (currentKnowledge.status === 1 ? 'success' : 'info')">
              {{ currentKnowledge.status === 0 ? '未开始' : (currentKnowledge.status === 1 ? '进行中' : '已结束') }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="所在位置">
            {{ currentKnowledge.location }}
          <el-descriptions-item label="培训讲师:">
            {{ currentKnowledge.trainingLecturer }}
          </el-descriptions-item>
          <el-descriptions-item label="管控措施">
            {{ currentKnowledge.controlMeasures }}
          <el-descriptions-item label="培训开始时间:">
            {{ currentKnowledge.trainingDate + ' ' + currentKnowledge.openingTime }}
          </el-descriptions-item>
          <el-descriptions-item label="库存数量">
            {{ currentKnowledge.stockQty }}
          <el-descriptions-item label="培训结束时间:">
            {{ currentKnowledge.trainingDate + ' ' + currentKnowledge.endTime }}
          </el-descriptions-item>
          <el-descriptions-item label="管控责任人">
            {{ currentKnowledge.principalUserId }}
          <el-descriptions-item label="培训目标:">
            {{ currentKnowledge.trainingObjectives }}
          </el-descriptions-item>
          <el-descriptions-item label="责任人联系电话">
            {{ currentKnowledge.principalMobile }}
          <el-descriptions-item label="参加对象:">
            {{ currentKnowledge.participants }}
          </el-descriptions-item>
          <el-descriptions-item label="风险等级">
            <el-tag :type="getTypeTagType(currentKnowledge.riskLevel)">
              {{ currentKnowledge.riskLevel }}
          <el-descriptions-item label="培训方式:">
            <el-tag type="primary">
              {{ getTrainingModeLabel(currentKnowledge.trainingMode) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="规格 / é£Žé™©æè¿°">
            {{ currentKnowledge.specInfo }}
          <el-descriptions-item label="培训地点:">
            {{ currentKnowledge.placeTraining }}
          </el-descriptions-item>
          <el-descriptions-item label="课时:">
            {{ currentKnowledge.classHour }}
          </el-descriptions-item>
          <el-descriptions-item label="课程学分:">
            {{ currentKnowledge.projectCredits }}
          </el-descriptions-item>
          <el-descriptions-item label="报名人数:">
            {{ currentKnowledge.nums }}
          </el-descriptions-item>
          <el-descriptions-item label="附件列表:">
            <el-button type="primary"
                       size="small"
                       @click="downLoadFile(endform)">附件列表</el-button>
          </el-descriptions-item>
        </el-descriptions>
        <!-- <el-divider style="margin: 20px 0;" /> -->
        <div class="classtitle"
             style="margin-top: 40px;margin-bottom: 30px;">课程评价</div>
        <el-form ref="formRef"
                 :model="form"
                 :rules="rules"
                 label-position="top"
                 label-width="150px">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="评价人:"
                            prop="courseCode">
                <el-input v-model="endform.assessmentUserName"
                          disabled
                          placeholder="请选择评价人" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="评价时间:"
                            prop="trainingDate">
                <el-date-picker style="width: 100%"
                                disabled
                                v-model="endform.assessmentDate"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                type="date"
                                placeholder="请选择"
                                clearable />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="考核方式:"
                            prop="assessmentMethod">
                <el-input v-model="endform.assessmentMethod"
                          placeholder="请选择考核方式" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="本次课程综合评价:"
                            prop="comprehensiveAssessment">
                <el-input v-model="endform.comprehensiveAssessment"
                          placeholder="请输入本次课程综合评价" />
              </el-form-item>
            </el-col>
          </el-row>
          <el-form-item label="培训摘要:"
                        prop="trainingAbstract">
            <el-input v-model="endform.trainingAbstract"
                      type="textarea"
                      :rows="2"
                      placeholder="请输入培训摘要" />
          </el-form-item>
          <!-- <el-row :gutter="30">
            <el-col :span="24">
              <el-form-item label="附件材料:"
                            prop="remark">
                <el-upload v-model:file-list="fileList"
                           :action="upload.url"
                           multiple
                           ref="fileUpload"
                           auto-upload
                           :headers="upload.headers"
                           :before-upload="handleBeforeUpload"
                           :on-error="handleUploadError"
                           :on-success="handleUploadSuccess"
                           :on-remove="handleRemove">
                  <el-button type="primary"
                             v-if="operationType !== 'view'">上传</el-button>
                  <template #tip
                            v-if="operationType !== 'view'">
                    <div class="el-upload__tip">
                      æ–‡ä»¶æ ¼å¼æ”¯æŒ
                      doc,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                    </div>
                  </template>
                </el-upload>
              </el-form-item>
            </el-col>
          </el-row> -->
        </el-form>
        <div class="classtitle"
             style="margin-top: 40px;">考核列表</div>
        <el-table style="margin-top: 20px;"
                  :data="endform.safeTrainingDetailsDtoList"
                  border
                  fit
                  stripe
                  highlight-current-row>
          <el-table-column prop="nickName"
                           label="姓名" />
          <el-table-column prop="phonenumber"
                           label="电话号码" />
          <el-table-column prop="examinationResults"
                           label="考核结果">
            <template #default="scope">
              <el-select v-model="scope.row.examinationResults"
                         placeholder="请选择考核结果">
                <el-option label="合格"
                           value="合格" />
                <el-option label="不合格"
                           value="不合格" />
              </el-select>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm2">提交</el-button>
          <el-button @click="viewDialogVisible = false">关闭</el-button>
        </span>
      </template>
@@ -265,7 +393,13 @@
    safeTrainingFileListPage,
    safeTrainingFileAdd,
    safeTrainingFileDel,
    safeTrainingSign,
    safeTrainingGet,
    safeTrainingSave,
  } from "@/api/safeProduction/safetyTrainingAssessment.js";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  const userStore = useUserStore();
  // è¡¨å•验证规则
  const rules = {
@@ -284,12 +418,17 @@
    ],
    classHour: [{ required: true, message: "请输入课时", trigger: "blur" }],
  };
  const upload = reactive({
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
  });
  // å“åº”式数据
  const data = reactive({
    searchForm: {
      name: "",
      type: "",
      trainingDate: "",
      state: 0,
    },
    tableLoading: false,
    page: {
@@ -342,6 +481,13 @@
    );
    return item ? item.label : val;
  };
  // åˆ‡æ¢tab查询
  const tabhandleQuery = val => {
    searchForm.value.state = val.paneName;
    console.log(searchForm.value.state, "searchForm.value.state");
    handleQuery();
  };
  // è¡¨å•引用
  const formRef = ref();
  const riskLevelOptions = ref([
@@ -351,56 +497,69 @@
    { value: "重大风险", label: "重大风险" },
  ]);
  const fileList = ref([]);
  // è¡¨æ ¼åˆ—配置
  const tableColumn = ref([
    {
      label: "课程编号",
      prop: "courseCode",
      width: 150,
      showOverflowTooltip: true,
    },
    {
      label: "培训日期",
      prop: "trainingDate",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "开始时间",
      prop: "openingTime",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "结束时间",
      prop: "endTime",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "培训目标",
      prop: "trainingObjectives",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "参加对象",
      prop: "participants",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "培训内容",
      prop: "trainingContent",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "培训讲师",
      prop: "trainingLecturer",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "项目学分",
      prop: "projectCredits",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "培训方式",
      prop: "trainingMode",
      width: 120,
      showOverflowTooltip: true,
      formatData: params => {
        return getTrainingModeLabel(params);
@@ -409,11 +568,19 @@
    {
      label: "培训地点",
      prop: "placeTraining",
      width: 200,
      showOverflowTooltip: true,
    },
    {
      label: "课时",
      prop: "classHour",
      width: 120,
      showOverflowTooltip: true,
    },
    {
      label: "报名人数",
      prop: "nums",
      width: 120,
      showOverflowTooltip: true,
    },
    {
@@ -421,27 +588,47 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      width: 300,
      operation: [
        {
          name: "签到",
          type: "text",
          disabled: row => row.state !== 1,
          clickFun: row => {
            signIn(row);
          },
        },
        {
          name: "编辑",
          type: "text",
          disabled: row => row.state !== 0,
          clickFun: row => {
            openForm("edit", row);
          },
        },
        {
          name: "导出",
          type: "danger",
          type: "text",
          clickFun: row => {
            exportKnowledge(row);
          },
          color: "#C49000",
        },
        {
          name: "附件",
          type: "danger",
          type: "text",
          clickFun: row => {
            downLoadFile(row);
          },
          color: "#007AFF",
        },
        {
          name: "结果明细",
          type: "text",
          // disabled: row => row.state !== 2,
          clickFun: row => {
            viewResultDetail(row);
          },
        },
        // {
@@ -457,12 +644,133 @@
  const userList = ref([]);
  // ç”Ÿå‘½å‘¨æœŸ
  onMounted(() => {
    getCurrentFactoryName();
    getList();
    startAutoRefresh();
    userListNoPage().then(res => {
      userList.value = res.data;
    });
  });
  const endform = ref({
    assessmentUserId: "", //评价人
    assessmentUserName: "", //评价人姓名
    assessmentMethod: "", //考核方式
    assessmentDate: "", //评价时间
    comprehensiveAssessment: "", //综合评价
    trainingAbstract: "", //培训摘要
    safeTrainingFileList: [], //培训附件
    safeTrainingDetailsDtoList: [], //考核结果详情
  });
  const operationType = ref("edit");
  const viewResultDetail = row => {
    // fileList.value = [];
    operationType.value = "edit";
    safeTrainingGet({ id: row.id }).then(res => {
      if (res.code === 200) {
        console.log(res.data, "res.data");
        currentKnowledge.value = JSON.parse(JSON.stringify(res.data));
        currentKnowledge.value.nums = row.nums;
        viewDialogVisible.value = true;
        endform.value = { ...res.data };
        endform.value.assessmentUserName = endform.value.assessmentUserName
          ? endform.value.assessmentUserName
          : currentUserName.value;
        endform.value.assessmentUserId = endform.value.assessmentUserId
          ? endform.value.assessmentUserId
          : currentUserId.value;
        endform.value.assessmentDate = dayjs().format("YYYY-MM-DD");
      } else {
        proxy.$modal.msgError(res.msg || "查询详情失败");
      }
    });
  };
  // ä¸Šä¼ å‰æ ¡æ£€
  function handleBeforeUpload(file) {
    proxy.$modal.loading("正在上传文件,请稍候...");
    return true;
  }
  // ä¸Šä¼ å¤±è´¥
  function handleUploadError(err) {
    proxy.$modal.msgError("上传文件失败");
    proxy.$modal.closeLoading();
  }
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  function handleUploadSuccess(res, file, uploadFiles) {
    proxy.$modal.closeLoading();
    if (res.code === 200) {
      // ç¡®ä¿ tempFileIds å­˜åœ¨ä¸”为数组
      if (!endform.value.safeTrainingFileList) {
        endform.value.safeTrainingFileList = [];
      }
      endform.value.safeTrainingFileList.push({
        id: res.data.tempId,
        fileName: res.data.originalName,
        url: res.data.tempPath,
        safeTrainingId: currentKnowledge.value.id,
      });
      proxy.$modal.msgSuccess("上传成功");
    } else {
      proxy.$modal.msgError(res.msg);
      proxy.$refs.fileUpload.handleRemove(file);
    }
  }
  // ç§»é™¤æ–‡ä»¶
  function handleRemove(file) {
    if (operationType.value === "edit") {
      let index = endform.value.safeTrainingFileList.findIndex(
        item => item.fileName === file.name
      );
      if (index !== -1) {
        endform.value.safeTrainingFileList.splice(index, 1);
      }
    }
  }
  const submitForm2 = () => {
    endform.value.safeTrainingDetailsDtoList.forEach((item, index) => {
      if (!item.examinationResults) {
        proxy.$modal.msgError(`请选择${item.nickName}的考核结果`);
        return;
      }
    });
    console.log(endform.value, "endform.value");
    proxy.$modal.loading("正在提交,请稍候...");
    safeTrainingSave(endform.value).then(res => {
      proxy.$modal.closeLoading();
      if (res.code === 200) {
        proxy.$modal.msgSuccess("提交成功");
        getList();
        viewDialogVisible.value = false;
      } else {
        proxy.$modal.msgError(res.msg || "提交失败");
      }
    });
  };
  const opendetail = row => {
    proxy.$router.push({
      path: "/safeProduction/safetyTrainingAssessmentDetail",
    });
  };
  const signIn = row => {
    ElMessageBox.confirm("确认签到吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      safeTrainingSign({
        safeTrainingId: row.id,
        userId: currentUserId.value,
      }).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess("签到成功");
          getList();
        } else {
          proxy.$modal.msgError(res.msg || "签到失败");
        }
      });
    });
  };
  // å¤„理用户选择变化
  const handleUserChange = userId => {
@@ -627,7 +935,7 @@
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = `培训记录_${row.courseCode}.xlsx`;
        link.download = `培训记录_${row.courseCode}.docx`;
        // æ¨¡æ‹Ÿç‚¹å‡»ä¸‹è½½
        document.body.appendChild(link);
@@ -670,6 +978,13 @@
  const handleSelectionChange = selection => {
    selectedIds.value = selection.map(item => item.id);
  };
  const currentUserId = ref("");
  const currentUserName = ref("");
  const getCurrentFactoryName = async () => {
    let res = await userStore.getInfo();
    currentUserId.value = res.user.userId;
    currentUserName.value = res.user.nickName;
  };
  // æ‰“开表单
  const openForm = (type, row = null) => {
@@ -710,12 +1025,6 @@
      });
    }
    dialogVisible.value = true;
  };
  // æŸ¥çœ‹åŸ¹è®­è¯¦æƒ…
  const viewKnowledge = row => {
    currentKnowledge.value = { ...row };
    viewDialogVisible.value = true;
  };
  // èŽ·å–ç±»åž‹æ ‡ç­¾ç±»åž‹
@@ -936,4 +1245,12 @@
  :deep(.danger-row td) {
    color: #e95a66 !important;
  }
  .classtitle {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    border-left: 4px solid #409eff;
    padding-left: 12px;
    margin-bottom: 12px;
  }
</style>