/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal, Checkbox, Empty, Input, Tabs, Typography, } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark, } from '@douyinfe/semi-illustrations'; import { IconSearch } from '@douyinfe/semi-icons'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const normalizeModels = (models = []) => Array.from( new Set( (models || []).map((model) => String(model || '').trim()).filter(Boolean), ), ); const filterByKeyword = (models = [], keyword = '') => { const normalizedKeyword = String(keyword || '') .trim() .toLowerCase(); if (!normalizedKeyword) { return models; } return models.filter((model) => String(model).toLowerCase().includes(normalizedKeyword), ); }; const ChannelUpstreamUpdateModal = ({ visible, addModels = [], removeModels = [], preferredTab = 'add', confirmLoading = false, onConfirm, onCancel, }) => { const { t } = useTranslation(); const isMobile = useIsMobile(); const normalizedAddModels = useMemo( () => normalizeModels(addModels), [addModels], ); const normalizedRemoveModels = useMemo( () => normalizeModels(removeModels), [removeModels], ); const [selectedAddModels, setSelectedAddModels] = useState([]); const [selectedRemoveModels, setSelectedRemoveModels] = useState([]); const [keyword, setKeyword] = useState(''); const [activeTab, setActiveTab] = useState('add'); const [partialSubmitConfirmed, setPartialSubmitConfirmed] = useState(false); const addTabEnabled = normalizedAddModels.length > 0; const removeTabEnabled = normalizedRemoveModels.length > 0; const filteredAddModels = useMemo( () => filterByKeyword(normalizedAddModels, keyword), [normalizedAddModels, keyword], ); const filteredRemoveModels = useMemo( () => filterByKeyword(normalizedRemoveModels, keyword), [normalizedRemoveModels, keyword], ); useEffect(() => { if (!visible) { return; } setSelectedAddModels([]); setSelectedRemoveModels([]); setKeyword(''); setPartialSubmitConfirmed(false); const normalizedPreferredTab = preferredTab === 'remove' ? 'remove' : 'add'; if (normalizedPreferredTab === 'remove' && removeTabEnabled) { setActiveTab('remove'); return; } if (normalizedPreferredTab === 'add' && addTabEnabled) { setActiveTab('add'); return; } setActiveTab(addTabEnabled ? 'add' : 'remove'); }, [visible, addTabEnabled, removeTabEnabled, preferredTab]); const currentModels = activeTab === 'add' ? filteredAddModels : filteredRemoveModels; const currentSelectedModels = activeTab === 'add' ? selectedAddModels : selectedRemoveModels; const currentSetSelectedModels = activeTab === 'add' ? setSelectedAddModels : setSelectedRemoveModels; const selectedAddCount = selectedAddModels.length; const selectedRemoveCount = selectedRemoveModels.length; const checkedCount = currentModels.filter((model) => currentSelectedModels.includes(model), ).length; const isAllChecked = currentModels.length > 0 && checkedCount === currentModels.length; const isIndeterminate = checkedCount > 0 && checkedCount < currentModels.length; const handleToggleAllCurrent = (checked) => { if (checked) { const merged = normalizeModels([ ...currentSelectedModels, ...currentModels, ]); currentSetSelectedModels(merged); return; } const currentSet = new Set(currentModels); currentSetSelectedModels( currentSelectedModels.filter((model) => !currentSet.has(model)), ); }; const tabList = [ { itemKey: 'add', tab: `${t('新增模型')} (${selectedAddCount}/${normalizedAddModels.length})`, disabled: !addTabEnabled, }, { itemKey: 'remove', tab: `${t('删除模型')} (${selectedRemoveCount}/${normalizedRemoveModels.length})`, disabled: !removeTabEnabled, }, ]; const submitSelectedChanges = () => { onConfirm?.({ addModels: selectedAddModels, removeModels: selectedRemoveModels, }); }; const handleSubmit = () => { const hasAnySelected = selectedAddCount > 0 || selectedRemoveCount > 0; if (!hasAnySelected) { submitSelectedChanges(); return; } const hasBothPending = addTabEnabled && removeTabEnabled; const hasUnselectedAdd = addTabEnabled && selectedAddCount === 0; const hasUnselectedRemove = removeTabEnabled && selectedRemoveCount === 0; if (hasBothPending && (hasUnselectedAdd || hasUnselectedRemove)) { if (partialSubmitConfirmed) { submitSelectedChanges(); return; } const missingTab = hasUnselectedAdd ? 'add' : 'remove'; const missingType = hasUnselectedAdd ? t('新增') : t('删除'); const missingCount = hasUnselectedAdd ? normalizedAddModels.length : normalizedRemoveModels.length; setActiveTab(missingTab); Modal.confirm({ title: t('仍有未处理项'), content: t( '你还没有处理{{type}}模型({{count}}个)。是否仅提交当前已勾选内容?', { type: missingType, count: missingCount, }, ), okText: t('仅提交已勾选'), cancelText: t('去处理{{type}}', { type: missingType }), centered: true, onOk: () => { setPartialSubmitConfirmed(true); submitSelectedChanges(); }, }); return; } submitSelectedChanges(); }; return (
{t( '可勾选需要执行的变更:新增会加入渠道模型列表,删除会从渠道模型列表移除。', )} setActiveTab(key)} />
{t('新增已选 {{selected}} / {{total}}', { selected: selectedAddCount, total: normalizedAddModels.length, })} {t('删除已选 {{selected}} / {{total}}', { selected: selectedRemoveCount, total: normalizedRemoveModels.length, })}
} placeholder={t('搜索模型')} value={keyword} onChange={(value) => setKeyword(value)} showClear />
{currentModels.length === 0 ? ( } darkModeImage={ } description={t('暂无匹配模型')} style={{ padding: 24 }} /> ) : ( currentSetSelectedModels(normalizeModels(values)) } >
{currentModels.map((model) => ( {model} ))}
)}
{t('已选择 {{selected}} / {{total}}', { selected: checkedCount, total: currentModels.length, })} handleToggleAllCurrent(e.target.checked)} />
); }; export default ChannelUpstreamUpdateModal;