MissingModelsModal.jsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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, { useEffect, useState } from 'react';
  16. import { Modal, Table, Spin, Button, Typography, Empty, Input } from '@douyinfe/semi-ui';
  17. import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
  18. import { IconSearch } from '@douyinfe/semi-icons';
  19. import { API, showError } from '../../../../helpers';
  20. import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants';
  21. import { useIsMobile } from '../../../../hooks/common/useIsMobile';
  22. const MissingModelsModal = ({
  23. visible,
  24. onClose,
  25. onConfigureModel,
  26. t,
  27. }) => {
  28. const [loading, setLoading] = useState(false);
  29. const [missingModels, setMissingModels] = useState([]);
  30. const [searchKeyword, setSearchKeyword] = useState('');
  31. const [currentPage, setCurrentPage] = useState(1);
  32. const isMobile = useIsMobile();
  33. const fetchMissing = async () => {
  34. setLoading(true);
  35. try {
  36. const res = await API.get('/api/models/missing');
  37. if (res.data.success) {
  38. setMissingModels(res.data.data || []);
  39. } else {
  40. showError(res.data.message);
  41. }
  42. } catch (_) {
  43. showError(t('获取未配置模型失败'));
  44. }
  45. setLoading(false);
  46. };
  47. useEffect(() => {
  48. if (visible) {
  49. fetchMissing();
  50. setSearchKeyword('');
  51. setCurrentPage(1);
  52. } else {
  53. setMissingModels([]);
  54. }
  55. }, [visible]);
  56. // 过滤和分页逻辑
  57. const filteredModels = missingModels.filter((model) =>
  58. model.toLowerCase().includes(searchKeyword.toLowerCase())
  59. );
  60. const dataSource = (() => {
  61. const start = (currentPage - 1) * MODEL_TABLE_PAGE_SIZE;
  62. const end = start + MODEL_TABLE_PAGE_SIZE;
  63. return filteredModels.slice(start, end).map((model) => ({
  64. model,
  65. key: model,
  66. }));
  67. })();
  68. const columns = [
  69. {
  70. title: t('模型名称'),
  71. dataIndex: 'model',
  72. render: (text) => (
  73. <div className="flex items-center">
  74. <Typography.Text strong>{text}</Typography.Text>
  75. </div>
  76. )
  77. },
  78. {
  79. title: '',
  80. dataIndex: 'operate',
  81. fixed: 'right',
  82. width: 100,
  83. render: (text, record) => (
  84. <Button
  85. type="primary"
  86. size="small"
  87. onClick={() => onConfigureModel(record.model)}
  88. >
  89. {t('配置')}
  90. </Button>
  91. )
  92. }
  93. ];
  94. return (
  95. <Modal
  96. title={
  97. <div className="flex flex-col gap-2 w-full">
  98. <div className="flex items-center gap-2">
  99. <Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
  100. {t('未配置的模型列表')}
  101. </Typography.Text>
  102. <Typography.Text type="tertiary" size="small">
  103. {t('共')} {missingModels.length} {t('个未配置模型')}
  104. </Typography.Text>
  105. </div>
  106. </div>
  107. }
  108. visible={visible}
  109. onCancel={onClose}
  110. footer={null}
  111. size={isMobile ? 'full-width' : 'medium'}
  112. className="!rounded-lg"
  113. >
  114. <Spin spinning={loading}>
  115. {missingModels.length === 0 && !loading ? (
  116. <Empty
  117. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  118. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  119. description={t('暂无缺失模型')}
  120. style={{ padding: 30 }}
  121. />
  122. ) : (
  123. <div className="missing-models-content">
  124. {/* 搜索框 */}
  125. <div className="flex items-center justify-end gap-2 w-full mb-4">
  126. <Input
  127. placeholder={t('搜索模型...')}
  128. value={searchKeyword}
  129. onChange={(v) => {
  130. setSearchKeyword(v);
  131. setCurrentPage(1);
  132. }}
  133. className="!w-full"
  134. prefix={<IconSearch />}
  135. showClear
  136. />
  137. </div>
  138. {/* 表格 */}
  139. {filteredModels.length > 0 ? (
  140. <Table
  141. columns={columns}
  142. dataSource={dataSource}
  143. pagination={{
  144. currentPage: currentPage,
  145. pageSize: MODEL_TABLE_PAGE_SIZE,
  146. total: filteredModels.length,
  147. showSizeChanger: false,
  148. onPageChange: (page) => setCurrentPage(page),
  149. }}
  150. />
  151. ) : (
  152. <Empty
  153. image={<IllustrationNoResult style={{ width: 100, height: 100 }} />}
  154. darkModeImage={<IllustrationNoResultDark style={{ width: 100, height: 100 }} />}
  155. description={searchKeyword ? t('未找到匹配的模型') : t('暂无缺失模型')}
  156. style={{ padding: 20 }}
  157. />
  158. )}
  159. </div>
  160. )}
  161. </Spin>
  162. </Modal>
  163. );
  164. };
  165. export default MissingModelsModal;