<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>
|