ChannelKeyDisplay.jsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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 [email protected]
  14. */
  15. import React from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { Card, Button, Typography, Tag } from '@douyinfe/semi-ui';
  18. import { copy, showSuccess } from '../../../helpers';
  19. /**
  20. * 解析密钥数据,支持多种格式
  21. * @param {string} keyData - 密钥数据
  22. * @param {Function} t - 翻译函数
  23. * @returns {Array} 解析后的密钥数组
  24. */
  25. const parseChannelKeys = (keyData, t) => {
  26. if (!keyData) return [];
  27. const trimmed = keyData.trim();
  28. // 检查是否是JSON数组格式(如Vertex AI)
  29. if (trimmed.startsWith('[')) {
  30. try {
  31. const parsed = JSON.parse(trimmed);
  32. if (Array.isArray(parsed)) {
  33. return parsed.map((item, index) => ({
  34. id: index,
  35. content: typeof item === 'string' ? item : JSON.stringify(item, null, 2),
  36. type: typeof item === 'string' ? 'text' : 'json',
  37. label: `${t('密钥')} ${index + 1}`
  38. }));
  39. }
  40. } catch (e) {
  41. // 如果解析失败,按普通文本处理
  42. console.warn('Failed to parse JSON keys:', e);
  43. }
  44. }
  45. // 检查是否是多行密钥(按换行符分割)
  46. const lines = trimmed.split('\n').filter(line => line.trim());
  47. if (lines.length > 1) {
  48. return lines.map((line, index) => ({
  49. id: index,
  50. content: line.trim(),
  51. type: 'text',
  52. label: `${t('密钥')} ${index + 1}`
  53. }));
  54. }
  55. // 单个密钥
  56. return [{
  57. id: 0,
  58. content: trimmed,
  59. type: trimmed.startsWith('{') ? 'json' : 'text',
  60. label: t('密钥')
  61. }];
  62. };
  63. /**
  64. * 可复用的密钥显示组件
  65. * @param {Object} props
  66. * @param {string} props.keyData - 密钥数据
  67. * @param {boolean} props.showSuccessIcon - 是否显示成功图标
  68. * @param {string} props.successText - 成功文本
  69. * @param {boolean} props.showWarning - 是否显示安全警告
  70. * @param {string} props.warningText - 警告文本
  71. */
  72. const ChannelKeyDisplay = ({
  73. keyData,
  74. showSuccessIcon = true,
  75. successText,
  76. showWarning = true,
  77. warningText
  78. }) => {
  79. const { t } = useTranslation();
  80. const parsedKeys = parseChannelKeys(keyData, t);
  81. const isMultipleKeys = parsedKeys.length > 1;
  82. const handleCopyAll = () => {
  83. copy(keyData);
  84. showSuccess(t('所有密钥已复制到剪贴板'));
  85. };
  86. const handleCopyKey = (content) => {
  87. copy(content);
  88. showSuccess(t('密钥已复制到剪贴板'));
  89. };
  90. return (
  91. <div className="space-y-4">
  92. {/* 成功状态 */}
  93. {showSuccessIcon && (
  94. <div className="flex items-center gap-2">
  95. <svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
  96. <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
  97. </svg>
  98. <Typography.Text strong className="text-green-700">
  99. {successText || t('验证成功')}
  100. </Typography.Text>
  101. </div>
  102. )}
  103. {/* 密钥内容 */}
  104. <div className="space-y-3">
  105. <div className="flex items-center justify-between">
  106. <Typography.Text strong>
  107. {isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')}
  108. </Typography.Text>
  109. {isMultipleKeys && (
  110. <div className="flex items-center gap-2">
  111. <Typography.Text type="tertiary" size="small">
  112. {t('共 {{count}} 个密钥', { count: parsedKeys.length })}
  113. </Typography.Text>
  114. <Button
  115. size="small"
  116. type="primary"
  117. theme="outline"
  118. onClick={handleCopyAll}
  119. >
  120. {t('复制全部')}
  121. </Button>
  122. </div>
  123. )}
  124. </div>
  125. <div className="space-y-3 max-h-80 overflow-auto">
  126. {parsedKeys.map((keyItem) => (
  127. <Card key={keyItem.id} className="!rounded-lg !border !border-gray-200 dark:!border-gray-700">
  128. <div className="space-y-2">
  129. <div className="flex items-center justify-between">
  130. <Typography.Text strong size="small" className="text-gray-700 dark:text-gray-300">
  131. {keyItem.label}
  132. </Typography.Text>
  133. <div className="flex items-center gap-2">
  134. {keyItem.type === 'json' && (
  135. <Tag size="small" color="blue">{t('JSON')}</Tag>
  136. )}
  137. <Button
  138. size="small"
  139. type="primary"
  140. theme="outline"
  141. icon={
  142. <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
  143. <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
  144. <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
  145. </svg>
  146. }
  147. onClick={() => handleCopyKey(keyItem.content)}
  148. >
  149. {t('复制')}
  150. </Button>
  151. </div>
  152. </div>
  153. <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 max-h-40 overflow-auto">
  154. <Typography.Text
  155. code
  156. className="text-xs font-mono break-all whitespace-pre-wrap text-gray-800 dark:text-gray-200"
  157. >
  158. {keyItem.content}
  159. </Typography.Text>
  160. </div>
  161. {keyItem.type === 'json' && (
  162. <Typography.Text type="tertiary" size="small" className="block">
  163. {t('JSON格式密钥,请确保格式正确')}
  164. </Typography.Text>
  165. )}
  166. </div>
  167. </Card>
  168. ))}
  169. </div>
  170. {isMultipleKeys && (
  171. <div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
  172. <Typography.Text type="tertiary" size="small" className="text-blue-700 dark:text-blue-300">
  173. <svg className="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
  174. <path fillRule="evenodd" 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" clipRule="evenodd" />
  175. </svg>
  176. {t('检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。')}
  177. </Typography.Text>
  178. </div>
  179. )}
  180. </div>
  181. {/* 安全警告 */}
  182. {showWarning && (
  183. <div className="bg-yellow-50 dark:bg-yellow-900 rounded-lg p-4">
  184. <div className="flex items-start">
  185. <svg className="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
  186. <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
  187. </svg>
  188. <div>
  189. <Typography.Text strong className="text-yellow-800 dark:text-yellow-200">
  190. {t('安全提醒')}
  191. </Typography.Text>
  192. <Typography.Text className="block text-yellow-700 dark:text-yellow-300 text-sm mt-1">
  193. {warningText || t('请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。')}
  194. </Typography.Text>
  195. </div>
  196. </div>
  197. </div>
  198. )}
  199. </div>
  200. );
  201. };
  202. export default ChannelKeyDisplay;