PrefillGroupManagement.jsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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, { useState, useEffect } from 'react';
  16. import {
  17. SideSheet,
  18. Button,
  19. Typography,
  20. Space,
  21. Tag,
  22. Popconfirm,
  23. Card,
  24. Avatar,
  25. Spin,
  26. Empty,
  27. } from '@douyinfe/semi-ui';
  28. import {
  29. IconPlus,
  30. IconLayers,
  31. } from '@douyinfe/semi-icons';
  32. import {
  33. IllustrationNoResult,
  34. IllustrationNoResultDark,
  35. } from '@douyinfe/semi-illustrations';
  36. import { API, showError, showSuccess, stringToColor } from '../../../../helpers';
  37. import { useTranslation } from 'react-i18next';
  38. import { useIsMobile } from '../../../../hooks/common/useIsMobile';
  39. import CardTable from '../../../common/ui/CardTable';
  40. import EditPrefillGroupModal from './EditPrefillGroupModal';
  41. import { renderLimitedItems, renderDescription } from '../../../common/ui/RenderUtils';
  42. const { Text, Title } = Typography;
  43. const PrefillGroupManagement = ({ visible, onClose }) => {
  44. const { t } = useTranslation();
  45. const isMobile = useIsMobile();
  46. const [loading, setLoading] = useState(false);
  47. const [groups, setGroups] = useState([]);
  48. const [showEdit, setShowEdit] = useState(false);
  49. const [editingGroup, setEditingGroup] = useState({ id: undefined });
  50. const typeOptions = [
  51. { label: t('模型组'), value: 'model' },
  52. { label: t('标签组'), value: 'tag' },
  53. { label: t('端点组'), value: 'endpoint' },
  54. ];
  55. // 加载组列表
  56. const loadGroups = async () => {
  57. setLoading(true);
  58. try {
  59. const res = await API.get('/api/prefill_group');
  60. if (res.data.success) {
  61. setGroups(res.data.data || []);
  62. } else {
  63. showError(res.data.message || t('获取组列表失败'));
  64. }
  65. } catch (error) {
  66. showError(t('获取组列表失败'));
  67. }
  68. setLoading(false);
  69. };
  70. // 删除组
  71. const deleteGroup = async (id) => {
  72. try {
  73. const res = await API.delete(`/api/prefill_group/${id}`);
  74. if (res.data.success) {
  75. showSuccess(t('删除成功'));
  76. loadGroups();
  77. } else {
  78. showError(res.data.message || t('删除失败'));
  79. }
  80. } catch (error) {
  81. showError(t('删除失败'));
  82. }
  83. };
  84. // 编辑组
  85. const handleEdit = (group = {}) => {
  86. setEditingGroup(group);
  87. setShowEdit(true);
  88. };
  89. // 关闭编辑
  90. const closeEdit = () => {
  91. setShowEdit(false);
  92. setTimeout(() => {
  93. setEditingGroup({ id: undefined });
  94. }, 300);
  95. };
  96. // 编辑成功回调
  97. const handleEditSuccess = () => {
  98. closeEdit();
  99. loadGroups();
  100. };
  101. // 表格列定义
  102. const columns = [
  103. {
  104. title: t('组名'),
  105. dataIndex: 'name',
  106. key: 'name',
  107. render: (text, record) => (
  108. <Space>
  109. <Text strong>{text}</Text>
  110. <Tag color="white" shape="circle" size="small">
  111. {typeOptions.find(opt => opt.value === record.type)?.label || record.type}
  112. </Tag>
  113. </Space>
  114. ),
  115. },
  116. {
  117. title: t('描述'),
  118. dataIndex: 'description',
  119. key: 'description',
  120. render: (text) => renderDescription(text, 150),
  121. },
  122. {
  123. title: t('项目内容'),
  124. dataIndex: 'items',
  125. key: 'items',
  126. render: (items, record) => {
  127. try {
  128. if (record.type === 'endpoint') {
  129. const obj = typeof items === 'string' ? JSON.parse(items || '{}') : (items || {});
  130. const keys = Object.keys(obj);
  131. if (keys.length === 0) return <Text type="tertiary">{t('暂无项目')}</Text>;
  132. return renderLimitedItems({
  133. items: keys,
  134. renderItem: (key, idx) => (
  135. <Tag key={idx} size="small" shape='circle' color={stringToColor(key)}>
  136. {key}
  137. </Tag>
  138. ),
  139. maxDisplay: 3,
  140. });
  141. }
  142. const itemsArray = typeof items === 'string' ? JSON.parse(items) : items;
  143. if (!Array.isArray(itemsArray) || itemsArray.length === 0) {
  144. return <Text type="tertiary">{t('暂无项目')}</Text>;
  145. }
  146. return renderLimitedItems({
  147. items: itemsArray,
  148. renderItem: (item, idx) => (
  149. <Tag key={idx} size="small" shape='circle' color={stringToColor(item)}>
  150. {item}
  151. </Tag>
  152. ),
  153. maxDisplay: 3,
  154. });
  155. } catch {
  156. return <Text type="tertiary">{t('数据格式错误')}</Text>;
  157. }
  158. },
  159. },
  160. {
  161. title: '',
  162. key: 'action',
  163. fixed: 'right',
  164. width: 140,
  165. render: (_, record) => (
  166. <Space>
  167. <Button
  168. size="small"
  169. onClick={() => handleEdit(record)}
  170. >
  171. {t('编辑')}
  172. </Button>
  173. <Popconfirm
  174. title={t('确定删除此组?')}
  175. onConfirm={() => deleteGroup(record.id)}
  176. >
  177. <Button
  178. size="small"
  179. type="danger"
  180. >
  181. {t('删除')}
  182. </Button>
  183. </Popconfirm>
  184. </Space>
  185. ),
  186. },
  187. ];
  188. useEffect(() => {
  189. if (visible) {
  190. loadGroups();
  191. }
  192. }, [visible]);
  193. return (
  194. <>
  195. <SideSheet
  196. placement="left"
  197. title={
  198. <Space>
  199. <Tag color='blue' shape='circle'>
  200. {t('管理')}
  201. </Tag>
  202. <Title heading={4} className='m-0'>
  203. {t('预填组管理')}
  204. </Title>
  205. </Space>
  206. }
  207. visible={visible}
  208. onCancel={onClose}
  209. width={isMobile ? '100%' : 800}
  210. bodyStyle={{ padding: '0' }}
  211. closeIcon={null}
  212. >
  213. <Spin spinning={loading}>
  214. <div className='p-2'>
  215. <Card className='!rounded-2xl shadow-sm border-0'>
  216. <div className='flex items-center mb-2'>
  217. <Avatar size='small' color='blue' className='mr-2 shadow-md'>
  218. <IconLayers size={16} />
  219. </Avatar>
  220. <div>
  221. <Text className='text-lg font-medium'>{t('组列表')}</Text>
  222. <div className='text-xs text-gray-600'>{t('管理模型、标签、端点等预填组')}</div>
  223. </div>
  224. </div>
  225. <div className="flex justify-end mb-4">
  226. <Button
  227. type="primary"
  228. theme='solid'
  229. size="small"
  230. icon={<IconPlus />}
  231. onClick={() => handleEdit()}
  232. >
  233. {t('新建组')}
  234. </Button>
  235. </div>
  236. {groups.length > 0 ? (
  237. <CardTable
  238. columns={columns}
  239. dataSource={groups}
  240. rowKey="id"
  241. hidePagination={true}
  242. size="small"
  243. scroll={{ x: 'max-content' }}
  244. />
  245. ) : (
  246. <Empty
  247. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  248. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  249. description={t('暂无预填组')}
  250. style={{ padding: 30 }}
  251. />
  252. )}
  253. </Card>
  254. </div>
  255. </Spin>
  256. </SideSheet>
  257. {/* 编辑组件 */}
  258. <EditPrefillGroupModal
  259. visible={showEdit}
  260. onClose={closeEdit}
  261. editingGroup={editingGroup}
  262. onSuccess={handleEditSuccess}
  263. />
  264. </>
  265. );
  266. };
  267. export default PrefillGroupManagement;