'use client'; // ============================================================ // Presentation — App Version Management Page // Clean Architecture: 纯表现层,只读 Redux/Zustand 状态,调用 Use Cases // 不直接使用 HttpClient / apiClient / fetch // ============================================================ import React, { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { t } from '@/i18n/locales'; import { useAppDispatch, useAppSelector } from '@/store'; import { setAppType, setPlatformFilter, openUploadModal, closeUploadModal, openEditModal, closeEditModal, } from '@/store/slices/version.slice'; import { useUploadStore } from '@/store/zustand/upload.store'; import { listVersionsUseCase, parsePackageUseCase, uploadVersionUseCase, updateVersionUseCase, toggleVersionUseCase, deleteVersionUseCase, } from '@/application/use-cases/version.use-cases'; import type { AppVersion, AppType, AppPlatform } from '@/domain/entities'; import type { UpdateVersionInput } from '@/domain/repositories/version.repository.interface'; /* ── Styles ── */ const loadingBox: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 40, color: 'var(--color-text-tertiary)', }; const tabBtn = (active: boolean): React.CSSProperties => ({ padding: '8px 20px', border: 'none', borderBottom: active ? '2px solid var(--color-primary)' : '2px solid transparent', background: 'transparent', color: active ? 'var(--color-primary)' : 'var(--color-text-secondary)', cursor: 'pointer', fontWeight: active ? 600 : 400, }); const filterBtn = (active: boolean): React.CSSProperties => ({ padding: '4px 14px', border: `1px solid ${active ? 'var(--color-primary)' : 'var(--color-border)'}`, borderRadius: 'var(--radius-full)', background: active ? 'var(--color-primary-surface)' : 'transparent', color: active ? 'var(--color-primary)' : 'var(--color-text-secondary)', cursor: 'pointer', }); const primaryBtn: React.CSSProperties = { padding: '8px 20px', border: 'none', borderRadius: 'var(--radius-full)', background: 'var(--color-primary)', color: 'white', cursor: 'pointer', }; const thStyle: React.CSSProperties = { padding: '12px 16px', textAlign: 'left', whiteSpace: 'nowrap', color: 'var(--color-text-tertiary)', }; const tdStyle: React.CSSProperties = { padding: '12px 16px', color: 'var(--color-text-primary)' }; const badge = (bg: string, color: string): React.CSSProperties => ({ padding: '2px 10px', borderRadius: 'var(--radius-full)', background: bg, color, fontWeight: 500, }); const overlayStyle: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, }; const modalStyle: React.CSSProperties = { background: 'var(--color-surface)', borderRadius: 'var(--radius-lg)', padding: 28, width: 520, maxHeight: '80vh', overflow: 'auto', boxShadow: 'var(--shadow-lg)', }; const inputStyle: React.CSSProperties = { width: '100%', height: 40, border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', padding: '0 12px', background: 'var(--color-gray-50)', outline: 'none', boxSizing: 'border-box', }; const labelStyle: React.CSSProperties = { display: 'block', marginBottom: 4, marginTop: 14, color: 'var(--color-text-secondary)', }; /* ── Helpers ── */ function formatFileSize(bytes: string | number | null | undefined): string { const n = typeof bytes === 'string' ? parseInt(bytes, 10) : (bytes ?? 0); if (!n || isNaN(n)) return '-'; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`; return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`; } function formatDate(iso: string | null | undefined): string { if (!iso) return '-'; return new Date(iso).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } /* ── Query key factory ── */ const versionKeys = { list: (appType: AppType, platform: AppPlatform | '') => ['versions', appType, platform] as const, }; /* ── Main Page ── */ export const AppVersionManagementPage: React.FC = () => { const dispatch = useAppDispatch(); const { appType, platformFilter, showUpload, editingVersionId } = useAppSelector((s) => s.versions); const queryClient = useQueryClient(); const { data: versions, isLoading, error } = useQuery({ queryKey: versionKeys.list(appType, platformFilter), queryFn: () => listVersionsUseCase.execute(appType, platformFilter as AppPlatform | undefined), staleTime: 30_000, }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['versions', appType] }); const handleToggle = async (v: AppVersion) => { await toggleVersionUseCase.execute(v.id, !v.isEnabled); invalidate(); }; const handleDelete = async (v: AppVersion) => { if (!confirm(t('app_version_confirm_delete'))) return; await deleteVersionUseCase.execute(v.id); invalidate(); }; const list = Array.isArray(versions) ? versions : []; const editingVersion = editingVersionId ? list.find((v) => v.id === editingVersionId) ?? null : null; return (
| {t('app_version_version_name')} | {t('app_version_platform')} | {t('app_version_build_number')} | {t('app_version_file_size')} | {t('app_version_force_update')} | {t('status')} | {t('app_version_release_date')} | {t('actions')} |
|---|---|---|---|---|---|---|---|
| {v.versionName} | {v.platform === 'ANDROID' ? 'Android' : 'iOS'} | {v.buildNumber} | {formatFileSize(v.fileSize)} | {v.isForceUpdate ? {t('app_version_force_update')} : '-'} | {v.isEnabled ? t('app_version_enabled') : t('app_version_disabled')} | {formatDate(v.releaseDate || v.createdAt)} |