You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

193 line
4.9 KiB

  1. import React, { useEffect, useState, useRef } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { userRateLimitApi, showError, showSuccess } from '../../../../helpers';
  4. import {
  5. Button,
  6. Table,
  7. Modal,
  8. Form,
  9. Avatar,
  10. Typography,
  11. Popconfirm,
  12. } from '@douyinfe/semi-ui';
  13. import { IconDelete, IconPlus, IconClock } from '@douyinfe/semi-icons';
  14. const { Text } = Typography;
  15. const UserModelRateLimitSection = ({ userId }) => {
  16. const { t } = useTranslation();
  17. const [limits, setLimits] = useState([]);
  18. const [loading, setLoading] = useState(false);
  19. const [addModalVisible, setAddModalVisible] = useState(false);
  20. const formApiRef = useRef(null);
  21. const loadLimits = async () => {
  22. if (!userId) return;
  23. setLoading(true);
  24. try {
  25. const res = await userRateLimitApi.get(userId);
  26. const { success, data, message } = res.data;
  27. if (success) {
  28. setLimits(data || []);
  29. } else {
  30. showError(message);
  31. }
  32. } catch (e) {
  33. showError(e.message);
  34. }
  35. setLoading(false);
  36. };
  37. useEffect(() => {
  38. loadLimits();
  39. }, [userId]);
  40. const saveLimits = async (newLimits) => {
  41. try {
  42. const res = await userRateLimitApi.set(userId, newLimits);
  43. const { success, message } = res.data;
  44. if (success) {
  45. showSuccess(t('保存成功'));
  46. return true;
  47. } else {
  48. showError(message);
  49. return false;
  50. }
  51. } catch (e) {
  52. showError(e.message);
  53. return false;
  54. }
  55. };
  56. const handleAdd = async (values) => {
  57. const rpm = parseInt(values.rpm) || 0;
  58. const newItem = { model: values.model, rpm };
  59. const filtered = limits.filter((item) => item.model !== values.model);
  60. const newLimits = [...filtered, newItem];
  61. const ok = await saveLimits(newLimits);
  62. if (ok) {
  63. setLimits(newLimits);
  64. setAddModalVisible(false);
  65. formApiRef.current?.reset();
  66. }
  67. };
  68. const handleDelete = async (model) => {
  69. const newLimits = limits.filter((item) => item.model !== model);
  70. const ok = await saveLimits(newLimits);
  71. if (ok) {
  72. setLimits(newLimits);
  73. }
  74. };
  75. const columns = [
  76. {
  77. title: t('模型'),
  78. dataIndex: 'model',
  79. key: 'model',
  80. },
  81. {
  82. title: t('RPM 限制'),
  83. dataIndex: 'rpm',
  84. key: 'rpm',
  85. render: (rpm) =>
  86. rpm === 0 ? (
  87. <Text type='tertiary'>{t('不限制')}</Text>
  88. ) : (
  89. <Text>{rpm}</Text>
  90. ),
  91. },
  92. {
  93. title: t('操作'),
  94. key: 'action',
  95. render: (_, record) => (
  96. <Popconfirm
  97. title={t('确认删除该限制?')}
  98. onConfirm={() => handleDelete(record.model)}
  99. >
  100. <Button
  101. type='danger'
  102. size='small'
  103. icon={<IconDelete />}
  104. >
  105. {t('删除')}
  106. </Button>
  107. </Popconfirm>
  108. ),
  109. },
  110. ];
  111. return (
  112. <>
  113. <div className='flex items-center justify-between mb-2'>
  114. <div className='flex items-center'>
  115. <Avatar size='small' color='cyan' className='mr-2 shadow-md'>
  116. <IconClock size={16} />
  117. </Avatar>
  118. <div>
  119. <Text className='text-lg font-medium'>
  120. {t('模型速率限制(RPM)')}
  121. </Text>
  122. <div className='text-xs text-gray-600'>
  123. {t('为该用户配置各模型每分钟请求数上限,0 表示不限制')}
  124. </div>
  125. </div>
  126. </div>
  127. <Button
  128. size='small'
  129. icon={<IconPlus />}
  130. onClick={() => setAddModalVisible(true)}
  131. >
  132. {t('添加限制')}
  133. </Button>
  134. </div>
  135. <Table
  136. columns={columns}
  137. dataSource={limits}
  138. loading={loading}
  139. rowKey='model'
  140. size='small'
  141. pagination={false}
  142. empty={t('暂无速率限制')}
  143. />
  144. <Modal
  145. title={t('添加模型速率限制')}
  146. visible={addModalVisible}
  147. onOk={() => formApiRef.current?.submitForm()}
  148. onCancel={() => {
  149. setAddModalVisible(false);
  150. formApiRef.current?.reset();
  151. }}
  152. >
  153. <Form
  154. getFormApi={(api) => (formApiRef.current = api)}
  155. onSubmit={handleAdd}
  156. >
  157. <Form.Input
  158. field='model'
  159. label={t('模型名称')}
  160. placeholder={t('请输入精确的模型名,如 gpt-4o')}
  161. rules={[{ required: true, message: t('请输入模型名称') }]}
  162. style={{ width: '100%' }}
  163. />
  164. <Form.InputNumber
  165. field='rpm'
  166. label={t('RPM 限制')}
  167. placeholder='60'
  168. initValue={60}
  169. min={0}
  170. step={10}
  171. extraText={t('0 表示不限制')}
  172. rules={[{ required: true, message: t('请输入 RPM 限制') }]}
  173. style={{ width: '100%' }}
  174. />
  175. </Form>
  176. </Modal>
  177. </>
  178. );
  179. };
  180. export default UserModelRateLimitSection;