index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import { View, Text, Input } from '@tarojs/components';
  2. import { useState } from 'react';
  3. import Taro from '@tarojs/taro';
  4. import {
  5. Button,
  6. Input as NutInput,
  7. Dialog,
  8. Toast,
  9. TextArea,
  10. ToastOptions,
  11. Radio,
  12. } from '@nutui/nutui-react-taro';
  13. import { AlipayIcon, BackCardIcon } from '@/components/Icon';
  14. import { transferApi } from '@/services/apis';
  15. import { useUserStore } from '@/stores/user';
  16. import { BankcardExtInfoSchema } from '@/schemas/account';
  17. import './index.less';
  18. export default function Transfer() {
  19. // 状态管理
  20. const [showConfirm, setShowConfirm] = useState<boolean>(false);
  21. const [isLoading, setIsLoading] = useState<boolean>(false);
  22. const [transferData, setTransferData] = useState<{
  23. amount: string;
  24. identity: string;
  25. name: string;
  26. identityType: string;
  27. remark: string;
  28. bankcardExtInfo: BankcardExtInfoSchema;
  29. }>({
  30. amount: '',
  31. identity: '',
  32. name: '',
  33. identityType: 'alipay',
  34. remark: '',
  35. bankcardExtInfo: {
  36. account_type: '1',
  37. },
  38. });
  39. // 常用金额选项
  40. const quickAmounts = ['10000', '20000', '30000'];
  41. // 常用备注选项
  42. const quickRemarks = ['消费', '工资', '奖金', '其他'];
  43. // 处理金额输入
  44. const handleAmountChange = (value: string) => {
  45. // 只允许输入数字和小数点
  46. let filtered = value.replace(/[^0-9.]/g, '');
  47. // 限制只有一个小数点
  48. const parts = filtered.split('.');
  49. if (parts.length > 2) {
  50. filtered = parts[0] + '.' + parts.slice(1).join('');
  51. }
  52. // 限制小数点后两位
  53. if (parts.length === 2 && parts[1].length > 2) {
  54. filtered = parts[0] + '.' + parts[1].substring(0, 2);
  55. }
  56. // 限制最大金额
  57. const numValue = parseFloat(filtered);
  58. if (!Number.isNaN(numValue) && numValue > 10000000) {
  59. showToast({
  60. content: '单笔转账金额不能超过1000万元',
  61. });
  62. filtered = '';
  63. }
  64. console.log(filtered);
  65. handleQuickAmount(filtered);
  66. };
  67. // 选择常用金额
  68. const handleQuickAmount = (value: string) => {
  69. setTransferData({ ...transferData, amount: value });
  70. };
  71. // 处理收款方姓名输入
  72. const handleNameChange = (value: string) => {
  73. setTransferData({ ...transferData, name: value });
  74. };
  75. // 处理收款方账号输入
  76. const handleIdentityChange = (value: string) => {
  77. setTransferData({ ...transferData, identity: value });
  78. };
  79. // 选择账户类型
  80. const handleidentityType = (type: string) => {
  81. setTransferData({
  82. ...transferData,
  83. identityType: type,
  84. identity: '',
  85. bankcardExtInfo: {
  86. account_type: '1',
  87. },
  88. });
  89. };
  90. // 选择常用备注
  91. const handleQuickRemark = (value: string) => {
  92. setTransferData({ ...transferData, remark: value });
  93. };
  94. // 处理银行卡账户类型选择
  95. const handleAccountTypeChange = (value: string) => {
  96. setTransferData({
  97. ...transferData,
  98. bankcardExtInfo: {
  99. ...transferData.bankcardExtInfo,
  100. account_type: value,
  101. },
  102. });
  103. };
  104. // 处理银行卡机构名称输入
  105. const handleInstNameChange = (value: string) => {
  106. setTransferData({
  107. ...transferData,
  108. bankcardExtInfo: {
  109. ...transferData.bankcardExtInfo,
  110. inst_name: value,
  111. },
  112. });
  113. };
  114. const showToast = (options: ToastOptions) => {
  115. Toast.show('transfer-toast', options);
  116. };
  117. // 打开确认弹窗
  118. const handleOpenConfirm = () => {
  119. // 验证输入
  120. if (!transferData.amount || parseFloat(transferData.amount) <= 0) {
  121. showToast({
  122. content: '请输入转账金额',
  123. });
  124. return;
  125. }
  126. if (!transferData.name) {
  127. showToast({
  128. content: `请输入收款方真实姓名`,
  129. });
  130. return;
  131. }
  132. if (!transferData.identity) {
  133. showToast({
  134. content: `请输入收款方${transferData.identityType === 'alipay' ? '支付宝' : '银行卡'}账号`,
  135. });
  136. return;
  137. }
  138. if (
  139. transferData.identityType === 'bank' &&
  140. transferData.bankcardExtInfo.account_type === '1' &&
  141. !transferData.bankcardExtInfo.inst_name
  142. ) {
  143. showToast({
  144. content: '请输入银行卡机构名称',
  145. });
  146. return;
  147. }
  148. setShowConfirm(true);
  149. };
  150. // 确认转账
  151. const handleConfirmTransfer = async () => {
  152. setIsLoading(true);
  153. setShowConfirm(false);
  154. try {
  155. const response = await transferApi({
  156. enterpriseId: useUserStore.getState().enterpriseId,
  157. amount: transferData.amount,
  158. payeeInfo: {
  159. identityType: transferData.identityType,
  160. name: transferData.name,
  161. identity: transferData.identity,
  162. bankcardExtInfo:
  163. transferData.identityType === 'alipay' ? undefined : transferData.bankcardExtInfo,
  164. },
  165. });
  166. // 显示成功提示
  167. showToast({
  168. icon: 'success',
  169. content: '转账成功',
  170. });
  171. // 清空输入
  172. setTransferData({
  173. amount: '',
  174. identity: '',
  175. name: '',
  176. identityType: 'alipay',
  177. remark: '',
  178. bankcardExtInfo: {
  179. account_type: '1',
  180. },
  181. });
  182. // 跳转到转账记录页面
  183. Taro.navigateTo({ url: '/pages/transfer/record/index' });
  184. } catch (error: unknown) {
  185. showToast({
  186. icon: 'error',
  187. content: error instanceof Error ? error.message : '转账失败',
  188. });
  189. } finally {
  190. setIsLoading(false);
  191. }
  192. };
  193. return (
  194. <View className="transfer">
  195. <Toast id="transfer-toast" />
  196. {/* 金额输入区域 */}
  197. <View className="amount-section">
  198. <Text className="amount-label">转账金额(元)</Text>
  199. <Input
  200. className="amount-input"
  201. value={transferData.amount}
  202. onInput={(e) => handleAmountChange(e.detail.value)}
  203. placeholder="0.00"
  204. type="digit"
  205. maxlength={16}
  206. disabled={isLoading}
  207. focus
  208. />
  209. <View className="quick-amounts">
  210. {quickAmounts.map((item) => (
  211. <View
  212. key={item}
  213. className={`quick-amount-btn ${transferData.amount === item ? 'active' : ''} ${isLoading ? 'disabled' : ''}`}
  214. onClick={() => handleQuickAmount(item)}
  215. >
  216. {item}元
  217. </View>
  218. ))}
  219. </View>
  220. </View>
  221. {/* 收款方信息区域 */}
  222. <View className="recipient-section">
  223. <View className="section-title">收款方信息</View>
  224. <View style={{ padding: '10px 20px' }}>
  225. <View className="recipient-input">
  226. <View className="nut-input-label">收款方姓名</View>
  227. <NutInput
  228. value={transferData.name}
  229. onChange={(value) => handleNameChange(value)}
  230. placeholder="请输入真实姓名"
  231. disabled={isLoading}
  232. maxLength={20}
  233. clearable
  234. />
  235. </View>
  236. <View className="account-type">
  237. <Text className="nut-input-label">账户类型</Text>
  238. <View className="type-options">
  239. <View
  240. className={`type-option ${transferData.identityType === 'alipay' ? 'active' : ''} ${isLoading ? 'disabled' : ''}`}
  241. onClick={() => handleidentityType('alipay')}
  242. >
  243. <View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  244. <AlipayIcon size={16} /> <Text style={{ marginLeft: '5px' }}>支付宝</Text>
  245. </View>
  246. </View>
  247. <View
  248. className={`type-option ${transferData.identityType === 'bank' ? 'active' : ''} ${isLoading ? 'disabled' : ''}`}
  249. onClick={() => handleidentityType('bank')}
  250. >
  251. <View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  252. <BackCardIcon size={20} /> <Text style={{ marginLeft: '5px' }}>银行卡</Text>
  253. </View>
  254. </View>
  255. </View>
  256. </View>
  257. <View className="recipient-input">
  258. <View className="nut-input-label">
  259. {transferData.identityType === 'alipay' ? '支付宝账号' : '银行卡账号'}
  260. </View>
  261. <NutInput
  262. value={transferData.identity}
  263. onChange={(value) => handleIdentityChange(value)}
  264. placeholder={
  265. transferData.identityType === 'alipay' ? '请输入支付宝账号' : '请输入银行卡号'
  266. }
  267. disabled={isLoading}
  268. maxLength={30}
  269. clearable
  270. />
  271. </View>
  272. {transferData.identityType === 'bank' && (
  273. <>
  274. <View className="recipient-input">
  275. <View className="nut-input-label">银行卡类型</View>
  276. <Radio.Group
  277. value={transferData.bankcardExtInfo.account_type}
  278. onChange={(value) => handleAccountTypeChange(value as string)}
  279. direction="horizontal"
  280. style={
  281. {
  282. // '--nutui-radio-button-border-radius': '8px',
  283. } as any
  284. }
  285. >
  286. <Radio shape="button" value="1">
  287. 公司账户
  288. </Radio>
  289. <Radio shape="button" value="2">
  290. 个人账户
  291. </Radio>
  292. </Radio.Group>
  293. </View>
  294. {transferData.bankcardExtInfo.account_type === '1' && (
  295. <View className="recipient-input">
  296. <View className="nut-input-label">银行卡机构名称</View>
  297. <NutInput
  298. value={transferData.bankcardExtInfo.inst_name || ''}
  299. onChange={(value) => handleInstNameChange(value)}
  300. placeholder="请输入机构名称"
  301. disabled={isLoading}
  302. maxLength={20}
  303. clearable
  304. />
  305. </View>
  306. )}
  307. </>
  308. )}
  309. </View>
  310. </View>
  311. {/* 备注区域 */}
  312. <View className="remark-section">
  313. <View className="section-title">
  314. 转账备注<Text style={{ color: '#b6b4b4' }}>(可选)</Text>
  315. </View>
  316. <View style={{ padding: '0px 20px' }}>
  317. <TextArea
  318. className="remark-input"
  319. value={transferData.remark}
  320. onChange={(value) => handleQuickRemark(value)}
  321. disabled={isLoading}
  322. maxLength={300}
  323. />
  324. <View className="quick-remarks">
  325. {quickRemarks.map((item) => (
  326. <View
  327. key={item}
  328. className={`quick-remark ${transferData.remark === item ? 'active' : ''} ${isLoading ? 'disabled' : ''}`}
  329. onClick={() => handleQuickRemark(item)}
  330. >
  331. {item}
  332. </View>
  333. ))}
  334. </View>
  335. </View>
  336. </View>
  337. {/* 提交按钮 */}
  338. <View className="submit-section">
  339. <Button
  340. className="submit-btn"
  341. type="primary"
  342. size="large"
  343. block
  344. loading={isLoading}
  345. onClick={handleOpenConfirm}
  346. >
  347. {isLoading ? '提交中...' : '确认转账'}
  348. </Button>
  349. </View>
  350. {/* 确认弹窗 */}
  351. <Dialog
  352. visible={showConfirm}
  353. title="确认转账"
  354. confirmText="确认"
  355. cancelText="取消"
  356. onConfirm={handleConfirmTransfer}
  357. onCancel={() => setShowConfirm(false)}
  358. >
  359. <View className="modal-info">
  360. <View className="info-item">
  361. <Text className="label">转账金额:</Text>
  362. <Text className="value">¥{transferData.amount}</Text>
  363. </View>
  364. <View className="info-item">
  365. <Text className="label">
  366. {transferData.identityType === 'alipay' ? '支付宝账号' : '银行卡账号'}:
  367. </Text>
  368. <Text className="value">{transferData.identity}</Text>
  369. </View>
  370. <View className="info-item">
  371. <Text className="label">备注:</Text>
  372. <Text className="value">{transferData.remark || '无'}</Text>
  373. </View>
  374. </View>
  375. </Dialog>
  376. </View>
  377. );
  378. }