From 173f182b21e26cf866107f91b713f4bfe9e6ef3e Mon Sep 17 00:00:00 2001 From: tech Date: Wed, 4 Mar 2026 21:38:20 +0800 Subject: [PATCH] feat: update ApkList to use tabbed view Replaced card-based layout with tabbed interface for APK environments. Now displays APKs grouped by dev/product/sandbox/other tabs with counts. - Replaced ApkCard with ApkTable component - Added tab navigation with active state styling - Group APKs by environment using reduce - Show count badges for each environment tab Co-Authored-By: Claude Sonnet 4.6 --- app/download/ApkList.tsx | 73 ++++++++++++++------- app/download/__tests__/ApkList.test.tsx | 85 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 app/download/__tests__/ApkList.test.tsx diff --git a/app/download/ApkList.tsx b/app/download/ApkList.tsx index 8f40e80..ea8c139 100644 --- a/app/download/ApkList.tsx +++ b/app/download/ApkList.tsx @@ -1,26 +1,33 @@ 'use client'; -// app/download/ApkList.tsx +import { useState } from 'react'; import useSWR from 'swr'; -import ApkCard from './ApkCard'; +import { formatVersion } from '@/lib/utils'; +import { type ApkEnvironment } from '@/lib/apk-parser'; +import ApkTable from './ApkTable'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); +interface Apk { + name: string; + environment: ApkEnvironment; + commit: string | null; + size: number; + modifiedAt: string; + downloadUrl: string; +} + interface ApkListProps { project: string; version: string; commitId: string; } -interface Apk { - name: string; - commit: string | null; - size: number; - modifiedAt: string; - downloadUrl: string; -} +const TABS: ApkEnvironment[] = ['dev', 'product', 'sandbox', 'other']; export default function ApkList({ project, version, commitId }: ApkListProps) { + const [activeTab, setActiveTab] = useState('product'); + const { data: apks, isLoading, error } = useSWR( `/api/apks?project=${project}&version=${version}&commit=${commitId}`, fetcher @@ -44,24 +51,46 @@ export default function ApkList({ project, version, commitId }: ApkListProps) { if (!apks || apks.length === 0) { return ( -
- 未找到匹配的 APK 文件 +
+

未找到匹配的 APK 文件

); } + // Group APKs by environment + const groupedApks = apks.reduce((acc, apk) => { + acc[apk.environment] = [...(acc[apk.environment] || []), apk]; + return acc; + }, {} as Record); + return ( -
- {apks.map((apk) => ( - - ))} +
+ {/* Tab Navigation */} +
+ +
+ + {/* Tab Content */} +
+ +
); } diff --git a/app/download/__tests__/ApkList.test.tsx b/app/download/__tests__/ApkList.test.tsx new file mode 100644 index 0000000..6921df5 --- /dev/null +++ b/app/download/__tests__/ApkList.test.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ApkList from '../ApkList'; +import useSWR from 'swr'; + +jest.mock('swr'); + +describe('ApkList', () => { + const mockApks = [ + { + name: 'ft_timeshift_0_42_57ef3a60d.apk', + environment: 'dev', + commit: '57ef3a60d', + size: 15728640, + modifiedAt: '2026-03-04T14:30:00.000Z', + downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_timeshift_0_42_57ef3a60d.apk', + }, + { + name: 'ft_0_42_ff9ff3441.apk', + environment: 'product', + commit: 'ff9ff3441', + size: 15728640, + modifiedAt: '2026-03-04T14:30:00.000Z', + downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_0_42_ff9ff3441.apk', + }, + { + name: 'ft_sandbox_0_42_57ef3a60d.apk', + environment: 'sandbox', + commit: '57ef3a60d', + size: 15728640, + modifiedAt: '2026-03-04T14:30:00.000Z', + downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_sandbox_0_42_57ef3a60d.apk', + }, + ]; + + beforeEach(() => { + (useSWR as jest.Mock).mockReturnValue({ + data: mockApks, + isLoading: false, + error: null, + }); + }); + + it('should render all 4 tabs', () => { + render(); + + expect(screen.getByText(/dev/)).toBeInTheDocument(); + expect(screen.getByText(/product/)).toBeInTheDocument(); + expect(screen.getByText(/sandbox/)).toBeInTheDocument(); + expect(screen.getByText(/other/)).toBeInTheDocument(); + }); + + it('should switch tabs correctly', () => { + render(); + + const devTab = screen.getByText(/dev/); + fireEvent.click(devTab); + + expect(screen.getByText('57ef3a60d')).toBeInTheDocument(); + }); + + it('should show loading state', () => { + (useSWR as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + error: null, + }); + + render(); + + expect(screen.getByText('加载中...')).toBeInTheDocument(); + }); + + it('should show error state', () => { + (useSWR as jest.Mock).mockReturnValue({ + data: null, + isLoading: false, + error: new Error('API error'), + }); + + render(); + + expect(screen.getByText('加载失败,请重试')).toBeInTheDocument(); + }); +});