PrefillGroupManagement.jsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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) => {
  127. try {
  128. const itemsArray = typeof items === 'string' ? JSON.parse(items) : items;
  129. if (!Array.isArray(itemsArray) || itemsArray.length === 0) {
  130. return <Text type="tertiary">{t('暂无项目')}</Text>;
  131. }
  132. return renderLimitedItems({
  133. items: itemsArray,
  134. renderItem: (item, idx) => (
  135. <Tag key={idx} size="small" shape='circle' color={stringToColor(item)}>
  136. {item}
  137. </Tag>
  138. ),
  139. maxDisplay: 3,
  140. });
  141. } catch {
  142. return <Text type="tertiary">{t('数据格式错误')}</Text>;
  143. }
  144. },
  145. },
  146. {
  147. title: '',
  148. key: 'action',
  149. fixed: 'right',
  150. width: 140,
  151. render: (_, record) => (
  152. <Space>
  153. <Button
  154. size="small"
  155. onClick={() => handleEdit(record)}
  156. >
  157. {t('编辑')}
  158. </Button>
  159. <Popconfirm
  160. title={t('确定删除此组?')}
  161. onConfirm={() => deleteGroup(record.id)}
  162. >
  163. <Button
  164. size="small"
  165. type="danger"
  166. >
  167. {t('删除')}
  168. </Button>
  169. </Popconfirm>
  170. </Space>
  171. ),
  172. },
  173. ];
  174. useEffect(() => {
  175. if (visible) {
  176. loadGroups();
  177. }
  178. }, [visible]);
  179. return (
  180. <>
  181. <SideSheet
  182. placement="left"
  183. title={
  184. <Space>
  185. <Tag color='blue' shape='circle'>
  186. {t('管理')}
  187. </Tag>
  188. <Title heading={4} className='m-0'>
  189. {t('预填组管理')}
  190. </Title>
  191. </Space>
  192. }
  193. visible={visible}
  194. onCancel={onClose}
  195. width={isMobile ? '100%' : 800}
  196. bodyStyle={{ padding: '0' }}
  197. closeIcon={null}
  198. >
  199. <Spin spinning={loading}>
  200. <div className='p-2'>
  201. <Card className='!rounded-2xl shadow-sm border-0'>
  202. <div className='flex items-center mb-2'>
  203. <Avatar size='small' color='blue' className='mr-2 shadow-md'>
  204. <IconLayers size={16} />
  205. </Avatar>
  206. <div>
  207. <Text className='text-lg font-medium'>{t('组列表')}</Text>
  208. <div className='text-xs text-gray-600'>{t('管理模型、标签、端点等预填组')}</div>
  209. </div>
  210. </div>
  211. <div className="flex justify-end mb-4">
  212. <Button
  213. type="primary"
  214. theme='solid'
  215. size="small"
  216. icon={<IconPlus />}
  217. onClick={() => handleEdit()}
  218. >
  219. {t('新建组')}
  220. </Button>
  221. </div>
  222. {groups.length > 0 ? (
  223. <CardTable
  224. columns={columns}
  225. dataSource={groups}
  226. rowKey="id"
  227. hidePagination={true}
  228. size="small"
  229. scroll={{ x: 'max-content' }}
  230. />
  231. ) : (
  232. <Empty
  233. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  234. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  235. description={t('暂无预填组')}
  236. style={{ padding: 30 }}
  237. />
  238. )}
  239. </Card>
  240. </div>
  241. </Spin>
  242. </SideSheet>
  243. {/* 编辑组件 */}
  244. <EditPrefillGroupModal
  245. visible={showEdit}
  246. onClose={closeEdit}
  247. editingGroup={editingGroup}
  248. onSuccess={handleEditSuccess}
  249. />
  250. </>
  251. );
  252. };
  253. export default PrefillGroupManagement;