yuan
6 天以前 0a9ef97a28c820b8f560bd5f0cb7834706880880
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_pro_河南鹤壁
已修改21个文件
2203 ■■■■ 文件已修改
src/api/equipmentManagement/measurementEquipment.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/financialStatements.js 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInRecord.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/financeAssistant.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/filesDia.vue 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/financialStatements/index.vue 1460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 90 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 106 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/thePaymentLedger/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderEdit/index.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index0.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/financialAnalysis/components/center-top.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/measurementEquipment.js
@@ -52,4 +52,31 @@
        method:"post",
        data
    })
}
// 通用附件查询
export function getStorageAttachmentList(query) {
  return request({
    url: "/storageAttachment/list",
    method: "get",
    params: query,
  });
}
// 通用附件保存
export function addStorageAttachment(data) {
  return request({
    url: "/storageAttachment/add",
    method: "post",
    data: data,
  });
}
// 通用附件删除
export function delStorageAttachment(ids) {
  return request({
    url: "/storageAttachment/delete",
    method: "delete",
    data: ids,
  });
}
src/api/financialManagement/financialStatements.js
@@ -1,31 +1,13 @@
import request from "@/utils/request";
// 根据日期查询
export const reportForms = (params) => {
  console.log(params);
/**
 * 获取财务报表月度明细
 * @param {Object} params { entryDateStart, entryDateEnd }
 */
export function accountStatementDetailsByMonth(params) {
  return request({
    url: "/account/accountExpense/report/forms",
    url: "/accounting/accountStatementDetailsByMonth",
    method: "get",
    params,
  });
};
// 查询每月数据-收入
export const reportIncome = (params) => {
  console.log(params);
  return request({
    url: "/account/accountExpense/report/income",
    method: "get",
    params,
  });
};
// 查询每月数据-支出
export const reportExpense = (params) => {
  console.log(params);
  return request({
    url: "/account/accountExpense/report/expense",
    method: "get",
    params,
  });
};
}
src/api/inventoryManagement/stockInRecord.js
@@ -41,4 +41,13 @@
        method: "post",
        data,
    });
};
// 批量反审入库记录(仅驳回状态可反审)
export const batchUnapproveStockInRecords = (data) => {
    return request({
        url: "/stockInRecord/reAudit",
        method: "post",
        data,
    });
};
src/components/AIChatSidebar/assistants/financeAssistant.js
@@ -14,13 +14,13 @@
  allowFileUpload: false,
  emptySessionText: '暂无财务会话',
  quickPrompts: [
    '生成本周经营周报(利润与现金流)',
    '分析本月利润下降原因',
    '近30天哪个客户利润贡献最高',
    '查看本月经营驾驶舱',
    '查询近30天亏损订单',
    '分析近30天库存资金占用',
    '预测未来3个月现金流',
    '生成本周经营周报',
    '为什么利润下降',
    '哪个客户最赚钱',
    '哪个工序成本最高'
  ]
}
src/components/AIChatSidebar/index.vue
@@ -227,6 +227,12 @@
                      :id="`ai-chart-${index}-${key}`"
                  ></div>
                </div>
                <div
                    v-else-if="message.chartMarkdownParseFailed"
                    class="chart-empty-state"
                >
                  图表解析失败,请稍后重试。
                </div>
                <!-- 表格内容 -->
                <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
@@ -2112,6 +2118,7 @@
          purchaseData: null,
          purchaseIntentData: null,
          financeData: null,
          chartMarkdownParseFailed: false,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -2361,6 +2368,7 @@
  messageObj.purchaseData = null
  messageObj.purchaseIntentData = null
  messageObj.financeData = null
  messageObj.chartMarkdownParseFailed = false
  if (isPurchaseIntentNotRecognized) {
    messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
@@ -3615,7 +3623,8 @@
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null,
    financeData: null
    financeData: null,
    chartMarkdownParseFailed: false
  })
  outputState.value[botMsgIndex] = {
@@ -3671,6 +3680,7 @@
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
    }
    currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
    // 最终解析确保图表渲染
    if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
@@ -3744,7 +3754,8 @@
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null,
    financeData: null
    financeData: null,
    chartMarkdownParseFailed: false
  }
  messages.value.push(botMsg)
@@ -3794,6 +3805,7 @@
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
    }
    currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
  }).catch(err => {
    if (err.name === 'CanceledError' || err.name === 'AbortError') {
      console.log('Request aborted by user')
@@ -3835,6 +3847,126 @@
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/\n/g, '<br>')
}
const localChartMarkdownImagePattern = /!\[[^\]]*]\((https?:\/\/local\/generate_chart\?[^)\s]+)\)/gi
const parseLocalChartOptionText = (optionText = '') => {
  const text = String(optionText || '').trim()
  if (!text) return null
  const parseCandidates = [text]
  try {
    const decoded = decodeURIComponent(text)
    if (decoded && decoded !== text) {
      parseCandidates.push(decoded)
    }
  } catch (err) {
    // Keep original text candidate.
  }
  for (const candidate of parseCandidates) {
    try {
      const parsed = JSON.parse(candidate)
      if (isPlainObject(parsed)) {
        return parsed
      }
    } catch (err) {
      continue
    }
  }
  return null
}
const parseLocalChartOptionFromUrl = (urlText = '') => {
  try {
    const url = new URL(String(urlText || '').trim())
    if (String(url.hostname || '').toLowerCase() !== 'local' || !String(url.pathname || '').includes('/generate_chart')) {
      return null
    }
    const optionText = url.searchParams.get('options')
    return parseLocalChartOptionText(optionText)
  } catch (err) {
    return null
  }
}
const extractLocalChartMarkdown = (text = '') => {
  const sourceText = String(text || '')
  if (!sourceText) {
    return {
      cleanedText: '',
      hasLocalChartMarkdown: false,
      chartOptions: null,
      parseFailed: false
    }
  }
  let hasLocalChartMarkdown = false
  let chartIndex = 0
  const chartOptions = {}
  const cleanedText = sourceText.replace(localChartMarkdownImagePattern, (fullMatch, chartUrl) => {
    hasLocalChartMarkdown = true
    const option = parseLocalChartOptionFromUrl(chartUrl)
    if (option) {
      chartOptions[`markdownChart_${chartIndex++}`] = option
    }
    return ''
  })
  const normalizedText = cleanedText
      .replace(/\n[ \t]*\n[ \t]*\n+/g, '\n\n')
      .trim()
  const hasParsedCharts = Object.keys(chartOptions).length > 0
  return {
    cleanedText: normalizedText,
    hasLocalChartMarkdown,
    chartOptions: hasParsedCharts ? chartOptions : null,
    parseFailed: hasLocalChartMarkdown && !hasParsedCharts
  }
}
const applyLocalChartMarkdownFallback = (displayText, msgIndex) => {
  const messageObj = messages.value[msgIndex]
  if (!messageObj || messageObj.isUser) return displayText
  const {
    cleanedText,
    hasLocalChartMarkdown,
    chartOptions,
    parseFailed
  } = extractLocalChartMarkdown(displayText)
  if (!hasLocalChartMarkdown) {
    return displayText
  }
  if (chartOptions) {
    messageObj.chartOptions = chartOptions
    messageObj.chartRenderReady = true
    messageObj.chartMarkdownParseFailed = false
    const streamState = outputState.value[msgIndex]
    if (!streamState || !streamState.hasRenderedChart) {
      renderCharts(msgIndex, chartOptions)
      if (streamState) {
        streamState.hasRenderedChart = true
      }
    }
    return cleanedText || '已为您生成分析图表。'
  }
  if (!messageObj.chartOptions || !messageObj.chartRenderReady) {
    messageObj.chartOptions = null
    messageObj.chartRenderReady = false
    messageObj.chartMarkdownParseFailed = parseFailed
  }
  return cleanedText || '图表解析失败,请稍后重试。'
}
const convertStreamOutput = (output, msgIndex) => {
@@ -3902,6 +4034,7 @@
    }
  }
  display = applyLocalChartMarkdownFallback(display, msgIndex)
  let html = convertTextToHtml(display)
  // 还原代码块
@@ -4884,6 +5017,18 @@
  margin-bottom: 12px;
}
.chart-empty-state {
  margin-top: 12px;
  width: 100%;
  border-radius: 10px;
  border: 1px dashed rgba(148, 163, 184, 0.6);
  background: #f8fafc;
  color: #64748b;
  font-size: 13px;
  line-height: 1.6;
  padding: 12px;
}
.table-wrapper {
  margin-top: 12px;
  background: #fff;
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -27,22 +27,22 @@
        <el-row>
          <el-col :span="12">
            <el-form-item label="巡检项目" prop="inspectionProject">
              <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" />
              <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="是否启用" prop="isEnabled">
              <el-radio-group v-model="form.isEnabled">
                <el-radio :value="1">是</el-radio>
                <el-radio :value="0">否</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
            </el-form-item>
          </el-col>
        </el-row>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -92,6 +92,7 @@
  import { Delete, Plus } from "@element-plus/icons-vue";
  import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  // 组件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
@@ -194,7 +195,19 @@
      },
    },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "createTime", label: "登记日期", minWidth: 100 },
    {
      prop: "createTime",
      label: "登记日期",
      minWidth: 100,
      formatData: cell => {
        if (!cell) return "-";
        try {
          return dayjs(cell).format("YYYY-MM-DD HH:mm:ss");
        } catch {
          return cell;
        }
      },
    },
    {
      prop: "inspectionResult",
      label: "巡检结果",
src/views/equipmentManagement/ledger/Form.vue
@@ -156,6 +156,16 @@
                    />
                </el-form-item>
            </el-col>
      <el-col :span="24">
        <el-form-item label="设备图片" prop="storageBlobDTOs">
          <AttachmentUploadImage
            v-model:fileList="fileList"
            :limit="20"
            :fileSize="5"
            :buttonText="'上传图片'"
          />
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>
@@ -170,7 +180,8 @@
  calculateTaxExclusiveTotalPrice,
} from "@/utils/summarizeTable";
import { ElMessage } from "element-plus";
import {ref, getCurrentInstance} from "vue";
import { ref, getCurrentInstance, computed } from "vue";
import AttachmentUploadImage from '@/components/AttachmentUpload/image/index.vue';
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
@@ -230,6 +241,18 @@
  // createUser: useUserStore().nickName, // 录入人
  createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 录入日期
    planRuntimeTime: dayjs().format("YYYY-MM-DD"), // 录入日期
  storageBlobDTOs: undefined, // 设备图片提交
  storageBlobVOs: undefined, // 设备图片展示
});
const fileList = computed({
  get() {
    return form.storageBlobVOs || [];
  },
  set(val) {
    form.storageBlobDTOs = val;
    form.storageBlobVOs = val;
  }
});
const loadForm = async (id) => {
@@ -259,6 +282,8 @@
    } else {
      form.planRuntimeTime = undefined;
    }
    form.storageBlobVOs = data.storageBlobVOs;
    form.storageBlobDTOs = data.storageBlobVOs;
  }
};
src/views/equipmentManagement/ledger/index.vue
@@ -109,13 +109,52 @@
        </div>
      </template>
    </el-dialog>
    <!-- 详情对话框 -->
    <el-dialog v-model="detailDialogVisible" title="设备台账详情" width="60%" draggable>
      <el-descriptions :column="2" border>
        <el-descriptions-item label="设备名称">{{ detailData.deviceName }}</el-descriptions-item>
        <el-descriptions-item label="规格型号">{{ detailData.deviceModel }}</el-descriptions-item>
        <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item>
        <el-descriptions-item label="设备类型">{{ detailData.type }}</el-descriptions-item>
        <el-descriptions-item label="供应商">{{ detailData.supplierName }}</el-descriptions-item>
        <el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item>
        <el-descriptions-item label="单位">{{ detailData.unit }}</el-descriptions-item>
        <el-descriptions-item label="数量">{{ detailData.number }}</el-descriptions-item>
        <el-descriptions-item label="启用折旧">{{ detailData.isDepr === 1 ? '是' : '否' }}</el-descriptions-item>
        <el-descriptions-item label="每年折旧金额">{{ detailData.annualDepreciationAmount }}</el-descriptions-item>
        <el-descriptions-item label="含税单价">{{ detailData.taxIncludingPriceUnit }}</el-descriptions-item>
        <el-descriptions-item label="含税总价">{{ detailData.taxIncludingPriceTotal }}</el-descriptions-item>
        <el-descriptions-item label="税率(%)">{{ detailData.taxRate }}</el-descriptions-item>
        <el-descriptions-item label="不含税总价">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item>
        <el-descriptions-item label="录入日期">{{ detailData.createTime }}</el-descriptions-item>
        <el-descriptions-item label="预计运行时间">{{ detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' }}</el-descriptions-item>
        <el-descriptions-item label="设备图片" :span="2">
          <div v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" style="display: flex; gap: 10px; flex-wrap: wrap;">
            <el-image
              v-for="(file, index) in detailData.storageBlobVOs"
              :key="index"
              :src="file.previewURL || file.url"
              :preview-src-list="detailData.storageBlobVOs.map(u => u.previewURL || u.url)"
              :initial-index="index"
              style="width: 100px; height: 100px"
              fit="cover"
            />
          </div>
          <span v-else>无图片</span>
        </el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <el-button @click="detailDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger } from "@/api/equipmentManagement/ledger";
import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger";
import { onMounted, getCurrentInstance, ref, reactive } from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
@@ -135,6 +174,9 @@
const qrDialogVisible = ref(false);
const qrCodeUrl = ref("");
const qrRowData = ref(null);
const detailDialogVisible = ref(false);
const detailData = ref({});
// 导入相关
const uploadRef = ref(null)
@@ -218,8 +260,14 @@
            label: "操作",
            align: "center",
            fixed: 'right',
            width: 150,
            width: 180,
            operation: [
        {
          name: "详情",
          clickFun: (row) => {
            handleDetail(row);
          },
        },
                {
                    name: "编辑",
                    clickFun: (row) => {
@@ -227,7 +275,7 @@
                    },
                },
                {
                    name: "生成二维码",
                    name: "二维码",
                    clickFun: (row) => {
                        showQRCode(row)
                    },
@@ -248,6 +296,13 @@
const edit = (id) => {
  modalRef.value.loadForm(id);
};
const handleDetail = async (row) => {
  const { code, data } = await getLedgerById(row.id);
  if (code == 200) {
    detailData.value = data;
    detailDialogVisible.value = true;
  }
};
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
    pagination.pageSize = limit;
src/views/equipmentManagement/measurementEquipment/filesDia.vue
@@ -13,7 +13,7 @@
            :action="uploadUrl"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            name="file"
            name="files"
            :show-file-list="false"
            :headers="headers"
            style="display: inline;margin-right: 10px"
@@ -51,10 +51,10 @@
import filePreview from '@/components/filePreview/index.vue'
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import {
  fileAdd,
  fileDel,
  fileListPage
} from "@/api/financialManagement/revenueManagement.js";
  addStorageAttachment,
  delStorageAttachment,
  getStorageAttachmentList
} from "@/api/equipmentManagement/measurementEquipment.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -101,7 +101,7 @@
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 上传的图片服务器地址
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的服务器地址
// 打开弹框
const openDialog = (row,type) => {
@@ -113,12 +113,13 @@
const paginationSearch = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    // 前端分页暂不处理,直接调用获取全量列表
    getList();
};
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
        page.total = res.data.total;
  getStorageAttachmentList({recordId: currentId.value, recordType: accountType.value}).then(res => {
    tableData.value = res.data;
        page.total = res.data ? res.data.length : 0;
  })
}
// 表格选择数据
@@ -134,22 +135,21 @@
// 上传成功处理
function handleUploadSuccess(res, file) {
  // 如果上传成功
  if (res.code == 200) {
    const fileRow = {}
    fileRow.name = res.data.originalName
    fileRow.url = res.data.tempPath
    uploadFile(fileRow)
  if (res.code == 200 && res.data && res.data.length > 0) {
    const mergedFiles = [...tableData.value, ...res.data];
    const storageAttachmentDTO = {
      recordType: accountType.value,
      recordId: currentId.value,
      application: "file",
      storageBlobDTOs: mergedFiles
    };
    addStorageAttachment(storageAttachmentDTO).then(r => {
      proxy.$modal.msgSuccess("文件上传成功");
      getList()
    })
  } else {
    proxy.$modal.msgError("文件上传失败");
  }
}
function uploadFile(file) {
  file.accountId = currentId.value;
  file.accountType = accountType.value;
  fileAdd(file).then(res => {
    proxy.$modal.msgSuccess("文件上传成功");
    getList()
  })
}
// 上传失败处理
function handleUploadError() {
@@ -163,7 +163,7 @@
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
    ids = selectedRows.value.map((item) => item.storageAttachmentId);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
@@ -173,7 +173,7 @@
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    fileDel(ids).then((res) => {
    delStorageAttachment(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
      getList();
    });
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -177,7 +177,7 @@
// 打开附件弹框
const openFilesFormDia = (row) => {
    filesDia.value?.openDialog(row,'计量器具台账')
    filesDia.value?.openDialog(row,'measuring_instrument_ledger')
};
const dbRowClick = (row)=>{
src/views/equipmentManagement/upkeep/index.vue
@@ -344,7 +344,13 @@
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
    {
      prop: "registrationDate",
      label: "登记日期",
      minWidth: 100,
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
      fixed: "right",
      label: "操作",
src/views/financialManagement/financialStatements/index.vue
@@ -1,160 +1,186 @@
 <template>
<template>
  <div style="padding: 20px;">
    <!-- 页面标题和月份筛选 -->
    <div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;">
      <el-date-picker
        v-model="dateRange"
        type="monthrange"
        format="YYYY-MM"
        value-format="YYYY-MM"
        range-separator="至"
        start-placeholder="开始月份"
        end-placeholder="结束月份"
        :disabled-date="disabledDate"
        @change="handleDateChange"
        class="w-full md:w-auto"
        style="margin-right: 30px;"
      />
      <el-button
        type="primary"
        icon="Refresh"
        @click="resetDateRange"
        size="default"
      >
    <div class="w-full md:w-auto flex items-center gap-3"
         style="margin-bottom: 20px;">
      <el-date-picker v-model="dateRange"
                      type="monthrange"
                      format="YYYY-MM"
                      value-format="YYYY-MM"
                      range-separator="至"
                      start-placeholder="开始月份"
                      end-placeholder="结束月份"
                      :disabled-date="disabledDate"
                      @change="handleDateChange"
                      class="w-full md:w-auto"
                      style="margin-right: 30px;" />
      <el-button type="primary"
                 icon="Refresh"
                 @click="resetDateRange"
                 size="default">
        重置
      </el-button>
    </div>
    <main class="container mx-auto px-4 pb-10">
      <!-- 财务指标卡片 -->
      <div class="stats-cards">
        <!-- 总营收 -->
        <div class="stat-card stat-card-blue">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletBlue@2x.png" alt="总营收" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletBlue@2x.png"
                 alt="总营收" /></div>
          <div class="stat-content">
            <div class="stat-label">总营收</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }} 元</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 总支出 -->
        <div class="stat-card stat-card-orange">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletOrange@2x.png" alt="总支出" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletOrange@2x.png"
                 alt="总支出" /></div>
          <div class="stat-content">
            <div class="stat-label">总支出</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }} 元</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 总收入笔数 -->
        <div class="stat-card stat-card-green">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletGreen@2x.png" alt="总收入笔数" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletGreen@2x.png"
                 alt="应收账款" /></div>
          <div class="stat-content">
            <div class="stat-label">总收入笔数</div>
            <div class="stat-value">{{ pageInfo.incomeNumber || 0 }} 笔</div>
            <div class="stat-label">应收账款</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalReceivable || 0) }}{{ Math.abs(pageInfo.totalReceivable) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 总支出笔数 -->
        <div class="stat-card stat-card-red">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletRed@2x.png" alt="总支出笔数" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletRed@2x.png"
                 alt="应付账款" /></div>
          <div class="stat-content">
            <div class="stat-label">总支出笔数</div>
            <div class="stat-value">{{ pageInfo.expenseNumber || 0 }} 笔</div>
            <div class="stat-label">应付账款</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalPayable || 0) }}{{ Math.abs(pageInfo.totalPayable) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 净收入 -->
        <div class="stat-card stat-card-yellow">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletYellow@2x.png" alt="净收入" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletYellow@2x.png"
                 alt="净利润" /></div>
          <div class="stat-content">
            <div class="stat-label">净收入</div>
            <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }} 元</div>
            <div class="stat-label">净利润</div>
            <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }}{{ Math.abs(pageInfo.netRevenue) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
      </div>
      <!-- 中间图表区域 -->
      <!-- 图表区域 -->
      <div class="charts-row">
        <!-- 左侧:收入支出分析 -->
        <!-- 1. 收支构成分析 (双环形图 + 净利中心) -->
        <el-card class="chart-card">
          <h2 class="section-title">收入支出分析</h2>
          <div class="pie-chart-container">
            <Echarts
              :legend="pieLegendIncomeExpense"
              :chartStyle="chartStylePie"
              :series="pieSeriesIncomeExpense"
              :tooltip="pieTooltipIncomeExpense"
              style="height: 320px; width: 100%;">
            </Echarts>
            <div class="pie-stats">
              <div class="bar-stat-item">
                <span class="bar-stat-label">收入数量</span>
                <span class="bar-stat-value">{{ pageInfo.incomeNumber || 0 }}</span>
          <template #header>
            <div class="card-header">
              <span class="header-title">收支构成及净利分析</span>
              <el-tooltip content="左侧为收入构成,右侧为支出构成,中间展示盈亏净额"
                          placement="top">
                <el-icon>
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
            </div>
          </template>
          <div class="financial-overview-container">
            <!-- 收入展示 (左侧) -->
            <div style="width:60%">
              <div class="overview-item income"
                   style="margin-bottom: 20px;">
                <div class="overview-box">
                  <div class="icon-circle">
                    <el-icon>
                      <TrendCharts />
                    </el-icon>
                  </div>
                  <div class="data-content">
                    <div class="label">本期总收入</div>
                    <div class="value">{{ formatMoney(pageInfo.totalIncome) }}</div>
                    <div class="unit">RMB{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' / 元' : '' }}</div>
                  </div>
                  <div class="bg-decoration">INCOME</div>
                </div>
              </div>
              <div class="bar-stat-item">
                <span class="bar-stat-label">支出数量</span>
                <span class="bar-stat-value">{{ pageInfo.expenseNumber || 0 }}</span>
              <div class="overview-item expense">
                <div class="overview-box">
                  <div class="icon-circle">
                    <el-icon>
                      <Sell />
                    </el-icon>
                  </div>
                  <div class="data-content">
                    <div class="label">本期总支出</div>
                    <div class="value">{{ formatMoney(pageInfo.totalExpense) }}</div>
                    <div class="unit">RMB{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' / 元' : '' }}</div>
                  </div>
                  <div class="bg-decoration">EXPENSE</div>
                </div>
              </div>
            </div>
            <!-- 净利润核心指示 (中间) -->
            <div class="profit-indicator">
              <div class="profit-gauge-wrapper">
                <Echarts :chartStyle="chartStylePie"
                         :series="profitGaugeSeries"
                         :tooltip="gaugeTooltip"
                         style="height: 200px; width: 100%; max-width: 200px;">
                </Echarts>
                <div class="profit-center-text">
                  <div class="label">净利润</div>
                  <div class="value"
                       :class="pageInfo.netRevenue >= 0 ? 'plus' : 'minus'">
                    {{ pageInfo.netRevenue >= 0 ? '+' : '' }}{{ formatMoney(pageInfo.netRevenue) }}
                  </div>
                  <div class="rate">利润率: {{ pageInfo.totalIncome > 0 ? ((pageInfo.netRevenue / pageInfo.totalIncome) * 100).toFixed(1) : 0 }}%</div>
                </div>
              </div>
            </div>
            <!-- 支出展示 (右侧) -->
          </div>
        </el-card>
        <!-- 右侧:行项盈利分析 -->
        <!-- 2. 应收/应付对冲分析 (柱状图) -->
        <el-card class="chart-card">
          <h2 class="section-title">行项盈利分析</h2>
          <div class="bar-chart-header">
            <div class="bar-stat-item">
              <span class="bar-stat-label">当前总个数</span>
              <span class="bar-stat-value">{{ allBarTypes.value?.length || 0 }}</span>
          <template #header>
            <div class="card-header">
              <span class="header-title">应收/应付概览</span>
              <el-tooltip content="对比当前各月份的应收账款与应付账款"
                          placement="top">
                <el-icon>
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
            </div>
            <div class="bar-stat-item">
              <span class="bar-stat-label">支出金额</span>
              <span class="bar-stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}</span>
            </div>
            <div class="bar-stat-item">
              <span class="bar-stat-label">收入金额</span>
              <span class="bar-stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}</span>
            </div>
          </div>
          <Echarts
            ref="barChart"
            :chartStyle="chartStyle"
            :grid="barGrid"
            :legend="barLegend"
            :series="barSeries"
            :tooltip="barTooltip"
            :xAxis="barXAxis"
            :yAxis="barYAxis"
            style="height: 300px; width: 100%;">
          </template>
          <Echarts :chartStyle="chartStyle"
                   :grid="barGrid"
                   :legend="barLegend"
                   :series="barSeries"
                   :tooltip="barTooltip"
                   :xAxis="barXAxis"
                   :yAxis="barYAxis"
                   style="height: 270px; width: 100%;">
          </Echarts>
        </el-card>
      </div>
      <!-- 底部:营收趋势分析 -->
      <!-- 3. 财务综合趋势分析 (折线图) -->
      <el-card class="trend-chart-card">
        <h2 class="section-title">营收趋势分析</h2>
        <Echarts
          ref="trendChart"
          :chartStyle="chartStyle"
          :grid="grid"
          :legend="trendLegend"
          :series="trendSeries"
          :tooltip="tooltip"
          :xAxis="xAxis0"
          :yAxis="trendYAxis"
          style="height: 350px; width: 100%;">
        <template #header>
          <div class="card-header">
            <span class="header-title">财务绩效综合趋势</span>
            <el-tooltip content="展示收入、支出及净利润的月度变化趋势"
                        placement="top">
              <el-icon>
                <QuestionFilled />
              </el-icon>
            </el-tooltip>
          </div>
        </template>
        <Echarts :chartStyle="chartStyle"
                 :grid="trendGrid"
                 :legend="trendLegend"
                 :series="trendSeries"
                 :tooltip="trendTooltip"
                 :xAxis="trendXAxis"
                 :yAxis="trendYAxis"
                 style="height: 350px; width: 100%;">
        </Echarts>
      </el-card>
    </main>
@@ -162,833 +188,461 @@
</template>
<script setup>
import { ref, computed, onMounted, reactive, nextTick, getCurrentInstance } from 'vue';
import 'element-plus/dist/index.css';
import Echarts from "@/components/Echarts/echarts.vue";
import { reportForms,reportIncome,reportExpense } from "@/api/financialManagement/financialStatements";
import dayjs from "dayjs";
  import {
    ref,
    computed,
    onMounted,
    reactive,
    nextTick,
    getCurrentInstance,
  } from "vue";
  import { QuestionFilled, TrendCharts, Sell } from "@element-plus/icons-vue";
  import Echarts from "@/components/Echarts/echarts.vue";
  import { accountStatementDetailsByMonth } from "@/api/financialManagement/financialStatements";
  import dayjs from "dayjs";
// 日期范围
const dateRange = ref(null);
const { proxy } = getCurrentInstance();
const chartStyle = {
    width: '100%',
    height: '100%', // 设置图表容器的高度
  position:'relative',
}
const grid = {
    left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
}
const lineLegend = {
    show: false,
}
// 折线图提示框
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 lineSeries0 = ref([])
const lineSeries1 = ref([])
  const { proxy } = getCurrentInstance();
  const dateRange = ref(null);
  const pageInfo = reactive({
    totalIncome: 0,
    totalExpense: 0,
    totalReceivable: 0,
    totalPayable: 0,
    netRevenue: 0,
  });
// 根据月份范围生成 x 轴数据
const generateMonthLabels = (startMonth, endMonth) => {
  const labels = [];
  let current = dayjs(startMonth);
  const end = dayjs(endMonth);
  while (current.isBefore(end) || current.isSame(end, 'month')) {
    labels.push(`${current.month() + 1}月`);
    current = current.add(1, 'month');
  }
  return labels;
};
  const chartStyle = { width: "100%", height: "100%", position: "relative" };
  const chartStylePie = { width: "100%", height: "100%" };
const xAxis0 = ref([
  {
    type: 'category',
    axisTick: { show: true, alignWithLabel: true },
    data: [],
  },
]);
const xAxis1 = ref([
  {
    type: 'category',
    axisTick: { show: true, alignWithLabel: true },
    data: [],
  },
]);
const yAxis0 = [
{
    type: 'value',
    name: '收入统计', // 左侧y轴
    position: 'left',
    min: 0,
    // 坐标轴名称样式
    nameTextStyle: {
      color: '#000',
      fontSize: 14,
    },
  }
]
  const monthlyTrendList = ref([]);
  const receivablePayableList = ref([]);
const yAxis1 = [
{
    type: 'value',
    name: '支出统计', // 左侧y轴
    position: 'left',
    min: 0,
    // 坐标轴名称样式
    nameTextStyle: {
      color: '#000',
      fontSize: 14,
    },
  }
]
  // --- 1. 收支构成分析 (简化版逻辑) ---
  const gaugeTooltip = { show: false };
const chartStylePie = {
    width: '100%',
    height: '100%' // 设置图表容器的高度
}
const pieColors = ['#F04864','#FACC14', '#8543E0', '#1890FF', '#13C2C2','#2FC25B']; // 可根据实际调整
const pieData0 = ref([]);
const pieData1 = ref([]);
const pieLegend0 = computed(() => ({
  show: true,
  top: 'center',
  left: '60%',
  orient: 'vertical',
  icon: 'circle',
  data: (pieData0.value || []).filter(item => item && item.name).map(item => item.name),
  formatter: function(name) {
    if (!name) return '';
    const item = pieData0.value.find(i => i && i.name === name);
    if (!item) return name;
    return `${name} | ${item.percent} ${item.amount}`;
  },
  textStyle: {
    color: '#333',
    fontSize: 14,
    lineHeight: 26,
  }
}));
const pieLegend1 = computed(() => ({
  show: true,
  top: 'center',
  left: '60%',
  orient: 'vertical',
  icon: 'circle',
  data: (pieData1.value || []).filter(item => item && item.name).map(item => item.name),
  formatter: function(name) {
    if (!name) return '';
    const item = pieData1.value.find(i => i && i.name === name);
    if (!item) return name;
    return `${name} | ${item.percent} ${item.amount}`;
  },
  textStyle: {
    color: '#333',
    fontSize: 14,
    lineHeight: 26,
  }
}));
const materialPieSeries0 = computed(() => [
  {
    type: 'pie',
    radius: ['50%', '65%'],
    center: ['25%', '50%'],
    avoidLabelOverlap: false,
    itemStyle: {
      borderColor: '#fff',
      borderWidth: 2
    },
    label: {
      show: false
    },
    data: (pieData0.value || []).filter(item => item && item.name),
    color: pieColors
  }
]);
const materialPieSeries1 = computed(() => [
  {
    type: 'pie',
    radius: ['50%', '65%'],
    center: ['25%', '50%'],
    avoidLabelOverlap: false,
    itemStyle: {
      borderColor: '#fff',
      borderWidth: 2
    },
    label: {
      show: false
    },
    data: (pieData1.value || []).filter(item => item && item.name),
    color: pieColors
  }
]);
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.percent}</div>
        <div>金额:${params.data.amount}</div>
      </div>
    `;
  }
})
const pageInfo = ref({
})
// 格式化金额
const formatMoney = (value) => {
  if (!value && value !== 0) return '0';
  return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
// 收入支出分析饼图
const pieDataIncomeExpense = computed(() => {
  const totalIncome = Number(pageInfo.value.totalIncome) || 0;
  const totalExpense = Number(pageInfo.value.totalExpense) || 0;
  const total = totalIncome + totalExpense;
  if (total === 0) {
    return [
      { name: '收入', value: 0, percent: '0%' },
      { name: '支出', value: 0, percent: '0%' }
    ];
  }
  const incomePercent = ((totalIncome / total) * 100).toFixed(0);
  const expensePercent = ((totalExpense / total) * 100).toFixed(0);
  return [
    { name: '收入', value: totalIncome, percent: `${incomePercent}%` },
    { name: '支出', value: totalExpense, percent: `${expensePercent}%` }
  ];
});
const pieLegendIncomeExpense = computed(() => ({
  show: false
}));
const pieTooltipIncomeExpense = reactive({
  trigger: 'item',
  formatter: function(params) {
    if (!params.data) return params.name;
    return `${params.name}占比 ${params.percent}%`;
  }
});
const pieSeriesIncomeExpense = computed(() => [
  {
    type: 'pie',
    radius: ['0%', '70%'],
    center: ['50%', '50%'],
    avoidLabelOverlap: true,
    itemStyle: {
      borderColor: '#fff',
      borderWidth: 2
    },
    label: {
      show: true,
      position: 'outside',
      formatter: function(params) {
        return `${params.name}占比 ${params.percent}%`;
      },
      fontSize: 14,
      color: '#333'
    },
    labelLine: {
      show: true,
      length: 15,
      length2: 10,
      lineStyle: {
        color: '#333'
      }
    },
    emphasis: {
      label: {
        show: true,
        fontSize: 16,
        fontWeight: 'bold'
      }
    },
    data: pieDataIncomeExpense.value,
    color: ['#1890FF', '#FACC14']
  }
]);
// 行项盈利分析柱状图
const barXAxis = computed(() => {
  return [{
    type: 'category',
    data: (allBarTypes.value && allBarTypes.value.length > 0) ? allBarTypes.value : ['项目1', '项目2', '项目3', '项目4', '项目5', '项目6', '项目7'],
    axisTick: { show: true, alignWithLabel: true },
  }];
});
const barYAxis = [{
  type: 'value',
  name: '单位: 元',
  position: 'left',
  min: 0,
  nameTextStyle: {
    color: '#000',
    fontSize: 14,
  },
}];
const barGrid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  containLabel: true
};
const barLegend = {
  show: true,
  top: 10,
  right: 10,
};
// 获取所有类型名称
const allBarTypes = computed(() => {
  const incomeTypes = (lineSeries0.value || []).map(item => item.name || item.typeName).filter(Boolean);
  const expenseTypes = (lineSeries1.value || []).map(item => item.name || item.typeName).filter(Boolean);
  return [...new Set([...incomeTypes, ...expenseTypes])];
});
const barSeries = computed(() => {
  if (allBarTypes.value.length === 0) {
  const profitGaugeSeries = computed(() => {
    const rate =
      pageInfo.totalIncome > 0
        ? (pageInfo.netRevenue / pageInfo.totalIncome) * 100
        : 0;
    return [
      {
        name: '支出',
        type: 'bar',
        data: [],
        itemStyle: { color: '#1890FF' }
        type: "gauge",
        startAngle: 210,
        endAngle: -30,
        min: 0,
        max: 100,
        splitNumber: 10,
        radius: "100%",
        progress: {
          show: true,
          width: 14,
          itemStyle: { color: pageInfo.netRevenue >= 0 ? "#10b981" : "#f43f5e" },
        },
        pointer: { show: false },
        axisLine: { lineStyle: { width: 14, color: [[1, "#f1f5f9"]] } },
        axisTick: { show: false },
        splitLine: { show: false },
        axisLabel: { show: false },
        anchor: { show: false },
        title: { show: false },
        detail: { show: false },
        data: [{ value: Math.max(0, Math.min(100, rate)) }],
      },
      {
        name: '收入',
        type: 'bar',
        data: [],
        itemStyle: { color: '#13C2C2' }
      }
    ];
  }
  // 计算每个项目的总收入(汇总所有月份)
  const incomeData = allBarTypes.value.map(typeName => {
    const incomeItem = (lineSeries0.value || []).find(item => (item.name || item.typeName) === typeName);
    if (incomeItem && incomeItem.data && Array.isArray(incomeItem.data)) {
      return incomeItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0);
    }
    return 0;
  });
  // 计算每个项目的总支出(汇总所有月份)
  const expenseData = allBarTypes.value.map(typeName => {
    const expenseItem = (lineSeries1.value || []).find(item => (item.name || item.typeName) === typeName);
    if (expenseItem && expenseItem.data && Array.isArray(expenseItem.data)) {
      return expenseItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0);
    }
    return 0;
  });
  return [
  // --- 2. 应收/应付概览 (柱状图) ---
  const barGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true };
  const barLegend = { top: "0", right: "center" };
  const barXAxis = computed(() => [
    {
      name: '支出',
      type: 'bar',
      data: expenseData,
      itemStyle: { color: '#1890FF' }
      type: "category",
      data: receivablePayableList.value.map(item => item.month || ""),
      axisTick: { alignWithLabel: true },
    },
  ]);
  const barYAxis = [{ type: "value", name: "金额 (元)" }];
  const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
  const barSeries = computed(() => [
    {
      name: "应收账款",
      type: "bar",
      barWidth: "30%",
      data: receivablePayableList.value.map(item => item.receivable || 0),
      itemStyle: { color: "#10b981" },
    },
    {
      name: '收入',
      type: 'bar',
      data: incomeData,
      itemStyle: { color: '#13C2C2' }
    }
  ];
});
      name: "应付账款",
      type: "bar",
      barWidth: "30%",
      data: receivablePayableList.value.map(item => item.payable || 0),
      itemStyle: { color: "#ef4444" },
    },
  ]);
const barTooltip = reactive({
  trigger: 'axis',
  axisPointer: {
    type: 'shadow'
  },
  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>`;
        const value = typeof p.value === 'number' ? p.value.toFixed(2) : p.value;
        return `${colorDot}${p.seriesName} ${value}`;
      })
      .join('<br/>');
    return `<div>${axisLabel}</div><div>${rows}</div>`;
  }
});
// 营收趋势分析
const trendLegend = {
  show: true,
  top: 10,
  right: 10,
};
const trendYAxis = [{
  type: 'value',
  name: '单位: 元',
  position: 'left',
  min: 0,
  nameTextStyle: {
    color: '#000',
    fontSize: 14,
  },
}];
const trendSeries = computed(() => {
  // 汇总所有支出类型的数据
  let expenseTrend = [];
  if (lineSeries1.value.length > 0) {
    const monthCount = Math.max(...lineSeries1.value.map(item => item.data?.length || 0));
    expenseTrend = Array(monthCount).fill(0);
    lineSeries1.value.forEach(item => {
      if (item.data && Array.isArray(item.data)) {
        item.data.forEach((val, index) => {
          if (index < monthCount) {
            expenseTrend[index] += Number(val) || 0;
          }
        });
      }
    });
  }
  // 汇总所有收入类型的数据
  let incomeTrend = [];
  if (lineSeries0.value.length > 0) {
    const monthCount = Math.max(...lineSeries0.value.map(item => item.data?.length || 0));
    incomeTrend = Array(monthCount).fill(0);
    lineSeries0.value.forEach(item => {
      if (item.data && Array.isArray(item.data)) {
        item.data.forEach((val, index) => {
          if (index < monthCount) {
            incomeTrend[index] += Number(val) || 0;
          }
        });
      }
    });
  }
  return [
  // --- 3. 财务综合趋势分析 (折线图) ---
  const trendGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true };
  const trendLegend = { top: "0", right: "center" };
  const trendXAxis = computed(() => [
    {
      name: '支出',
      type: 'line',
      data: expenseTrend,
      itemStyle: { color: '#1890FF' },
      smooth: true
      type: "category",
      boundaryGap: false,
      data: monthlyTrendList.value.map(item => item.month || ""),
    },
  ]);
  const trendYAxis = [{ type: "value", name: "金额 (元)" }];
  const trendTooltip = { trigger: "axis" };
  const trendSeries = computed(() => [
    {
      name: "总营收",
      type: "line",
      smooth: true,
      data: monthlyTrendList.value.map(item => item.income || 0),
      itemStyle: { color: "#4f46e5" },
      areaStyle: { opacity: 0.1 },
    },
    {
      name: '收入',
      type: 'line',
      data: incomeTrend,
      itemStyle: { color: '#13C2C2' },
      smooth: true
    }
  ];
});
      name: "总支出",
      type: "line",
      smooth: true,
      data: monthlyTrendList.value.map(item => item.expense || 0),
      itemStyle: { color: "#f97316" },
    },
    {
      name: "净利润",
      type: "line",
      smooth: true,
      data: monthlyTrendList.value.map(item => item.profit || 0),
      lineStyle: { width: 4, type: "dashed" },
      itemStyle: { color: "#10b981" },
    },
  ]);
// 获取最近六个月的范围
const getLastSixMonths = () => {
  const endMonth = dayjs().format('YYYY-MM');
  const startMonth = dayjs().subtract(5, 'month').format('YYYY-MM');
  return [startMonth, endMonth];
};
  // --- 公用逻辑 ---
  const formatMoney = val => {
    return val;
  };
const getData = async () => {
  if (!dateRange.value || !Array.isArray(dateRange.value) || dateRange.value.length !== 2) {
    return;
  }
  const startDateStr = dateRange.value[0];
  const endDateStr = dateRange.value[1];
  if (!startDateStr || !endDateStr) {
    return;
  }
  // 验证日期格式并转换为完整日期
  const startDate = dayjs(startDateStr);
  const endDate = dayjs(endDateStr);
  if (!startDate.isValid() || !endDate.isValid()) {
    console.error('无效的日期格式');
    return;
  }
  // 更新 x 轴数据
  const monthLabels = generateMonthLabels(startDateStr, endDateStr);
  xAxis0.value[0].data = monthLabels;
  xAxis1.value[0].data = monthLabels;
  // 开始月份拼接第一天,结束月份拼接最后一天
  const entryDateStart = startDate.startOf('month').format('YYYY-MM-DD');
  const entryDateEnd = endDate.endOf('month').format('YYYY-MM-DD');
  try {
    const {code,data} = await reportForms({entryDateStart, entryDateEnd});
    if(code === 200 && data) {
      pageInfo.value = data || {};
      // 安全处理数据,过滤掉 null 或 undefined
      pieData0.value = (data.incomeType || []).filter(item => item && item.typeName).map(item=>({
        name:item.typeName || '',
        value:item.account || 0,
        percent:`${((item.proportion || 0) * 100).toFixed(2)}%`,
        amount:`¥${(item.account || 0).toFixed(2)}`
      }))
      pieData1.value = (data.expenseType || []).filter(item => item && item.typeName).map(item=>({
        name:item.typeName || '',
        value:item.account || 0,
        percent:`${((item.proportion || 0) * 100).toFixed(2)}%`,
        amount:`¥${(item.account || 0).toFixed(2)}`
      }))
    }
  } catch (error) {
    console.error('获取财务指标数据失败:', error);
  }
  try{
    const {code,data} = await reportIncome({entryDateStart, entryDateEnd});
    if(code==200 && data && Array.isArray(data)){
      lineSeries0.value = data.filter(item => item && item.typeName).map(item=>({
        name:item.typeName || '',
        type: 'line',
        data:(item.account || []).map(val => Number(val) || 0)
      }))
    }
  }catch (error) {
    console.error('获取财务指标数据失败:', error);
  }
  try{
    const {code,data} = await reportExpense({entryDateStart, entryDateEnd});
    if(code==200 && data && Array.isArray(data)){
      lineSeries1.value = data.filter(item => item && item.typeName).map(item=>({
        name:item.typeName || '',
        type: 'line',
        data:(item.account || []).map(val => Number(val) || 0)
      }))
    }
  }catch (error) {
    console.error('获取财务指标数据失败:', error);
  }
};
  const handleDateChange = val => {
    if (val) getData();
  };
// 初始化
onMounted(() => {
  // 设置默认值为最近六个月
  const defaultRange = getLastSixMonths();
  dateRange.value = defaultRange;
  // 使用 nextTick 确保组件完全渲染后再调用
  nextTick(() => {
  const resetDateRange = () => {
    dateRange.value = [
      dayjs().subtract(5, "month").format("YYYY-MM"),
      dayjs().format("YYYY-MM"),
    ];
    getData();
  };
  const disabledDate = time => dayjs(time).isAfter(dayjs(), "month");
  const getData = async () => {
    if (!dateRange.value || dateRange.value.length !== 2) return;
    const params = {
      entryDateStart: dayjs(dateRange.value[0])
        .startOf("month")
        .format("YYYY-MM-DD"),
      entryDateEnd: dayjs(dateRange.value[1]).endOf("month").format("YYYY-MM-DD"),
    };
    try {
      const res = await accountStatementDetailsByMonth(params);
      if (res.code === 200 && res.data) {
        const data = res.data;
        // 更新顶部汇总卡片数据
        pageInfo.totalIncome = data.totalIncome || 0;
        pageInfo.totalExpense = data.totalExpense || 0;
        pageInfo.totalReceivable = data.accountsReceivable || 0;
        pageInfo.totalPayable = data.accountsPayable || 0;
        pageInfo.netRevenue = data.netRevenue || 0;
        // 更新图表数据
        monthlyTrendList.value = data.monthlyTrendList || [];
        receivablePayableList.value = data.receivablePayableList || [];
      }
    } catch (error) {
      console.error("获取财务报表数据失败:", error);
    }
  };
  onMounted(() => {
    resetDateRange();
  });
});
// 限制月份选择范围(最多12个月)
const disabledDate = (time) => {
  // 如果没有选择开始月份,不禁用任何日期
  if (!dateRange.value || !Array.isArray(dateRange.value) || !dateRange.value[0]) {
    return false;
  }
  const startMonth = dayjs(dateRange.value[0]);
  const currentMonth = dayjs(time);
  // 如果当前月份在开始月份之前,禁用
  if (currentMonth.isBefore(startMonth, 'month')) {
    return true;
  }
  // 计算最大允许的月份(开始月份 + 11个月 = 12个月)
  const maxMonth = startMonth.add(11, 'month');
  // 禁用超过12个月的月份
  return currentMonth.isAfter(maxMonth, 'month');
};
// 处理月份范围变化
const handleDateChange = (newRange) => {
  if (!newRange || !Array.isArray(newRange) || newRange.length !== 2) {
    return;
  }
  // 验证月份范围不超过12个月
  const startDate = dayjs(newRange[0]);
  const endDate = dayjs(newRange[1]);
  const monthDiff = endDate.diff(startDate, 'month');
  if (monthDiff > 11) {
    proxy.$modal.msgWarning('最多只能选择12个月份');
    // 自动调整为12个月
    const adjustedEnd = startDate.add(11, 'month').format('YYYY-MM');
    dateRange.value = [newRange[0], adjustedEnd];
    getData();
    return;
  }
  dateRange.value = newRange;
  getData();
};
// 重置月份范围
const resetDateRange = () => {
  // 重置为最近六个月
  dateRange.value = getLastSixMonths();
  getData();
};
</script>
<style scoped lang="scss">
/* 基础样式补充 */
:root {
  --el-color-primary: #4f46e5;
}
/* 统计卡片样式 */
.stats-cards {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 20px;
  margin-bottom: 20px;
}
.stat-card {
  background: #fff;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  padding: 20px;
  display: flex;
  align-items: center;
  gap: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  transition: all 0.3s;
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }
  .stat-icon {
    width: 48px;
    height: 48px;
    flex-shrink: 0;
    img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }
  .stat-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .stat-label {
    font-size: 14px;
    color: #666;
    line-height: 1.2;
  }
  .stat-value {
    font-size: 24px;
    font-weight: 600;
    color: #333;
    line-height: 1.2;
  }
  .stat-trend {
    font-size: 12px;
    line-height: 1.2;
    &.trend-up {
      color: #f56c6c;
    }
    &.trend-down {
      color: #67c23a;
    }
  }
}
/* 图表行布局 */
.charts-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-bottom: 20px;
}
.chart-card {
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  :deep(.el-card__body) {
    padding: 20px !important;
  }
}
.trend-chart-card {
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  :deep(.el-card__body) {
    padding: 20px !important;
  }
}
/* 饼图容器 */
.pie-chart-container {
  position: relative;
  .pie-stats {
    display: flex;
    justify-content: space-between;
  .stats-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 20px;
    margin-top: 20px;
    .bar-stat-item {
    margin-bottom: 24px;
  }
  .stat-card {
    background: #fff;
    border: 1px solid #edf2f7;
    border-radius: 12px;
    padding: 24px;
    display: flex;
    align-items: center;
    gap: 16px;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    &:hover {
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
      transform: translateY(-4px);
    }
    .stat-icon {
      width: 56px;
      height: 56px;
      background: #f7fafc;
      border-radius: 12px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      img {
        width: 32px;
        height: 32px;
      }
    }
    .stat-content {
      .stat-label {
        font-size: 14px;
        color: #718096;
        margin-bottom: 4px;
      }
      .stat-value {
        font-size: 20px;
        font-weight: 700;
        color: #2d3748;
      }
    }
  }
  .charts-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 24px;
    margin-bottom: 24px;
  }
  @media (min-width: 1200px) {
    .charts-row {
      grid-template-columns: repeat(2, 1fr);
    }
  }
  .chart-card,
  .trend-chart-card {
    border-radius: 16px;
    border: none;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 15px;
      background: #f5f7fa;
      border-radius: 6px;
      flex: 1;
      .bar-stat-label {
        font-size: 14px;
        color: #666;
      }
      .bar-stat-value {
        font-size: 18px;
      .header-title {
        font-size: 16px;
        font-weight: 600;
        color: #333;
        color: #1a202c;
      }
      .el-icon {
        color: #a0aec0;
        cursor: help;
      }
    }
  }
}
/* 柱状图头部统计 */
.bar-chart-header {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 20px;
  .bar-stat-item {
  .financial-overview-container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    padding: 15px;
    background: #f5f7fa;
    border-radius: 6px;
    flex: 1;
    .bar-stat-label {
      font-size: 14px;
      color: #666;
    flex-wrap: nowrap;
    gap: 10px;
    padding: 20px 0;
    width: 100%;
    overflow: hidden;
    .overview-item {
      flex: 1;
      min-width: 0; // 允许在 flex 容器中缩写,防止内容撑开
      display: flex;
      justify-content: center;
      .overview-box {
        position: relative;
        width: 100%;
        max-width: 320px;
        height: 110px;
        background: #f8fafc;
        border-radius: 12px;
        padding: 12px 16px;
        display: flex;
        align-items: center;
        gap: 12px;
        overflow: hidden;
        transition: all 0.3s ease;
        &:hover {
          transform: translateY(-5px);
          box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
        }
        .icon-circle {
          flex-shrink: 0;
          width: 42px;
          height: 42px;
          border-radius: 10px;
          display: flex;
          align-items: center;
          justify-content: center;
          font-size: 20px;
          z-index: 2;
        }
        .data-content {
          z-index: 2;
          min-width: 0;
          .label {
            font-size: 13px;
            color: #718096;
            margin-bottom: 2px;
            font-weight: 500;
            white-space: nowrap;
          }
          .value {
            font-size: 18px;
            font-weight: 800;
            color: #1a202c;
            line-height: 1.2;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
          }
          .unit {
            font-size: 11px;
            color: #a0aec0;
          }
        }
        .bg-decoration {
          position: absolute;
          right: -5px;
          bottom: -5px;
          font-size: 32px;
          font-weight: 950;
          color: rgba(0, 0, 0, 0.03);
          font-style: italic;
          user-select: none;
          z-index: 1;
        }
      }
      &.income {
        .icon-circle {
          background: #eef2ff;
          color: #4f46e5;
        }
        .overview-box {
          border-left: 5px solid #4f46e5;
        }
      }
      &.expense {
        .icon-circle {
          background: #fff7ed;
          color: #f97316;
        }
        .overview-box {
          border-left: 5px solid #f97316;
        }
      }
    }
    .bar-stat-value {
      font-size: 18px;
      font-weight: 600;
      color: #333;
    .profit-indicator {
      flex: 0 40%; // 固定宽度,不参与弹性缩放以保证仪表盘完整
      display: flex;
      justify-content: center;
      align-items: center;
      .profit-gauge-wrapper {
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        // max-width: 180px;
        .profit-center-text {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          text-align: center;
          width: 100%;
          .label {
            font-size: 12px;
            color: #718096;
            font-weight: 500;
          }
          .value {
            font-size: 20px;
            font-weight: 800;
            margin: 2px 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            &.plus {
              color: #10b981;
            }
            &.minus {
              color: #f43f5e;
            }
          }
          .rate {
            font-size: 11px;
            color: #a0aec0;
            font-weight: 500;
          }
        }
      }
    }
    // 针对非常窄的屏幕进行整体缩放
    @media (max-width: 1400px) {
      transform-origin: center;
      // 如果容器太窄,通过缩小内部元素来适应
      // 这里不使用 transform: scale 因为会影响布局流,改用内部尺寸微调
      .overview-item .overview-box {
        padding: 10px;
        gap: 8px;
        .value {
          font-size: 16px;
        }
        .icon-circle {
          width: 36px;
          height: 36px;
          font-size: 18px;
        }
      }
      .profit-indicator {
        flex: 0 40%;
        .profit-gauge-wrapper .value {
          font-size: 18px;
        }
      }
    }
  }
}
/* 标题样式 */
.section-title {
  position: relative;
  font-size: 18px;
  color: #333;
  padding-left: 12px;
  margin-bottom: 20px;
  font-weight: 700;
  &::before {
    position: absolute;
    left: 0;
    top: 2px;
    content: '';
    width: 4px;
    height: 18px;
    background-color: #002FA7;
    border-radius: 2px;
  }
}
/* 响应式设计 */
@media (max-width: 1400px) {
  .stats-cards {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (max-width: 1024px) {
  .stats-cards {
    grid-template-columns: repeat(2, 1fr);
  }
  .charts-row {
    grid-template-columns: 1fr;
  }
}
@media (max-width: 640px) {
  .stats-cards {
    grid-template-columns: 1fr;
  }
}
</style>
src/views/index.vue
@@ -1307,9 +1307,12 @@
const statisticsReceivable = async () => {
  const res = await statisticsReceivablePayable({ type: 1 });
  const data = res?.data || {};
  const payableMoney = Number(data.payableMoney ?? 0);
  const receivableMoney = Number(data.receivableMoney ?? 0);
  barSeries.value[0].data = [
    { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } },
    { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } },
    { value: payableMoney, itemStyle: { color: barColors2[0] } },
    { value: receivableMoney, itemStyle: { color: barColors2[1] } },
  ];
};
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -71,10 +71,15 @@
      </el-form>
    </div>
    <div class="actions">
      <el-button type="primary" @click="handleBatchApprove">审批</el-button>
      <el-button type="primary"
                 :disabled="!canBatchApprove"
                 @click="handleBatchApprove">审批</el-button>
      <el-button :disabled="!canReverseApprove"
                 @click="handleReverseApprove">反审</el-button>
      <el-button @click="handleOut">导出</el-button>
      <el-button type="danger"
                 plain
                 :disabled="!canDelete"
                 @click="handleDelete">删除
      </el-button>
    </div>
@@ -89,7 +94,7 @@
                height="calc(100vh - 18.5em)">
        <el-table-column align="center"
                         type="selection"
                         :selectable="isRowSelectableForApprove"
                         :selectable="isRowSelectable"
                         width="55"/>
        <el-table-column align="center"
                         label="序号"
@@ -97,7 +102,7 @@
                         width="60"/>
        <el-table-column label="入库批次"
                         prop="inboundBatches"
                         width="280"
                         width="200"
                         show-overflow-tooltip/>
        <el-table-column label="入库时间"
                         prop="createTime"
@@ -127,6 +132,16 @@
            {{ getRecordType(scope.row.recordType) }}
          </template>
        </el-table-column>
        <el-table-column
            v-if="showSourceOrderNoColumn"
            label="源单号"
            width="150"
            prop="sourceOrderNo"
            show-overflow-tooltip>
          <template #default="scope">
            {{ formatSourceOrderNo(scope.row?.sourceOrderNo) }}
          </template>
        </el-table-column>
        <el-table-column label="审批状态"
                         prop="approvalStatus"
                         show-overflow-tooltip>
@@ -153,6 +168,7 @@
  ref,
  reactive,
  toRefs,
  computed,
  onMounted,
  getCurrentInstance,
} from "vue";
@@ -161,6 +177,7 @@
  getStockInRecordListPage,
  batchDeletePendingStockInRecords,
  batchApproveStockInRecords,
  batchUnapproveStockInRecords,
} from "@/api/inventoryManagement/stockInRecord.js";
import {
  findAllQualifiedStockInRecordTypeOptions, 
@@ -245,8 +262,33 @@
  return status === 0 || status === "0" || status === "pending" || status === "PENDING" || status === null || status === undefined || status === "";
};
const isRowSelectableForApprove = row => {
  return isPendingApproval(row?.approvalStatus);
const isRejectedApproval = status => {
  return status === 2 || status === "2" || status === "rejected" || status === "REJECTED";
};
const isRowSelectable = row => {
  return isPendingApproval(row?.approvalStatus) || isRejectedApproval(row?.approvalStatus);
};
const canBatchApprove = computed(() => {
  return selectedRows.value.length > 0
      && selectedRows.value.every(row => isPendingApproval(row.approvalStatus));
});
const canReverseApprove = computed(() => {
  return selectedRows.value.length > 0
      && selectedRows.value.every(row => isRejectedApproval(row.approvalStatus));
});
const canDelete = computed(() => canBatchApprove.value);
const showSourceOrderNoColumn = computed(() => {
  const topParentProductId = Number(props.topParentProductId);
  return topParentProductId === 276 || topParentProductId === 278;
});
const formatSourceOrderNo = (value) => {
  const text = String(value ?? "").trim();
  return text || "--";
};
const pageProductChange = obj => {
@@ -283,14 +325,40 @@
// 表格选择数据
const handleSelectionChange = selection => {
  selectedRows.value = selection.filter(item => item.id && isPendingApproval(item.approvalStatus));
  selectedRows.value = selection.filter(item => item.id && isRowSelectable(item));
};
const expandedRowKeys = ref([]);
const handleReverseApprove = () => {
  if (!canReverseApprove.value) {
    proxy.$modal.msgWarning("请选择已驳回的数据");
    return;
  }
  const ids = selectedRows.value.map(item => item.id);
  ElMessageBox.confirm("反审后记录将恢复为待审批状态,是否确认反审?", "反审", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        batchUnapproveStockInRecords({ids})
            .then(() => {
              proxy.$modal.msgSuccess("反审成功");
              getList();
            })
            .catch(() => {
              proxy.$modal.msgError("反审失败");
            });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
const handleBatchApprove = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择数据");
  if (!canBatchApprove.value) {
    proxy.$modal.msgWarning("请选择待审批的数据");
    return;
  }
  const ids = selectedRows.value.map(item => item.id);
@@ -344,8 +412,8 @@
// 删除
const handleDelete = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择数据");
  if (!canDelete.value) {
    proxy.$modal.msgWarning("请选择待审批的数据");
    return;
  }
  const ids = selectedRows.value.map(item => item.id);
@@ -390,4 +458,4 @@
  justify-content: flex-end;
  margin-bottom: 10px;
}
</style>
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -42,6 +42,20 @@
                            clearable
                            @change="changeDaterange" />
          </el-form-item>
          <el-form-item label="入库状态:">
            <el-select v-model="searchForm.stockInStatus"
                       placeholder="请选择"
                       clearable
                       style="width: 240px"
                       @change="handleQuery">
              <el-option label="待入库"
                         value="待入库" />
              <el-option label="入库中"
                         value="入库中" />
              <el-option label="完全入库"
                         value="完全入库" />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary"
                       @click="handleQuery"> 搜索
@@ -92,6 +106,16 @@
                               prop="specificationModel" />
              <el-table-column label="单位"
                               prop="unit" />
                               <el-table-column label="入库审核状态"
                               prop="stockInApprovalStatus"
                               width="120">
                <template #default="scope">
                  <el-tag :type="getStockInApprovalStatusType(scope.row.stockInApprovalStatus)"
                          size="small">
                    {{ scope.row.stockInApprovalStatus || '--' }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="数量"
                               prop="quantity" />
              <el-table-column label="可用数量"
@@ -143,6 +167,17 @@
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="入库状态"
                         prop="stockInStatus"
                         width="100"
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getStockInStatusType(scope.row.stockInStatus)"
                    size="small">
              {{ scope.row.stockInStatus || '--' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="签订日期"
                         prop="executionDate"
                         width="100"
@@ -176,7 +211,7 @@
            <el-button link
                       type="primary"
                       @click="openForm('edit', scope.row)"
                       :disabled="scope.row.approvalStatus !== 1 && scope.row.approvalStatus !== 4">编辑
                       :disabled="scope.row.stockInStatus === '完全入库'">编辑
            </el-button>
            <el-button link
                       type="primary"
@@ -403,6 +438,16 @@
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="入库审核状态"
                           prop="stockInApprovalStatus"
                           width="120">
            <template #default="scope">
              <el-tag :type="getStockInApprovalStatusType(scope.row.stockInApprovalStatus)"
                      size="small">
                {{ scope.row.stockInApprovalStatus || '--' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column fixed="right"
                           label="操作"
                           min-width="60"
@@ -410,7 +455,8 @@
            <template #default="scope">
              <el-button link
                         type="primary"
                         @click="openProductForm('edit', scope.row, scope.$index)">编辑
                         @click="openProductForm('edit', scope.row, scope.$index)"
                         :disabled="scope.row.stockInApprovalStatus === '完全入库'">编辑
              </el-button>
            </template>
          </el-table-column>
@@ -723,6 +769,26 @@
      2: "warning", // 审批中 - 橙色
      3: "success", // 审批通过 - 绿色
      4: "danger", // 审批失败 - 红色
    };
    return typeMap[status] || "";
  };
  // 获取入库状态标签类型
  const getStockInStatusType = status => {
    const typeMap = {
      "待入库": "info", // 待入库 - 灰色
      "入库中": "warning", // 入库中 - 橙色
      "完全入库": "success", // 完全入库 - 绿色
    };
    return typeMap[status] || "";
  };
  // 获取入库审核状态标签类型
  const getStockInApprovalStatusType = status => {
    const typeMap = {
      "待入库": "info", // 待入库 - 灰色
      "入库中": "warning", // 入库中 - 橙色
      "完全入库": "success", // 完全入库 - 绿色
    };
    return typeMap[status] || "";
  };
@@ -1206,10 +1272,10 @@
  };
  // 打开弹框
  const openForm = async (type, row) => {
    // 编辑时检查审核状态,只有待审核(1)和审批失败(4)才能编辑
    // 编辑时检查入库状态,完全入库时不能编辑
    if (type === "edit" && row) {
      if (row.approvalStatus !== 1 && row.approvalStatus !== 4) {
        proxy.$modal.msgWarning("只有待审核和审批失败状态的记录才能编辑");
      if (row.stockInStatus === '完全入库') {
        proxy.$modal.msgWarning("完全入库状态的记录不能编辑");
        return;
      }
    }
@@ -1256,9 +1322,11 @@
        currentId.value = row.id;
        try {
          const purchaseRes = await getPurchaseById({ id: row.id, type: 2 });
          form.value = { ...purchaseRes };
          productData.value = purchaseRes.productData || [];
          form.value = { ...purchaseRes, stockInStatus: row.stockInStatus };
          fileList.value = purchaseRes.storageBlobVOS || [];
          // 使用 productList 接口获取产品列表,以获取入库审核状态
          const productRes = await productList({ salesLedgerId: row.id, type: 2 });
          productData.value = productRes.data || [];
        } catch (error) {
          console.error("加载采购台账数据失败:", error);
          proxy.$modal.msgError("加载数据失败");
@@ -1375,6 +1443,12 @@
  };
  // 打开产品弹框
  const openProductForm = async (type, row, index) => {
    // 编辑时检查产品入库审核状态,完全入库时不能编辑
    if (type === "edit" && row && row.stockInApprovalStatus === '完全入库') {
      proxy.$modal.msgWarning("完全入库状态的产品不能编辑");
      return;
    }
    productOperationType.value = type;
    productOperationIndex.value = index;
    productForm.value = {};
@@ -1545,8 +1619,9 @@
    addOrUpdateSalesLedgerProduct(productForm.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeProductDia();
      getPurchaseById({ id: currentId.value, type: 2 }).then(res => {
        productData.value = res.productData;
      // 使用 productList 接口刷新产品列表,以获取入库审核状态
      productList({ salesLedgerId: currentId.value, type: 2 }).then(res => {
        productData.value = res.data || [];
      });
    });
  };
@@ -1554,6 +1629,14 @@
  const deleteProduct = () => {
    if (productSelectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    // 检查选中的产品中是否有完全入库的
    const hasFullyStocked = productSelectedRows.value.some(
      row => row.stockInApprovalStatus === '完全入库'
    );
    if (hasFullyStocked) {
      proxy.$modal.msgWarning("选中的产品中包含完全入库的产品,无法删除");
      return;
    }
    if (operationType.value === "add") {
@@ -1579,8 +1662,9 @@
          delProduct(ids).then(res => {
            proxy.$modal.msgSuccess("删除成功");
            closeProductDia();
            getPurchaseById({ id: currentId.value, type: 2 }).then(res => {
              productData.value = res.productData;
            // 使用 productList 接口刷新产品列表,以获取入库审核状态
            productList({ salesLedgerId: currentId.value, type: 2 }).then(res => {
              productData.value = res.data || [];
            });
          });
        })
src/views/procurementManagement/thePaymentLedger/index.vue
@@ -33,6 +33,7 @@
<script setup>
import { ref } from "vue";
import { Search } from "@element-plus/icons-vue";
import dayjs from "dayjs";
// import { registrationList } from "@/api/procurementManagement/paymentLedger.js";
const tableColumn = ref([
  {
@@ -54,6 +55,8 @@
  {
    label: "登记日期",
    prop: "registrationtDate",
    formatData: cell =>
      cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
  },
]);
const tableData = ref([]);
src/views/productionManagement/workOrderEdit/index.vue
@@ -73,6 +73,18 @@
               title="指定报工人"
               width="800px">
      <div class="assign-reporter-content">
        <div class="search-box">
          <el-input
            v-model="employeeSearchKeyword"
            placeholder="搜索人员姓名"
            clearable
            @input="handleEmployeeSearch"
            style="width: 350px">
            <template #prefix>
              <i class="el-icon-search"></i>
            </template>
          </el-input>
        </div>
        <div class="selected-tags-box"
             v-if="selectedEmployeeIds.length > 0">
          <div class="tags-label">已选择:</div>
@@ -90,7 +102,7 @@
             v-loading="employeeTableLoading">
          <el-checkbox-group v-model="selectedEmployeeIds">
            <div class="employee-grid">
              <div v-for="item in employeeTableData"
              <div v-for="item in filteredEmployeeList"
                   :key="item.userId"
                   class="employee-item">
                <el-checkbox :label="item.userId"
@@ -103,9 +115,9 @@
              </div>
            </div>
          </el-checkbox-group>
          <div v-if="employeeTableData.length === 0"
          <div v-if="filteredEmployeeList.length === 0"
               class="empty-text">
            暂无匹配人员
            {{ employeeSearchKeyword ? '无匹配人员' : '暂无人员数据' }}
          </div>
        </div>
      </div>
@@ -121,7 +133,7 @@
</template>
<script setup>
  import { getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
  import { getCurrentInstance, onMounted, reactive, ref, toRefs, computed } from "vue";
  import { ElMessageBox } from "element-plus";
  import {
    productWorkOrderPage,
@@ -253,8 +265,24 @@
  const employeeSearchForm = reactive({
    staffName: "",
  });
  const employeeSearchKeyword = ref("");
  const selectedEmployeeIds = ref([]);
  const currentWorkOrder = ref(null);
  const filteredEmployeeList = computed(() => {
    const keyword = employeeSearchKeyword.value.trim().toLowerCase();
    if (!keyword) {
      return employeeTableData.value;
    }
    return employeeTableData.value.filter(item => {
      const name = (item.nickName || "").toLowerCase();
      const dept = (item.dept?.deptName || "").toLowerCase();
      return name.includes(keyword) || dept.includes(keyword);
    });
  });
  const handleEmployeeSearch = () => {
  };
  const data = reactive({
    searchForm: {
@@ -416,6 +444,12 @@
  }
  .assign-reporter-content {
    .search-box {
      margin-bottom: 16px;
      display: flex;
      justify-content: center;
    }
    .selected-tags-box {
      margin-bottom: 16px;
      padding: 12px;
src/views/reportAnalysis/dataDashboard/index0.vue
@@ -925,15 +925,18 @@
    })
}
// 应付应收统计
const statisticsReceivable = (type) => {
    statisticsReceivablePayable({type: radio1.value}).then((res) => {
const statisticsReceivable = (type = radio1.value) => {
    statisticsReceivablePayable({ type }).then((res) => {
        const data = res?.data || {}
        const payableMoney = Number(data.payableMoney ?? 0)
        const receivableMoney = Number(data.receivableMoney ?? 0)
        // 设置应付金额数据
        barSeries.value[0].data = [
            { value: res.data.payableMoney }
            { value: payableMoney }
        ]
        // 设置应收金额数据
        barSeries.value[1].data = [
            { value: res.data.receivableMoney }
            { value: receivableMoney }
        ]
    })
}
@@ -2031,4 +2034,4 @@
  color: #B8C8E0;
  font-size: 11px;
}
</style>
</style>
src/views/reportAnalysis/financialAnalysis/components/center-top.vue
@@ -112,50 +112,69 @@
  profitRate: { value: 0, trend: 0 },
})
const fetchMonthlyIncome = async () => {
  const res = await getMonthlyIncome()
  const data = res?.data || {}
const toNumber = (val) => {
  const num = Number(val)
  return Number.isFinite(num) ? num : 0
}
  income.value.amount = data.monthlyIncome ?? 0
  const collectionRate = Number(data.collectionRate ?? 0)
  const overdueRate = Number(data.overdueRate ?? 0)
  income.value.repayRate = {
    value: collectionRate,
    trend: collectionRate >= 0 ? 1 : -1,
  }
  income.value.overdueCount = data.overdueNum ?? 0
  income.value.overdueRate = {
    value: overdueRate,
    trend: overdueRate >= 0 ? 1 : -1,
const fetchMonthlyIncome = async () => {
  try {
    const res = await getMonthlyIncome()
    const data = res?.data || {}
    income.value.amount = toNumber(data.monthlyIncome)
    const collectionRate = toNumber(data.collectionRate)
    const overdueRate = toNumber(data.overdueRate)
    income.value.repayRate = {
      value: collectionRate,
      trend: collectionRate >= 0 ? 1 : -1,
    }
    income.value.overdueCount = toNumber(data.overdueNum)
    income.value.overdueRate = {
      value: overdueRate,
      trend: overdueRate >= 0 ? 1 : -1,
    }
  } catch {
    income.value.amount = 0
    income.value.repayRate = { value: 0, trend: 0 }
    income.value.overdueCount = 0
    income.value.overdueRate = { value: 0, trend: 0 }
  }
}
const fetchMonthlyExpenditure = async () => {
  const res = await getMonthlyExpenditure()
  const data = res?.data || {}
  try {
    const res = await getMonthlyExpenditure()
    const data = res?.data || {}
  expense.value.amount = data.monthlyExpenditure ?? 0
  const paymentRate = Number(data.paymentRate ?? 0)
  expense.value.netProfit = {
    value: paymentRate,
    trend: paymentRate >= 0 ? 1 : -1,
  }
  expense.value.grossProfit = data.grossProfit ?? 0
    expense.value.amount = toNumber(data.monthlyExpenditure)
    const paymentRate = toNumber(data.paymentRate)
    expense.value.netProfit = {
      value: paymentRate,
      trend: paymentRate >= 0 ? 1 : -1,
    }
    expense.value.grossProfit = toNumber(data.grossProfit)
  const profitMarginRate = Number(data.profitMarginRate ?? 0)
  expense.value.profitRate = {
    value: profitMarginRate,
    trend: profitMarginRate >= 0 ? 1 : -1,
    const profitMarginRate = toNumber(data.profitMarginRate)
    expense.value.profitRate = {
      value: profitMarginRate,
      trend: profitMarginRate >= 0 ? 1 : -1,
    }
  } catch {
    expense.value.amount = 0
    expense.value.netProfit = { value: 0, trend: 0 }
    expense.value.grossProfit = 0
    expense.value.profitRate = { value: 0, trend: 0 }
  }
}
const isWanAmount = (val) => {
  const num = Number(val) || 0
  const num = toNumber(val)
  return Math.abs(num) >= 10000
}
const formatAmountWanNumber = (val) => {
  const num = Number(val) || 0
  const num = toNumber(val)
  if (Math.abs(num) >= 10000) {
    return (num / 10000).toFixed(2)
  }
@@ -163,7 +182,7 @@
}
const formatPercent = (val) => {
  const num = Number(val) || 0
  const num = toNumber(val)
  // 百分比展示始终用绝对值,小数保留两位
  return `${Math.abs(num).toFixed(2)}%`
}
src/views/system/user/index.vue
@@ -511,6 +511,9 @@
        roleOptions.value = response.roles
        form.value.postIds = response.postIds
        form.value.roleIds = response.roleIds
        if (response.deptIds && response.deptIds.length > 0) {
            form.value.deptId = response.deptIds[0]
        }
        open.value = true
        title.value = "修改用户"
        form.password = ""