From 98312d84f785c09574bfd5add730ea448fb0cbd5 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 13 十月 2026 11:29:17 +0800
Subject: [PATCH] feat: 财务 1.惠牧添加财务模块
---
src/views/erp/finance/ledger/data.ts | 214 ++
src/views/erp/finance/subject/data.ts | 210 ++
src/views/erp/finance/voucher/modules/form.vue | 157 ++
src/views/erp/finance/fixed-asset/index.vue | 217 ++
src/views/erp/finance/voucher/modules/entry-table.vue | 361 ++++
src/views/erp/finance/fixed-asset/modules/form.vue | 89 +
src/views/erp/finance/statistics/index.vue | 272 +++
src/views/erp/finance/intangible-asset/modules/form.vue | 90 +
src/api/erp/finance/fixed-asset/index.ts | 93 +
src/views/erp/finance/voucher/data.ts | 297 ++++
src/views/erp/finance/ledger/detail/index.vue | 100 +
src/api/erp/finance/ledger/index.ts | 54
src/views/erp/finance/ledger/general/index.vue | 103 +
src/views/erp/finance/receipt/modules/item-form.vue | 1
src/views/erp/finance/subject/index.vue | 224 +++
src/views/erp/finance/fixed-asset/data.ts | 389 +++++
src/views/erp/finance/statistics/modules/trend-chart.vue | 101 +
src/views/erp/finance/intangible-asset/index.vue | 217 ++
src/views/erp/finance/subject/modules/form.vue | 96 +
src/api/erp/finance/statistics/index.ts | 60
src/views/erp/finance/voucher/index.vue | 298 ++++
src/api/erp/finance/subject/index.ts | 77 +
src/api/erp/finance/intangible-asset/index.ts | 94 +
src/api/erp/finance/voucher/index.ts | 101 +
src/views/erp/finance/voucher/modules/account-select.vue | 150 ++
src/packages/constants/src/dict-enum.ts | 11
src/views/erp/finance/intangible-asset/data.ts | 341 ++++
27 files changed, 4,416 insertions(+), 1 deletions(-)
diff --git a/src/api/erp/finance/fixed-asset/index.ts b/src/api/erp/finance/fixed-asset/index.ts
new file mode 100644
index 0000000..9d29306
--- /dev/null
+++ b/src/api/erp/finance/fixed-asset/index.ts
@@ -0,0 +1,93 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace FinFixedAssetApi {
+ /** 鍥哄畾璧勪骇淇℃伅 */
+ export interface FixedAsset {
+ id?: number; // 璧勪骇缂栧彿
+ assetCode?: string; // 璧勪骇缂栫爜
+ assetName?: string; // 璧勪骇鍚嶇О
+ category?: string; // 璧勪骇绫诲埆
+ specification?: string; // 瑙勬牸鍨嬪彿
+ purchaseDate?: string; // 璐疆鏃ユ湡 yyyy-MM-dd
+ originalValue?: number; // 璧勪骇鍘熷��
+ usefulLife?: number; // 浣跨敤骞撮檺锛堝勾锛�
+ residualRate?: number; // 娈嬪�肩巼锛�%锛�
+ monthlyDepreciation?: number; // 鏈堟姌鏃ч锛堝悗绔绠楋紝鍙锛�
+ accumulatedDepreciation?: number; // 绱鎶樻棫
+ netValue?: number; // 鍑�鍊�
+ location?: string; // 瀛樻斁鍦扮偣
+ departmentId?: number; // 浣跨敤閮ㄩ棬缂栧彿锛坰ystem_dept.id锛岃〃鍗曟彁浜ょ敤锛�
+ departmentName?: string; // 浣跨敤閮ㄩ棬鍚嶇О锛堝悗绔В鏋愶紝鍙锛�
+ keeperId?: number; // 淇濈浜虹紪鍙凤紙system_users.id锛岃〃鍗曟彁浜ょ敤锛�
+ keeperName?: string; // 淇濈浜烘樀绉帮紙鍚庣瑙f瀽锛屽彧璇伙級
+ status?: number; // 鐘舵�侊細1鍦ㄧ敤 2闂茬疆 3缁翠慨涓� 4宸叉姤搴�
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+
+ /** 璁℃彁鎶樻棫缁撴灉 */
+ export interface DepreciateResult {
+ processedCount: number; // 鏈璁℃彁鐨勮祫浜ф暟閲�
+ totalMonthlyDepreciation: number; // 鏈璁℃彁鎶樻棫鍚堣
+ }
+}
+
+/** 鏌ヨ鍥哄畾璧勪骇鍒嗛〉 */
+export function getFixedAssetPage(params: PageParam) {
+ return requestClient.get<PageResult<FinFixedAssetApi.FixedAsset>>(
+ '/finance/fixed-asset/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鍥哄畾璧勪骇鍒楄〃锛堜笉鍒嗛〉锛岀敤浜庣粺璁★級 */
+export function getFixedAssetList() {
+ return requestClient.get<FinFixedAssetApi.FixedAsset[]>(
+ '/finance/fixed-asset/list',
+ );
+}
+
+/** 鏌ヨ鍥哄畾璧勪骇璇︽儏 */
+export function getFixedAsset(id: number) {
+ return requestClient.get<FinFixedAssetApi.FixedAsset>(
+ `/finance/fixed-asset/get?id=${id}`,
+ );
+}
+
+/** 鏂板鍥哄畾璧勪骇 */
+export function createFixedAsset(data: FinFixedAssetApi.FixedAsset) {
+ return requestClient.post('/finance/fixed-asset/create', data);
+}
+
+/** 淇敼鍥哄畾璧勪骇 */
+export function updateFixedAsset(data: FinFixedAssetApi.FixedAsset) {
+ return requestClient.put('/finance/fixed-asset/update', data);
+}
+
+/** 鍒犻櫎鍥哄畾璧勪骇 */
+export function deleteFixedAsset(id: number) {
+ return requestClient.delete(`/finance/fixed-asset/delete?id=${id}`);
+}
+
+/** 鎵归噺鍒犻櫎鍥哄畾璧勪骇 */
+export function deleteFixedAssetList(ids: number[]) {
+ return requestClient.delete(
+ `/finance/fixed-asset/delete-list?ids=${ids.join(',')}`,
+ );
+}
+
+/** 璁℃彁鎶樻棫锛坕ds 涓虹┖琛ㄧず鍏ㄩ儴璁℃彁锛� */
+export function depreciateFixedAssetList(ids: number[]) {
+ return requestClient.post<FinFixedAssetApi.DepreciateResult>(
+ `/finance/fixed-asset/depreciate?ids=${ids.join(',')}`,
+ );
+}
+
+/** 瀵煎嚭鍥哄畾璧勪骇 Excel */
+export function exportFixedAsset(params: any) {
+ return requestClient.download('/finance/fixed-asset/export-excel', {
+ params,
+ });
+}
diff --git a/src/api/erp/finance/intangible-asset/index.ts b/src/api/erp/finance/intangible-asset/index.ts
new file mode 100644
index 0000000..4e1f3a3
--- /dev/null
+++ b/src/api/erp/finance/intangible-asset/index.ts
@@ -0,0 +1,94 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace FinIntangibleAssetApi {
+ /** 鏃犲舰璧勪骇淇℃伅 */
+ export interface IntangibleAsset {
+ id?: number; // 璧勪骇缂栧彿
+ assetCode?: string; // 璧勪骇缂栫爜
+ assetName?: string; // 璧勪骇鍚嶇О
+ category?: string; // 璧勪骇绫诲埆
+ certificateNo?: string; // 璇佷功缂栧彿
+ acquisitionDate?: string; // 鍙栧緱鏃ユ湡 yyyy-MM-dd
+ originalValue?: number; // 璧勪骇鍘熷��
+ amortizationPeriod?: number; // 鎽婇攢骞撮檺锛堝勾锛�
+ residualRate?: number; // 娈嬪�肩巼锛�%锛�
+ monthlyAmortization?: number; // 鏈堟憡閿�棰濓紙鍚庣璁$畻锛屽彧璇伙級
+ accumulatedAmortization?: number; // 绱鎽婇攢
+ netValue?: number; // 鍑�鍊�
+ validityDate?: string; // 鏈夋晥鏈熻嚦 yyyy-MM-dd
+ status?: number; // 鐘舵�侊細1鍦ㄧ敤 2宸插け鏁� 3宸叉憡閿�瀹�
+ description?: string; // 璧勪骇鎻忚堪
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+
+ /** 璁℃彁鎽婇攢缁撴灉 */
+ export interface AmortizeResult {
+ processedCount: number; // 鏈璁℃彁鐨勮祫浜ф暟閲�
+ totalMonthlyAmortization: number; // 鏈璁℃彁鎽婇攢鍚堣
+ }
+}
+
+/** 鏌ヨ鏃犲舰璧勪骇鍒嗛〉 */
+export function getIntangibleAssetPage(params: PageParam) {
+ return requestClient.get<PageResult<FinIntangibleAssetApi.IntangibleAsset>>(
+ '/finance/intangible-asset/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鏃犲舰璧勪骇鍒楄〃锛堜笉鍒嗛〉锛岀敤浜庣粺璁★級 */
+export function getIntangibleAssetList() {
+ return requestClient.get<FinIntangibleAssetApi.IntangibleAsset[]>(
+ '/finance/intangible-asset/list',
+ );
+}
+
+/** 鏌ヨ鏃犲舰璧勪骇璇︽儏 */
+export function getIntangibleAsset(id: number) {
+ return requestClient.get<FinIntangibleAssetApi.IntangibleAsset>(
+ `/finance/intangible-asset/get?id=${id}`,
+ );
+}
+
+/** 鏂板鏃犲舰璧勪骇 */
+export function createIntangibleAsset(
+ data: FinIntangibleAssetApi.IntangibleAsset,
+) {
+ return requestClient.post('/finance/intangible-asset/create', data);
+}
+
+/** 淇敼鏃犲舰璧勪骇 */
+export function updateIntangibleAsset(
+ data: FinIntangibleAssetApi.IntangibleAsset,
+) {
+ return requestClient.put('/finance/intangible-asset/update', data);
+}
+
+/** 鍒犻櫎鏃犲舰璧勪骇 */
+export function deleteIntangibleAsset(id: number) {
+ return requestClient.delete(`/finance/intangible-asset/delete?id=${id}`);
+}
+
+/** 鎵归噺鍒犻櫎鏃犲舰璧勪骇 */
+export function deleteIntangibleAssetList(ids: number[]) {
+ return requestClient.delete(
+ `/finance/intangible-asset/delete-list?ids=${ids.join(',')}`,
+ );
+}
+
+/** 璁℃彁鎽婇攢锛坕ds 涓虹┖琛ㄧず鍏ㄩ儴璁℃彁锛� */
+export function amortizeIntangibleAssetList(ids: number[]) {
+ return requestClient.post<FinIntangibleAssetApi.AmortizeResult>(
+ `/finance/intangible-asset/amortize?ids=${ids.join(',')}`,
+ );
+}
+
+/** 瀵煎嚭鏃犲舰璧勪骇 Excel */
+export function exportIntangibleAsset(params: any) {
+ return requestClient.download('/finance/intangible-asset/export-excel', {
+ params,
+ });
+}
diff --git a/src/api/erp/finance/ledger/index.ts b/src/api/erp/finance/ledger/index.ts
new file mode 100644
index 0000000..f469fb7
--- /dev/null
+++ b/src/api/erp/finance/ledger/index.ts
@@ -0,0 +1,54 @@
+import { requestClient } from '#/api/request';
+
+export namespace FinLedgerApi {
+ /** 鎬昏处 / 鏄庣粏璐︽煡璇㈠弬鏁� */
+ export interface LedgerParams {
+ subjectCode: string; // 绉戠洰缂栫爜
+ startMonth?: string; // 璧峰鏈堜唤 yyyy-MM
+ endMonth?: string; // 缁撴潫鏈堜唤 yyyy-MM
+ includeChildren?: boolean; // 鏄惁鍖呭惈涓嬬骇绉戠洰
+ }
+
+ /** 鎬昏处 / 鏄庣粏璐﹁ */
+ export interface LedgerRow {
+ rowType: string; // 琛岀被鍨嬶細opening鏈熷垵 entry鏄庣粏 monthly_total鏈堝悎璁� yearly_total骞村悎璁�
+ period?: string; // 鏈熼棿 yyyy-MM
+ date?: string; // 鏃ユ湡
+ voucherNo?: string; // 鍑瘉瀛楀彿
+ subjectCode?: string; // 绉戠洰缂栫爜
+ subjectName?: string; // 绉戠洰鍚嶇О
+ summary?: string; // 鎽樿
+ debit?: number; // 鍊熸柟閲戦
+ credit?: number; // 璐锋柟閲戦
+ direction?: number; // 浣欓鏂瑰悜锛�1鍊� 2璐�
+ balance?: number; // 浣欓
+ }
+}
+
+/** 鏌ヨ鎬昏处 */
+export function getGeneralLedger(params: FinLedgerApi.LedgerParams) {
+ return requestClient.get<FinLedgerApi.LedgerRow[]>('/finance/ledger/general', {
+ params,
+ });
+}
+
+/** 鏌ヨ鏄庣粏璐� */
+export function getDetailLedger(params: FinLedgerApi.LedgerParams) {
+ return requestClient.get<FinLedgerApi.LedgerRow[]>('/finance/ledger/detail', {
+ params,
+ });
+}
+
+/** 瀵煎嚭鎬昏处 Excel */
+export function exportGeneralLedger(params: FinLedgerApi.LedgerParams) {
+ return requestClient.download('/finance/ledger/general/export-excel', {
+ params,
+ });
+}
+
+/** 瀵煎嚭鏄庣粏璐� Excel */
+export function exportDetailLedger(params: FinLedgerApi.LedgerParams) {
+ return requestClient.download('/finance/ledger/detail/export-excel', {
+ params,
+ });
+}
diff --git a/src/api/erp/finance/statistics/index.ts b/src/api/erp/finance/statistics/index.ts
new file mode 100644
index 0000000..2381d7b
--- /dev/null
+++ b/src/api/erp/finance/statistics/index.ts
@@ -0,0 +1,60 @@
+import { requestClient } from '#/api/request';
+
+export namespace FinStatisticsApi {
+ /** 浼氳鏍哥畻鐪嬫澘鏌ヨ鍙傛暟 */
+ export interface StatisticsParams {
+ startMonth?: string; // 璧峰鏈堜唤 yyyy-MM锛岀暀绌洪粯璁ゆ湰骞� 1 鏈�
+ endMonth?: string; // 缁撴潫鏈堜唤 yyyy-MM锛岀暀绌洪粯璁ゅ綋鍓嶆湀
+ }
+
+ /** 浼氳鏍哥畻鐪嬫澘鎬昏 */
+ export interface StatisticsOverview {
+ startDate: string; // 瀹為檯缁熻璧峰鏃ユ湡
+ endDate: string; // 瀹為檯缁熻缁撴潫鏃ユ湡
+ receiptPrice: number; // 鏀舵閲戦锛圗RP 宸插鏍告敹娆惧崟锛�
+ paymentPrice: number; // 浠樻閲戦锛圗RP 宸插鏍镐粯娆惧崟锛�
+ netProfit: number; // 鍑�鍒╂鼎 = 鏀舵 - 浠樻
+ saleOrderPrice: number; // 閿�鍞鍗曢噾棰濓紙宸插鏍革級
+ purchaseOrderPrice: number; // 閲囪喘璁㈠崟閲戦锛堝凡瀹℃牳锛�
+
+ fixedAssetOriginalValue: number; // 鍥哄畾璧勪骇鍘熷�煎悎璁�
+ fixedAssetAccumulatedDepreciation: number; // 鍥哄畾璧勪骇绱鎶樻棫鍚堣
+ fixedAssetNetValue: number; // 鍥哄畾璧勪骇鍑�鍊煎悎璁�
+ intangibleAssetOriginalValue: number; // 鏃犲舰璧勪骇鍘熷�煎悎璁�
+ intangibleAssetAccumulatedAmortization: number; // 鏃犲舰璧勪骇绱鎽婇攢鍚堣
+ intangibleAssetNetValue: number; // 鏃犲舰璧勪骇鍑�鍊煎悎璁�
+ totalAssetNetValue: number; // 璧勪骇鍑�鍊煎悎璁�
+
+ subjectCount: number; // 浼氳绉戠洰鏁伴噺
+ voucherCount: number; // 鍑瘉鎬绘暟
+ voucherUnpostedCount: number; // 鏈繃璐﹀嚟璇佹暟閲�
+ voucherPostedCount: number; // 宸茶繃璐﹀嚟璇佹暟閲�
+ voucherCancelledCount: number; // 宸蹭綔搴熷嚟璇佹暟閲�
+ }
+
+ /** 鏈堝害瓒嬪娍 */
+ export interface StatisticsTrend {
+ month: string; // 鏈堜唤 yyyy-MM
+ receiptPrice: number; // 鏀舵閲戦
+ paymentPrice: number; // 浠樻閲戦
+ netProfit: number; // 鍑�鍒╂鼎
+ saleOrderPrice: number; // 閿�鍞鍗曢噾棰�
+ purchaseOrderPrice: number; // 閲囪喘璁㈠崟閲戦
+ }
+}
+
+/** 鑾峰彇浼氳鏍哥畻鐪嬫澘鎬昏 */
+export function getStatisticsOverview(params?: FinStatisticsApi.StatisticsParams) {
+ return requestClient.get<FinStatisticsApi.StatisticsOverview>(
+ '/finance/statistics/overview',
+ { params },
+ );
+}
+
+/** 鑾峰彇鏈堝害鏀舵敮瓒嬪娍锛堟湡闂村唴鏃犳暟鎹殑鏈堜唤琛� 0锛� */
+export function getStatisticsTrend(params?: FinStatisticsApi.StatisticsParams) {
+ return requestClient.get<FinStatisticsApi.StatisticsTrend[]>(
+ '/finance/statistics/trend',
+ { params },
+ );
+}
diff --git a/src/api/erp/finance/subject/index.ts b/src/api/erp/finance/subject/index.ts
new file mode 100644
index 0000000..a804d8a
--- /dev/null
+++ b/src/api/erp/finance/subject/index.ts
@@ -0,0 +1,77 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace FinAccountSubjectApi {
+ /** 浼氳绉戠洰淇℃伅 */
+ export interface AccountSubject {
+ id?: number; // 绉戠洰缂栧彿
+ parentId?: number; // 涓婄骇绉戠洰缂栧彿锛岀┖琛ㄧず涓�绾х鐩�
+ subjectCode: string; // 绉戠洰缂栫爜
+ subjectName: string; // 绉戠洰鍚嶇О
+ subjectType: number; // 绉戠洰绫诲瀷锛�1璧勪骇绫� 2璐熷�虹被 3鎵�鏈夎�呮潈鐩婄被 4鎴愭湰绫� 5鎹熺泭绫�
+ balanceDirection: number; // 浣欓鏂瑰悜锛�1鍊� 2璐�
+ status: number; // 鐘舵�侊細0鍚敤 1绂佺敤
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ children?: AccountSubject[]; // 瀛愮鐩紙鏍戝舰杩斿洖锛�
+ }
+}
+
+/** 鏌ヨ浼氳绉戠洰鍒嗛〉 */
+export function getAccountSubjectPage(params: PageParam) {
+ return requestClient.get<PageResult<FinAccountSubjectApi.AccountSubject>>(
+ '/finance/account-subject/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ浼氳绉戠洰褰曪紙鏍戝舰锛夊垪琛� */
+export function getAccountSubjectList(params?: any) {
+ return requestClient.get<FinAccountSubjectApi.AccountSubject[]>(
+ '/finance/account-subject/list',
+ { params },
+ );
+}
+
+/** 鏌ヨ浼氳绉戠洰绮剧畝鍒楄〃锛堜笉鍒嗛〉銆佸钩閾猴級 */
+export function getAccountSubjectSimpleList() {
+ return requestClient.get<FinAccountSubjectApi.AccountSubject[]>(
+ '/finance/account-subject/simple-list',
+ );
+}
+
+/** 鏌ヨ浼氳绉戠洰璇︽儏 */
+export function getAccountSubject(id: number) {
+ return requestClient.get<FinAccountSubjectApi.AccountSubject>(
+ `/finance/account-subject/get?id=${id}`,
+ );
+}
+
+/** 鏂板浼氳绉戠洰 */
+export function createAccountSubject(
+ data: FinAccountSubjectApi.AccountSubject,
+) {
+ return requestClient.post('/finance/account-subject/create', data);
+}
+
+/** 淇敼浼氳绉戠洰 */
+export function updateAccountSubject(
+ data: FinAccountSubjectApi.AccountSubject,
+) {
+ return requestClient.put('/finance/account-subject/update', data);
+}
+
+/** 鍒犻櫎浼氳绉戠洰锛堟敮鎸佹壒閲忥紝鍚笅绾х鐩級 */
+export function deleteAccountSubject(ids: number[]) {
+ return requestClient.delete(
+ `/finance/account-subject/delete?ids=${ids.join(',')}`,
+ );
+}
+
+/** 瀵煎嚭浼氳绉戠洰 Excel */
+export function exportAccountSubject(params: any) {
+ return requestClient.download('/finance/account-subject/export-excel', {
+ params,
+ });
+}
diff --git a/src/api/erp/finance/voucher/index.ts b/src/api/erp/finance/voucher/index.ts
new file mode 100644
index 0000000..e693e17
--- /dev/null
+++ b/src/api/erp/finance/voucher/index.ts
@@ -0,0 +1,101 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace FinVoucherApi {
+ /** 鍑瘉鍒嗗綍 */
+ export interface VoucherEntry {
+ id?: number; // 鍒嗗綍缂栧彿
+ rowNo?: number; // 琛屽彿
+ subjectCode?: string; // 绉戠洰缂栫爜
+ subjectName?: string; // 绉戠洰鍚嶇О
+ summary?: string; // 鎽樿
+ debit?: number; // 鍊熸柟閲戦
+ credit?: number; // 璐锋柟閲戦
+ auxiliaryType?: number; // 杈呭姪鏍哥畻绫诲瀷
+ auxiliaryId?: string; // 杈呭姪鏍哥畻瀵硅薄缂栧彿
+ auxiliaryName?: string; // 杈呭姪鏍哥畻瀵硅薄鍚嶇О
+ }
+
+ /** 璁拌处鍑瘉 */
+ export interface Voucher {
+ id?: number; // 鍑瘉缂栧彿
+ voucherNo?: string; // 鍑瘉瀛楀彿锛岀暀绌烘椂鍚庣鑷姩鐢熸垚
+ voucherDate: string; // 鍑瘉鏃ユ湡
+ summary?: string; // 鎽樿
+ debit?: number; // 鍊熸柟鍚堣
+ credit?: number; // 璐锋柟鍚堣
+ status?: number; // 鍑瘉鐘舵�侊細0鏈繃璐� 10宸茶繃璐� 20宸蹭綔搴�
+ creator?: string; // 鍒跺崟浜猴紙鐢ㄦ埛缂栧彿锛屼粎鐢ㄤ簬鎼滅储鏉′欢锛�
+ creatorName?: string; // 鍒跺崟浜烘樀绉帮紙鍒楄〃銆佸鍑哄睍绀虹敤锛�
+ attachmentCount?: number; // 闄勪欢寮犳暟
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ entries?: VoucherEntry[]; // 鍑瘉鍒嗗綍
+ attachmentList?: SystemStorageApi.StorageBlob[]; // 闄勪欢鍒楄〃
+ hasAttachment?: boolean; // 鏄惁鏈夐檮浠�
+ blobIds?: number[]; // 闄勪欢 blobId 鍒楄〃锛堟彁浜ょ敤锛�
+ }
+}
+
+/** 鏌ヨ璁拌处鍑瘉鍒嗛〉 */
+export function getVoucherPage(params: PageParam) {
+ return requestClient.get<PageResult<FinVoucherApi.Voucher>>(
+ '/finance/voucher/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ璁拌处鍑瘉鍒嗛〉锛堝惈鍒嗗綍锛岀敤浜庡嚟璇佸垪琛ㄥ睍寮�涓庢墦鍗帮級 */
+export function getVoucherPageWithEntry(params: PageParam) {
+ return requestClient.get<PageResult<FinVoucherApi.Voucher>>(
+ '/finance/voucher/page-with-entry',
+ { params },
+ );
+}
+
+/** 鏌ヨ璁拌处鍑瘉璇︽儏锛堝惈鍒嗗綍涓庨檮浠讹級 */
+export function getVoucher(id: number) {
+ return requestClient.get<FinVoucherApi.Voucher>(
+ `/finance/voucher/get?id=${id}`,
+ );
+}
+
+/** 鏂板璁拌处鍑瘉 */
+export function createVoucher(data: FinVoucherApi.Voucher) {
+ return requestClient.post('/finance/voucher/create', data);
+}
+
+/** 淇敼璁拌处鍑瘉 */
+export function updateVoucher(data: FinVoucherApi.Voucher) {
+ return requestClient.put('/finance/voucher/update', data);
+}
+
+/** 鍒犻櫎璁拌处鍑瘉 */
+export function deleteVoucher(id: number) {
+ return requestClient.delete(`/finance/voucher/delete?id=${id}`);
+}
+
+/** 鎵归噺鍒犻櫎璁拌处鍑瘉 */
+export function deleteVoucherList(ids: number[]) {
+ return requestClient.delete(
+ `/finance/voucher/delete-list?ids=${ids.join(',')}`,
+ );
+}
+
+/** 鍑瘉杩囪处 */
+export function postVoucher(id: number) {
+ return requestClient.put('/finance/voucher/post', null, { params: { id } });
+}
+
+/** 鍑瘉浣滃簾 */
+export function cancelVoucher(id: number) {
+ return requestClient.put('/finance/voucher/cancel', null, { params: { id } });
+}
+
+/** 瀵煎嚭璁拌处鍑瘉 Excel */
+export function exportVoucher(params: any) {
+ return requestClient.download('/finance/voucher/export-excel', { params });
+}
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 53e2121..47c4f65 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -144,6 +144,16 @@
ERP_STOCK_RECORD_BIZ_TYPE: 'erp_stock_record_biz_type', // 搴撳瓨鏄庣粏鐨勪笟鍔$被鍨�
} as const;
+/** ========== FINANCE - 璐㈠姟绠$悊妯″潡 ========== */
+const FINANCE_DICT = {
+ FINANCE_SUBJECT_TYPE: 'finance_subject_type', // 浼氳绉戠洰绫诲瀷
+ FINANCE_BALANCE_DIRECTION: 'finance_balance_direction', // 绉戠洰浣欓鏂瑰悜
+ FINANCE_AUXILIARY_TYPE: 'finance_auxiliary_type', // 杈呭姪鏍哥畻绫诲瀷
+ FINANCE_VOUCHER_STATUS: 'finance_voucher_status', // 璁拌处鍑瘉鐘舵��
+ FINANCE_FIXED_ASSET_STATUS: 'finance_fixed_asset_status', // 鍥哄畾璧勪骇鐘舵��
+ FINANCE_INTANGIBLE_ASSET_STATUS: 'finance_intangible_asset_status', // 鏃犲舰璧勪骇鐘舵��
+} as const;
+
/** ========== AI - 浜哄伐鏅鸿兘妯″潡 ========== */
const AI_DICT = {
AI_GENERATE_MODE: 'ai_generate_mode', // AI 鐢熸垚妯″紡
@@ -351,6 +361,7 @@
...BPM_DICT,
...CRM_DICT,
...ERP_DICT,
+ ...FINANCE_DICT,
...HRM_DICT,
...INFRA_DICT,
...IM_DICT,
diff --git a/src/views/erp/finance/fixed-asset/data.ts b/src/views/erp/finance/fixed-asset/data.ts
new file mode 100644
index 0000000..5de9461
--- /dev/null
+++ b/src/views/erp/finance/fixed-asset/data.ts
@@ -0,0 +1,389 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinFixedAssetApi } from '#/api/erp/finance/fixed-asset';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import dayjs from 'dayjs';
+
+/**
+ * 浣跨敤閮ㄩ棬涓嬫媺锛氱郴缁熼儴闂ㄦ爲锛坴alue = 閮ㄩ棬缂栧彿锛�
+ * 涓� CRM 缁熻椤电殑銆屽綊灞為儴闂ㄣ�嶅啓娉曚繚鎸佷竴鑷�
+ */
+function departmentSelectProps() {
+ return {
+ allowClear: true,
+ class: 'w-full',
+ api: async () => handleTree(await getSimpleDeptList()),
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '璇烽�夋嫨浣跨敤閮ㄩ棬',
+ treeDefaultExpandAll: true,
+ };
+}
+
+/**
+ * 淇濈浜轰笅鎷夛細绯荤粺鐢ㄦ埛鍒楄〃锛坴alue = 鐢ㄦ埛缂栧彿锛宭abel = 鏄电О锛�
+ * 涓� WMS 鍗曟嵁閲岄�夌敤鎴风殑鍐欐硶淇濇寔涓�鑷�
+ */
+function keeperSelectProps() {
+ return {
+ allowClear: true,
+ class: 'w-full',
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '璇烽�夋嫨淇濈浜�',
+ showSearch: true,
+ };
+}
+
+/** 璧勪骇鐘舵�侊細1鍦ㄧ敤 2闂茬疆 3缁翠慨涓� 4宸叉姤搴� */
+export const FIXED_ASSET_STATUS_IN_USE = 1;
+export const FIXED_ASSET_STATUS_IDLE = 2;
+export const FIXED_ASSET_STATUS_REPAIRING = 3;
+export const FIXED_ASSET_STATUS_SCRAPPED = 4;
+
+/** 鍙鎻愭姌鏃х殑鐘舵�侊紙宸叉姤搴熺殑璧勪骇涓嶅啀璁℃彁锛� */
+export const DEPRECIABLE_FIXED_ASSET_STATUS = [
+ FIXED_ASSET_STATUS_IN_USE,
+ FIXED_ASSET_STATUS_IDLE,
+ FIXED_ASSET_STATUS_REPAIRING,
+];
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'assetCode',
+ label: '璧勪骇缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х紪鐮侊紝濡� GD2026100001',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'assetName',
+ label: '璧勪骇鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜у悕绉�',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'category',
+ label: '璧勪骇绫诲埆',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х被鍒紝濡� 鐢熶骇璁惧',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'specification',
+ label: '瑙勬牸鍨嬪彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鏍煎瀷鍙�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'purchaseDate',
+ label: '璐疆鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨璐疆鏃ユ湡',
+ showTime: false,
+ valueFormat: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD',
+ class: 'w-full',
+ },
+ rules: 'required',
+ defaultValue: dayjs().format('YYYY-MM-DD'),
+ },
+ {
+ fieldName: 'originalValue',
+ label: '璧勪骇鍘熷��',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ヨ祫浜у師鍊�',
+ min: 0,
+ precision: 2,
+ },
+ rules: z.number().min(0).default(0),
+ },
+ {
+ fieldName: 'usefulLife',
+ label: '浣跨敤骞撮檺锛堝勾锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ヤ娇鐢ㄥ勾闄�',
+ min: 1,
+ precision: 0,
+ },
+ defaultValue: 10,
+ rules: z.number().min(1).default(10),
+ },
+ {
+ fieldName: 'residualRate',
+ label: '娈嬪�肩巼锛�%锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ユ畫鍊肩巼',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ defaultValue: 5,
+ rules: z.number().min(0).max(100).default(5),
+ help: '鏈堟姌鏃ч =锛堝師鍊� 脳锛�1 - 娈嬪�肩巼锛夛級梅锛堜娇鐢ㄥ勾闄� 脳 12锛�',
+ },
+ {
+ fieldName: 'location',
+ label: '瀛樻斁鍦扮偣',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ瓨鏀惧湴鐐�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'departmentId',
+ label: '浣跨敤閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: departmentSelectProps(),
+ },
+ {
+ fieldName: 'keeperId',
+ label: '淇濈浜�',
+ component: 'ApiSelect',
+ componentProps: keeperSelectProps(),
+ },
+ {
+ fieldName: 'status',
+ label: '璧勪骇鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.FINANCE_FIXED_ASSET_STATUS,
+ 'number',
+ ),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: FIXED_ASSET_STATUS_IN_USE,
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'assetCode',
+ label: '璧勪骇缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х紪鐮�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'assetName',
+ label: '璧勪骇鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜у悕绉�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '璧勪骇绫诲埆',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х被鍒�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'purchaseDate',
+ label: '璐疆鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'departmentId',
+ label: '浣跨敤閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: departmentSelectProps(),
+ },
+ {
+ fieldName: 'keeperId',
+ label: '淇濈浜�',
+ component: 'ApiSelect',
+ componentProps: keeperSelectProps(),
+ },
+ {
+ fieldName: 'status',
+ label: '璧勪骇鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.FINANCE_FIXED_ASSET_STATUS,
+ 'number',
+ ),
+ placeholder: '璇烽�夋嫨璧勪骇鐘舵��',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<FinFixedAssetApi.FixedAsset>['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'assetCode',
+ title: '璧勪骇缂栫爜',
+ minWidth: 150,
+ fixed: 'left',
+ },
+ {
+ field: 'assetName',
+ title: '璧勪骇鍚嶇О',
+ minWidth: 160,
+ },
+ {
+ field: 'category',
+ title: '璧勪骇绫诲埆',
+ minWidth: 120,
+ },
+ {
+ field: 'specification',
+ title: '瑙勬牸鍨嬪彿',
+ minWidth: 140,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'purchaseDate',
+ title: '璐疆鏃ユ湡',
+ minWidth: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'originalValue',
+ title: '璧勪骇鍘熷��',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'usefulLife',
+ title: '浣跨敤骞撮檺',
+ minWidth: 100,
+ align: 'right',
+ },
+ {
+ field: 'residualRate',
+ title: '娈嬪�肩巼(%)',
+ minWidth: 110,
+ align: 'right',
+ },
+ {
+ field: 'monthlyDepreciation',
+ title: '鏈堟姌鏃ч',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'accumulatedDepreciation',
+ title: '绱鎶樻棫',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'netValue',
+ title: '鍑�鍊�',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ // 鍚庣 departmentId 瀛樼殑鏄儴闂ㄧ紪鍙凤紝鍒楄〃灞曠ず鐢ㄥ悗绔В鏋愬ソ鐨� departmentName
+ field: 'departmentName',
+ title: '浣跨敤閮ㄩ棬',
+ minWidth: 120,
+ },
+ {
+ field: 'keeperName',
+ title: '淇濈浜�',
+ minWidth: 100,
+ },
+ {
+ field: 'status',
+ title: '璧勪骇鐘舵��',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.FINANCE_FIXED_ASSET_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/erp/finance/fixed-asset/index.vue b/src/views/erp/finance/fixed-asset/index.vue
new file mode 100644
index 0000000..6600aef
--- /dev/null
+++ b/src/views/erp/finance/fixed-asset/index.vue
@@ -0,0 +1,217 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinFixedAssetApi } from '#/api/erp/finance/fixed-asset';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteFixedAsset,
+ deleteFixedAssetList,
+ depreciateFixedAssetList,
+ exportFixedAsset,
+ getFixedAssetPage,
+} from '#/api/erp/finance/fixed-asset';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 鍥哄畾璧勪骇鍒楄〃 */
+defineOptions({ name: 'FinFixedAsset' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportFixedAsset(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鍥哄畾璧勪骇.xls', source: data });
+}
+
+/** 鏂板鍥哄畾璧勪骇 */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫鍥哄畾璧勪骇 */
+function handleEdit(row: FinFixedAssetApi.FixedAsset) {
+ formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎鍥哄畾璧勪骇 */
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await (ids.length === 1
+ ? deleteFixedAsset(ids[0]!)
+ : deleteFixedAssetList(ids));
+ checkedIds.value = [];
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 璁℃彁鎶樻棫 */
+async function handleDepreciate(ids: number[]) {
+ const hideLoading = message.loading({
+ content: '璁℃彁鎶樻棫涓�...',
+ duration: 0,
+ });
+ try {
+ const result = await depreciateFixedAssetList(ids);
+ message.success(
+ `鏈璁℃彁 ${result.processedCount} 椤硅祫浜э紝鎶樻棫鍚堣 ${result.totalMonthlyDepreciation} 鍏僠,
+ );
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: FinFixedAssetApi.FixedAsset[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getFixedAssetPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<FinFixedAssetApi.FixedAsset>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Grid table-title="鍥哄畾璧勪骇鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鍥哄畾璧勪骇']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['finance:fixed-asset:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '璁℃彁鎶樻棫',
+ type: 'primary',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['finance:fixed-asset:depreciate'],
+ popConfirm: {
+ title: isEmpty(checkedIds)
+ ? '鏈嬀閫夎祫浜э紝灏嗗鍏ㄩ儴鏈姤搴熻祫浜ц鎻愪竴涓湀鎶樻棫锛岀‘璁ょ户缁悧锛�'
+ : `纭瀵瑰凡鍕鹃�夌殑 ${checkedIds.length} 椤硅祫浜ц鎻愪竴涓湀鎶樻棫鍚楋紵`,
+ confirm: handleDepreciate.bind(null, checkedIds),
+ },
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['finance:fixed-asset:export'],
+ onClick: handleExport,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:fixed-asset:delete'],
+ popConfirm: {
+ title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛�',
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['finance:fixed-asset:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '璁℃彁鎶樻棫',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['finance:fixed-asset:depreciate'],
+ ifShow: () => row.status !== 4,
+ popConfirm: {
+ title: `纭瀵硅祫浜с��${row.assetName}銆戣鎻愪竴涓湀鎶樻棫鍚楋紵`,
+ confirm: handleDepreciate.bind(null, [row.id!]),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:fixed-asset:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.assetName]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/fixed-asset/modules/form.vue b/src/views/erp/finance/fixed-asset/modules/form.vue
new file mode 100644
index 0000000..17c2e2a
--- /dev/null
+++ b/src/views/erp/finance/fixed-asset/modules/form.vue
@@ -0,0 +1,89 @@
+<script lang="ts" setup>
+import type { FinFixedAssetApi } from '#/api/erp/finance/fixed-asset';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createFixedAsset,
+ getFixedAsset,
+ updateFixedAsset,
+} from '#/api/erp/finance/fixed-asset';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<FinFixedAssetApi.FixedAsset>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['鍥哄畾璧勪骇'])
+ : $t('ui.actionTitle.create', ['鍥哄畾璧勪骇']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as FinFixedAssetApi.FixedAsset;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateFixedAsset(data)
+ : createFixedAsset(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formData.value = undefined;
+ await formApi.resetForm();
+ const data = modalApi.getData<FinFixedAssetApi.FixedAsset>();
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getFixedAsset(data.id);
+ await formApi.setValues(formData.value, false);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-1/2" :title="getTitle">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/erp/finance/intangible-asset/data.ts b/src/views/erp/finance/intangible-asset/data.ts
new file mode 100644
index 0000000..ee27bd3
--- /dev/null
+++ b/src/views/erp/finance/intangible-asset/data.ts
@@ -0,0 +1,341 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinIntangibleAssetApi } from '#/api/erp/finance/intangible-asset';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import dayjs from 'dayjs';
+
+/** 璧勪骇鐘舵�侊細1鍦ㄧ敤 2宸插け鏁� 3宸叉憡閿�瀹� */
+export const INTANGIBLE_STATUS_IN_USE = 1;
+export const INTANGIBLE_STATUS_INVALID = 2;
+export const INTANGIBLE_STATUS_AMORTIZED = 3;
+
+/** 鍙鎻愭憡閿�鐨勭姸鎬侊紙浠呫�屽湪鐢ㄣ�嶇殑璧勪骇鎽婇攢锛屽凡澶辨晥/宸叉憡瀹屼笉鍐嶈鎻愶級 */
+export const AMORTIZABLE_INTANGIBLE_STATUS = [INTANGIBLE_STATUS_IN_USE];
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'assetCode',
+ label: '璧勪骇缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х紪鐮侊紝濡� WX2026100001',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'assetName',
+ label: '璧勪骇鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜у悕绉�',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'category',
+ label: '璧勪骇绫诲埆',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х被鍒紝濡� 杞欢钁椾綔鏉�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'certificateNo',
+ label: '璇佷功缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘉涔︾紪鍙�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'acquisitionDate',
+ label: '鍙栧緱鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍙栧緱鏃ユ湡',
+ showTime: false,
+ valueFormat: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD',
+ class: 'w-full',
+ },
+ rules: 'required',
+ defaultValue: dayjs().format('YYYY-MM-DD'),
+ },
+ {
+ fieldName: 'validityDate',
+ label: '鏈夋晥鏈熻嚦',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鏈夋晥鏈熸埅姝㈡棩鏈�',
+ showTime: false,
+ valueFormat: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD',
+ class: 'w-full',
+ },
+ },
+ {
+ fieldName: 'originalValue',
+ label: '璧勪骇鍘熷��',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ヨ祫浜у師鍊�',
+ min: 0,
+ precision: 2,
+ },
+ rules: z.number().min(0).default(0),
+ },
+ {
+ fieldName: 'amortizationPeriod',
+ label: '鎽婇攢骞撮檺锛堝勾锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ユ憡閿�骞撮檺',
+ min: 1,
+ precision: 0,
+ },
+ defaultValue: 10,
+ rules: z.number().min(1).default(10),
+ },
+ {
+ fieldName: 'residualRate',
+ label: '娈嬪�肩巼锛�%锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ユ畫鍊肩巼',
+ min: 0,
+ max: 100,
+ precision: 2,
+ },
+ defaultValue: 0,
+ rules: z.number().min(0).max(100).default(0),
+ help: '鏈堟憡閿�棰� =锛堝師鍊� 脳锛�1 - 娈嬪�肩巼锛夛級梅锛堟憡閿�骞撮檺 脳 12锛�',
+ },
+ {
+ fieldName: 'status',
+ label: '璧勪骇鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.FINANCE_INTANGIBLE_ASSET_STATUS,
+ 'number',
+ ),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ defaultValue: INTANGIBLE_STATUS_IN_USE,
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'description',
+ label: '璧勪骇鎻忚堪',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜ф弿杩�',
+ rows: 2,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'assetCode',
+ label: '璧勪骇缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х紪鐮�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'assetName',
+ label: '璧勪骇鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜у悕绉�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '璧勪骇绫诲埆',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ祫浜х被鍒�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'certificateNo',
+ label: '璇佷功缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘉涔︾紪鍙�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'acquisitionDate',
+ label: '鍙栧緱鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '璧勪骇鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.FINANCE_INTANGIBLE_ASSET_STATUS,
+ 'number',
+ ),
+ placeholder: '璇烽�夋嫨璧勪骇鐘舵��',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<FinIntangibleAssetApi.IntangibleAsset>['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'assetCode',
+ title: '璧勪骇缂栫爜',
+ minWidth: 150,
+ fixed: 'left',
+ },
+ {
+ field: 'assetName',
+ title: '璧勪骇鍚嶇О',
+ minWidth: 160,
+ },
+ {
+ field: 'category',
+ title: '璧勪骇绫诲埆',
+ minWidth: 120,
+ },
+ {
+ field: 'certificateNo',
+ title: '璇佷功缂栧彿',
+ minWidth: 140,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'acquisitionDate',
+ title: '鍙栧緱鏃ユ湡',
+ minWidth: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'validityDate',
+ title: '鏈夋晥鏈熻嚦',
+ minWidth: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'originalValue',
+ title: '璧勪骇鍘熷��',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'amortizationPeriod',
+ title: '鎽婇攢骞撮檺',
+ minWidth: 100,
+ align: 'right',
+ },
+ {
+ field: 'residualRate',
+ title: '娈嬪�肩巼(%)',
+ minWidth: 110,
+ align: 'right',
+ },
+ {
+ field: 'monthlyAmortization',
+ title: '鏈堟憡閿�棰�',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'accumulatedAmortization',
+ title: '绱鎽婇攢',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'netValue',
+ title: '鍑�鍊�',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'status',
+ title: '璧勪骇鐘舵��',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.FINANCE_INTANGIBLE_ASSET_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/erp/finance/intangible-asset/index.vue b/src/views/erp/finance/intangible-asset/index.vue
new file mode 100644
index 0000000..2718d56
--- /dev/null
+++ b/src/views/erp/finance/intangible-asset/index.vue
@@ -0,0 +1,217 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinIntangibleAssetApi } from '#/api/erp/finance/intangible-asset';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ amortizeIntangibleAssetList,
+ deleteIntangibleAsset,
+ deleteIntangibleAssetList,
+ exportIntangibleAsset,
+ getIntangibleAssetPage,
+} from '#/api/erp/finance/intangible-asset';
+import { $t } from '#/locales';
+
+import { INTANGIBLE_STATUS_IN_USE, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 鏃犲舰璧勪骇鍒楄〃 */
+defineOptions({ name: 'FinIntangibleAsset' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportIntangibleAsset(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鏃犲舰璧勪骇.xls', source: data });
+}
+
+/** 鏂板鏃犲舰璧勪骇 */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫鏃犲舰璧勪骇 */
+function handleEdit(row: FinIntangibleAssetApi.IntangibleAsset) {
+ formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎鏃犲舰璧勪骇 */
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await (ids.length === 1
+ ? deleteIntangibleAsset(ids[0]!)
+ : deleteIntangibleAssetList(ids));
+ checkedIds.value = [];
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 璁℃彁鎽婇攢 */
+async function handleAmortize(ids: number[]) {
+ const hideLoading = message.loading({
+ content: '璁℃彁鎽婇攢涓�...',
+ duration: 0,
+ });
+ try {
+ const result = await amortizeIntangibleAssetList(ids);
+ message.success(
+ `鏈璁℃彁 ${result.processedCount} 椤硅祫浜э紝鎽婇攢鍚堣 ${result.totalMonthlyAmortization} 鍏僠,
+ );
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: FinIntangibleAssetApi.IntangibleAsset[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getIntangibleAssetPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<FinIntangibleAssetApi.IntangibleAsset>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Grid table-title="鏃犲舰璧勪骇鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鏃犲舰璧勪骇']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['finance:intangible-asset:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '璁℃彁鎽婇攢',
+ type: 'primary',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['finance:intangible-asset:amortize'],
+ popConfirm: {
+ title: isEmpty(checkedIds)
+ ? '鏈嬀閫夎祫浜э紝灏嗗鍏ㄩ儴銆屽湪鐢ㄣ�嶄笖鏈憡瀹岀殑鏃犲舰璧勪骇璁℃彁涓�涓湀鎽婇攢锛岀‘璁ょ户缁悧锛�'
+ : `纭瀵瑰凡鍕鹃�夌殑 ${checkedIds.length} 椤硅祫浜ц鎻愪竴涓湀鎽婇攢鍚楋紵`,
+ confirm: handleAmortize.bind(null, checkedIds),
+ },
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['finance:intangible-asset:export'],
+ onClick: handleExport,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:intangible-asset:delete'],
+ popConfirm: {
+ title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛�',
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['finance:intangible-asset:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '璁℃彁鎽婇攢',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['finance:intangible-asset:amortize'],
+ ifShow: () => row.status === INTANGIBLE_STATUS_IN_USE,
+ popConfirm: {
+ title: `纭瀵硅祫浜с��${row.assetName}銆戣鎻愪竴涓湀鎽婇攢鍚楋紵`,
+ confirm: handleAmortize.bind(null, [row.id!]),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:intangible-asset:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.assetName]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/intangible-asset/modules/form.vue b/src/views/erp/finance/intangible-asset/modules/form.vue
new file mode 100644
index 0000000..4e9520b
--- /dev/null
+++ b/src/views/erp/finance/intangible-asset/modules/form.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { FinIntangibleAssetApi } from '#/api/erp/finance/intangible-asset';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createIntangibleAsset,
+ getIntangibleAsset,
+ updateIntangibleAsset,
+} from '#/api/erp/finance/intangible-asset';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<FinIntangibleAssetApi.IntangibleAsset>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['鏃犲舰璧勪骇'])
+ : $t('ui.actionTitle.create', ['鏃犲舰璧勪骇']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as FinIntangibleAssetApi.IntangibleAsset;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateIntangibleAsset(data)
+ : createIntangibleAsset(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formData.value = undefined;
+ await formApi.resetForm();
+ const data = modalApi.getData<FinIntangibleAssetApi.IntangibleAsset>();
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getIntangibleAsset(data.id);
+ await formApi.setValues(formData.value, false);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-1/2" :title="getTitle">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/erp/finance/ledger/data.ts b/src/views/erp/finance/ledger/data.ts
new file mode 100644
index 0000000..d41cfd2
--- /dev/null
+++ b/src/views/erp/finance/ledger/data.ts
@@ -0,0 +1,214 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinLedgerApi } from '#/api/erp/finance/ledger';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { z } from '#/adapter/form';
+import { getAccountSubjectList } from '#/api/erp/finance/subject';
+
+import dayjs from 'dayjs';
+
+/** 琛岀被鍨� -> 灞曠ず鏂囨 */
+export const LEDGER_ROW_TYPE_LABEL: Record<string, string> = {
+ opening: '鏈熷垵浣欓',
+ entry: '鏄庣粏',
+ monthly_total: '鏈湀鍚堣',
+ yearly_total: '鏈勾绱',
+};
+
+/** 琛岀被鍨� -> Tag 棰滆壊 */
+export const LEDGER_ROW_TYPE_COLOR: Record<string, string> = {
+ opening: 'blue',
+ entry: 'default',
+ monthly_total: 'orange',
+ yearly_total: 'purple',
+};
+
+/**
+ * 璐﹂〉鏌ヨ琛ㄥ崟
+ *
+ * 璇存槑锛氳处椤靛彧缁熻銆屽凡杩囪处銆嶇殑鍑瘉锛屾湭杩囪处銆佸凡浣滃簾鐨勫嚟璇佷笉璁″叆銆�
+ */
+export function useLedgerFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'subjectCode',
+ label: '浼氳绉戠洰',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: getAccountSubjectList,
+ labelField: 'subjectName',
+ valueField: 'subjectCode',
+ childrenField: 'children',
+ placeholder: '璇烽�夋嫨浼氳绉戠洰',
+ treeDefaultExpandAll: true,
+ showSearch: true,
+ treeNodeFilterProp: 'subjectName',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'startMonth',
+ label: '璧峰鏈堜唤',
+ component: 'DatePicker',
+ componentProps: {
+ picker: 'month',
+ allowClear: true,
+ valueFormat: 'YYYY-MM',
+ format: 'YYYY-MM',
+ style: { width: '100%' },
+ placeholder: '閫夋嫨璧峰鏈堜唤',
+ },
+ defaultValue: dayjs().startOf('year').format('YYYY-MM'),
+ },
+ {
+ fieldName: 'endMonth',
+ label: '缁撴潫鏈堜唤',
+ component: 'DatePicker',
+ componentProps: {
+ picker: 'month',
+ allowClear: true,
+ valueFormat: 'YYYY-MM',
+ format: 'YYYY-MM',
+ style: { width: '100%' },
+ placeholder: '閫夋嫨缁撴潫鏈堜唤',
+ },
+ defaultValue: dayjs().format('YYYY-MM'),
+ },
+ {
+ fieldName: 'includeChildren',
+ label: '鍖呭惈涓嬬骇绉戠洰',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ defaultValue: false,
+ rules: z.boolean().default(false).optional(),
+ help: '寮�鍚悗锛屾墍閫夌鐩強鍏跺叏閮ㄤ笅绾х鐩殑鍙戠敓棰濅笌浣欓鍚堝苟缁熻',
+ },
+ ];
+}
+
+/** 鎬昏处鍒楋細鏈熷垵 + 閫愭湀鍚堣 + 鏈勾绱 */
+export function useGeneralColumns(): VxeTableGridOptions<FinLedgerApi.LedgerRow>['columns'] {
+ return [
+ {
+ field: 'rowType',
+ title: '琛岀被鍨�',
+ width: 110,
+ fixed: 'left',
+ slots: { default: 'rowType' },
+ },
+ {
+ field: 'period',
+ title: '鏈熼棿',
+ width: 110,
+ },
+ {
+ field: 'subjectCode',
+ title: '绉戠洰缂栫爜',
+ minWidth: 120,
+ },
+ {
+ field: 'subjectName',
+ title: '绉戠洰鍚嶇О',
+ minWidth: 160,
+ },
+ {
+ field: 'debit',
+ title: '鍊熸柟鍙戠敓棰�',
+ minWidth: 140,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'credit',
+ title: '璐锋柟鍙戠敓棰�',
+ minWidth: 140,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'direction',
+ title: '鏂瑰悜',
+ width: 80,
+ align: 'center',
+ slots: { default: 'direction' },
+ },
+ {
+ field: 'balance',
+ title: '鏈熸湯浣欓',
+ minWidth: 140,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ ];
+}
+
+/** 鏄庣粏璐﹀垪锛氭湡鍒� + 閫愮瑪鍒嗗綍 + 缁撹浆鍚堣 */
+export function useDetailColumns(): VxeTableGridOptions<FinLedgerApi.LedgerRow>['columns'] {
+ return [
+ {
+ field: 'rowType',
+ title: '琛岀被鍨�',
+ width: 110,
+ fixed: 'left',
+ slots: { default: 'rowType' },
+ },
+ {
+ field: 'date',
+ title: '鏃ユ湡',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'voucherNo',
+ title: '鍑瘉瀛楀彿',
+ minWidth: 140,
+ },
+ {
+ field: 'subjectCode',
+ title: '绉戠洰缂栫爜',
+ minWidth: 120,
+ },
+ {
+ field: 'summary',
+ title: '鎽樿',
+ minWidth: 200,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'debit',
+ title: '鍊熸柟閲戦',
+ minWidth: 140,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'credit',
+ title: '璐锋柟閲戦',
+ minWidth: 140,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'direction',
+ title: '鏂瑰悜',
+ width: 80,
+ align: 'center',
+ slots: { default: 'direction' },
+ },
+ {
+ field: 'balance',
+ title: '浣欓',
+ minWidth: 140,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ ];
+}
+
+/** 浣欓鏂瑰悜瀛楀吀绫诲瀷锛屼緵椤甸潰娓叉煋鏂瑰悜鏍囩浣跨敤 */
+export const LEDGER_DIRECTION_DICT = DICT_TYPE.FINANCE_BALANCE_DIRECTION;
diff --git a/src/views/erp/finance/ledger/detail/index.vue b/src/views/erp/finance/ledger/detail/index.vue
new file mode 100644
index 0000000..cfd60d0
--- /dev/null
+++ b/src/views/erp/finance/ledger/detail/index.vue
@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinLedgerApi } from '#/api/erp/finance/ledger';
+
+import { Page } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { exportDetailLedger, getDetailLedger } from '#/api/erp/finance/ledger';
+import { DictTag } from '#/components/dict-tag';
+import { $t } from '#/locales';
+
+import {
+ LEDGER_DIRECTION_DICT,
+ LEDGER_ROW_TYPE_COLOR,
+ LEDGER_ROW_TYPE_LABEL,
+ useDetailColumns,
+ useLedgerFormSchema,
+} from '../data';
+
+/** 鏄庣粏璐� */
+defineOptions({ name: 'FinDetailLedger' });
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportDetailLedger({
+ ...((await gridApi.formApi.getValues()) as FinLedgerApi.LedgerParams),
+ });
+ downloadFileFromBlobPart({ fileName: '鏄庣粏璐�.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useLedgerFormSchema(),
+ },
+ gridOptions: {
+ columns: useDetailColumns(),
+ height: 'auto',
+ keepSource: true,
+ pagerConfig: {
+ enabled: false,
+ },
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) => {
+ // 绉戠洰涓哄繀濉紝鏈�夌鐩椂鐩存帴杩斿洖绌鸿〃锛岄伩鍏嶆棤鎰忎箟鐨勮剰鏌ヨ
+ if (!formValues.subjectCode) {
+ return [];
+ }
+ return await getDetailLedger(formValues as FinLedgerApi.LedgerParams);
+ },
+ },
+ },
+ rowConfig: {
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<FinLedgerApi.LedgerRow>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <Grid table-title="鏄庣粏璐︼紙浠呯粺璁″凡杩囪处鍑瘉锛屾寜鍑瘉閫愮瑪缁撹浆浣欓锛�">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['finance:ledger:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+
+ <template #rowType="{ row }">
+ <Tag :color="LEDGER_ROW_TYPE_COLOR[row.rowType] ?? 'default'">
+ {{ LEDGER_ROW_TYPE_LABEL[row.rowType] ?? row.rowType }}
+ </Tag>
+ </template>
+
+ <template #direction="{ row }">
+ <DictTag
+ v-if="row.direction"
+ :type="LEDGER_DIRECTION_DICT"
+ :value="row.direction"
+ />
+ <span v-else>--</span>
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/ledger/general/index.vue b/src/views/erp/finance/ledger/general/index.vue
new file mode 100644
index 0000000..8154b29
--- /dev/null
+++ b/src/views/erp/finance/ledger/general/index.vue
@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinLedgerApi } from '#/api/erp/finance/ledger';
+
+import { Page } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ exportGeneralLedger,
+ getGeneralLedger,
+} from '#/api/erp/finance/ledger';
+import { DictTag } from '#/components/dict-tag';
+import { $t } from '#/locales';
+
+import {
+ LEDGER_DIRECTION_DICT,
+ LEDGER_ROW_TYPE_COLOR,
+ LEDGER_ROW_TYPE_LABEL,
+ useGeneralColumns,
+ useLedgerFormSchema,
+} from '../data';
+
+/** 鎬昏处 */
+defineOptions({ name: 'FinGeneralLedger' });
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportGeneralLedger({
+ ...((await gridApi.formApi.getValues()) as FinLedgerApi.LedgerParams),
+ });
+ downloadFileFromBlobPart({ fileName: '鎬昏处.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useLedgerFormSchema(),
+ },
+ gridOptions: {
+ columns: useGeneralColumns(),
+ height: 'auto',
+ keepSource: true,
+ pagerConfig: {
+ enabled: false,
+ },
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) => {
+ // 绉戠洰涓哄繀濉紝鏈�夌鐩椂鐩存帴杩斿洖绌鸿〃锛岄伩鍏嶆棤鎰忎箟鐨勮剰鏌ヨ
+ if (!formValues.subjectCode) {
+ return [];
+ }
+ return await getGeneralLedger(formValues as FinLedgerApi.LedgerParams);
+ },
+ },
+ },
+ rowConfig: {
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<FinLedgerApi.LedgerRow>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <Grid table-title="鎬昏处锛堜粎缁熻宸茶繃璐﹀嚟璇侊級">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['finance:ledger:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+
+ <template #rowType="{ row }">
+ <Tag :color="LEDGER_ROW_TYPE_COLOR[row.rowType] ?? 'default'">
+ {{ LEDGER_ROW_TYPE_LABEL[row.rowType] ?? row.rowType }}
+ </Tag>
+ </template>
+
+ <template #direction="{ row }">
+ <DictTag
+ v-if="row.direction"
+ :type="LEDGER_DIRECTION_DICT"
+ :value="row.direction"
+ />
+ <span v-else>--</span>
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/receipt/modules/item-form.vue b/src/views/erp/finance/receipt/modules/item-form.vue
index 1ec800a..e4dc8e5 100644
--- a/src/views/erp/finance/receipt/modules/item-form.vue
+++ b/src/views/erp/finance/receipt/modules/item-form.vue
@@ -223,7 +223,6 @@
v-model:value="row.receiptPrice"
:precision="2"
:disabled="disabled"
- :formatter="erpPriceInputFormatter"
placeholder="璇疯緭鍏ユ湰娆℃敹娆�"
@change="handleRowChange(row)"
/>
diff --git a/src/views/erp/finance/statistics/index.vue b/src/views/erp/finance/statistics/index.vue
new file mode 100644
index 0000000..10261ff
--- /dev/null
+++ b/src/views/erp/finance/statistics/index.vue
@@ -0,0 +1,272 @@
+<script lang="ts" setup>
+import type { FinStatisticsApi } from '#/api/erp/finance/statistics';
+
+import { computed, onMounted, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+import { erpNumberFormatter } from '@vben/utils';
+
+import { Button, Card, DatePicker, Col, Row, Spin, Tag } from 'ant-design-vue';
+
+import {
+ getStatisticsOverview,
+ getStatisticsTrend,
+} from '#/api/erp/finance/statistics';
+
+import TrendChart from './modules/trend-chart.vue';
+
+/** 璐㈠姟鎶ヨ〃 / 浼氳鏍哥畻鐪嬫澘 */
+defineOptions({ name: 'FinStatistics' });
+
+const loading = ref(false);
+const overview = ref<FinStatisticsApi.StatisticsOverview>();
+const trendList = ref<FinStatisticsApi.StatisticsTrend[]>([]);
+
+/** 鏌ヨ鏉′欢锛氶粯璁ゆ湰骞� 1 鏈堣嚦褰撳墠鏈� */
+const startMonth = ref<string>();
+const endMonth = ref<string>();
+
+/** 椤堕儴鏀舵敮姒傝鍗$墖 */
+const incomeCards = computed(() => [
+ {
+ title: '鏀舵閲戦',
+ value: overview.value?.receiptPrice ?? 0,
+ color: '#18a058',
+ tip: 'ERP 宸插鏍告敹娆惧崟鍚堣',
+ },
+ {
+ title: '浠樻閲戦',
+ value: overview.value?.paymentPrice ?? 0,
+ color: '#d03050',
+ tip: 'ERP 宸插鏍镐粯娆惧崟鍚堣',
+ },
+ {
+ title: '鍑�鍒╂鼎',
+ value: overview.value?.netProfit ?? 0,
+ color: '#2f7df6',
+ tip: '鏀舵閲戦 - 浠樻閲戦',
+ },
+ {
+ title: '閿�鍞鍗曢噾棰�',
+ value: overview.value?.saleOrderPrice ?? 0,
+ color: '#7c3aed',
+ tip: 'ERP 宸插鏍搁攢鍞鍗曞悎璁�',
+ },
+ {
+ title: '閲囪喘璁㈠崟閲戦',
+ value: overview.value?.purchaseOrderPrice ?? 0,
+ color: '#f59e0b',
+ tip: 'ERP 宸插鏍搁噰璐鍗曞悎璁�',
+ },
+]);
+
+/** 璧勪骇姒傝鍗$墖 */
+const assetCards = computed(() => [
+ {
+ title: '鍥哄畾璧勪骇鍑�鍊�',
+ value: overview.value?.fixedAssetNetValue ?? 0,
+ sub: `鍘熷�� ${formatAmount(overview.value?.fixedAssetOriginalValue)} / 绱鎶樻棫 ${formatAmount(overview.value?.fixedAssetAccumulatedDepreciation)}`,
+ color: '#2f7df6',
+ },
+ {
+ title: '鏃犲舰璧勪骇鍑�鍊�',
+ value: overview.value?.intangibleAssetNetValue ?? 0,
+ sub: `鍘熷�� ${formatAmount(overview.value?.intangibleAssetOriginalValue)} / 绱鎽婇攢 ${formatAmount(overview.value?.intangibleAssetAccumulatedAmortization)}`,
+ color: '#18a058',
+ },
+ {
+ title: '璧勪骇鍑�鍊煎悎璁�',
+ value: overview.value?.totalAssetNetValue ?? 0,
+ sub: '鍥哄畾璧勪骇鍑�鍊� + 鏃犲舰璧勪骇鍑�鍊�',
+ color: '#7c3aed',
+ },
+]);
+
+/** 鍑瘉姒傝 */
+const voucherCards = computed(() => [
+ {
+ title: '浼氳绉戠洰',
+ value: overview.value?.subjectCount ?? 0,
+ color: '#2f7df6',
+ },
+ {
+ title: '鍑瘉鎬绘暟',
+ value: overview.value?.voucherCount ?? 0,
+ color: '#7c3aed',
+ },
+ {
+ title: '鏈繃璐�',
+ value: overview.value?.voucherUnpostedCount ?? 0,
+ color: '#f59e0b',
+ },
+ {
+ title: '宸茶繃璐�',
+ value: overview.value?.voucherPostedCount ?? 0,
+ color: '#18a058',
+ },
+ {
+ title: '宸蹭綔搴�',
+ value: overview.value?.voucherCancelledCount ?? 0,
+ color: '#d03050',
+ },
+]);
+
+/** 缁熻鏈熼棿灞曠ず鏂囨 */
+const periodText = computed(() => {
+ if (!overview.value) {
+ return '';
+ }
+ return `${overview.value.startDate} ~ ${overview.value.endDate}`;
+});
+
+function formatAmount(value?: number) {
+ return erpNumberFormatter(value ?? 0, 2);
+}
+
+/** 鍔犺浇鐪嬫澘鏁版嵁 */
+async function loadData() {
+ loading.value = true;
+ try {
+ const params: FinStatisticsApi.StatisticsParams = {
+ startMonth: startMonth.value,
+ endMonth: endMonth.value,
+ };
+ const [overviewData, trendData] = await Promise.all([
+ getStatisticsOverview(params),
+ getStatisticsTrend(params),
+ ]);
+ overview.value = overviewData;
+ trendList.value = trendData ?? [];
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 閲嶇疆涓洪粯璁ゆ湡闂达紙鏈勾 1 鏈堣嚦褰撳墠鏈堬級 */
+function handleReset() {
+ startMonth.value = undefined;
+ endMonth.value = undefined;
+ loadData();
+}
+
+onMounted(loadData);
+</script>
+
+<template>
+ <Page>
+ <Spin :spinning="loading">
+ <div class="flex flex-col gap-4">
+ <!-- 鏌ヨ鏉′欢 -->
+ <Card :body-style="{ padding: '12px 16px' }" class="shadow-sm">
+ <div class="flex flex-wrap items-center gap-3">
+ <span class="font-medium">缁熻鏈熼棿锛�</span>
+ <DatePicker
+ v-model:value="startMonth"
+ picker="month"
+ format="YYYY-MM"
+ value-format="YYYY-MM"
+ placeholder="璧峰鏈堜唤"
+ style="width: 160px"
+ />
+ <span>~</span>
+ <DatePicker
+ v-model:value="endMonth"
+ picker="month"
+ format="YYYY-MM"
+ value-format="YYYY-MM"
+ placeholder="缁撴潫鏈堜唤"
+ style="width: 160px"
+ />
+ <Button type="primary" @click="loadData">鏌ヨ</Button>
+ <Button @click="handleReset">閲嶇疆</Button>
+ <Tag color="blue" class="ml-auto">
+ 鏁版嵁鏉ユ簮锛欵RP 宸插鏍稿崟鎹� + 鏈ā鍧楀凡杩囪处鍑瘉 / 璧勪骇
+ </Tag>
+ </div>
+ </Card>
+
+ <!-- 鏀舵敮姒傝 -->
+ <div>
+ <div class="mb-2 text-muted-foreground">
+ 鏀舵敮涓庤喘閿�姒傝
+ <span v-if="periodText">锛坽{ periodText }}锛�</span>
+ </div>
+ <div
+ class="grid grid-cols-5 gap-4 max-xl:grid-cols-3 max-sm:grid-cols-1"
+ >
+ <Card
+ v-for="item in incomeCards"
+ :key="item.title"
+ :body-style="{ padding: '12px 16px 16px' }"
+ class="h-full shadow-sm"
+ :style="{ borderTop: `3px solid ${item.color}` }"
+ >
+ <div class="mb-2 text-muted-foreground">{{ item.title }}</div>
+ <div
+ class="text-2xl font-bold"
+ :style="{ color: item.color }"
+ >
+ {{ formatAmount(item.value) }}
+ </div>
+ <div class="mt-2 text-xs text-muted-foreground">
+ {{ item.tip }}
+ </div>
+ </Card>
+ </div>
+ </div>
+
+ <!-- 璧勪骇涓庡嚟璇佹瑙� -->
+ <Row :gutter="16">
+ <Col :lg="12" :md="24" :sm="24" :xs="24">
+ <div class="mb-2 text-muted-foreground">璧勪骇姒傝锛堟椂鐐规暟锛�</div>
+ <div class="grid grid-cols-3 gap-4 max-sm:grid-cols-1">
+ <Card
+ v-for="item in assetCards"
+ :key="item.title"
+ :body-style="{ padding: '12px 16px 16px' }"
+ class="h-full shadow-sm"
+ :style="{ borderTop: `3px solid ${item.color}` }"
+ >
+ <div class="mb-2 text-muted-foreground">{{ item.title }}</div>
+ <div
+ class="text-2xl font-bold"
+ :style="{ color: item.color }"
+ >
+ {{ formatAmount(item.value) }}
+ </div>
+ <div class="mt-2 text-xs text-muted-foreground">
+ {{ item.sub }}
+ </div>
+ </Card>
+ </div>
+ </Col>
+ <Col :lg="12" :md="24" :sm="24" :xs="24">
+ <div class="mb-2 text-muted-foreground">鍑瘉姒傝</div>
+ <div class="grid grid-cols-5 gap-4 max-sm:grid-cols-2">
+ <Card
+ v-for="item in voucherCards"
+ :key="item.title"
+ :body-style="{ padding: '12px 16px 16px' }"
+ class="h-full shadow-sm"
+ :style="{ borderTop: `3px solid ${item.color}` }"
+ >
+ <div class="mb-2 truncate text-muted-foreground">
+ {{ item.title }}
+ </div>
+ <div
+ class="text-2xl font-bold"
+ :style="{ color: item.color }"
+ >
+ {{ item.value }}
+ </div>
+ </Card>
+ </div>
+ </Col>
+ </Row>
+
+ <!-- 鏈堝害瓒嬪娍 -->
+ <TrendChart :list="trendList" :loading="loading" />
+ </div>
+ </Spin>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/statistics/modules/trend-chart.vue b/src/views/erp/finance/statistics/modules/trend-chart.vue
new file mode 100644
index 0000000..b3e3626
--- /dev/null
+++ b/src/views/erp/finance/statistics/modules/trend-chart.vue
@@ -0,0 +1,101 @@
+<script lang="ts" setup>
+import type { EChartsOption, EchartsUIType } from '@vben/plugins/echarts';
+import type { FinStatisticsApi } from '#/api/erp/finance/statistics';
+
+import { ref, watch } from 'vue';
+
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card } from 'ant-design-vue';
+
+interface Props {
+ list?: FinStatisticsApi.StatisticsTrend[];
+ loading?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ list: () => [],
+ loading: false,
+});
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+/** 鏌辩姸绯诲垪鍏叡閰嶇疆锛坱ype 闇�涓哄瓧闈㈤噺锛屾晠鐢� as const锛� */
+const BAR_SERIES = { type: 'bar', barMaxWidth: 24 } as const;
+/** 鎶樼嚎绯诲垪鍏叡閰嶇疆 */
+const LINE_SERIES = { type: 'line', smooth: true, symbolSize: 6 } as const;
+
+/** 閲戦鎶樼嚎/鏌辩姸鍥鹃厤缃紙series 鍦� watch 涓寜鏈堜唤鏁版嵁濉厖锛� */
+const baseChartOptions: EChartsOption = {
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 60,
+ containLabel: true,
+ },
+ legend: {
+ top: 15,
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鏀舵敮瓒嬪娍' },
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ padding: [5, 10],
+ },
+ xAxis: {
+ type: 'category',
+ axisTick: { show: false },
+ data: [],
+ },
+ yAxis: {
+ type: 'value',
+ axisTick: { show: false },
+ },
+};
+
+watch(
+ () => props.list,
+ (list) => {
+ if (!list) {
+ return;
+ }
+ renderEcharts({
+ ...baseChartOptions,
+ xAxis: {
+ ...baseChartOptions.xAxis,
+ data: list.map((item) => item.month),
+ },
+ series: [
+ {
+ ...BAR_SERIES,
+ name: '鏀舵閲戦',
+ data: list.map((item) => item.receiptPrice ?? 0),
+ },
+ {
+ ...BAR_SERIES,
+ name: '浠樻閲戦',
+ data: list.map((item) => item.paymentPrice ?? 0),
+ },
+ {
+ ...LINE_SERIES,
+ name: '鍑�鍒╂鼎',
+ data: list.map((item) => item.netProfit ?? 0),
+ },
+ ],
+ });
+ },
+ { immediate: true, deep: true },
+);
+</script>
+
+<template>
+ <Card title="鏈堝害鏀舵敮瓒嬪娍" class="h-full shadow-sm" :loading="loading">
+ <EchartsUI ref="chartRef" height="360px" />
+ </Card>
+</template>
diff --git a/src/views/erp/finance/subject/data.ts b/src/views/erp/finance/subject/data.ts
new file mode 100644
index 0000000..ff4572b
--- /dev/null
+++ b/src/views/erp/finance/subject/data.ts
@@ -0,0 +1,210 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinAccountSubjectApi } from '#/api/erp/finance/subject';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getAccountSubjectList } from '#/api/erp/finance/subject';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'parentId',
+ label: '涓婄骇绉戠洰',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => handleTree(await getAccountSubjectList()),
+ labelField: 'subjectName',
+ valueField: 'id',
+ childrenField: 'children',
+ placeholder: '涓嶉�夋嫨鏃跺垱寤轰负涓�绾х鐩�',
+ treeDefaultExpandAll: true,
+ },
+ help: '鍙湁鏈骇绉戠洰鍙互琚嚟璇佸紩鐢紱宸插瓨鍦ㄤ笅绾х鐩椂涓嶅彲鍐嶆敼涓轰粬绉戠洰鐨勪笅绾�',
+ rules: z.number().optional(),
+ },
+ {
+ fieldName: 'subjectCode',
+ label: '绉戠洰缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ鐩紪鐮侊紝濡� 1001',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'subjectName',
+ label: '绉戠洰鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ鐩悕绉帮紝濡� 搴撳瓨鐜伴噾',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'subjectType',
+ label: '绉戠洰绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.FINANCE_SUBJECT_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨绉戠洰绫诲瀷',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'balanceDirection',
+ label: '浣欓鏂瑰悜',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.FINANCE_BALANCE_DIRECTION, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'subjectCode',
+ label: '绉戠洰缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ鐩紪鐮�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'subjectName',
+ label: '绉戠洰鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ鐩悕绉�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'subjectType',
+ label: '绉戠洰绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.FINANCE_SUBJECT_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨绉戠洰绫诲瀷',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<FinAccountSubjectApi.AccountSubject>['columns'] {
+ return [
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'subjectCode',
+ title: '绉戠洰缂栫爜',
+ minWidth: 140,
+ fixed: 'left',
+ treeNode: true,
+ },
+ {
+ field: 'subjectName',
+ title: '绉戠洰鍚嶇О',
+ minWidth: 180,
+ },
+ {
+ field: 'subjectType',
+ title: '绉戠洰绫诲瀷',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.FINANCE_SUBJECT_TYPE },
+ },
+ },
+ {
+ field: 'balanceDirection',
+ title: '浣欓鏂瑰悜',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.FINANCE_BALANCE_DIRECTION },
+ },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 150,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/erp/finance/subject/index.vue b/src/views/erp/finance/subject/index.vue
new file mode 100644
index 0000000..d709a4a
--- /dev/null
+++ b/src/views/erp/finance/subject/index.vue
@@ -0,0 +1,224 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinAccountSubjectApi } from '#/api/erp/finance/subject';
+
+import { ref } from 'vue';
+
+import { confirm, Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, handleTree, isEmpty } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteAccountSubject,
+ exportAccountSubject,
+ getAccountSubjectPage,
+} from '#/api/erp/finance/subject';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 璐㈠姟浼氳绉戠洰鍒楄〃 */
+defineOptions({ name: 'FinAccountSubject' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒囨崲鏍戝舰灞曞紑/鏀剁缉鐘舵�� */
+const isExpanded = ref(true);
+function handleExpand() {
+ isExpanded.value = !isExpanded.value;
+ gridApi.grid.setAllTreeExpand(isExpanded.value);
+}
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportAccountSubject(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '浼氳绉戠洰.xls', source: data });
+}
+
+/** 鍒涘缓浼氳绉戠洰 */
+function handleCreate() {
+ formModalApi.setData({ parentId: undefined }).open();
+}
+
+/** 鏂板涓嬬骇绉戠洰 */
+function handleAppend(row: FinAccountSubjectApi.AccountSubject) {
+ formModalApi.setData({ parentId: row.id }).open();
+}
+
+/** 缂栬緫浼氳绉戠洰 */
+function handleEdit(row: FinAccountSubjectApi.AccountSubject) {
+ formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎浼氳绉戠洰 */
+async function handleDelete(row: FinAccountSubjectApi.AccountSubject) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.subjectName]),
+ duration: 0,
+ });
+ try {
+ await deleteAccountSubject([row.id!]);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.subjectName]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鎵归噺鍒犻櫎浼氳绉戠洰 */
+async function handleDeleteBatch() {
+ await confirm($t('ui.actionMessage.deleteBatchConfirm'));
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deletingBatch'),
+ duration: 0,
+ });
+ try {
+ await deleteAccountSubject(checkedIds.value);
+ checkedIds.value = [];
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: FinAccountSubjectApi.AccountSubject[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ pagerConfig: {
+ enabled: false,
+ },
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) => {
+ // 涓�娆℃�ф媺鍙栧叏閮ㄥ懡涓殑绉戠洰锛屽啀鍦ㄥ唴瀛橀噷缁勮涓烘爲锛涘垎椤靛鏍戝舰灞曠ず娌℃湁鎰忎箟
+ const page = await getAccountSubjectPage({
+ pageNo: 1,
+ pageSize: -1,
+ ...formValues,
+ });
+ return handleTree(page.list);
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ treeConfig: {
+ parentField: 'parentId',
+ rowField: 'id',
+ transform: true,
+ expandAll: true,
+ reserve: true,
+ },
+ } as VxeTableGridOptions<FinAccountSubjectApi.AccountSubject>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Grid table-title="浼氳绉戠洰鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['浼氳绉戠洰']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['finance:account-subject:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['finance:account-subject:export'],
+ onClick: handleExport,
+ },
+ {
+ label: isExpanded ? '鏀剁缉' : '灞曞紑',
+ type: 'primary',
+ onClick: handleExpand,
+ },
+ {
+ label: $t('ui.actionTitle.deleteBatch'),
+ type: 'primary',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:account-subject:delete'],
+ disabled: isEmpty(checkedIds),
+ onClick: handleDeleteBatch,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鏂板涓嬬骇',
+ type: 'link',
+ icon: ACTION_ICON.ADD,
+ auth: ['finance:account-subject:create'],
+ onClick: handleAppend.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['finance:account-subject:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:account-subject:delete'],
+ disabled: !!(row.children && row.children.length > 0),
+ popConfirm: {
+ disabled: !!(row.children && row.children.length > 0),
+ title: `纭瑕佸垹闄ょ鐩��${row.subjectCode} ${row.subjectName}銆嶅悧锛熷瓨鍦ㄤ笅绾х鐩垨宸茶鍑瘉寮曠敤鏃舵棤娉曞垹闄ゃ�俙,
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/subject/modules/form.vue b/src/views/erp/finance/subject/modules/form.vue
new file mode 100644
index 0000000..ea81950
--- /dev/null
+++ b/src/views/erp/finance/subject/modules/form.vue
@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { FinAccountSubjectApi } from '#/api/erp/finance/subject';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createAccountSubject,
+ getAccountSubject,
+ updateAccountSubject,
+} from '#/api/erp/finance/subject';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type SubjectFormModalData = Partial<FinAccountSubjectApi.AccountSubject>;
+
+const emit = defineEmits(['success']);
+const formData = ref<FinAccountSubjectApi.AccountSubject>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['浼氳绉戠洰'])
+ : $t('ui.actionTitle.create', ['浼氳绉戠洰']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as FinAccountSubjectApi.AccountSubject;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateAccountSubject(data)
+ : createAccountSubject(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formData.value = undefined;
+ await formApi.resetForm();
+ const data = modalApi.getData<SubjectFormModalData>();
+ if (!data) {
+ return;
+ }
+ // 缂栬緫锛氬姞杞借鎯咃紱鏂板涓嬬骇锛氫粎棰勫~涓婄骇绉戠洰
+ if (data.id) {
+ modalApi.lock();
+ try {
+ formData.value = await getAccountSubject(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ } else if (data.parentId) {
+ await formApi.setValues({ parentId: data.parentId });
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-2/5" :title="getTitle">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/erp/finance/voucher/data.ts b/src/views/erp/finance/voucher/data.ts
new file mode 100644
index 0000000..3953605
--- /dev/null
+++ b/src/views/erp/finance/voucher/data.ts
@@ -0,0 +1,297 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinVoucherApi } from '#/api/erp/finance/voucher';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import dayjs from 'dayjs';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鏂板/淇敼鐨勮〃鍗曪紙鍒嗗綍鐢� entry-table 鍗曠嫭缁存姢锛� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ component: 'Input',
+ fieldName: 'id',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'voucherNo',
+ label: '鍑瘉瀛楀彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '鐣欑┖鑷姩鐢熸垚',
+ allowClear: true,
+ },
+ help: '鐣欑┖鏃剁敱绯荤粺鎸夈�愯 + 骞存湀 + 4 浣嶆祦姘淬�戣嚜鍔ㄧ敓鎴�',
+ },
+ {
+ fieldName: 'voucherDate',
+ label: '鍑瘉鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍑瘉鏃ユ湡',
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ class: 'w-full',
+ },
+ rules: 'required',
+ defaultValue: dayjs().format('YYYY-MM-DD'),
+ },
+ {
+ // 涓庡嚟璇佸瓧鍙枫�佸嚟璇佹棩鏈熷苟鎺掍竴琛岋紝閬垮厤涓夊垪鏍呮牸閲岀┖鍑轰竴鏍�
+ fieldName: 'attachmentCount',
+ label: '闄勪欢寮犳暟',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ placeholder: '璇疯緭鍏ラ檮浠跺紶鏁�',
+ min: 0,
+ precision: 0,
+ },
+ defaultValue: 0,
+ rules: z.number().min(0).default(0),
+ },
+ {
+ fieldName: 'summary',
+ label: '鍑瘉鎽樿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ嚟璇佹憳瑕侊紝濡傦細鏀粯 10 鏈堝姙鍏垂',
+ allowClear: true,
+ maxLength: 200,
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'entries',
+ label: '鍑瘉鍒嗗綍',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'blobIds',
+ label: '鍑瘉闄勪欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ helpText: '鏀寔鍥剧墖銆丳DF銆丒xcel 绛夋牸寮忥紝鍗曚釜鏂囦欢涓嶈秴杩� 50MB',
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 鍒嗗綍琛ㄦ牸鐨勫瓧娈� */
+export function useEntryColumns(
+ disabled: boolean,
+): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '搴忓彿', width: 55, fixed: 'left' },
+ {
+ field: 'summary',
+ title: '鎽樿',
+ minWidth: 140,
+ slots: { default: 'summary' },
+ },
+ {
+ field: 'subjectCode',
+ title: '浼氳绉戠洰',
+ minWidth: 180,
+ slots: { default: 'subject' },
+ },
+ {
+ field: 'debit',
+ title: '鍊熸柟閲戦',
+ minWidth: 130,
+ align: 'center',
+ slots: { default: 'debit' },
+ },
+ {
+ field: 'credit',
+ title: '璐锋柟閲戦',
+ minWidth: 130,
+ align: 'center',
+ slots: { default: 'credit' },
+ },
+ {
+ field: 'auxiliaryType',
+ title: '杈呭姪鏍哥畻',
+ minWidth: 120,
+ slots: { default: 'auxiliaryType' },
+ },
+ {
+ field: 'auxiliaryName',
+ title: '杈呭姪鏍哥畻瀵硅薄',
+ minWidth: 150,
+ slots: { default: 'auxiliaryName' },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 70,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ visible: !disabled,
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'voucherNo',
+ label: '鍑瘉瀛楀彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ嚟璇佸瓧鍙�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'summary',
+ label: '鍑瘉鎽樿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ嚟璇佹憳瑕�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'voucherDate',
+ label: '鍑瘉鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ showTime: false,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'creator',
+ label: '鍒跺崟浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ埗鍗曚汉璐﹀彿',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鍑瘉鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.FINANCE_VOUCHER_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鍑瘉鐘舵��',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<FinVoucherApi.Voucher>['columns'] {
+ return [
+ {
+ type: 'expand',
+ width: 45,
+ slots: { content: 'expand_content' },
+ },
+ { type: 'checkbox', width: 40 },
+ {
+ field: 'voucherNo',
+ title: '鍑瘉瀛楀彿',
+ minWidth: 150,
+ fixed: 'left',
+ },
+ {
+ field: 'voucherDate',
+ title: '鍑瘉鏃ユ湡',
+ minWidth: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'summary',
+ title: '鎽樿',
+ minWidth: 200,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'debit',
+ title: '鍊熸柟鍚堣',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'credit',
+ title: '璐锋柟鍚堣',
+ minWidth: 130,
+ align: 'right',
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'status',
+ title: '鍑瘉鐘舵��',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.FINANCE_VOUCHER_STATUS },
+ },
+ },
+ {
+ // 鍚庣 creator 瀛樼殑鏄敤鎴� ID锛岀洿鎺ュ睍绀烘槸鏁板瓧锛屽垪閲岃窡瀵煎嚭閮芥敼鐢� creatorName锛堟樀绉帮級锛�
+ // 鎼滅储妗嗕粛鎸� creator 浼犲弬锛岃涔変笉鍙�
+ field: 'creatorName',
+ title: '鍒跺崟浜�',
+ minWidth: 120,
+ },
+ {
+ field: 'attachmentCount',
+ title: '闄勪欢寮犳暟',
+ minWidth: 100,
+ },
+ {
+ title: '闄勪欢',
+ field: 'attachmentList',
+ minWidth: 100,
+ slots: { default: 'attachment' },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/erp/finance/voucher/index.vue b/src/views/erp/finance/voucher/index.vue
new file mode 100644
index 0000000..bf9eca1
--- /dev/null
+++ b/src/views/erp/finance/voucher/index.vue
@@ -0,0 +1,298 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinVoucherApi } from '#/api/erp/finance/voucher';
+
+import { ref } from 'vue';
+
+import { confirm, Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import {
+ ACTION_ICON,
+ TableAction,
+ useVbenVxeGrid,
+ VxeColumn,
+ VxeTable,
+} from '#/adapter/vxe-table';
+import {
+ cancelVoucher,
+ deleteVoucherList,
+ exportVoucher,
+ getVoucherPageWithEntry,
+ postVoucher,
+} from '#/api/erp/finance/voucher';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 璁拌处鍑瘉鍒楄〃 */
+defineOptions({ name: 'FinVoucher' });
+
+/** 鍑瘉鐘舵�侊細0鏈繃璐� 10宸茶繃璐� 20宸蹭綔搴� */
+const VOUCHER_STATUS_UNPOSTED = 0;
+const VOUCHER_STATUS_POSTED = 10;
+const VOUCHER_STATUS_CANCELLED = 20;
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportVoucher(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '璁拌处鍑瘉.xls', source: data });
+}
+
+/** 鏂板璁拌处鍑瘉 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫璁拌处鍑瘉 */
+function handleEdit(row: FinVoucherApi.Voucher) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅璇︽儏 */
+function handleDetail(row: FinVoucherApi.Voucher) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鍒犻櫎璁拌处鍑瘉 */
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deleteVoucherList(ids);
+ checkedIds.value = [];
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鍑瘉杩囪处 */
+async function handlePost(row: FinVoucherApi.Voucher) {
+ const hideLoading = message.loading({ content: '杩囪处涓�...', duration: 0 });
+ try {
+ await postVoucher(row.id!);
+ message.success(`鍑瘉銆�${row.voucherNo}銆戣繃璐︽垚鍔焋);
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鍑瘉浣滃簾 */
+async function handleCancel(row: FinVoucherApi.Voucher) {
+ await confirm(
+ `纭瑕佷綔搴熷嚟璇併��${row.voucherNo}銆戝悧锛熶綔搴熷悗璇ュ嚟璇佷笉鍐嶈鍏ユ�昏处涓庢槑缁嗚处锛屼笖涓嶅彲鎭㈠銆俙,
+ );
+ const hideLoading = message.loading({ content: '浣滃簾涓�...', duration: 0 });
+ try {
+ await cancelVoucher(row.id!);
+ message.success(`鍑瘉銆�${row.voucherNo}銆戝凡浣滃簾`);
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: FinVoucherApi.Voucher[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getVoucherPageWithEntry({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<FinVoucherApi.Voucher>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Grid table-title="璁拌处鍑瘉鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['璁拌处鍑瘉']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['finance:voucher:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['finance:voucher:export'],
+ onClick: handleExport,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:voucher:delete'],
+ popConfirm: {
+ title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛熷凡杩囪处鐨勫嚟璇佷笉鍏佽鍒犻櫎銆�',
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+
+ <template #expand_content="{ row }">
+ <VxeTable
+ :data="row.entries ?? []"
+ border
+ size="small"
+ show-overflow
+ class="mx-2"
+ >
+ <VxeColumn title="搴忓彿" width="60">
+ <template #default="{ row: entry }">
+ {{ entry.rowNo ?? '-' }}
+ </template>
+ </VxeColumn>
+ <VxeColumn title="鎽樿" min-width="180">
+ <template #default="{ row: entry }">
+ {{ entry.summary || '-' }}
+ </template>
+ </VxeColumn>
+ <VxeColumn title="浼氳绉戠洰" min-width="220">
+ <template #default="{ row: entry }">
+ {{ entry.subjectCode }} {{ entry.subjectName }}
+ </template>
+ </VxeColumn>
+ <VxeColumn title="鍊熸柟閲戦" align="right" width="140">
+ <template #default="{ row: entry }">
+ {{ entry.debit ? entry.debit.toFixed(2) : '-' }}
+ </template>
+ </VxeColumn>
+ <VxeColumn title="璐锋柟閲戦" align="right" width="140">
+ <template #default="{ row: entry }">
+ {{ entry.credit ? entry.credit.toFixed(2) : '-' }}
+ </template>
+ </VxeColumn>
+ <VxeColumn title="杈呭姪鏍哥畻" min-width="150">
+ <template #default="{ row: entry }">
+ {{ entry.auxiliaryName || '-' }}
+ </template>
+ </VxeColumn>
+ </VxeTable>
+ </template>
+
+ <template #attachment="{ row }">
+ <Tag v-if="!row.hasAttachment" color="warning">鏈笂浼�</Tag>
+ <Tag v-else color="success">宸蹭笂浼�</Tag>
+ </template>
+
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['finance:voucher:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['finance:voucher:update'],
+ ifShow: () => row.status === VOUCHER_STATUS_UNPOSTED,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '杩囪处',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['finance:voucher:update-status'],
+ ifShow: () => row.status === VOUCHER_STATUS_UNPOSTED,
+ popConfirm: {
+ title: `纭杩囪处鍑瘉銆�${row.voucherNo}銆戝悧锛熻繃璐﹀悗灏嗚鍏ユ�昏处涓庢槑缁嗚处銆俙,
+ confirm: handlePost.bind(null, row),
+ },
+ },
+ {
+ label: '浣滃簾',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:voucher:update-status'],
+ ifShow: () =>
+ row.status === VOUCHER_STATUS_UNPOSTED ||
+ row.status === VOUCHER_STATUS_POSTED,
+ onClick: handleCancel.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['finance:voucher:delete'],
+ ifShow: () =>
+ row.status === VOUCHER_STATUS_UNPOSTED ||
+ row.status === VOUCHER_STATUS_CANCELLED,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.voucherNo]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/finance/voucher/modules/account-select.vue b/src/views/erp/finance/voucher/modules/account-select.vue
new file mode 100644
index 0000000..4b6b093
--- /dev/null
+++ b/src/views/erp/finance/voucher/modules/account-select.vue
@@ -0,0 +1,150 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinAccountSubjectApi } from '#/api/erp/finance/subject';
+
+import { nextTick, ref } from 'vue';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getAccountSubjectPage } from '#/api/erp/finance/subject';
+
+defineOptions({ name: 'FinAccountSelect' });
+
+const emit = defineEmits<{
+ selected: [subject: FinAccountSubjectApi.AccountSubject];
+}>();
+
+const open = ref(false); // 寮圭獥鏄惁鎵撳紑
+const selectedRow = ref<FinAccountSubjectApi.AccountSubject>(); // 閫変腑鐨勭鐩�
+
+/** 琛ㄦ牸閰嶇疆 */
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: [
+ {
+ fieldName: 'subjectCode',
+ label: '绉戠洰缂栫爜',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ョ鐩紪鐮�', allowClear: true },
+ },
+ {
+ fieldName: 'subjectName',
+ label: '绉戠洰鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ョ鐩悕绉�', allowClear: true },
+ },
+ ],
+ },
+ gridOptions: {
+ columns: [
+ { type: 'radio', width: 50 },
+ {
+ field: 'subjectCode',
+ title: '绉戠洰缂栫爜',
+ minWidth: 140,
+ treeNode: true,
+ },
+ { field: 'subjectName', title: '绉戠洰鍚嶇О', minWidth: 200 },
+ {
+ field: 'subjectType',
+ title: '绉戠洰绫诲瀷',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: 'finance_subject_type' },
+ },
+ },
+ {
+ field: 'balanceDirection',
+ title: '浣欓鏂瑰悜',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: 'finance_balance_direction' },
+ },
+ },
+ ],
+ height: 480,
+ keepSource: true,
+ radioConfig: {
+ highlight: true,
+ trigger: 'row',
+ },
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) => {
+ // 鏍戝舰灞曠ず涓嶉�傜敤鍒嗛〉锛屼竴娆℃�ф媺鍙栧叏閮ㄥ懡涓鐩�
+ const page = await getAccountSubjectPage({
+ pageNo: 1,
+ pageSize: -1,
+ status: 0,
+ ...formValues,
+ });
+ return {
+ list: page.list,
+ total: page.list.length,
+ };
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ treeConfig: {
+ parentField: 'parentId',
+ rowField: 'id',
+ transform: true,
+ expandAll: true,
+ reserve: true,
+ },
+ } as VxeTableGridOptions<FinAccountSubjectApi.AccountSubject>,
+ gridEvents: {
+ radioChange: ({ row }: { row: FinAccountSubjectApi.AccountSubject }) => {
+ selectedRow.value = row;
+ },
+ },
+});
+
+/** 鎵撳紑寮圭獥 */
+async function openModal() {
+ open.value = true;
+ selectedRow.value = undefined;
+ // 鐗规畩锛氬脊绐楁槸 destroy-on-close锛岄噷闈� Grid 闅忓脊绐楅噸寤猴紝蹇呴』绛夋寕杞藉畬鎴愬悗 gridApi 鎵嶆湁鐪熷疄渚�
+ // 锛坴ben 鐨� gridApi.grid 鍒濆鏄┖瀵硅薄锛宮ount 鏃舵墠璧嬪�硷級锛屽惁鍒� clearRadioRow 浼氭姤 is not a function
+ await nextTick();
+ await gridApi.grid.clearRadioRow();
+ await gridApi.formApi.resetForm();
+ await gridApi.query();
+}
+
+/** 纭閫夋嫨 */
+function handleConfirm() {
+ if (!selectedRow.value) {
+ message.warning('璇烽�夋嫨涓�鏉′細璁$鐩�');
+ return;
+ }
+ emit('selected', selectedRow.value);
+ open.value = false;
+}
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+ <Modal
+ v-model:open="open"
+ title="閫夋嫨浼氳绉戠洰"
+ width="70%"
+ :destroy-on-close="true"
+ @ok="handleConfirm"
+ @cancel="open = false"
+ >
+ <Grid table-title="浼氳绉戠洰鍒楄〃锛堜粎鍚敤鐘舵�佺殑绉戠洰鍙鍑瘉寮曠敤锛�" />
+ </Modal>
+</template>
diff --git a/src/views/erp/finance/voucher/modules/entry-table.vue b/src/views/erp/finance/voucher/modules/entry-table.vue
new file mode 100644
index 0000000..6ec1c95
--- /dev/null
+++ b/src/views/erp/finance/voucher/modules/entry-table.vue
@@ -0,0 +1,361 @@
+<script lang="ts" setup>
+import type { FinVoucherApi } from '#/api/erp/finance/voucher';
+
+import { computed, nextTick, ref, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { Input, InputNumber, Select } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+
+import { useEntryColumns } from '../data';
+import AccountSelect from './account-select.vue';
+
+interface EntryRow extends FinVoucherApi.VoucherEntry {
+ seq: number; // 鍓嶇琛屾爣璇嗭紝鐢ㄤ簬琛ㄦ牸瀹氫綅
+}
+
+interface Props {
+ entries?: FinVoucherApi.VoucherEntry[];
+ disabled?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ entries: () => [],
+ disabled: false,
+});
+
+const emit = defineEmits(['update:entries']);
+
+/** 鍒嗗綍琛岋紝seq 鐢ㄤ簬琛ㄦ牸琛屽畾浣嶏紙鏂板鍑瘉鏃跺垎褰曡繕娌℃湁 id锛� */
+const tableData = ref<EntryRow[]>([]);
+let seq = 0;
+
+/** 鎶婂悗绔垎褰曡浆涓哄甫 seq 鐨勮〃鏍艰 */
+function toRows(entries: FinVoucherApi.VoucherEntry[]): EntryRow[] {
+ return entries.map((entry) => ({ ...entry, seq: ++seq }));
+}
+
+/** 鍚堣锛氬�熸柟銆佽捶鏂广�佸樊棰� */
+const summaries = computed(() => {
+ const debit = tableData.value.reduce(
+ (sum, item) => sum + (item.debit || 0),
+ 0,
+ );
+ const credit = tableData.value.reduce(
+ (sum, item) => sum + (item.credit || 0),
+ 0,
+ );
+ return { debit, credit, diff: debit - credit };
+});
+
+/** 鏄惁鍊熻捶骞宠 */
+const isBalanced = computed(
+ () => summaries.value.debit > 0 && summaries.value.diff === 0,
+);
+
+/** 琛ㄦ牸閰嶇疆 */
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useEntryColumns(props.disabled),
+ data: tableData.value,
+ minHeight: 300,
+ autoResize: true,
+ border: true,
+ rowConfig: {
+ keyField: 'seq',
+ isHover: true,
+ },
+ pagerConfig: {
+ enabled: false,
+ },
+ toolbarConfig: {
+ enabled: false,
+ },
+ },
+});
+
+/** 鐩戝惉澶栭儴浼犲叆鐨勫垎褰� */
+watch(
+ () => props.entries,
+ async (entries) => {
+ if (!entries) {
+ return;
+ }
+ tableData.value = toRows(entries);
+ await nextTick(); // 鐗规畩锛氫繚璇� gridApi 宸茬粡鍒濆鍖�
+ await gridApi.grid.reloadData(tableData.value);
+ },
+ { immediate: true },
+);
+
+/** 閫氱煡鐖剁粍浠跺垎褰曞凡鍙樻洿 */
+function emitChange() {
+ const entries = tableData.value.map(({ seq: _seq, ...entry }) => entry);
+ emit('update:entries', entries);
+}
+
+/** 淇敼鏌愯鏁版嵁 */
+function handleRowChange(row: EntryRow) {
+ const index = tableData.value.findIndex((item) => item.seq === row.seq);
+ if (index === -1) {
+ tableData.value.push(row);
+ } else {
+ tableData.value[index] = row;
+ }
+ emitChange();
+}
+
+/** 褰曞叆鍊熸柟閲戦鏃舵竻绌鸿捶鏂归噾棰濓紝鍙嶄箣浜︾劧锛堝悓涓�琛屽彧鑳芥湁涓�鏂规湁閲戦锛� */
+function handleDebitChange(row: EntryRow) {
+ if (row.debit && row.debit > 0) {
+ row.credit = 0;
+ }
+ handleRowChange(row);
+}
+
+function handleCreditChange(row: EntryRow) {
+ if (row.credit && row.credit > 0) {
+ row.debit = 0;
+ }
+ handleRowChange(row);
+}
+
+/** 閫夋嫨浼氳绉戠洰 */
+const accountSelectRef = ref();
+const editingRow = ref<EntryRow>();
+
+function handleOpenAccount(row: EntryRow) {
+ if (props.disabled) {
+ return;
+ }
+ editingRow.value = row;
+ accountSelectRef.value?.open();
+}
+
+function handleSelectAccount(subject: {
+ subjectCode: string;
+ subjectName: string;
+}) {
+ const row = editingRow.value;
+ if (!row) {
+ return;
+ }
+ row.subjectCode = subject.subjectCode;
+ row.subjectName = subject.subjectName;
+ handleRowChange(row);
+}
+
+/** 娣诲姞涓�琛岀┖鐧藉垎褰� */
+function handleAdd() {
+ const row: EntryRow = {
+ seq: ++seq,
+ subjectCode: undefined,
+ subjectName: undefined,
+ summary: '',
+ debit: 0,
+ credit: 0,
+ auxiliaryType: undefined,
+ auxiliaryId: '',
+ auxiliaryName: '',
+ };
+ tableData.value.push(row);
+ gridApi.grid.reloadData(tableData.value);
+ emitChange();
+}
+
+/** 鍒犻櫎涓�琛屽垎褰� */
+function handleDelete(row: EntryRow) {
+ const index = tableData.value.findIndex((item) => item.seq === row.seq);
+ if (index !== -1) {
+ tableData.value.splice(index, 1);
+ }
+ gridApi.grid.reloadData(tableData.value);
+ emitChange();
+}
+
+/** 琛ㄥ崟鏍¢獙锛氫笌鍚庣 FinVoucherServiceImpl 鐨勬牎楠屽彛寰勪繚鎸佷竴鑷� */
+function validate() {
+ const rows = tableData.value.filter(
+ (item) => item.subjectCode || item.debit || item.credit,
+ );
+ if (rows.length === 0) {
+ throw new Error('璇疯嚦灏戝綍鍏ヤ竴鏉″嚟璇佸垎褰�');
+ }
+ for (let i = 0; i < rows.length; i++) {
+ const item = rows[i]!;
+ if (!item.subjectCode) {
+ throw new Error(`绗� ${i + 1} 琛岋細宸插綍鍏ラ噾棰濅絾鏈�夋嫨浼氳绉戠洰`);
+ }
+ const debit = item.debit || 0;
+ const credit = item.credit || 0;
+ if (debit <= 0 && credit <= 0) {
+ throw new Error(`绗� ${i + 1} 琛岋細宸查�夋嫨浼氳绉戠洰浣嗘湭褰曞叆閲戦`);
+ }
+ if (debit > 0 && credit > 0) {
+ throw new Error(`绗� ${i + 1} 琛岋細鍊熸柟涓庤捶鏂归噾棰濅笉鑳藉悓鏃跺ぇ浜� 0`);
+ }
+ }
+ if (summaries.value.debit <= 0) {
+ throw new Error('鍑瘉鍊熻捶鍚堣蹇呴』澶т簬 0');
+ }
+ if (summaries.value.diff !== 0) {
+ throw new Error(
+ `鍑瘉鍊熻捶涓嶅钩琛★細鍊熸柟鍚堣 ${erpPriceInputFormatter(summaries.value.debit)}锛宍 +
+ `璐锋柟鍚堣 ${erpPriceInputFormatter(summaries.value.credit)}锛宍 +
+ `宸 ${erpPriceInputFormatter(summaries.value.diff)}`,
+ );
+ }
+}
+
+defineExpose({ validate });
+</script>
+
+<template>
+ <div>
+ <Grid class="w-full">
+ <template #summary="{ row }">
+ <Input
+ v-model:value="row.summary"
+ :disabled="disabled"
+ placeholder="璇疯緭鍏ユ憳瑕�"
+ @change="handleRowChange(row)"
+ />
+ </template>
+
+ <template #subject="{ row }">
+ <div
+ class="flex cursor-pointer items-center"
+ :class="{ 'text-primary': !disabled }"
+ @click="handleOpenAccount(row)"
+ >
+ <span v-if="row.subjectCode">
+ {{ row.subjectCode }} {{ row.subjectName }}
+ </span>
+ <span v-else class="text-muted-foreground">璇烽�夋嫨浼氳绉戠洰</span>
+ </div>
+ </template>
+
+ <template #debit="{ row }">
+ <InputNumber
+ v-model:value="row.debit"
+ :precision="2"
+ :min="0"
+ :disabled="disabled"
+ class="w-full"
+ placeholder="鍊熸柟閲戦"
+ @change="handleDebitChange(row)"
+ />
+ </template>
+
+ <template #credit="{ row }">
+ <InputNumber
+ v-model:value="row.credit"
+ :precision="2"
+ :min="0"
+ :disabled="disabled"
+ class="w-full"
+ placeholder="璐锋柟閲戦"
+ @change="handleCreditChange(row)"
+ />
+ </template>
+
+ <template #auxiliaryType="{ row }">
+ <Select
+ v-model:value="row.auxiliaryType"
+ :disabled="disabled"
+ allow-clear
+ class="w-full"
+ placeholder="鏃�"
+ @change="handleRowChange(row)"
+ >
+ <Select.Option
+ v-for="dict in getDictOptions(
+ DICT_TYPE.FINANCE_AUXILIARY_TYPE,
+ 'number',
+ )"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </Select.Option>
+ </Select>
+ </template>
+
+ <template #auxiliaryName="{ row }">
+ <Input
+ v-model:value="row.auxiliaryName"
+ :disabled="disabled"
+ placeholder="璇疯緭鍏ヨ緟鍔╂牳绠楀璞�"
+ @change="handleRowChange(row)"
+ />
+ </template>
+
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鍒犻櫎',
+ type: 'link',
+ danger: true,
+ popConfirm: {
+ title: '纭鍒犻櫎璇ュ垎褰曞悧锛�',
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+
+ <template #bottom>
+ <div class="mt-2 rounded border border-border bg-muted p-2">
+ <div class="flex justify-between text-sm">
+ <span class="font-medium text-foreground">鍚堣锛�</span>
+ <div class="flex space-x-4">
+ <span>
+ 鍊熸柟鍚堣锛歿{ erpPriceInputFormatter(summaries.debit) }}
+ </span>
+ <span>
+ 璐锋柟鍚堣锛歿{ erpPriceInputFormatter(summaries.credit) }}
+ </span>
+ <span :class="isBalanced ? 'text-green-600' : 'text-red-500'">
+ 宸锛歿{ erpPriceInputFormatter(summaries.diff) }}
+ {{ isBalanced ? '锛堝�熻捶骞宠 锛�' : '锛堝�熻捶涓嶅钩琛★級' }}
+ </span>
+ </div>
+ </div>
+ </div>
+ <TableAction
+ v-if="!disabled"
+ class="mt-2 flex justify-center"
+ :actions="[
+ {
+ label: '娣诲姞鍒嗗綍',
+ type: 'default',
+ onClick: handleAdd,
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+
+ <!-- 浼氳绉戠洰閫夋嫨 -->
+ <AccountSelect ref="accountSelectRef" @selected="handleSelectAccount" />
+ </div>
+</template>
+
+<style scoped>
+/*
+ * 鍊熸柟/璐锋柟閲戦鍒楀眳涓��
+ * vxe 鍒楃殑 align: 'center' 鍙綔鐢ㄤ簬鍗曞厓鏍硷紝鑰� ant 鐨� InputNumber 鍐呴儴 <input>
+ * 锛�.ant-input-number-input锛夎嚜甯� text-align: start锛屼細鎶婃暟瀛楀帇鍥炲乏杈癸紝瀵艰嚧琛ㄥご灞呬腑銆�
+ * 鏁板瓧闈犲乏鐨勯敊浣嶏紝鎵�浠ヨ繖閲岃繛杈撳叆妗嗗唴鐨勬枃瀛椾竴璧疯鐩栥��
+ * 鏈粍浠跺彧鏈夎繖涓や釜 InputNumber锛堝�熸柟銆佽捶鏂癸級锛屼笉鍙楀奖鍝嶅叾瀹冨垪銆�
+ */
+:deep(.ant-input-number .ant-input-number-input) {
+ text-align: center;
+}
+</style>
diff --git a/src/views/erp/finance/voucher/modules/form.vue b/src/views/erp/finance/voucher/modules/form.vue
new file mode 100644
index 0000000..9b32c9c
--- /dev/null
+++ b/src/views/erp/finance/voucher/modules/form.vue
@@ -0,0 +1,157 @@
+<script lang="ts" setup>
+import type { FinVoucherApi } from '#/api/erp/finance/voucher';
+import type { FormType } from '../data';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createVoucher,
+ getVoucher,
+ updateVoucher,
+} from '#/api/erp/finance/voucher';
+
+import { useFormSchema } from '../data';
+import EntryTable from './entry-table.vue';
+
+const emit = defineEmits(['success']);
+const formData = ref<FinVoucherApi.Voucher>();
+
+const formType = ref<FormType>('create'); // 琛ㄥ崟绫诲瀷锛�'create' | 'edit' | 'detail'
+const entryTableRef = ref<InstanceType<typeof EntryTable>>();
+// 鍒嗗綍鐨勫敮涓�鏁版嵁婧愶細蹇呴』鐢ㄧǔ瀹氱殑 ref锛屼笉鑳藉湪妯℃澘閲岀幇閫� []銆�
+// 鍚﹀垯鐖剁粍浠舵瘡娆℃覆鏌撻兘浼氱粰 EntryTable 浼犱竴涓柊鐨勭┖鏁扮粍锛岃Е鍙戝叾 watch 閲嶅缓琛ㄦ牸锛�
+// 鎶婄敤鎴峰垰銆屾坊鍔犲垎褰曘�嶅姞杩涘幓鐨勮鍐叉帀锛堟柊澧炴ā寮忎笅 formData 涓� undefined 鏃跺繀鐜帮級銆�
+const entries = ref<FinVoucherApi.VoucherEntry[]>([]);
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['璁拌处鍑瘉']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['璁拌处鍑瘉']);
+ }
+ return '璁拌处鍑瘉璇︽儏';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 100,
+ },
+ // 涓夊垪鏍呮牸锛氬嚟璇佸瓧鍙�/鍑瘉鏃ユ湡/闄勪欢寮犳暟 鍗犳弧涓�琛岋紝鍏朵綑瀛楁鏁磋閾哄紑锛�
+ // 閬垮厤涓ゅ垪甯冨眬涓嬫憳瑕併�侀檮浠跺紶鏁板彸鍗婅竟鐣欑┖锛堜笌鏀舵鍗曘�佷粯娆惧崟寮圭獥淇濇寔涓�鑷达級
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+/**
+ * 鍚屾鍒嗗綍鍒拌〃鍗曞�硷紝闅忚〃鍗曚竴璧锋彁浜ゃ��
+ * 娉ㄦ剰锛氳繖閲屽彧鍐欒〃鍗曞�硷紝**涓嶈鎶� value 鍥炲啓鍒� entries**鈥斺��
+ * entries 鍙湪鎵撳紑寮圭獥鏃朵粠鏈嶅姟绔挱绉嶄竴娆★紝涔嬪悗鐨勫疄鏃舵暟鎹互 EntryTable 鍐呴儴琛ㄦ牸涓哄噯銆�
+ * 涓�鏃﹀洖鍐欙紝props.entries 鍙樺寲浼氳Е鍙� EntryTable 鐨� watch 閲嶅缓鏁村紶琛ㄦ牸锛屽鑷存瘡鏁蹭竴涓�
+ * 鏁板瓧/閫変竴娆$鐩氨閲嶉�犱竴閬嶈锛堣鏍囪瘑鍙樺寲銆佽緭鍏ユ澶辩劍锛夈��
+ */
+function handleUpdateEntries(value: FinVoucherApi.VoucherEntry[]) {
+ formApi.setValues({ entries: value });
+}
+
+/** 鍒涘缓鎴栨洿鏂拌璐﹀嚟璇� */
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ // 鍒嗗綍鏍¢獙涓庡悗绔� FinVoucherServiceImpl 鐨勫彛寰勪竴鑷达紝鍓嶇鍏堟嫤涓�閬嶏紝閬垮厤鏃犳剰涔夌殑璇锋眰
+ const entryTableInstance = Array.isArray(entryTableRef.value)
+ ? entryTableRef.value[0]
+ : entryTableRef.value;
+ try {
+ entryTableInstance?.validate();
+ } catch (error: any) {
+ message.error(error.message || '鍑瘉鍒嗗綍鏍¢獙澶辫触');
+ return;
+ }
+
+ modalApi.lock();
+ const data = (await formApi.getValues()) as FinVoucherApi.Voucher;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formType.value === 'create'
+ ? createVoucher(data)
+ : updateVoucher(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formData.value = undefined;
+ entries.value = [];
+ await formApi.resetForm();
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data?.formType ?? 'create';
+ formApi.setDisabled(formType.value === 'detail');
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getVoucher(data.id);
+ await formApi.setValues(formData.value, false);
+ entries.value = formData.value?.entries ?? [];
+ // 鐢� attachmentList 浠f浛 blobIds锛岃涓婁紶缁勪欢鑳藉睍绀烘枃浠跺悕鍜岄瑙堥摼鎺�
+ if (formData.value?.attachmentList?.length) {
+ const blobIds = formData.value.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ }));
+ await formApi.setFieldValue('blobIds', blobIds);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal
+ :title="getTitle"
+ class="w-5/6"
+ :close-on-click-modal="false"
+ :show-confirm-button="formType !== 'detail'"
+ >
+ <Form class="mx-3">
+ <template #entries>
+ <EntryTable
+ ref="entryTableRef"
+ :entries="entries"
+ :disabled="formType === 'detail'"
+ class="w-full"
+ @update:entries="handleUpdateEntries"
+ />
+ </template>
+ </Form>
+ </Modal>
+</template>
--
Gitblit v1.9.3