/* 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 { Modal, Table, Checkbox, Typography, Empty, Tag, Popover, } from '@douyinfe/semi-ui'; import { MousePointerClick } from 'lucide-react'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const { Text } = Typography; const FIELD_LABELS = { description: '描述', icon: '图标', tags: '标签', vendor: '供应商', name_rule: '命名规则', status: '状态', }; const FIELD_KEYS = Object.keys(FIELD_LABELS); const UpstreamConflictModal = ({ visible, onClose, conflicts = [], onSubmit, t, loading = false, }) => { const [selections, setSelections] = useState({}); const isMobile = useIsMobile(); const formatValue = (v) => { if (v === null || v === undefined) return '-'; if (typeof v === 'string') return v || '-'; try { return JSON.stringify(v, null, 2); } catch (_) { return String(v); } }; useEffect(() => { if (visible) { const init = {}; conflicts.forEach((item) => { init[item.model_name] = new Set(); }); setSelections(init); } else { setSelections({}); } }, [visible, conflicts]); const toggleField = (modelName, field, checked) => { setSelections((prev) => { const next = { ...prev }; const set = new Set(next[modelName] || []); if (checked) set.add(field); else set.delete(field); next[modelName] = set; return next; }); }; const columns = useMemo(() => { const base = [ { title: t('模型'), dataIndex: 'model_name', fixed: 'left', render: (text) => {text}, }, ]; const cols = FIELD_KEYS.map((fieldKey) => { const rawLabel = FIELD_LABELS[fieldKey] || fieldKey; const label = t(rawLabel); // 统计列头复选框状态(仅统计存在该字段冲突的行) const presentRows = (conflicts || []).filter((row) => (row.fields || []).some((f) => f.field === fieldKey), ); const selectedCount = presentRows.filter((row) => selections[row.model_name]?.has(fieldKey), ).length; const allCount = presentRows.length; if (allCount === 0) { return null; // 若此字段在所有行中都不存在,则不展示该列 } const headerChecked = allCount > 0 && selectedCount === allCount; const headerIndeterminate = selectedCount > 0 && selectedCount < allCount; const onHeaderChange = (e) => { const checked = e?.target?.checked; setSelections((prev) => { const next = { ...prev }; (conflicts || []).forEach((row) => { const hasField = (row.fields || []).some( (f) => f.field === fieldKey, ); if (!hasField) return; const set = new Set(next[row.model_name] || []); if (checked) set.add(fieldKey); else set.delete(fieldKey); next[row.model_name] = set; }); return next; }); }; return { title: (
{label}
), dataIndex: fieldKey, render: (_, record) => { const f = (record.fields || []).find((x) => x.field === fieldKey); if (!f) return -; const checked = selections[record.model_name]?.has(fieldKey) || false; return ( toggleField(record.model_name, fieldKey, e?.target?.checked) } >
{t('本地')}
                        {formatValue(f.local)}
                      
{t('官方')}
                        {formatValue(f.upstream)}
                      
} > } > {t('点击查看差异')}
); }, }; }); return [...base, ...cols.filter(Boolean)]; }, [t, selections, conflicts]); const dataSource = conflicts.map((c) => ({ key: c.model_name, model_name: c.model_name, fields: c.fields || [], })); const handleOk = async () => { const payload = Object.entries(selections) .map(([modelName, set]) => ({ model_name: modelName, fields: Array.from(set || []), })) .filter((x) => x.fields.length > 0); if (payload.length === 0) { onClose?.(); return; } const ok = await onSubmit?.(payload); if (ok) onClose?.(); }; return ( {dataSource.length === 0 ? ( ) : ( <>
{t('仅会覆盖你勾选的字段,未勾选的字段保持本地不变。')}
)} ); }; export default UpstreamConflictModal;