From 43d00eec5989caa78c44c736853230a13d8e9f6c Mon Sep 17 00:00:00 2001
From: liding <756868258@qq.com>
Date: 星期四, 08 十月 2026 14:51:53 +0800
Subject: [PATCH] 财务管理系统模块优化
---
src/views/inventoryManagement/index.vue | 1
src/views/financialManagement/fundsManagement/FinanceWorkspace.vue | 651 +++++++++++++++++++++++
src/api/financialManagement/expenseManagement.js | 8
src/views/financialManagement/financialReconciliation/index.vue | 311 +---------
src/views/financialManagement/expenseManagement/Form.vue | 9
src/views/financialManagement/expenseManagement/index.vue | 16
src/views/financialManagement/financialReconciliation/ReconciliationWorkspace.vue | 484 +++++++++++++++++
src/api/financialManagement/fundsManagement.js | 17
src/views/financialManagement/fundsManagement/index.vue | 9
src/api/financialManagement/financialReconciliation.js | 8
reconciliation-repro.html | 18
src/views/financialManagement/expenseManagement/ExpenseCategoryReport.vue | 72 ++
src/views/financialManagement/financialStatements/index.vue | 42 +
13 files changed, 1,354 insertions(+), 292 deletions(-)
diff --git a/reconciliation-repro.html b/reconciliation-repro.html
new file mode 100644
index 0000000..891248c
--- /dev/null
+++ b/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>
diff --git a/src/api/financialManagement/expenseManagement.js b/src/api/financialManagement/expenseManagement.js
index 317dc47..f5b6257 100644
--- a/src/api/financialManagement/expenseManagement.js
+++ b/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,
+ });
+}
diff --git a/src/api/financialManagement/financialReconciliation.js b/src/api/financialManagement/financialReconciliation.js
index d2c08d1..6241b12 100644
--- a/src/api/financialManagement/financialReconciliation.js
+++ b/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" });
diff --git a/src/api/financialManagement/fundsManagement.js b/src/api/financialManagement/fundsManagement.js
index a970638..0cf7ae5 100644
--- a/src/api/financialManagement/fundsManagement.js
+++ b/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({
diff --git a/src/views/financialManagement/expenseManagement/ExpenseCategoryReport.vue b/src/views/financialManagement/expenseManagement/ExpenseCategoryReport.vue
new file mode 100644
index 0000000..ac7a8e9
--- /dev/null
+++ b/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>
diff --git a/src/views/financialManagement/expenseManagement/Form.vue b/src/views/financialManagement/expenseManagement/Form.vue
index 9cfe5da..933d53f 100644
--- a/src/views/financialManagement/expenseManagement/Form.vue
+++ b/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;
diff --git a/src/views/financialManagement/expenseManagement/index.vue b/src/views/financialManagement/expenseManagement/index.vue
index a45c32d..a79e321 100644
--- a/src/views/financialManagement/expenseManagement/index.vue
+++ b/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",
diff --git a/src/views/financialManagement/financialReconciliation/ReconciliationWorkspace.vue b/src/views/financialManagement/financialReconciliation/ReconciliationWorkspace.vue
new file mode 100644
index 0000000..fd23f2c
--- /dev/null
+++ b/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>
diff --git a/src/views/financialManagement/financialReconciliation/index.vue b/src/views/financialManagement/financialReconciliation/index.vue
index fa4db0a..87355a1 100644
--- a/src/views/financialManagement/financialReconciliation/index.vue
+++ b/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>
diff --git a/src/views/financialManagement/financialStatements/index.vue b/src/views/financialManagement/financialStatements/index.vue
index e5f9b23..4a0b674 100644
--- a/src/views/financialManagement/financialStatements/index.vue
+++ b/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>
diff --git a/src/views/financialManagement/fundsManagement/FinanceWorkspace.vue b/src/views/financialManagement/fundsManagement/FinanceWorkspace.vue
new file mode 100644
index 0000000..05c5afd
--- /dev/null
+++ b/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>
diff --git a/src/views/financialManagement/fundsManagement/index.vue b/src/views/financialManagement/fundsManagement/index.vue
index 845dc52..c20f7cb 100644
--- a/src/views/financialManagement/fundsManagement/index.vue
+++ b/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>
diff --git a/src/views/inventoryManagement/index.vue b/src/views/inventoryManagement/index.vue
index f371e26..b39bc62 100644
--- a/src/views/inventoryManagement/index.vue
+++ b/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,
--
Gitblit v1.9.3