mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-23 20:22:12 +08:00
fix(profile): virtualize editor lists for rule/proxy/group editors (#7944)
* fix(profile): virtualize editor lists for rule/proxy/group editors * refactor(list): remove feedbackClone from sortable item and list integration * refactor(profile): use type instead of export type for category * refactor(ui): rename measureElementRef to measureElement in sortable components
This commit is contained in:
@@ -13,12 +13,24 @@ interface SortableItemProps {
|
||||
index: number
|
||||
group?: string
|
||||
disabled?: boolean
|
||||
// 用于给 tanstack virtual 列表测量当前拖动元素尺寸
|
||||
measureElement?: (element: HTMLDivElement | null) => void
|
||||
dataIndex?: number
|
||||
style?: CSSProperties
|
||||
children: ReactNode | ((props: SortableItemRenderProps) => ReactNode)
|
||||
}
|
||||
|
||||
export const SortableItem = (props: SortableItemProps) => {
|
||||
const { id, index, group, disabled, children, style } = props
|
||||
const {
|
||||
id,
|
||||
index,
|
||||
group,
|
||||
disabled,
|
||||
measureElement,
|
||||
dataIndex,
|
||||
children,
|
||||
style,
|
||||
} = props
|
||||
const { ref, handleRef, sortable, isDragging } = useSortable({
|
||||
id,
|
||||
index,
|
||||
@@ -30,8 +42,9 @@ export const SortableItem = (props: SortableItemProps) => {
|
||||
(element: HTMLDivElement | null) => {
|
||||
handleRef(element?.querySelector('[data-sortable-handle]') ?? null)
|
||||
ref(element)
|
||||
measureElement?.(element)
|
||||
},
|
||||
[handleRef, ref],
|
||||
[handleRef, ref, measureElement],
|
||||
)
|
||||
|
||||
const mergedStyle: CSSProperties = {
|
||||
@@ -53,7 +66,7 @@ export const SortableItem = (props: SortableItemProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setRef} style={mergedStyle}>
|
||||
<div ref={setRef} data-index={dataIndex} style={mergedStyle}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { move } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { type CSSProperties, type ReactNode, useRef, useState } from 'react'
|
||||
|
||||
import { SortableItem } from '../base'
|
||||
|
||||
type GroupedVirtualCategory = 'prepend' | 'original' | 'append'
|
||||
|
||||
export interface GroupedVirtualItem<T> {
|
||||
id: string
|
||||
category: GroupedVirtualCategory
|
||||
item: T
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function buildGroupedItems<T>(
|
||||
prepend: T[],
|
||||
original: T[],
|
||||
append: T[],
|
||||
getKey: (item: T) => string,
|
||||
): GroupedVirtualItem<T>[] {
|
||||
const build = (
|
||||
category: GroupedVirtualCategory,
|
||||
list: T[],
|
||||
): GroupedVirtualItem<T>[] =>
|
||||
list.map((item) => ({
|
||||
id: `${category}:${getKey(item)}`,
|
||||
category,
|
||||
item,
|
||||
}))
|
||||
|
||||
return [
|
||||
...build('prepend', prepend),
|
||||
...build('original', original),
|
||||
...build('append', append),
|
||||
]
|
||||
}
|
||||
|
||||
interface GroupedVirtualListProps<T> {
|
||||
items: GroupedVirtualItem<T>[]
|
||||
estimateSize?: number
|
||||
renderItem: (entry: GroupedVirtualItem<T>) => ReactNode
|
||||
onReorder: (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => void
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于编辑规则/节点/代理组功能的 prepend/original/append 分组虚拟化列表
|
||||
*/
|
||||
export function GroupedVirtualList<T>(props: GroupedVirtualListProps<T>) {
|
||||
const {
|
||||
items: itemsProp,
|
||||
estimateSize = 56,
|
||||
renderItem,
|
||||
onReorder,
|
||||
style,
|
||||
} = props
|
||||
const [items, setItems] = useState(itemsProp)
|
||||
const [prevItemsProp, setPrevItemsProp] = useState(itemsProp)
|
||||
const snapshotRef = useRef(itemsProp)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
if (itemsProp !== prevItemsProp) {
|
||||
setPrevItemsProp(itemsProp)
|
||||
setItems(itemsProp)
|
||||
}
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan: 15,
|
||||
gap: 8,
|
||||
getItemKey: (index) => items[index]?.id ?? index,
|
||||
})
|
||||
|
||||
const onDragStart = () => {
|
||||
snapshotRef.current = items
|
||||
}
|
||||
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (!source || !target) return
|
||||
|
||||
const sourceEntry = items.find((entry) => entry.id === source.id)
|
||||
const targetEntry = items.find((entry) => entry.id === target.id)
|
||||
if (
|
||||
!sourceEntry ||
|
||||
!targetEntry ||
|
||||
sourceEntry.category !== targetEntry.category
|
||||
) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
setItems((current) => move(current, event))
|
||||
}
|
||||
|
||||
const onDragEnd = (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
|
||||
if (event.canceled) {
|
||||
setItems(snapshotRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const snapshot = snapshotRef.current
|
||||
const sourceEntry = snapshot.find((entry) => entry.id === source.id)
|
||||
if (!sourceEntry) return
|
||||
|
||||
const { category } = sourceEntry
|
||||
if (category !== 'prepend' && category !== 'append') return
|
||||
|
||||
const activeIndex = snapshot
|
||||
.filter((entry) => entry.category === category)
|
||||
.findIndex((entry) => entry.id === source.id)
|
||||
const overIndex = items
|
||||
.filter((entry) => entry.category === category)
|
||||
.findIndex((entry) => entry.id === source.id)
|
||||
if (activeIndex < 0 || overIndex < 0 || activeIndex === overIndex) return
|
||||
|
||||
onReorder(category, activeIndex, overIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<div ref={scrollRef} style={{ ...style, overflow: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
position: 'relative',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vi) => {
|
||||
const entry = items[vi.index]
|
||||
if (!entry) return null
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
key={vi.key}
|
||||
id={entry.id}
|
||||
index={vi.index}
|
||||
disabled={entry.category === 'original'}
|
||||
measureElement={virtualizer.measureElement}
|
||||
dataIndex={vi.index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${vi.start}px`,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{renderItem(entry)}
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</DragDropProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -43,13 +35,7 @@ import {
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
|
||||
import { GroupItem } from '@/components/profile/group-item'
|
||||
import {
|
||||
getNetworkInterfaces,
|
||||
@@ -63,6 +49,12 @@ import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import { MONACO_FONT_FAMILY } from '@/utils/font-family'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
import {
|
||||
buildGroupedItems,
|
||||
type GroupedVirtualItem,
|
||||
GroupedVirtualList,
|
||||
} from './grouped-virtual-list'
|
||||
|
||||
interface Props {
|
||||
proxiesUid: string
|
||||
mergeUid: string
|
||||
@@ -203,106 +195,71 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
[appendSeq, match],
|
||||
)
|
||||
|
||||
const renderItem = (index: number): React.ReactNode => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
buildGroupedItems(
|
||||
filteredPrependSeq,
|
||||
filteredGroupList,
|
||||
filteredAppendSeq,
|
||||
(group) => group.name,
|
||||
),
|
||||
[filteredPrependSeq, filteredGroupList, filteredAppendSeq],
|
||||
)
|
||||
|
||||
const renderItem = (entry: GroupedVirtualItem<IProxyGroupConfig>) => {
|
||||
const { category, item } = entry
|
||||
|
||||
if (category === 'original') {
|
||||
const isDeleted = deleteSeq.includes(item.name)
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v.name !== item.name),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredGroupList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<GroupItem
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
<GroupItem
|
||||
type={isDeleted ? 'delete' : 'original'}
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
if (isDeleted) {
|
||||
setDeleteSeq(deleteSeq.filter((v) => v !== item.name))
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, item.name])
|
||||
}
|
||||
group={filteredGroupList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredGroupList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredGroupList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
|
||||
if (category === 'prepend') {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
<GroupItem
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupItem
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
const onReorder = (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => {
|
||||
const list = category === 'prepend' ? prependSeq : appendSeq
|
||||
const filtered =
|
||||
category === 'prepend' ? filteredPrependSeq : filteredAppendSeq
|
||||
const setList = category === 'prepend' ? setPrependSeq : setAppendSeq
|
||||
const activeRealIndex = findRealIndex(list, filtered, activeIndex)
|
||||
const overRealIndex = findRealIndex(list, filtered, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
@@ -311,55 +268,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
setList(arrayMove(list, activeRealIndex, overRealIndex))
|
||||
}
|
||||
|
||||
// prevent drag over if source and target are in different groups
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
@@ -1152,22 +1063,12 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredGroupList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<GroupedVirtualList
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
onReorder={onReorder}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -35,12 +27,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor } from '@/components/base'
|
||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -50,6 +37,12 @@ import { MONACO_FONT_FAMILY } from '@/utils/font-family'
|
||||
import parseUri from '@/utils/uri-parser'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
import {
|
||||
buildGroupedItems,
|
||||
type GroupedVirtualItem,
|
||||
GroupedVirtualList,
|
||||
} from './grouped-virtual-list'
|
||||
|
||||
interface Props {
|
||||
profileUid: string
|
||||
property: string
|
||||
@@ -107,106 +100,71 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
[appendSeq, match],
|
||||
)
|
||||
|
||||
const renderItem = (index: number): React.ReactNode => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
buildGroupedItems(
|
||||
filteredPrependSeq,
|
||||
filteredProxyList,
|
||||
filteredAppendSeq,
|
||||
(proxy) => proxy.name,
|
||||
),
|
||||
[filteredPrependSeq, filteredProxyList, filteredAppendSeq],
|
||||
)
|
||||
|
||||
const renderItem = (entry: GroupedVirtualItem<IProxyConfig>) => {
|
||||
const { category, item } = entry
|
||||
|
||||
if (category === 'original') {
|
||||
const isDeleted = deleteSeq.includes(item.name)
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(
|
||||
prependSeq.filter((v) => v.name !== item.name),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredProxyList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<ProxyItem
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
<ProxyItem
|
||||
type={isDeleted ? 'delete' : 'original'}
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
if (isDeleted) {
|
||||
setDeleteSeq(deleteSeq.filter((v) => v !== item.name))
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, item.name])
|
||||
}
|
||||
proxy={filteredProxyList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredProxyList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredProxyList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
|
||||
if (category === 'prepend') {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
<ProxyItem
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ProxyItem
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
const onReorder = (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => {
|
||||
const list = category === 'prepend' ? prependSeq : appendSeq
|
||||
const filtered =
|
||||
category === 'prepend' ? filteredPrependSeq : filteredAppendSeq
|
||||
const setList = category === 'prepend' ? setPrependSeq : setAppendSeq
|
||||
const activeRealIndex = findRealIndex(list, filtered, activeIndex)
|
||||
const overRealIndex = findRealIndex(list, filtered, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
@@ -215,52 +173,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
setList(arrayMove(list, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
||||
const handleParseAsync = (cb: (proxies: IProxyConfig[]) => void) => {
|
||||
const proxies: IProxyConfig[] = []
|
||||
@@ -522,22 +437,12 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredProxyList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<GroupedVirtualList
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
onReorder={onReorder}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -36,13 +29,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
|
||||
import { RuleItem } from '@/components/profile/rule-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -54,6 +41,12 @@ import getSystem from '@/utils/get-system'
|
||||
import { isValidIpCidr } from '@/utils/network'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
import {
|
||||
buildGroupedItems,
|
||||
type GroupedVirtualItem,
|
||||
GroupedVirtualList,
|
||||
} from './grouped-virtual-list'
|
||||
|
||||
interface Props {
|
||||
groupsUid: string
|
||||
mergeUid: string
|
||||
@@ -302,117 +295,83 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
[appendSeq, match],
|
||||
)
|
||||
|
||||
const renderItem = (index: number): React.ReactNode => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
buildGroupedItems(
|
||||
filteredPrependSeq,
|
||||
filteredRuleList,
|
||||
filteredAppendSeq,
|
||||
(rule) => rule,
|
||||
),
|
||||
[filteredPrependSeq, filteredRuleList, filteredAppendSeq],
|
||||
)
|
||||
|
||||
const renderItem = (entry: GroupedVirtualItem<string>) => {
|
||||
const { category, item } = entry
|
||||
|
||||
if (category === 'original') {
|
||||
const isDeleted = deleteSeq.includes(item)
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`prepend:${item}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onAppend={() => {
|
||||
setAppendSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredRuleList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<RuleItem
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? 'delete'
|
||||
: 'original'
|
||||
<RuleItem
|
||||
type={isDeleted ? 'delete' : 'original'}
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
if (isDeleted) {
|
||||
setDeleteSeq(deleteSeq.filter((v) => v !== item))
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, item])
|
||||
}
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
}
|
||||
|
||||
if (category === 'prepend') {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`append:${item}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onPrepend={() => {
|
||||
setPrependSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
</SortableItem>
|
||||
<RuleItem
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onAppend={() => {
|
||||
setAppendSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
})}
|
||||
</>
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RuleItem
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
onPrepend={() => {
|
||||
setPrependSeq((prev) =>
|
||||
prev.includes(item) ? prev : [...prev, item],
|
||||
)
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list)
|
||||
const [removed] = result.splice(startIndex, 1)
|
||||
result.splice(endIndex, 0, removed)
|
||||
return result
|
||||
}
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
const onReorder = (
|
||||
category: 'prepend' | 'append',
|
||||
activeIndex: number,
|
||||
overIndex: number,
|
||||
) => {
|
||||
const list = category === 'prepend' ? prependSeq : appendSeq
|
||||
const filtered =
|
||||
category === 'prepend' ? filteredPrependSeq : filteredAppendSeq
|
||||
const setList = category === 'prepend' ? setPrependSeq : setAppendSeq
|
||||
const activeRealIndex = findRealIndex(list, filtered, activeIndex)
|
||||
const overRealIndex = findRealIndex(list, filtered, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
@@ -421,52 +380,9 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(reorder(prependSeq, activeRealIndex, overRealIndex))
|
||||
setList(arrayMove(list, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(reorder(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
@@ -854,22 +770,12 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredRuleList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
<GroupedVirtualList
|
||||
items={items}
|
||||
renderItem={renderItem}
|
||||
onReorder={onReorder}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user