Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 

386 wiersze
9.0 KiB

  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 {
  17. Button,
  18. Space,
  19. Tag,
  20. Typography,
  21. Modal,
  22. Tooltip,
  23. } from '@douyinfe/semi-ui';
  24. import {
  25. timestamp2string,
  26. getLobeHubIcon,
  27. stringToColor,
  28. } from '../../../helpers';
  29. import {
  30. renderLimitedItems,
  31. renderDescription,
  32. } from '../../common/ui/RenderUtils';
  33. const { Text } = Typography;
  34. // Render timestamp
  35. function renderTimestamp(timestamp) {
  36. return <>{timestamp2string(timestamp)}</>;
  37. }
  38. // Render model icon column: prefer model.icon, then fallback to vendor icon
  39. const renderModelIconCol = (record, vendorMap) => {
  40. const iconKey = record?.icon || vendorMap[record?.vendor_id]?.icon;
  41. if (!iconKey) return '-';
  42. return (
  43. <div className='flex items-center justify-center'>
  44. {getLobeHubIcon(iconKey, 20)}
  45. </div>
  46. );
  47. };
  48. // Render vendor column with icon
  49. const renderVendorTag = (vendorId, vendorMap, t) => {
  50. if (!vendorId || !vendorMap[vendorId]) return '-';
  51. const v = vendorMap[vendorId];
  52. return (
  53. <Tag
  54. color='white'
  55. shape='circle'
  56. prefixIcon={getLobeHubIcon(v.icon || 'Layers', 14)}
  57. >
  58. {v.name}
  59. </Tag>
  60. );
  61. };
  62. // Render groups (enable_groups)
  63. const renderGroups = (groups) => {
  64. if (!groups || groups.length === 0) return '-';
  65. return renderLimitedItems({
  66. items: groups,
  67. renderItem: (g, idx) => (
  68. <Tag key={idx} size='small' shape='circle' color={stringToColor(g)}>
  69. {g}
  70. </Tag>
  71. ),
  72. });
  73. };
  74. // Render tags
  75. const renderTags = (text) => {
  76. if (!text) return '-';
  77. const tagsArr = text.split(',').filter(Boolean);
  78. return renderLimitedItems({
  79. items: tagsArr,
  80. renderItem: (tag, idx) => (
  81. <Tag key={idx} size='small' shape='circle' color={stringToColor(tag)}>
  82. {tag}
  83. </Tag>
  84. ),
  85. });
  86. };
  87. // Render endpoints (supports object map or legacy array)
  88. const renderEndpoints = (value) => {
  89. try {
  90. const parsed = typeof value === 'string' ? JSON.parse(value) : value;
  91. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  92. const keys = Object.keys(parsed || {});
  93. if (keys.length === 0) return '-';
  94. return renderLimitedItems({
  95. items: keys,
  96. renderItem: (key, idx) => (
  97. <Tag key={idx} size='small' shape='circle' color={stringToColor(key)}>
  98. {key}
  99. </Tag>
  100. ),
  101. maxDisplay: 3,
  102. });
  103. }
  104. if (Array.isArray(parsed)) {
  105. if (parsed.length === 0) return '-';
  106. return renderLimitedItems({
  107. items: parsed,
  108. renderItem: (ep, idx) => (
  109. <Tag key={idx} color='white' size='small' shape='circle'>
  110. {ep}
  111. </Tag>
  112. ),
  113. maxDisplay: 3,
  114. });
  115. }
  116. return value || '-';
  117. } catch (_) {
  118. return value || '-';
  119. }
  120. };
  121. // Render quota types (array) using common limited items renderer
  122. const renderQuotaTypes = (arr, t) => {
  123. if (!Array.isArray(arr) || arr.length === 0) return '-';
  124. return renderLimitedItems({
  125. items: arr,
  126. renderItem: (qt, idx) => {
  127. if (qt === 1) {
  128. return (
  129. <Tag key={`${qt}-${idx}`} color='teal' size='small' shape='circle'>
  130. {t('按次计费')}
  131. </Tag>
  132. );
  133. }
  134. if (qt === 0) {
  135. return (
  136. <Tag key={`${qt}-${idx}`} color='violet' size='small' shape='circle'>
  137. {t('按量计费')}
  138. </Tag>
  139. );
  140. }
  141. return (
  142. <Tag key={`${qt}-${idx}`} color='white' size='small' shape='circle'>
  143. {qt}
  144. </Tag>
  145. );
  146. },
  147. maxDisplay: 3,
  148. });
  149. };
  150. // Render bound channels
  151. const renderBoundChannels = (channels) => {
  152. if (!channels || channels.length === 0) return '-';
  153. return renderLimitedItems({
  154. items: channels,
  155. renderItem: (c, idx) => (
  156. <Tag key={idx} color='white' size='small' shape='circle'>
  157. {c.name}({c.type})
  158. </Tag>
  159. ),
  160. });
  161. };
  162. // Render operations column
  163. const renderOperations = (
  164. text,
  165. record,
  166. setEditingModel,
  167. setShowEdit,
  168. manageModel,
  169. refresh,
  170. t,
  171. ) => {
  172. return (
  173. <Space wrap>
  174. {record.status === 1 ? (
  175. <Button
  176. type='danger'
  177. size='small'
  178. onClick={() => manageModel(record.id, 'disable', record)}
  179. >
  180. {t('禁用')}
  181. </Button>
  182. ) : (
  183. <Button
  184. size='small'
  185. onClick={() => manageModel(record.id, 'enable', record)}
  186. >
  187. {t('启用')}
  188. </Button>
  189. )}
  190. <Button
  191. type='tertiary'
  192. size='small'
  193. onClick={() => {
  194. setEditingModel(record);
  195. setShowEdit(true);
  196. }}
  197. >
  198. {t('编辑')}
  199. </Button>
  200. <Button
  201. type='danger'
  202. size='small'
  203. onClick={() => {
  204. Modal.confirm({
  205. title: t('确定是否要删除此模型?'),
  206. content: t('此修改将不可逆'),
  207. onOk: () => {
  208. (async () => {
  209. await manageModel(record.id, 'delete', record);
  210. await refresh();
  211. })();
  212. },
  213. });
  214. }}
  215. >
  216. {t('删除')}
  217. </Button>
  218. </Space>
  219. );
  220. };
  221. // 名称匹配类型渲染(带匹配数量 Tooltip)
  222. const renderNameRule = (rule, record, t) => {
  223. const map = {
  224. 0: { color: 'green', label: t('精确') },
  225. 1: { color: 'blue', label: t('前缀') },
  226. 2: { color: 'orange', label: t('包含') },
  227. 3: { color: 'purple', label: t('后缀') },
  228. };
  229. const cfg = map[rule];
  230. if (!cfg) return '-';
  231. let label = cfg.label;
  232. if (rule !== 0 && record.matched_count) {
  233. label = `${cfg.label} ${record.matched_count}${t('个模型')}`;
  234. }
  235. const tagElement = (
  236. <Tag color={cfg.color} size='small' shape='circle'>
  237. {label}
  238. </Tag>
  239. );
  240. if (
  241. rule === 0 ||
  242. !record.matched_models ||
  243. record.matched_models.length === 0
  244. ) {
  245. return tagElement;
  246. }
  247. return (
  248. <Tooltip content={record.matched_models.join(', ')} showArrow>
  249. {tagElement}
  250. </Tooltip>
  251. );
  252. };
  253. export const getModelsColumns = ({
  254. t,
  255. manageModel,
  256. setEditingModel,
  257. setShowEdit,
  258. refresh,
  259. vendorMap,
  260. }) => {
  261. return [
  262. {
  263. title: t('排序'),
  264. dataIndex: 'sort_order',
  265. width: 60,
  266. },
  267. {
  268. title: t('图标'),
  269. dataIndex: 'icon',
  270. width: 70,
  271. align: 'center',
  272. render: (text, record) => renderModelIconCol(record, vendorMap),
  273. },
  274. {
  275. title: t('模型名称'),
  276. dataIndex: 'model_name',
  277. render: (text) => (
  278. <Text copyable onClick={(e) => e.stopPropagation()}>
  279. {text}
  280. </Text>
  281. ),
  282. },
  283. {
  284. title: t('匹配类型'),
  285. dataIndex: 'name_rule',
  286. render: (val, record) => renderNameRule(val, record, t),
  287. },
  288. {
  289. title: t('参与官方同步'),
  290. dataIndex: 'sync_official',
  291. render: (val) => (
  292. <Tag size='small' shape='circle' color={val === 1 ? 'green' : 'orange'}>
  293. {val === 1 ? t('是') : t('否')}
  294. </Tag>
  295. ),
  296. },
  297. {
  298. title: t('描述'),
  299. dataIndex: 'description',
  300. render: (text) => renderDescription(text, 200),
  301. },
  302. {
  303. title: t('供应商'),
  304. dataIndex: 'vendor_id',
  305. render: (vendorId, record) => renderVendorTag(vendorId, vendorMap, t),
  306. },
  307. {
  308. title: t('标签'),
  309. dataIndex: 'tags',
  310. render: renderTags,
  311. },
  312. {
  313. title: t('端点'),
  314. dataIndex: 'endpoints',
  315. render: renderEndpoints,
  316. },
  317. {
  318. title: t('已绑定渠道'),
  319. dataIndex: 'bound_channels',
  320. render: renderBoundChannels,
  321. },
  322. {
  323. title: t('可用分组'),
  324. dataIndex: 'enable_groups',
  325. render: renderGroups,
  326. },
  327. {
  328. title: t('计费类型'),
  329. dataIndex: 'quota_types',
  330. render: (qts) => renderQuotaTypes(qts, t),
  331. },
  332. {
  333. title: t('创建时间'),
  334. dataIndex: 'created_time',
  335. render: (text, record, index) => {
  336. return <div>{renderTimestamp(text)}</div>;
  337. },
  338. },
  339. {
  340. title: t('更新时间'),
  341. dataIndex: 'updated_time',
  342. render: (text, record, index) => {
  343. return <div>{renderTimestamp(text)}</div>;
  344. },
  345. },
  346. {
  347. title: '',
  348. dataIndex: 'operate',
  349. fixed: 'right',
  350. render: (text, record, index) =>
  351. renderOperations(
  352. text,
  353. record,
  354. setEditingModel,
  355. setShowEdit,
  356. manageModel,
  357. refresh,
  358. t,
  359. ),
  360. },
  361. ];
  362. };