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