5747cef7e826014682a02cd00da8769e1fa1e65a..0152b0200faff12f0ece317cdb32dcbcf486757d
9 天以前 yaowanxin
添加库存核算统计页面,调整固定资产页面图表
0152b0 对比 | 目录
9 天以前 yaowanxin
固定资产模块界面设备台账统计图表,设备列表
8fd61b 对比 | 目录
已添加1个文件
已修改3个文件
1116 ■■■■ 文件已修改
src/api/equipmentManagement/ledger.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockIn.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/accounting/index.vue 710 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/inventoryAccounting/index.vue 390 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/ledger.js
@@ -42,3 +42,11 @@
    method: "get",
  });
};
// è®¾å¤‡å°è´¦å›¾è¡¨
export const getAssetInfo = (params) => {
    return request({
        url: "/device/ledger/report/forms",
        method: "get",
        params
    });
}
src/api/inventoryManagement/stockIn.js
@@ -64,5 +64,11 @@
}
//
//查询库存图表数据
export function getStockInChartData() {
    return request({
        url: '/stockin/listReport',
        method: 'get'
    })
}
src/views/financialManagement/accounting/index.vue
@@ -1,231 +1,547 @@
<template>
  <div class="app-container">
    <el-row :gutter="16" class="mb-16">
      <el-col :span="12">
        <el-card shadow="hover">
          <div class="section-title">统一会计科目体系</div>
          <el-tree
            :data="accountTree"
            node-key="code"
            :props="{ label: 'label', children: 'children' }"
            highlight-current
            default-expand-all
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card shadow="hover">
          <div class="section-title">凭证模板</div>
          <el-table :data="voucherTemplates" border size="small">
            <el-table-column prop="name" label="模板名称" min-width="140" />
            <el-table-column prop="bizScene" label="业务场景" min-width="140" />
            <el-table-column prop="debit" label="借方科目" min-width="160" />
            <el-table-column prop="credit" label="贷方科目" min-width="160" />
            <el-table-column prop="auxDims" label="辅助核算维度" min-width="180">
              <template #default="scope">
                <el-space wrap>
                  <el-tag v-for="dim in scope.row.auxDims" :key="dim" size="small" type="info">{{ dim }}</el-tag>
                </el-space>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>
  <div style="padding: 20px;">
    <!-- é¡µé¢æ ‡é¢˜å’Œç­›é€‰æ¡ä»¶ -->
    <div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;">
      <el-button
        type="primary"
        icon="Refresh"
        @click="resetFilters"
        size="default"
      >
        æŸ¥è¯¢
      </el-button>
    </div>
    <el-row :gutter="16" class="mb-16">
      <el-col :span="12">
        <el-card shadow="hover">
          <div class="section-title">业务流程 â†’ å‡­è¯è‡ªåŠ¨ç”Ÿæˆ</div>
          <div class="toolbar">
            <el-text type="info">演示数据仅用于展示结构与字段</el-text>
          </div>
          <el-table :data="generatedVouchers" border size="small">
            <el-table-column prop="date" label="日期" width="110" />
            <el-table-column prop="bizScene" label="业务场景" min-width="120" />
            <el-table-column prop="summary" label="摘要" min-width="160" />
            <el-table-column prop="amount" label="金额(Â¥)" width="110" />
            <el-table-column prop="status" label="状态" width="100">
              <template #default="scope">
                <el-tag :type="scope.row.status === '已生成' ? 'success' : 'warning'">{{ scope.row.status }}</el-tag>
              </template>
            </el-table-column>
          </el-table>
    <main class="container mx-auto px-4 pb-10">
      <!-- å›ºå®šèµ„产指标卡片 -->
      <div class="grid-container">
        <!-- è®¾å¤‡æ€»æ•° -->
        <el-card class="bg2">
          <p>设备总数</p>
          <h3>
            {{ assetInfo.totalEquipment }}
          </h3>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card shadow="hover">
          <div class="section-title">多维辅助核算</div>
          <div class="dims">
            <el-tag type="success">客户</el-tag>
            <el-tag type="warning">项目</el-tag>
            <el-tag type="info">部门</el-tag>
            <el-tag type="primary">管理员</el-tag>
          </div>
          <el-table :data="auxSummary" size="small" border>
            <el-table-column prop="dimension" label="维度" width="100" />
            <el-table-column prop="category" label="类别" min-width="140" />
            <el-table-column prop="debit" label="借方(本期)" width="110" />
            <el-table-column prop="credit" label="贷方(本期)" width="110" />
            <el-table-column prop="balance" label="余额" width="100" />
          </el-table>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="16" class="mb-16">
      <el-col :span="12">
        <el-card shadow="hover">
          <div class="section-title">结账任务(月/å­£/年)</div>
          <div class="toolbar">
            <el-space>
              <el-button size="small" @click="runClose('月结')">执行月结</el-button>
              <el-button size="small" @click="runClose('季报')">执行季报</el-button>
              <el-button size="small" @click="runClose('年度结账')">执行年度结账</el-button>
            </el-space>
          </div>
          <el-timeline style="margin-top: 6px;">
            <el-timeline-item
              v-for="item in closingTasks"
              :key="item.id"
              :type="item.type"
              :timestamp="item.time"
              placement="top"
            >
              <div class="close-item">
                <div class="title">{{ item.name }}</div>
                <el-tag :type="item.status === '完成' ? 'success' : 'info'" size="small">{{ item.status }}</el-tag>
        <!-- èµ„产原值 -->
        <el-card class="bg3">
          <p>资产原值</p>
          <h3>
            Â¥{{ assetInfo.totalOriginalValue }}
          </h3>
        </el-card>
        <!-- ç´¯è®¡æŠ˜æ—§ -->
        <el-card class="bg4">
          <p>累计折旧</p>
          <h3>
            Â¥{{ assetInfo.totalDepreciation }}
          </h3>
        </el-card>
        <!-- å‡€å€¼ -->
        <el-card class="bg5">
          <p>净值</p>
          <h3>
            Â¥{{ assetInfo.totalNetValue }}
          </h3>
        </el-card>
      </div>
      <!-- å›ºå®šèµ„产统计图表 -->
      <div class="grid-layout">
        <!-- æŒ‰è®¾å¤‡ç±»åž‹ç»Ÿè®¡ -->
        <el-card style="margin-bottom: 20px;">
          <h2 class="section-title">设备类型分布</h2>
          <div class="echarts">
            <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;">{{ assetInfo.totalEquipment }}</span>
              </div>
            </el-timeline-item>
          </el-timeline>
            </Echarts>
            <Echarts
                ref="chart"
                :chartStyle="chartStyle"
                :grid="grid"
                :legend="lineLegend"
                :series="typeDistributionLineSeries"
                :tooltip="tooltip"
                :xAxis="xAxis"
                :yAxis="yAxis"
                style="height: 260px; width: 64%;"></Echarts>
          </div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card shadow="hover">
          <div class="section-title">审计留痕与审批权限</div>
          <el-table :data="auditTrail" border size="small">
            <el-table-column prop="time" label="时间" width="160" />
            <el-table-column prop="action" label="动作" min-width="160" />
            <el-table-column prop="bizScene" label="关联业务" min-width="140" />
            <el-table-column prop="role" label="执行角色" width="120" />
            <el-table-column prop="result" label="结果" width="100">
              <template #default="scope">
                <el-tag :type="scope.row.result === '通过' ? 'success' : (scope.row.result === '驳回' ? 'danger' : 'info')">
                  {{ scope.row.result }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>
      </div>
      <!-- è®¾å¤‡å°è´¦è¡¨æ ¼ -->
      <el-card style="margin-bottom: 20px;">
        <el-table
          :data="equipmentList"
          stripe
          style="width: 100%"
          :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
        >
          <el-table-column prop="id" label="资产编号" width="120" />
          <el-table-column prop="deviceName" label="设备名称" width="250" />
          <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" />
          <el-table-column prop="originalValue" label="原值(元)" width="120">
            <template #default="{ row }">
              Â¥{{ formatCurrency(row.taxIncludingPriceTotal) }}
            </template>
          </el-table-column>
          <el-table-column prop="depreciation" label="累计折旧(元)" width="140">
            <template #default="{ row }">
              Â¥{{ formatCurrency(row.taxIncludingPriceTotal-row.unTaxIncludingPriceTotal) }}
            </template>
          </el-table-column>
          <el-table-column prop="netValue" label="净值(元)" width="120">
            <template #default="{ row }">
              Â¥{{ formatCurrency(row.unTaxIncludingPriceTotal) }}
            </template>
          </el-table-column>
          <el-table-column prop="status" label="状态" width="100">
            <template #default="{ row }">
              <el-tag
                :type="getStatusTagType(row.status)"
                size="small"
              >
                {{ row.status }}
              </el-tag>
            </template>
          </el-table-column>
        </el-table>
        <!-- åˆ†é¡µ -->
        <div class="pagination-container">
          <el-pagination
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :current-page="pagination.currentPage"
            :page-sizes="[10, 20, 50, 100]"
            :page-size="pagination.pageSize"
            layout="total, sizes, prev, pager, next, jumper"
            :total="pagination.total"
          />
        </div>
      </el-card>
    </main>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, onMounted, reactive } from 'vue';
import 'element-plus/dist/index.css';
import Echarts from "@/components/Echarts/echarts.vue";
import { getLedgerPage, getAssetInfo } from "@/api/equipmentManagement/ledger";
import dayjs from "dayjs";
// ç§‘目树(示例)
const accountTree = ref([
  { code: '1001', label: '资产', children: [
    { code: '100101', label: '库存现金' },
    { code: '100102', label: '银行存款' },
    { code: '1122', label: '应收账款' },
    { code: '1601', label: '固定资产' },
  ]},
  { code: '2001', label: '负债', children: [
    { code: '2202', label: '应付账款' },
    { code: '2241', label: '其他应付款' },
  ]},
  { code: '3001', label: '所有者权益', children: [
    { code: '3103', label: '本年利润' },
  ]},
  { code: '4001', label: '成本费用', children: [
    { code: '5601', label: '制造费用' },
    { code: '6602', label: '管理费用' },
  ]},
])
// ç­›é€‰æ¡ä»¶
const dateRange = ref(null);
const equipmentType = ref('');
// å‡­è¯æ¨¡æ¿ï¼ˆç¤ºä¾‹ï¼‰
const voucherTemplates = ref([
  { name: '销售收入确认', bizScene: '销售出库', debit: '1122 åº”收账款', credit: '6001 ä¸»è¥ä¸šåŠ¡æ”¶å…¥', auxDims: ['客户','项目'] },
  { name: '采购应付确认', bizScene: '采购入库', debit: '1403 åœ¨é€”物资', credit: '2202 åº”付账款', auxDims: ['项目','部门'] },
  { name: '费用报销', bizScene: '费用单', debit: '6602 ç®¡ç†è´¹ç”¨', credit: '1002 é“¶è¡Œå­˜æ¬¾', auxDims: ['部门'] },
  { name: '固定资产折旧', bizScene: '月末折旧', debit: '6602 ç®¡ç†è´¹ç”¨', credit: '1602 ç´¯è®¡æŠ˜æ—§', auxDims: ['部门'] },
])
// è‡ªåŠ¨ç”Ÿæˆçš„å‡­è¯ï¼ˆç¤ºä¾‹ï¼‰
const generatedVouchers = ref([
  { date: '2025-10-01', bizScene: '销售出库', summary: '确认应收与收入', amount: 128000, status: '已生成' },
  { date: '2025-10-03', bizScene: '采购入库', summary: '确认到货应付', amount: 56000, status: '已生成' },
  { date: '2025-10-05', bizScene: '费用单', summary: '办公费用报销', amount: 3200, status: '已生成' },
])
// å›ºå®šèµ„产信息
const assetInfo = ref({
  totalEquipment: 0,
  totalOriginalValue: 0,
  totalDepreciation: 0,
  totalNetValue: 0
});
// æ— æ¨¡æ‹Ÿç”Ÿæˆæ“ä½œï¼Œä»…展示静态示例数据
// è®¾å¤‡åˆ—表
const equipmentList = ref([]);
const pagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 0
});
// è¾…助核算示例汇总(无个人姓名,仅维度类别)
const auxSummary = ref([
  { dimension: '客户', category: '重点客户集合', debit: 320000, credit: 210000, balance: 110000 },
  { dimension: '项目', category: '项目A', debit: 150000, credit: 120000, balance: 30000 },
  { dimension: '部门', category: '运营中心', debit: 42000, credit: 18000, balance: 24000 },
  { dimension: '管理员', category: '系统角色', debit: 0, credit: 0, balance: 0 },
])
// å›¾è¡¨é…ç½®
const chartStyle = {
  width: '100%',
  height: '100%',
  position: 'relative',
};
// ç»“账任务
const closingTasks = ref([
  { id: 1, name: '2025å¹´10月 æœˆç»“', time: '2025-10-31 18:00', status: '完成', type: 'success' },
  { id: 2, name: '2025å¹´Q4 å­£æŠ¥', time: '2025-12-31 18:00', status: '计划', type: 'info' },
  { id: 3, name: '2025年度 å¹´åº¦ç»“è´¦', time: '2025-12-31 23:00', status: '计划', type: 'info' },
])
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  containLabel: true
};
function runClose(kind) {
  closingTasks.value.unshift({
    id: Date.now(),
    name: `${new Date().getFullYear()}å¹´${kind}`,
    time: new Date().toISOString().replace('T',' ').slice(0,16),
    status: '完成',
    type: 'success',
  })
}
const lineLegend = {
  show: false,
};
// å®¡è®¡ç•™ç—•(不含个人姓名,仅角色/机制)
const auditTrail = ref([
  { time: '2025-10-01 09:12', action: '销售出库触发凭证生成', bizScene: '销售出库', role: '系统自动化', result: '通过' },
  { time: '2025-10-03 14:20', action: '采购入库触发应付确认', bizScene: '采购入库', role: '系统自动化', result: '通过' },
  { time: '2025-10-05 10:03', action: '费用单审批', bizScene: '费用单', role: '财务审批', result: '通过' },
  { time: '2025-10-08 16:45', action: '凭证过账', bizScene: '总账', role: '会计审核', result: '通过' },
  { time: '2025-10-31 18:05', action: '月结完成并锁账', bizScene: '总账', role: '系统自动化', result: '通过' },
])
// æŠ˜çº¿å›¾æç¤ºæ¡†
const tooltip = reactive({
  trigger: 'axis',
  axisPointer: {
    type: 'line',
    lineStyle: { color: '#aaa' }
  },
  // è‡ªå®šä¹‰å†…容
  formatter: function (params) {
    if (!params || !params.length) return '';
    const axisLabel = params[0].axisValueLabel || params[0].axisValue || '';
    const rows = params
      .map(p => {
        const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`;
        return `${colorDot}${p.seriesName}: ${p.value}`;
      })
      .join('<br/>');
    return `<div>${axisLabel}</div><div>${rows}</div>`;
  }
});
const xAxis = ref([
  {
    type: 'category',
    axisTick: { show: true, alignWithLabel: true },
    data: [],
  },
]);
const yAxis = [
  {
    type: 'value',
    name: '数量/金额', // å·¦ä¾§yè½´
    position: 'left',
    min: 0,
    // åæ ‡è½´åç§°æ ·å¼
    nameTextStyle: {
      color: '#000',
      fontSize: 14,
    },
  }
];
const chartStylePie = {
  width: '100%',
  height: '100%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
};
const pieColors = ['#F04864', '#FACC14', '#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,
  }
}));
// é¥¼å›¾ç³»åˆ—
const typeDistributionSeries = computed(() => [
  {
    type: 'pie',
    radius: ['50%', '65%'],
    center: ['25%', '50%'],
    avoidLabelOverlap: false,
    itemStyle: {
      borderColor: '#fff',
      borderWidth: 2
    },
    label: {
      show: false
    },
    data: typeDistributionData.value,
    color: pieColors
  }
]);
// æŠ˜çº¿å›¾æ•°æ®
const typeDistributionLineSeries = ref([]);
// é¥¼å›¾æç¤ºæ¡†
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 equipmentTypeOptions = ref([]);
// èŽ·å–æ•°æ®
const fetchData = async () => {
  try {
    // èŽ·å–å›ºå®šèµ„äº§æ±‡æ€»ä¿¡æ¯
    const assetInfoRes = await getAssetInfo({
      startDate: dateRange.value ? dateRange.value[0] : null,
      endDate: dateRange.value ? dateRange.value[1] : null,
      equipmentType: equipmentType.value
    });
    if (assetInfoRes.code === 200) {
      assetInfo.value = assetInfoRes.data;
    }
    // èŽ·å–è®¾å¤‡åˆ—è¡¨
    const equipmentListRes = await getLedgerPage({
      current: pagination.value.currentPage,
      size: pagination.value.pageSize,
      startDate: dateRange.value ? dateRange.value[0] : null,
      endDate: dateRange.value ? dateRange.value[1] : null,
      equipmentType: equipmentType.value
    });
    if (equipmentListRes.code === 200) {
      equipmentList.value = equipmentListRes.data.records;
      pagination.value.total = equipmentListRes.data.total;
      // æ ¹æ® equipmentList æŒ‰ deviceName è¿›è¡Œåˆ†ç±»ç»Ÿè®¡
      const deviceNameMap = {};
      equipmentList.value.forEach(item => {
        const deviceName = item.deviceName;
        if (!deviceNameMap[deviceName]) {
          deviceNameMap[deviceName] = {
            name: deviceName,
            count: 0,
            totalValue: 0
          };
        }
        deviceNameMap[deviceName].count += item.number || 1; // å‡è®¾ number ä¸ºè®¾å¤‡æ•°é‡
        deviceNameMap[deviceName].totalValue += item.taxIncludingPriceTotal || 0; // ç´¯åŠ å«ç¨Žæ€»ä»·
      });
      // è½¬æ¢ä¸º typeDistributionData æ ¼å¼
      typeDistributionData.value = Object.values(deviceNameMap).map(item => ({
        name: item.name,
        value: item.count,
        count: item.count,
        amount: `Â¥${formatCurrency(item.totalValue)}`
      }));
      // æ›´æ–°x轴数据
      xAxis.value[0].data = typeDistributionData.value.map(item => item.name);
      // æž„建折线图数据
      typeDistributionLineSeries.value = [
        {
          name: '设备数量',
          type: 'line',
          data: typeDistributionData.value.map(item => item.count)
        }
      ];
    }
  } catch (error) {
    console.error('获取固定资产数据失败:', error);
  }
};
// åˆå§‹åŒ–
onMounted(() => {
  // èŽ·å–åˆ—è¡¨æ•°æ®
  fetchData();
});
// æ ¼å¼åŒ–货币
const formatCurrency = (value) => {
  if (!value) return '0.00';
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
// èŽ·å–çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStatusTagType = (status) => {
  switch (status) {
    case '在用':
      return 'success';
    case '闲置':
      return 'info';
    case '维修中':
      return 'warning';
    case '报废':
      return 'danger';
    default:
      return 'info';
  }
};
// é‡ç½®ç­›é€‰æ¡ä»¶
const resetFilters = () => {
  dateRange.value = null;
  equipmentType.value = '';
  fetchData();
};
// åˆ†é¡µå¤„理
const handleSizeChange = (size) => {
  pagination.value.pageSize = size;
  fetchData();
};
const handleCurrentChange = (page) => {
  pagination.value.currentPage = page;
  fetchData();
};
</script>
<style scoped lang="scss">
.app-container {
  padding: 16px;
/* åŸºç¡€æ ·å¼è¡¥å…… */
:root {
  --el-color-primary: #4f46e5;
}
.page-header {
  margin-bottom: 12px;
  h2 { margin: 0 0 6px 0; font-weight: 600; }
  p { margin: 0; color: #666; }
.el-card {
  position: relative;
  border-radius: 12px;
  padding: 14px 10px 10px 10px;
  box-shadow: 0 2px 8px #eee;
  :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;
  }
}
.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-5) */
@media (min-width: 1024px) {
  .grid-container {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}
/* å¡ç‰‡æ‚¬åœæ•ˆæžœå¢žå¼º */
.el-card:hover {
  transform: translateY(-2px);
}
.echarts {
  display: flex;
  justify-content: space-between;
}
/* å›¾è¡¨å®¹å™¨æ ·å¼ */
.el-chart {
  width: 100%;
  height: 100%;
}
.section-title {
  position: relative;
  font-size: 18px;
  color: #333;
  padding-left: 10px;
  margin-bottom: 10px;
  font-weight: 700;
}
.section-title::before {
  content: '';
  position: absolute;
  left: 0; top: 0.2em;
  width: 4px; height: 1.2em;
  background: #002FA7;
  left: 0;
  top: 0px;
  content: '';
  width: 4px;
  height: 18px;
  background-color: #002FA7;
  border-radius: 2px;
}
.mb-16 { margin-bottom: 16px; }
.toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.dims { display: flex; gap: 8px; margin-bottom: 10px; }
.close-item { display: flex; align-items: center; gap: 8px; }
.chart-num {
  position: absolute;
  z-index: 3;
  top: 92px;
  left: 92px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: center;
}
</style>
src/views/financialManagement/inventoryAccounting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,390 @@
<template>
  <div class="inventory-statistics">
    <!-- ç­›é€‰è¡¨å• -->
    <div class="filter-form">
      <el-form :model="filterForm" inline>
<!--        <el-form-item label="时间范围">-->
<!--          <el-date-picker-->
<!--            v-model="filterForm.dateRange"-->
<!--            type="daterange"-->
<!--            range-separator="至"-->
<!--            start-placeholder="开始日期"-->
<!--            end-placeholder="结束日期"-->
<!--          />-->
<!--        </el-form-item>-->
<!--        <el-form-item label="供应商名称">-->
<!--          <el-input v-model="filterForm.supplierName" style="width: 240px" placeholder="请输入" clearable prefix-icon="Search" />-->
<!--        </el-form-item>-->
<!--        <el-form-item label="产品名称">-->
<!--          <el-input v-model="filterForm.productCategory" style="width: 240px" placeholder="请输入" clearable prefix-icon="Search" />-->
<!--        </el-form-item>-->
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
<!--          <el-button @click="handleReset">重置</el-button>-->
<!--          <el-button type="success" @click="handleExport">导出</el-button>-->
        </el-form-item>
      </el-form>
    </div>
    <!-- ç»Ÿè®¡æ±‡æ€»å¡ç‰‡ -->
    <div class="summary-cards">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-card class="summary-card">
            <div class="summary-item">
              <p class="summary-title">总库存数量</p>
              <p class="summary-value">{{ summaryData.totalInventoryCount }}</p>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="summary-card">
            <div class="summary-item">
              <p class="summary-title">总库存金额</p>
              <p class="summary-value">Â¥{{ summaryData.totalInventoryValue }}</p>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="summary-card">
            <div class="summary-item">
              <p class="summary-title">库存变动数量</p>
              <p class="summary-value">{{ summaryData.inventoryChangeCount }}</p>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="summary-card">
            <div class="summary-item">
              <p class="summary-title">库存变动金额</p>
              <p class="summary-value">Â¥{{ summaryData.inventoryChangeValue }}</p>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <div class="chart-section">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card class="chart-card">
            <template #header>
              <div class="card-header">
                <span>库存分类占比</span>
              </div>
            </template>
            <div id="category-pie-chart" style="height: 400px;"></div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card class="chart-card">
            <template #header>
              <div class="card-header">
                <span>库存金额趋势</span>
              </div>
            </template>
            <div id="amount-trend-chart" style="height: 400px;"></div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        v-loading="loading"
        border
        style="width: 100%"
        :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="供应商名称" prop="supplierName" width="240" show-overflow-tooltip />
        <el-table-column label="产品" prop="productCategory" min-width="100" show-overflow-tooltip />
        <el-table-column label="规格型号" prop="specificationModel" min-width="200" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" width="70" show-overflow-tooltip />
        <el-table-column label="入库数量" prop="inboundNum" width="90" show-overflow-tooltip />
        <el-table-column label="库存数量" prop="inboundNum0" width="90" show-overflow-tooltip />
        <el-table-column label="含税单价" prop="taxInclusiveUnitPrice" width="100" show-overflow-tooltip />
        <el-table-column label="含税总价" prop="taxInclusiveTotalPrice" width="100" show-overflow-tooltip />
        <el-table-column label="税率(%)" prop="taxRate" width="80" show-overflow-tooltip />
        <el-table-column label="不含税总价" prop="taxExclusiveTotalPrice" width="100" show-overflow-tooltip />
        <el-table-column label="入库人" prop="createBy" width="100" show-overflow-tooltip />
      </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>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import {getStockInChartData, getStockInPage} from "@/api/inventoryManagement/stockIn.js";
// çŠ¶æ€å˜é‡
const loading = ref(false)
const total = ref(0)
const tableData = ref([])
const summaryData = ref({})
const page = reactive({
  current: 1,
  size: 100,
})
// å›¾è¡¨å®žä¾‹
const categoryPieChart = ref(null)
const amountTrendChart = ref(null)
// ç­›é€‰è¡¨å•
const filterForm = reactive({
  dateRange: [],
  supplierName: '',
  productCategory: ''
})
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  loadData()
}
// åˆå§‹åŒ–数据
onMounted(() => {
  loadSummaryData()
  loadData()
})
// åŠ è½½ç»Ÿè®¡æ±‡æ€»æ•°æ®
const loadSummaryData = () => {
  getStockInChartData().then(res => {
    summaryData.value = res.data
  })
}
// åŠ è½½åº“å­˜æ•°æ®
const loadData = () => {
  loading.value = true
  getStockInPage({ ...filterForm, ...page }).then(res => {
    loading.value = false
    tableData.value = res.data.records
    total.value = res.data.total
    console.log('res', res.data.records)
    // æ•°æ®åŠ è½½å®ŒæˆåŽæ¸²æŸ“å›¾è¡¨
    nextTick(() => {
      renderCategoryPieChart()
      renderAmountTrendChart()
    })
  }).catch(() => {
    loading.value = false
  })
}
// æ¸²æŸ“分类占比饼图
const renderCategoryPieChart = () => {
  if (!categoryPieChart.value) {
    categoryPieChart.value = echarts.init(document.getElementById('category-pie-chart'))
  }
  // æ ¹æ® tableData æŒ‰ productCategory åˆ†ç±»å¹¶è®¡ç®— inboundNum0 æ•°é‡æ€»å’Œ
  const categoryMap = tableData.value.reduce((acc, cur) => {
    acc[cur.productCategory] = (acc[cur.productCategory] || 0) + cur.inboundNum0
    return acc
  }, {})
  // å°†åˆ†ç±»ç»“果转换为 ECharts é¥¼å›¾æ‰€éœ€çš„æ•°æ®æ ¼å¼
  const categoryData = Object.entries(categoryMap).map(([name, value]) => ({
    name: name,
    value: value
  }))
  const option = {
    title: {
      text: '库存分类占比',
      left: 'center'
    },
    tooltip: {
      trigger: 'item',
      formatter: '{a} <br/>{b}: {c} ({d}%)'
    },
    legend: {
      orient: 'vertical',
      left: 'left'
    },
    series: [
      {
        name: '库存分类',
        type: 'pie',
        radius: ['40%', '70%'],
        avoidLabelOverlap: false,
        itemStyle: {
          borderRadius: 10,
          borderColor: '#fff',
          borderWidth: 2
        },
        label: {
          show: true,
          formatter: '{b}: {d}%'
        },
        emphasis: {
          label: {
            show: true,
            fontSize: '16',
            fontWeight: 'bold'
          }
        },
        data: categoryData
      }
    ]
  }
  categoryPieChart.value.setOption(option)
}
// æ¸²æŸ“金额趋势折线图
const renderAmountTrendChart = () => {
  if (!amountTrendChart.value) {
    amountTrendChart.value = echarts.init(document.getElementById('amount-trend-chart'))
  }
  // æŒ‰æœˆä»½åˆ†ç»„并计算taxInclusiveTotalPrice总和
  const monthlyAmounts = tableData.value.reduce((acc, cur) => {
    const date = new Date(cur.createTime);
    const month = date.getMonth() + 1;
    // ç¡®ä¿month在1-12范围内
    if (month >= 1 && month <= 12) {
      acc[month] = (acc[month] || 0) + cur.taxInclusiveTotalPrice;
    }
    return acc;
  }, {});
  // ç”Ÿæˆ12个月的数据,缺失月份用0代替
  const amounts = [];
  for (let i = 1; i <= 12; i++) {
    amounts.push(monthlyAmounts[i] || 0);
  }
  const dates = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  const option = {
    title: {
      text: '库存金额趋势',
      left: 'center'
    },
    tooltip: {
      trigger: 'axis',
      formatter: '{b}: Â¥{c}'
    },
    xAxis: {
      type: 'category',
      data: dates
    },
    yAxis: {
      type: 'value',
      axisLabel: {
        formatter: 'Â¥{value}'
      }
    },
    series: [
      {
        name: '库存金额',
        type: 'line',
        data: amounts,
        smooth: true,
        areaStyle: {}
      }
    ]
  }
  amountTrendChart.value.setOption(option)
}
// æŸ¥è¯¢æ“ä½œ
const handleSearch = () => {
  loadData()
}
// é‡ç½®æ“ä½œ
const handleReset = () => {
  filterForm.dateRange = []
  filterForm.supplierName = ''
  filterForm.productCategory = ''
  loadData()
}
// å¯¼å‡ºæ“ä½œ
const handleExport = () => {
  console.log('导出数据')
}
// çª—口大小改变时,重新调整图表大小
window.addEventListener('resize', () => {
  if (categoryPieChart.value) categoryPieChart.value.resize()
  if (amountTrendChart.value) amountTrendChart.value.resize()
})
</script>
<style scoped>
.inventory-statistics {
  padding: 20px;
}
.filter-form {
  margin-bottom: 20px;
}
.summary-cards {
  margin-bottom: 20px;
}
.summary-card {
  text-align: center;
  height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.summary-item {
  width: 100%;
}
.summary-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 5px;
}
.summary-value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
}
.summary-value.warning {
  color: #e6a23c;
}
.summary-value.danger {
  color: #f56c6c;
}
.chart-section {
  margin-bottom: 20px;
}
.chart-card {
  height: 460px;
}
.card-header {
  font-weight: bold;
}
.table_list {
  margin-top: 20px;
}
.pagination {
  text-align: right;
  margin-top: 20px;
}
</style>