diff --git a/frontend/desktop/src/api/auth.ts b/frontend/desktop/src/api/auth.ts index c29ec9655..03de96408 100644 --- a/frontend/desktop/src/api/auth.ts +++ b/frontend/desktop/src/api/auth.ts @@ -218,6 +218,9 @@ export const _enterpriseRealNameAuthCancelRequest = (request: AxiosInstance) => ); }; +export const _getBanksListRequest = (request: AxiosInstance) => () => + request>>('/api/account/enterpriseRealName?type=banks'); + export const _getAmount = (request: AxiosInstance) => () => request>('/api/account/getAmount'); export const _verifyToken = (request: AxiosInstance) => () => @@ -274,5 +277,6 @@ export const enterpriseRealNameAuthCancelRequest = _enterpriseRealNameAuthCancel export const faceAuthGenerateQRcodeUriRequest = _faceAuthGenerateQRcodeUriRequest(request); export const getFaceAuthStatusRequest = _getFaceAuthStatusRequest(request); +export const getBanksListRequest = _getBanksListRequest(request); export const getAmount = _getAmount(request); diff --git a/frontend/desktop/src/components/account/RealNameModal.tsx b/frontend/desktop/src/components/account/RealNameModal.tsx index a2b721909..10219c0b4 100644 --- a/frontend/desktop/src/components/account/RealNameModal.tsx +++ b/frontend/desktop/src/components/account/RealNameModal.tsx @@ -20,11 +20,16 @@ import { Spinner, Link, FormErrorMessage, - FlexProps + FlexProps, + Menu, + MenuButton, + MenuList, + MenuItem, + Icon } from '@chakra-ui/react'; import { CloseIcon, RefreshIcon, useMessage, WarningIcon } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState, useMemo, useRef } from 'react'; import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -36,7 +41,8 @@ import { enterpriseRealNameAuthVerifyRequest, faceAuthGenerateQRcodeUriRequest, getFaceAuthStatusRequest, - refreshRealNameQRecodeUriRequest + refreshRealNameQRecodeUriRequest, + getBanksListRequest } from '@/api/auth'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import useSessionStore from '@/stores/session'; @@ -360,7 +366,7 @@ export function RealNameAuthForm( boxShadow: '0px 1px 2px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.15)' }, - _focus: { boxShadow: 'none' } + _focus: {} }} > {t('common:enterprise_verification')} @@ -591,6 +597,47 @@ function EnterpriseVerification( const queryClient = useQueryClient(); const domain = useConfigStore((state) => state.cloudConfig?.domain); + + const [selectedBank, setSelectedBank] = useState(''); + + const [searchKeyword, setSearchKeyword] = useState(''); + + const searchInputRef = useRef(null); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const clearSearch = useCallback(() => { + setSearchKeyword(''); + }, []); + + const { data: banksResponse, isLoading: banksLoading, error: banksError } = useQuery( + ['banksList'], + getBanksListRequest, + { + refetchOnWindowFocus: false, + select: (response) => { + const banksData = response?.data || {}; + return Object.entries(banksData).map(([key, value]) => ({ + code: key, + name: String(value || ''), + shortName: String(key || '') + })); + } + } + ); + + const banksList = banksResponse || []; + const filteredBanksList = useMemo(() => { + if (!searchKeyword.trim()) { + return banksList; + } + return banksList.filter(bank => { + const bankShortName = bank.shortName.toLowerCase(); + const keyword = searchKeyword.toLowerCase(); + return bankShortName.includes(keyword); + }); + }, [banksList, searchKeyword]); + const schema = z.object({ key: z.string().min(1, { message: t('common:enterprise_key_required') }), accountBank: z.string().min(1, { message: t('common:account_bank_required') }), @@ -620,6 +667,7 @@ function EnterpriseVerification( register: registerMain, handleSubmit: handleMainSubmit, reset: resetMain, + setValue: setMainValue, formState: { errors: mainErrors } } = useForm>({ resolver: zodResolver(schema), @@ -653,9 +701,23 @@ function EnterpriseVerification( usrName, contactInfo }); + setSelectedBank(accountBank || ''); } }, [enterpriseRealNameAuthInfo?.data, resetMain]); + + const handleBankSelect = useCallback((bankName: string) => { + setSelectedBank(bankName); + setMainValue('accountBank', bankName); + setSearchKeyword(''); + }, [setMainValue]); + + + const resetSearchState = useCallback(() => { + clearSearch(); + setSelectedBank(''); + }, [clearSearch]); + const canPayment = enterpriseRealNameAuthInfo?.data?.paymentStatus !== PAYMENTSTATUS.PROCESSING; const canVerify = enterpriseRealNameAuthInfo?.data?.paymentStatus === PAYMENTSTATUS.PROCESSING; @@ -721,6 +783,7 @@ function EnterpriseVerification( queryClient.invalidateQueries(['enterpriseRealNameAuthInfo']); resetMain(); resetVerification(); + resetSearchState(); if (props.onFormSuccess) { props.onFormSuccess(); @@ -1038,6 +1101,7 @@ function EnterpriseVerification( + {t('common:bank_name')} + + { + const value = e.target.value; + setSearchKeyword(value); + + if (selectedBank && value !== selectedBank) { + setSelectedBank(''); + setMainValue('accountBank', ''); + } + + if (value) { + setIsMenuOpen(true); + } + }} + onFocus={() => { + if (filteredBanksList.length > 0) { + setIsMenuOpen(true); + } + }} + onBlur={(e) => { + + setTimeout(() => { + setIsMenuOpen(false); + }, 150); + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.preventDefault(); + clearSearch(); + setIsMenuOpen(false); + } else if (e.key === 'Enter' && filteredBanksList.length > 0) { + e.preventDefault(); + + if (filteredBanksList.length === 1) { + handleBankSelect(filteredBanksList[0].shortName); + setIsMenuOpen(false); + } + } + }} + h="32px" + px="12px" + pr="40px" + w="full" + borderRadius="6px" + borderColor={isMenuOpen ? "brightBlue.600" : "grayModern.200"} + bg="grayModern.50" + fontSize="14px" + fontWeight={400} + lineHeight="20px" + color="grayModern.900" + backgroundColor={selectedBank ? "rgb(231, 240, 254)" : "grayModern.50"} + _disabled={{ + bg: "grayModern.100", + color: "grayModern.400", + cursor: "not-allowed", + opacity: 0.6 + }} + autoComplete="off" + /> + + + {(searchKeyword || selectedBank) && ( + { + e.stopPropagation(); + setSelectedBank(''); + setMainValue('accountBank', ''); + setSearchKeyword(''); + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + }} + _hover={{ color: "grayModern.600" }} + > + + + )} + { + e.stopPropagation(); + if (isMenuOpen) { + setIsMenuOpen(false); + } else { + setIsMenuOpen(true); + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + } + }} + > + + + + + {isMenuOpen && ( + + + {Array.isArray(filteredBanksList) && filteredBanksList.length > 0 ? ( + filteredBanksList.map((bank, index) => ( + { + handleBankSelect(bank.shortName); + setIsMenuOpen(false); + }} + h="48px" + px="12px" + py="8px" + cursor="pointer" + bg={selectedBank === bank.shortName ? "brightBlue.50" : "white"} + _hover={{ + bg: selectedBank === bank.shortName ? "brightBlue.100" : "grayModern.50" + }} + transition="background-color 0.1s ease" + > + + + + {bank.shortName.charAt(0)} + + + + + {bank.shortName} + + + {selectedBank === bank.shortName && ( + + + + )} + + + )) + ) : searchKeyword ? ( + + 未找到相关银行 + + ) : ( + + {banksError ? t('common:get_code_failed') : '请选择银行'} + + )} + + + )} +