TwoFactorAuthModal.jsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { Modal, Button, Input, Typography } from '@douyinfe/semi-ui';
  18. /**
  19. * 可复用的两步验证模态框组件
  20. * @param {Object} props
  21. * @param {boolean} props.visible - 是否显示模态框
  22. * @param {string} props.code - 验证码值
  23. * @param {boolean} props.loading - 是否正在验证
  24. * @param {Function} props.onCodeChange - 验证码变化回调
  25. * @param {Function} props.onVerify - 验证回调
  26. * @param {Function} props.onCancel - 取消回调
  27. * @param {string} props.title - 模态框标题
  28. * @param {string} props.description - 验证描述文本
  29. * @param {string} props.placeholder - 输入框占位文本
  30. */
  31. const TwoFactorAuthModal = ({
  32. visible,
  33. code,
  34. loading,
  35. onCodeChange,
  36. onVerify,
  37. onCancel,
  38. title,
  39. description,
  40. placeholder,
  41. }) => {
  42. const { t } = useTranslation();
  43. const handleKeyDown = (e) => {
  44. if (e.key === 'Enter' && code && !loading) {
  45. onVerify();
  46. }
  47. };
  48. return (
  49. <Modal
  50. title={
  51. <div className='flex items-center'>
  52. <div className='w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3'>
  53. <svg
  54. className='w-4 h-4 text-blue-600 dark:text-blue-400'
  55. fill='currentColor'
  56. viewBox='0 0 20 20'
  57. >
  58. <path
  59. fillRule='evenodd'
  60. d='M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
  61. clipRule='evenodd'
  62. />
  63. </svg>
  64. </div>
  65. {title || t('安全验证')}
  66. </div>
  67. }
  68. visible={visible}
  69. onCancel={onCancel}
  70. footer={
  71. <>
  72. <Button onClick={onCancel}>{t('取消')}</Button>
  73. <Button
  74. type='primary'
  75. loading={loading}
  76. disabled={!code || loading}
  77. onClick={onVerify}
  78. >
  79. {t('验证')}
  80. </Button>
  81. </>
  82. }
  83. width={500}
  84. style={{ maxWidth: '90vw' }}
  85. >
  86. <div className='space-y-6'>
  87. {/* 安全提示 */}
  88. <div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-4'>
  89. <div className='flex items-start'>
  90. <svg
  91. className='w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0'
  92. fill='currentColor'
  93. viewBox='0 0 20 20'
  94. >
  95. <path
  96. fillRule='evenodd'
  97. d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
  98. clipRule='evenodd'
  99. />
  100. </svg>
  101. <div>
  102. <Typography.Text
  103. strong
  104. className='text-blue-800 dark:text-blue-200'
  105. >
  106. {t('安全验证')}
  107. </Typography.Text>
  108. <Typography.Text className='block text-blue-700 dark:text-blue-300 text-sm mt-1'>
  109. {description || t('为了保护账户安全,请验证您的两步验证码。')}
  110. </Typography.Text>
  111. </div>
  112. </div>
  113. </div>
  114. {/* 验证码输入 */}
  115. <div>
  116. <Typography.Text strong className='block mb-2'>
  117. {t('验证身份')}
  118. </Typography.Text>
  119. <Input
  120. placeholder={placeholder || t('请输入认证器验证码或备用码')}
  121. value={code}
  122. onChange={onCodeChange}
  123. size='large'
  124. maxLength={8}
  125. onKeyDown={handleKeyDown}
  126. autoFocus
  127. />
  128. <Typography.Text type='tertiary' size='small' className='mt-2 block'>
  129. {t('支持6位TOTP验证码或8位备用码')}
  130. </Typography.Text>
  131. </div>
  132. </div>
  133. </Modal>
  134. );
  135. };
  136. export default TwoFactorAuthModal;