spring
12 小时以前 6c7375701b519377752df5da89e8c3910c1661d8
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
已修改7个文件
382 ■■■■■ 文件已修改
src/api/viewIndex.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/viewIndex.js
@@ -60,6 +60,57 @@
export const getWorkInProcessTurnover= ()=>{
    return request({
        url: '/home/workInProcessTurnover',
        method: 'get'
    })
}
// 客户营收贡献数值分析
export const customerRevenueAnalysis = (params) => {
    return request({
        url: '/home/customerRevenueAnalysis',
        method: 'get',
        params
    })
}
// 员工-客户-供应商总数
export const summaryStatistics = () => {
    return request({
        url: '/home/summaryStatistics',
        method: 'get'
    })
}
// 各部门人员分布
export const deptStaffDistribution = () => {
    return request({
        url: '/home/deptStaffDistribution',
        method: 'get'
    })
}
// 供应商采购排名
export const supplierPurchaseRanking = (query) => {
    return request({
        url: '/home/supplierPurchaseRanking',
        method: 'get',
        params: query
    })
}
// 客户金额贡献排名
export const customerContributionRanking = (query) => {
    return request({
        url: '/home/customerContributionRanking',
        method: 'get',
        params: query
    })
}
// 各产品大类分布
export const productCategoryDistribution = () => {
    return request({
        url: '/home/productCategoryDistribution',
        method: 'get'
    })
}
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
@@ -3,6 +3,7 @@
    <PanelHeader title="人员分布" />
    <div class="main-panel panel-item-customers">
      <Echarts
        ref="echartsRef"
        :chartStyle="chartStyle"
        :legend="pieLegend"
        :series="pieSeries"
@@ -16,18 +17,16 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { getProgressStatistics } from '@/api/viewIndex.js'
import { ref, onMounted, computed } from 'vue'
import { deptStaffDistribution } from '@/api/viewIndex.js'
import PanelHeader from '../PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
/**
 * @introduction 把数组中key值相同的那一项提取出来,组成一个对象
 * @description 详细描述
 * @param {参数类型} array 传入的数组 [{a:"1",b:"2"},{a:"2",b:"3"}]
 * @param {参数类型} key  属性名 a
 * @return {返回类型说明}
 * @exception [违例类型] [违例类型说明]
 */
function array2obj(array, key) {
  const resObj = {}
@@ -42,33 +41,32 @@
  height: '100%',
}
// 饼图数据(示例)
const pieDatas = [
  { value: 335, name: '进入区域', percent: '10' },
  { value: 310, name: '区域入侵', percent: '40' },
  { value: 274, name: '人员聚集', percent: '30' },
  { value: 235, name: '越界侦测', percent: '20' },
]
const echartsRef = ref(null)
const pieDatas = ref([])
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
const pieLegendData = pieDatas.map((d, idx) => ({
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],
      color: pieColors[idx % pieColors.length],
  },
}))
const pieObjData = array2obj(pieDatas, 'name')
const pieLegend = {
  return {
  orient: 'vertical',
  top: 'center',
  left: '50%',
  itemGap: 30,
  data: pieLegendData,
    data: data,
  formatter: function (name) {
    return `{title|${name}}{value|${pieObjData[name].value}}{unit|人}{percent|${pieObjData[name].percent}}{unit|%}`
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|人}{percent|${item.rate}}{unit|%}`
  },
  textStyle: {
    rich: {
@@ -97,28 +95,28 @@
    },
  },
}
})
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c} ({d}%)',
}
const pieSeries = ref([
const pieSeries = computed(() => [
  {
    name: '访问来源',
    name: '人员分布',
    type: 'pie',
    radius: '70%',
    center: ['20%', '50%'],
    itemStyle: {
      // 给每个扇区加分隔缝隙,颜色取深色背景
      borderColor: '#0a1c3a',
      borderWidth: 4,
      borderWidth: 2,
    },
    label: {
      show: false
    },
    data: pieDatas,
    roseType: 'radius',
    minAngle: 15,
    data: pieDatas.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
@@ -131,6 +129,24 @@
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const getDeptStaffDistribution = () => {
  deptStaffDistribution().then(res => {
    if (res.code === 200) {
      pieDatas.value = res.data.items.map(item => ({
        name: item.name,
        value: parseInt(item.value),
        rate: item.rate
      }))
    }
  }).catch(err => {
    console.error('获取部门人员分布数据失败:', err)
  })
}
onMounted(() => {
  getDeptStaffDistribution()
})
</script>
<style scoped>
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
@@ -95,7 +95,7 @@
            <div class="todo-division">待办事由:{{ item.approveReason }}</div>
              <div style="display: flex;justify-content: space-between;align-items: center;"
              >
                <div class="todo-title">申请类型:{{ item.approveId }}</div>
                <div class="todo-title">申请类型:{{ item.approveTypeName }}</div>
                <div class="todo-division">申请部门:{{ item.approveDeptName }}</div>
                <div class="todo-time">{{ item.approveTime }}</div>
              </div>
@@ -111,10 +111,7 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { homeTodos } from '@/api/viewIndex.js'
import { staffOnJobListPage } from '@/api/personnelManagement/employeeRecord.js'
import { listCustomer } from '@/api/basicData/customerFile.js'
import { listSupplier } from '@/api/basicData/supplierManageFile.js'
import { homeTodos, summaryStatistics } from '@/api/viewIndex.js'
import { getLedgerPage } from '@/api/equipmentManagement/ledger.js'
import { getRepairPage } from '@/api/equipmentManagement/repair.js'
import { getUpkeepPage } from '@/api/equipmentManagement/upkeep.js'
@@ -124,10 +121,10 @@
const totalStaff = ref(0)
const totalCustomers = ref(0)
const totalSuppliers = ref(0)
// 同比(占位值,可接入真实接口)
const staffYoY = ref(-0.52) // 示例:-0.52%
const customersYoY = ref(0.82) // 示例:0.82%
const suppliersYoY = ref(0.1) // 示例:0.10%
// 同比
const staffYoY = ref(0)
const customersYoY = ref(0)
const suppliersYoY = ref(0)
const equipmentNum = ref(0)
const equipmentRepair = ref(0)
const equipmentMaintain = ref(0)
@@ -146,18 +143,15 @@
// 获取员工、客户、供应商数量
const getNum = () => {
  const params = {
    pageNum: -1,
    pageSize: -1,
  }
  staffOnJobListPage({ ...params, staffState: 1 }).then((res) => {
    totalStaff.value = res.data.total
  })
  listCustomer(params).then((res) => {
    totalCustomers.value = res.total
  })
  listSupplier(params).then((res) => {
    totalSuppliers.value = res.data.total
  summaryStatistics().then((res) => {
    totalStaff.value = res.data.totalStaff
    staffYoY.value = res.data.staffGrowthRate
    totalCustomers.value = res.data.totalCustomer
    customersYoY.value = res.data.customerGrowthRate
    totalSuppliers.value = res.data.totalSupplier
    suppliersYoY.value = res.data.supplierGrowthRate
  }).catch(err => {
    console.error('获取基础统计数据失败:', err)
  })
}
@@ -348,6 +342,11 @@
  color: #d0e7ff;
}
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
}
.compare-value {
  font-weight: 600;
}
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
@@ -42,18 +42,16 @@
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { qualityStatistics } from '@/api/viewIndex.js'
import { customerRevenueAnalysis } from '@/api/viewIndex.js'
import { listCustomer } from '@/api/basicData/customerFile.js'
const dateType = ref(1) // 1=周 2=月 3=季度
const customerValue = ref(null)
const customerOptions = ref([])
// 质检统计对象
const qualityStatisticsObject = ref({
  supplierNum: 0,
  processNum: 0,
  factoryNum: 0,
// 营收分析数据
const revenueData = ref({
  items: []
})
const chartStyle = {
@@ -130,25 +128,34 @@
  },
]
// 质检统计
const qualityStatisticsInfo = () => {
  qualityStatistics()
// 获取客户营收分析数据
const getCustomerRevenueAnalysis = () => {
  if (customerOptions.value.length > 0 && !customerValue.value) {
    // 默认选中第一个客户
    customerValue.value = customerOptions.value[0].value
  }
  if (!customerValue.value) return
  const params = {
    customerId: customerValue.value,
    type: dateType.value
  }
  customerRevenueAnalysis(params)
    .then((res) => {
      // 切换筛选条件时,先清空再填充,避免重复 push
      xAxis1.value[0].data = []
      barSeries1.value[0].data = []
      res.data.item.forEach((item) => {
        xAxis1.value[0].data.push(item.date)
        // 这里暂用 supplierNum 作为柱状图数值(接口返回里当前也有这三个字段)
        barSeries1.value[0].data.push(item.supplierNum)
      const items = res.data?.items || []
      items.forEach((item) => {
        xAxis1.value[0].data.push(item.name)
        barSeries1.value[0].data.push(item.value)
      })
      qualityStatisticsObject.value.supplierNum = res.data.supplierNum
      qualityStatisticsObject.value.processNum = res.data.processNum
      qualityStatisticsObject.value.factoryNum = res.data.factoryNum
      revenueData.value = res.data
    })
    .catch((error) => {
      console.error('获取质检统计失败:', error)
      console.error('获取客户营收分析失败:', error)
    })
}
@@ -161,6 +168,12 @@
      label: r.customerName || r.name || r.customer || '-',
      value: r.id ?? r.customerId ?? r.customerCode ?? r.customerName,
    }))
    // 获取到选项后,如果还没选中,默认选中第一个
    if (customerOptions.value.length > 0 && !customerValue.value) {
      customerValue.value = customerOptions.value[0].value
      getCustomerRevenueAnalysis()
    }
  } catch (e) {
    // 接口异常时给一组模拟客户,保证UI可用
    customerOptions.value = [
@@ -174,14 +187,11 @@
}
const handleFilterChange = () => {
  // 目前 qualityStatistics 接口未携带筛选参数,这里先统一触发刷新,避免重复数据
  // 若后端后续支持 customerId/type,可在 qualityStatistics() 处改为传参
  qualityStatisticsInfo()
  getCustomerRevenueAnalysis()
}
onMounted(() => {
  fetchCustomerOptions()
  qualityStatisticsInfo()
})
</script>
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -3,6 +3,7 @@
    <PanelHeader title="产品大类" />
    <div class="panel-item-customers">
      <div style="height: 70%">
      <div style="height: 100%">
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -27,6 +28,7 @@
// 数据列表(来自接口)
const dataList = ref([])
import { productCategoryDistribution } from '@/api/viewIndex.js'
// 颜色列表
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
@@ -45,6 +47,29 @@
// 图例配置(右侧竖排)
const landLegend = {
// 在制品工序柱状图配置
const workInProcessXAxis = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: {
      color: '#B8C8E0',
      interval: 0,
      rotate: 25
    },
    data: [],
  },
])
const workInProcessYAxis = [
  {
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    name: '',
  },
]
const workInProcessBarLegend = {
  show: false,
  icon: 'circle',
  data: [],
@@ -128,6 +153,12 @@
      lineStyle: {
        color: '#B8C8E0',
      },
    name: '产品数量',
    type: 'bar',
    barWidth: 25,
    barGap: 0,
    emphasis: {
      focus: 'series',
    },
    itemStyle: {
      color: function (params) {
@@ -143,6 +174,19 @@
    radius: ['35%', '40%'],
    center: ['50%', '50%'],
    silent: true,
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 0, color: '#4EE4FF' },
          { offset: 1, color: '#00A4ED' },
        ],
      },
      borderRadius: [4, 4, 0, 0]
    },
    label: {
      show: false,
    },
@@ -159,6 +203,7 @@
const chartStyle = {
  width: '100%',
  height: '150%',
  height: '100%',
}
const loadData = async () => {
@@ -179,10 +224,44 @@
    landLegend.data = []
    landSeries.value[0].data = []
  }
const grid = {
  left: '3%',
  right: '4%',
  bottom: '15%',
  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 getProductCategoryDistribution = () => {
  productCategoryDistribution()
    .then((res) => {
      if (res.code === 200 && res.data && res.data.items) {
        workInProcessXAxis.value[0].data = res.data.items.map(item => item.name)
        workInProcessBarSeries.value[0].data = res.data.items.map(item => parseInt(item.value))
      }
    })
    .catch((error) => {
      console.error('获取各产品大类分布失败:', error)
    })
}
onMounted(() => {
  loadData()
  getProductCategoryDistribution()
})
</script>
@@ -192,47 +271,5 @@
  padding: 18px;
  width: 100%;
  height: 420px;
}
.quality-cards {
  display: flex;
  gap: 12px;
  width: 100%;
  height: 54px;
  justify-content: space-between;
  align-items: center;
}
.quality-cardSec {
  display: flex;
}
.quality-cardTitle {
  font-weight: 400;
  font-size: 14px;
  color: #ffffff;
  display: flex;
  align-items: flex-start;
  flex-direction: column;
}
.quality-card {
  width: 80px;
  height: 60px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.quality-card.one {
  background-image: url('@/assets/BI/yuancailiaoyijianicon@2x.png');
}
.quality-card.two {
  background-image: url('@/assets/BI/guochengyijianicon@2x.png');
}
.quality-card.three {
  background-image: url('@/assets/BI/chuchangyijianicon@2x.png');
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -26,6 +26,7 @@
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerContributionRanking } from '@/api/viewIndex.js'
const chartStyle = {
  width: '100%',
@@ -49,7 +50,6 @@
// 原始数据(统一成 { NAME, NUM })
const dataArr = ref([])
// 仅保留金额最高的 5 条(并按从小到大展示,视觉上最高在最下方)
const dataArray = computed(() => {
  const sortedAsc = [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
  return sortedAsc.length > 5 ? sortedAsc.slice(-5) : sortedAsc
@@ -183,6 +183,7 @@
    z: 6,
    type: 'bar',
    barWidth: 25,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(255,255,255,.1)',
      barBorderRadius: [0, 20, 20, 0],
@@ -194,6 +195,7 @@
    type: 'bar',
    barWidth: 25,
    barGap: '-100%',
    tooltip: { show: false },
    itemStyle: {
      color: {
        type: 'linear',
@@ -274,12 +276,30 @@
  dataArr.value = getMockListByType(type).map(normalizeItem)
}
const handleDateTypeChange = () => {
const fetchCustomerRanking = () => {
  customerContributionRanking({ type: dateType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
          NAME: item.customerName,
          NUM: item.totalAmount
        }))
      } else {
  setMockData(dateType.value)
      }
    })
    .catch((error) => {
      console.error('获取客户金额贡献排名失败:', error)
      setMockData(dateType.value)
    })
}
const handleDateTypeChange = () => {
  fetchCustomerRanking()
}
onMounted(() => {
  setMockData(dateType.value)
  fetchCustomerRanking()
})
</script>
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -25,7 +25,7 @@
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { statisticsReceivablePayable } from '@/api/viewIndex.js'
import { supplierPurchaseRanking } from '@/api/viewIndex.js'
const chartStyle = {
  width: '100%',
@@ -242,6 +242,7 @@
      z: 6,
      type: 'bar',
      barWidth: 25,
      tooltip: { show: false },
      itemStyle: {
        color: 'rgba(255,255,255,.1)',
        barBorderRadius: [0, 20, 20, 0],
@@ -252,6 +253,7 @@
      type: 'bar',
      barWidth: 25,
      barGap: '-100%',
      tooltip: { show: false },
      itemStyle: {
        color: {
          type: 'linear',
@@ -282,16 +284,15 @@
  ]
})
// 应付应收统计
const statisticsReceivable = () => {
  statisticsReceivablePayable({ type: radio1.value })
// 供应商采购排名
const fetchSupplierRanking = () => {
  supplierPurchaseRanking({ type: radio1.value })
    .then((res) => {
      // 假设 API 返回的数据格式为数组,包含 NAME 和 NUM 字段
      // 如果返回格式不同,需要根据实际 API 调整
      if (res.data && Array.isArray(res.data)) {
        dataArr.value = res.data
      } else if (res.data && res.data.list) {
        dataArr.value = res.data.list
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
          NAME: item.supplierName,
          NUM: item.totalAmount
        }))
      } else {
        // 如果没有数据,使用模拟数据
        dataArr.value = [
@@ -318,11 +319,11 @@
// 处理日期类型切换
const handleDateTypeChange = (value) => {
  statisticsReceivable()
  fetchSupplierRanking()
}
onMounted(() => {
  statisticsReceivable()
  fetchSupplierRanking()
})
</script>