Browse Source

Merge pull request #3166 from somnifex/main

为渠道参数覆盖可视化规则提供拖拽排序支持
Calcium-Ion 2 months ago
parent
commit
ac72f90fc5
2 changed files with 155 additions and 20 deletions
  1. 1 0
      .gitignore
  2. 154 20
      web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx

+ 1 - 0
.gitignore

@@ -1,6 +1,7 @@
 .idea
 .vscode
 .zed
+.history
 upload
 *.exe
 *.db

+ 154 - 20
web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx

@@ -34,7 +34,7 @@ import {
   TextArea,
   Typography,
 } from '@douyinfe/semi-ui';
-import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
+import { IconDelete, IconMenu, IconPlus } from '@douyinfe/semi-icons';
 import { copy, showError, showSuccess, verifyJSON } from '../../../../helpers';
 import {
   CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
@@ -800,6 +800,38 @@ const normalizeOperation = (operation = {}) => ({
 
 const createDefaultOperation = () => normalizeOperation({ mode: 'set' });
 
+const reorderOperations = (
+  sourceOperations = [],
+  sourceId,
+  targetId,
+  position = 'before',
+) => {
+  if (!sourceId || !targetId || sourceId === targetId) {
+    return sourceOperations;
+  }
+
+  const sourceIndex = sourceOperations.findIndex((item) => item.id === sourceId);
+
+  if (sourceIndex < 0) {
+    return sourceOperations;
+  }
+
+  const nextOperations = [...sourceOperations];
+  const [moved] = nextOperations.splice(sourceIndex, 1);
+  let insertIndex = nextOperations.findIndex((item) => item.id === targetId);
+
+  if (insertIndex < 0) {
+    return sourceOperations;
+  }
+
+  if (position === 'after') {
+    insertIndex += 1;
+  }
+
+  nextOperations.splice(insertIndex, 0, moved);
+  return nextOperations;
+};
+
 const getOperationSummary = (operation = {}, index = 0) => {
   const mode = operation.mode || 'set';
   const modeLabel = OPERATION_MODE_LABEL_MAP[mode] || mode;
@@ -1037,6 +1069,9 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
   const [operationSearch, setOperationSearch] = useState('');
   const [selectedOperationId, setSelectedOperationId] = useState('');
   const [expandedConditionMap, setExpandedConditionMap] = useState({});
+  const [draggedOperationId, setDraggedOperationId] = useState('');
+  const [dragOverOperationId, setDragOverOperationId] = useState('');
+  const [dragOverPosition, setDragOverPosition] = useState('before');
   const [templateGroupKey, setTemplateGroupKey] = useState('basic');
   const [templatePresetKey, setTemplatePresetKey] = useState('operations_default');
   const [fieldGuideVisible, setFieldGuideVisible] = useState(false);
@@ -1055,6 +1090,9 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setOperationSearch('');
     setSelectedOperationId(nextState.operations[0]?.id || '');
     setExpandedConditionMap({});
+    setDraggedOperationId('');
+    setDragOverOperationId('');
+    setDragOverPosition('before');
     if (nextState.visualMode === 'legacy') {
       setTemplateGroupKey('basic');
       setTemplatePresetKey('legacy_default');
@@ -1583,6 +1621,67 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
     setSelectedOperationId(created.id);
   };
 
+  const resetOperationDragState = useCallback(() => {
+    setDraggedOperationId('');
+    setDragOverOperationId('');
+    setDragOverPosition('before');
+  }, []);
+
+  const moveOperation = useCallback(
+    (sourceId, targetId, position = 'before') => {
+      if (!sourceId || !targetId || sourceId === targetId) {
+        return;
+      }
+      setOperations((prev) =>
+        reorderOperations(prev, sourceId, targetId, position),
+      );
+      setSelectedOperationId(sourceId);
+    },
+    [],
+  );
+
+  const handleOperationDragStart = useCallback((event, operationId) => {
+    setDraggedOperationId(operationId);
+    setSelectedOperationId(operationId);
+    event.dataTransfer.effectAllowed = 'move';
+    event.dataTransfer.setData('text/plain', operationId);
+  }, []);
+
+  const handleOperationDragOver = useCallback(
+    (event, operationId) => {
+      event.preventDefault();
+      if (!draggedOperationId || draggedOperationId === operationId) {
+        return;
+      }
+      const rect = event.currentTarget.getBoundingClientRect();
+      const position =
+        event.clientY - rect.top > rect.height / 2 ? 'after' : 'before';
+      setDragOverOperationId(operationId);
+      setDragOverPosition(position);
+      event.dataTransfer.dropEffect = 'move';
+    },
+    [draggedOperationId],
+  );
+
+  const handleOperationDrop = useCallback(
+    (event, operationId) => {
+      event.preventDefault();
+      const sourceId =
+        draggedOperationId || event.dataTransfer.getData('text/plain');
+      const position =
+        dragOverOperationId === operationId ? dragOverPosition : 'before';
+      moveOperation(sourceId, operationId, position);
+      resetOperationDragState();
+    },
+    [
+      dragOverOperationId,
+      dragOverPosition,
+      draggedOperationId,
+      moveOperation,
+      resetOperationDragState,
+    ],
+  );
+
   const duplicateOperation = (operationId) => {
     let insertedId = '';
     setOperations((prev) => {
@@ -1941,14 +2040,31 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
                               );
                               const isActive =
                                 operation.id === selectedOperationId;
+                              const isDragging =
+                                operation.id === draggedOperationId;
+                              const isDropTarget =
+                                operation.id === dragOverOperationId &&
+                                draggedOperationId &&
+                                draggedOperationId !== operation.id;
                               return (
                                 <div
                                   key={operation.id}
                                   role='button'
                                   tabIndex={0}
+                                  draggable={operations.length > 1}
                                   onClick={() =>
                                     setSelectedOperationId(operation.id)
                                   }
+                                  onDragStart={(event) =>
+                                    handleOperationDragStart(event, operation.id)
+                                  }
+                                  onDragOver={(event) =>
+                                    handleOperationDragOver(event, operation.id)
+                                  }
+                                  onDrop={(event) =>
+                                    handleOperationDrop(event, operation.id)
+                                  }
+                                  onDragEnd={resetOperationDragState}
                                   onKeyDown={(event) => {
                                     if (
                                       event.key === 'Enter' ||
@@ -1966,35 +2082,53 @@ const ParamOverrideEditorModal = ({ visible, value, onSave, onCancel }) => {
                                     border: isActive
                                       ? '1px solid var(--semi-color-primary)'
                                       : '1px solid var(--semi-color-border)',
+                                    opacity: isDragging ? 0.6 : 1,
+                                    boxShadow: isDropTarget
+                                      ? dragOverPosition === 'after'
+                                        ? 'inset 0 -3px 0 var(--semi-color-primary)'
+                                        : 'inset 0 3px 0 var(--semi-color-primary)'
+                                      : 'none',
                                   }}
                                 >
                                   <div className='flex items-start justify-between gap-2'>
-                                    <div>
-                                      <Text strong>{`#${index + 1}`}</Text>
-                                      <Text
-                                        type='tertiary'
-                                        size='small'
-                                        className='block mt-1'
+                                    <div className='flex items-start gap-2 min-w-0'>
+                                      <div
+                                        className='flex-shrink-0'
+                                        style={{
+                                          color: 'var(--semi-color-text-2)',
+                                          cursor: operations.length > 1 ? 'grab' : 'default',
+                                          marginTop: 1,
+                                        }}
                                       >
-                                        {getOperationSummary(operation, index)}
-                                      </Text>
-                                      {String(operation.description || '').trim() ? (
+                                        <IconMenu />
+                                      </div>
+                                      <div className='min-w-0'>
+                                        <Text strong>{`#${index + 1}`}</Text>
                                         <Text
                                           type='tertiary'
                                           size='small'
                                           className='block mt-1'
-                                          style={{
-                                            lineHeight: 1.5,
-                                            wordBreak: 'break-word',
-                                            overflow: 'hidden',
-                                            display: '-webkit-box',
-                                            WebkitLineClamp: 2,
-                                            WebkitBoxOrient: 'vertical',
-                                          }}
                                         >
-                                          {operation.description}
+                                          {getOperationSummary(operation, index)}
                                         </Text>
-                                      ) : null}
+                                        {String(operation.description || '').trim() ? (
+                                          <Text
+                                            type='tertiary'
+                                            size='small'
+                                            className='block mt-1'
+                                            style={{
+                                              lineHeight: 1.5,
+                                              wordBreak: 'break-word',
+                                              overflow: 'hidden',
+                                              display: '-webkit-box',
+                                              WebkitLineClamp: 2,
+                                              WebkitBoxOrient: 'vertical',
+                                            }}
+                                          >
+                                            {operation.description}
+                                          </Text>
+                                        ) : null}
+                                      </div>
                                     </div>
                                     <Tag size='small' color='grey'>
                                       {(operation.conditions || []).length}