MissingModelsModal.jsx 5.4 KB

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