/*
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;