From e92059fc7507d9e7e4d23a99fe2ca990f887ed17 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 6 Mar 2026 11:17:26 -0800 Subject: [PATCH] refactor(admin-web): add Presentation hooks layer for app-versions - useVersionList: React Query + Use Case, select guard, invalidate helper - useVersionMutations: toggle/delete wrapped with onSuccess callback - useUpload: parse+upload flow extracted from modal component - AppVersionManagementPage: purely declarative, zero business logic in JSX Co-Authored-By: Claude Sonnet 4.6 --- .../app-versions/AppVersionManagementPage.tsx | 169 +++++++----------- .../views/app-versions/hooks/use-upload.ts | 67 +++++++ .../app-versions/hooks/use-version-list.ts | 27 +++ .../hooks/use-version-mutations.ts | 23 +++ 4 files changed, 183 insertions(+), 103 deletions(-) create mode 100644 frontend/admin-web/src/views/app-versions/hooks/use-upload.ts create mode 100644 frontend/admin-web/src/views/app-versions/hooks/use-version-list.ts create mode 100644 frontend/admin-web/src/views/app-versions/hooks/use-version-mutations.ts diff --git a/frontend/admin-web/src/views/app-versions/AppVersionManagementPage.tsx b/frontend/admin-web/src/views/app-versions/AppVersionManagementPage.tsx index 5d72ac1..5367105 100644 --- a/frontend/admin-web/src/views/app-versions/AppVersionManagementPage.tsx +++ b/frontend/admin-web/src/views/app-versions/AppVersionManagementPage.tsx @@ -2,12 +2,17 @@ // ============================================================ // Presentation — App Version Management Page -// Clean Architecture: 纯表现层,只读 Redux/Zustand 状态,调用 Use Cases -// 不直接使用 HttpClient / apiClient / fetch +// 严格四层 Clean Architecture: +// Domain → Infrastructure → Application(Use Cases) → Presentation +// +// 本文件只做声明式渲染: +// - Redux (version.slice) → 全局 UI 状态(tab/filter/modal 开关) +// - Zustand (upload.store) → 上传表单临时状态 +// - Custom Hooks → 桥接 Application 层(数据/副作用) +// - 零直接 HTTP 调用,零 Use Case 直接调用 // ============================================================ import React, { useState } from 'react'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; import { t } from '@/i18n/locales'; import { useAppDispatch, useAppSelector } from '@/store'; import { @@ -16,14 +21,10 @@ import { 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 { updateVersionUseCase } from '@/application/use-cases/version.use-cases'; +import { useVersionList } from './hooks/use-version-list'; +import { useVersionMutations } from './hooks/use-version-mutations'; +import { useUpload } from './hooks/use-upload'; import type { AppVersion, AppType, AppPlatform } from '@/domain/entities'; import type { UpdateVersionInput } from '@/domain/repositories/version.repository.interface'; @@ -84,7 +85,6 @@ const labelStyle: React.CSSProperties = { 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`; @@ -98,45 +98,28 @@ function formatDate(iso: string | null | undefined): string { }); } -/* ── Query key factory ── */ -const versionKeys = { - list: (appType: AppType, platform: AppPlatform | '') => - ['versions', appType, platform] as const, -}; - -/* ── Main Page ── */ +/* ── 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, - }); + // Data hook (React Query + Use Case) + const { data: versions, isLoading, error, invalidate } = + useVersionList(appType, platformFilter); - const invalidate = () => - queryClient.invalidateQueries({ queryKey: ['versions', appType] }); + // Mutation hooks + const { toggle, remove } = useVersionMutations(invalidate); - 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; + const list = versions ?? []; + const editingVersion = editingVersionId + ? list.find((v) => v.id === editingVersionId) ?? null + : null; return (
+ {/* Header */}

{t('app_version_title')}

))} @@ -214,16 +200,22 @@ export const AppVersionManagementPage: React.FC = () => { {formatDate(v.releaseDate || v.createdAt)} - - - @@ -253,63 +245,23 @@ export const AppVersionManagementPage: React.FC = () => { ); }; -/* ── Upload Modal — reads/writes Zustand upload store ── */ +/* ── Upload Modal — 只做渲染,逻辑在 useUpload hook ── */ const UploadModal: React.FC<{ appType: AppType; onClose: () => void; onSuccess: () => void; }> = ({ appType, onClose, onSuccess }) => { + // 读 Zustand 状态 const { file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate, isParsing, parseWarning, isUploading, uploadError, - setFile, setPlatform, setVersionName, setBuildNumber, setChangelog, - setMinOsVersion, setIsForceUpdate, setIsParsing, setParseWarning, - setIsUploading, setUploadError, reset, + setPlatform, setVersionName, setBuildNumber, setChangelog, + setMinOsVersion, setIsForceUpdate, reset, } = useUploadStore(); - const handleFileChange = async (e: React.ChangeEvent) => { - const f = e.target.files?.[0]; - if (!f) return; - setFile(f); - setParseWarning(''); - if (f.name.endsWith('.apk')) setPlatform('ANDROID'); - else if (f.name.endsWith('.ipa')) setPlatform('IOS'); - - setIsParsing(true); - try { - console.log('[Upload] Parsing:', f.name, (f.size / 1024 / 1024).toFixed(1) + 'MB'); - const info = await parsePackageUseCase.execute(f); - console.log('[Upload] Parse result:', info); - if (info?.versionName) setVersionName(info.versionName); - if (info?.versionCode) setBuildNumber(String(info.versionCode)); - if (info?.minSdkVersion) setMinOsVersion(info.minSdkVersion); - } catch (err: any) { - console.warn('[Upload] Parse failed:', err?.message); - setParseWarning('无法自动解析安装包信息,请手动填写版本号'); - } - setIsParsing(false); - }; - - const handleSubmit = async () => { - if (!file) { setUploadError(t('app_version_upload_file')); return; } - setIsUploading(true); - setUploadError(''); - try { - console.log('[Upload] Uploading:', file.name, (file.size / 1024 / 1024).toFixed(1) + 'MB'); - const result = await uploadVersionUseCase.execute({ - file, appType, platform, versionName, buildNumber, - changelog, minOsVersion, isForceUpdate, - }); - console.log('[Upload] Success:', result); - reset(); - onSuccess(); - } catch (err: any) { - console.error('[Upload] Failed:', err); - setUploadError(err?.message || 'Upload failed'); - } - setIsUploading(false); - }; + // 副作用逻辑在 hook 里 + const { handleFileChange, handleSubmit } = useUpload(appType, onSuccess); const handleClose = () => { reset(); onClose(); }; @@ -351,13 +303,16 @@ const UploadModal: React.FC<{ ({t('optional_auto_from_apk')}) - setVersionName(e.target.value)} placeholder="1.0.0" /> + setVersionName(e.target.value)} placeholder="1.0.0" /> - setBuildNumber(e.target.value)} placeholder="100" /> + setBuildNumber(e.target.value)} placeholder="100" /> - setMinOsVersion(e.target.value)} + setMinOsVersion(e.target.value)} placeholder={platform === 'ANDROID' ? 'e.g. 8.0' : 'e.g. 14.0'} /> @@ -365,12 +320,17 @@ const UploadModal: React.FC<{ style={{ ...inputStyle, height: 'auto', padding: '8px 12px', resize: 'vertical' }} /> {uploadError &&
{uploadError}
} - {isUploading &&
正在上传,大文件需要较长时间,请耐心等待...
} + {isUploading && ( +
+ 正在上传,大文件需要较长时间,请耐心等待... +
+ )}