From d7538a8fb830c3de582b25e70566211987c4151f Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 28 九月 2026 18:13:48 +0800
Subject: [PATCH] feat(modules): 新增财务、会员管理和课题管理模块

---
 src/views/research/application/modules/form.vue                    |  164 +
 src/views/research/application/modules/review-panel.vue            |  221 +
 src/api/research/application/index.ts                              |  146 +
 src/views/research/application/modules/review-form.vue             |  174 +
 src/views/finance/reimbursement/data.ts                            |  307 ++
 src/views/ums/application/data.ts                                  |  348 ++
 src/views/research/application/modules/fund-form.vue               |  233 +
 src/views/research/application/modules/fund-panel.vue              |  236 ++
 src/views/research/application/modules/process-select-modal.vue    |   77 
 src/views/ums/application/index.vue                                |  261 ++
 src/views/research/application/components/index.ts                 |    1 
 src/api/research/guideline/index.ts                                |   80 
 src/views/research/application/detail/approve/index.vue            |  279 ++
 src/views/ums/application/modules/process-select-modal.vue         |   74 
 src/views/finance/reimbursement/modules/form.vue                   |  179 +
 src/views/ums/member/index.vue                                     |  183 +
 src/views/research/guideline/modules/form.vue                      |  112 
 src/views/research/application/components/member-select.vue        |  132 +
 src/views/ums/application/detail/approve/index.vue                 |  196 +
 src/views/finance/reimbursement/modules/item-form.vue              |  223 +
 src/views/ums/member/directory.vue                                 |  139 +
 src/views/ums/member/modules/form.vue                              |  123 +
 src/views/research/application/data.ts                             |  548 ++++
 src/api/research/fund/index.ts                                     |   48 
 src/views/ums/application/modules/form.vue                         |  161 +
 src/api/finance/reimbursement/index.ts                             |  113 
 src/views/finance/reimbursement/index.vue                          |  285 ++
 .playwright-mcp/_e2e_attachment.txt                                |    6 
 src/views/research/application/components/member-select-dialog.vue |  172 +
 src/views/research/guideline/index.vue                             |  226 +
 .playwright-mcp/page-2026-09-28T10-11-52-064Z.yml                  |   29 
 .playwright-mcp/page-2026-09-28T10-11-57-412Z.yml                  |  143 +
 src/views/ums/member/data.ts                                       |  432 +++
 src/views/finance/reimbursement/detail/approve/index.vue           |  199 +
 src/api/ums/application/index.ts                                   |   88 
 src/views/research/guideline/data.ts                               |  252 ++
 src/api/ums/member/index.ts                                        |   74 
 src/packages/constants/src/dict-enum.ts                            |   28 
 src/views/research/application/index.vue                           |  311 ++
 src/views/finance/reimbursement/modules/process-select-modal.vue   |   76 
 40 files changed, 7,079 insertions(+), 0 deletions(-)

diff --git a/.playwright-mcp/_e2e_attachment.txt b/.playwright-mcp/_e2e_attachment.txt
new file mode 100644
index 0000000..a8359df
--- /dev/null
+++ b/.playwright-mcp/_e2e_attachment.txt
@@ -0,0 +1,6 @@
+璇鹃鐢虫姤涔﹂檮浠讹紙E2E 娴嬭瘯锛�
+
+璇鹃鍚嶇О锛氶潰鍚戞櫤鑳藉伐鍘傜殑鏁板瓧瀛敓鍏抽敭鎶�鏈爺绌�
+鐢虫姤浜猴細寮犱紵
+
+鏈枃浠剁敤浜庣鍒扮楠岃瘉闄勪欢涓婁紶涓庣粦瀹氭祦绋嬨��
diff --git a/.playwright-mcp/page-2026-09-28T10-11-52-064Z.yml b/.playwright-mcp/page-2026-09-28T10-11-52-064Z.yml
new file mode 100644
index 0000000..3ae6b7c
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-28T10-11-52-064Z.yml
@@ -0,0 +1,29 @@
+- generic [active] [ref=f34e1]:
+  - banner [ref=f34e2]:
+    - generic [ref=f34e3]: MOM
+    - heading "椤圭洰涓氬姟鍏ㄦ櫙" [level=1] [ref=f34e5]
+    - textbox "鎼滅储涓氬姟锛氶攢鍞鍗� / 宸ュ崟 / IQC / 搴撳瓨 / 鍥炴鈥�" [ref=f34e7]
+  - navigation [ref=f34e8]:
+    - generic [ref=f34e9]: 鎬昏
+    - generic [ref=f34e10] [cursor=pointer]: 椤圭洰姒傝
+    - generic [ref=f34e12] [cursor=pointer]: 涓氬姟鍏ㄦ櫙鍥�
+    - generic [ref=f34e14]: 鏍稿績涓氬姟娴佺▼
+    - generic [ref=f34e15] [cursor=pointer]: 閿�鍞祦绋�
+    - generic [ref=f34e17] [cursor=pointer]: 閲囪喘娴佺▼
+    - generic [ref=f34e19] [cursor=pointer]: 鐢熶骇娴佺▼
+    - generic [ref=f34e21] [cursor=pointer]: 璐ㄩ噺娴佺▼
+    - generic [ref=f34e23] [cursor=pointer]: 浠撳偍娴佺▼
+    - generic [ref=f34e25] [cursor=pointer]: 鍞悗娴佺▼
+    - generic [ref=f34e27] [cursor=pointer]: 璐㈠姟鎶ラ攢娴佺▼
+    - generic [ref=f34e29] [cursor=pointer]: 浼氬憳鍏ヤ細娴佺▼
+    - generic [ref=f34e31] [cursor=pointer]: 璇鹃绠$悊娴佺▼
+    - generic [ref=f34e33]: 缁撴瀯涓庢暟鎹�
+    - generic [ref=f34e34] [cursor=pointer]: 绯荤粺鍏崇郴
+    - generic [ref=f34e36] [cursor=pointer]: 鏁版嵁娴�
+    - generic [ref=f34e38] [cursor=pointer]: 涓氬姟瀵硅薄
+    - generic [ref=f34e40] [cursor=pointer]: 瑙掕壊鏉冮檺
+    - generic [ref=f34e42]: 鎵╁睍
+    - generic [ref=f34e43] [cursor=pointer]: AI 涓氬姟
+    - generic [ref=f34e45] [cursor=pointer]: 寰呯‘璁や簨椤�
+  - main [ref=f34e47]
+  - contentinfo [ref=f34e48]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-09-28T10-11-57-412Z.yml b/.playwright-mcp/page-2026-09-28T10-11-57-412Z.yml
new file mode 100644
index 0000000..279bf8d
--- /dev/null
+++ b/.playwright-mcp/page-2026-09-28T10-11-57-412Z.yml
@@ -0,0 +1,143 @@
+- generic [active] [ref=f34e1]:
+  - banner [ref=f34e2]:
+    - generic [ref=f34e3]: MOM
+    - generic [ref=f34e4]:
+      - heading "MOM 宸ュ巶鏁板瓧鍖栧钩鍙帮紙mom-pro2锛� 路 涓氬姟鍏ㄦ櫙" [level=1] [ref=f34e49]
+      - generic [ref=f34e50]: 娣峰悎绯荤粺 | MOM 鍒堕�犺繍钀ョ鐞嗙郴缁�
+    - textbox "鎼滅储涓氬姟锛氶攢鍞鍗� / 宸ュ崟 / IQC / 搴撳瓨 / 鍥炴鈥�" [ref=f34e7]
+  - navigation [ref=f34e8]:
+    - generic [ref=f34e9]: 鎬昏
+    - generic [ref=f34e10] [cursor=pointer]: 椤圭洰姒傝
+    - generic [ref=f34e12] [cursor=pointer]: 涓氬姟鍏ㄦ櫙鍥�
+    - generic [ref=f34e14]: 鏍稿績涓氬姟娴佺▼
+    - generic [ref=f34e15] [cursor=pointer]: 閿�鍞祦绋�
+    - generic [ref=f34e17] [cursor=pointer]: 閲囪喘娴佺▼
+    - generic [ref=f34e19] [cursor=pointer]: 鐢熶骇娴佺▼
+    - generic [ref=f34e21] [cursor=pointer]: 璐ㄩ噺娴佺▼
+    - generic [ref=f34e23] [cursor=pointer]: 浠撳偍娴佺▼
+    - generic [ref=f34e25] [cursor=pointer]: 鍞悗娴佺▼
+    - generic [ref=f34e27] [cursor=pointer]: 璐㈠姟鎶ラ攢娴佺▼
+    - generic [ref=f34e29] [cursor=pointer]: 浼氬憳鍏ヤ細娴佺▼
+    - generic [ref=f34e31] [cursor=pointer]: 璇鹃绠$悊娴佺▼
+    - generic [ref=f34e33]: 缁撴瀯涓庢暟鎹�
+    - generic [ref=f34e34] [cursor=pointer]: 绯荤粺鍏崇郴
+    - generic [ref=f34e36] [cursor=pointer]: 鏁版嵁娴�
+    - generic [ref=f34e38] [cursor=pointer]: 涓氬姟瀵硅薄
+    - generic [ref=f34e40] [cursor=pointer]: 瑙掕壊鏉冮檺
+    - generic [ref=f34e42]: 鎵╁睍
+    - generic [ref=f34e43] [cursor=pointer]: AI 涓氬姟
+    - generic [ref=f34e45] [cursor=pointer]: 寰呯‘璁や簨椤�
+  - main [ref=f34e47]:
+    - generic [ref=f34e184]:
+      - heading "璇鹃绠$悊娴佺▼" [level=2] [ref=f34e185]
+      - paragraph [ref=f34e186]: 鐙珛绉戠爺妯″潡锛坹udao-module-research锛夛細瀛︿細鍙戝竷璇鹃鎸囧崡锛堥噸鐐�/涓�鑸垎绫伙級鈫� 浼氬憳鍦ㄧ嚎鎻愪氦璇鹃鐢虫姤涔� 鈫� 鎻愪氦 BPM 涓夌骇瀹℃壒锛堢涔﹀褰㈠紡瀹℃煡 鈫� 涓撳璇勫 鈫� 瀛︿細绔嬮」瀹℃壒锛夛紝涓撳璇勫垎浠ョ粨鏋勫寲鏄庣粏瀛愯〃璁板綍骞剁敱鍚庣鑷姩姹囨�诲姞鏉冨潎鍒嗐�佹帓鍚嶄笌鎺ㄨ崘缁撹 鈫� 瀹℃壒閫氳繃鐢熸垚绔嬮」缂栧彿骞跺彂甯冪郴缁熷叕鍛� 鈫� 鎸夋壒娆$櫥璁扮粡璐规嫧浠樿褰曪紙鍚嚟璇侀檮浠讹級銆傞櫌绾т簩鏈熷啀琛ョ粨椤归獙鏀朵笌鎴愭灉褰掓。銆�
+      - generic [ref=f34e187]:
+        - generic [ref=f34e188]:
+          - generic [ref=f34e189]: 璇鹃绠$悊娴佺▼ 路 mermaid/14-research-flow.mmd
+          - generic [ref=f34e190]:
+            - button "锛�" [ref=f34e191] [cursor=pointer]
+            - button "锛�" [ref=f34e192] [cursor=pointer]
+            - button "閲嶇疆" [ref=f34e193] [cursor=pointer]
+            - button "鍏ㄥ睆" [ref=f34e194] [cursor=pointer]
+        - document [ref=f34e198]:
+          - generic [ref=f34e200]:
+            - generic [ref=f34e201]:
+              - paragraph [ref=f34e208]: 绔嬮」瀹℃壒 BPM research_topic_approve
+              - paragraph [ref=f34e215]: 鍦ㄧ嚎鐢虫姤
+              - paragraph [ref=f34e222]: 璇鹃鎸囧崡鍙戝竷
+            - generic [ref=f34e253]:
+              - paragraph [ref=f34e259]: 宸蹭骇鐢熺敵鎶ュ崟鍒欑姝㈠垹闄�
+              - paragraph [ref=f34e265]: publish 鍙戝竷
+              - paragraph [ref=f34e271]: close 鎴
+              - paragraph [ref=f34e277]: 鎸囧崡宸插彂甯冩墠鍙綔涓虹敵鎶ュ墠鎻�
+              - paragraph [ref=f34e283]: delete 鍒犻櫎路浠呰崏绋� 0 鏀捐路杩炲甫娓呯悊闄勪欢
+              - paragraph [ref=f34e289]: submit 閫夊鎵规祦绋� processDefinitionKey
+              - paragraph [ref=f34e295]: "submit 鍓嶉�愭潯鏍¢獙: 鈶� 鎸囧崡蹇呴』宸插彂甯� 鈶� 褰撳墠鏃堕棿蹇呴』鍦ㄧ敵鎶ヨ捣姝㈡湡鍐�"
+              - paragraph [ref=f34e301]: 浠呭鎵逛腑鍙鍒犳敼涓撳璇勫垎
+              - paragraph [ref=f34e307]: 鍚庣閫愯閲嶇畻 compositeScore = 閫夐0.3 + 鏂规0.3 + 鍥㈤槦0.2 + 鎴愭灉0.2
+              - paragraph [ref=f34e313]: "鍥炲啓涓诲崟: reviewAvgScore=鍏ㄩ儴涓撳鍧囧垎 路 reviewPass=鍧囧垎杈炬爣 涓� 澶氭暟缁撹涓烘帹鑽愮珛椤�"
+              - paragraph [ref=f34e319]: 绉樹功澶勬眹鎬荤敵鎶ヤ功涓庤瘎瀹$粨鏋�
+              - paragraph [ref=f34e325]: 閫氳繃
+              - paragraph [ref=f34e331]: 閫氳繃
+              - paragraph [ref=f34e337]: BPM 瀹℃壒閫氳繃
+              - paragraph [ref=f34e343]: BPM 瀹℃壒椹冲洖
+              - paragraph [ref=f34e349]: BPM 瀹℃壒鍙栨秷
+              - paragraph [ref=f34e355]: 椹冲洖
+              - paragraph [ref=f34e361]: 淇敼鍚庨噸鏂� submit
+              - paragraph [ref=f34e367]: publishNotice 鍙戝竷绔嬮」閫氱煡
+              - paragraph [ref=f34e373]: 鐧昏鎷ㄦ璁板綍 research_fund_record
+              - paragraph [ref=f34e379]: "閲戦鏍¢獙: 宸茬櫥璁伴噾棰� + 鏈閲戦 涓嶅緱瓒呭嚭鐢虫姤鍗曠敵璇风粡璐孤疯秴鍑烘椂鎶ュ嚭鏈 宸茬櫥璁� 鍚堣 棰勭畻鍥涢」鏁板��"
+              - paragraph [ref=f34e385]: 浠呭緟鎷ㄤ粯 0 鍙敼鍙垹
+              - paragraph [ref=f34e391]: 鐘舵�佹敼涓哄凡鎷ㄤ粯鏃跺繀椤诲凡鏈夊嚟璇侀檮浠�
+            - generic [ref=f34e392]:
+              - paragraph [ref=f34e399]: 鏂板缓鎸囧崡路閫夌被鍒� 1閲嶇偣璇鹃 / 2涓�鑸棰�
+              - paragraph [ref=f34e406]: 濉寚鍗楀悕绉� 鐢虫姤璧锋鏃堕棿 鎸囧崡姝f枃 闄勪欢
+              - paragraph [ref=f34e413]: 鎸囧崡缂栧彿 GL + yyyyMMdd + 6浣嶆祦姘�(Redis路姣忔棩閲嶇疆)
+              - paragraph [ref=f34e420]: 鑽夌 0路浠呰崏绋垮彲鏀瑰彲鍒�
+              - paragraph [ref=f34e427]: 鎻愮ず璇ユ寚鍗椾笅宸叉湁 N 寮犵敵鎶ュ崟路闇�鍏堝鐞嗙敵鎶ュ崟
+              - paragraph [ref=f34e434]: 宸插彂甯� 20路鍥炲啓 publishTime
+              - paragraph [ref=f34e441]: 宸叉埅姝� 30
+              - paragraph [ref=f34e448]: 閫夋寚鍗椔疯姹傚凡鍙戝竷 涓斿湪鐢虫姤鏈熷唴
+              - paragraph [ref=f34e455]: 閫変細鍛� ums_member 鑷姩甯﹀嚭 濮撳悕 宸ヤ綔鍗曚綅 鑱岀О 鐢佃瘽 閭
+              - paragraph [ref=f34e462]: 濉棰樺悕绉� 璇鹃缁勬垚鍛� 鐢宠缁忚垂 鐢虫姤涔︽鏂� 鐢虫姤闄勪欢
+              - paragraph [ref=f34e469]: 鐢虫姤缂栧彿 SB + yyyyMMdd + 6浣嶆祦姘绰风被鍒啑浣欒嚜鎸囧崡涓嶄俊浠诲墠绔紶鍊�
+              - paragraph [ref=f34e476]: 鑽夌 0
+              - paragraph [ref=f34e483]: 瀹℃壒涓� 15路鍥炲啓 processInstanceId
+              - paragraph [ref=f34e490]: 浠讳竴涓嶆弧瓒冲嵆鎷掔粷鎻愪氦路鎻愮ず甯︽寚鍗楀悕涓庡疄闄呯姸鎬佹垨璧锋鏃堕棿
+              - paragraph [ref=f34e497]: 涓撳璇勫鏄庣粏 research_topic_review路涓�浣嶄笓瀹朵竴琛�
+              - paragraph [ref=f34e504]: 鎸夊姞鏉冨垎闄嶅簭閲嶆帓鍚嶆 ranking
+              - paragraph [ref=f34e511]: 鐢虫姤鍗曡瘎瀹℃眹鎬宦峰垹闄ゆ渶鍚庝竴鏉℃槑缁嗘椂娓呯┖姹囨��
+              - paragraph [ref=f34e518]: 涓夎妭鐐归『搴徛峰鎵规柟娉曚笌瀹℃壒浜哄湪娴佺▼瀹氫箟鍐呴厤缃�
+              - paragraph [ref=f34e525]: Activity_secretariat_review 绉樹功澶勫舰寮忓鏌�
+              - paragraph [ref=f34e532]: Activity_expert_review 涓撳璇勫
+              - paragraph [ref=f34e539]: Activity_council_approve 瀛︿細绔嬮」瀹℃壒
+              - paragraph [ref=f34e546]: 宸茬珛椤� 20路鐩戝惉鍣ㄧ敓鎴� 绔嬮」缂栧彿 LX + yyyyMMdd + 6浣嶆祦姘� 涓� approveTime路宸叉湁缂栧彿鍒欏箓绛夎烦杩�
+              - paragraph [ref=f34e553]: 宸查┏鍥� 35
+              - paragraph [ref=f34e560]: system_notice 鍏憡路鏍囬銆庡叧浜庡叕甯冪珛椤硅棰樼殑閫氱煡銆徛锋鏂囧惈璇鹃鍚� 绔嬮」缂栧彿 绫诲埆 鐢虫姤浜� 鍗曚綅 璧勫姪閲戦路鍥炲啓 noticeId路閲嶅鍙戝竷琚嫤鎴�
+              - paragraph [ref=f34e567]: 濉壒娆″悕绉� 鎷ㄤ粯閲戦 鎷ㄤ粯鏃ユ湡 鏀舵鍗曚綅 鎷ㄤ粯鐘舵�� 鍑瘉闄勪欢
+              - paragraph [ref=f34e574]: 寰呮嫧浠� 0
+              - paragraph [ref=f34e581]: 宸叉嫧浠� 20
+              - paragraph [ref=f34e588]: 闄勪欢 recordType = research_guideline
+              - paragraph [ref=f34e595]: 闄勪欢 recordType = research_application
+              - paragraph [ref=f34e602]: 闄勪欢 recordType = research_fund_record
+              - paragraph [ref=f34e609]: "瀹℃壒涓績鐪嬪崟: 娴佺▼瀹氫箟琛ㄥ崟绫诲瀷=鑷畾涔夎〃鍗暵峰垱寤鸿矾寰� /research/application/index 路 鏌ョ湅璺緞 /research/application/detail/approve/index.vue路鎸� businessKey 鍥炴樉鐢虫姤鍗�"
+              - paragraph [ref=f34e616]: "瀹℃壒浜哄彇鍊�: 涓夎妭鐐瑰湪娴佺▼瀹氫箟鍐呴厤濂藉鎵规柟娉�(渚濇瀹℃壒)涓庡鎵逛汉绛栫暐路閮ㄧ讲鍚庣敱绠$悊鍛樺湪 BPM 娴佺▼璁捐鍣ㄦ寜瀛︿細瀹為檯缁勭粐璋冩暣路鎻愪氦鏃跺墠绔彧璁╅�夋祦绋嬩笉閫夊鎵逛汉"
+              - paragraph [ref=f34e623]: "涓�鏈熻竟鐣�: 缁忚垂鎷ㄤ粯鍙湪鏈ā鍧楄惤鎷ㄦ璁板綍路涓庤储鍔℃ā鍧楁棤鏁版嵁鍚屾(涓嶅仛璺ㄦā鍧楀鎺�)"
+              - paragraph [ref=f34e630]: "浜屾湡鑼冨洿(鏈浘鏈鐩�): 缁撻」楠屾敹 缁撻」璇佷功 鎴愭灉褰掓。鐭ヨ瘑搴� 閲囩撼鎯呭喌涓庣ぞ浼氭晥鐩婃爣娉�"
+              - paragraph [ref=f34e637]: 鐙珛妯″潡 yudao-module-research路瀵� ums_member 涓鸿蒋鍏宠仈(鏃犲閿�)路浠呭鐢� BPM 瀹℃壒銆乻ystem 鍏憡涓庨檮浠朵腑蹇�
+      - heading "姝ラ涓庤鑹�" [level=3] [ref=f34e646]
+      - table [ref=f34e647]:
+        - rowgroup [ref=f34e648]:
+          - row [ref=f34e649]:
+            - columnheader "闃舵" [ref=f34e650]
+            - columnheader "鍔ㄤ綔锛堝惈鐪熷疄鐘舵�佹祦杞級" [ref=f34e651]
+            - columnheader "瑙掕壊" [ref=f34e652]
+          - row [ref=f34e653]:
+            - cell "鍙戝竷鎸囧崡" [ref=f34e654]
+            - 'cell "鏂板缓鎸囧崡閫夌被鍒� 1閲嶇偣璇鹃 / 2涓�鑸棰橈紝濉敵鎶ヨ捣姝㈡椂闂翠笌鎸囧崡姝f枃锛涚紪鍙� GL+yyyyMMdd+6浣嶆祦姘达紙Redis 姣忔棩閲嶇疆锛夛紱status: 0鑽夌/20宸插彂甯�/30宸叉埅姝€�備粎鑽夌鍙敼鍙垹锛屽彂甯冨悗鍗冲喕缁擄紱宸蹭骇鐢熺敵鎶ュ崟鐨勬寚鍗楃姝㈠垹闄わ紙鎻愮ず璇ユ寚鍗椾笅宸叉湁 N 寮犵敵鎶ュ崟锛�" [ref=f34e655]'
+            - cell "瀛︿細绉樹功澶勩�佽棰樼鐞嗗憳" [ref=f34e656]
+          - row [ref=f34e657]:
+            - cell "鍦ㄧ嚎鐢虫姤" [ref=f34e658]
+            - 'cell "浠庡凡鍙戝竷涓斿湪鐢虫姤鏈熷唴鐨勬寚鍗楁柊寤虹敵鎶ヤ功锛涢�変細鍛橈紙ums_member锛夎嚜鍔ㄥ甫鍑哄鍚�/宸ヤ綔鍗曚綅/鑱岀О/鑱旂郴鐢佃瘽/閭锛屽彲鏀癸紱濉棰樺悕绉般�佽棰樼粍鎴愬憳銆佺敵璇风粡璐广�佺敵鎶ヤ功姝f枃锛涚紪鍙� SB+yyyyMMdd+6浣嶆祦姘达紱绫诲埆鍐椾綑鑷寚鍗楋紝涓嶄俊浠诲墠绔紶鍊硷紱status: 0鑽夌" [ref=f34e659]'
+            - cell "璇鹃鐢虫姤浜猴紙浼氬憳锛夈�佽棰樼鐞嗗憳" [ref=f34e660]
+          - row [ref=f34e661]:
+            - cell "鎻愪氦瀹℃壒" [ref=f34e662]
+            - cell "submit 浼� id 涓� processDefinitionKey锛坬uery 鍙傛暟锛夆啋 status=15 瀹℃壒涓苟鍥炲啓 processInstanceId銆傛彁浜ゅ墠閫愭潯鏍¢獙锛氣憼 鎸囧崡蹇呴』宸插彂甯� 鈶� 褰撳墠鏃堕棿蹇呴』鍦ㄧ敵鎶ヨ捣姝㈡湡鍐呪�斺�斾换涓�涓嶆弧瓒冲嵆鎷掔粷锛屾彁绀哄甫鎸囧崡鍚嶄笌瀹為檯鐘舵�佹垨璧锋鏃堕棿锛涗粎鑽夌涓庡凡椹冲洖鍙敼鍙噸鎻愶紝浠呰崏绋垮彲鍒狅紙杩炲甫娓呴檮浠讹級" [ref=f34e663]
+            - cell "璇鹃鐢虫姤浜�" [ref=f34e664]
+          - row [ref=f34e665]:
+            - cell "涓撳璇勫" [ref=f34e666]
+            - cell "鐢虫姤鍗曞浜庡鎵逛腑(15)鏃跺綍鍏ヤ笓瀹惰瘎鍒嗘槑缁嗭紙涓�浣嶄笓瀹朵竴琛岋細濮撳悕/鍗曚綅/鑱岀О + 閫夐浠峰��0.3/鐮旂┒鏂规0.3/鍥㈤槦鍩虹0.2/棰勬湡鎴愭灉0.2 鍥涢」鍒嗗�� + 缁撹 + 鎰忚锛夛紱鍚庣姣忔澧炲垹鏀瑰悗閲嶇畻 compositeScore 鍔犳潈鍚堣銆佹寜鍔犳潈鍒嗛檷搴忛噸鎺� ranking锛屽苟鍥炲啓涓诲崟 reviewAvgScore锛堝叏閮ㄤ笓瀹跺潎鍒嗭級涓� reviewPass锛堝潎鍒嗚揪鏍囦笖澶氭暟缁撹涓烘帹鑽愮珛椤癸級锛涙槑缁嗗叏閮ㄥ垹闄ゆ椂姹囨�诲�兼竻绌轰负鏈眹鎬伙紱鍥涢」鍒嗗�煎潎鍦� 0~100 涔嬪鏃舵彁绀烘寚鍑哄叿浣撶淮搴︿笌瀹為檯鍊�" [ref=f34e667]
+            - cell "璇勫涓撳銆佸浼氱涔﹀銆佽棰樼鐞嗗憳" [ref=f34e668]
+          - row [ref=f34e669]:
+            - cell "绔嬮」瀹℃壒" [ref=f34e670]
+            - cell "BPM 娴佺▼ research_topic_approve 椤哄簭璧颁笁鑺傜偣锛氱涔﹀褰㈠紡瀹℃煡 鈫� 涓撳璇勫 鈫� 瀛︿細绔嬮」瀹℃壒銆傚鎵规柟娉曚笌瀹℃壒浜哄湪娴佺▼瀹氫箟鍐呴厤缃紝閮ㄧ讲鍚庣敱绠$悊鍛樺湪 BPM 娴佺▼璁捐鍣ㄦ寜瀛︿細瀹為檯缁勭粐璋冩暣锛涘鎵逛腑蹇冨唴鐨勮嚜瀹氫箟琛ㄥ崟鍙鍥炴樉鐢虫姤鍗� + 璇勫鏄庣粏 + 鎷ㄤ粯璁板綍銆傞�氳繃鈫�20 宸茬珛椤广�侀┏鍥炩啋35 宸查┏鍥炪�佸彇娑堚啋閫�鍥� 0 鑽夌" [ref=f34e671]
+            - cell "瀛︿細绉樹功澶勶紙褰㈠紡瀹℃煡锛夈�佽瘎瀹′笓瀹讹紙璇勫鑺傜偣锛夈�佸浼氱悊浜嬩細锛堢珛椤瑰鎵癸級" [ref=f34e672]
+          - row [ref=f34e673]:
+            - cell "绔嬮」涓庨�氱煡" [ref=f34e674]
+            - cell "鐩戝惉鍣� ResearchApplicationStatusListener 鍏堝洖鍐欏鎵圭姸鎬侊紝瀹℃壒閫氳繃鍚庡啀璋� approveApplication锛氱敓鎴愮珛椤圭紪鍙� LX+yyyyMMdd+6浣嶆祦姘� 涓� approveTime锛堝凡鏈夌紪鍙峰垯骞傜瓑璺宠繃锛夈�傜偣銆屽彂甯冪珛椤归�氱煡銆嶇敓鎴� system_notice 鍏憡锛堟爣棰樸�庡叧浜庡叕甯冪珛椤硅棰樼殑閫氱煡銆忥紝姝f枃鍚棰樺悕/绔嬮」缂栧彿/绫诲埆/鐢虫姤浜�/鍗曚綅/璧勫姪閲戦锛夛紝鍐欏洖 noticeId 闃查噸澶嶁�斺�旈噸澶嶇偣鍑绘寜宸插彂甯冩嫤鎴苟鍛婄煡涓婃鍙戝竷鏃堕棿" [ref=f34e675]
+            - cell "璇鹃绠$悊鍛樸�佸浼氱涔﹀" [ref=f34e676]
+          - row [ref=f34e677]:
+            - cell "缁忚垂鎷ㄤ粯" [ref=f34e678]
+            - 'cell "浠呫�屽凡绔嬮」(20)銆嶇殑鐢虫姤鍗曞彲鐧昏鎷ㄦ璁板綍锛堟壒娆″悕绉�/鎷ㄤ粯閲戦/鎷ㄤ粯鏃ユ湡/鏀舵鍗曚綅/鐘舵��/澶囨敞/鍑瘉闄勪欢锛夛紱閲戦鏍¢獙锛氬凡鐧昏閲戦 + 鏈閲戦 涓嶅緱瓒呭嚭璇ュ崟鐢宠缁忚垂锛岃秴鍑烘椂鎻愮ず鎶ュ嚭鏈銆佸凡鐧昏銆佸悎璁°�侀绠楀洓椤规暟鍊硷紱status: 0寰呮嫧浠�/20宸叉嫧浠橈紝鍑瘉鏄庣粏璧� system 闄勪欢涓績 recordType=research_fund_record锛岀姸鎬佹敼涓哄凡鎷ㄤ粯鏃跺嚟璇佸繀濉紱浠呭緟鎷ㄤ粯鐘舵�佸彲鏀瑰彲鍒狅紝缂栬緫鏃朵細鎶婃湰琛岄噾棰濆姞鍥炰綑棰濇樉绀�" [ref=f34e679]'
+            - cell "瀛︿細璐㈠姟銆佽棰樼鐞嗗憳" [ref=f34e680]
+  - contentinfo [ref=f34e48]: 鍩轰簬 dev_business 鍒嗘敮 87bf4a99 婧愮爜锛�6 缁勫苟琛屾簮鐮佽蛋璇伙紙MES 鐢熶骇璐ㄩ噺 / MES 浠撳偍 / ERP / CRM+SRM / 鍞悗+HRM / AI+BI+MDM+BPM+搴曞骇锛夛紱2026-09-28 澧為噺琛ュ厖 yudao-module-finance 鐙珛璐㈠姟鎶ラ攢妯″潡锛堟姤閿�鍗� + 鏄庣粏 + 鍙戠エ闄勪欢 + BPM 瀹℃壒 + 褰掓。锛変笌 yudao-module-ums 鐙珛浼氬憳绠$悊妯″潡锛堝叆浼氱敵璇� + BPM 涓ょ骇瀹℃壒 + 瀹℃壒閫氳繃鑷姩寤轰細鍛樹笌鐧诲綍璐﹀彿 + 浼氬憳鍒楄〃/閫氳褰曪級锛�2026-09-28 澧為噺锛欱PM 娴佺▼瀹氫箟 ums_membership_approve锛堝叆浼氬鎵癸級宸插湪 mom-jssm 閮ㄧ讲骞跺苟鍏� config_export_all 闃舵涓�锛堝垎绫� ums_membership_approve锛岀涔﹀鍒濆 鈫� 鐞嗕簨浼氬璁紝涓よ妭鐐瑰鎵逛汉鍧囦负銆岃鑹层�嶇瓥鐣ャ�佽鑹�=瓒呯骇绠$悊鍛�(1) 鍏滃簳锛岃〃鍗曠被鍨�=鑷畾涔夎〃鍗曪紝鏌ョ湅璺緞 /ums/application/detail/approve/index.vue锛夛紱鍚屾椂淇 mom-jssm 鐢� CREATE TABLE ... LIKE 鍏嬮殕瀵艰嚧鐨� 47 涓寚鍚� ruoyi-vue-pro 鐨� act_*/flw_* 璺ㄥ簱澶栭敭锛堥噸寤轰负鎸囧悜 mom-jssm 鑷韩锛屽鍎胯棰勬涓� 0锛夛紱2026-09-28 澧為噺锛氭柊澧� yudao-module-research 鐙珛绉戠爺璇鹃妯″潡涓�鏈燂紙璇鹃鎸囧崡鍙戝竷 + 鍦ㄧ嚎鐢虫姤 + 涓撳璇勫鏄庣粏 + BPM 涓夌骇绔嬮」瀹℃壒 + 绔嬮」閫氱煡鍏憡 + 缁忚垂鎷ㄤ粯璁板綍锛�4 寮� research_* 琛� / 5 涓瓧鍏� / bpm_category id=8002 / 娴佺▼ research_topic_approve / 鑿滃崟 1075510~1075553锛夛紝骞跺苟鍏� config_export_all_20260928 闃舵涓�锛涚敵鎶ヤ汉鍙� ums_member锛堣蒋鍏宠仈锛夈�佺珛椤归�氱煡澶嶇敤 system_notice 鍏憡銆佹嫧娆惧嚟璇佽蛋 system 闄勪欢涓績 recordType=research_fund_record 路 鐢熸垚浜� 2026-09-28 路 缁存姢璇疯蛋鎶�鑳� project-business-analysis
\ No newline at end of file
diff --git a/src/api/finance/reimbursement/index.ts b/src/api/finance/reimbursement/index.ts
new file mode 100644
index 0000000..d26a375
--- /dev/null
+++ b/src/api/finance/reimbursement/index.ts
@@ -0,0 +1,113 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace FinanceReimbursementApi {
+  /** 鎶ラ攢鏄庣粏琛� */
+  export interface ReimbursementItem {
+    id?: number;
+    reimbursementId?: number;
+    expenseType: number; // 璐圭敤绫诲瀷锛屽瓧鍏� finance_expense_type
+    invoiceNo?: string; // 鍙戠エ鍙风爜
+    invoiceAmount: number; // 鍙戠エ閲戦
+    invoiceDate?: string; // 鍙戠エ鏃ユ湡 yyyy-MM-dd
+    drawer?: string; // 寮�绁ㄦ柟
+    description?: string; // 璐圭敤璇存槑
+  }
+
+  /** 鎶ラ攢鍗曚俊鎭� */
+  export interface Reimbursement {
+    id?: number;
+    no?: string; // 鎶ラ攢鍗曞彿锛屽悗绔敓鎴�
+    title: string; // 鎶ラ攢鏍囬
+    totalAmount: number; // 鎶ラ攢鎬婚噾棰�
+    status?: number; // 鐘舵�侊紝瀛楀吀 finance_reimbursement_status
+    applicantId?: number; // 鐢宠浜� ID
+    applicantName?: string; // 鐢宠浜哄鍚�
+    applyDate: string; // 鐢宠鏃ユ湡 yyyy-MM-dd
+    processInstanceId?: string; // BPM 娴佺▼瀹炰緥 ID
+    archiveTime?: string; // 褰掓。鏃堕棿
+    remark?: string; // 澶囨敞
+    creator?: string; // 鍒涘缓浜� ID
+    creatorName?: string; // 鍒涘缓浜哄鍚�
+    createTime?: string; // 鍒涘缓鏃堕棿
+    updateTime?: string; // 鏇存柊鏃堕棿
+    items?: ReimbursementItem[]; // 鎶ラ攢鏄庣粏
+    attachmentList?: SystemStorageApi.StorageBlob[]; // 鍙戠エ闄勪欢
+    hasAttachment?: boolean; // 鏄惁鏈夊彂绁ㄩ檮浠�
+    blobIds?: number[]; // 鎻愪氦鏃剁殑闄勪欢 ID 鍒楄〃锛堝叏閲忥級
+  }
+
+  /** 瀹℃壒娴佺▼瀹氫箟 */
+  export interface ProcessDefinition {
+    id: string;
+    key: string;
+    name: string;
+  }
+}
+
+/** 鏌ヨ鎶ラ攢鍗曞垎椤� */
+export function getReimbursementPage(params: PageParam) {
+  return requestClient.get<PageResult<FinanceReimbursementApi.Reimbursement>>(
+    '/finance/reimbursement/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鎶ラ攢鍗曡鎯� */
+export function getReimbursement(id: number) {
+  return requestClient.get<FinanceReimbursementApi.Reimbursement>(
+    `/finance/reimbursement/get?id=${id}`,
+  );
+}
+
+/** 鏂板鎶ラ攢鍗� */
+export function createReimbursement(
+  data: FinanceReimbursementApi.Reimbursement,
+) {
+  return requestClient.post('/finance/reimbursement/create', data);
+}
+
+/** 淇敼鎶ラ攢鍗曪紙浠呰崏绋裤�佸凡椹冲洖鍙敼锛� */
+export function updateReimbursement(
+  data: FinanceReimbursementApi.Reimbursement,
+) {
+  return requestClient.put('/finance/reimbursement/update', data);
+}
+
+/** 鍒犻櫎鎶ラ攢鍗曪紙浠呰崏绋垮彲鍒狅級 */
+export function deleteReimbursement(id: number) {
+  return requestClient.delete('/finance/reimbursement/delete', {
+    params: { id },
+  });
+}
+
+/** 鎻愪氦鎶ラ攢鍗曞鎵� */
+export function submitReimbursement(id: number, processDefinitionKey: string) {
+  return requestClient.post('/finance/reimbursement/submit', null, {
+    params: { id, processDefinitionKey },
+  });
+}
+
+/** 褰掓。鎶ラ攢鍗曪紙浠呭凡閫氳繃鍙綊妗o級 */
+export function archiveReimbursement(id: number) {
+  return requestClient.post('/finance/reimbursement/archive', null, {
+    params: { id },
+  });
+}
+
+/** 鑾峰彇鎶ラ攢鍗曞鎵规祦绋嬪垪琛� */
+export function getReimbursementApproveProcessList() {
+  return requestClient.get<FinanceReimbursementApi.ProcessDefinition[]>(
+    '/finance/reimbursement/approve-process-list',
+  );
+}
+
+/** 瀵煎嚭鎶ラ攢鍗� Excel */
+export function exportReimbursement(params: any) {
+  return requestClient.download('/finance/reimbursement/export-excel', {
+    params,
+  });
+}
diff --git a/src/api/research/application/index.ts b/src/api/research/application/index.ts
new file mode 100644
index 0000000..f96ab84
--- /dev/null
+++ b/src/api/research/application/index.ts
@@ -0,0 +1,146 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace ResearchApplicationApi {
+  /** 璇鹃鐢虫姤涔� */
+  export interface Application {
+    id?: number;
+    no?: string; // 鐢虫姤缂栧彿锛屽悗绔敓鎴愶紙SB 鍓嶇紑锛�
+    guidelineId?: number; // 鎵�灞炶棰樻寚鍗� ID
+    guidelineTitle?: string; // 鎵�灞炶棰樻寚鍗楀悕绉�
+    title: string; // 璇鹃鍚嶇О
+    category?: number; // 璇鹃绫诲埆锛堝悗绔嚜鎸囧崡鍐椾綑锛屽墠绔彧璇伙級
+    memberId?: number; // 鐢虫姤浼氬憳 ID
+    applicantName: string; // 鐢虫姤浜哄鍚�/鍗曚綅鍚嶇О
+    applicantUserId?: number; // 鐢虫姤鐧诲綍鐢ㄦ埛 ID
+    workUnit?: string; // 宸ヤ綔鍗曚綅
+    jobTitle?: string; // 鑱岀О
+    contactPhone?: string; // 鑱旂郴鐢佃瘽
+    contactEmail?: string; // 鑱旂郴閭
+    teamMembers?: string; // 璇鹃缁勬垚鍛橈紙姣忚涓�鏉★細濮撳悕|鍗曚綅|鑱岀О锛�
+    budgetAmount?: number; // 鐢宠缁忚垂锛堝厓锛�
+    planContent?: string; // 鐢虫姤涔︽鏂�
+    status?: number; // 鐘舵�侊紝瀛楀吀 research_application_status
+    processInstanceId?: string; // BPM 娴佺▼瀹炰緥 ID
+    reviewAvgScore?: number; // 涓撳璇勫鍔犳潈鍧囧垎锛堝悗绔眹鎬伙級
+    reviewPass?: number; // 璇勫缁撹锛�0-鏈�氳繃 1-鎺ㄨ崘绔嬮」锛堝悗绔眹鎬伙級
+    researchNo?: string; // 绔嬮」缂栧彿锛屽鎵归�氳繃鍚庣敓鎴�
+    approveTime?: string; // 绔嬮」鏃堕棿
+    noticeId?: number; // 宸插彂甯冪殑绔嬮」閫氱煡鍏憡 ID
+    remark?: string; // 澶囨敞
+    creator?: string; // 鍒涘缓浜� ID
+    creatorName?: string; // 鍒涘缓浜哄鍚�
+    createTime?: string; // 鍒涘缓鏃堕棿
+    attachmentList?: SystemStorageApi.StorageBlob[]; // 闄勪欢鍒楄〃
+    hasAttachment?: boolean; // 鏄惁鏈夐檮浠�
+    blobIds?: number[]; // 鎻愪氦鏃剁殑闄勪欢 ID 鍒楄〃锛堝叏閲忥級
+  }
+
+  /** 瀹℃壒娴佺▼瀹氫箟 */
+  export interface ProcessDefinition {
+    id: string;
+    key: string;
+    name: string;
+  }
+
+  /** 涓撳璇勫鏄庣粏 */
+  export interface TopicReview {
+    id?: number;
+    applicationId?: number; // 璇鹃鐢虫姤涔� ID
+    evaluatorName: string; // 涓撳濮撳悕
+    evaluatorUnit?: string; // 涓撳鎵�鍦ㄥ崟浣�
+    evaluatorTitle?: string; // 涓撳鑱岀О
+    topicScore?: number; // 閫夐浠峰�间笌鎰忎箟寰楀垎锛堟潈閲� 0.3锛�
+    planScore?: number; // 鐮旂┒鏂规鍙鎬у緱鍒嗭紙鏉冮噸 0.3锛�
+    teamScore?: number; // 鍥㈤槦涓庣爺绌跺熀纭�寰楀垎锛堟潈閲� 0.2锛�
+    resultScore?: number; // 棰勬湡鎴愭灉涓庢晥鐩婂緱鍒嗭紙鏉冮噸 0.2锛�
+    compositeScore?: number; // 鍔犳潈鍚堣寰楀垎锛堝悗绔绠楋級
+    ranking?: number; // 鏈敵鎶ュ崟鍐呰瘎鍒嗘帓鍚嶏紙鍚庣璁$畻锛�
+    conclusion?: number; // 璇勫缁撹锛屽瓧鍏� research_review_conclusion
+    opinion?: string; // 璇勫鎰忚
+    reviewTime?: string; // 璇勫鏃堕棿
+    creatorName?: string; // 褰曞叆浜哄鍚�
+    createTime?: string; // 鍒涘缓鏃堕棿
+  }
+}
+
+/** 鏌ヨ璇鹃鐢虫姤涔﹀垎椤� */
+export function getApplicationPage(params: PageParam) {
+  return requestClient.get<PageResult<ResearchApplicationApi.Application>>(
+    '/research/application/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ璇鹃鐢虫姤涔﹁鎯� */
+export function getApplication(id: number) {
+  return requestClient.get<ResearchApplicationApi.Application>(
+    `/research/application/get?id=${id}`,
+  );
+}
+
+/** 鏂板璇鹃鐢虫姤涔︼紙鑽夌锛� */
+export function createApplication(data: ResearchApplicationApi.Application) {
+  return requestClient.post('/research/application/create', data);
+}
+
+/** 淇敼璇鹃鐢虫姤涔︼紙浠呰崏绋裤�佸凡椹冲洖鍙敼锛� */
+export function updateApplication(data: ResearchApplicationApi.Application) {
+  return requestClient.put('/research/application/update', data);
+}
+
+/** 鍒犻櫎璇鹃鐢虫姤涔︼紙浠呰崏绋垮彲鍒狅級 */
+export function deleteApplication(id: number) {
+  return requestClient.delete('/research/application/delete', {
+    params: { id },
+  });
+}
+
+/** 鎻愪氦璇鹃鐢虫姤涔﹀鎵� */
+export function submitApplication(id: number, processDefinitionKey: string) {
+  return requestClient.post('/research/application/submit', null, {
+    params: { id, processDefinitionKey },
+  });
+}
+
+/** 鑾峰彇璇鹃绔嬮」瀹℃壒娴佺▼鍒楄〃 */
+export function getApplicationApproveProcessList() {
+  return requestClient.get<ResearchApplicationApi.ProcessDefinition[]>(
+    '/research/application/approve-process-list',
+  );
+}
+
+/** 鍙戝竷绔嬮」閫氱煡 */
+export function publishApplicationNotice(id: number) {
+  return requestClient.post('/research/application/publish-notice', null, {
+    params: { id },
+  });
+}
+
+/** 鏌ヨ鏌愮敵鎶ヤ功鐨勪笓瀹惰瘎瀹℃槑缁嗗垪琛� */
+export function getTopicReviewList(applicationId: number) {
+  return requestClient.get<ResearchApplicationApi.TopicReview[]>(
+    '/research/topic-review/list',
+    { params: { applicationId } },
+  );
+}
+
+/** 褰曞叆涓撳璇勫鏄庣粏 */
+export function createTopicReview(data: ResearchApplicationApi.TopicReview) {
+  return requestClient.post('/research/topic-review/create', data);
+}
+
+/** 淇敼涓撳璇勫鏄庣粏 */
+export function updateTopicReview(data: ResearchApplicationApi.TopicReview) {
+  return requestClient.put('/research/topic-review/update', data);
+}
+
+/** 鍒犻櫎涓撳璇勫鏄庣粏 */
+export function deleteTopicReview(id: number) {
+  return requestClient.delete('/research/topic-review/delete', {
+    params: { id },
+  });
+}
diff --git a/src/api/research/fund/index.ts b/src/api/research/fund/index.ts
new file mode 100644
index 0000000..3e7ceab
--- /dev/null
+++ b/src/api/research/fund/index.ts
@@ -0,0 +1,48 @@
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace ResearchFundRecordApi {
+  /** 缁忚垂鎷ㄤ粯璁板綍 */
+  export interface FundRecord {
+    id?: number;
+    applicationId: number; // 鍏宠仈璇鹃鐢虫姤涔� ID
+    batchName?: string; // 鎷ㄤ粯鎵规锛屽銆岀涓�鏈熴��
+    amount: number; // 鎷ㄤ粯閲戦锛堝厓锛�
+    payDate?: string; // 鎷ㄤ粯鏃ユ湡
+    payee?: string; // 鏀舵鍗曚綅/璐︽埛鍚�
+    status?: number; // 鎷ㄤ粯鐘舵�侊紝瀛楀吀 research_fund_status
+    remark?: string; // 澶囨敞
+    creator?: string; // 鍒涘缓浜� ID
+    creatorName?: string; // 鍒涘缓浜哄鍚�
+    createTime?: string; // 鍒涘缓鏃堕棿
+    attachmentList?: SystemStorageApi.StorageBlob[]; // 鍑瘉闄勪欢鍒楄〃
+    hasAttachment?: boolean; // 鏄惁鏈夊嚟璇侀檮浠�
+    blobIds?: number[]; // 鎻愪氦鏃剁殑鍑瘉闄勪欢 ID 鍒楄〃锛堝叏閲忥級
+  }
+}
+
+/** 鏌ヨ鏌愯棰樼敵鎶ヤ功鐨勭粡璐规嫧浠樿褰曞垪琛� */
+export function getFundRecordList(applicationId: number) {
+  return requestClient.get<ResearchFundRecordApi.FundRecord[]>(
+    '/research/fund-record/list',
+    { params: { applicationId } },
+  );
+}
+
+/** 鏂板缁忚垂鎷ㄤ粯璁板綍 */
+export function createFundRecord(data: ResearchFundRecordApi.FundRecord) {
+  return requestClient.post('/research/fund-record/create', data);
+}
+
+/** 淇敼缁忚垂鎷ㄤ粯璁板綍锛堜粎寰呮嫧浠樺彲鏀癸級 */
+export function updateFundRecord(data: ResearchFundRecordApi.FundRecord) {
+  return requestClient.put('/research/fund-record/update', data);
+}
+
+/** 鍒犻櫎缁忚垂鎷ㄤ粯璁板綍锛堜粎寰呮嫧浠樺彲鍒狅級 */
+export function deleteFundRecord(id: number) {
+  return requestClient.delete('/research/fund-record/delete', {
+    params: { id },
+  });
+}
diff --git a/src/api/research/guideline/index.ts b/src/api/research/guideline/index.ts
new file mode 100644
index 0000000..44a5b43
--- /dev/null
+++ b/src/api/research/guideline/index.ts
@@ -0,0 +1,80 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace ResearchGuidelineApi {
+  /** 璇鹃鎸囧崡 */
+  export interface Guideline {
+    id?: number;
+    no?: string; // 鎸囧崡缂栧彿锛屽悗绔敓鎴愶紙GL 鍓嶇紑锛�
+    title: string; // 鎸囧崡鍚嶇О
+    category: number; // 璇鹃绫诲埆锛屽瓧鍏� research_guideline_category
+    applyStartTime?: string; // 鐢虫姤寮�濮嬫椂闂�
+    applyEndTime?: string; // 鐢虫姤鎴鏃堕棿
+    content?: string; // 鎸囧崡姝f枃
+    publishStatus?: number; // 鍙戝竷鐘舵�侊紝瀛楀吀 research_guideline_status
+    publishTime?: string; // 鍙戝竷鏃堕棿
+    remark?: string; // 澶囨敞
+    creator?: string; // 鍒涘缓浜� ID
+    creatorName?: string; // 鍒涘缓浜哄鍚�
+    createTime?: string; // 鍒涘缓鏃堕棿
+    attachmentList?: SystemStorageApi.StorageBlob[]; // 闄勪欢鍒楄〃
+    hasAttachment?: boolean; // 鏄惁鏈夐檮浠�
+    blobIds?: number[]; // 鎻愪氦鏃剁殑闄勪欢 ID 鍒楄〃锛堝叏閲忥級
+  }
+}
+
+/** 鏌ヨ璇鹃鎸囧崡鍒嗛〉 */
+export function getGuidelinePage(params: PageParam) {
+  return requestClient.get<PageResult<ResearchGuidelineApi.Guideline>>(
+    '/research/guideline/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ璇鹃鎸囧崡璇︽儏 */
+export function getGuideline(id: number) {
+  return requestClient.get<ResearchGuidelineApi.Guideline>(
+    `/research/guideline/get?id=${id}`,
+  );
+}
+
+/** 鏌ヨ宸插彂甯冪殑璇鹃鎸囧崡鍒楄〃锛堜緵鐢虫姤閫夋寚鍗楋級 */
+export function getPublishedGuidelineList() {
+  return requestClient.get<ResearchGuidelineApi.Guideline[]>(
+    '/research/guideline/published-list',
+  );
+}
+
+/** 鏂板璇鹃鎸囧崡锛堣崏绋匡級 */
+export function createGuideline(data: ResearchGuidelineApi.Guideline) {
+  return requestClient.post('/research/guideline/create', data);
+}
+
+/** 淇敼璇鹃鎸囧崡锛堜粎鑽夌鍙敼锛� */
+export function updateGuideline(data: ResearchGuidelineApi.Guideline) {
+  return requestClient.put('/research/guideline/update', data);
+}
+
+/** 鍒犻櫎璇鹃鎸囧崡锛堜粎鑽夌涓旀棤鐢虫姤涔﹀彲鍒狅級 */
+export function deleteGuideline(id: number) {
+  return requestClient.delete('/research/guideline/delete', {
+    params: { id },
+  });
+}
+
+/** 鍙戝竷璇鹃鎸囧崡 */
+export function publishGuideline(id: number) {
+  return requestClient.put('/research/guideline/publish', null, {
+    params: { id },
+  });
+}
+
+/** 鎴璇鹃鎸囧崡 */
+export function closeGuideline(id: number) {
+  return requestClient.put('/research/guideline/close', null, {
+    params: { id },
+  });
+}
diff --git a/src/api/ums/application/index.ts b/src/api/ums/application/index.ts
new file mode 100644
index 0000000..c5550ae
--- /dev/null
+++ b/src/api/ums/application/index.ts
@@ -0,0 +1,88 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace UmsApplicationApi {
+  /** 鍏ヤ細鐢宠 */
+  export interface Application {
+    id?: number;
+    no?: string; // 鐢宠缂栧彿锛屽悗绔敓鎴�
+    memberType: number; // 浼氬憳绫诲瀷锛屽瓧鍏� ums_member_type
+    applicantName: string; // 鐢宠浜哄鍚嶏紙涓汉锛夋垨鍗曚綅鍚嶇О锛堝崟浣嶏級
+    contactPhone?: string; // 鑱旂郴鐢佃瘽
+    contactEmail?: string; // 閭
+    legalPerson?: string; // 娉曞畾浠h〃浜猴紙鍗曚綅涓撴湁锛�
+    organizationCode?: string; // 缁熶竴绀句細淇$敤浠g爜锛堝崟浣嶄笓鏈夛級
+    researchDirection?: string; // 鐮旂┒鏂瑰悜
+    workUnit?: string; // 宸ヤ綔鍗曚綅锛堜釜浜轰笓鏈夛級
+    title?: string; // 鑱岀О锛堜釜浜轰笓鏈夛級
+    memberGrade?: string; // 鐢宠绛夌骇锛堟寜 memberType 鍙栦笉鍚屽瓧鍏革級
+    regionCode?: string; // 鍦板尯缂栫爜
+    industry?: string; // 琛屼笟锛屽瓧鍏� ums_industry
+    status?: number; // 鐘舵�侊紝瀛楀吀 ums_application_status
+    processInstanceId?: string; // BPM 娴佺▼瀹炰緥 ID
+    remark?: string; // 澶囨敞
+    creator?: string; // 鍒涘缓浜� ID
+    creatorName?: string; // 鍒涘缓浜哄鍚�
+    createTime?: string; // 鍒涘缓鏃堕棿
+    updateTime?: string; // 鏇存柊鏃堕棿
+    attachmentList?: SystemStorageApi.StorageBlob[]; // 闄勪欢鍒楄〃
+    hasAttachment?: boolean; // 鏄惁鏈夐檮浠�
+    blobIds?: number[]; // 鎻愪氦鏃剁殑闄勪欢 ID 鍒楄〃锛堝叏閲忥級
+  }
+
+  /** 瀹℃壒娴佺▼瀹氫箟 */
+  export interface ProcessDefinition {
+    id: string;
+    key: string;
+    name: string;
+  }
+}
+
+/** 鏌ヨ鍏ヤ細鐢宠鍒嗛〉 */
+export function getApplicationPage(params: PageParam) {
+  return requestClient.get<PageResult<UmsApplicationApi.Application>>(
+    '/ums/application/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鍏ヤ細鐢宠璇︽儏 */
+export function getApplication(id: number) {
+  return requestClient.get<UmsApplicationApi.Application>(
+    `/ums/application/get?id=${id}`,
+  );
+}
+
+/** 鏂板鍏ヤ細鐢宠 */
+export function createApplication(data: UmsApplicationApi.Application) {
+  return requestClient.post('/ums/application/create', data);
+}
+
+/** 淇敼鍏ヤ細鐢宠锛堜粎鑽夌銆佸凡椹冲洖鍙敼锛� */
+export function updateApplication(data: UmsApplicationApi.Application) {
+  return requestClient.put('/ums/application/update', data);
+}
+
+/** 鍒犻櫎鍏ヤ細鐢宠锛堜粎鑽夌鍙垹锛� */
+export function deleteApplication(id: number) {
+  return requestClient.delete('/ums/application/delete', {
+    params: { id },
+  });
+}
+
+/** 鎻愪氦鍏ヤ細鐢宠瀹℃壒 */
+export function submitApplication(id: number, processDefinitionKey: string) {
+  return requestClient.post('/ums/application/submit', null, {
+    params: { id, processDefinitionKey },
+  });
+}
+
+/** 鑾峰彇鍏ヤ細鐢宠瀹℃壒娴佺▼鍒楄〃 */
+export function getApplicationApproveProcessList() {
+  return requestClient.get<UmsApplicationApi.ProcessDefinition[]>(
+    '/ums/application/approve-process-list',
+  );
+}
\ No newline at end of file
diff --git a/src/api/ums/member/index.ts b/src/api/ums/member/index.ts
new file mode 100644
index 0000000..685cb4f
--- /dev/null
+++ b/src/api/ums/member/index.ts
@@ -0,0 +1,74 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import type { SystemStorageApi } from '#/api/system/storage';
+
+import { requestClient } from '#/api/request';
+
+export namespace UmsMemberApi {
+  /** 浼氬憳 */
+  export interface Member {
+    id?: number;
+    no?: string; // 浼氬憳缂栧彿锛屽悗绔敓鎴�
+    userId?: number; // 鍏宠仈绯荤粺鐢ㄦ埛 ID
+    username?: string; // 鍏宠仈鐢ㄦ埛鍚嶏紙鏄电О锛�
+    applicationId?: number; // 鏉ユ簮鐢宠鍗� ID
+    memberType: number; // 浼氬憳绫诲瀷锛屽瓧鍏� ums_member_type
+    memberGrade: string; // 浼氬憳绛夌骇锛堟寜 memberType 鍙栦笉鍚屽瓧鍏革級
+    name: string; // 濮撳悕锛堜釜浜猴級鎴栧崟浣嶅悕绉帮紙鍗曚綅锛�
+    contactUser?: string; // 鑱旂郴浜猴紙鍗曚綅鐢級
+    contactPhone?: string; // 鑱旂郴鐢佃瘽
+    contactEmail?: string; // 閭
+    legalPerson?: string; // 娉曞畾浠h〃浜猴紙鍗曚綅涓撴湁锛�
+    organizationCode?: string; // 缁熶竴绀句細淇$敤浠g爜
+    researchDirection?: string; // 鐮旂┒鏂瑰悜
+    workUnit?: string; // 宸ヤ綔鍗曚綅锛堜釜浜轰笓鏈夛級
+    title?: string; // 鑱岀О锛堜釜浜轰笓鏈夛級
+    unitIntro?: string; // 鍗曚綅绠�浠嬶紙鍗曚綅涓撴湁锛�
+    regionCode?: string; // 鍦板尯缂栫爜
+    industry?: string; // 琛屼笟锛屽瓧鍏� ums_industry
+    joinDate?: string; // 鍏ヤ細鏃ユ湡
+    expireDate?: string; // 鏈夋晥鏈熻嚦
+    status?: number; // 鐘舵�侊紝瀛楀吀 ums_member_status
+    remark?: string; // 澶囨敞
+    createTime?: string; // 鍒涘缓鏃堕棿
+    attachmentList?: SystemStorageApi.StorageBlob[]; // 闄勪欢鍒楄〃
+    hasAttachment?: boolean; // 鏄惁鏈夐檮浠�
+    blobIds?: number[]; // 鎻愪氦鏃剁殑闄勪欢 ID 鍒楄〃锛堝叏閲忥級
+  }
+}
+
+/** 鏌ヨ浼氬憳閫氳褰曞垎椤� */
+export function getMemberPage(params: PageParam) {
+  return requestClient.get<PageResult<UmsMemberApi.Member>>('/ums/member/page', {
+    params,
+  });
+}
+
+/** 鏌ヨ浼氬憳璇︽儏 */
+export function getMember(id: number) {
+  return requestClient.get<UmsMemberApi.Member>(`/ums/member/get?id=${id}`);
+}
+
+/** 閫氳繃鐢ㄦ埛 ID 鏌ヨ浼氬憳 */
+export function getMemberByUserId(userId: number) {
+  return requestClient.get<UmsMemberApi.Member>(
+    `/ums/member/get-by-user-id?userId=${userId}`,
+  );
+}
+
+/** 鏂板浼氬憳 */
+export function createMember(data: UmsMemberApi.Member) {
+  return requestClient.post('/ums/member/create', data);
+}
+
+/** 淇敼浼氬憳 */
+export function updateMember(data: UmsMemberApi.Member) {
+  return requestClient.put('/ums/member/update', data);
+}
+
+/** 鍒犻櫎浼氬憳 */
+export function deleteMember(id: number) {
+  return requestClient.delete('/ums/member/delete', {
+    params: { id },
+  });
+}
\ No newline at end of file
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 53e2121..08aed55 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -144,6 +144,31 @@
   ERP_STOCK_RECORD_BIZ_TYPE: 'erp_stock_record_biz_type', // 搴撳瓨鏄庣粏鐨勪笟鍔$被鍨�
 } as const;
 
+/** ========== FINANCE - 璐㈠姟妯″潡 ========== */
+const FINANCE_DICT = {
+  FINANCE_REIMBURSEMENT_STATUS: 'finance_reimbursement_status', // 鎶ラ攢鍗曠姸鎬�
+  FINANCE_EXPENSE_TYPE: 'finance_expense_type', // 璐圭敤绫诲瀷
+} as const;
+
+/** ========== UMS - 浼氬憳绠$悊妯″潡 ========== */
+const UMS_DICT = {
+  UMS_APPLICATION_STATUS: 'ums_application_status', // 鍏ヤ細鐢宠鐘舵��
+  UMS_MEMBER_TYPE: 'ums_member_type', // 浼氬憳绫诲瀷
+  UMS_MEMBER_STATUS: 'ums_member_status', // 浼氬憳鐘舵��
+  UMS_PERSONAL_GRADE: 'ums_personal_grade', // 涓汉浼氬憳绛夌骇
+  UMS_UNIT_GRADE: 'ums_unit_grade', // 鍗曚綅浼氬憳绛夌骇
+  UMS_INDUSTRY: 'ums_industry', // 浼氬憳琛屼笟
+} as const;
+
+/** ========== RESEARCH - 璇鹃绠$悊妯″潡 ========== */
+const RESEARCH_DICT = {
+  RESEARCH_GUIDELINE_CATEGORY: 'research_guideline_category', // 璇鹃鎸囧崡绫诲埆锛堥噸鐐�/涓�鑸級
+  RESEARCH_GUIDELINE_STATUS: 'research_guideline_status', // 璇鹃鎸囧崡鍙戝竷鐘舵��
+  RESEARCH_APPLICATION_STATUS: 'research_application_status', // 璇鹃鐢虫姤涔︾姸鎬�
+  RESEARCH_REVIEW_CONCLUSION: 'research_review_conclusion', // 涓撳璇勫缁撹
+  RESEARCH_FUND_STATUS: 'research_fund_status', // 缁忚垂鎷ㄤ粯鐘舵��
+} as const;
+
 /** ========== AI - 浜哄伐鏅鸿兘妯″潡 ========== */
 const AI_DICT = {
   AI_GENERATE_MODE: 'ai_generate_mode', // AI 鐢熸垚妯″紡
@@ -351,12 +376,15 @@
   ...BPM_DICT,
   ...CRM_DICT,
   ...ERP_DICT,
+  ...FINANCE_DICT,
   ...HRM_DICT,
   ...INFRA_DICT,
   ...IM_DICT,
   ...IOT_DICT,
   ...MES_DICT,
   ...MDM_DICT,
+  ...UMS_DICT,
+  ...RESEARCH_DICT,
   ...WMS_DICT,
   ...MEMBER_DICT,
   ...MP_DICT,
diff --git a/src/views/finance/reimbursement/data.ts b/src/views/finance/reimbursement/data.ts
new file mode 100644
index 0000000..1b887c6
--- /dev/null
+++ b/src/views/finance/reimbursement/data.ts
@@ -0,0 +1,307 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鎶ラ攢鍗曠姸鎬侊紙瀵瑰簲瀛楀吀 finance_reimbursement_status锛� */
+export const ReimbursementStatus = {
+  DRAFT: 0, // 鑽夌
+  APPROVING: 15, // 瀹℃壒涓�
+  APPROVED: 20, // 宸查�氳繃
+  REJECTED: 35, // 宸查┏鍥�
+  ARCHIVED: 40, // 宸插綊妗�
+} as const;
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '鎶ラ攢鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鐣欑┖鍒欑敱绯荤粺鑷姩鐢熸垚',
+        maxlength: 36,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '鎶ラ攢鏍囬',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ姤閿�鏍囬',
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'applyDate',
+      label: '鐢宠鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐢宠鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+        style: { width: '100%' },
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'totalAmount',
+      label: '鎶ラ攢鎬婚噾棰�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        placeholder: '鐢辨槑缁嗛噾棰濇眹鎬伙紝鍙墜鍔ㄤ慨鏀�',
+        precision: 2,
+        formatter: erpPriceInputFormatter,
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '鍙戠エ闄勪欢',
+      component: 'FileUpload',
+      // 娉ㄦ剰锛歷alueKey 涓� id锛屾彁浜ゆ椂 formApi 浼氭妸鏂囦欢瀵硅薄鎻愬彇涓� blobId 鏁扮粍
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 20,
+        multiple: true,
+        showDescription: formType !== 'detail',
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'items',
+      label: '鎶ラ攢鏄庣粏',
+      component: 'Input',
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鎶ラ攢鏄庣粏琛ㄦ牸鍒楋紙鏄庣粏闅忎富琛ㄤ竴娆℃�ф彁浜わ紝鍓嶇鍙鍒犺锛� */
+export function useItemColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'seq', title: '搴忓彿', width: 55, fixed: 'left' },
+    {
+      field: 'expenseType',
+      title: '璐圭敤绫诲瀷',
+      minWidth: 120,
+      slots: { default: 'expenseType' },
+    },
+    {
+      field: 'invoiceNo',
+      title: '鍙戠エ鍙风爜',
+      minWidth: 160,
+      slots: { default: 'invoiceNo' },
+    },
+    {
+      field: 'invoiceAmount',
+      title: '鍙戠エ閲戦',
+      minWidth: 130,
+      slots: { default: 'invoiceAmount' },
+    },
+    {
+      field: 'invoiceDate',
+      title: '鍙戠エ鏃ユ湡',
+      minWidth: 140,
+      slots: { default: 'invoiceDate' },
+    },
+    {
+      field: 'drawer',
+      title: '寮�绁ㄦ柟',
+      minWidth: 180,
+      slots: { default: 'drawer' },
+    },
+    {
+      field: 'description',
+      title: '璐圭敤璇存槑',
+      minWidth: 180,
+      slots: { default: 'description' },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 80,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/**
+ * 鏄庣粏琛屾姇褰变负鍙彁浜ゅ璞★細瀛楁鐧藉悕鍗曪紝涓�**涓�寰嬩笉甯� id / reimbursementId**銆�
+ * 鍚庣淇濆瓨鏄庣粏鏄�屽厛鍒犲悗澧炪�嶅叏閲忚鐩栵紙杞垹鏃ц锛夛紝鑻ュ洖浼犲師 id 浼氫笌宸茶蒋鍒犺涓婚敭鍐茬獊銆�
+ * 鍥炴樉涓庢彁浜や袱鏉¤矾寰勯兘蹇呴』缁忚繃姝ゅ嚱鏁般��
+ */
+export function toSubmitItems(
+  items?: FinanceReimbursementApi.ReimbursementItem[],
+): FinanceReimbursementApi.ReimbursementItem[] {
+  return (items ?? []).map((item) => ({
+    expenseType: item.expenseType,
+    invoiceNo: item.invoiceNo,
+    invoiceAmount: item.invoiceAmount,
+    invoiceDate: item.invoiceDate,
+    drawer: item.drawer,
+    description: item.description,
+  }));
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '鎶ラ攢鍗曞彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ姤閿�鍗曞彿',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '鎶ラ攢鏍囬',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ姤閿�鏍囬',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.FINANCE_REIMBURSEMENT_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'applicantId',
+      label: '鐢宠浜�',
+      component: 'ApiSelect',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐢宠浜�',
+        allowClear: true,
+        showSearch: true,
+        api: getSimpleUserList,
+        labelField: 'nickname',
+        valueField: 'id',
+      },
+    },
+    {
+      fieldName: 'applyDate',
+      label: '鐢宠鏃ユ湡',
+      component: 'RangePicker',
+      componentProps: {
+        // 鍚庣 LocalDate[] 鍙帴鍙� yyyy-MM-dd锛屼笉鑳藉甫鏃跺垎绉�
+        valueFormat: 'YYYY-MM-DD',
+        allowClear: true,
+        style: { width: '100%' },
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '鎶ラ攢鍗曞彿',
+      width: 175,
+      fixed: 'left',
+    },
+    {
+      field: 'title',
+      title: '鎶ラ攢鏍囬',
+      minWidth: 160,
+    },
+    {
+      field: 'totalAmount',
+      title: '鎶ラ攢鎬婚噾棰�',
+      minWidth: 120,
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 95,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.FINANCE_REIMBURSEMENT_STATUS },
+      },
+    },
+    {
+      field: 'applicantName',
+      title: '鐢宠浜�',
+      minWidth: 100,
+    },
+    {
+      field: 'applyDate',
+      title: '鐢宠鏃ユ湡',
+      width: 115,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'hasAttachment',
+      title: '鏄惁涓婁紶鍙戠エ',
+      minWidth: 110,
+      slots: { default: 'hasAttachment' },
+    },
+    {
+      field: 'creatorName',
+      title: '鍒涘缓浜�',
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 300,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/finance/reimbursement/detail/approve/index.vue b/src/views/finance/reimbursement/detail/approve/index.vue
new file mode 100644
index 0000000..1e425ad
--- /dev/null
+++ b/src/views/finance/reimbursement/detail/approve/index.vue
@@ -0,0 +1,199 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, nextTick, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getReimbursement } from '#/api/finance/reimbursement';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'FinanceReimbursementApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<FinanceReimbursementApi.Reimbursement>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+  return val === 0 || val ? String(val) : '-';
+}
+
+/** 鎶ラ攢鏄庣粏锛堝鎵硅鎯呭彧璇诲垪锛� */
+function useReimbursementItemColumns(): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'seq', title: '搴忓彿', width: 55 },
+    {
+      field: 'expenseType',
+      title: '璐圭敤绫诲瀷',
+      minWidth: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.FINANCE_EXPENSE_TYPE },
+      },
+    },
+    {
+      field: 'invoiceNo',
+      title: '鍙戠エ鍙风爜',
+      minWidth: 160,
+      formatter: ({ cellValue }) => textRender(cellValue),
+    },
+    {
+      field: 'invoiceAmount',
+      title: '鍙戠エ閲戦',
+      minWidth: 120,
+      formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+    },
+    {
+      field: 'invoiceDate',
+      title: '鍙戠エ鏃ユ湡',
+      minWidth: 120,
+      formatter: ({ cellValue }) => textRender(cellValue),
+    },
+    {
+      field: 'drawer',
+      title: '寮�绁ㄦ柟',
+      minWidth: 160,
+      formatter: ({ cellValue }) => textRender(cellValue),
+    },
+    {
+      field: 'description',
+      title: '璐圭敤璇存槑',
+      minWidth: 160,
+      formatter: ({ cellValue }) => textRender(cellValue),
+    },
+  ];
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useReimbursementItemColumns(),
+    data: [],
+    minHeight: 200,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  },
+});
+
+const [Descriptions] = useDescription({
+  bordered: true,
+  column: 2,
+  schema: [
+    { label: '鎶ラ攢鍗曞彿', field: 'no', render: textRender },
+    { label: '鎶ラ攢鏍囬', field: 'title', render: textRender },
+    {
+      label: '鎶ラ攢鎬婚噾棰濓紙鍏冿級',
+      field: 'totalAmount',
+      render: (val) => erpPriceInputFormatter(val) || '-',
+    },
+    {
+      label: '鐘舵��',
+      field: 'status',
+      render: (val) =>
+        h(DictTag, {
+          type: DICT_TYPE.FINANCE_REIMBURSEMENT_STATUS,
+          value: val,
+        }),
+    },
+    { label: '鐢宠浜�', field: 'applicantName', render: textRender },
+    {
+      label: '鐢宠鏃ユ湡',
+      field: 'applyDate',
+      render: (val) => textRender(val),
+    },
+    {
+      label: '褰掓。鏃堕棿',
+      field: 'archiveTime',
+      render: (val) => (val ? (formatDateTime(val) as string) : '-'),
+    },
+    { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+    {
+      label: '鍒涘缓鏃堕棿',
+      field: 'createTime',
+      render: (val) => formatDateTime(val) as string,
+    },
+    { label: '澶囨敞', field: 'remark', render: textRender },
+  ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+  try {
+    loading.value = true;
+    formData.value = await getReimbursement(Number(props.id));
+    if (formData.value?.items?.length) {
+      await nextTick();
+      await gridApi.grid.reloadData(formData.value.items);
+    }
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<template>
+  <ContentWrap class="m-2">
+    <Spin :spinning="loading" tip="鍔犺浇涓�...">
+      <div class="space-y-4">
+        <!-- 鎶ラ攢鍗曞熀鏈俊鎭� -->
+        <div class="rounded-lg border border-border bg-background p-4">
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">鎶ラ攢鍗曞熀鏈俊鎭�</span>
+          </div>
+          <Descriptions :data="formData" />
+        </div>
+        <!-- 鎶ラ攢鏄庣粏 -->
+        <div
+          v-if="formData?.items?.length"
+          class="rounded-lg border border-border bg-background p-4"
+        >
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">鎶ラ攢鏄庣粏</span>
+          </div>
+          <Grid />
+        </div>
+        <!-- 鍙戠エ闄勪欢 -->
+        <div
+          v-if="formData?.attachmentList?.length"
+          class="rounded-lg border border-border bg-background p-4"
+        >
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">鍙戠エ闄勪欢</span>
+          </div>
+          <div class="flex flex-wrap gap-3">
+            <a
+              v-for="file in formData.attachmentList"
+              :key="file.id"
+              :href="file.previewURL || file.url"
+              target="_blank"
+              class="text-blue-500 hover:underline"
+            >
+              {{ file.name || `鍙戠エ-${file.id}` }}
+            </a>
+          </div>
+        </div>
+      </div>
+    </Spin>
+  </ContentWrap>
+</template>
diff --git a/src/views/finance/reimbursement/index.vue b/src/views/finance/reimbursement/index.vue
new file mode 100644
index 0000000..49f64c9
--- /dev/null
+++ b/src/views/finance/reimbursement/index.vue
@@ -0,0 +1,285 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  archiveReimbursement,
+  deleteReimbursement,
+  exportReimbursement,
+  getReimbursementApproveProcessList,
+  getReimbursementPage,
+} from '#/api/finance/reimbursement';
+import { $t } from '#/locales';
+
+import {
+  ReimbursementStatus,
+  useGridColumns,
+  useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+
+/** 璐㈠姟鎶ラ攢鍗曞垪琛� */
+defineOptions({ name: 'FinanceReimbursement' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+  const data = await exportReimbursement(await gridApi.formApi.getValues());
+  downloadFileFromBlobPart({ fileName: '鎶ラ攢鍗�.xls', source: data });
+}
+
+/** 鏂板鎶ラ攢鍗� */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鎶ラ攢鍗曪紙浠呰崏绋裤�佸凡椹冲洖鍙紪杈戯級 */
+function handleEdit(row: FinanceReimbursementApi.Reimbursement) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅鎶ラ攢鍗曡鎯� */
+function handleDetail(row: FinanceReimbursementApi.Reimbursement) {
+  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 Promise.all(ids.map((id) => deleteReimbursement(id)));
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛紝娴佺▼ key 涓嶅啓姝� */
+async function handleSubmit(row: FinanceReimbursementApi.Reimbursement) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getReimbursementApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堝埌 BPM 娴佺▼璁捐鍣ㄥ彂甯冩姤閿�瀹℃壒娴佺▼');
+      return;
+    }
+    processModalApi.setData({ id: row.id, processList }).open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 褰掓。鎶ラ攢鍗� */
+async function handleArchive(row: FinanceReimbursementApi.Reimbursement) {
+  const hideLoading = message.loading({
+    content: '褰掓。涓�...',
+    duration: 0,
+  });
+  try {
+    await archiveReimbursement(row.id!);
+    message.success('褰掓。鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: FinanceReimbursementApi.Reimbursement) {
+  if (!row.processInstanceId) {
+    message.warning('璇ユ姤閿�鍗曞皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: FinanceReimbursementApi.Reimbursement[];
+}) {
+  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 getReimbursementPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<FinanceReimbursementApi.Reimbursement>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
+    <Grid table-title="鎶ラ攢鍗曞垪琛�">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['鎶ラ攢鍗�']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['finance:reimbursement:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: $t('ui.actionTitle.export'),
+              type: 'primary',
+              icon: ACTION_ICON.DOWNLOAD,
+              auth: ['finance:reimbursement:export'],
+              onClick: handleExport,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['finance:reimbursement:delete'],
+              popConfirm: {
+                title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛燂紙浠呰崏绋跨姸鎬佸彲鍒犻櫎锛塦,
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #hasAttachment="{ row }">
+        <Tag :color="row.hasAttachment ? 'success' : 'default'">
+          {{ row.hasAttachment ? '鏈�' : '鏃�' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['finance:reimbursement:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['finance:reimbursement:update'],
+              ifShow: () =>
+                row.status === ReimbursementStatus.DRAFT ||
+                row.status === ReimbursementStatus.REJECTED,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['finance:reimbursement:update-status'],
+              ifShow: () =>
+                row.status === ReimbursementStatus.DRAFT ||
+                row.status === ReimbursementStatus.REJECTED,
+              onClick: handleSubmit.bind(null, row),
+            },
+            {
+              label: '褰掓。',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['finance:reimbursement:archive'],
+              ifShow: () => row.status === ReimbursementStatus.APPROVED,
+              popConfirm: {
+                title: `纭褰掓。鎶ラ攢鍗� ${row.no} 鍚楋紵褰掓。鍚庝笉鍙啀淇敼`,
+                confirm: handleArchive.bind(null, row),
+              },
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['finance:reimbursement:query'],
+              ifShow: () => !!row.processInstanceId,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['finance:reimbursement:delete'],
+              ifShow: () => row.status === ReimbursementStatus.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/finance/reimbursement/modules/form.vue b/src/views/finance/reimbursement/modules/form.vue
new file mode 100644
index 0000000..06fb782
--- /dev/null
+++ b/src/views/finance/reimbursement/modules/form.vue
@@ -0,0 +1,179 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { message } from 'ant-design-vue';
+import dayjs from 'dayjs';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createReimbursement,
+  getReimbursement,
+  updateReimbursement,
+} from '#/api/finance/reimbursement';
+
+import { toSubmitItems, useFormSchema } from '../data';
+import ItemForm from './item-form.vue';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<FinanceReimbursementApi.Reimbursement>({
+  title: '',
+  totalAmount: 0,
+  applyDate: '',
+  remark: '',
+  items: [],
+});
+
+const formType = ref<FormType>('create'); // 琛ㄥ崟绫诲瀷锛�'create' | 'edit' | 'detail'
+const itemFormRef = ref<InstanceType<typeof ItemForm>>();
+
+/**
+ * 鏄庣粏琛ㄦ牸鐨勫洖鏄炬暟鎹簮锛氬彧鍦ㄦ墦寮�寮圭獥鏃惰祴鍊硷紙缂栬緫鍥炴樉 / 鏂板缃┖锛夈��
+ * 鐢ㄦ埛鍦ㄨ〃鏍奸噷鐨勫鍒犳敼鍙�氳繃 emit 鍗曞悜娴佸埌琛ㄥ崟鍊硷紝缁濅笉鍥炲啓鍒拌繖閲岋紝
+ * 鍚﹀垯 prop 鍙樺寲浼氳琛ㄦ牸鍙嶅閲嶈浇锛堜笖涓庡瓙缁勪欢 watcher 褰㈡垚姝诲惊鐜級銆�
+ */
+const initialItems = ref<FinanceReimbursementApi.ReimbursementItem[]>([]);
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['鎶ラ攢鍗�']);
+  } else if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['鎶ラ攢鍗�']);
+  } else {
+    return '鎶ラ攢鍗曡鎯�';
+  }
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+/** 鍚屾鏄庣粏琛屽埌琛ㄥ崟鍊硷紝淇濊瘉闅忎富琛ㄤ竴娆℃�ф彁浜� */
+function handleUpdateItems(items: FinanceReimbursementApi.ReimbursementItem[]) {
+  // 绗簩涓弬鏁颁负 false锛氳烦杩囧悎骞剁畻娉曪紝鍚﹀垯鏁扮粍绫诲瀷鐨勫�间細琚繃婊ゆ帀
+  formApi.setValues({ items }, false);
+}
+
+/** 鏄庣粏閲戦鍙樺寲鏃跺洖濉富琛ㄦ姤閿�鎬婚噾棰濓紙鐢ㄦ埛浠嶅彲鎵嬪姩淇敼锛� */
+function handleUpdateTotalAmount(totalAmount: number) {
+  formApi.setValues({ totalAmount }, false);
+}
+
+/** 鍒涘缓鎴栨洿鏂版姤閿�鍗� */
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    const itemFormInstance = Array.isArray(itemFormRef.value)
+      ? itemFormRef.value[0]
+      : itemFormRef.value;
+    try {
+      itemFormInstance?.validate();
+    } catch (error: any) {
+      message.error(error.message || '鎶ラ攢鏄庣粏楠岃瘉澶辫触');
+      return;
+    }
+
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as FinanceReimbursementApi.Reimbursement;
+    // 鍙戠エ闄勪欢涓哄叏閲忔浛鎹㈣涔夛細娓呯┖鍏ㄩ儴鍙戠エ鏃跺繀椤绘樉寮忎紶绌烘暟缁勶紝
+    // 浼� null 鍚庣浼氳涓恒�屼笉鍔ㄩ檮浠躲�嶏紝瀵艰嚧鍒犻櫎鎿嶄綔涓嶇敓鏁�
+    data.blobIds = data.blobIds ?? [];
+    try {
+      await (formType.value === 'create'
+        ? createReimbursement(data)
+        : updateReimbursement(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = {} as FinanceReimbursementApi.Reimbursement;
+      initialItems.value = [];
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    formApi.updateSchema(useFormSchema(formType.value));
+    if (!data || !data.id) {
+      initialItems.value = [];
+      // 鏂板缓鏃堕粯璁ょ敵璇锋棩鏈熶负浠婂ぉ
+      await formApi.setValues(
+        { applyDate: dayjs().format('YYYY-MM-DD') },
+        false,
+      );
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getReimbursement(data.id);
+      // 鏄庣粏琛ㄦ牸鐨勮 key 鐢ㄦ暟鎹簱 id锛屾晠鍥炴樉鏁版嵁婧愪繚鐣欏師 id
+      initialItems.value = formData.value.items ?? [];
+      // 宸叉湁鍙戠エ杞垚涓婁紶缁勪欢鍙瘑鍒殑鏂囦欢瀵硅薄锛屾彁浜ゆ椂鎸� valueKey=id 鎻愬彇涓� blobId
+      const blobIds = (formData.value.attachmentList ?? []).map((item) => ({
+        uid: String(item.id),
+        name: item.name || '',
+        url: item.url || '',
+        status: 'done',
+        id: item.id,
+      }));
+      await formApi.setValues(
+        {
+          ...formData.value,
+          // 琛ㄥ崟閲岀殑 items 鍙敤浜庢彁浜わ紝蹇呴』鍓ユ帀琛� id锛堝悗绔�屽厛鍒犲悗澧炪�嶄細涓庤蒋鍒犺涓婚敭鍐茬獊锛�
+          items: toSubmitItems(formData.value.items),
+          blobIds,
+        },
+        false,
+      );
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :close-on-click-modal="false"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3">
+      <template #items>
+        <ItemForm
+          ref="itemFormRef"
+          :items="initialItems"
+          :disabled="formType === 'detail'"
+          class="w-full"
+          @update:items="handleUpdateItems"
+          @update:total-amount="handleUpdateTotalAmount"
+        />
+      </template>
+    </Form>
+  </Modal>
+</template>
diff --git a/src/views/finance/reimbursement/modules/item-form.vue b/src/views/finance/reimbursement/modules/item-form.vue
new file mode 100644
index 0000000..3c852a4
--- /dev/null
+++ b/src/views/finance/reimbursement/modules/item-form.vue
@@ -0,0 +1,223 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
+
+import type { DefaultOptionType } from 'ant-design-vue/es/select';
+
+import { nextTick, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { Button, DatePicker, Input, InputNumber, Select } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { $t } from '#/locales';
+
+import { toSubmitItems, useItemColumns } from '../data';
+
+const props = withDefaults(
+  defineProps<{
+    items?: FinanceReimbursementApi.ReimbursementItem[];
+    disabled?: boolean;
+  }>(),
+  {
+    items: () => [],
+    disabled: false,
+  },
+);
+
+const emit = defineEmits(['update:items', 'update:totalAmount']);
+
+const expenseTypeOptions = getDictOptions(
+  DICT_TYPE.FINANCE_EXPENSE_TYPE,
+  'number',
+) as DefaultOptionType[];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useItemColumns(),
+    data: [],
+    minHeight: 250,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  } as VxeTableGridOptions<FinanceReimbursementApi.ReimbursementItem>,
+});
+
+/** 杈撳叆鏃朵笉琛ラ浂锛屽け鐒﹀悗浠嶆寜閲戦鏍煎紡淇濈暀涓や綅灏忔暟 */
+const amountFormatter = (
+  value: number | string | undefined,
+  info: { input: string; userTyping: boolean },
+) => {
+  return info.userTyping ? info.input : erpPriceInputFormatter(value);
+};
+
+/** 璇诲彇褰撳墠鍏ㄩ儴鏄庣粏琛岋紙鎻愪氦褰㈡�侊細涓嶅惈琛� id锛� */
+function collectItems(): FinanceReimbursementApi.ReimbursementItem[] {
+  const rows =
+    (gridApi.grid.getData() as FinanceReimbursementApi.ReimbursementItem[]) ??
+    [];
+  const removed =
+    (gridApi.grid.getRemoveRecords() as FinanceReimbursementApi.ReimbursementItem[]) ??
+    [];
+  const inserted =
+    (gridApi.grid.getInsertRecords() as FinanceReimbursementApi.ReimbursementItem[]) ??
+    [];
+  const isRemoved = (row: FinanceReimbursementApi.ReimbursementItem) =>
+    removed.some(
+      (item) => item === row || (row.id != null && item.id === row.id),
+    );
+  return toSubmitItems([...rows.filter((row) => !isRemoved(row)), ...inserted]);
+}
+
+/** 閲嶆柊姹囨�绘槑缁嗗苟鍚屾缁欑埗缁勪欢 */
+function recalc() {
+  const rows = collectItems();
+  const totalAmount = rows.reduce(
+    (sum, row) => sum + (Number(row.invoiceAmount) || 0),
+    0,
+  );
+  emit('update:items', rows);
+  emit('update:totalAmount', Number(totalAmount.toFixed(2)));
+}
+
+/** 娣诲姞鏄庣粏琛� */
+async function handleAdd() {
+  await gridApi.grid.insertAt(
+    {} as unknown as FinanceReimbursementApi.ReimbursementItem,
+    -1,
+  );
+  recalc();
+}
+
+/** 鍒犻櫎鏄庣粏琛� */
+async function handleDelete(row: FinanceReimbursementApi.ReimbursementItem) {
+  await gridApi.grid.remove(row);
+  recalc();
+}
+
+/** 鏍¢獙鏄庣粏锛氳嚦灏� 1 琛岋紝涓旀瘡琛岃垂鐢ㄧ被鍨嬩笌鍙戠エ閲戦蹇呭~ */
+function validate() {
+  const rows = collectItems();
+  if (rows.length === 0) {
+    throw new Error('璇疯嚦灏戞坊鍔犱竴鏉℃姤閿�鏄庣粏');
+  }
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i];
+    if (row?.expenseType === undefined || row?.expenseType === null) {
+      throw new Error(`绗� ${i + 1} 琛岋細璇烽�夋嫨璐圭敤绫诲瀷`);
+    }
+    if (!row?.invoiceAmount || Number(row.invoiceAmount) <= 0) {
+      throw new Error(`绗� ${i + 1} 琛岋細鍙戠エ閲戦蹇呴』澶т簬 0`);
+    }
+  }
+}
+
+defineExpose({ validate });
+
+/**
+ * 鍥炴樉鏄庣粏锛堝崟鍚戯細鐖� 鈫� 琛ㄦ牸锛夈��
+ * 姝� watcher 鍐�**绂佹**鍥炰紶 emit('update:items')锛氱埗缁勪欢浼氭妸鏂版暟缁勫啓鍥� :items锛�
+ * 浜庢槸鏈� watcher 琚啀娆¤Е鍙戯紝鏋勬垚 prop 鈫� emit 姝诲惊鐜紝鏂板/缂栬緫鏃堕〉闈細鐩存帴鍗℃銆�
+ * 鎬婚噾棰濆悓鐞嗭紝鍥炴樉鏃朵笉鍥炲~锛岄伩鍏嶈鐩栫敤鎴锋墜宸ヨ皟鏁磋繃鐨勬姤閿�鎬婚噾棰濄��
+ */
+watch(
+  () => props.items,
+  async (items) => {
+    if (!items) {
+      return;
+    }
+    await nextTick();
+    await gridApi.grid.loadData([...items]);
+  },
+  { immediate: true },
+);
+</script>
+
+<template>
+  <Grid class="w-full">
+    <template #expenseType="{ row }">
+      <Select
+        v-model:value="row.expenseType"
+        :disabled="disabled"
+        :options="expenseTypeOptions"
+        placeholder="璇烽�夋嫨璐圭敤绫诲瀷"
+        allow-clear
+        @change="() => recalc()"
+      />
+    </template>
+    <template #invoiceNo="{ row }">
+      <Input
+        v-model:value="row.invoiceNo"
+        :disabled="disabled"
+        placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�"
+        @change="() => recalc()"
+      />
+    </template>
+    <template #invoiceAmount="{ row }">
+      <InputNumber
+        v-model:value="row.invoiceAmount"
+        :disabled="disabled"
+        :precision="2"
+        :formatter="amountFormatter"
+        class="!w-full"
+        placeholder="璇疯緭鍏ュ彂绁ㄩ噾棰�"
+        @change="() => recalc()"
+      />
+    </template>
+    <template #invoiceDate="{ row }">
+      <DatePicker
+        v-model:value="row.invoiceDate"
+        :disabled="disabled"
+        value-format="YYYY-MM-DD"
+        placeholder="璇烽�夋嫨鍙戠エ鏃ユ湡"
+        class="w-full"
+        @change="() => recalc()"
+      />
+    </template>
+    <template #drawer="{ row }">
+      <Input
+        v-model:value="row.drawer"
+        :disabled="disabled"
+        placeholder="璇疯緭鍏ュ紑绁ㄦ柟"
+        @change="() => recalc()"
+      />
+    </template>
+    <template #description="{ row }">
+      <Input
+        v-model:value="row.description"
+        :disabled="disabled"
+        placeholder="璇疯緭鍏ヨ垂鐢ㄨ鏄�"
+        @change="() => recalc()"
+      />
+    </template>
+    <template #actions="{ row }">
+      <TableAction
+        :actions="[
+          {
+            label: $t('common.delete'),
+            danger: true,
+            type: 'link',
+            icon: ACTION_ICON.DELETE,
+            disabled,
+            onClick: handleDelete.bind(null, row),
+          },
+        ]"
+      />
+    </template>
+  </Grid>
+  <div v-if="!disabled" class="-mt-4 flex justify-center">
+    <Button type="primary" ghost @click="handleAdd"> 娣诲姞鎶ラ攢鏄庣粏 </Button>
+  </div>
+</template>
diff --git a/src/views/finance/reimbursement/modules/process-select-modal.vue b/src/views/finance/reimbursement/modules/process-select-modal.vue
new file mode 100644
index 0000000..98b4b99
--- /dev/null
+++ b/src/views/finance/reimbursement/modules/process-select-modal.vue
@@ -0,0 +1,76 @@
+<script lang="ts" setup>
+import type { FinanceReimbursementApi } from '#/api/finance/reimbursement';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitReimbursement } from '#/api/finance/reimbursement';
+
+const emit = defineEmits(['success']);
+
+const reimbursementId = ref<number>();
+const processList = ref<FinanceReimbursementApi.ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+/** 娴佺▼ key 鍙兘鏉ヨ嚜鍚庣瀹炴椂鍒楄〃锛岀姝㈠墠绔啓姝� */
+const processOptions = computed(() =>
+  processList.value.map((item) => ({ label: item.name, value: item.key })),
+);
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!selectedProcessKey.value) {
+      message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+      return;
+    }
+    modalApi.lock();
+    try {
+      await submitReimbursement(
+        reimbursementId.value!,
+        selectedProcessKey.value,
+      );
+      await modalApi.close();
+      emit('success');
+      message.success('鎻愪氦鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      reimbursementId.value = undefined;
+      selectedProcessKey.value = '';
+      processList.value = [];
+      return;
+    }
+    const data =
+      modalApi.getData<{
+        id: number;
+        processList: FinanceReimbursementApi.ProcessDefinition[];
+      }>();
+    reimbursementId.value = data.id;
+    processList.value = data.processList || [];
+    // 榛樿閫夋嫨绗竴涓祦绋�
+    if (processList.value.length > 0) {
+      selectedProcessKey.value = processList.value[0]!.key;
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+    <div class="mb-4">
+      <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+      <Select
+        v-model:value="selectedProcessKey"
+        :options="processOptions"
+        style="width: 200px"
+        placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+      />
+    </div>
+  </Modal>
+</template>
diff --git a/src/views/research/application/components/index.ts b/src/views/research/application/components/index.ts
new file mode 100644
index 0000000..8a90b00
--- /dev/null
+++ b/src/views/research/application/components/index.ts
@@ -0,0 +1 @@
+export { default as ResearchMemberSelect } from './member-select.vue';
diff --git a/src/views/research/application/components/member-select-dialog.vue b/src/views/research/application/components/member-select-dialog.vue
new file mode 100644
index 0000000..bb4c4a1
--- /dev/null
+++ b/src/views/research/application/components/member-select-dialog.vue
@@ -0,0 +1,172 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { UmsMemberApi } from '#/api/ums/member';
+
+import { nextTick, ref } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getMemberPage } from '#/api/ums/member';
+
+defineOptions({ name: 'ResearchMemberSelectDialog' });
+
+const emit = defineEmits<{
+  selected: [rows: UmsMemberApi.Member[]];
+}>();
+
+const open = ref(false);
+const selectedRows = ref<UmsMemberApi.Member[]>([]);
+const preSelectedId = ref<number>();
+
+function getTableRows() {
+  return gridApi.grid.getTableData().fullData as UmsMemberApi.Member[];
+}
+
+async function applyPreSelection() {
+  if (!preSelectedId.value) {
+    return;
+  }
+  const row = getTableRows().find((item) => item.id === preSelectedId.value);
+  if (!row) {
+    return;
+  }
+  await gridApi.grid.setRadioRow(row);
+  selectedRows.value = [row];
+}
+
+async function handleCellDblclick({ row }: { row: UmsMemberApi.Member }) {
+  selectedRows.value = [row];
+  await gridApi.grid.setRadioRow(row);
+  handleConfirm();
+}
+
+const gridColumns = [
+  { field: 'no', title: '浼氬憳缂栧彿', width: 180 },
+  {
+    field: 'name',
+    title: '濮撳悕 / 鍗曚綅鍚嶇О',
+    minWidth: 180,
+  },
+  {
+    field: 'memberType',
+    title: '浼氬憳绫诲瀷',
+    width: 110,
+    cellRender: {
+      name: 'CellDict',
+      props: { type: DICT_TYPE.UMS_MEMBER_TYPE },
+    },
+  },
+  { field: 'contactUser', title: '鑱旂郴浜�', width: 110 },
+  { field: 'contactPhone', title: '鑱旂郴鐢佃瘽', width: 140 },
+  { field: 'workUnit', title: '宸ヤ綔鍗曚綅', minWidth: 180 },
+  { field: 'title', title: '鑱岀О', width: 110 },
+];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: [
+      {
+        fieldName: 'name',
+        label: '濮撳悕 / 鍗曚綅鍚嶇О',
+        component: 'Input',
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇疯緭鍏ュ鍚嶆垨鍗曚綅鍚嶇О',
+        },
+      },
+      {
+        fieldName: 'contactPhone',
+        label: '鑱旂郴鐢佃瘽',
+        component: 'Input',
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇疯緭鍏ヨ仈绯荤數璇�',
+        },
+      },
+    ],
+  },
+  gridOptions: {
+    columns: [{ type: 'radio', width: 50 }, ...gridColumns],
+    height: 440,
+    keepSource: true,
+    radioConfig: {
+      highlight: true,
+      trigger: 'row',
+    },
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getMemberPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<UmsMemberApi.Member>,
+  gridEvents: {
+    cellDblclick: handleCellDblclick,
+    radioChange: ({ row }: { row: UmsMemberApi.Member }) => {
+      selectedRows.value = [row];
+    },
+  },
+});
+
+async function resetQueryState() {
+  selectedRows.value = [];
+  await gridApi.grid.clearRadioRow();
+  await gridApi.formApi.resetForm();
+}
+
+async function openModal(selectedId?: number) {
+  open.value = true;
+  preSelectedId.value = selectedId;
+  await nextTick();
+  await resetQueryState();
+  await gridApi.query();
+  await nextTick();
+  await applyPreSelection();
+}
+
+async function closeModal() {
+  open.value = false;
+  await resetQueryState();
+}
+
+function handleConfirm() {
+  const row = selectedRows.value[0];
+  if (!row) {
+    message.warning('璇烽�夋嫨涓�浣嶄細鍛樹綔涓虹敵鎶ヤ汉');
+    return;
+  }
+  emit('selected', [row]);
+  open.value = false;
+}
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+  <Modal
+    v-model:open="open"
+    title="閫夋嫨鐢虫姤浜猴紙浼氬憳锛�"
+    width="72%"
+    :destroy-on-close="true"
+    @ok="handleConfirm"
+    @cancel="closeModal"
+  >
+    <Grid table-title="浼氬憳鍒楄〃" />
+  </Modal>
+</template>
diff --git a/src/views/research/application/components/member-select.vue b/src/views/research/application/components/member-select.vue
new file mode 100644
index 0000000..00f9442
--- /dev/null
+++ b/src/views/research/application/components/member-select.vue
@@ -0,0 +1,132 @@
+<script lang="ts" setup>
+import type { UmsMemberApi } from '#/api/ums/member';
+
+import { computed, ref, useAttrs, watch } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Input, Tooltip } from 'ant-design-vue';
+
+import { getMember } from '#/api/ums/member';
+
+import MemberSelectDialog from './member-select-dialog.vue';
+
+defineOptions({ name: 'ResearchMemberSelect', inheritAttrs: false });
+
+const props = withDefaults(
+  defineProps<{
+    allowClear?: boolean;
+    disabled?: boolean;
+    modelValue?: number;
+    placeholder?: string;
+  }>(),
+  {
+    allowClear: true,
+    disabled: false,
+    modelValue: undefined,
+    placeholder: '璇烽�夋嫨鐢虫姤浜猴紙浼氬憳锛�',
+  },
+);
+const emit = defineEmits<{
+  change: [item: UmsMemberApi.Member | undefined];
+  'update:modelValue': [value: number | undefined];
+}>();
+
+const attrs = useAttrs();
+const dialogRef = ref<InstanceType<typeof MemberSelectDialog>>();
+const hovering = ref(false);
+const selectedItem = ref<UmsMemberApi.Member>();
+
+const displayLabel = computed(() => selectedItem.value?.name ?? '');
+const showClear = computed(
+  () =>
+    props.allowClear &&
+    !props.disabled &&
+    hovering.value &&
+    props.modelValue !== null,
+);
+
+async function resolveMemberById(id: number | undefined) {
+  if (!id) {
+    selectedItem.value = undefined;
+    return;
+  }
+  if (selectedItem.value?.id === id) {
+    return;
+  }
+  selectedItem.value = await getMember(id);
+}
+
+watch(
+  () => props.modelValue,
+  (value) => {
+    resolveMemberById(value);
+  },
+  { immediate: true },
+);
+
+function clearSelected() {
+  selectedItem.value = undefined;
+  emit('update:modelValue', undefined);
+  emit('change', undefined);
+}
+
+function handleClick(event: MouseEvent) {
+  if (props.disabled) {
+    return;
+  }
+  const target = event.target as HTMLElement;
+  if (showClear.value && target.closest('.ant-input-suffix')) {
+    event.stopPropagation();
+    clearSelected();
+    return;
+  }
+  dialogRef.value?.open(props.modelValue ?? undefined);
+}
+
+function handleSelected(rows: UmsMemberApi.Member[]) {
+  const item = rows[0];
+  if (!item) {
+    return;
+  }
+  selectedItem.value = item;
+  emit('update:modelValue', item.id);
+  emit('change', item);
+}
+</script>
+
+<template>
+  <div
+    v-bind="attrs"
+    class="w-full"
+    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
+    @click="handleClick"
+    @mouseenter="hovering = true"
+    @mouseleave="hovering = false"
+  >
+    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+      <template #title>
+        <div v-if="selectedItem" class="leading-6">
+          <div>浼氬憳缂栧彿锛歿{ selectedItem.no || '-' }}</div>
+          <div>鑱旂郴鐢佃瘽锛歿{ selectedItem.contactPhone || '-' }}</div>
+          <div>宸ヤ綔鍗曚綅锛歿{ selectedItem.workUnit || '-' }}</div>
+          <div>鑱岀О锛歿{ selectedItem.title || '-' }}</div>
+        </div>
+      </template>
+      <Input
+        :disabled="disabled"
+        :placeholder="placeholder"
+        :value="displayLabel"
+        readonly
+      >
+        <template #suffix>
+          <IconifyIcon
+            class="size-4"
+            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
+          />
+        </template>
+      </Input>
+    </Tooltip>
+  </div>
+  <MemberSelectDialog ref="dialogRef" @selected="handleSelected" />
+</template>
diff --git a/src/views/research/application/data.ts b/src/views/research/application/data.ts
new file mode 100644
index 0000000..b427dd3
--- /dev/null
+++ b/src/views/research/application/data.ts
@@ -0,0 +1,548 @@
+import type { DefaultOptionType } from 'ant-design-vue/es/select';
+
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ResearchGuidelineApi } from '#/api/research/guideline';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { erpPriceInputFormatter } from '@vben/utils';
+
+import { markRaw } from 'vue';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+import { ResearchMemberSelect } from './components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 璇鹃鐢虫姤涔︾姸鎬侊紙瀵瑰簲瀛楀吀 research_application_status锛� */
+export const ApplicationStatus = {
+  DRAFT: 0, // 鑽夌
+  APPROVING: 15, // 瀹℃壒涓�
+  APPROVED: 20, // 宸茬珛椤�
+  REJECTED: 35, // 宸查┏鍥�
+} as const;
+
+/** 缁忚垂鎷ㄤ粯鐘舵�侊紙瀵瑰簲瀛楀吀 research_fund_status锛� */
+export const FundStatus = {
+  PENDING: 0, // 寰呮嫧浠�
+  PAID: 20, // 宸叉嫧浠�
+} as const;
+
+/** 璇鹃鐢虫姤涔﹁〃鍗曢厤缃� */
+export function useFormSchema(
+  formType: FormType,
+  formApi?: VbenFormApi,
+  guidelines: ResearchGuidelineApi.Guideline[] = [],
+): VbenFormSchema[] {
+  const guidelineOptions: DefaultOptionType[] = guidelines.map((item) => ({
+    label: item.no ? `${item.no} ${item.title}` : item.title,
+    value: item.id,
+  }));
+
+  const readonly = formType === 'detail';
+
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '鐢虫姤缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鐢虫姤缂栧彿鐢辩郴缁熻嚜鍔ㄧ敓鎴�',
+        disabled: true,
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => formType !== 'create',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_APPLICATION_STATUS, 'number'),
+        disabled: true,
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => formType !== 'create',
+      },
+    },
+    {
+      fieldName: 'guidelineId',
+      label: '鎵�灞炶棰樻寚鍗�',
+      component: 'Select',
+      componentProps: {
+        options: guidelineOptions,
+        placeholder: '璇烽�夋嫨璇鹃鎸囧崡',
+        allowClear: true,
+        showSearch: true,
+        optionFilterProp: 'label',
+        disabled: readonly,
+        class: '!w-full',
+        onChange: (value: number) => {
+          const guideline = guidelines.find((item) => item.id === value);
+          formApi?.setFieldValue('category', guideline?.category);
+        },
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'category',
+      label: '璇鹃绫诲埆',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'),
+        disabled: true,
+        placeholder: '鐢辨墍閫夎棰樻寚鍗楄嚜鍔ㄥ甫鍏�',
+      },
+      help: '鐢辨墍閫夎棰樻寚鍗楄嚜鍔ㄥ甫鍏ワ紝鏃犻渶濉啓',
+    },
+    {
+      fieldName: 'title',
+      label: '璇鹃鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ棰樺悕绉�',
+        maxlength: 255,
+        disabled: readonly,
+      },
+      rules: 'required',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'memberId',
+      label: '鐢虫姤浜猴紙浼氬憳锛�',
+      component: markRaw(ResearchMemberSelect),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐢虫姤浜猴紝閫夋嫨鍚庤嚜鍔ㄥ甫鍏ュ崟浣嶃�佽亴绉颁笌鑱旂郴鏂瑰紡',
+        disabled: readonly,
+        onChange: (member: Record<string, unknown> | undefined) => {
+          if (!formApi) {
+            return;
+          }
+          formApi.setFieldValue('applicantName', member?.name ?? '');
+          formApi.setFieldValue('workUnit', member?.workUnit ?? '');
+          formApi.setFieldValue('jobTitle', member?.title ?? '');
+          formApi.setFieldValue('contactPhone', member?.contactPhone ?? '');
+          formApi.setFieldValue('contactEmail', member?.contactEmail ?? '');
+        },
+      },
+      help: '閫夋嫨浼氬憳鍚庤嚜鍔ㄥ甫鍏ュ鍚�/鍗曚綅/鑱岀О/鑱旂郴鐢佃瘽/閭锛屼粛鍙墜宸ヨ皟鏁�',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'applicantName',
+      label: '鐢虫姤浜哄鍚�/鍗曚綅鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵鎶ヤ汉濮撳悕鎴栧崟浣嶅悕绉�',
+        maxlength: 64,
+        disabled: readonly,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'workUnit',
+      label: '宸ヤ綔鍗曚綅',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ伐浣滃崟浣�',
+        maxlength: 128,
+        disabled: readonly,
+      },
+    },
+    {
+      fieldName: 'jobTitle',
+      label: '鑱岀О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ亴绉�',
+        maxlength: 64,
+        disabled: readonly,
+      },
+    },
+    {
+      fieldName: 'contactPhone',
+      label: '鑱旂郴鐢佃瘽',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ仈绯荤數璇�',
+        maxlength: 20,
+        disabled: readonly,
+      },
+    },
+    {
+      fieldName: 'contactEmail',
+      label: '鑱旂郴閭',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ仈绯婚偖绠�',
+        maxlength: 128,
+        disabled: readonly,
+      },
+    },
+    {
+      fieldName: 'budgetAmount',
+      label: '鐢宠缁忚垂锛堝厓锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        placeholder: '璇疯緭鍏ョ敵璇风粡璐�',
+        precision: 2,
+        min: 0,
+        formatter: erpPriceInputFormatter,
+        disabled: readonly,
+      },
+    },
+    {
+      fieldName: 'teamMembers',
+      label: '璇鹃缁勬垚鍛�',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '姣忚涓�鏉★紝鏍煎紡锛氬鍚峾鍗曚綅|鑱岀О\n渚嬪锛氭潕鍥泑XX澶у|鍓暀鎺�',
+        autoSize: { minRows: 3, maxRows: 6 },
+        disabled: readonly,
+      },
+      help: '姣忚涓�鏉★紝鐢ㄧ珫绾垮垎闅旓細濮撳悕|鍗曚綅|鑱岀О',
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'planContent',
+      label: '鐢虫姤涔︽鏂�',
+      component: 'Textarea',
+      componentProps: {
+        placeholder:
+          '璇峰~鍐欑爺绌惰儗鏅笌鎰忎箟銆佺爺绌剁洰鏍囥�佺爺绌舵柟妗堜笌鎶�鏈矾绾裤�侀鏈熸垚鏋滀笌鏁堢泭绛夊唴瀹�',
+        autoSize: { minRows: 8, maxRows: 16 },
+        disabled: readonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: readonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '鐢虫姤涔﹂檮浠�',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        showDescription: !readonly,
+        disabled: readonly,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '鐢虫姤缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵鎶ョ紪鍙�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '璇鹃鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ棰樺悕绉�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'category',
+      label: '璇鹃绫诲埆',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨璇鹃绫诲埆',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_APPLICATION_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'createTime',
+      label: '鍒涘缓鏃堕棿',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鍒楅厤缃� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '鐢虫姤缂栧彿',
+      width: 180,
+      fixed: 'left',
+    },
+    {
+      field: 'title',
+      title: '璇鹃鍚嶇О',
+      minWidth: 220,
+    },
+    {
+      field: 'guidelineTitle',
+      title: '鎵�灞炶棰樻寚鍗�',
+      minWidth: 180,
+    },
+    {
+      field: 'category',
+      title: '璇鹃绫诲埆',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY },
+      },
+    },
+    {
+      field: 'applicantName',
+      title: '鐢虫姤浜�',
+      minWidth: 120,
+    },
+    {
+      field: 'workUnit',
+      title: '宸ヤ綔鍗曚綅',
+      minWidth: 160,
+    },
+    {
+      field: 'budgetAmount',
+      title: '鐢宠缁忚垂锛堝厓锛�',
+      minWidth: 130,
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'reviewAvgScore',
+      title: '璇勫鍧囧垎',
+      minWidth: 100,
+      formatter: ({ cellValue }) =>
+        cellValue === null || cellValue === undefined
+          ? '-'
+          : Number(cellValue).toFixed(2),
+    },
+    {
+      field: 'reviewPass',
+      title: '璇勫缁撹',
+      minWidth: 110,
+      slots: { default: 'reviewPass' },
+    },
+    {
+      field: 'researchNo',
+      title: '绔嬮」缂栧彿',
+      width: 180,
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 95,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.RESEARCH_APPLICATION_STATUS },
+      },
+    },
+    {
+      field: 'hasAttachment',
+      title: '闄勪欢',
+      minWidth: 80,
+      slots: { default: 'hasAttachment' },
+    },
+    {
+      field: 'creatorName',
+      title: '鍒涘缓浜�',
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 330,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
+
+/** 鏁板�肩┖鍊煎崰浣嶏紙VXE 鍒� formatter锛� */
+function dashIfEmpty({ cellValue }: { cellValue: unknown }) {
+  return cellValue === null || cellValue === undefined ? '-' : String(cellValue);
+}
+
+/** 涓撳璇勫鏄庣粏鍒楋紙璇勫闈㈡澘 / 瀹℃壒璇︽儏鍏辩敤锛� */
+export function useReviewColumns(
+  showActions = false,
+): VxeTableGridOptions['columns'] {
+  const columns: VxeTableGridOptions['columns'] = [
+    { type: 'seq', title: '搴忓彿', width: 55, fixed: 'left' },
+    { field: 'evaluatorName', title: '涓撳濮撳悕', minWidth: 100 },
+    { field: 'evaluatorUnit', title: '涓撳鍗曚綅', minWidth: 160 },
+    { field: 'evaluatorTitle', title: '涓撳鑱岀О', minWidth: 100 },
+    {
+      field: 'topicScore',
+      title: '閫夐浠峰�硷紙0.3锛�',
+      minWidth: 120,
+      formatter: dashIfEmpty,
+    },
+    {
+      field: 'planScore',
+      title: '鏂规鍙鎬э紙0.3锛�',
+      minWidth: 130,
+      formatter: dashIfEmpty,
+    },
+    {
+      field: 'teamScore',
+      title: '鍥㈤槦鍩虹锛�0.2锛�',
+      minWidth: 120,
+      formatter: dashIfEmpty,
+    },
+    {
+      field: 'resultScore',
+      title: '棰勬湡鎴愭灉锛�0.2锛�',
+      minWidth: 120,
+      formatter: dashIfEmpty,
+    },
+    {
+      field: 'compositeScore',
+      title: '鍔犳潈鍚堣',
+      minWidth: 110,
+      formatter: dashIfEmpty,
+    },
+    {
+      field: 'ranking',
+      title: '鎺掑悕',
+      width: 70,
+      formatter: dashIfEmpty,
+    },
+    {
+      field: 'conclusion',
+      title: '璇勫缁撹',
+      minWidth: 110,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.RESEARCH_REVIEW_CONCLUSION },
+      },
+    },
+    { field: 'opinion', title: '璇勫鎰忚', minWidth: 200 },
+    { field: 'creatorName', title: '褰曞叆浜�', minWidth: 100 },
+    {
+      field: 'createTime',
+      title: '褰曞叆鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+  ];
+  if (showActions) {
+    columns.push({
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    });
+  }
+  return columns;
+}
+
+/** 缁忚垂鎷ㄤ粯璁板綍鍒楋紙鎷ㄤ粯闈㈡澘 / 瀹℃壒璇︽儏鍏辩敤锛� */
+export function useFundColumns(
+  showActions = false,
+): VxeTableGridOptions['columns'] {
+  const columns: VxeTableGridOptions['columns'] = [
+    { type: 'seq', title: '搴忓彿', width: 55 },
+    { field: 'batchName', title: '鎷ㄤ粯鎵规', minWidth: 130 },
+    {
+      field: 'amount',
+      title: '鎷ㄤ粯閲戦锛堝厓锛�',
+      minWidth: 130,
+      formatter: 'formatAmount2',
+    },
+    {
+      field: 'payDate',
+      title: '鎷ㄤ粯鏃ユ湡',
+      width: 120,
+      formatter: dashIfEmpty,
+    },
+    { field: 'payee', title: '鏀舵鍗曚綅/璐︽埛鍚�', minWidth: 180 },
+    {
+      field: 'status',
+      title: '鎷ㄤ粯鐘舵��',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.RESEARCH_FUND_STATUS },
+      },
+    },
+    {
+      field: 'hasAttachment',
+      title: '鎷ㄤ粯鍑瘉',
+      minWidth: 100,
+      slots: { default: 'hasAttachment' },
+    },
+    { field: 'creatorName', title: '鐧昏浜�', minWidth: 100 },
+    {
+      field: 'createTime',
+      title: '鐧昏鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+  ];
+  if (showActions) {
+    columns.push({
+      title: '鎿嶄綔',
+      width: 130,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    });
+  }
+  return columns;
+}
diff --git a/src/views/research/application/detail/approve/index.vue b/src/views/research/application/detail/approve/index.vue
new file mode 100644
index 0000000..59f0539
--- /dev/null
+++ b/src/views/research/application/detail/approve/index.vue
@@ -0,0 +1,279 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ResearchApplicationApi } from '#/api/research/application';
+import type { ResearchFundRecordApi } from '#/api/research/fund';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Spin, Tag } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getApplication, getTopicReviewList } from '#/api/research/application';
+import { getFundRecordList } from '#/api/research/fund';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+import {
+  FundStatus,
+  useFundColumns,
+  useReviewColumns,
+} from '../../data';
+
+defineOptions({ name: 'ResearchApplicationApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ResearchApplicationApi.Application>();
+const reviews = ref<ResearchApplicationApi.TopicReview[]>([]);
+const fundRecords = ref<ResearchFundRecordApi.FundRecord[]>([]);
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+  return val === 0 || val ? String(val) : '-';
+}
+
+/** 閲戦瀛楁绌哄�煎崰浣� */
+function amountRender(val: string | number | undefined) {
+  return val === null || val === undefined || val === ''
+    ? '-'
+    : (erpPriceInputFormatter(val) as string);
+}
+
+const [Descriptions] = useDescription({
+  bordered: true,
+  column: 2,
+  schema: [
+    { label: '鐢虫姤缂栧彿', field: 'no', render: textRender },
+    { label: '鐢虫姤浜哄鍚�/鍗曚綅鍚嶇О', field: 'applicantName', render: textRender },
+    { label: '璇鹃鍚嶇О', field: 'title', render: textRender },
+    { label: '鎵�灞炶棰樻寚鍗�', field: 'guidelineTitle', render: textRender },
+    {
+      label: '璇鹃绫诲埆',
+      field: 'category',
+      render: (val) =>
+        h(DictTag, {
+          type: DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY,
+          value: val,
+        }),
+    },
+    {
+      label: '鐘舵��',
+      field: 'status',
+      render: (val) =>
+        h(DictTag, {
+          type: DICT_TYPE.RESEARCH_APPLICATION_STATUS,
+          value: val,
+        }),
+    },
+    { label: '宸ヤ綔鍗曚綅', field: 'workUnit', render: textRender },
+    { label: '鑱岀О', field: 'jobTitle', render: textRender },
+    { label: '鑱旂郴鐢佃瘽', field: 'contactPhone', render: textRender },
+    { label: '鑱旂郴閭', field: 'contactEmail', render: textRender },
+    {
+      label: '鐢宠缁忚垂锛堝厓锛�',
+      field: 'budgetAmount',
+      render: amountRender,
+    },
+    {
+      label: '璇勫鍔犳潈鍧囧垎',
+      field: 'reviewAvgScore',
+      render: (val) =>
+        val === null || val === undefined
+          ? '鏈眹鎬�'
+          : Number(val).toFixed(2),
+    },
+    {
+      label: '璇勫缁撹',
+      field: 'reviewPass',
+      render: (val) =>
+        val === null || val === undefined
+          ? '鏈眹鎬�'
+          : h(
+              Tag,
+              { color: val === 1 ? 'success' : 'error' },
+              () => (val === 1 ? '鎺ㄨ崘绔嬮」' : '涓嶆帹鑽愮珛椤�'),
+            ),
+    },
+    { label: '绔嬮」缂栧彿', field: 'researchNo', render: textRender },
+    {
+      label: '绔嬮」鏃堕棿',
+      field: 'approveTime',
+      render: (val) => (val ? (formatDateTime(val) as string) : '-'),
+    },
+    { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+    {
+      label: '鍒涘缓鏃堕棿',
+      field: 'createTime',
+      render: (val) => (val ? (formatDateTime(val) as string) : '-'),
+    },
+    { label: '澶囨敞', field: 'remark', render: textRender },
+  ] as DescriptionItemSchema[],
+});
+
+const [ReviewGrid, reviewGridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useReviewColumns(false),
+    data: [],
+    minHeight: 200,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  } as VxeTableGridOptions<ResearchApplicationApi.TopicReview>,
+});
+
+const [FundGrid, fundGridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useFundColumns(false),
+    data: [],
+    minHeight: 200,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  } as VxeTableGridOptions<ResearchFundRecordApi.FundRecord>,
+});
+
+onMounted(async () => {
+  try {
+    loading.value = true;
+    const applicationId = Number(props.id);
+    const [application, reviewList, recordList] = await Promise.all([
+      getApplication(applicationId),
+      getTopicReviewList(applicationId),
+      getFundRecordList(applicationId),
+    ]);
+    formData.value = application;
+    reviews.value = reviewList;
+    fundRecords.value = recordList;
+    reviewGridApi.setGridOptions({ data: reviewList });
+    fundGridApi.setGridOptions({ data: recordList });
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<template>
+  <ContentWrap class="m-2">
+    <Spin :spinning="loading" tip="鍔犺浇涓�...">
+      <div class="space-y-4">
+        <!-- 鐢虫姤涔﹀熀鏈俊鎭� -->
+        <div class="rounded-lg border border-border bg-background p-4">
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">璇鹃鐢虫姤涔﹀熀鏈俊鎭�</span>
+          </div>
+          <Descriptions :data="formData" />
+        </div>
+
+        <!-- 璇鹃缁勬垚鍛� -->
+        <div
+          v-if="formData?.teamMembers"
+          class="rounded-lg border border-border bg-background p-4"
+        >
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">璇鹃缁勬垚鍛�</span>
+          </div>
+          <pre class="whitespace-pre-wrap break-all font-sans">{{
+            formData.teamMembers
+          }}</pre>
+        </div>
+
+        <!-- 鐢虫姤涔︽鏂� -->
+        <div
+          v-if="formData?.planContent"
+          class="rounded-lg border border-border bg-background p-4"
+        >
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">鐢虫姤涔︽鏂�</span>
+          </div>
+          <pre class="whitespace-pre-wrap break-all font-sans">{{
+            formData.planContent
+          }}</pre>
+        </div>
+
+        <!-- 涓撳璇勫鏄庣粏 -->
+        <div class="rounded-lg border border-border bg-background p-4">
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">涓撳璇勫鏄庣粏</span>
+            <span v-if="reviews.length" class="text-gray-500">
+              锛堝叡 {{ reviews.length }} 浣嶄笓瀹讹級
+            </span>
+          </div>
+          <ReviewGrid v-if="reviews.length" />
+          <p v-else class="text-gray-500">鏆傛棤涓撳璇勫璁板綍</p>
+        </div>
+
+        <!-- 缁忚垂鎷ㄤ粯璁板綍 -->
+        <div class="rounded-lg border border-border bg-background p-4">
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">缁忚垂鎷ㄤ粯璁板綍</span>
+          </div>
+          <FundGrid v-if="fundRecords.length">
+            <template #hasAttachment="{ row }">
+              <Tag :color="row.hasAttachment ? 'success' : 'warning'">
+                {{
+                  row.hasAttachment
+                    ? '宸蹭笂浼�'
+                    : row.status === FundStatus.PAID
+                      ? '缂哄け'
+                      : '涓嶉渶瑕�'
+                }}
+              </Tag>
+            </template>
+          </FundGrid>
+          <p v-else class="text-gray-500">鏆傛棤缁忚垂鎷ㄤ粯璁板綍</p>
+        </div>
+
+        <!-- 鐢虫姤涔﹂檮浠� -->
+        <div
+          v-if="formData?.attachmentList?.length"
+          class="rounded-lg border border-border bg-background p-4"
+        >
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">鐢虫姤涔﹂檮浠�</span>
+          </div>
+          <div class="flex flex-wrap gap-3">
+            <a
+              v-for="file in formData.attachmentList"
+              :key="file.id"
+              :href="file.previewURL || file.url"
+              target="_blank"
+              class="text-blue-500 hover:underline"
+            >
+              {{ file.name || `闄勪欢-${file.id}` }}
+            </a>
+          </div>
+        </div>
+      </div>
+    </Spin>
+  </ContentWrap>
+</template>
diff --git a/src/views/research/application/index.vue b/src/views/research/application/index.vue
new file mode 100644
index 0000000..b66ce43
--- /dev/null
+++ b/src/views/research/application/index.vue
@@ -0,0 +1,311 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ResearchApplicationApi } from '#/api/research/application';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteApplication,
+  getApplicationApproveProcessList,
+  getApplicationPage,
+  publishApplicationNotice,
+} from '#/api/research/application';
+import { $t } from '#/locales';
+
+import {
+  ApplicationStatus,
+  useGridColumns,
+  useGridFormSchema,
+} from './data';
+import FundPanel from './modules/fund-panel.vue';
+import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+import ReviewPanel from './modules/review-panel.vue';
+
+/** 璇鹃鐢虫姤涔﹀垪琛� */
+defineOptions({ name: 'ResearchApplication' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
+  destroyOnClose: true,
+});
+
+const [ReviewModal, reviewModalApi] = useVbenModal({
+  connectedComponent: ReviewPanel,
+  destroyOnClose: true,
+});
+
+const [FundModal, fundModalApi] = useVbenModal({
+  connectedComponent: FundPanel,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鏂板鐢虫姤涔� */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鐢虫姤涔︼紙浠呰崏绋裤�佸凡椹冲洖鍙紪杈戯級 */
+function handleEdit(row: ResearchApplicationApi.Application) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅鐢虫姤涔﹁鎯� */
+function handleDetail(row: ResearchApplicationApi.Application) {
+  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 Promise.all(ids.map((id) => deleteApplication(id)));
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛紝娴佺▼ key 涓嶅啓姝� */
+async function handleSubmit(row: ResearchApplicationApi.Application) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getApplicationApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning(
+        '鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堝埌 BPM 娴佺▼璁捐鍣ㄥ彂甯冭棰樼珛椤瑰鎵规祦绋�',
+      );
+      return;
+    }
+    processModalApi.setData({ id: row.id, processList }).open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 涓撳璇勫闈㈡澘 */
+function handleReview(row: ResearchApplicationApi.Application) {
+  reviewModalApi.setData({ id: row.id }).open();
+}
+
+/** 缁忚垂鎷ㄤ粯闈㈡澘 */
+function handleFund(row: ResearchApplicationApi.Application) {
+  fundModalApi.setData({ id: row.id }).open();
+}
+
+/** 鍙戝竷绔嬮」閫氱煡 */
+async function handlePublishNotice(row: ResearchApplicationApi.Application) {
+  const hideLoading = message.loading({ content: '鍙戝竷涓�...', duration: 0 });
+  try {
+    await publishApplicationNotice(row.id!);
+    message.success('绔嬮」閫氱煡宸插彂甯冨埌绯荤粺鍏憡锛屽彲鍦ㄥ叕鍛婃ā鍧楁煡鐪�');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒杩涘害 */
+function handleProcessDetail(row: ResearchApplicationApi.Application) {
+  if (!row.processInstanceId) {
+    message.warning('璇ョ敵鎶ヤ功灏氭湭鎻愪氦瀹℃壒');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: ResearchApplicationApi.Application[];
+}) {
+  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 getApplicationPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ResearchApplicationApi.Application>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
+    <ReviewModal @success="handleRefresh" />
+    <FundModal @success="handleRefresh" />
+    <Grid table-title="璇鹃鐢虫姤涔﹀垪琛�">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['璇鹃鐢虫姤涔�']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['research:application:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['research:application:delete'],
+              popConfirm: {
+                title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛燂紙浠呰崏绋跨姸鎬佸彲鍒犻櫎锛�',
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #reviewPass="{ row }">
+        <Tag v-if="row.reviewPass === 1" color="success">鎺ㄨ崘绔嬮」</Tag>
+        <Tag v-else-if="row.reviewPass === 0" color="error">涓嶆帹鑽愮珛椤�</Tag>
+        <span v-else>-</span>
+      </template>
+      <template #hasAttachment="{ row }">
+        <Tag :color="row.hasAttachment ? 'success' : 'default'">
+          {{ row.hasAttachment ? '鏈�' : '鏃�' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:application:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['research:application:update'],
+              ifShow: () =>
+                row.status === ApplicationStatus.DRAFT ||
+                row.status === ApplicationStatus.REJECTED,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['research:application:submit'],
+              ifShow: () =>
+                row.status === ApplicationStatus.DRAFT ||
+                row.status === ApplicationStatus.REJECTED,
+              onClick: handleSubmit.bind(null, row),
+            },
+            {
+              label: '涓撳璇勫',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:review:query'],
+              onClick: handleReview.bind(null, row),
+            },
+            {
+              label: '缁忚垂鎷ㄤ粯',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:fund:query'],
+              onClick: handleFund.bind(null, row),
+            },
+            {
+              label: '鍙戝竷绔嬮」閫氱煡',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:application:notice'],
+              ifShow: () =>
+                row.status === ApplicationStatus.APPROVED && !row.noticeId,
+              onClick: handlePublishNotice.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:application:query'],
+              ifShow: () => !!row.processInstanceId,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['research:application:delete'],
+              ifShow: () => row.status === ApplicationStatus.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/research/application/modules/form.vue b/src/views/research/application/modules/form.vue
new file mode 100644
index 0000000..a7e2a95
--- /dev/null
+++ b/src/views/research/application/modules/form.vue
@@ -0,0 +1,164 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { ResearchApplicationApi } from '#/api/research/application';
+import type { ResearchGuidelineApi } from '#/api/research/guideline';
+
+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 {
+  createApplication,
+  getApplication,
+  updateApplication,
+} from '#/api/research/application';
+import {
+  getGuideline,
+  getPublishedGuidelineList,
+} from '#/api/research/guideline';
+
+import { useFormSchema } from '../data';
+
+defineOptions({ name: 'ResearchApplicationForm' });
+
+const emit = defineEmits(['success']);
+
+const formType = ref<FormType>('create');
+/** 渚涖�屾墍灞炶棰樻寚鍗椼�嶄笅鎷変笌绫诲埆甯﹀嚭浣跨敤 */
+const guidelines = ref<ResearchGuidelineApi.Guideline[]>([]);
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['璇鹃鐢虫姤涔�']);
+  }
+  if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['璇鹃鐢虫姤涔�']);
+  }
+  return '璇鹃鐢虫姤涔﹁鎯�';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 130,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: [],
+  showDefaultActions: false,
+});
+
+/** 鍙彁浜ゅ悗绔� SaveReqVO 鎺ュ彈鐨勫瓧娈碉紝閬垮厤鍥炰紶 category/reviewAvgScore 绛夋淳鐢熷瓧娈� */
+async function buildSubmitData() {
+  const values = (await formApi.getValues()) as ResearchApplicationApi.Application;
+  return {
+    id: values.id,
+    guidelineId: values.guidelineId,
+    title: values.title,
+    memberId: values.memberId,
+    applicantName: values.applicantName,
+    workUnit: values.workUnit,
+    jobTitle: values.jobTitle,
+    contactPhone: values.contactPhone,
+    contactEmail: values.contactEmail,
+    teamMembers: values.teamMembers,
+    budgetAmount: values.budgetAmount,
+    planContent: values.planContent,
+    remark: values.remark,
+    // 闄勪欢涓哄叏閲忔浛鎹㈣涔夛細娓呯┖鍏ㄩ儴闄勪欢鏃跺繀椤绘樉寮忎紶绌烘暟缁勶紝
+    // 浼� null 鍚庣浼氳涓恒�屼笉鍔ㄩ檮浠躲�嶏紝瀵艰嚧鍒犻櫎鎿嶄綔涓嶇敓鏁�
+    blobIds: values.blobIds ?? [],
+  } as ResearchApplicationApi.Application;
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = await buildSubmitData();
+    try {
+      await (formType.value === 'create'
+        ? createApplication(data)
+        : updateApplication(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      guidelines.value = [];
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    modalApi.lock();
+    try {
+      if (data?.id) {
+        const application = await getApplication(data.id);
+        guidelines.value = await resolveGuidelineOptions(application);
+        formApi.setState({
+          schema: useFormSchema(formType.value, formApi, guidelines.value),
+        });
+        formApi.setDisabled(formType.value === 'detail');
+        // 宸叉湁闄勪欢杞垚涓婁紶缁勪欢鍙瘑鍒殑鏂囦欢瀵硅薄锛屾彁浜ゆ椂鎸� valueKey=id 鎻愬彇涓� blobId
+        const blobIds = (application.attachmentList ?? []).map((item) => ({
+          uid: String(item.id),
+          name: item.name || '',
+          url: item.url || '',
+          status: 'done',
+          id: item.id,
+        }));
+        await formApi.setValues({ ...application, blobIds }, false);
+      } else {
+        guidelines.value = await getPublishedGuidelineList();
+        formApi.setState({
+          schema: useFormSchema(formType.value, formApi, guidelines.value),
+        });
+        formApi.setDisabled(false);
+        await formApi.resetForm();
+      }
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+
+/**
+ * 缂栬緫/璇︽儏鏃舵寚鍗楀彲鑳藉凡鎴锛堜笉鍦ㄣ�屽凡鍙戝竷銆嶅垪琛ㄩ噷锛夛紝
+ * 闇�瑕佸崟鐙彇鍥炲綋鍓嶆墍灞炴寚鍗楄ˉ杩涢�夐」锛屽惁鍒欎笅鎷夋樉绀轰负绌恒��
+ */
+async function resolveGuidelineOptions(
+  application: ResearchApplicationApi.Application,
+): Promise<ResearchGuidelineApi.Guideline[]> {
+  const published = await getPublishedGuidelineList();
+  const currentId = application.guidelineId;
+  if (!currentId || published.some((item) => item.id === currentId)) {
+    return published;
+  }
+  const current = await getGuideline(currentId);
+  return [...published, current];
+}
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :close-on-click-modal="false"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3" />
+  </Modal>
+</template>
diff --git a/src/views/research/application/modules/fund-form.vue b/src/views/research/application/modules/fund-form.vue
new file mode 100644
index 0000000..33b2be5
--- /dev/null
+++ b/src/views/research/application/modules/fund-form.vue
@@ -0,0 +1,233 @@
+<script lang="ts" setup>
+import type { ResearchFundRecordApi } from '#/api/research/fund';
+
+import { computed, ref } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createFundRecord, updateFundRecord } from '#/api/research/fund';
+
+import { FundStatus } from '../data';
+
+defineOptions({ name: 'ResearchFundRecordForm' });
+
+const emit = defineEmits(['success']);
+
+const isEdit = ref(false);
+/** 鐢宠缁忚垂鍑忓幓宸茬櫥璁版嫧浠橀噾棰濆悗鐨勫彲鐢ㄤ綑棰� */
+const remainder = ref<number>();
+
+const alertMessage = computed(() => {
+  if (remainder.value === undefined) {
+    return '';
+  }
+  return `鏈棰樼敵璇风粡璐瑰彲鐢ㄤ綑棰濓細${remainder.value.toFixed(2)} 鍏冿紝鎷ㄦ閲戦绱涓嶅緱瓒呰繃鐢宠缁忚垂`;
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 130,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'applicationId',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'batchName',
+      label: '鎷ㄤ粯鎵规',
+      component: 'Input',
+      componentProps: {
+        placeholder: '濡傦細绗竴鏈�',
+        maxlength: 64,
+      },
+    },
+    {
+      fieldName: 'amount',
+      label: '鎷ㄤ粯閲戦锛堝厓锛�',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        placeholder: '璇疯緭鍏ユ湰娆℃嫧浠橀噾棰�',
+        precision: 2,
+        min: 0,
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'payDate',
+      label: '鎷ㄤ粯鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鎷ㄤ粯鏃ユ湡',
+        // 鍚庣涓� LocalDate锛屽彧鑳戒紶 yyyy-MM-dd
+        valueFormat: 'YYYY-MM-DD',
+        style: { width: '100%' },
+      },
+    },
+    {
+      fieldName: 'payee',
+      label: '鏀舵鍗曚綅/璐︽埛鍚�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ敹娆惧崟浣嶆垨璐︽埛鍚�',
+        maxlength: 128,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鎷ㄤ粯鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_FUND_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鎷ㄤ粯鐘舵��',
+      },
+      rules: 'selectRequired',
+      help: '閫夋嫨銆屽凡鎷ㄤ粯銆嶆椂蹇呴』涓婁紶鎷ㄤ粯鍑瘉闄勪欢',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 2, maxRows: 3 },
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '鎷ㄤ粯鍑瘉',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        showDescription: true,
+      },
+      formItemClass: 'col-span-2',
+    },
+  ],
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    const values =
+      (await formApi.getValues()) as ResearchFundRecordApi.FundRecord;
+    // 銆屽凡鎷ㄤ粯銆嶅繀椤绘湁鍑瘉锛屽厛鍦ㄥ墠绔嫤涓�閬擄紝閬垮厤鐢ㄦ埛鐧界瓑涓�娆¤姹�
+    if (
+      values.status === FundStatus.PAID &&
+      (!values.blobIds || values.blobIds.length === 0)
+    ) {
+      message.warning('鎷ㄤ粯鐘舵�佷负銆屽凡鎷ㄤ粯銆嶆椂锛岃鍏堜笂浼犳嫧浠樺嚟璇侀檮浠�');
+      return;
+    }
+    modalApi.lock();
+    // 闄勪欢涓哄叏閲忔浛鎹㈣涔夛細娓呯┖鍏ㄩ儴鍑瘉鏃跺繀椤绘樉寮忎紶绌烘暟缁�
+    const data = {
+      id: values.id,
+      applicationId: values.applicationId,
+      batchName: values.batchName,
+      amount: values.amount,
+      payDate: values.payDate,
+      payee: values.payee,
+      status: values.status,
+      remark: values.remark,
+      blobIds: values.blobIds ?? [],
+    } as ResearchFundRecordApi.FundRecord;
+    try {
+      await (data.id ? updateFundRecord(data) : createFundRecord(data));
+      await modalApi.close();
+      emit('success');
+      message.success('淇濆瓨鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      isEdit.value = false;
+      remainder.value = undefined;
+      return;
+    }
+    const data = modalApi.getData<{
+      applicationId: number;
+      remainder?: number;
+      record?: ResearchFundRecordApi.FundRecord;
+    }>();
+    isEdit.value = !!data.record;
+    remainder.value = data.remainder;
+    await formApi.resetForm();
+    if (data.record) {
+      const blobIds = (data.record.attachmentList ?? []).map((item) => ({
+        uid: String(item.id),
+        name: item.name || '',
+        url: item.url || '',
+        status: 'done',
+        id: item.id,
+      }));
+      await formApi.setValues(
+        {
+          ...data.record,
+          applicationId: data.applicationId,
+          blobIds,
+        },
+        false,
+      );
+    } else {
+      await formApi.setValues(
+        {
+          applicationId: data.applicationId,
+          status: FundStatus.PENDING,
+        },
+        false,
+      );
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="isEdit ? '缂栬緫鎷ㄦ璁板綍' : '鐧昏鎷ㄦ璁板綍'"
+    class="w-1/2"
+    :close-on-click-modal="false"
+  >
+    <Alert
+      v-if="alertMessage"
+      class="mx-3 mb-3"
+      type="info"
+      show-icon
+      :message="alertMessage"
+    />
+    <Form class="mx-3" />
+  </Modal>
+</template>
diff --git a/src/views/research/application/modules/fund-panel.vue b/src/views/research/application/modules/fund-panel.vue
new file mode 100644
index 0000000..f3900fe
--- /dev/null
+++ b/src/views/research/application/modules/fund-panel.vue
@@ -0,0 +1,236 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ResearchApplicationApi } from '#/api/research/application';
+import type { ResearchFundRecordApi } from '#/api/research/fund';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Alert, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getApplication } from '#/api/research/application';
+import { deleteFundRecord, getFundRecordList } from '#/api/research/fund';
+
+import { ApplicationStatus, FundStatus, useFundColumns } from '../data';
+import FundForm from './fund-form.vue';
+
+defineOptions({ name: 'ResearchFundPanel' });
+
+const emit = defineEmits(['success']);
+
+const applicationId = ref<number>();
+const application = ref<ResearchApplicationApi.Application>();
+const records = ref<ResearchFundRecordApi.FundRecord[]>([]);
+
+/** 浠呫�屽凡绔嬮」銆嶇殑鐢虫姤鍗曞彲鐧昏鎷ㄦ */
+const editable = computed(
+  () => application.value?.status === ApplicationStatus.APPROVED,
+);
+
+/** 宸茬櫥璁版嫧浠橀噾棰濆悎璁� */
+const registeredAmount = computed(() =>
+  records.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0),
+);
+
+/** 鐢宠缁忚垂鍙敤浣欓锛堣礋鏁拌〃绀哄凡瓒呴鐧昏锛� */
+const remainder = computed(
+  () => (Number(application.value?.budgetAmount) || 0) - registeredAmount.value,
+);
+
+const [FundFormModal, fundFormModalApi] = useVbenModal({
+  connectedComponent: FundForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useFundColumns(true),
+    data: [],
+    minHeight: 280,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  } as VxeTableGridOptions<ResearchFundRecordApi.FundRecord>,
+});
+
+async function refresh() {
+  if (!applicationId.value) {
+    return;
+  }
+  const [recordList, applicationDetail] = await Promise.all([
+    getFundRecordList(applicationId.value),
+    getApplication(applicationId.value),
+  ]);
+  records.value = recordList;
+  application.value = applicationDetail;
+  gridApi.setGridOptions({ data: recordList });
+}
+
+/** 鎷ㄦ璁板綍澧炲垹鏀瑰悗锛氭湰闈㈡澘閲嶈浇锛屽悓鏃堕�氱煡鍒楄〃椤靛埛鏂� */
+async function handleChanged() {
+  await refresh();
+  emit('success');
+}
+
+function handleCreate() {
+  fundFormModalApi
+    .setData({ applicationId: applicationId.value!, remainder: remainder.value })
+    .open();
+}
+
+function handleEdit(row: ResearchFundRecordApi.FundRecord) {
+  fundFormModalApi
+    .setData({
+      applicationId: applicationId.value!,
+      // 缂栬緫鏃舵妸褰撳墠璁板綍鑷韩閲戦鍔犲洖锛岄伩鍏嶇敤鎴风湅鍒般�屼綑棰濄�嶅亸灏忎骇鐢熻瑙�
+      remainder: remainder.value + (Number(row.amount) || 0),
+      record: row,
+    })
+    .open();
+}
+
+async function handleDelete(row: ResearchFundRecordApi.FundRecord) {
+  const hideLoading = message.loading({ content: '鍒犻櫎涓�...', duration: 0 });
+  try {
+    await deleteFundRecord(row.id!);
+    message.success('鍒犻櫎鎴愬姛');
+    await handleChanged();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      applicationId.value = undefined;
+      application.value = undefined;
+      records.value = [];
+      return;
+    }
+    const data = modalApi.getData<{ id: number }>();
+    applicationId.value = data.id;
+    modalApi.lock();
+    try {
+      const [recordList, applicationDetail] = await Promise.all([
+        getFundRecordList(data.id),
+        getApplication(data.id),
+      ]);
+      records.value = recordList;
+      application.value = applicationDetail;
+      gridApi.setGridOptions({ data: recordList });
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="缁忚垂鎷ㄤ粯" class="w-4/5" :show-confirm-button="false">
+    <div class="space-y-3">
+      <Alert
+        v-if="!editable"
+        type="info"
+        show-icon
+        message="浠呫�屽凡绔嬮」銆嶇殑璇鹃鐢虫姤涔﹀彲浠ョ櫥璁扮粡璐规嫧浠樿褰�"
+      />
+      <div class="flex flex-wrap items-center gap-6 rounded border border-border p-3">
+        <div>
+          <span class="text-gray-500">璇鹃鍚嶇О锛�</span>
+          <span class="font-medium">{{ application?.title || '-' }}</span>
+        </div>
+        <div>
+          <span class="text-gray-500">绔嬮」缂栧彿锛�</span>
+          <span class="font-medium">{{ application?.researchNo || '鏈珛椤�' }}</span>
+        </div>
+        <div>
+          <span class="text-gray-500">鐢宠缁忚垂锛�</span>
+          <span class="font-medium">
+            {{ Number(application?.budgetAmount || 0).toFixed(2) }} 鍏�
+          </span>
+        </div>
+        <div>
+          <span class="text-gray-500">宸茬櫥璁版嫧浠橈細</span>
+          <span class="font-medium">{{ registeredAmount.toFixed(2) }} 鍏�</span>
+        </div>
+        <div>
+          <span class="text-gray-500">鍙敤浣欓锛�</span>
+          <span
+            class="font-medium"
+            :class="remainder < 0 ? 'text-red-500' : ''"
+          >
+            {{ remainder.toFixed(2) }} 鍏�
+          </span>
+        </div>
+      </div>
+
+      <div v-if="editable">
+        <TableAction
+          :actions="[
+            {
+              label: '鐧昏鎷ㄦ璁板綍',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['research:fund:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </div>
+
+      <Grid>
+        <template #hasAttachment="{ row }">
+          <Tag :color="row.hasAttachment ? 'success' : 'warning'">
+            {{
+              row.hasAttachment
+                ? '宸蹭笂浼�'
+                : row.status === FundStatus.PAID
+                  ? '缂哄け'
+                  : '涓嶉渶瑕�'
+            }}
+          </Tag>
+        </template>
+        <template #actions="{ row }">
+          <TableAction
+            v-if="editable"
+            :actions="[
+              {
+                label: '缂栬緫',
+                type: 'link',
+                icon: ACTION_ICON.EDIT,
+                auth: ['research:fund:update'],
+                ifShow: () => row.status === FundStatus.PENDING,
+                onClick: handleEdit.bind(null, row),
+              },
+              {
+                label: '鍒犻櫎',
+                type: 'link',
+                danger: true,
+                icon: ACTION_ICON.DELETE,
+                auth: ['research:fund:delete'],
+                ifShow: () => row.status === FundStatus.PENDING,
+                popConfirm: {
+                  title: `纭鍒犻櫎銆�${row.batchName || '鏈潯'}銆嶆嫧娆捐褰曞悧锛焋,
+                  confirm: handleDelete.bind(null, row),
+                },
+              },
+            ]"
+          />
+        </template>
+      </Grid>
+    </div>
+  </Modal>
+  <FundFormModal @success="handleChanged" />
+</template>
diff --git a/src/views/research/application/modules/process-select-modal.vue b/src/views/research/application/modules/process-select-modal.vue
new file mode 100644
index 0000000..faf7988
--- /dev/null
+++ b/src/views/research/application/modules/process-select-modal.vue
@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import type { ResearchApplicationApi } from '#/api/research/application';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitApplication } from '#/api/research/application';
+
+defineOptions({ name: 'ResearchApplicationProcessSelect' });
+
+const emit = defineEmits(['success']);
+
+const applicationId = ref<number>();
+const processList = ref<ResearchApplicationApi.ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+/** 娴佺▼ key 鍙兘鏉ヨ嚜鍚庣瀹炴椂鍒楄〃锛岀姝㈠墠绔啓姝� */
+const processOptions = computed(() =>
+  processList.value.map((item) => ({ label: item.name, value: item.key })),
+);
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!selectedProcessKey.value) {
+      message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+      return;
+    }
+    modalApi.lock();
+    try {
+      await submitApplication(applicationId.value!, selectedProcessKey.value);
+      await modalApi.close();
+      emit('success');
+      message.success('鎻愪氦鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      applicationId.value = undefined;
+      selectedProcessKey.value = '';
+      processList.value = [];
+      return;
+    }
+    const data = modalApi.getData<{
+      id: number;
+      processList: ResearchApplicationApi.ProcessDefinition[];
+    }>();
+    applicationId.value = data.id;
+    processList.value = data.processList || [];
+    // 榛樿閫夋嫨绗竴涓祦绋�
+    if (processList.value.length > 0) {
+      selectedProcessKey.value = processList.value[0]!.key;
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+    <div class="mb-4">
+      <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+      <Select
+        v-model:value="selectedProcessKey"
+        :options="processOptions"
+        style="width: 240px"
+        placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+      />
+    </div>
+    <p class="text-sm text-gray-500">
+      鎻愪氦鍚庝緷娆℃祦杞細绉樹功澶勫舰寮忓鏌� 鈫� 涓撳璇勫 鈫� 瀛︿細绔嬮」瀹℃壒
+    </p>
+  </Modal>
+</template>
diff --git a/src/views/research/application/modules/review-form.vue b/src/views/research/application/modules/review-form.vue
new file mode 100644
index 0000000..2e6f772
--- /dev/null
+++ b/src/views/research/application/modules/review-form.vue
@@ -0,0 +1,174 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '#/adapter/form';
+import type { ResearchApplicationApi } from '#/api/research/application';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createTopicReview,
+  updateTopicReview,
+} from '#/api/research/application';
+
+defineOptions({ name: 'ResearchTopicReviewForm' });
+
+const emit = defineEmits(['success']);
+
+/** 鍥涗釜璇勫垎缁村害涓庢潈閲嶏紙涓庡悗绔� ResearchTopicReviewServiceImpl 淇濇寔涓�鑷达級 */
+const SCORE_ITEMS = [
+  { fieldName: 'topicScore', label: '閫夐浠峰�间笌鎰忎箟', weight: '0.3' },
+  { fieldName: 'planScore', label: '鐮旂┒鏂规鍙鎬�', weight: '0.3' },
+  { fieldName: 'teamScore', label: '鍥㈤槦涓庣爺绌跺熀纭�', weight: '0.2' },
+  { fieldName: 'resultScore', label: '棰勬湡鎴愭灉涓庢晥鐩�', weight: '0.2' },
+] as const;
+
+const schema: VbenFormSchema[] = [
+  {
+    fieldName: 'id',
+    component: 'Input',
+    dependencies: {
+      triggerFields: [''],
+      show: () => false,
+    },
+  },
+  {
+    fieldName: 'applicationId',
+    component: 'Input',
+    dependencies: {
+      triggerFields: [''],
+      show: () => false,
+    },
+  },
+  {
+    fieldName: 'evaluatorName',
+    label: '涓撳濮撳悕',
+    component: 'Input',
+    componentProps: {
+      placeholder: '璇疯緭鍏ヤ笓瀹跺鍚�',
+      maxlength: 64,
+    },
+    rules: 'required',
+  },
+  {
+    fieldName: 'evaluatorUnit',
+    label: '涓撳鍗曚綅',
+    component: 'Input',
+    componentProps: {
+      placeholder: '璇疯緭鍏ヤ笓瀹舵墍鍦ㄥ崟浣�',
+      maxlength: 128,
+    },
+  },
+  {
+    fieldName: 'evaluatorTitle',
+    label: '涓撳鑱岀О',
+    component: 'Input',
+    componentProps: {
+      placeholder: '璇疯緭鍏ヤ笓瀹惰亴绉�',
+      maxlength: 64,
+    },
+  },
+  ...SCORE_ITEMS.map((item) => ({
+    fieldName: item.fieldName,
+    label: `${item.label}锛堟潈閲� ${item.weight}锛塦,
+    component: 'InputNumber' as const,
+    componentProps: {
+      class: '!w-full',
+      placeholder: '0 ~ 100',
+      min: 0,
+      max: 100,
+      precision: 2,
+      addonAfter: '鍒�',
+    },
+  })),
+  {
+    fieldName: 'conclusion',
+    label: '璇勫缁撹',
+    component: 'Select',
+    componentProps: {
+      options: getDictOptions(DICT_TYPE.RESEARCH_REVIEW_CONCLUSION, 'number'),
+      placeholder: '璇烽�夋嫨璇勫缁撹',
+      allowClear: true,
+    },
+    rules: 'selectRequired',
+  },
+  {
+    fieldName: 'opinion',
+    label: '璇勫鎰忚',
+    component: 'Textarea',
+    componentProps: {
+      placeholder: '璇疯緭鍏ュ璇ヨ棰樼殑璇勫鎰忚',
+      maxlength: 1000,
+      showCount: true,
+      autoSize: { minRows: 4, maxRows: 8 },
+    },
+    formItemClass: 'col-span-2',
+  },
+];
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 170,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema,
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data =
+      (await formApi.getValues()) as ResearchApplicationApi.TopicReview;
+    try {
+      await (data.id
+        ? updateTopicReview(data)
+        : createTopicReview(data));
+      await modalApi.close();
+      emit('success');
+      message.success('淇濆瓨鎴愬姛锛屽姞鏉冨悎璁′笌鎺掑悕宸茶嚜鍔ㄩ噸绠�');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{
+      applicationId: number;
+      review?: ResearchApplicationApi.TopicReview;
+    }>();
+    await formApi.resetForm();
+    if (data.review) {
+      await formApi.setValues(
+        { ...data.review, applicationId: data.applicationId },
+        false,
+      );
+    } else {
+      await formApi.setValues({ applicationId: data.applicationId }, false);
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="涓撳璇勫垎褰曞叆" class="w-1/2" :close-on-click-modal="false">
+    <Form class="mx-3" />
+    <p class="mx-3 text-sm text-gray-500">
+      鍔犳潈鍚堣 = 閫夐浠峰�济�0.3 + 鏂规鍙鎬�0.3 + 鍥㈤槦鍩虹脳0.2 + 棰勬湡鎴愭灉脳0.2锛岀敱绯荤粺鑷姩璁$畻
+    </p>
+  </Modal>
+</template>
diff --git a/src/views/research/application/modules/review-panel.vue b/src/views/research/application/modules/review-panel.vue
new file mode 100644
index 0000000..b0eb66d
--- /dev/null
+++ b/src/views/research/application/modules/review-panel.vue
@@ -0,0 +1,221 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ResearchApplicationApi } from '#/api/research/application';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Alert, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteTopicReview,
+  getApplication,
+  getTopicReviewList,
+} from '#/api/research/application';
+
+import { ApplicationStatus, useReviewColumns } from '../data';
+import ReviewForm from './review-form.vue';
+
+defineOptions({ name: 'ResearchTopicReviewPanel' });
+
+const emit = defineEmits(['success']);
+
+const applicationId = ref<number>();
+const application = ref<ResearchApplicationApi.Application>();
+const reviews = ref<ResearchApplicationApi.TopicReview[]>([]);
+
+/** 浠呫�屽鎵逛腑銆嶇殑鐢虫姤鍗曞彲澧炲垹鏀逛笓瀹惰瘎鍒� */
+const editable = computed(
+  () => application.value?.status === ApplicationStatus.APPROVING,
+);
+
+const STATUS_LABELS: Record<number, string> = {
+  [ApplicationStatus.DRAFT]: '鑽夌',
+  [ApplicationStatus.APPROVING]: '瀹℃壒涓�',
+  [ApplicationStatus.APPROVED]: '宸茬珛椤�',
+  [ApplicationStatus.REJECTED]: '宸查┏鍥�',
+};
+
+const statusLabel = computed(() =>
+  application.value?.status === undefined ||
+  application.value?.status === null
+    ? '鏈煡'
+    : (STATUS_LABELS[application.value.status] ?? '鏈煡'),
+);
+
+const [ReviewFormModal, reviewFormModalApi] = useVbenModal({
+  connectedComponent: ReviewForm,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  gridOptions: {
+    columns: useReviewColumns(true),
+    data: [],
+    minHeight: 300,
+    autoResize: true,
+    border: true,
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    pagerConfig: {
+      enabled: false,
+    },
+    toolbarConfig: {
+      enabled: false,
+    },
+  } as VxeTableGridOptions<ResearchApplicationApi.TopicReview>,
+});
+
+/** 鎷夊彇璇勫鏄庣粏骞跺悓姝ヤ富鍗曟眹鎬诲�硷紙鍚庣鍦ㄥ鍒犳敼鍚庡凡閲嶇畻鍧囧垎涓庣粨璁猴級 */
+async function refresh() {
+  if (!applicationId.value) {
+    return;
+  }
+  const [reviewList, applicationDetail] = await Promise.all([
+    getTopicReviewList(applicationId.value),
+    getApplication(applicationId.value),
+  ]);
+  reviews.value = reviewList;
+  application.value = applicationDetail;
+  gridApi.setGridOptions({ data: reviewList });
+}
+
+/** 璇勫鏄庣粏鍙樺寲鍚庯細鏈潰鏉块噸杞斤紝鍚屾椂閫氱煡鍒楄〃椤靛埛鏂帮紙鍧囧垎浼氬睍绀哄湪鍒楄〃锛� */
+async function handleChanged() {
+  await refresh();
+  emit('success');
+}
+
+function handleCreate() {
+  reviewFormModalApi
+    .setData({ applicationId: applicationId.value! })
+    .open();
+}
+
+function handleEdit(row: ResearchApplicationApi.TopicReview) {
+  reviewFormModalApi
+    .setData({ applicationId: applicationId.value!, review: row })
+    .open();
+}
+
+async function handleDelete(row: ResearchApplicationApi.TopicReview) {
+  const hideLoading = message.loading({ content: '鍒犻櫎涓�...', duration: 0 });
+  try {
+    await deleteTopicReview(row.id!);
+    message.success('鍒犻櫎鎴愬姛锛屽潎鍒嗕笌鎺掑悕宸查噸鏂版眹鎬�');
+    await handleChanged();
+  } finally {
+    hideLoading();
+  }
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      applicationId.value = undefined;
+      application.value = undefined;
+      reviews.value = [];
+      return;
+    }
+    const data = modalApi.getData<{ id: number }>();
+    applicationId.value = data.id;
+    modalApi.lock();
+    try {
+      await refresh();
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="涓撳璇勫" class="w-4/5" :show-confirm-button="false">
+    <div class="space-y-3">
+      <Alert
+        v-if="!editable"
+        type="info"
+        show-icon
+        :message="`褰撳墠鐢虫姤鍗曠姸鎬佷负銆�${statusLabel}銆嶏紝浠呫�屽鎵逛腑銆嶇姸鎬佸彲褰曞叆鎴栦慨鏀逛笓瀹惰瘎鍒哷"
+      />
+      <div class="flex flex-wrap items-center gap-6 rounded border border-border p-3">
+        <div>
+          <span class="text-gray-500">璇鹃鍚嶇О锛�</span>
+          <span class="font-medium">{{ application?.title || '-' }}</span>
+        </div>
+        <div>
+          <span class="text-gray-500">璇勫鍔犳潈鍧囧垎锛�</span>
+          <span class="font-medium">
+            {{
+              application?.reviewAvgScore === null ||
+              application?.reviewAvgScore === undefined
+                ? '鏈眹鎬�'
+                : Number(application.reviewAvgScore).toFixed(2)
+            }}
+          </span>
+        </div>
+        <div>
+          <span class="text-gray-500">璇勫缁撹锛�</span>
+          <Tag
+            v-if="application?.reviewPass !== null && application?.reviewPass !== undefined"
+            :color="application.reviewPass === 1 ? 'success' : 'error'"
+          >
+            {{ application.reviewPass === 1 ? '鎺ㄨ崘绔嬮」' : '涓嶆帹鑽愮珛椤�' }}
+          </Tag>
+          <span v-else>鏈眹鎬�</span>
+        </div>
+        <div>
+          <span class="text-gray-500">宸插綍鍏ヤ笓瀹舵暟锛�</span>
+          <span class="font-medium">{{ reviews.length }}</span>
+        </div>
+      </div>
+
+      <div v-if="editable">
+        <TableAction
+          :actions="[
+            {
+              label: '褰曞叆涓撳璇勫垎',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['research:review:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </div>
+
+      <Grid>
+        <template #actions="{ row }">
+          <TableAction
+            v-if="editable"
+            :actions="[
+              {
+                label: '缂栬緫',
+                type: 'link',
+                icon: ACTION_ICON.EDIT,
+                auth: ['research:review:update'],
+                onClick: handleEdit.bind(null, row),
+              },
+              {
+                label: '鍒犻櫎',
+                type: 'link',
+                danger: true,
+                icon: ACTION_ICON.DELETE,
+                auth: ['research:review:delete'],
+                popConfirm: {
+                  title: `纭鍒犻櫎涓撳銆�${row.evaluatorName}銆嶇殑璇勫垎璁板綍鍚楋紵鍒犻櫎鍚庡潎鍒嗕笌鎺掑悕灏嗛噸鏂版眹鎬籤,
+                  confirm: handleDelete.bind(null, row),
+                },
+              },
+            ]"
+          />
+        </template>
+      </Grid>
+    </div>
+  </Modal>
+  <ReviewFormModal @success="handleChanged" />
+</template>
diff --git a/src/views/research/guideline/data.ts b/src/views/research/guideline/data.ts
new file mode 100644
index 0000000..9c53bb7
--- /dev/null
+++ b/src/views/research/guideline/data.ts
@@ -0,0 +1,252 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 璇鹃鎸囧崡鍙戝竷鐘舵�侊紙瀵瑰簲瀛楀吀 research_guideline_status锛� */
+export const GuidelineStatus = {
+  DRAFT: 0, // 鑽夌
+  PUBLISHED: 20, // 宸插彂甯�
+  CLOSED: 30, // 宸叉埅姝�
+} as const;
+
+/** 琛ㄥ崟閰嶇疆 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '鎸囧崡缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鎸囧崡缂栧彿鐢辩郴缁熻嚜鍔ㄧ敓鎴�',
+        disabled: true,
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => formType !== 'create',
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '鎸囧崡鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ棰樻寚鍗楀悕绉�',
+        maxlength: 255,
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'category',
+      label: '璇鹃绫诲埆',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨璇鹃绫诲埆',
+        allowClear: true,
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'applyStartTime',
+      label: '鐢虫姤寮�濮嬫椂闂�',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐢虫姤寮�濮嬫椂闂�',
+        showTime: true,
+        // 鍚庣涓� LocalDateTime锛屽繀椤诲甫鏃跺垎绉�
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        style: { width: '100%' },
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'applyEndTime',
+      label: '鐢虫姤鎴鏃堕棿',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐢虫姤鎴鏃堕棿',
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        style: { width: '100%' },
+        disabled: formType === 'detail',
+      },
+      help: '鎴鏃堕棿涔嬪悗涓嶈兘鍐嶆彁浜よ棰樼敵鎶ヤ功',
+    },
+    {
+      fieldName: 'content',
+      label: '鎸囧崡姝f枃',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ寚鍗楁鏂囷紙鐮旂┒鏂瑰悜銆佺敵鎶ヨ姹傜瓑锛�',
+        autoSize: { minRows: 6, maxRows: 12 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '鎸囧崡闄勪欢',
+      component: 'FileUpload',
+      // valueKey 涓� id锛屾彁浜ゆ椂 formApi 浼氭妸鏂囦欢瀵硅薄鎻愬彇涓� blobId 鏁扮粍
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        showDescription: formType !== 'detail',
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '鎸囧崡缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ寚鍗楃紪鍙�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '鎸囧崡鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ寚鍗楀悕绉�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'category',
+      label: '璇鹃绫诲埆',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY, 'number'),
+        placeholder: '璇烽�夋嫨璇鹃绫诲埆',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'publishStatus',
+      label: '鍙戝竷鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.RESEARCH_GUIDELINE_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鍙戝竷鐘舵��',
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鍒楅厤缃� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '鎸囧崡缂栧彿',
+      width: 180,
+      fixed: 'left',
+    },
+    {
+      field: 'title',
+      title: '鎸囧崡鍚嶇О',
+      minWidth: 220,
+    },
+    {
+      field: 'category',
+      title: '璇鹃绫诲埆',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.RESEARCH_GUIDELINE_CATEGORY },
+      },
+    },
+    {
+      field: 'applyStartTime',
+      title: '鐢虫姤寮�濮嬫椂闂�',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'applyEndTime',
+      title: '鐢虫姤鎴鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'publishStatus',
+      title: '鍙戝竷鐘舵��',
+      minWidth: 95,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.RESEARCH_GUIDELINE_STATUS },
+      },
+    },
+    {
+      field: 'publishTime',
+      title: '鍙戝竷鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'hasAttachment',
+      title: '闄勪欢',
+      minWidth: 80,
+      slots: { default: 'hasAttachment' },
+    },
+    {
+      field: 'creatorName',
+      title: '鍒涘缓浜�',
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 260,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/research/guideline/index.vue b/src/views/research/guideline/index.vue
new file mode 100644
index 0000000..33625f5
--- /dev/null
+++ b/src/views/research/guideline/index.vue
@@ -0,0 +1,226 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ResearchGuidelineApi } from '#/api/research/guideline';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  closeGuideline,
+  deleteGuideline,
+  getGuidelinePage,
+  publishGuideline,
+} from '#/api/research/guideline';
+import { $t } from '#/locales';
+
+import { GuidelineStatus, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 璇鹃鎸囧崡鍒楄〃 */
+defineOptions({ name: 'ResearchGuideline' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鏂板鎸囧崡 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鎸囧崡锛堜粎鑽夌鍙紪杈戯級 */
+function handleEdit(row: ResearchGuidelineApi.Guideline) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅鎸囧崡璇︽儏 */
+function handleDetail(row: ResearchGuidelineApi.Guideline) {
+  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 Promise.all(ids.map((id) => deleteGuideline(id)));
+    message.success($t('ui.actionMessage.deleteSuccess'));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鍙戝竷鎸囧崡 */
+async function handlePublish(row: ResearchGuidelineApi.Guideline) {
+  const hideLoading = message.loading({ content: '鍙戝竷涓�...', duration: 0 });
+  try {
+    await publishGuideline(row.id!);
+    message.success('鍙戝竷鎴愬姛锛屼細鍛樼幇鍦ㄥ彲浠ユ嵁姝ゆ彁浜よ棰樼敵鎶�');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鎴鎸囧崡 */
+async function handleClose(row: ResearchGuidelineApi.Guideline) {
+  const hideLoading = message.loading({ content: '鎴涓�...', duration: 0 });
+  try {
+    await closeGuideline(row.id!);
+    message.success('宸叉埅姝紝璇ユ寚鍗椾笉鍐嶆帴鍙楁柊鐨勮棰樼敵鎶�');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: ResearchGuidelineApi.Guideline[];
+}) {
+  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 getGuidelinePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<ResearchGuidelineApi.Guideline>,
+  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: ['research:guideline:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['research:guideline:delete'],
+              popConfirm: {
+                title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛燂紙浠呰崏绋跨姸鎬佷笖鏃犺棰樼敵鎶ヤ功鐨勬寚鍗楀彲鍒犻櫎锛�',
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #hasAttachment="{ row }">
+        <Tag :color="row.hasAttachment ? 'success' : 'default'">
+          {{ row.hasAttachment ? '鏈�' : '鏃�' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:guideline:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['research:guideline:update'],
+              ifShow: () => row.publishStatus === GuidelineStatus.DRAFT,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鍙戝竷',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['research:guideline:publish'],
+              ifShow: () => row.publishStatus === GuidelineStatus.DRAFT,
+              popConfirm: {
+                title: `纭鍙戝竷鎸囧崡 ${row.no} 鍚楋紵鍙戝竷鍚庝笉鍙啀淇敼锛屼細鍛樺彲鎹鎻愪氦璇鹃鐢虫姤`,
+                confirm: handlePublish.bind(null, row),
+              },
+            },
+            {
+              label: '鎴',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['research:guideline:publish'],
+              ifShow: () => row.publishStatus === GuidelineStatus.PUBLISHED,
+              popConfirm: {
+                title: `纭鎴鎸囧崡 ${row.no} 鍚楋紵鎴鍚庝笉鍐嶆帴鍙楁柊鐨勮棰樼敵鎶,
+                confirm: handleClose.bind(null, row),
+              },
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['research:guideline:delete'],
+              ifShow: () => row.publishStatus === GuidelineStatus.DRAFT,
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/research/guideline/modules/form.vue b/src/views/research/guideline/modules/form.vue
new file mode 100644
index 0000000..f92416f
--- /dev/null
+++ b/src/views/research/guideline/modules/form.vue
@@ -0,0 +1,112 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { ResearchGuidelineApi } from '#/api/research/guideline';
+
+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 {
+  createGuideline,
+  getGuideline,
+  updateGuideline,
+} from '#/api/research/guideline';
+
+import { useFormSchema } from '../data';
+
+defineOptions({ name: 'ResearchGuidelineForm' });
+
+const emit = defineEmits(['success']);
+
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['璇鹃鎸囧崡']);
+  }
+  if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['璇鹃鎸囧崡']);
+  }
+  return '璇鹃鎸囧崡璇︽儏';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as ResearchGuidelineApi.Guideline;
+    // 闄勪欢涓哄叏閲忔浛鎹㈣涔夛細娓呯┖鍏ㄩ儴闄勪欢鏃跺繀椤绘樉寮忎紶绌烘暟缁勶紝
+    // 浼� null 鍚庣浼氳涓恒�屼笉鍔ㄩ檮浠躲�嶏紝瀵艰嚧鍒犻櫎鎿嶄綔涓嶇敓鏁�
+    data.blobIds = data.blobIds ?? [];
+    try {
+      await (formType.value === 'create'
+        ? createGuideline(data)
+        : updateGuideline(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    formApi.setState({ schema: useFormSchema(formType.value) });
+    if (!data?.id) {
+      await formApi.resetForm();
+      return;
+    }
+    modalApi.lock();
+    try {
+      const guideline = await getGuideline(data.id);
+      // 宸叉湁闄勪欢杞垚涓婁紶缁勪欢鍙瘑鍒殑鏂囦欢瀵硅薄锛屾彁浜ゆ椂鎸� valueKey=id 鎻愬彇涓� blobId
+      const blobIds = (guideline.attachmentList ?? []).map((item) => ({
+        uid: String(item.id),
+        name: item.name || '',
+        url: item.url || '',
+        status: 'done',
+        id: item.id,
+      }));
+      await formApi.setValues({ ...guideline, blobIds }, false);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :close-on-click-modal="false"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3" />
+  </Modal>
+</template>
diff --git a/src/views/ums/application/data.ts b/src/views/ums/application/data.ts
new file mode 100644
index 0000000..6472d40
--- /dev/null
+++ b/src/views/ums/application/data.ts
@@ -0,0 +1,348 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { markRaw } from 'vue';
+
+import { AreaCascader } from '#/components/area';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鍏ヤ細鐢宠鐘舵�侊紙瀵瑰簲瀛楀吀 ums_application_status锛� */
+export const ApplicationStatus = {
+  DRAFT: 0, // 鑽夌
+  APPROVING: 15, // 瀹℃壒涓�
+  APPROVED: 20, // 宸查�氳繃
+  REJECTED: 35, // 宸查┏鍥�
+} as const;
+
+/** 浼氬憳绫诲瀷锛堝搴斿瓧鍏� ums_member_type锛� */
+export const MemberType = {
+  PERSONAL: 1, // 涓汉浼氬憳
+  UNIT: 2, // 鍗曚綅浼氬憳
+} as const;
+
+/** 鏍规嵁浼氬憳绫诲瀷鑾峰彇绛夌骇瀛楀吀閫夐」 */
+export function getGradeDictOptions(memberType?: number) {
+  return getDictOptions(
+    memberType === MemberType.UNIT
+      ? DICT_TYPE.UMS_UNIT_GRADE
+      : DICT_TYPE.UMS_PERSONAL_GRADE,
+    'string',
+  );
+}
+
+/** 琛ㄥ崟閰嶇疆 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '鐢宠缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '鐢宠缂栧彿鐢辩郴缁熻嚜鍔ㄧ敓鎴�',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'memberType',
+      label: '浼氬憳绫诲瀷',
+      component: 'Select',
+      componentProps: (_values, form) => ({
+        options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨浼氬憳绫诲瀷',
+        allowClear: true,
+        disabled: formType === 'detail',
+        onChange: () => {
+          // 鍒囨崲浼氬憳绫诲瀷鍚庢竻绌哄凡閫夌瓑绾э紙涓汉涓庡崟浣嶄娇鐢ㄤ笉鍚岀瓑绾у瓧鍏革級
+          form.setFieldValue('memberGrade', undefined);
+        },
+      }),
+      rules: 'required',
+    },
+    {
+      fieldName: 'applicantName',
+      label: '鐢宠浜�/鍗曚綅鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '涓汉濉啓濮撳悕锛屽崟浣嶅~鍐欏崟浣嶅悕绉�',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'memberGrade',
+      label: '鐢宠绛夌骇',
+      component: 'Select',
+      componentProps: (values) => ({
+        options: getGradeDictOptions(values.memberType),
+        placeholder: '璇烽�夋嫨鐢宠绛夌骇',
+        allowClear: true,
+        disabled: formType === 'detail',
+      }),
+      rules: 'required',
+    },
+    {
+      fieldName: 'contactPhone',
+      label: '鑱旂郴鐢佃瘽锛堢櫥褰曡处鍙凤級',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ仈绯荤數璇濓紝灏嗕綔涓轰細鍛樼郴缁熺櫥褰曡处鍙�',
+        maxlength: 20,
+        disabled: formType === 'detail',
+      },
+      help: '瀹℃壒閫氳繃鍚庡皢鑷姩鍒涘缓绯荤粺鐧诲綍璐﹀彿锛氱敤鎴峰悕涓庡垵濮嬪瘑鐮佸潎涓鸿仈绯荤數璇濓紝璇峰姟蹇呭~鍐欏噯纭�',
+    },
+    {
+      fieldName: 'contactEmail',
+      label: '閭',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ偖绠�',
+        maxlength: 128,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'workUnit',
+      label: '宸ヤ綔鍗曚綅',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ伐浣滃崟浣嶏紙涓汉浼氬憳濉啓锛�',
+        maxlength: 128,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.PERSONAL,
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '鑱岀О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ亴绉帮紙涓汉浼氬憳濉啓锛�',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.PERSONAL,
+      },
+    },
+    {
+      fieldName: 'legalPerson',
+      label: '娉曞畾浠h〃浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ硶瀹氫唬琛ㄤ汉锛堝崟浣嶄細鍛樺~鍐欙級',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.UNIT,
+      },
+    },
+    {
+      fieldName: 'organizationCode',
+      label: '缁熶竴绀句細淇$敤浠g爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜锛堝崟浣嶄細鍛樺~鍐欙級',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.UNIT,
+      },
+    },
+    {
+      fieldName: 'researchDirection',
+      label: '鐮旂┒鏂瑰悜',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ爺绌舵柟鍚�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'regionCode',
+      label: '鍦板尯',
+      component: markRaw(AreaCascader),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍦板尯',
+        allowClear: true,
+        class: '!w-full',
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'industry',
+      label: '琛屼笟',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'),
+        placeholder: '璇烽�夋嫨琛屼笟',
+        allowClear: true,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_APPLICATION_STATUS, 'number'),
+        disabled: true,
+      },
+      dependencies: {
+        triggerFields: [''],
+        show: () => formType !== 'create',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '闄勪欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        showDescription: formType !== 'detail',
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '鐢宠缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ敵璇风紪鍙�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_APPLICATION_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'memberType',
+      label: '浼氬憳绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨浼氬憳绫诲瀷',
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鍒楅厤缃� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '鐢宠缂栧彿',
+      width: 180,
+      fixed: 'left',
+    },
+    {
+      field: 'applicantName',
+      title: '鐢宠浜�/鍗曚綅',
+      minWidth: 140,
+    },
+    {
+      field: 'memberType',
+      title: '浼氬憳绫诲瀷',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.UMS_MEMBER_TYPE },
+      },
+    },
+    {
+      field: 'memberGrade',
+      title: '鐢宠绛夌骇',
+      minWidth: 100,
+      slots: { default: 'memberGrade' },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 95,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.UMS_APPLICATION_STATUS },
+      },
+    },
+    {
+      field: 'hasAttachment',
+      title: '闄勪欢',
+      minWidth: 80,
+      slots: { default: 'hasAttachment' },
+    },
+    {
+      field: 'creatorName',
+      title: '鍒涘缓浜�',
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      width: 165,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 290,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/ums/application/detail/approve/index.vue b/src/views/ums/application/detail/approve/index.vue
new file mode 100644
index 0000000..e4edc48
--- /dev/null
+++ b/src/views/ums/application/detail/approve/index.vue
@@ -0,0 +1,196 @@
+<script lang="ts" setup>
+import type { UmsApplicationApi } from '#/api/ums/application';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { computed, h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Alert, Spin } from 'ant-design-vue';
+
+import { getApplication } from '#/api/ums/application';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+import { ApplicationStatus, MemberType } from '../../data';
+
+defineOptions({ name: 'UmsApplicationApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<UmsApplicationApi.Application>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+  return val === 0 || val ? String(val) : '-';
+}
+
+/** 鐢宠绛夌骇锛氭寜浼氬憳绫诲瀷鍙栦釜浜�/鍗曚綅涓嶅悓鐨勭瓑绾у瓧鍏� */
+function getGradeDictType(memberType?: number) {
+  return memberType === MemberType.UNIT
+    ? DICT_TYPE.UMS_UNIT_GRADE
+    : DICT_TYPE.UMS_PERSONAL_GRADE;
+}
+
+const [Descriptions] = useDescription({
+  bordered: true,
+  column: 2,
+  schema: [
+    { label: '鐢宠缂栧彿', field: 'no', render: textRender },
+    {
+      label: '浼氬憳绫诲瀷',
+      field: 'memberType',
+      render: (val) =>
+        h(DictTag, { type: DICT_TYPE.UMS_MEMBER_TYPE, value: val }),
+    },
+    { label: '鐢宠浜�/鍗曚綅鍚嶇О', field: 'applicantName', render: textRender },
+    {
+      label: '鐢宠绛夌骇',
+      field: 'memberGrade',
+      render: (val, data) =>
+        h(DictTag, {
+          type: getGradeDictType(data?.memberType),
+          value: val,
+        }),
+    },
+    {
+      label: '鐘舵��',
+      field: 'status',
+      render: (val) =>
+        h(DictTag, { type: DICT_TYPE.UMS_APPLICATION_STATUS, value: val }),
+    },
+    { label: '鑱旂郴鐢佃瘽', field: 'contactPhone', render: textRender },
+    { label: '閭', field: 'contactEmail', render: textRender },
+    {
+      label: '鐮旂┒鏂瑰悜',
+      field: 'researchDirection',
+      render: textRender,
+      span: 2,
+    },
+    {
+      label: '娉曞畾浠h〃浜�',
+      field: 'legalPerson',
+      render: textRender,
+      show: (data) => data?.memberType === MemberType.UNIT,
+    },
+    {
+      label: '缁熶竴绀句細淇$敤浠g爜',
+      field: 'organizationCode',
+      render: textRender,
+      show: (data) => data?.memberType === MemberType.UNIT,
+    },
+    {
+      label: '宸ヤ綔鍗曚綅',
+      field: 'workUnit',
+      render: textRender,
+      show: (data) => data?.memberType === MemberType.PERSONAL,
+    },
+    {
+      label: '鑱岀О',
+      field: 'title',
+      render: textRender,
+      show: (data) => data?.memberType === MemberType.PERSONAL,
+    },
+    {
+      label: '鍦板尯缂栫爜',
+      field: 'regionCode',
+      render: textRender,
+    },
+    {
+      label: '琛屼笟',
+      field: 'industry',
+      render: (val) => h(DictTag, { type: DICT_TYPE.UMS_INDUSTRY, value: val }),
+    },
+    { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+    {
+      label: '鍒涘缓鏃堕棿',
+      field: 'createTime',
+      render: (val) => (val ? (formatDateTime(val) as string) : '-'),
+    },
+    { label: '澶囨敞', field: 'remark', render: textRender, span: 2 },
+  ] as DescriptionItemSchema[],
+});
+
+/** 璐﹀彿鍑嵁鎻愰啋锛氬鎵逛腑鎻愮ず瀹℃壒浜烘牳瀵硅仈绯荤數璇濓紝閫氳繃鍚庢彁绀鸿处鍙峰凡寮�閫氬強鍒濆瀵嗙爜 */
+const credentialTip = computed(() => {
+  const status = formData.value?.status;
+  const phone = formData.value?.contactPhone;
+  if (status === ApplicationStatus.APPROVING) {
+    return {
+      type: 'warning' as const,
+      message: `瀹℃壒閫氳繃鍚庡皢鑷姩涓虹敵璇蜂汉鍒涘缓绯荤粺鐧诲綍璐﹀彿锛堢敤鎴峰悕涓庡垵濮嬪瘑鐮佸潎涓鸿仈绯荤數璇� ${phone || '鏈~鍐�'}锛夛紝璇峰厛鏍稿鑱旂郴鐢佃瘽鏄惁鍑嗙‘鍚庡啀閫氳繃銆俙,
+    };
+  }
+  if (status === ApplicationStatus.APPROVED) {
+    if (!phone) {
+      return {
+        type: 'warning' as const,
+        message:
+          '鏈~鍐欒仈绯荤數璇濓紝璐﹀彿鐢ㄦ埛鍚�/瀵嗙爜浣跨敤浜嗗厹搴曞�� unknown锛岃鍒颁細鍛樺垪琛ㄦ牳瀹炲苟鎵嬪伐淇銆�',
+      };
+    }
+    return {
+      type: 'success' as const,
+      message: `绯荤粺鐧诲綍璐﹀彿宸茶嚜鍔ㄥ紑閫氾細鐢ㄦ埛鍚� ${phone}锛屽垵濮嬪瘑鐮� ${phone}銆傝閫氱煡鐢宠浜哄敖蹇櫥褰曞苟淇敼瀵嗙爜銆俙,
+    };
+  }
+  return null;
+});
+
+onMounted(async () => {
+  try {
+    loading.value = true;
+    formData.value = await getApplication(Number(props.id));
+  } finally {
+    loading.value = false;
+  }
+});
+</script>
+
+<template>
+  <ContentWrap class="m-2">
+    <Spin :spinning="loading" tip="鍔犺浇涓�...">
+      <div class="space-y-4">
+        <!-- 璐﹀彿鍑嵁鎻愰啋 -->
+        <Alert
+          v-if="credentialTip"
+          :type="credentialTip.type"
+          :message="credentialTip.message"
+          show-icon
+        />
+        <!-- 鍏ヤ細鐢宠鍩烘湰淇℃伅 -->
+        <div class="rounded-lg border border-border bg-background p-4">
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">鍏ヤ細鐢宠鍩烘湰淇℃伅</span>
+          </div>
+          <Descriptions :data="formData" />
+        </div>
+        <!-- 璇佹槑鏉愭枡闄勪欢 -->
+        <div
+          v-if="formData?.attachmentList?.length"
+          class="rounded-lg border border-border bg-background p-4"
+        >
+          <div class="mb-4 flex items-center gap-2">
+            <span class="inline-block h-4 w-1 rounded bg-primary" />
+            <span class="text-base font-semibold">璇佹槑鏉愭枡闄勪欢</span>
+          </div>
+          <div class="flex flex-wrap gap-3">
+            <a
+              v-for="file in formData.attachmentList"
+              :key="file.id"
+              :href="file.previewURL || file.url"
+              target="_blank"
+              class="text-blue-500 hover:underline"
+            >
+              {{ file.name || file.originalFilename || `闄勪欢-${file.id}` }}
+            </a>
+          </div>
+        </div>
+      </div>
+    </Spin>
+  </ContentWrap>
+</template>
diff --git a/src/views/ums/application/index.vue b/src/views/ums/application/index.vue
new file mode 100644
index 0000000..1846b3a
--- /dev/null
+++ b/src/views/ums/application/index.vue
@@ -0,0 +1,261 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { UmsApplicationApi } from '#/api/ums/application';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  deleteApplication,
+  getApplicationApproveProcessList,
+  getApplicationPage,
+} from '#/api/ums/application';
+
+import {
+  ApplicationStatus,
+  MemberType,
+  useGridColumns,
+  useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+
+/** 鍏ヤ細鐢宠鍒楄〃 */
+defineOptions({ name: 'UmsApplication' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+  connectedComponent: ProcessSelectModal,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鏂板鍏ヤ細鐢宠 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鍏ヤ細鐢宠锛堜粎鑽夌銆佸凡椹冲洖鍙紪杈戯級 */
+function handleEdit(row: UmsApplicationApi.Application) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅鍏ヤ細鐢宠璇︽儏 */
+function handleDetail(row: UmsApplicationApi.Application) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鍒犻櫎鍏ヤ細鐢宠锛堜粎鑽夌鍙垹锛� */
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: '鍒犻櫎涓�...',
+    duration: 0,
+  });
+  try {
+    await Promise.all(ids.map((id) => deleteApplication(id)));
+    message.success('鍒犻櫎鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: UmsApplicationApi.Application) {
+  const hideLoading = message.loading({
+    content: '鑾峰彇瀹℃壒娴佺▼...',
+    duration: 0,
+  });
+  try {
+    const processList = await getApplicationApproveProcessList();
+    hideLoading();
+    if (!processList || processList.length === 0) {
+      message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堝埌 BPM 娴佺▼璁捐鍣ㄥ彂甯冨叆浼氬鎵规祦绋�');
+      return;
+    }
+    processModalApi.setData({ id: row.id, processList }).open();
+  } catch {
+    hideLoading();
+  }
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: UmsApplicationApi.Application) {
+  if (!row.processInstanceId) {
+    message.warning('璇ョ敵璇峰皻鏈彁浜ゅ鎵�');
+    return;
+  }
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: { id: row.processInstanceId },
+  });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+  records,
+}: {
+  records: UmsApplicationApi.Application[];
+}) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+/** 绛夌骇瀛楀吀鏄犲皠锛氭牴鎹細鍛樼被鍨嬫樉绀哄搴旂殑绛夌骇鍚嶇О */
+const personalGradeOptions = getDictOptions(DICT_TYPE.UMS_PERSONAL_GRADE, 'string');
+const unitGradeOptions = getDictOptions(DICT_TYPE.UMS_UNIT_GRADE, 'string');
+function getGradeLabel(row: UmsApplicationApi.Application) {
+  const options =
+    row.memberType === MemberType.UNIT
+      ? unitGradeOptions
+      : personalGradeOptions;
+  return options?.find((o) => o.value === row.memberGrade)?.label || row.memberGrade;
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getApplicationPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<UmsApplicationApi.Application>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <ProcessModal @success="handleRefresh" />
+    <Grid table-title="鍏ヤ細鐢宠鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '鏂板鍏ヤ細鐢宠',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['ums:application:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['ums:application:delete'],
+              popConfirm: {
+                title: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛燂紙浠呰崏绋跨姸鎬佸彲鍒犻櫎锛�',
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #memberGrade="{ row }">
+        <span>{{ getGradeLabel(row) }}</span>
+      </template>
+      <template #hasAttachment="{ row }">
+        <Tag :color="row.hasAttachment ? 'success' : 'default'">
+          {{ row.hasAttachment ? '鏈�' : '鏃�' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '璇︽儏',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['ums:application:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: '缂栬緫',
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['ums:application:update'],
+              ifShow: () =>
+                row.status === ApplicationStatus.DRAFT ||
+                row.status === ApplicationStatus.REJECTED,
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鎻愪氦瀹℃壒',
+              type: 'link',
+              icon: ACTION_ICON.AUDIT,
+              auth: ['ums:application:submit'],
+              ifShow: () =>
+                row.status === ApplicationStatus.DRAFT ||
+                row.status === ApplicationStatus.REJECTED,
+              onClick: handleSubmit.bind(null, row),
+            },
+            {
+              label: '鏌ョ湅瀹℃壒',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['ums:application:query'],
+              ifShow: () => !!row.processInstanceId,
+              onClick: handleProcessDetail.bind(null, row),
+            },
+            {
+              label: '鍒犻櫎',
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['ums:application:delete'],
+              ifShow: () => row.status === ApplicationStatus.DRAFT,
+              popConfirm: {
+                title: `纭鍒犻櫎鐢宠 ${row.no} 鍚楋紵`,
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/ums/application/modules/form.vue b/src/views/ums/application/modules/form.vue
new file mode 100644
index 0000000..65d9838
--- /dev/null
+++ b/src/views/ums/application/modules/form.vue
@@ -0,0 +1,161 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { UmsApplicationApi } from '#/api/ums/application';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+  createApplication,
+  getApplication,
+  updateApplication,
+} from '#/api/ums/application';
+
+import { ApplicationStatus, useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<UmsApplicationApi.Application>({
+  memberType: 1,
+  applicantName: '',
+  memberGrade: '',
+  contactPhone: '',
+  contactEmail: '',
+  legalPerson: '',
+  organizationCode: '',
+  researchDirection: '',
+  workUnit: '',
+  title: '',
+  regionCode: '',
+  industry: '',
+  remark: '',
+});
+
+const formType = ref<FormType>('create'); // 琛ㄥ崟绫诲瀷锛�'create' | 'edit' | 'detail'
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['鍏ヤ細鐢宠']);
+  } else if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['鍏ヤ細鐢宠']);
+  } else {
+    return '鍏ヤ細鐢宠璇︽儏';
+  }
+});
+
+/** 璐﹀彿鍑嵁鎻愰啋锛氫粎鍦ㄨ鎯呮�佷笖瀹℃壒宸查�氳繃鏃跺睍绀� */
+const credentialTip = computed(() => {
+  if (
+    formType.value !== 'detail' ||
+    formData.value.status !== ApplicationStatus.APPROVED
+  ) {
+    return null;
+  }
+  const phone = formData.value.contactPhone;
+  if (!phone) {
+    return {
+      type: 'warning' as const,
+      message:
+        '鏈~鍐欒仈绯荤數璇濓紝璐﹀彿鐢ㄦ埛鍚�/瀵嗙爜浣跨敤浜嗗厹搴曞�� unknown锛岃鍒颁細鍛樺垪琛ㄦ牳瀹炲苟鎵嬪伐淇銆�',
+    };
+  }
+  return {
+    type: 'success' as const,
+    message: `绯荤粺鐧诲綍璐﹀彿宸茶嚜鍔ㄥ紑閫氾細鐢ㄦ埛鍚� ${phone}锛屽垵濮嬪瘑鐮� ${phone}銆傝閫氱煡鐢宠浜哄敖蹇櫥褰曞苟淇敼瀵嗙爜銆俙,
+  };
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+/** 鍒涘缓鎴栨洿鏂板叆浼氱敵璇� */
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+
+    modalApi.lock();
+    const data = (await formApi.getValues()) as UmsApplicationApi.Application;
+    // 闄勪欢涓哄叏閲忔浛鎹㈣涔夛細娓呯┖闄勪欢鏃跺繀椤绘樉寮忎紶绌烘暟缁�
+    data.blobIds = data.blobIds ?? [];
+    try {
+      await (formType.value === 'create'
+        ? createApplication(data)
+        : updateApplication(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = {} as UmsApplicationApi.Application;
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    formApi.updateSchema(useFormSchema(formType.value));
+    if (!data || !data.id) {
+      // 鏂板缓鏃堕粯璁や釜浜轰細鍛樼瓑绾ч�夐」
+      await formApi.setValues({ memberType: 1 }, false);
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getApplication(data.id);
+      // 宸叉湁闄勪欢杞垚涓婁紶缁勪欢鍙瘑鍒殑鏂囦欢瀵硅薄锛屾彁浜ゆ椂鎸� valueKey=id 鎻愬彇涓� blobId
+      const blobIds = (formData.value.attachmentList ?? []).map((item) => ({
+        uid: String(item.id),
+        name: item.name || '',
+        url: item.url || '',
+        status: 'done',
+        id: item.id,
+      }));
+      await formApi.setValues(
+        { ...formData.value, blobIds },
+        false,
+      );
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :close-on-click-modal="false"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Alert
+      v-if="credentialTip"
+      :type="credentialTip.type"
+      :message="credentialTip.message"
+      show-icon
+      class="mx-3 mb-3"
+    />
+    <Form class="mx-3" />
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/ums/application/modules/process-select-modal.vue b/src/views/ums/application/modules/process-select-modal.vue
new file mode 100644
index 0000000..77b63c5
--- /dev/null
+++ b/src/views/ums/application/modules/process-select-modal.vue
@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import type { UmsApplicationApi } from '#/api/ums/application';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitApplication } from '#/api/ums/application';
+
+const emit = defineEmits(['success']);
+
+const applicationId = ref<number>();
+const processList = ref<UmsApplicationApi.ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() =>
+  processList.value.map((item) => ({ label: item.name, value: item.key })),
+);
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    if (!selectedProcessKey.value) {
+      message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+      return;
+    }
+    modalApi.lock();
+    try {
+      await submitApplication(applicationId.value!, selectedProcessKey.value);
+      await modalApi.close();
+      emit('success');
+      message.success(
+        '鎻愪氦鎴愬姛锛屽鎵归�氳繃鍚庡皢鑷姩鍒涘缓鐧诲綍璐﹀彿锛堢敤鎴峰悕涓庡垵濮嬪瘑鐮佸潎涓鸿仈绯荤數璇濓級',
+        6,
+      );
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      applicationId.value = undefined;
+      selectedProcessKey.value = '';
+      processList.value = [];
+      return;
+    }
+    const data =
+      modalApi.getData<{
+        id: number;
+        processList: UmsApplicationApi.ProcessDefinition[];
+      }>();
+    applicationId.value = data.id;
+    processList.value = data.processList || [];
+    if (processList.value.length > 0) {
+      selectedProcessKey.value = processList.value[0]!.key;
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+    <div class="mb-4">
+      <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+      <Select
+        v-model:value="selectedProcessKey"
+        :options="processOptions"
+        style="width: 200px"
+        placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+      />
+    </div>
+  </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/ums/member/data.ts b/src/views/ums/member/data.ts
new file mode 100644
index 0000000..5fec58c
--- /dev/null
+++ b/src/views/ums/member/data.ts
@@ -0,0 +1,432 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { markRaw } from 'vue';
+
+import { AreaCascader } from '#/components/area';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 浼氬憳绫诲瀷锛堝搴斿瓧鍏� ums_member_type锛� */
+export const MemberType = {
+  PERSONAL: 1, // 涓汉浼氬憳
+  UNIT: 2, // 鍗曚綅浼氬憳
+} as const;
+
+/** 浼氬憳鐘舵�侊紙瀵瑰簲瀛楀吀 ums_member_status锛� */
+export const MemberStatus = {
+  NORMAL: 1, // 姝e父
+  SUSPENDED: 0, // 鏆傚仠
+} as const;
+
+/** 鏍规嵁浼氬憳绫诲瀷鑾峰彇绛夌骇瀛楀吀閫夐」 */
+export function getGradeDictOptions(memberType?: number) {
+  return getDictOptions(
+    memberType === MemberType.UNIT
+      ? DICT_TYPE.UMS_UNIT_GRADE
+      : DICT_TYPE.UMS_PERSONAL_GRADE,
+    'string',
+  );
+}
+
+/** 琛ㄥ崟閰嶇疆 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'no',
+      label: '浼氬憳缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '浼氬憳缂栧彿鐢辩郴缁熻嚜鍔ㄧ敓鎴�',
+        disabled: true,
+      },
+    },
+    {
+      fieldName: 'memberType',
+      label: '浼氬憳绫诲瀷',
+      component: 'Select',
+      componentProps: (_values, form) => ({
+        options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨浼氬憳绫诲瀷',
+        allowClear: true,
+        disabled: formType === 'detail',
+        onChange: () => {
+          // 鍒囨崲浼氬憳绫诲瀷鍚庢竻绌哄凡閫夌瓑绾э紙涓汉涓庡崟浣嶄娇鐢ㄤ笉鍚岀瓑绾у瓧鍏革級
+          form.setFieldValue('memberGrade', undefined);
+        },
+      }),
+      rules: 'required',
+    },
+    {
+      fieldName: 'name',
+      label: '濮撳悕/鍗曚綅鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '涓汉濉啓濮撳悕锛屽崟浣嶅~鍐欏崟浣嶅悕绉�',
+        maxlength: 128,
+        disabled: formType === 'detail',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'memberGrade',
+      label: '浼氬憳绛夌骇',
+      component: 'Select',
+      componentProps: (values) => ({
+        options: getGradeDictOptions(values.memberType),
+        placeholder: '璇烽�夋嫨浼氬憳绛夌骇',
+        allowClear: true,
+        disabled: formType === 'detail',
+      }),
+      rules: 'required',
+    },
+    {
+      fieldName: 'contactUser',
+      label: '鑱旂郴浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ仈绯讳汉锛堝崟浣嶄細鍛樺~鍐欙級',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.UNIT,
+      },
+    },
+    {
+      fieldName: 'contactPhone',
+      label: '鑱旂郴鐢佃瘽',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ仈绯荤數璇�',
+        maxlength: 20,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'contactEmail',
+      label: '閭',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ラ偖绠�',
+        maxlength: 128,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'workUnit',
+      label: '宸ヤ綔鍗曚綅',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ伐浣滃崟浣嶏紙涓汉浼氬憳濉啓锛�',
+        maxlength: 128,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.PERSONAL,
+      },
+    },
+    {
+      fieldName: 'title',
+      label: '鑱岀О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヨ亴绉帮紙涓汉浼氬憳濉啓锛�',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.PERSONAL,
+      },
+    },
+    {
+      fieldName: 'legalPerson',
+      label: '娉曞畾浠h〃浜�',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ硶瀹氫唬琛ㄤ汉锛堝崟浣嶄細鍛樺~鍐欙級',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.UNIT,
+      },
+    },
+    {
+      fieldName: 'organizationCode',
+      label: '缁熶竴绀句細淇$敤浠g爜',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜锛堝崟浣嶄細鍛樺~鍐欙級',
+        maxlength: 64,
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.UNIT,
+      },
+    },
+    {
+      fieldName: 'researchDirection',
+      label: '鐮旂┒鏂瑰悜',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ爺绌舵柟鍚�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'unitIntro',
+      label: '鍗曚綅绠�浠�',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ崟浣嶇畝浠嬶紙鍗曚綅浼氬憳濉啓锛�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      dependencies: {
+        triggerFields: ['memberType'],
+        show: (values) => values.memberType === MemberType.UNIT,
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'regionCode',
+      label: '鍦板尯',
+      component: markRaw(AreaCascader),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍦板尯',
+        allowClear: true,
+        class: '!w-full',
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'industry',
+      label: '琛屼笟',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'),
+        placeholder: '璇烽�夋嫨琛屼笟',
+        allowClear: true,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'joinDate',
+      label: '鍏ヤ細鏃ユ湡',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍏ヤ細鏃ユ湡',
+        valueFormat: 'YYYY-MM-DD',
+        style: { width: '100%' },
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'expireDate',
+      label: '鏈夋晥鏈熻嚦',
+      component: 'DatePicker',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鏈夋晥鏈�',
+        valueFormat: 'YYYY-MM-DD',
+        style: { width: '100%' },
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_MEMBER_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+        disabled: formType === 'detail',
+      },
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        autoSize: { minRows: 2, maxRows: 3 },
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+    {
+      fieldName: 'blobIds',
+      label: '闄勪欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        showDescription: formType !== 'detail',
+        disabled: formType === 'detail',
+      },
+      formItemClass: 'col-span-2',
+    },
+  ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'no',
+      label: '浼氬憳缂栧彿',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ヤ細鍛樼紪鍙�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '濮撳悕/鍗曚綅',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ鍚嶆垨鍗曚綅鍚嶇О',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'memberType',
+      label: '浼氬憳绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_MEMBER_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨浼氬憳绫诲瀷',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'regionCode',
+      label: '鍦板尯',
+      component: markRaw(AreaCascader),
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍦板尯',
+        allowClear: true,
+        class: '!w-full',
+      },
+    },
+    {
+      fieldName: 'industry',
+      label: '琛屼笟',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_INDUSTRY, 'string'),
+        placeholder: '璇烽�夋嫨琛屼笟',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        options: getDictOptions(DICT_TYPE.UMS_MEMBER_STATUS, 'number'),
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鍒楅厤缃� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      type: 'checkbox',
+      width: 50,
+      fixed: 'left',
+    },
+    {
+      field: 'no',
+      title: '浼氬憳缂栧彿',
+      width: 180,
+      fixed: 'left',
+    },
+    {
+      field: 'name',
+      title: '濮撳悕/鍗曚綅',
+      minWidth: 150,
+    },
+    {
+      field: 'memberType',
+      title: '浼氬憳绫诲瀷',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.UMS_MEMBER_TYPE },
+      },
+    },
+    {
+      field: 'memberGrade',
+      title: '浼氬憳绛夌骇',
+      minWidth: 100,
+      slots: { default: 'memberGrade' },
+    },
+    {
+      field: 'contactPhone',
+      title: '鑱旂郴鐢佃瘽',
+      minWidth: 130,
+    },
+    {
+      field: 'industry',
+      title: '琛屼笟',
+      minWidth: 100,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.UMS_INDUSTRY },
+      },
+    },
+    {
+      field: 'joinDate',
+      title: '鍏ヤ細鏃ユ湡',
+      width: 115,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 85,
+      cellRender: {
+        name: 'CellDict',
+        props: { type: DICT_TYPE.UMS_MEMBER_STATUS },
+      },
+    },
+    {
+      field: 'hasAttachment',
+      title: '闄勪欢',
+      minWidth: 80,
+      slots: { default: 'hasAttachment' },
+    },
+    {
+      title: '鎿嶄綔',
+      width: 180,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/ums/member/directory.vue b/src/views/ums/member/directory.vue
new file mode 100644
index 0000000..b1330ac
--- /dev/null
+++ b/src/views/ums/member/directory.vue
@@ -0,0 +1,139 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { UmsMemberApi } from '#/api/ums/member';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getMemberPage } from '#/api/ums/member';
+
+import { MemberType, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 浼氬憳閫氳褰曪紙鎸夊湴鍖�/琛屼笟妫�绱紝鍙锛� */
+defineOptions({ name: 'UmsMemberDirectory' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鏌ョ湅浼氬憳璇︽儏 */
+function handleDetail(row: UmsMemberApi.Member) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 绛夌骇瀛楀吀鏄犲皠锛氭牴鎹細鍛樼被鍨嬫樉绀哄搴旂殑绛夌骇鍚嶇О */
+const personalGradeOptions = getDictOptions(DICT_TYPE.UMS_PERSONAL_GRADE, 'string');
+const unitGradeOptions = getDictOptions(DICT_TYPE.UMS_UNIT_GRADE, 'string');
+function getGradeLabel(row: UmsMemberApi.Member) {
+  const options =
+    row.memberType === MemberType.UNIT ? unitGradeOptions : personalGradeOptions;
+  return options?.find((o) => o.value === row.memberGrade)?.label || row.memberGrade;
+}
+
+const [Grid] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: [
+      { type: 'seq', title: '搴忓彿', width: 55, fixed: 'left' },
+      { field: 'no', title: '浼氬憳缂栧彿', width: 180, fixed: 'left' },
+      { field: 'name', title: '濮撳悕/鍗曚綅', minWidth: 150 },
+      {
+        field: 'memberType',
+        title: '浼氬憳绫诲瀷',
+        minWidth: 100,
+        cellRender: {
+          name: 'CellDict',
+          props: { type: DICT_TYPE.UMS_MEMBER_TYPE },
+        },
+      },
+      {
+        field: 'memberGrade',
+        title: '浼氬憳绛夌骇',
+        minWidth: 100,
+        slots: { default: 'memberGrade' },
+      },
+      { field: 'contactPhone', title: '鑱旂郴鐢佃瘽', minWidth: 130 },
+      { field: 'contactEmail', title: '閭', minWidth: 160 },
+      { field: 'workUnit', title: '宸ヤ綔鍗曚綅', minWidth: 160 },
+      {
+        field: 'industry',
+        title: '琛屼笟',
+        minWidth: 100,
+        cellRender: {
+          name: 'CellDict',
+          props: { type: DICT_TYPE.UMS_INDUSTRY },
+        },
+      },
+      {
+        field: 'regionCode',
+        title: '鍦板尯',
+        minWidth: 110,
+      },
+      {
+        field: 'joinDate',
+        title: '鍏ヤ細鏃ユ湡',
+        width: 115,
+        formatter: 'formatDate',
+      },
+      {
+        title: '鎿嶄綔',
+        width: 90,
+        fixed: 'right',
+        slots: { default: 'actions' },
+      },
+    ],
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getMemberPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<UmsMemberApi.Member>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal />
+    <Grid table-title="浼氬憳閫氳褰�">
+      <template #memberGrade="{ row }">
+        <span>{{ getGradeLabel(row) }}</span>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '璇︽儏',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['ums:member:directory'],
+              onClick: handleDetail.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/ums/member/index.vue b/src/views/ums/member/index.vue
new file mode 100644
index 0000000..aebb821
--- /dev/null
+++ b/src/views/ums/member/index.vue
@@ -0,0 +1,183 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { UmsMemberApi } from '#/api/ums/member';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deleteMember, getMemberPage } from '#/api/ums/member';
+
+import { MemberType, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** 浼氬憳鍒楄〃 */
+defineOptions({ name: 'UmsMember' });
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鏂板浼氬憳 */
+function handleCreate() {
+  formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫浼氬憳 */
+function handleEdit(row: UmsMemberApi.Member) {
+  formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅浼氬憳璇︽儏 */
+function handleDetail(row: UmsMemberApi.Member) {
+  formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鍒犻櫎浼氬憳 */
+async function handleDelete(ids: number[]) {
+  const hideLoading = message.loading({
+    content: '鍒犻櫎涓�...',
+    duration: 0,
+  });
+  try {
+    await Promise.all(ids.map((id) => deleteMember(id)));
+    message.success('鍒犻櫎鎴愬姛');
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({ records }: { records: UmsMemberApi.Member[] }) {
+  checkedIds.value = records.map((item) => item.id!);
+}
+
+/** 绛夌骇瀛楀吀鏄犲皠锛氭牴鎹細鍛樼被鍨嬫樉绀哄搴旂殑绛夌骇鍚嶇О */
+const personalGradeOptions = getDictOptions(DICT_TYPE.UMS_PERSONAL_GRADE, 'string');
+const unitGradeOptions = getDictOptions(DICT_TYPE.UMS_UNIT_GRADE, 'string');
+function getGradeLabel(row: UmsMemberApi.Member) {
+  const options =
+    row.memberType === MemberType.UNIT ? unitGradeOptions : personalGradeOptions;
+  return options?.find((o) => o.value === row.memberGrade)?.label || row.memberGrade;
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getMemberPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<UmsMemberApi.Member>,
+  gridEvents: {
+    checkboxAll: handleRowCheckboxChange,
+    checkboxChange: handleRowCheckboxChange,
+  },
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Grid table-title="浼氬憳鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '鏂板浼氬憳',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['ums:member:create'],
+              onClick: handleCreate,
+            },
+            {
+              label: '鎵归噺鍒犻櫎',
+              type: 'primary',
+              danger: true,
+              disabled: isEmpty(checkedIds),
+              icon: ACTION_ICON.DELETE,
+              auth: ['ums:member:delete'],
+              popConfirm: {
+                title: '鏄惁鍒犻櫎鎵�閫変腑浼氬憳锛�',
+                confirm: handleDelete.bind(null, checkedIds),
+              },
+            },
+          ]"
+        />
+      </template>
+      <template #memberGrade="{ row }">
+        <span>{{ getGradeLabel(row) }}</span>
+      </template>
+      <template #hasAttachment="{ row }">
+        <Tag :color="row.hasAttachment ? 'success' : 'default'">
+          {{ row.hasAttachment ? '鏈�' : '鏃�' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '璇︽儏',
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['ums:member:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: '缂栬緫',
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['ums:member:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鍒犻櫎',
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['ums:member:delete'],
+              popConfirm: {
+                title: `纭鍒犻櫎浼氬憳 ${row.name} 鍚楋紵`,
+                confirm: handleDelete.bind(null, [row.id!]),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/ums/member/modules/form.vue b/src/views/ums/member/modules/form.vue
new file mode 100644
index 0000000..198c33d
--- /dev/null
+++ b/src/views/ums/member/modules/form.vue
@@ -0,0 +1,123 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { UmsMemberApi } from '#/api/ums/member';
+
+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 { createMember, getMember, updateMember } from '#/api/ums/member';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<UmsMemberApi.Member>({
+  memberType: 1,
+  memberGrade: '',
+  name: '',
+  contactPhone: '',
+  contactEmail: '',
+  legalPerson: '',
+  organizationCode: '',
+  researchDirection: '',
+  workUnit: '',
+  title: '',
+  regionCode: '',
+  industry: '',
+  remark: '',
+});
+
+const formType = ref<FormType>('create'); // 琛ㄥ崟绫诲瀷锛�'create' | 'edit' | 'detail'
+
+const getTitle = computed(() => {
+  if (formType.value === 'create') {
+    return $t('ui.actionTitle.create', ['浼氬憳']);
+  } else if (formType.value === 'edit') {
+    return $t('ui.actionTitle.edit', ['浼氬憳']);
+  } else {
+    return '浼氬憳璇︽儏';
+  }
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    labelWidth: 120,
+  },
+  wrapperClass: 'grid-cols-2',
+  layout: 'vertical',
+  schema: useFormSchema(formType.value),
+  showDefaultActions: false,
+});
+
+/** 鍒涘缓鎴栨洿鏂颁細鍛� */
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+
+    modalApi.lock();
+    const data = (await formApi.getValues()) as UmsMemberApi.Member;
+    data.blobIds = data.blobIds ?? [];
+    try {
+      await (formType.value === 'create'
+        ? createMember(data)
+        : updateMember(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = {} as UmsMemberApi.Member;
+      return;
+    }
+    const data = modalApi.getData<{ formType: FormType; id?: number }>();
+    formType.value = data.formType;
+    formApi.setDisabled(formType.value === 'detail');
+    formApi.updateSchema(useFormSchema(formType.value));
+    if (!data || !data.id) {
+      await formApi.setValues({ memberType: 1, status: 1 }, false);
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getMember(data.id);
+      // 宸叉湁闄勪欢杞垚涓婁紶缁勪欢鍙瘑鍒殑鏂囦欢瀵硅薄锛屾彁浜ゆ椂鎸� valueKey=id 鎻愬彇涓� blobId
+      const blobIds = (formData.value.attachmentList ?? []).map((item) => ({
+        uid: String(item.id),
+        name: item.name || '',
+        url: item.url || '',
+        status: 'done',
+        id: item.id,
+      }));
+      await formApi.setValues({ ...formData.value, blobIds }, false);
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal
+    :title="getTitle"
+    class="w-3/4"
+    :close-on-click-modal="false"
+    :show-confirm-button="formType !== 'detail'"
+  >
+    <Form class="mx-3" />
+  </Modal>
+</template>
\ No newline at end of file

--
Gitblit v1.9.3