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 <noreply@anthropic.com>
This commit is contained in:
parent
3765e8e6b1
commit
e92059fc75
|
|
@ -2,12 +2,17 @@
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Presentation — App Version Management Page
|
// Presentation — App Version Management Page
|
||||||
// Clean Architecture: 纯表现层,只读 Redux/Zustand 状态,调用 Use Cases
|
// 严格四层 Clean Architecture:
|
||||||
// 不直接使用 HttpClient / apiClient / fetch
|
// 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 React, { useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { t } from '@/i18n/locales';
|
import { t } from '@/i18n/locales';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store';
|
import { useAppDispatch, useAppSelector } from '@/store';
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,14 +21,10 @@ import {
|
||||||
openEditModal, closeEditModal,
|
openEditModal, closeEditModal,
|
||||||
} from '@/store/slices/version.slice';
|
} from '@/store/slices/version.slice';
|
||||||
import { useUploadStore } from '@/store/zustand/upload.store';
|
import { useUploadStore } from '@/store/zustand/upload.store';
|
||||||
import {
|
import { updateVersionUseCase } from '@/application/use-cases/version.use-cases';
|
||||||
listVersionsUseCase,
|
import { useVersionList } from './hooks/use-version-list';
|
||||||
parsePackageUseCase,
|
import { useVersionMutations } from './hooks/use-version-mutations';
|
||||||
uploadVersionUseCase,
|
import { useUpload } from './hooks/use-upload';
|
||||||
updateVersionUseCase,
|
|
||||||
toggleVersionUseCase,
|
|
||||||
deleteVersionUseCase,
|
|
||||||
} from '@/application/use-cases/version.use-cases';
|
|
||||||
import type { AppVersion, AppType, AppPlatform } from '@/domain/entities';
|
import type { AppVersion, AppType, AppPlatform } from '@/domain/entities';
|
||||||
import type { UpdateVersionInput } from '@/domain/repositories/version.repository.interface';
|
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 {
|
function formatFileSize(bytes: string | number | null | undefined): string {
|
||||||
const n = typeof bytes === 'string' ? parseInt(bytes, 10) : (bytes ?? 0);
|
const n = typeof bytes === 'string' ? parseInt(bytes, 10) : (bytes ?? 0);
|
||||||
if (!n || isNaN(n)) return '-';
|
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) return `${(n / 1024).toFixed(1)} KB`;
|
||||||
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||||
|
|
@ -98,45 +98,28 @@ function formatDate(iso: string | null | undefined): string {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Query key factory ── */
|
/* ── Main Page — 纯声明式,无业务逻辑 ── */
|
||||||
const versionKeys = {
|
|
||||||
list: (appType: AppType, platform: AppPlatform | '') =>
|
|
||||||
['versions', appType, platform] as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ── Main Page ── */
|
|
||||||
|
|
||||||
export const AppVersionManagementPage: React.FC = () => {
|
export const AppVersionManagementPage: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { appType, platformFilter, showUpload, editingVersionId } =
|
const { appType, platformFilter, showUpload, editingVersionId } =
|
||||||
useAppSelector((s) => s.versions);
|
useAppSelector((s) => s.versions);
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { data: versions, isLoading, error } = useQuery({
|
// Data hook (React Query + Use Case)
|
||||||
queryKey: versionKeys.list(appType, platformFilter),
|
const { data: versions, isLoading, error, invalidate } =
|
||||||
queryFn: () => listVersionsUseCase.execute(appType, platformFilter as AppPlatform | undefined),
|
useVersionList(appType, platformFilter);
|
||||||
staleTime: 30_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const invalidate = () =>
|
// Mutation hooks
|
||||||
queryClient.invalidateQueries({ queryKey: ['versions', appType] });
|
const { toggle, remove } = useVersionMutations(invalidate);
|
||||||
|
|
||||||
const handleToggle = async (v: AppVersion) => {
|
const list = versions ?? [];
|
||||||
await toggleVersionUseCase.execute(v.id, !v.isEnabled);
|
const editingVersion = editingVersionId
|
||||||
invalidate();
|
? list.find((v) => v.id === editingVersionId) ?? null
|
||||||
};
|
: null;
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Header */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||||
<h1 style={{ margin: 0 }}>{t('app_version_title')}</h1>
|
<h1 style={{ margin: 0 }}>{t('app_version_title')}</h1>
|
||||||
<button style={primaryBtn} onClick={() => dispatch(openUploadModal())}>
|
<button style={primaryBtn} onClick={() => dispatch(openUploadModal())}>
|
||||||
|
|
@ -156,8 +139,11 @@ export const AppVersionManagementPage: React.FC = () => {
|
||||||
{/* Platform filter — RTK state */}
|
{/* Platform filter — RTK state */}
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
|
||||||
{(['', 'ANDROID', 'IOS'] as (AppPlatform | '')[]).map((p) => (
|
{(['', 'ANDROID', 'IOS'] as (AppPlatform | '')[]).map((p) => (
|
||||||
<button key={p || 'all'} style={filterBtn(platformFilter === p)}
|
<button
|
||||||
onClick={() => dispatch(setPlatformFilter(p as AppPlatform | ''))}>
|
key={p || 'all'}
|
||||||
|
style={filterBtn(platformFilter === p)}
|
||||||
|
onClick={() => dispatch(setPlatformFilter(p as AppPlatform | ''))}
|
||||||
|
>
|
||||||
{p === '' ? t('app_version_all_platforms') : p === 'ANDROID' ? 'Android' : 'iOS'}
|
{p === '' ? t('app_version_all_platforms') : p === 'ANDROID' ? 'Android' : 'iOS'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
@ -214,16 +200,22 @@ export const AppVersionManagementPage: React.FC = () => {
|
||||||
</td>
|
</td>
|
||||||
<td style={tdStyle}>{formatDate(v.releaseDate || v.createdAt)}</td>
|
<td style={tdStyle}>{formatDate(v.releaseDate || v.createdAt)}</td>
|
||||||
<td style={{ ...tdStyle, whiteSpace: 'nowrap' }}>
|
<td style={{ ...tdStyle, whiteSpace: 'nowrap' }}>
|
||||||
<button onClick={() => dispatch(openEditModal(v.id))}
|
<button
|
||||||
style={{ border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', padding: '3px 10px', background: 'transparent', cursor: 'pointer', marginRight: 6 }}>
|
onClick={() => dispatch(openEditModal(v.id))}
|
||||||
|
style={{ border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', padding: '3px 10px', background: 'transparent', cursor: 'pointer', marginRight: 6 }}
|
||||||
|
>
|
||||||
{t('edit')}
|
{t('edit')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => handleToggle(v)}
|
<button
|
||||||
style={{ border: `1px solid ${v.isEnabled ? 'var(--color-warning)' : 'var(--color-success)'}`, borderRadius: 'var(--radius-sm)', padding: '3px 10px', background: 'transparent', cursor: 'pointer', color: v.isEnabled ? 'var(--color-warning)' : 'var(--color-success)', marginRight: 6 }}>
|
onClick={() => toggle(v)}
|
||||||
|
style={{ border: `1px solid ${v.isEnabled ? 'var(--color-warning)' : 'var(--color-success)'}`, borderRadius: 'var(--radius-sm)', padding: '3px 10px', background: 'transparent', cursor: 'pointer', color: v.isEnabled ? 'var(--color-warning)' : 'var(--color-success)', marginRight: 6 }}
|
||||||
|
>
|
||||||
{v.isEnabled ? t('disable') : t('enable')}
|
{v.isEnabled ? t('disable') : t('enable')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => handleDelete(v)}
|
<button
|
||||||
style={{ border: '1px solid var(--color-error)', borderRadius: 'var(--radius-sm)', padding: '3px 10px', background: 'transparent', cursor: 'pointer', color: 'var(--color-error)' }}>
|
onClick={() => remove(v)}
|
||||||
|
style={{ border: '1px solid var(--color-error)', borderRadius: 'var(--radius-sm)', padding: '3px 10px', background: 'transparent', cursor: 'pointer', color: 'var(--color-error)' }}
|
||||||
|
>
|
||||||
{t('delete')}
|
{t('delete')}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -253,63 +245,23 @@ export const AppVersionManagementPage: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Upload Modal — reads/writes Zustand upload store ── */
|
/* ── Upload Modal — 只做渲染,逻辑在 useUpload hook ── */
|
||||||
|
|
||||||
const UploadModal: React.FC<{
|
const UploadModal: React.FC<{
|
||||||
appType: AppType;
|
appType: AppType;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}> = ({ appType, onClose, onSuccess }) => {
|
}> = ({ appType, onClose, onSuccess }) => {
|
||||||
|
// 读 Zustand 状态
|
||||||
const {
|
const {
|
||||||
file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate,
|
file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate,
|
||||||
isParsing, parseWarning, isUploading, uploadError,
|
isParsing, parseWarning, isUploading, uploadError,
|
||||||
setFile, setPlatform, setVersionName, setBuildNumber, setChangelog,
|
setPlatform, setVersionName, setBuildNumber, setChangelog,
|
||||||
setMinOsVersion, setIsForceUpdate, setIsParsing, setParseWarning,
|
setMinOsVersion, setIsForceUpdate, reset,
|
||||||
setIsUploading, setUploadError, reset,
|
|
||||||
} = useUploadStore();
|
} = useUploadStore();
|
||||||
|
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
// 副作用逻辑在 hook 里
|
||||||
const f = e.target.files?.[0];
|
const { handleFileChange, handleSubmit } = useUpload(appType, onSuccess);
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => { reset(); onClose(); };
|
const handleClose = () => { reset(); onClose(); };
|
||||||
|
|
||||||
|
|
@ -351,13 +303,16 @@ const UploadModal: React.FC<{
|
||||||
({t('optional_auto_from_apk')})
|
({t('optional_auto_from_apk')})
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input style={inputStyle} value={versionName} onChange={(e) => setVersionName(e.target.value)} placeholder="1.0.0" />
|
<input style={inputStyle} value={versionName}
|
||||||
|
onChange={(e) => setVersionName(e.target.value)} placeholder="1.0.0" />
|
||||||
|
|
||||||
<label style={labelStyle}>{t('app_version_build_number')}</label>
|
<label style={labelStyle}>{t('app_version_build_number')}</label>
|
||||||
<input style={inputStyle} value={buildNumber} onChange={(e) => setBuildNumber(e.target.value)} placeholder="100" />
|
<input style={inputStyle} value={buildNumber}
|
||||||
|
onChange={(e) => setBuildNumber(e.target.value)} placeholder="100" />
|
||||||
|
|
||||||
<label style={labelStyle}>{t('app_version_min_os')}</label>
|
<label style={labelStyle}>{t('app_version_min_os')}</label>
|
||||||
<input style={inputStyle} value={minOsVersion} onChange={(e) => setMinOsVersion(e.target.value)}
|
<input style={inputStyle} value={minOsVersion}
|
||||||
|
onChange={(e) => setMinOsVersion(e.target.value)}
|
||||||
placeholder={platform === 'ANDROID' ? 'e.g. 8.0' : 'e.g. 14.0'} />
|
placeholder={platform === 'ANDROID' ? 'e.g. 8.0' : 'e.g. 14.0'} />
|
||||||
|
|
||||||
<label style={labelStyle}>{t('app_version_changelog')}</label>
|
<label style={labelStyle}>{t('app_version_changelog')}</label>
|
||||||
|
|
@ -365,12 +320,17 @@ const UploadModal: React.FC<{
|
||||||
style={{ ...inputStyle, height: 'auto', padding: '8px 12px', resize: 'vertical' }} />
|
style={{ ...inputStyle, height: 'auto', padding: '8px 12px', resize: 'vertical' }} />
|
||||||
|
|
||||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 8, marginTop: 16 }}>
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 8, marginTop: 16 }}>
|
||||||
<input type="checkbox" checked={isForceUpdate} onChange={(e) => setIsForceUpdate(e.target.checked)} />
|
<input type="checkbox" checked={isForceUpdate}
|
||||||
|
onChange={(e) => setIsForceUpdate(e.target.checked)} />
|
||||||
{t('app_version_force_update')}
|
{t('app_version_force_update')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{uploadError && <div style={{ color: 'var(--color-error)', marginTop: 8 }}>{uploadError}</div>}
|
{uploadError && <div style={{ color: 'var(--color-error)', marginTop: 8 }}>{uploadError}</div>}
|
||||||
{isUploading && <div style={{ color: 'var(--color-info)', marginTop: 4 }}>正在上传,大文件需要较长时间,请耐心等待...</div>}
|
{isUploading && (
|
||||||
|
<div style={{ color: 'var(--color-info)', marginTop: 4 }}>
|
||||||
|
正在上传,大文件需要较长时间,请耐心等待...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 12, marginTop: 20, justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: 12, marginTop: 20, justifyContent: 'flex-end' }}>
|
||||||
<button onClick={handleClose}
|
<button onClick={handleClose}
|
||||||
|
|
@ -387,7 +347,7 @@ const UploadModal: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Edit Modal — local useState (无需持久化,单次操作) ── */
|
/* ── Edit Modal — local useState(单次操作,无需持久化) ── */
|
||||||
|
|
||||||
const EditModal: React.FC<{
|
const EditModal: React.FC<{
|
||||||
version: AppVersion;
|
version: AppVersion;
|
||||||
|
|
@ -432,16 +392,19 @@ const EditModal: React.FC<{
|
||||||
style={{ ...inputStyle, height: 'auto', padding: '8px 12px', resize: 'vertical' }} />
|
style={{ ...inputStyle, height: 'auto', padding: '8px 12px', resize: 'vertical' }} />
|
||||||
|
|
||||||
<label style={labelStyle}>{t('app_version_min_os')}</label>
|
<label style={labelStyle}>{t('app_version_min_os')}</label>
|
||||||
<input style={inputStyle} value={minOsVersion} onChange={(e) => setMinOsVersion(e.target.value)}
|
<input style={inputStyle} value={minOsVersion}
|
||||||
|
onChange={(e) => setMinOsVersion(e.target.value)}
|
||||||
placeholder={version.platform === 'ANDROID' ? 'e.g. 8.0' : 'e.g. 14.0'} />
|
placeholder={version.platform === 'ANDROID' ? 'e.g. 8.0' : 'e.g. 14.0'} />
|
||||||
|
|
||||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 8, marginTop: 16 }}>
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 8, marginTop: 16 }}>
|
||||||
<input type="checkbox" checked={isForceUpdate} onChange={(e) => setIsForceUpdate(e.target.checked)} />
|
<input type="checkbox" checked={isForceUpdate}
|
||||||
|
onChange={(e) => setIsForceUpdate(e.target.checked)} />
|
||||||
{t('app_version_force_update')}
|
{t('app_version_force_update')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 8 }}>
|
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<input type="checkbox" checked={isEnabled} onChange={(e) => setIsEnabled(e.target.checked)} />
|
<input type="checkbox" checked={isEnabled}
|
||||||
|
onChange={(e) => setIsEnabled(e.target.checked)} />
|
||||||
{t('app_version_enabled')}
|
{t('app_version_enabled')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
import type { AppType } from '@/domain/entities';
|
||||||
|
import { useUploadStore } from '@/store/zustand/upload.store';
|
||||||
|
import {
|
||||||
|
parsePackageUseCase,
|
||||||
|
uploadVersionUseCase,
|
||||||
|
} from '@/application/use-cases/version.use-cases';
|
||||||
|
|
||||||
|
export function useUpload(appType: AppType, onSuccess: () => void) {
|
||||||
|
const store = useUploadStore();
|
||||||
|
|
||||||
|
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const f = e.target.files?.[0];
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
store.setFile(f);
|
||||||
|
store.setParseWarning('');
|
||||||
|
|
||||||
|
if (f.name.endsWith('.apk')) store.setPlatform('ANDROID');
|
||||||
|
else if (f.name.endsWith('.ipa')) store.setPlatform('IOS');
|
||||||
|
|
||||||
|
store.setIsParsing(true);
|
||||||
|
try {
|
||||||
|
console.log('[useUpload] Parsing:', f.name, (f.size / 1024 / 1024).toFixed(1) + 'MB');
|
||||||
|
const info = await parsePackageUseCase.execute(f);
|
||||||
|
console.log('[useUpload] Parse result:', info);
|
||||||
|
if (info?.versionName) store.setVersionName(info.versionName);
|
||||||
|
if (info?.versionCode) store.setBuildNumber(String(info.versionCode));
|
||||||
|
if (info?.minSdkVersion) store.setMinOsVersion(info.minSdkVersion);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.warn('[useUpload] Parse failed:', err?.message);
|
||||||
|
store.setParseWarning('无法自动解析安装包信息,请手动填写版本号');
|
||||||
|
}
|
||||||
|
store.setIsParsing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const { file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate } =
|
||||||
|
useUploadStore.getState();
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
store.setUploadError('请选择 APK/IPA 文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setIsUploading(true);
|
||||||
|
store.setUploadError('');
|
||||||
|
try {
|
||||||
|
console.log('[useUpload] 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('[useUpload] Success:', result);
|
||||||
|
store.reset();
|
||||||
|
onSuccess();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[useUpload] Failed:', err);
|
||||||
|
store.setUploadError(err?.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
store.setIsUploading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleFileChange, handleSubmit };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { AppType, AppPlatform } from '@/domain/entities';
|
||||||
|
import { listVersionsUseCase } from '@/application/use-cases/version.use-cases';
|
||||||
|
|
||||||
|
export const versionKeys = {
|
||||||
|
all: ['versions'] as const,
|
||||||
|
list: (appType: AppType, platform: AppPlatform | '') =>
|
||||||
|
['versions', appType, platform] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useVersionList(appType: AppType, platformFilter: AppPlatform | '') {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: versionKeys.list(appType, platformFilter),
|
||||||
|
queryFn: () => listVersionsUseCase.execute(appType, platformFilter as AppPlatform | undefined),
|
||||||
|
staleTime: 30_000,
|
||||||
|
select: (data) => (Array.isArray(data) ? data : []),
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidate = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['versions', appType] });
|
||||||
|
|
||||||
|
return { ...query, invalidate };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { t } from '@/i18n/locales';
|
||||||
|
import type { AppVersion } from '@/domain/entities';
|
||||||
|
import {
|
||||||
|
toggleVersionUseCase,
|
||||||
|
deleteVersionUseCase,
|
||||||
|
} from '@/application/use-cases/version.use-cases';
|
||||||
|
|
||||||
|
export function useVersionMutations(onSuccess: () => void) {
|
||||||
|
const toggle = async (v: AppVersion) => {
|
||||||
|
await toggleVersionUseCase.execute(v.id, !v.isEnabled);
|
||||||
|
onSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = async (v: AppVersion) => {
|
||||||
|
if (!confirm(t('app_version_confirm_delete'))) return;
|
||||||
|
await deleteVersionUseCase.execute(v.id);
|
||||||
|
onSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { toggle, remove };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue