liding
5 天以前 43d00eec5989caa78c44c736853230a13d8e9f6c
财务管理系统模块优化
已添加4个文件
已修改9个文件
1646 ■■■■■ 文件已修改
reconciliation-repro.html 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/expenseManagement.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/financialReconciliation.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/fundsManagement.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/expenseManagement/ExpenseCategoryReport.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/expenseManagement/Form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/expenseManagement/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/financialReconciliation/ReconciliationWorkspace.vue 484 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/financialReconciliation/index.vue 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/financialStatements/index.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/fundsManagement/FinanceWorkspace.vue 651 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/fundsManagement/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/index.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
reconciliation-repro.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>reconciliation repro</title></head>
<body>
<div id="app"></div>
<script type="module">
    import {createApp} from '/node_modules/.vite/deps/vue.js';
    import ElementPlus from '/node_modules/.vite/deps/element-plus.js';
    import '/node_modules/element-plus/dist/index.css';
    import ReconciliationWorkspace
        from '/src/views/financialManagement/financialReconciliation/ReconciliationWorkspace.vue';
    createApp(ReconciliationWorkspace).use(ElementPlus).mount('#app');
</script>
</body>
</html>
src/api/financialManagement/expenseManagement.js
@@ -76,3 +76,11 @@
    data: query,
  });
}
export function getExpenseCategoryReport(params) {
  return request({
    url: "/account/accountExpense/report/category",
    method: "get",
    params,
  });
}
src/api/financialManagement/financialReconciliation.js
@@ -35,3 +35,11 @@
    data: ids,
  });
};
export const getReconciliationTaskPage = (params) => request({ url: "/finance/reconciliation/task/page", method: "get", params });
export const createReconciliationTask = (data) => request({ url: "/finance/reconciliation/task", method: "post", data });
export const getReconciliationDiff = (id) => request({ url: `/finance/reconciliation/task/${id}/diff`, method: "get" });
export const getReconciliationExternal = (id) => request({ url: `/finance/reconciliation/task/${id}/external`, method: "get" });
export const handleReconciliationTask = (id, data) => request({ url: `/finance/reconciliation/task/${id}/handle`, method: "put", data });
export const archiveReconciliationTask = (id) => request({ url: `/finance/reconciliation/task/${id}/archive`, method: "put" });
export const exportReconciliationDiff = (taskId) => request({ url: "/finance/reconciliation/export", method: "post", data: { taskId }, responseType: "blob" });
src/api/financialManagement/fundsManagement.js
@@ -12,12 +12,27 @@
// æ–°å¢žèµ„金记录
export const addFunds = (data) => {
  return request({
    url: "/finance/funds",
    url: "/finance/funds/add",
    method: "post",
    data,
  });
};
export const transferFunds = (data) => request({ url: "/finance/funds/transfer", method: "post", data });
export const getFinanceAccountPage = (params) => request({ url: "/finance/account/page", method: "get", params });
export const getFinanceAccountList = () => request({ url: "/finance/account/list", method: "get" });
export const addFinanceAccount = (data) => request({ url: "/finance/account", method: "post", data });
export const updateFinanceAccount = (data) => request({ url: "/finance/account", method: "put", data });
export const deleteFinanceAccount = (ids) => request({ url: "/finance/account", method: "delete", data: ids });
export const getFundsTrend = (params) => request({ url: "/finance/account/trend", method: "get", params });
export const getCashflowPlanPage = (params) => request({ url: "/finance/cashflowPlan/page", method: "get", params });
export const addCashflowPlan = (data) => request({ url: "/finance/cashflowPlan", method: "post", data });
export const updateCashflowPlan = (data) => request({ url: "/finance/cashflowPlan", method: "put", data });
export const deleteCashflowPlan = (ids) => request({ url: "/finance/cashflowPlan", method: "delete", data: ids });
export const getCashflowForecast = (params) => request({ url: "/finance/cashflowPlan/forecast", method: "get", params });
// æ›´æ–°èµ„金记录
export const updateFunds = (data) => {
  return request({
src/views/financialManagement/expenseManagement/ExpenseCategoryReport.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
<template>
  <el-collapse class="report-panel">
    <el-collapse-item title="支出大类占比与趋势" name="category">
      <div class="charts">
        <div ref="pieRef"></div>
        <div ref="trendRef"></div>
      </div>
    </el-collapse-item>
  </el-collapse>
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import * as echarts from "echarts";
import { getExpenseCategoryReport } from "@/api/financialManagement/expenseManagement";
const pieRef = ref(),
  trendRef = ref();
onMounted(async () => {
  const r = await getExpenseCategoryReport({});
  const share = r.data?.share || [],
    trend = r.data?.trend || [];
  await nextTick();
  echarts.init(pieRef.value).setOption({
    title: { text: "支出大类占比", left: "center" },
    tooltip: { trigger: "item" },
    series: [
      {
        type: "pie",
        radius: ["35%", "68%"],
        data: share.map((x) => ({ name: x.categoryName, value: x.amount })),
      },
    ],
  });
  const months = [...new Set(trend.map((x) => x.month))],
    names = [...new Set(trend.map((x) => x.categoryName))];
  echarts.init(trendRef.value).setOption({
    title: { text: "支出大类月度趋势", left: "center" },
    tooltip: { trigger: "axis" },
    legend: { top: 28 },
    xAxis: { type: "category", data: months },
    yAxis: { type: "value" },
    series: names.map((name) => ({
      name,
      type: "line",
      data: months.map(
        (month) =>
          trend.find((x) => x.month === month && x.categoryName === name)
            ?.amount || 0
      ),
    })),
  });
});
</script>
<style scoped>
.report-panel {
  margin-bottom: 12px;
}
.charts {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 12px;
}
.charts > div {
  height: 300px;
}
@media (max-width: 900px) {
  .charts {
    grid-template-columns: 1fr;
  }
}
</style>
src/views/financialManagement/expenseManagement/Form.vue
@@ -11,6 +11,11 @@
            clearable
          />
        </el-form-item>
        <el-form-item label="支出大类" prop="expenseCategory">
          <el-select v-model="form.expenseCategory" placeholder="请选择支出大类" clearable>
            <el-option v-for="item in finance_expense_category" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="支出类型" prop="expenseType">
          <el-select
            v-model="form.expenseType"
@@ -65,6 +70,7 @@
  name: "新增支出",
});
const { expense_types } = proxy.useDict("expense_types");
const { finance_expense_category } = proxy.useDict("finance_expense_category");
const { checkout_payment } = proxy.useDict("checkout_payment");
const formRef = ref(null);
const formRules = {
@@ -73,12 +79,14 @@
    expenseDescribed: [{ required: true, trigger: "blur", message: "请输入" }],
    expenseDate: [{ required: true, trigger: "change", message: "请选择" }],
  expenseType: [{ required: true, trigger: "change", message: "请选择" }],
  expenseCategory: [{ required: true, trigger: "change", message: "请选择支出大类" }],
  expenseMethod: [{ required: true, trigger: "change", message: "请选择" }],
}
const { form, resetForm } = useFormData({
  expenseDate: undefined, // æ”¯å‡ºæ—¥æœŸ
  expenseType: undefined, // æ”¯å‡ºç±»åž‹
  expenseCategory: undefined, // æ”¯å‡ºå¤§ç±»
  supplierName: undefined, // å®¢æˆ·åç§°
  expenseMoney: undefined, // æ”¯å‡ºé‡‘额
  expenseDescribed: undefined, // æ”¯å‡ºæè¿°
@@ -92,6 +100,7 @@
  if (code == 200) {
    form.expenseDate = data.expenseDate;
    form.expenseType = data.expenseType;
    form.expenseCategory = data.expenseCategory;
    form.supplierName = data.supplierName;
    form.expenseMoney = data.expenseMoney;
    form.expenseDescribed = data.expenseDescribed;
src/views/financialManagement/expenseManagement/index.vue
@@ -1,9 +1,15 @@
<template>
  <div class="app-container">
    <ExpenseCategoryReport />
    <el-form :model="filters" :inline="true">
      <el-form-item label="支出日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
      </el-form-item>
      <el-form-item label="支出大类:">
        <el-select v-model="filters.expenseCategory" placeholder="请选择" clearable style="width: 160px">
          <el-option v-for="item in finance_expense_category" :key="item.value" :label="item.label" :value="item.value" />
        </el-select>
      </el-form-item>
      <el-form-item label="付款方式:">
        <el-select
@@ -81,6 +87,7 @@
import { ElMessageBox, ElMessage } from "element-plus";
import dayjs from "dayjs";
import FilesDia from "../revenueManagement/filesDia.vue";
import ExpenseCategoryReport from "./ExpenseCategoryReport.vue";
defineOptions({
  name: "支出管理",
@@ -92,6 +99,7 @@
const modalRef = ref();
const { checkout_payment } = proxy.useDict("checkout_payment");
const { expense_types } = proxy.useDict("expense_types");
const { finance_expense_category } = proxy.useDict("finance_expense_category");
const filesDia = ref()
const {
@@ -106,6 +114,7 @@
  listPage,
  {
    expenseMethod: undefined,
    expenseCategory: undefined,
    entryDate: undefined,
  },
  [
@@ -115,6 +124,13 @@
      prop: "expenseDate",
    },
    {
      label: "支出大类",
      align: "center",
      prop: "expenseCategory",
      dataType: "tag",
      formatData: (params) => finance_expense_category.value.find((m) => m.value == params)?.label || "未分类",
    },
    {
      label: "支出类型",
      align: "center",
      prop: "expenseType",
src/views/financialManagement/financialReconciliation/ReconciliationWorkspace.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,484 @@
<template>
  <div class="reconciliation-workspace">
    <div class="workspace-heading">
      <h2>财务对账</h2>
      <p>比对收支、资金流水与外部凭证,记录差异处理方案并归档。</p>
    </div>
    <el-form inline :model="query">
      <el-form-item label="对账类型"
        ><el-select v-model="query.reconType" clearable style="width: 150px"
          ><el-option
            v-for="(label, key) in reconTypeText"
            :key="key"
            :label="label"
            :value="key" /></el-select
      ></el-form-item>
      <el-form-item label="状态"
        ><el-select v-model="query.status" clearable style="width: 130px"
          ><el-option
            v-for="(label, key) in statusText"
            :key="key"
            :label="label"
            :value="key" /></el-select
      ></el-form-item>
      <el-form-item label="对象"
        ><el-input v-model="query.target" clearable
      /></el-form-item>
      <el-form-item
        ><el-button type="primary" @click="loadTasks">查询</el-button
        ><el-button @click="openCreate">发起对账</el-button></el-form-item
      >
    </el-form>
    <el-alert
      v-if="loadError"
      :title="loadError"
      type="error"
      show-icon
      :closable="false"
      class="load-error"
    />
    <el-table
      :data="tasks"
      border
      v-loading="loading"
      empty-text="暂无对账任务,可点击“发起对账”创建"
    >
      <el-table-column prop="id" label="任务号" width="80" />
      <el-table-column label="类型"
        ><template #default="{ row }">{{
          reconTypeText[row.reconType]
        }}</template></el-table-column
      >
      <el-table-column label="期间" min-width="190"
        ><template #default="{ row }"
          >{{ row.periodStart }} è‡³ {{ row.periodEnd }}</template
        ></el-table-column
      >
      <el-table-column prop="target" label="对账对象" />
      <el-table-column prop="systemAmount" label="系统金额" />
      <el-table-column prop="externalAmount" label="对方金额" />
      <el-table-column prop="diffAmount" label="差额" />
      <el-table-column label="结果"
        ><template #default="{ row }"
          ><el-tag :type="row.result === '0' ? 'success' : 'danger'">{{
            row.result === "0" ? "一致" : "有差异"
          }}</el-tag></template
        ></el-table-column
      >
      <el-table-column label="状态"
        ><template #default="{ row }">{{
          statusText[row.status]
        }}</template></el-table-column
      >
      <el-table-column label="操作" width="260" fixed="right"
        ><template #default="{ row }">
          <el-button link type="primary" @click="showDiff(row)"
            >差异明细</el-button
          >
          <el-button
            link
            type="primary"
            :disabled="row.status === '2'"
            @click="openHandle(row)"
            >处理</el-button
          >
          <el-button
            link
            type="success"
            :disabled="row.status === '2'"
            @click="archive(row)"
            >归档</el-button
          >
          <el-button link @click="exportDiff(row)">导出</el-button>
        </template></el-table-column
      >
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      :page="query.current"
      :limit="query.size"
      @pagination="
        (p) => {
          query.current = p.page;
          query.size = p.limit;
          loadTasks();
        }
      "
    />
    <el-dialog v-model="createVisible" title="发起对账" width="760px">
      <el-form :model="createForm" label-width="100px">
        <el-form-item label="对账类型"
          ><el-select v-model="createForm.reconType"
            ><el-option
              v-for="(label, key) in reconTypeText"
              :key="key"
              :label="label"
              :value="key" /></el-select
        ></el-form-item>
        <el-form-item label="对账期间"
          ><el-date-picker
            v-model="period"
            type="daterange"
            value-format="YYYY-MM-DD"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
        /></el-form-item>
        <el-form-item label="资金账户"
          ><el-select v-model="createForm.accountId" clearable
            ><el-option
              v-for="a in accounts"
              :key="a.id"
              :label="a.accountName"
              :value="a.id" /></el-select
        ></el-form-item>
        <el-form-item label="对账对象"
          ><el-input
            v-model="createForm.target"
            placeholder="银行/供应商/客户名称"
        /></el-form-item>
      </el-form>
      <template v-if="createForm.reconType !== '0'">
        <el-divider content-position="left">外部流水(手工录入)</el-divider>
        <div class="external-actions">
          <el-button size="small" @click="addExternal">新增外部流水</el-button>
        </div>
        <el-table :data="createForm.externalList" border max-height="260">
          <el-table-column label="日期" width="145"
            ><template #default="{ row }"
              ><el-date-picker
                v-model="row.extDate"
                value-format="YYYY-MM-DD"
                style="width: 125px" /></template
          ></el-table-column>
          <el-table-column label="往来单位"
            ><template #default="{ row }"
              ><el-input v-model="row.extCounterparty" /></template
          ></el-table-column>
          <el-table-column label="方向" width="100"
            ><template #default="{ row }"
              ><el-select v-model="row.direction"
                ><el-option label="收入" :value="1" /><el-option
                  label="支出"
                  :value="-1" /></el-select></template
          ></el-table-column>
          <el-table-column label="金额" width="140"
            ><template #default="{ row }"
              ><el-input-number
                v-model="row.amount"
                :min="0.01"
                :precision="2"
                style="width: 120px" /></template
          ></el-table-column>
          <el-table-column label="凭证号"
            ><template #default="{ row }"
              ><el-input v-model="row.voucherNo" /></template
          ></el-table-column>
          <el-table-column width="60"
            ><template #default="{ $index }"
              ><el-button
                link
                type="danger"
                @click="createForm.externalList.splice($index, 1)"
                >删</el-button
              ></template
            ></el-table-column
          >
        </el-table>
      </template>
      <template #footer
        ><el-button @click="createVisible = false">取消</el-button
        ><el-button type="primary" @click="submitTask"
          >开始比对</el-button
        ></template
      >
    </el-dialog>
    <el-drawer v-model="diffVisible" title="对账差异明细" size="70%">
      <el-descriptions v-if="currentTask" :column="4" border class="summary">
        <el-descriptions-item label="任务号">{{
          currentTask.id
        }}</el-descriptions-item
        ><el-descriptions-item label="系统金额">{{
          currentTask.systemAmount
        }}</el-descriptions-item
        ><el-descriptions-item label="对方金额">{{
          currentTask.externalAmount
        }}</el-descriptions-item
        ><el-descriptions-item label="差额">{{
          currentTask.diffAmount
        }}</el-descriptions-item>
      </el-descriptions>
      <el-descriptions
        v-if="currentTask?.handleNote"
        :column="1"
        border
        class="summary"
      >
        <el-descriptions-item label="处理方案">{{
          currentTask.handleNote
        }}</el-descriptions-item>
      </el-descriptions>
      <el-table :data="diffs" border>
        <el-table-column label="差异类型"
          ><template #default="{ row }">{{
            diffTypeText[row.diffType]
          }}</template></el-table-column
        >
        <el-table-column prop="bizType" label="业务类型" /><el-table-column
          prop="bizId"
          label="业务ID"
        /><el-table-column prop="bizDate" label="日期" /><el-table-column
          prop="counterparty"
          label="往来单位"
        /><el-table-column
          prop="systemAmount"
          label="系统金额"
        /><el-table-column
          prop="externalAmount"
          label="对方金额"
        /><el-table-column prop="remark" label="说明" min-width="190" />
      </el-table>
      <el-divider content-position="left">外部凭证</el-divider>
      <el-table :data="externals" border
        ><el-table-column prop="extDate" label="日期" /><el-table-column
          prop="extCounterparty"
          label="往来单位" /><el-table-column
          prop="amount"
          label="金额" /><el-table-column prop="direction" label="方向"
          ><template #default="{ row }">{{
            row.direction === 1 ? "收入" : "支出"
          }}</template></el-table-column
        ><el-table-column prop="voucherNo" label="凭证号"
      /></el-table>
    </el-drawer>
    <el-dialog v-model="handleVisible" title="填写处理方案" width="520px">
      <el-input
        v-model="handleForm.handleNote"
        type="textarea"
        :rows="5"
        placeholder="请输入差异处理方案"
      />
      <template #footer
        ><el-button @click="handleVisible = false">取消</el-button
        ><el-button type="primary" @click="saveHandle"
          >保存并标记处理中</el-button
        ></template
      >
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { saveAs } from "file-saver";
import pagination from "@/components/PIMTable/Pagination.vue";
import { getFinanceAccountList } from "@/api/financialManagement/fundsManagement";
import {
  getReconciliationTaskPage,
  createReconciliationTask,
  getReconciliationDiff,
  getReconciliationExternal,
  handleReconciliationTask,
  archiveReconciliationTask,
  exportReconciliationDiff,
} from "@/api/financialManagement/financialReconciliation";
const reconTypeText = {
    0: "内部对账",
    1: "银行对账",
    2: "供应商对账",
    3: "客户对账",
  },
  statusText = { 0: "待处理", 1: "处理中", 2: "已归档" },
  diffTypeText = {
    0: "系统有外部无",
    1: "外部有系统无",
    2: "金额不符",
    3: "日期不符",
  };
const tasks = ref([]),
  total = ref(0),
  loading = ref(false),
  loadError = ref(""),
  accounts = ref([]),
  createVisible = ref(false),
  diffVisible = ref(false),
  handleVisible = ref(false),
  period = ref([]),
  diffs = ref([]),
  externals = ref([]),
  currentTask = ref(null);
const query = reactive({
    current: 1,
    size: 10,
    reconType: "",
    status: "",
    target: "",
  }),
  createForm = reactive({
    reconType: "0",
    accountId: null,
    target: "",
    externalList: [],
  }),
  handleForm = reactive({ id: null, handleNote: "" });
const loadTasks = async () => {
  loading.value = true;
  loadError.value = "";
  try {
    const r = await getReconciliationTaskPage(query);
    tasks.value = r.data?.records || [];
    total.value = r.data?.total || 0;
  } catch (error) {
    tasks.value = [];
    total.value = 0;
    loadError.value =
      error?.message ||
      error?.msg ||
      "对账任务加载失败,请检查后端服务及数据库脚本";
  } finally {
    loading.value = false;
  }
};
const openCreate = () => {
  Object.assign(createForm, {
    reconType: "0",
    accountId: null,
    target: "",
    externalList: [],
  });
  period.value = [];
  createVisible.value = true;
};
const addExternal = () =>
  createForm.externalList.push({
    extDate: "",
    extCounterparty: "",
    amount: null,
    direction: 1,
    voucherNo: "",
    remark: "",
  });
const submitTask = async () => {
  if (period.value.length !== 2) {
    ElMessage.warning("请选择对账期间");
    return;
  }
  if (createForm.reconType !== "0" && !createForm.target.trim()) {
    ElMessage.warning("请输入银行、供应商或客户名称");
    return;
  }
  if (
    createForm.reconType !== "0" &&
    createForm.externalList.some(
      (row) => !row.extDate || !row.amount || row.amount <= 0
    )
  ) {
    ElMessage.warning("请填写外部凭证的日期和正数金额");
    return;
  }
  try {
    await createReconciliationTask({
      ...createForm,
      periodStart: period.value[0],
      periodEnd: period.value[1],
    });
    ElMessage.success("对账任务已生成");
    createVisible.value = false;
    await loadTasks();
  } catch (error) {
    ElMessage.error(error?.message || "对账任务生成失败");
  }
};
const showDiff = async (row) => {
  currentTask.value = row;
  const [d, e] = await Promise.all([
    getReconciliationDiff(row.id),
    getReconciliationExternal(row.id),
  ]);
  diffs.value = d.data || [];
  externals.value = e.data || [];
  diffVisible.value = true;
};
const openHandle = (row) => {
  handleForm.id = row.id;
  handleForm.handleNote = row.handleNote || "";
  handleVisible.value = true;
};
const saveHandle = async () => {
  await handleReconciliationTask(handleForm.id, {
    handleNote: handleForm.handleNote,
    status: "1",
  });
  ElMessage.success("处理方案已保存");
  handleVisible.value = false;
  loadTasks();
};
const archive = async (row) => {
  if (row.result === "1" && !row.handleNote?.trim()) {
    ElMessage.warning("请先填写差异处理方案");
    return;
  }
  try {
    await ElMessageBox.confirm(
      "归档后该任务将进入已归档状态,是否继续?",
      "归档"
    );
    await archiveReconciliationTask(row.id);
    ElMessage.success("已归档");
    await loadTasks();
  } catch (error) {
    if (error !== "cancel" && error !== "close") {
      ElMessage.error(error?.message || "归档失败");
    }
  }
};
const exportDiff = async (row) => {
  const blob = await exportReconciliationDiff(row.id);
  saveAs(blob, `对账差异-${row.id}.xlsx`);
};
onMounted(() => {
  nextTick(() => {
    loadTasks();
    getFinanceAccountList()
      .then((r) => {
        accounts.value = r.data || [];
      })
      .catch((error) => {
        accounts.value = [];
        console.error("资金账户加载失败:", error);
      });
  });
});
</script>
<style scoped>
.reconciliation-workspace {
  padding: 4px;
}
.workspace-heading {
  margin-bottom: 16px;
}
.workspace-heading h2 {
  margin: 0 0 6px;
  font-size: 20px;
}
.workspace-heading p {
  margin: 0;
  color: #606266;
}
.load-error {
  margin-bottom: 12px;
}
.external-actions {
  margin-bottom: 8px;
}
.summary {
  margin-bottom: 16px;
}
</style>
src/views/financialManagement/financialReconciliation/index.vue
@@ -1,291 +1,46 @@
<template>
  <div class="app-container">
    <!-- æœç´¢åŒº -->
    <div class="search_form">
      <el-form :inline="true" :model="searchForm">
        <el-form-item label="往来单位:">
          <el-input v-model="searchForm.counterparty" placeholder="请输入往来单位" clearable style="width: 180px" />
        </el-form-item>
        <el-form-item label="对账状态:">
          <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 150px">
            <el-option label="未对账" value="pending" />
            <el-option label="对账中" value="processing" />
            <el-option label="已完成" value="done" />
          </el-select>
        </el-form-item>
        <!-- <el-form-item label="日期:">
          <el-date-picker
            v-model="searchForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            clearable
          />
        </el-form-item> -->
        <el-form-item>
          <el-button type="primary" @click="handleQuery">搜索</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
    <!-- æ“ä½œåŒº -->
    <div class="actions">
      <el-button type="primary" @click="openDialog('add')">新增</el-button>
      <el-button type="danger" plain @click="handleDelete">删除</el-button>
    </div>
    <!-- è¡¨æ ¼ -->
    <el-table
      :data="tableData"
      border
      v-loading="tableLoading"
      @selection-change="handleSelectionChange"
      style="width: 100%"
      height="calc(100vh - 280px)"
      :row-key="row => row.id"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column type="index" label="序号" width="60" align="center" />
      <el-table-column prop="accountDate" label="对账日期" width="130" show-overflow-tooltip />
      <el-table-column prop="counterparty" label="往来单位" width="180" show-overflow-tooltip />
      <el-table-column prop="amount" label="对账金额(元)" width="140" show-overflow-tooltip />
      <el-table-column prop="statusText" label="状态" width="120" show-overflow-tooltip />
      <el-table-column prop="remark" label="备注" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="150" align="center">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="openDialog('edit', scope.row)">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      layout="total, sizes, prev, pager, next, jumper"
      :page="page.current"
      :limit="page.size"
      @pagination="paginationChange"
  <div class="app-container financial-reconciliation-page">
    <el-alert
      v-if="runtimeError"
      title="财务对账页面加载失败"
      type="error"
      :description="runtimeError"
      show-icon
      :closable="false"
    />
    <!-- å¼¹çª— -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" @close="resetForm">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-form-item label="对账日期:" prop="accountDate">
          <el-date-picker v-model="form.accountDate" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" style="width: 100%" />
        </el-form-item>
        <el-form-item label="往来单位:" prop="counterparty">
          <el-input v-model="form.counterparty" placeholder="请输入往来单位" />
        </el-form-item>
        <el-form-item label="对账金额(元):" prop="amount">
          <el-input-number v-model="form.amount" :min="0" :precision="2" :step="100" style="width: 100%" />
        </el-form-item>
        <el-form-item label="状态:" prop="status">
          <el-select v-model="form.status" placeholder="请选择" style="width: 100%">
            <el-option label="未对账" value="pending" />
            <el-option label="对账中" value="processing" />
            <el-option label="已完成" value="done" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注:">
          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确认</el-button>
        </div>
      </template>
    </el-dialog>
    <ReconciliationWorkspace v-else :key="workspaceKey" />
    <el-button v-if="runtimeError" type="primary" class="retry-button" @click="retry">
      é‡æ–°åŠ è½½
    </el-button>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import pagination from '@/components/PIMTable/Pagination.vue'
import {
  getReconciliationPage,
  addReconciliation,
  updateReconciliation,
  deleteReconciliation
} from '@/api/financialManagement/financialReconciliation.js'
import { nextTick, onErrorCaptured, ref } from "vue";
import ReconciliationWorkspace from "./ReconciliationWorkspace.vue";
const tableData = ref([])
const tableLoading = ref(false)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('新增')
const formRef = ref(null)
const runtimeError = ref("");
const workspaceKey = ref(0);
const page = reactive({
  current: 1,
  size: 100
})
const total = ref(0)
onErrorCaptured((error) => {
  console.error("财务对账页面运行异常:", error);
  runtimeError.value = error?.stack || error?.message || String(error);
  return false;
});
const searchForm = reactive({
  counterparty: '',
  status: '',
  dateRange: []
})
const form = reactive({
  id: null,
  accountDate: '',
  counterparty: '',
  amount: null,
  status: '',
  remark: ''
})
const rules = {
  accountDate: [{ required: true, message: '请选择对账日期', trigger: 'change' }],
  counterparty: [{ required: true, message: '请输入往来单位', trigger: 'blur' }],
  amount: [{ required: true, message: '请输入对账金额', trigger: 'blur' }],
  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
const statusTextMap = {
  pending: '未对账',
  processing: '对账中',
  done: '已完成'
}
// èŽ·å–åˆ—è¡¨
const getList = () => {
  tableLoading.value = true
  const params = {
    ...searchForm,
    current: page.current,
    size: page.size
  }
  if (params.dateRange && params.dateRange.length === 2) {
    params.beginDate = params.dateRange[0]
    params.endDate = params.dateRange[1]
  }
  delete params.dateRange
  getReconciliationPage(params)
    .then(res => {
      if (res.code === 200) {
        tableData.value = (res.data?.records || []).map(item => ({
          ...item,
          statusText: statusTextMap[item.status] || item.status
        }))
        total.value = res.data?.total || 0
      }
    })
    .finally(() => {
      tableLoading.value = false
    })
}
const handleQuery = () => {
  page.current = 1
  getList()
}
const resetQuery = () => {
  searchForm.counterparty = ''
  searchForm.status = ''
  searchForm.dateRange = []
  handleQuery()
}
const paginationChange = (obj) => {
  page.current = obj.page
  page.size = obj.limit
  getList()
}
const handleSelectionChange = (rows) => {
  selectedRows.value = rows
}
const openDialog = (type, row) => {
  dialogTitle.value = type === 'add' ? '新增对账记录' : '编辑对账记录'
  if (type === 'edit' && row) {
    Object.assign(form, row)
  } else {
    resetForm()
  }
  dialogVisible.value = true
}
const resetForm = () => {
  Object.assign(form, {
    id: null,
    date: '',
    counterparty: '',
    amount: null,
    status: '',
    remark: ''
  })
}
const submitForm = () => {
  formRef.value?.validate(valid => {
    if (!valid) return
    const payload = { ...form }
    const request = payload.id ? updateReconciliation(payload) : addReconciliation(payload)
    request
      .then(res => {
        if (res.code === 200) {
          ElMessage.success('提交成功')
          dialogVisible.value = false
          getList()
        }
      })
      .catch(err => {
        ElMessage.error(err.msg || '提交失败')
      })
  })
}
const handleDelete = () => {
  if (!selectedRows.value.length) {
    ElMessage.warning('请选择要删除的记录')
    return
  }
  ElMessageBox.confirm('确认删除选中记录吗?', '提示', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const ids = selectedRows.value.map(r => r.id)
    deleteReconciliation(ids).then(res => {
      if (res.code === 200) {
        ElMessage.success('删除成功')
        getList()
        selectedRows.value = []
      }
    }).catch(err => {
      ElMessage.error(err.msg || '删除失败')
    })
  }).catch(() => {})
}
onMounted(() => {
  getList()
})
const retry = async () => {
  runtimeError.value = "";
  await nextTick();
  workspaceKey.value += 1;
};
</script>
<style scoped lang="scss">
.app-container {
  padding: 20px;
  .search_form {
    background: #fff;
    padding: 16px;
    border-radius: 6px;
    margin-bottom: 16px;
  }
  .actions {
    margin-bottom: 12px;
  }
<style scoped>
.financial-reconciliation-page {
  min-height: calc(100vh - 84px);
  background: #fff;
}
.retry-button {
  margin-top: 16px;
}
</style>
src/views/financialManagement/financialStatements/index.vue
@@ -130,14 +130,20 @@
</template>
<script setup>
defineOptions({ name: 'FinancialStatements' })
import { ref, computed, onMounted, reactive } from 'vue';
import { ElMessage } from 'element-plus';
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";
// æ—¥æœŸèŒƒå›´
const dateRange = ref(null);
const currentYear = dayjs().year();
const dateRange = ref([
  dayjs().startOf('year').format('YYYY-MM-DD'),
  dayjs().endOf('year').format('YYYY-MM-DD')
]);
const chartStyle = {
    width: '100%',
    height: '100%', // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
@@ -315,8 +321,16 @@
})
const pageInfo = ref({
})
const emptyPageInfo = () => ({
  totalIncome: 0,
  incomeNumber: 0,
  totalExpense: 0,
  expenseNumber: 0,
  netRevenue: 0,
  incomeType: [],
  expenseType: []
});
const pageInfo = ref(emptyPageInfo());
const getData = async () => {
  if (!dateRange.value || !dateRange.value.length) {
@@ -325,14 +339,14 @@
  try {
    const {code,data} = await reportForms({entryDateStart:dateRange.value[0], entryDateEnd:dateRange.value[1]});
    if(code === 200) {
      pageInfo.value = data
      pieData0.value = data.incomeType.map(item=>({
      pageInfo.value = { ...emptyPageInfo(), ...(data || {}) }
      pieData0.value = (data?.incomeType || []).map(item=>({
        name:item.typeName,
        value:item.account,
        percent:`${item.proportion*100}%`,
        amount:`Â¥${item.account}`
      }))
      pieData1.value = data.expenseType.map(item=>({
      pieData1.value = (data?.expenseType || []).map(item=>({
        name:item.typeName,
        value:item.account,
        percent:`${item.proportion*100}%`,
@@ -341,7 +355,11 @@
    }
  } catch (error) {
    pageInfo.value = emptyPageInfo();
    pieData0.value = [];
    pieData1.value = [];
    console.error('获取财务指标数据失败:', error);
    ElMessage.error(error?.msg || '财务汇总数据加载失败');
  }
  try{
    const {code,data} = await reportIncome();
@@ -349,7 +367,7 @@
      lineSeries0.value = data.map(item=>({
        name:item.typeName,
        type: 'line',
        data:item.account.map(item=>Number(item))
        data:(item.account || []).map(value => Number(value || 0))
      }))
    }
@@ -362,7 +380,7 @@
      lineSeries1.value = data.map(item=>({
        name:item.typeName,
        type: 'line',
        data:item.account.map(item=>Number(item))
        data:(item.account || []).map(value => Number(value || 0))
      }))
    }
@@ -374,7 +392,7 @@
// åˆå§‹åŒ–
onMounted(() => {
  // ä¸è®¾ç½®é»˜è®¤æ—¥æœŸï¼Œç”±ç”¨æˆ·æ‰‹åŠ¨é€‰æ‹©
  getData();
});
// å¤„理日期范围变化
@@ -387,7 +405,11 @@
// é‡ç½®æ—¥æœŸèŒƒå›´
const resetDateRange = () => {
  dateRange.value = null;
  dateRange.value = [
    `${currentYear}-01-01`,
    `${currentYear}-12-31`
  ];
  getData();
};
</script>
src/views/financialManagement/fundsManagement/FinanceWorkspace.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,651 @@
<template>
  <div class="finance-workspace">
    <el-tabs v-model="activeTab" @tab-change="loadTab">
      <el-tab-pane label="资金账户" name="accounts">
        <div class="toolbar">
          <el-button type="primary" @click="openAccount()">新增账户</el-button>
        </div>
        <div class="balance-grid">
          <el-card v-for="item in accounts" :key="item.id" shadow="hover">
            <div class="balance-title">
              {{ item.accountName }}
              <el-tag size="small">{{
                accountTypeText[item.accountType]
              }}</el-tag>
            </div>
            <div class="balance-amount">Â¥ {{ money(item.currentBalance) }}</div>
            <div class="muted">安全线 Â¥ {{ money(item.safetyLine) }}</div>
          </el-card>
        </div>
        <el-table :data="accounts" border v-loading="loading">
          <el-table-column prop="accountName" label="账户名称" />
          <el-table-column label="类型"
            ><template #default="{ row }">{{
              accountTypeText[row.accountType]
            }}</template></el-table-column
          >
          <el-table-column prop="bankName" label="开户行" />
          <el-table-column prop="bankNo" label="账号" />
          <el-table-column prop="openingBalance" label="期初余额" />
          <el-table-column prop="currentBalance" label="当前余额" />
          <el-table-column label="状态"
            ><template #default="{ row }">{{
              row.status === "0" ? "启用" : "停用"
            }}</template></el-table-column
          >
          <el-table-column label="操作" width="140"
            ><template #default="{ row }"
              ><el-button link type="primary" @click="openAccount(row)"
                >编辑</el-button
              ><el-button link type="danger" @click="removeAccount(row)"
                >删除</el-button
              ></template
            ></el-table-column
          >
        </el-table>
        <el-divider content-position="left">近30日资金流趋势</el-divider>
        <div ref="trendRef" class="chart"></div>
      </el-tab-pane>
      <el-tab-pane label="资金流水" name="flows">
        <el-form inline :model="flowQuery">
          <el-form-item label="账户"
            ><el-select
              v-model="flowQuery.accountId"
              clearable
              style="width: 180px"
              ><el-option
                v-for="a in accounts"
                :key="a.id"
                :label="a.accountName"
                :value="a.id" /></el-select
          ></el-form-item>
          <el-form-item label="类型"
            ><el-select
              v-model="flowQuery.flowType"
              clearable
              style="width: 130px"
              ><el-option
                v-for="(label, key) in flowTypeText"
                :key="key"
                :label="label"
                :value="key" /></el-select
          ></el-form-item>
          <el-form-item
            ><el-button type="primary" @click="loadFlows"
              >查询</el-button
            ></el-form-item
          >
        </el-form>
        <div class="toolbar">
          <el-button type="primary" @click="openFlow()">登记流水</el-button
          ><el-button @click="openTransfer">账户转账</el-button>
        </div>
        <el-table :data="flows" border v-loading="loading">
          <el-table-column prop="accountDate" label="日期" width="120" />
          <el-table-column prop="accountName" label="账户" />
          <el-table-column label="类型"
            ><template #default="{ row }">{{
              flowTypeText[row.flowType]
            }}</template></el-table-column
          >
          <el-table-column prop="amount" label="金额" />
          <el-table-column prop="counterparty" label="往来单位" />
          <el-table-column label="关联"
            ><template #default="{ row }">{{
              row.linkType ? `${row.linkType} #${row.linkId}` : "-"
            }}</template></el-table-column
          >
          <el-table-column prop="remark" label="备注" />
          <el-table-column label="操作" width="140"
            ><template #default="{ row }"
              ><template v-if="row.flowType !== '2'"
                ><el-button link type="primary" @click="openFlow(row)"
                  >编辑</el-button
                ><el-button link type="danger" @click="removeFlow(row)"
                  >删除</el-button
                ></template
              ><span v-else class="muted">成对流水</span></template
            ></el-table-column
          >
        </el-table>
        <pagination
          v-show="flowTotal > 0"
          :total="flowTotal"
          :page="flowQuery.current"
          :limit="flowQuery.size"
          @pagination="
            (p) => {
              flowQuery.current = p.page;
              flowQuery.size = p.limit;
              loadFlows();
            }
          "
        />
      </el-tab-pane>
      <el-tab-pane label="收支计划" name="plans">
        <el-alert
          v-if="forecast"
          :type="
            forecast.signal === 'gap'
              ? 'error'
              : forecast.signal === 'surplus'
              ? 'success'
              : 'info'
          "
          :closable="false"
          show-icon
          class="forecast"
        >
          <template #title
            >30天预测余额:¥ {{ money(forecast.finalBalance) }};{{
              forecast.signal === "gap"
                ? "存在资金缺口"
                : forecast.signal === "surplus"
                ? "高于安全线,存在盈余"
                : "处于安全线"
            }}</template
          >
        </el-alert>
        <div class="toolbar">
          <el-button type="primary" @click="openPlan()">新增计划</el-button
          ><el-button @click="loadForecast">刷新预测</el-button>
        </div>
        <el-table :data="plans" border v-loading="loading">
          <el-table-column prop="planDate" label="计划日期" />
          <el-table-column label="类型"
            ><template #default="{ row }">{{
              row.planType === "0" ? "计划收入" : "计划支出"
            }}</template></el-table-column
          >
          <el-table-column prop="expectedAmount" label="预计金额" />
          <el-table-column prop="category" label="分类" />
          <el-table-column prop="counterparty" label="往来单位" />
          <el-table-column label="状态"
            ><template #default="{ row }">{{
              planStatusText[row.status]
            }}</template></el-table-column
          >
          <el-table-column label="操作" width="140"
            ><template #default="{ row }"
              ><el-button link type="primary" @click="openPlan(row)"
                >编辑</el-button
              ><el-button link type="danger" @click="removePlan(row)"
                >删除</el-button
              ></template
            ></el-table-column
          >
        </el-table>
        <pagination
          v-show="planTotal > 0"
          :total="planTotal"
          :page="planQuery.current"
          :limit="planQuery.size"
          @pagination="
            (p) => {
              planQuery.current = p.page;
              planQuery.size = p.limit;
              loadPlans();
            }
          "
        />
      </el-tab-pane>
    </el-tabs>
    <el-dialog
      v-model="accountVisible"
      :title="accountForm.id ? '编辑账户' : '新增账户'"
      width="560px"
    >
      <el-form :model="accountForm" label-width="100px">
        <el-form-item label="账户名称"
          ><el-input v-model="accountForm.accountName"
        /></el-form-item>
        <el-form-item label="账户类型"
          ><el-radio-group v-model="accountForm.accountType"
            ><el-radio value="0">银行账户</el-radio
            ><el-radio value="1">现金账户</el-radio></el-radio-group
          ></el-form-item
        >
        <el-form-item label="开户行"
          ><el-input v-model="accountForm.bankName"
        /></el-form-item>
        <el-form-item label="银行账号"
          ><el-input v-model="accountForm.bankNo"
        /></el-form-item>
        <el-form-item label="期初余额"
          ><el-input-number
            v-model="accountForm.openingBalance"
            :precision="2"
            style="width: 100%"
        /></el-form-item>
        <el-form-item label="安全线"
          ><el-input-number
            v-model="accountForm.safetyLine"
            :precision="2"
            style="width: 100%"
        /></el-form-item>
        <el-form-item label="状态"
          ><el-switch
            v-model="accountForm.status"
            active-value="0"
            inactive-value="1"
            active-text="启用"
        /></el-form-item>
      </el-form>
      <template #footer
        ><el-button @click="accountVisible = false">取消</el-button
        ><el-button type="primary" @click="saveAccount"
          >保存</el-button
        ></template
      >
    </el-dialog>
    <el-dialog v-model="flowVisible" title="登记资金流水" width="560px">
      <el-form :model="flowForm" label-width="100px">
        <el-form-item label="日期"
          ><el-date-picker
            v-model="flowForm.accountDate"
            value-format="YYYY-MM-DD"
        /></el-form-item>
        <el-form-item label="账户"
          ><el-select v-model="flowForm.accountId"
            ><el-option
              v-for="a in accounts"
              :key="a.id"
              :label="a.accountName"
              :value="a.id" /></el-select
        ></el-form-item>
        <el-form-item label="流水类型"
          ><el-select v-model="flowForm.flowType"
            ><el-option
              v-for="(label, key) in nonTransferTypes"
              :key="key"
              :label="label"
              :value="key" /></el-select
        ></el-form-item>
        <el-form-item label="金额"
          ><el-input-number
            v-model="flowForm.amount"
            :min="0.01"
            :precision="2"
        /></el-form-item>
        <el-form-item label="往来单位"
          ><el-input v-model="flowForm.counterparty"
        /></el-form-item>
        <el-form-item label="关联类型"
          ><el-select v-model="flowForm.linkType" clearable
            ><el-option label="收入" value="Income" /><el-option
              label="支出"
              value="Expense" /></el-select
        ></el-form-item>
        <el-form-item label="关联ID"
          ><el-input-number v-model="flowForm.linkId" :min="1"
        /></el-form-item>
        <el-form-item label="备注"
          ><el-input v-model="flowForm.remark"
        /></el-form-item>
      </el-form>
      <template #footer
        ><el-button @click="flowVisible = false">取消</el-button
        ><el-button type="primary" @click="saveFlow">保存</el-button></template
      >
    </el-dialog>
    <el-dialog v-model="transferVisible" title="账户转账" width="520px">
      <el-form :model="transferForm" label-width="100px">
        <el-form-item label="转出账户"
          ><el-select v-model="transferForm.fromAccountId"
            ><el-option
              v-for="a in accounts"
              :key="a.id"
              :label="a.accountName"
              :value="a.id" /></el-select
        ></el-form-item>
        <el-form-item label="转入账户"
          ><el-select v-model="transferForm.toAccountId"
            ><el-option
              v-for="a in accounts"
              :key="a.id"
              :label="a.accountName"
              :value="a.id" /></el-select
        ></el-form-item>
        <el-form-item label="金额"
          ><el-input-number
            v-model="transferForm.amount"
            :min="0.01"
            :precision="2"
        /></el-form-item>
        <el-form-item label="日期"
          ><el-date-picker
            v-model="transferForm.transferDate"
            value-format="YYYY-MM-DD"
        /></el-form-item>
        <el-form-item label="备注"
          ><el-input v-model="transferForm.remark"
        /></el-form-item>
      </el-form>
      <template #footer
        ><el-button @click="transferVisible = false">取消</el-button
        ><el-button type="primary" @click="saveTransfer"
          >转账</el-button
        ></template
      >
    </el-dialog>
    <el-dialog
      v-model="planVisible"
      :title="planForm.id ? '编辑计划' : '新增计划'"
      width="560px"
    >
      <el-form :model="planForm" label-width="100px">
        <el-form-item label="计划类型"
          ><el-radio-group v-model="planForm.planType"
            ><el-radio value="0">计划收入</el-radio
            ><el-radio value="1">计划支出</el-radio></el-radio-group
          ></el-form-item
        >
        <el-form-item label="计划日期"
          ><el-date-picker
            v-model="planForm.planDate"
            value-format="YYYY-MM-DD"
        /></el-form-item>
        <el-form-item label="预计金额"
          ><el-input-number
            v-model="planForm.expectedAmount"
            :min="0.01"
            :precision="2"
        /></el-form-item>
        <el-form-item label="分类"
          ><el-input v-model="planForm.category"
        /></el-form-item>
        <el-form-item label="往来单位"
          ><el-input v-model="planForm.counterparty"
        /></el-form-item>
        <el-form-item label="状态"
          ><el-select v-model="planForm.status"
            ><el-option
              v-for="(label, key) in planStatusText"
              :key="key"
              :label="label"
              :value="key" /></el-select
        ></el-form-item>
        <el-form-item label="备注"
          ><el-input v-model="planForm.remark"
        /></el-form-item>
      </el-form>
      <template #footer
        ><el-button @click="planVisible = false">取消</el-button
        ><el-button type="primary" @click="savePlan">保存</el-button></template
      >
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from "vue";
import * as echarts from "echarts";
import { ElMessage, ElMessageBox } from "element-plus";
import pagination from "@/components/PIMTable/Pagination.vue";
import {
  getFundsPage,
  addFunds,
  updateFunds,
  deleteFunds,
  transferFunds,
  getFinanceAccountPage,
  addFinanceAccount,
  updateFinanceAccount,
  deleteFinanceAccount,
  getFundsTrend,
  getCashflowPlanPage,
  addCashflowPlan,
  updateCashflowPlan,
  deleteCashflowPlan,
  getCashflowForecast,
} from "@/api/financialManagement/fundsManagement";
const activeTab = ref("accounts"),
  loading = ref(false),
  accounts = ref([]),
  flows = ref([]),
  plans = ref([]),
  forecast = ref(null);
const flowTotal = ref(0),
  planTotal = ref(0),
  trendRef = ref(),
  accountVisible = ref(false),
  flowVisible = ref(false),
  transferVisible = ref(false),
  planVisible = ref(false);
const accountTypeText = { 0: "银行账户", 1: "现金账户" },
  flowTypeText = { 0: "收入", 1: "支出", 2: "转账", 3: "存入", 4: "取出" },
  nonTransferTypes = { 0: "收入", 1: "支出", 3: "存入", 4: "取出" },
  planStatusText = { 0: "待发生", 1: "已发生", 2: "已取消" };
const flowQuery = reactive({
    current: 1,
    size: 10,
    accountId: null,
    flowType: "",
  }),
  planQuery = reactive({ current: 1, size: 10 });
const accountForm = reactive({}),
  flowForm = reactive({}),
  transferForm = reactive({}),
  planForm = reactive({});
const money = (v) =>
  Number(v || 0).toLocaleString("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
const loadAccounts = async () => {
  loading.value = true;
  try {
    const r = await getFinanceAccountPage({ current: 1, size: 100 });
    accounts.value = r.data?.records || [];
    await nextTick();
    loadTrend();
  } finally {
    loading.value = false;
  }
};
const loadTrend = async () => {
  if (!trendRef.value) return;
  const end = new Date(),
    start = new Date(Date.now() - 29 * 86400000);
  const fmt = (d) => d.toISOString().slice(0, 10);
  const r = await getFundsTrend({ start: fmt(start), end: fmt(end) });
  const rows = r.data || [];
  echarts.init(trendRef.value).setOption({
    tooltip: { trigger: "axis" },
    legend: { data: ["收入", "支出", "净额"] },
    xAxis: { type: "category", data: rows.map((x) => x.date) },
    yAxis: { type: "value" },
    series: [
      { name: "收入", type: "line", data: rows.map((x) => x.income) },
      { name: "支出", type: "line", data: rows.map((x) => x.expense) },
      { name: "净额", type: "line", data: rows.map((x) => x.net) },
    ],
  });
};
const loadFlows = async () => {
  loading.value = true;
  try {
    const r = await getFundsPage(flowQuery);
    flows.value = r.data?.records || [];
    flowTotal.value = r.data?.total || 0;
  } finally {
    loading.value = false;
  }
};
const loadPlans = async () => {
  loading.value = true;
  try {
    const r = await getCashflowPlanPage(planQuery);
    plans.value = r.data?.records || [];
    planTotal.value = r.data?.total || 0;
    await loadForecast();
  } finally {
    loading.value = false;
  }
};
const loadForecast = async () => {
  const r = await getCashflowForecast({ days: 30 });
  forecast.value = r.data;
};
const loadTab = (name) =>
  name === "accounts"
    ? loadAccounts()
    : name === "flows"
    ? (accounts.value.length || loadAccounts(), loadFlows())
    : loadPlans();
const openAccount = (row) => {
  Object.assign(
    accountForm,
    {
      id: null,
      accountName: "",
      accountType: "0",
      bankName: "",
      bankNo: "",
      openingBalance: 0,
      safetyLine: 0,
      status: "0",
    },
    row || {}
  );
  accountVisible.value = true;
};
const saveAccount = async () => {
  await (accountForm.id
    ? updateFinanceAccount({ ...accountForm })
    : addFinanceAccount({ ...accountForm }));
  ElMessage.success("保存成功");
  accountVisible.value = false;
  loadAccounts();
};
const removeAccount = (row) =>
  ElMessageBox.confirm(`确认删除账户“${row.accountName}”吗?`, "提示").then(
    async () => {
      await deleteFinanceAccount([row.id]);
      ElMessage.success("删除成功");
      loadAccounts();
    }
  );
const openFlow = (row) => {
  Object.assign(
    flowForm,
    {
      id: null,
      accountDate: new Date().toISOString().slice(0, 10),
      accountId: null,
      flowType: "0",
      amount: null,
      counterparty: "",
      linkType: "",
      linkId: null,
      remark: "",
    },
    row || {}
  );
  flowVisible.value = true;
};
const saveFlow = async () => {
  await (flowForm.id
    ? updateFunds({ ...flowForm })
    : addFunds({ ...flowForm }));
  ElMessage.success("保存成功");
  flowVisible.value = false;
  loadFlows();
};
const removeFlow = (row) =>
  ElMessageBox.confirm("确认删除该资金流水吗?", "提示").then(async () => {
    await deleteFunds([row.id]);
    ElMessage.success("删除成功");
    loadFlows();
  });
const openTransfer = () => {
  Object.assign(transferForm, {
    fromAccountId: null,
    toAccountId: null,
    amount: null,
    transferDate: new Date().toISOString().slice(0, 10),
    remark: "",
  });
  transferVisible.value = true;
};
const saveTransfer = async () => {
  await transferFunds({ ...transferForm });
  ElMessage.success("转账成功");
  transferVisible.value = false;
  loadFlows();
};
const openPlan = (row) => {
  Object.assign(
    planForm,
    {
      id: null,
      planType: "0",
      planDate: "",
      expectedAmount: null,
      category: "",
      counterparty: "",
      status: "0",
      remark: "",
    },
    row || {}
  );
  planVisible.value = true;
};
const savePlan = async () => {
  await (planForm.id
    ? updateCashflowPlan({ ...planForm })
    : addCashflowPlan({ ...planForm }));
  ElMessage.success("保存成功");
  planVisible.value = false;
  loadPlans();
};
const removePlan = (row) =>
  ElMessageBox.confirm("确认删除该收支计划吗?", "提示").then(async () => {
    await deleteCashflowPlan([row.id]);
    ElMessage.success("删除成功");
    loadPlans();
  });
onMounted(loadAccounts);
</script>
<style scoped>
.finance-workspace {
  padding: 4px;
}
.toolbar {
  margin: 12px 0;
}
.balance-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}
.balance-title {
  display: flex;
  justify-content: space-between;
}
.balance-amount {
  font-size: 25px;
  font-weight: 600;
  color: #303133;
  margin: 14px 0;
}
.muted {
  color: #909399;
  font-size: 13px;
}
.chart {
  height: 300px;
}
.forecast {
  margin-bottom: 12px;
}
</style>
src/views/financialManagement/fundsManagement/index.vue
@@ -1,5 +1,7 @@
<template>
  <div class="app-container">
  <div class="funds-management-page">
  <FinanceWorkspace />
  <div v-if="false" class="app-container">
    <!-- æœç´¢åŒº -->
    <div class="search_form">
      <el-form :inline="true" :model="searchForm">
@@ -106,12 +108,15 @@
      </template>
    </el-dialog>
  </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import pagination from '@/components/PIMTable/Pagination.vue'
import FinanceWorkspace from './FinanceWorkspace.vue'
defineOptions({ name: 'FundsManagement' })
import {
  getFundsPage,
  addFunds,
@@ -276,7 +281,7 @@
}
onMounted(() => {
  getList()
  // æ–°ç‰ˆèµ„金工作区自行加载三个业务分区的数据。
})
</script>
src/views/inventoryManagement/index.vue
@@ -141,7 +141,6 @@
import {listPage, deleteLedger, update} from "@/api/lavorissce/ledger.js";
import {ElMessageBox, ElMessage} from "element-plus";
const { proxy } = getCurrentInstance();
import { getCurrentMonth } from "@/utils/util"
const page = ref({
    current: 1,