| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- /*
- 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 <https://www.gnu.org/licenses/>.
- 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 strong>{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: (
- <div className='flex items-center gap-2'>
- <Checkbox
- checked={headerChecked}
- indeterminate={headerIndeterminate}
- onChange={onHeaderChange}
- />
- <Text>{label}</Text>
- </div>
- ),
- dataIndex: fieldKey,
- render: (_, record) => {
- const f = (record.fields || []).find((x) => x.field === fieldKey);
- if (!f) return <Text type='tertiary'>-</Text>;
- const checked = selections[record.model_name]?.has(fieldKey) || false;
- return (
- <Checkbox
- checked={checked}
- onChange={(e) =>
- toggleField(record.model_name, fieldKey, e?.target?.checked)
- }
- >
- <Popover
- trigger='hover'
- position='top'
- content={
- <div className='p-2 max-w-[520px]'>
- <div className='mb-2'>
- <Text type='tertiary' size='small'>
- {t('本地')}
- </Text>
- <pre className='whitespace-pre-wrap m-0'>
- {formatValue(f.local)}
- </pre>
- </div>
- <div>
- <Text type='tertiary' size='small'>
- {t('官方')}
- </Text>
- <pre className='whitespace-pre-wrap m-0'>
- {formatValue(f.upstream)}
- </pre>
- </div>
- </div>
- }
- >
- <Tag
- color='white'
- size='small'
- prefixIcon={<MousePointerClick size={14} />}
- >
- {t('点击查看差异')}
- </Tag>
- </Popover>
- </Checkbox>
- );
- },
- };
- });
- 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 (
- <Modal
- title={t('选择要覆盖的冲突项')}
- visible={visible}
- onCancel={onClose}
- onOk={handleOk}
- confirmLoading={loading}
- okText={t('应用覆盖')}
- cancelText={t('取消')}
- width={isMobile ? '100%' : 1000}
- >
- {dataSource.length === 0 ? (
- <Empty description={t('无冲突项')} className='p-6' />
- ) : (
- <>
- <div className='mb-3 text-[var(--semi-color-text-2)]'>
- {t('仅会覆盖你勾选的字段,未勾选的字段保持本地不变。')}
- </div>
- <Table
- columns={columns}
- dataSource={dataSource}
- pagination={false}
- scroll={{ x: 'max-content' }}
- />
- </>
- )}
- </Modal>
- );
- };
- export default UpstreamConflictModal;
|