diff --git a/entrypoints/common/components/Root.tsx b/entrypoints/common/components/Root.tsx index 682eecf..818952b 100644 --- a/entrypoints/common/components/Root.tsx +++ b/entrypoints/common/components/Root.tsx @@ -70,6 +70,12 @@ export default function Root({ children }: { children: React.ReactNode }) { colorPrimary: primaryColor || PRIMARY_COLOR, colorBgContainer: themeTypeConfig.bgColor || '#fff', }, + components: { + Tree: { + motion: false, + algorithm: true, + }, + }, }} > The left-side list displays the tags, and the secondary menus represent the tab groups. The list supports searching for both tags and tab groups. The right-side panel displays all the tab groups and the tabs within each tab group. +
  • The tabs in the right-side panel support drag-and-drop sorting and cross-group movement. You can also drag the tabs from the right-side panel to the groups in the left-side list. Additionally, you can click the “Move To” button to move tab group and tabs.
  • +
  • When a tab group is locked, the group and its tabs cannot be removed or moved out. However, tabs from other tab groups can be moved into the locked group. To remove a tab group, you need to first unlock it.
  • +
  • When a tab group is starred, it will be placed at the top of the current category. Moving other tab groups to a position before the starred tab group will automatically star them as well. If a starred tab group is moved to a position after a non-starred tab group, the star status will be automatically released.
  • +
  • The Staging Area is a special category, fixed at the top of the list. When sending tabs, they will be saved to the Staging Area.
  • +
  • When opening a tab group, each of the tabs within the tab group will be opened in browser. If the "Automatically remove tabs from list when opening tabs or tab group" setting is enabled, the tabs will be removed from the list after they are opened.
  • +
  • The "Recover" action means recover the tags / tab groups / tabs from the "Recycle Bin" to the "List" home.
  • +
  • The "Ascending" and "Descending" only work for unstarred tab groups in the current category.
  • +
  • Tags / tab groups / tabs support drag-and-drop sorting. When a category/tab group is selected, it can be sorted using shortcut keys. The shortcut key mapping is shown in the table below:
  • + `, 'home.moveTo.missingTip': 'Please select a {type}', 'home.moveTo.mergeLabel': 'Merge Duplicate Items ?', diff --git a/entrypoints/common/locale/modules/home/zhCN.ts b/entrypoints/common/locale/modules/home/zhCN.ts index 42ce160..f436e3b 100644 --- a/entrypoints/common/locale/modules/home/zhCN.ts +++ b/entrypoints/common/locale/modules/home/zhCN.ts @@ -45,14 +45,17 @@ export default { 'home.tab.name': '名称', 'home.tab.url': '网址', - 'home.help.content.1': '1、左侧列表一级菜单表示分类,二级菜单表示标签组,左侧列表支持分类和标签组的搜索;右侧面板展示的是当前选中分类中的所有标签组以及标签组中的标签页。', - 'home.help.content.2': '2、中转站是一个特殊分类,固定在第一位,发送标签页时会自动保存到中转站,您可根据需要处理中转站中的标签组和标签页(移动、删除等操作)。', - 'home.help.content.3': '3、标签组锁定后可以移动,该标签组以及组内的标签页,将禁止删除和移出,但可以将其他标签组的标签页移入该标签组;如果想要删除或移出,可先解锁该标签组。', - 'home.help.content.4': '4、标签组星标后将在当前分类中置顶,移动其他标签组到星标状态的标签组之前,将自动被星标;移动星标状态的标签组到非星标的标签组之后,将自动解除星标状态。', - 'home.help.content.5': '5、点击恢复标签组时,会在浏览器中依次打开该标签组中的标签页,如果设置了“恢复标签页时自动从列表中删除”,则该标签组中的标签页会被删除。', - 'home.help.content.6': '6、“恢复”操作表示在浏览器中打开标签组中的标签页,而“还原”操作表示将回收站中的标签组还原到首页列表中。', - 'home.help.content.7': '7、“升序”和“降序”功能只对当前分类中的非星标的标签组进行排序。', - 'home.help.content.8': '8、分类/标签组/标签页支持拖拽排序,当分类/标签组处在选中状态时,可通过快捷键进行排序,快捷键映射见下表:', + 'home.help.content': ` +
  • 左侧列表一级菜单表示分类,二级菜单表示标签组,左侧列表支持分类和标签组的搜索;右侧面板展示的是当前选中分类中的所有标签组以及标签组中的标签页。
  • +
  • 右侧面板中的标签页支持拖拽排序以及跨组移动,您还可以将右侧面板中的标签页拖拽到左侧列表的标签组中,以实现跨分类移动;另外可点击“移动到”按钮进行标签组和标签页的移动操作。
  • +
  • 标签组锁定后可以移动,该标签组以及组内的标签页,将禁止删除和移出,但可以将其他标签组的标签页移入该标签组;如果想要删除或移出,可先解锁该标签组。
  • +
  • 标签组星标后将在当前分类中置顶,移动其他标签组到星标状态的标签组之前,将自动被星标;移动星标状态的标签组到非星标的标签组之后,将自动解除星标状态。
  • +
  • 中转站是一个特殊分类,固定在第一位,发送标签页时会自动保存到中转站,您可根据需要处理中转站中的标签组和标签页(移动、删除等操作)。
  • +
  • 点击打开标签组时,会在浏览器中依次打开该标签组中的标签页,如果设置了“打开标签页时自动从列表中删除”,则该标签组中的标签页会被删除。
  • +
  • “还原”操作表示将回收站中的标签组还原到首页列表中。
  • +
  • “升序”和“降序”功能只对当前分类中的非星标的标签组进行排序。
  • +
  • 分类/标签组/标签页支持拖拽排序,当分类/标签组处在选中状态时,可通过快捷键进行排序,快捷键映射见下表:
  • + `, 'home.moveTo.missingTip': '请选择{type}', 'home.moveTo.mergeLabel': '是否合并重复项', diff --git a/entrypoints/common/locale/modules/settings/enUS.ts b/entrypoints/common/locale/modules/settings/enUS.ts index b7df857..269e6ee 100644 --- a/entrypoints/common/locale/modules/settings/enUS.ts +++ b/entrypoints/common/locale/modules/settings/enUS.ts @@ -17,7 +17,7 @@ export default { 'settings.closeTabsAfterSendTabs': 'When sending tabs - whether to automatically close the tabs ?', 'settings.closeTabsAfterSendTabs.yes': 'Automatically close', 'settings.closeTabsAfterSendTabs.no': 'Do not', - 'settings.deleteAfterRestore': 'When opening tabs or tab groups - whether to remove the tabs ?', + 'settings.deleteAfterRestore': 'When opening tabs or tab group - whether to remove the tabs ?', 'settings.deleteAfterRestore.yes': 'Remove (pinned tabs remain)', 'settings.deleteAfterRestore.no': 'All remain (recommended)', 'settings.deleteUnlockedEmptyGroup': 'When clearing tabs - whether to remove the empty group ?', diff --git a/entrypoints/common/style/Common.styled.tsx b/entrypoints/common/style/Common.styled.tsx index f38d296..1bec2da 100644 --- a/entrypoints/common/style/Common.styled.tsx +++ b/entrypoints/common/style/Common.styled.tsx @@ -74,23 +74,23 @@ export const GlobalStyle = createGlobalStyle` color: ${(props) => props.theme.colorText || 'rgba(0, 0, 0, 0.88)'}; } - ::-webkit-scrollbar { - width: 10px; - height: 10px; + ::-webkit-scrollbar, .ant-tree-list-scrollbar { + width: 8px !important; + height: 8px !important; } ::-webkit-scrollbar-track { - border-radius: 5px; - background: var(--bg-color, #fff); + border-radius: 4px; + background: var(--bg-color, #fff) !important; } - ::-webkit-scrollbar-thumb { - border-radius: 5px; - background: ${(props) => props.theme.type === 'light' ? '#d9d9d9' : '#555'}; + ::-webkit-scrollbar-thumb, .ant-tree-list-scrollbar-thumb { + border-radius: 4px; + background: ${(props) => `${props.theme.type === 'light' ? '#d9d9d9' : '#555'} !important`}; box-shadow:inset 0 0 4px rgba(0, 0, 0, .3); } - ::-webkit-scrollbar-thumb:hover { - background: ${(props) => props.theme.type === 'light' ? '#bfbfbf' : '#888'}; + ::-webkit-scrollbar-thumb:hover, .ant-tree-list-scrollbar-thumb:hover { + background: ${(props) => `${props.theme.type === 'light' ? '#bfbfbf' : '#888'} !important`}; } `; diff --git a/entrypoints/options/home/Home.styled.tsx b/entrypoints/options/home/Home.styled.tsx index a6687b1..c0ccc29 100644 --- a/entrypoints/options/home/Home.styled.tsx +++ b/entrypoints/options/home/Home.styled.tsx @@ -8,7 +8,6 @@ export const StyledSidebarWrapper = styled.div<{ $sidebarWidth?: number; }>` position: relative; - height: calc(100vh - 180px); .sidebar-inner-box { width: ${(props) => props.$sidebarWidth || 280}px; @@ -79,8 +78,8 @@ export const StyledSidebarWrapper = styled.div<{ .sidebar-tree-wrapper { flex: 1; height: 0; - padding: 10px 0; - overflow: auto; + // padding: 10px 0; + // overflow: auto; .no-data { padding: 16px 0; button { @@ -112,6 +111,7 @@ export const StyledListWrapper = styled.div<{ export const StyledTreeNodeItem = styled.div` display: flex; align-items: center; + padding-right: 8px; cursor: pointer; .tree-node-title { width: 0; @@ -144,6 +144,15 @@ export const StyledFooterWrapper = styled.div<{ $paddingLeft?: number }>` transition: padding 0.2s ease-in-out; `; +export const StyledHelpInfoBox = styled.div` + ul { + list-style-type: disc; + li { + margin-bottom: 8px; + } + } +`; + export default { name: 'option-home-styled', }; diff --git a/entrypoints/options/home/RenderTreeNode.tsx b/entrypoints/options/home/RenderTreeNode.tsx index 07227db..a76e48a 100644 --- a/entrypoints/options/home/RenderTreeNode.tsx +++ b/entrypoints/options/home/RenderTreeNode.tsx @@ -20,6 +20,7 @@ export default function RenderTreeNode({ selected, container, refreshKey, + virtual = false, onAction, onTabItemDrop, // 这个 onTabItemDrop 只是为了方便右侧面板的标签页拖拽到左侧树的标签组,左侧树中的 分类和标签组的拖拽由 antd 的 Tree 组件自带实现 onMoveTo @@ -81,22 +82,32 @@ export default function RenderTreeNode({ setModalVisible(false); }; + /* 直接采用antd Tree的scrollTo方法 */ useEffect(() => { setTimeout(() => { if (selected && container && nodeRef.current) { - const containerRect = container.getBoundingClientRect(); - const nodeRect = nodeRef.current.getBoundingClientRect(); + container?.scrollTo({ key: node.key, offset: 80 }) + } + }, 100); + }, [container, refreshKey, selected]); - const scrollTop = container.scrollTop; + /* 非虚拟滚动模式可以采用这个方法 */ + // useEffect(() => { + // setTimeout(() => { + // if (selected && container && nodeRef.current) { + // const containerRect = container.getBoundingClientRect(); + // const nodeRect = nodeRef.current.getBoundingClientRect(); - if (nodeRect.top < containerRect.top) { - container.scrollTo(0, scrollTop - 80); - } else if (nodeRect.bottom > containerRect.bottom) { - container.scrollTo(0, scrollTop + 80); - } - } - }, 300); - }, [container, refreshKey, selected]); + // const scrollTop = container.scrollTop; + + // if (nodeRect.top < containerRect.top) { + // container.scrollTo(0, scrollTop - 80); + // } else if (nodeRect.bottom > containerRect.bottom) { + // container.scrollTo(0, scrollTop + 80); + // } + // } + // }, 300); + // }, [container, refreshKey, selected]); return ( // 这个 DropComponent 只是为了方便右侧面板的标签页拖拽到左侧树的标签组,左侧树中的 分类和标签组的拖拽由 antd 的 Tree 组件自带实现 diff --git a/entrypoints/options/home/index.tsx b/entrypoints/options/home/index.tsx index e34be1c..ece12ed 100644 --- a/entrypoints/options/home/index.tsx +++ b/entrypoints/options/home/index.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import { useState, useMemo, useCallback, useRef } from 'react'; import { theme, Flex, @@ -28,6 +28,7 @@ import { StyledListWrapper, StyledSidebarWrapper, StyledFooterWrapper, + StyledHelpInfoBox, } from './Home.styled'; import ToggleSidebarBtn from '../components/ToggleSidebarBtn'; import SortingBtns from './SortingBtns'; @@ -50,6 +51,7 @@ export default function Home() { const { token } = theme.useToken(); const { $fmt } = useIntlUtls(); const listRef = useRef(null); + const treeRef = useRef(null); const { loading, countInfo, @@ -86,6 +88,7 @@ export default function Home() { const { hotkeyList } = useHotkeys({ onAction: handleHotkeyAction }); + const [treeBoxHeight, setTreeBoxHeight] = useState(400); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [confirmModalVisible, setConfirmModalVisible] = useState(false); const [helpDrawerVisible, setHelpDrawerVisible] = useState(false); @@ -159,6 +162,11 @@ export default function Home() { }, 30); }; + // 是否开启虚拟滚动(数据量大时开启虚拟滚动) + const virtual = useMemo(() => { + return (countInfo?.groupCount || 0) > 100 || (countInfo?.tabCount || 0) > 1200; + }, [countInfo]); + // 搜索过滤后的 treeData const searchTreeData = useMemo(() => { if (!searchValue) return treeData; @@ -242,16 +250,18 @@ export default function Home() { [selectedTagKey] ); + useEffect(() => { + const listHeight = listRef.current?.offsetHeight || 400; + setTreeBoxHeight(listHeight); + }, []); + return ( <> - +
    @@ -295,7 +305,11 @@ export default function Home() { - {/* @@ -316,7 +330,9 @@ export default function Home() { {searchTreeData?.length > 0 ? ( setHelpDrawerVisible(false)} width={500} > - -

    {$fmt('home.help.content.1')}

    -

    {$fmt('home.help.content.2')}

    -

    {$fmt('home.help.content.3')}

    -

    {$fmt('home.help.content.4')}

    -

    {$fmt('home.help.content.5')}

    -

    {$fmt('home.help.content.6')}

    -

    {$fmt('home.help.content.7')}

    -

    {$fmt('home.help.content.8')}

    + +
      -

      +

      {$fmt('common.hotkeys')}

      -
      + ); diff --git a/entrypoints/options/home/types.ts b/entrypoints/options/home/types.ts index e189c6b..39c6dd6 100644 --- a/entrypoints/options/home/types.ts +++ b/entrypoints/options/home/types.ts @@ -28,11 +28,14 @@ export type RenderTreeNodeActionProps = { export type RenderTreeNodeProps = { node: TreeDataNodeUnion; selected?: boolean; - container?: HTMLElement | null; + container?: + | (HTMLElement & { scrollTo?: (props: { key: React.Key; offset?: number }) => void }) + | null; refreshKey?: string; + virtual?: boolean; onAction?: (props: RenderTreeNodeActionProps) => void; onTabItemDrop?: DndTabItemOnDropCallback; - onMoveTo?: ({moveData, targetData}: MoveToCallbackProps) => void; + onMoveTo?: ({ moveData, targetData }: MoveToCallbackProps) => void; }; // 需要移动的数据 diff --git a/entrypoints/options/importExport/index.tsx b/entrypoints/options/importExport/index.tsx index 7257a4e..9ae2380 100644 --- a/entrypoints/options/importExport/index.tsx +++ b/entrypoints/options/importExport/index.tsx @@ -32,23 +32,28 @@ export default function ImportExport() { const [formatType, setFormatType] = useState(1); const [importMode, setImportMode] = useState('append'); const [exportFormatType, setExportFormatType] = useState(1); + const [importLoading, setImportLoading] = useState(false); const [exportLoading, setExportLoading] = useState(false); const [downloadLoading, setDownloadLoading] = useState(false); // 导入操作 const handleImport: FormProps['onFinish'] = async (values) => { - const { formatType, importContent } = values; - const formatOption = formatTypeOptions.find((option) => option.type === formatType); - const funcName = formatOption?.funcName || 'niceTab'; - try { - const tagList = extContentImporter?.[funcName]?.(importContent); - await tabListUtils.importTags(tagList, importMode); + setImportLoading(true); + setTimeout(async () => { + const { formatType, importContent } = values; + const formatOption = formatTypeOptions.find((option) => option.type === formatType); + const funcName = formatOption?.funcName || 'niceTab'; + try { + const tagList = extContentImporter?.[funcName]?.(importContent); + await tabListUtils.importTags(tagList, importMode); - messageApi.success($fmt('importExport.importSuccess')); - form.setFieldValue('importContent', ''); - } catch { - messageApi.error($fmt('importExport.importFailed')); - } + messageApi.success($fmt('importExport.importSuccess')); + form.setFieldValue('importContent', ''); + } catch { + messageApi.error($fmt('importExport.importFailed')); + } + setImportLoading(false); + }, 500); }; // 选择文件导入 const handleSelectFile: UploadProps['beforeUpload'] = (file) => { @@ -67,8 +72,10 @@ export default function ImportExport() { }; const handlePreview = async () => { setExportLoading(true); - await getExportContent(); - setExportLoading(false); + setTimeout(async () => { + await getExportContent(); + setExportLoading(false); + }, 500); }; // 获取导出文本内容 const getExportContent = useCallback(async () => { @@ -96,15 +103,17 @@ export default function ImportExport() { // 导出文件 const handleDownload = useCallback(async () => { setDownloadLoading(true); - const now = dayjs().format('YYYY-MM-DD_HHmmss'); - const ext = exportFormatType == 1 ? 'json' : 'txt'; - const fileType = exportFormatType == 1 ? 'application/json' : 'text/plain'; - const fileName = `export_${ - exportFormatType == 1 ? 'nice-tab' : 'one-tab' - }_${now}.${ext}`; - const content = exportContent || (await getExportContent()); - saveAs(new Blob([content], { type: `${fileType};charset=utf-8` }), fileName); - setDownloadLoading(false); + setTimeout(async () => { + const now = dayjs().format('YYYY-MM-DD_HHmmss'); + const ext = exportFormatType == 1 ? 'json' : 'txt'; + const fileType = exportFormatType == 1 ? 'application/json' : 'text/plain'; + const fileName = `export_${ + exportFormatType == 1 ? 'nice-tab' : 'one-tab' + }_${now}.${ext}`; + const content = exportContent || (await getExportContent()); + saveAs(new Blob([content], { type: `${fileType};charset=utf-8` }), fileName); + setDownloadLoading(false); + }, 500); }, [exportFormatType]); return ( @@ -154,7 +163,7 @@ export default function ImportExport() { - {+formatType === 1 && ( @@ -163,7 +172,9 @@ export default function ImportExport() { showUploadList={false} beforeUpload={handleSelectFile} > - + )} diff --git a/package.json b/package.json index 752ee92..9e1d741 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nice-tab", "description": "A nice, convenient, powerful tab manager [open source]", "private": true, - "version": "2.2.4", + "version": "2.2.6", "type": "module", "scripts": { "dev": "wxt",