diff --git a/apps/monkey/copymanga-enhance/package.json b/apps/monkey/copymanga-enhance/package.json index fa57fa6..f746696 100644 --- a/apps/monkey/copymanga-enhance/package.json +++ b/apps/monkey/copymanga-enhance/package.json @@ -13,6 +13,7 @@ "lint:type-check": "tsc -p tsconfig.json --noEmit", "lint:formatter": "oxfmt --check", "lint:oxlint": "oxlint", + "fix": "concurrently -m 1 --timings npm:fix:*", "fix:formatter": "oxfmt" }, "dependencies": { diff --git a/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/useImageList.ts b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/useImageList.ts index da684ad..8e596d6 100644 --- a/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/useImageList.ts +++ b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/useImageList.ts @@ -1,5 +1,5 @@ import flow from '@scripts/flow' -import { defineComponent, onMounted, readonly, ref, unref, watch } from '@scripts/gm-vue' +import { onMounted, readonly, ref, unref, watch } from '@scripts/gm-vue' import Image from '../component/Image' import WhitePage from '../component/WhitePage' import { DirectionMode, PageType } from '../constant' @@ -7,11 +7,8 @@ import useDirectionMode from './useDirectionMode' import useImageInfoMap from './useImageInfoMap' import useRawImageList from './useRawImageList' import useWhitePage from './useWhitePage' - -type ImageItem = { - component: ReturnType | string - props: { key: string; pageType: PageType } & Record -} +import { injectDataIndexInternal } from './utils/injectDataIndex' +import { ImageItem } from './utils/types' export default function useImageList() { const rawImageListRef = useRawImageList() @@ -116,36 +113,7 @@ export default function useImageList() { return tempList.flat() } const injectDataIndex = (list: ImageItem[]): ImageItem[] => { - let portraitCount = 0 - return list.map((item, index) => { - let targetIndex = index + 1 - let side = 'A' - if (item.props.pageType === PageType.LANDSCAPE) { - portraitCount = 0 - side = 'A' - } else { - portraitCount++ - if (DirectionMode.RTL === unref(directionModeRef)) { - if (portraitCount % 2 === 0) { - targetIndex -= 1 - } else { - targetIndex += 1 - } - side = portraitCount % 2 === 1 ? 'R' : 'L' - } - if (DirectionMode.LTR === unref(directionModeRef)) { - side = portraitCount % 2 === 1 ? 'L' : 'R' - } - } - return { - ...item, - props: { - ...item.props, - 'data-index': targetIndex, - 'data-side': side, - }, - } - }) + return injectDataIndexInternal(list, unref(directionModeRef)) } const imagesRef = ref([]) function refresh() { diff --git a/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/injectDataIndex.test.ts b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/injectDataIndex.test.ts new file mode 100644 index 0000000..20cebd7 --- /dev/null +++ b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/injectDataIndex.test.ts @@ -0,0 +1,456 @@ +import { describe, expect, test } from 'vitest' +import { DirectionMode, PageType } from '../../constant' +import { injectDataIndexInternal } from './injectDataIndex' +import { ImageItem } from './types' + +describe('injectDataIndexInternal', () => { + // 创建测试用的 ImageItem + const createImageItem = (key: string, pageType: PageType): ImageItem => ({ + component: 'div', + props: { key, pageType }, + }) + + test('should handle empty list', () => { + const result = injectDataIndexInternal([], DirectionMode.LTR) + expect(result).toEqual([]) + }) + + describe('LTR mode', () => { + test('should handle landscape pages in LTR mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.LANDSCAPE), + createImageItem('img-2', PageType.LANDSCAPE), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(3) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('A') + }) + + test('should handle portrait pages in LTR mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.PORTRAIT), + createImageItem('img-3', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(4) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('L') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('R') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('L') + expect(result[3].props['data-index']).toBe(4) + expect(result[3].props['data-side']).toBe('R') + }) + + test('should handle mixed landscape and portrait pages in LTR mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.PORTRAIT), + createImageItem('img-3', PageType.LANDSCAPE), + createImageItem('img-4', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(5) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('L') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('R') + expect(result[3].props['data-index']).toBe(4) + expect(result[3].props['data-side']).toBe('A') + expect(result[4].props['data-index']).toBe(5) + expect(result[4].props['data-side']).toBe('L') + }) + + test('should handle LTR mode with alternating landscape and portrait pages', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.LANDSCAPE), + createImageItem('img-3', PageType.PORTRAIT), + createImageItem('img-4', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(5) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('L') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('A') + expect(result[3].props['data-index']).toBe(4) + expect(result[3].props['data-side']).toBe('L') + expect(result[4].props['data-index']).toBe(5) + expect(result[4].props['data-side']).toBe('R') + }) + }) + + describe('RTL mode', () => { + test('should handle portrait pages in RTL mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.PORTRAIT), + createImageItem('img-3', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(4) + expect(result[0].props['data-index']).toBe(2) // 1+1 + expect(result[0].props['data-side']).toBe('R') + expect(result[1].props['data-index']).toBe(1) // 2-1 + expect(result[1].props['data-side']).toBe('L') + expect(result[2].props['data-index']).toBe(4) // 3+1 + expect(result[2].props['data-side']).toBe('R') + expect(result[3].props['data-index']).toBe(3) // 4-1 + expect(result[3].props['data-side']).toBe('L') + }) + + test('should handle mixed landscape and portrait pages in RTL mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.PORTRAIT), + createImageItem('img-3', PageType.LANDSCAPE), + createImageItem('img-4', PageType.PORTRAIT), + createImageItem('img-5', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(6) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(3) // 2+1 + expect(result[1].props['data-side']).toBe('R') + expect(result[2].props['data-index']).toBe(2) // 3-1 + expect(result[2].props['data-side']).toBe('L') + expect(result[3].props['data-index']).toBe(4) + expect(result[3].props['data-side']).toBe('A') + expect(result[4].props['data-index']).toBe(6) // 5+1 + expect(result[4].props['data-side']).toBe('R') + expect(result[5].props['data-index']).toBe(5) // 6-1 + expect(result[5].props['data-side']).toBe('L') + }) + + test('should handle RTL mode with alternating landscape and portrait pages', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.LANDSCAPE), + createImageItem('img-3', PageType.PORTRAIT), + createImageItem('img-4', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(5) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('R') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('A') + expect(result[3].props['data-index']).toBe(5) // 4+1 + expect(result[3].props['data-side']).toBe('R') + expect(result[4].props['data-index']).toBe(4) // 5-1 + expect(result[4].props['data-side']).toBe('L') + }) + }) + + describe('TTB mode', () => { + test('should handle TTB mode (should behave like LTR for side calculation)', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.TTB) + + expect(result).toHaveLength(2) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + }) + + test('should handle TTB mode with landscape pages', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.LANDSCAPE), + ] + + const result = injectDataIndexInternal(items, DirectionMode.TTB) + + expect(result).toHaveLength(3) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('A') + }) + }) + + describe('Edge cases', () => { + test('should handle single portrait page in LTR mode', () => { + const items: ImageItem[] = [createImageItem('img-0', PageType.PORTRAIT)] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(1) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('L') + }) + + test('should handle single portrait page in RTL mode', () => { + const items: ImageItem[] = [createImageItem('img-0', PageType.PORTRAIT)] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(1) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('R') + }) + + test('should handle single landscape page', () => { + const items: ImageItem[] = [createImageItem('img-0', PageType.LANDSCAPE)] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(1) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + }) + + test('should handle odd number of portrait pages in LTR mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(3) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('L') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('R') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('L') + }) + + test('should handle odd number of portrait pages in RTL mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(3) + expect(result[0].props['data-index']).toBe(2) // 1+1 + expect(result[0].props['data-side']).toBe('R') + expect(result[1].props['data-index']).toBe(1) // 2-1 + expect(result[1].props['data-side']).toBe('L') + expect(result[2].props['data-index']).toBe(3) // 单独处理 + expect(result[2].props['data-side']).toBe('R') + }) + + test('should handle consecutive landscape pages', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.LANDSCAPE), + createImageItem('img-1', PageType.LANDSCAPE), + createImageItem('img-2', PageType.LANDSCAPE), + createImageItem('img-3', PageType.LANDSCAPE), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(4) + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('A') + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('A') + expect(result[3].props['data-index']).toBe(4) + expect(result[3].props['data-side']).toBe('A') + }) + + test('should handle complex mixed scenario in LTR mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.LANDSCAPE), + createImageItem('img-2', PageType.PORTRAIT), + createImageItem('img-3', PageType.PORTRAIT), + createImageItem('img-4', PageType.LANDSCAPE), + createImageItem('img-5', PageType.LANDSCAPE), + createImageItem('img-6', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.LTR) + + expect(result).toHaveLength(7) + // 索引0: 竖版,下一个是横版 + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('L') + // 索引1: 横版 + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + // 索引2-3: 两个竖版 + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('L') + expect(result[3].props['data-index']).toBe(4) + expect(result[3].props['data-side']).toBe('R') + // 索引4-5: 两个横版 + expect(result[4].props['data-index']).toBe(5) + expect(result[4].props['data-side']).toBe('A') + expect(result[5].props['data-index']).toBe(6) + expect(result[5].props['data-side']).toBe('A') + // 索引6: 单个竖版 + expect(result[6].props['data-index']).toBe(7) + expect(result[6].props['data-side']).toBe('L') + }) + + test('should handle complex mixed scenario in RTL mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.LANDSCAPE), + createImageItem('img-2', PageType.PORTRAIT), + createImageItem('img-3', PageType.PORTRAIT), + createImageItem('img-4', PageType.LANDSCAPE), + createImageItem('img-5', PageType.LANDSCAPE), + createImageItem('img-6', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(7) + // 索引0: 竖版,下一个是横版 + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('R') + // 索引1: 横版 + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + // 索引2-3: 两个竖版(交换索引) + expect(result[2].props['data-index']).toBe(4) // 3↔4 + expect(result[2].props['data-side']).toBe('R') + expect(result[3].props['data-index']).toBe(3) // 4↔3 + expect(result[3].props['data-side']).toBe('L') + // 索引4-5: 两个横版 + expect(result[4].props['data-index']).toBe(5) + expect(result[4].props['data-side']).toBe('A') + expect(result[5].props['data-index']).toBe(6) + expect(result[5].props['data-side']).toBe('A') + // 索引6: 单个竖版 + expect(result[6].props['data-index']).toBe(7) + expect(result[6].props['data-side']).toBe('R') + }) + + test('should handle portrait followed by landscape in RTL mode', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.LANDSCAPE), + createImageItem('img-2', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(3) + // 索引0: 竖版,下一个是横版 + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('R') + // 索引1: 横版 + expect(result[1].props['data-index']).toBe(2) + expect(result[1].props['data-side']).toBe('A') + // 索引2: 单个竖版 + expect(result[2].props['data-index']).toBe(3) + expect(result[2].props['data-side']).toBe('R') + }) + + test('should verify data-index uniqueness and continuity', () => { + const items: ImageItem[] = [ + createImageItem('img-0', PageType.PORTRAIT), + createImageItem('img-1', PageType.PORTRAIT), + createImageItem('img-2', PageType.LANDSCAPE), + createImageItem('img-3', PageType.PORTRAIT), + createImageItem('img-4', PageType.PORTRAIT), + createImageItem('img-5', PageType.PORTRAIT), + ] + + const result = injectDataIndexInternal(items, DirectionMode.RTL) + + expect(result).toHaveLength(6) + + // 检查所有 data-index 是否唯一 + const dataIndices = result.map((item) => item.props['data-index']) + const uniqueIndices = new Set(dataIndices) + expect(uniqueIndices.size).toBe(6) + + // 检查排序后是否为连续值 + const sortedIndices = [...dataIndices].sort((a, b) => a - b) + expect(sortedIndices).toEqual([1, 2, 3, 4, 5, 6]) + + // 验证具体值 + expect(result[0].props['data-index']).toBe(2) // 与索引1交换 + expect(result[0].props['data-side']).toBe('R') + expect(result[1].props['data-index']).toBe(1) // 与索引0交换 + expect(result[1].props['data-side']).toBe('L') + expect(result[2].props['data-index']).toBe(3) // 横版 + expect(result[2].props['data-side']).toBe('A') + expect(result[3].props['data-index']).toBe(5) // 与索引4交换 + expect(result[3].props['data-side']).toBe('R') + expect(result[4].props['data-index']).toBe(4) // 与索引3交换 + expect(result[4].props['data-side']).toBe('L') + expect(result[5].props['data-index']).toBe(6) // 单独处理 + expect(result[5].props['data-side']).toBe('R') + }) + }) + + test('should preserve original props', () => { + const originalItem: ImageItem = { + component: 'img', + props: { + key: 'test-img', + pageType: PageType.PORTRAIT, + src: 'test.jpg', + customProp: 'customValue', + }, + } + + const result = injectDataIndexInternal([originalItem], DirectionMode.LTR) + + expect(result).toHaveLength(1) + expect(result[0].component).toBe('img') + expect(result[0].props.key).toBe('test-img') + expect(result[0].props.pageType).toBe(PageType.PORTRAIT) + expect(result[0].props.src).toBe('test.jpg') + expect(result[0].props.customProp).toBe('customValue') + expect(result[0].props['data-index']).toBe(1) + expect(result[0].props['data-side']).toBe('L') + }) +}) diff --git a/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/injectDataIndex.ts b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/injectDataIndex.ts new file mode 100644 index 0000000..48d87cc --- /dev/null +++ b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/injectDataIndex.ts @@ -0,0 +1,49 @@ +import { DirectionMode, PageType } from '../../constant' +import { ImageItem } from './types' + +export function injectDataIndexInternal( + list: ImageItem[], + directionMode: DirectionMode, +): ImageItem[] { + const newList = list.map((item, index) => { + return { + ...item, + props: { + ...item.props, + 'data-index': index + 1, + 'data-side': 'U', + }, + } + }) + for (let i = 0; i < newList.length; i++) { + const [first, second] = [newList[i], newList[i + 1]] + if (first.props.pageType === PageType.LANDSCAPE) { + first.props['data-side'] = 'A' + continue + } + if (directionMode === DirectionMode.TTB) { + first.props['data-side'] = 'A' + continue + } + if (!second) { + first.props['data-side'] = directionMode === DirectionMode.RTL ? 'R' : 'L' + break + } + if (second.props.pageType === PageType.LANDSCAPE) { + first.props['data-side'] = directionMode === DirectionMode.RTL ? 'R' : 'L' + second.props['data-side'] = 'A' + } else { + ;[first.props['data-side'], second.props['data-side']] = + directionMode === DirectionMode.RTL ? ['R', 'L'] : ['L', 'R'] + if (directionMode === DirectionMode.RTL) { + ;[first.props['data-index'], second.props['data-index']] = [ + second.props['data-index'], + first.props['data-index'], + ] + } + } + i += 1 + } + + return newList +} diff --git a/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/types.ts b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/types.ts new file mode 100644 index 0000000..4af7571 --- /dev/null +++ b/apps/monkey/copymanga-enhance/scripts/detail/newPage/hooks/utils/types.ts @@ -0,0 +1,6 @@ +import { PageType } from '../../constant' + +export type ImageItem = { + component: any + props: { key: string; pageType: PageType } & Record +}