fix(admin-web): remove double-upload on app version page

- Remove auto-parse on file select (was uploading 48MB twice, took 100+ sec)
- Backend /upload already parses APK internally, version fields are now optional
- Show file name + size after selection
- Show progress hint during upload
- Better error extraction from API response
- Clear error when new file is selected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-06 10:11:07 -08:00
parent 4309e9e645
commit 9a40769e0d
2 changed files with 29 additions and 31 deletions

View File

@ -152,6 +152,7 @@ const translations: Record<Locale, Record<string, string>> = {
'app_version_upload_file': '选择 APK/IPA 文件', 'app_version_upload_file': '选择 APK/IPA 文件',
'app_version_parsing': '解析中...', 'app_version_parsing': '解析中...',
'app_version_uploading': '上传中...', 'app_version_uploading': '上传中...',
'optional_auto_from_apk': '选填,自动从安装包提取',
'app_version_confirm_delete': '确定删除此版本?此操作不可撤销。', 'app_version_confirm_delete': '确定删除此版本?此操作不可撤销。',
'app_version_no_versions': '暂无版本记录', 'app_version_no_versions': '暂无版本记录',
'app_version_edit': '编辑版本', 'app_version_edit': '编辑版本',
@ -918,6 +919,7 @@ const translations: Record<Locale, Record<string, string>> = {
'app_version_upload_file': 'Select APK/IPA File', 'app_version_upload_file': 'Select APK/IPA File',
'app_version_parsing': 'Parsing...', 'app_version_parsing': 'Parsing...',
'app_version_uploading': 'Uploading...', 'app_version_uploading': 'Uploading...',
'optional_auto_from_apk': 'optional, auto-extracted from package',
'app_version_confirm_delete': 'Delete this version? This action cannot be undone.', 'app_version_confirm_delete': 'Delete this version? This action cannot be undone.',
'app_version_no_versions': 'No versions found', 'app_version_no_versions': 'No versions found',
'app_version_edit': 'Edit Version', 'app_version_edit': 'Edit Version',
@ -1684,6 +1686,7 @@ const translations: Record<Locale, Record<string, string>> = {
'app_version_upload_file': 'APK/IPAファイルを選択', 'app_version_upload_file': 'APK/IPAファイルを選択',
'app_version_parsing': '解析中...', 'app_version_parsing': '解析中...',
'app_version_uploading': 'アップロード中...', 'app_version_uploading': 'アップロード中...',
'optional_auto_from_apk': '省略可、パッケージから自動抽出',
'app_version_confirm_delete': 'このバージョンを削除しますか?この操作は元に戻せません。', 'app_version_confirm_delete': 'このバージョンを削除しますか?この操作は元に戻せません。',
'app_version_no_versions': 'バージョンが見つかりません', 'app_version_no_versions': 'バージョンが見つかりません',
'app_version_edit': 'バージョンを編集', 'app_version_edit': 'バージョンを編集',

View File

@ -299,40 +299,21 @@ const UploadModal: React.FC<{
const [changelog, setChangelog] = useState(''); const [changelog, setChangelog] = useState('');
const [minOsVersion, setMinOsVersion] = useState(''); const [minOsVersion, setMinOsVersion] = useState('');
const [isForceUpdate, setIsForceUpdate] = useState(false); const [isForceUpdate, setIsForceUpdate] = useState(false);
const [parsing, setParsing] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0]; const f = e.target.files?.[0];
if (!f) return; if (!f) return;
setFile(f); setFile(f);
setError('');
// Auto-detect platform // Auto-detect platform from extension
if (f.name.endsWith('.apk')) setPlatform('ANDROID'); if (f.name.endsWith('.apk')) setPlatform('ANDROID');
else if (f.name.endsWith('.ipa')) setPlatform('IOS'); else if (f.name.endsWith('.ipa')) setPlatform('IOS');
// Auto-parse
setParsing(true);
try {
const formData = new FormData();
formData.append('file', f);
const info = await apiClient.post<{
versionCode?: number; versionName?: string; minSdkVersion?: string;
}>('/api/v1/admin/versions/parse', formData, {
timeout: 120000,
});
if (info?.versionName) setVersionName(info.versionName);
if (info?.versionCode) setBuildNumber(String(info.versionCode));
if (info?.minSdkVersion) setMinOsVersion(info.minSdkVersion);
} catch {
// Parsing failed, allow manual entry
}
setParsing(false);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file || !versionName) { if (!file) {
setError(t('app_version_upload_file')); setError(t('app_version_upload_file'));
return; return;
} }
@ -343,10 +324,10 @@ const UploadModal: React.FC<{
formData.append('file', file); formData.append('file', file);
formData.append('appType', appType); formData.append('appType', appType);
formData.append('platform', platform); formData.append('platform', platform);
formData.append('versionName', versionName); if (versionName) formData.append('versionName', versionName);
formData.append('buildNumber', buildNumber || '1'); if (buildNumber) formData.append('buildNumber', buildNumber);
formData.append('changelog', changelog); if (changelog) formData.append('changelog', changelog);
formData.append('minOsVersion', minOsVersion); if (minOsVersion) formData.append('minOsVersion', minOsVersion);
formData.append('isForceUpdate', String(isForceUpdate)); formData.append('isForceUpdate', String(isForceUpdate));
await apiClient.post('/api/v1/admin/versions/upload', formData, { await apiClient.post('/api/v1/admin/versions/upload', formData, {
@ -354,7 +335,7 @@ const UploadModal: React.FC<{
}); });
onSuccess(); onSuccess();
} catch (err: any) { } catch (err: any) {
setError(err?.message || 'Upload failed'); setError(err?.response?.data?.message || err?.message || 'Upload failed');
} }
setUploading(false); setUploading(false);
}; };
@ -370,7 +351,11 @@ const UploadModal: React.FC<{
onChange={handleFileChange} onChange={handleFileChange}
style={{ ...inputStyle, padding: '8px 12px', height: 'auto' }} style={{ ...inputStyle, padding: '8px 12px', height: 'auto' }}
/> />
{parsing && <div style={{ font: 'var(--text-caption)', color: 'var(--color-info)', marginTop: 4 }}>{t('app_version_parsing')}</div>} {file && (
<div style={{ font: 'var(--text-caption)', color: 'var(--color-text-secondary)', marginTop: 4 }}>
{file.name} ({(file.size / 1024 / 1024).toFixed(1)} MB)
</div>
)}
<label style={labelStyle}>{t('app_version_platform')}</label> <label style={labelStyle}>{t('app_version_platform')}</label>
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
@ -382,7 +367,12 @@ const UploadModal: React.FC<{
))} ))}
</div> </div>
<label style={labelStyle}>{t('app_version_version_name')} *</label> <label style={labelStyle}>
{t('app_version_version_name')}
<span style={{ font: 'var(--text-caption)', color: 'var(--color-text-secondary)', fontWeight: 400, marginLeft: 6 }}>
({t('optional_auto_from_apk')})
</span>
</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>
@ -405,12 +395,17 @@ const UploadModal: React.FC<{
</label> </label>
{error && <div style={{ color: 'var(--color-error)', font: 'var(--text-body-sm)', marginTop: 8 }}>{error}</div>} {error && <div style={{ color: 'var(--color-error)', font: 'var(--text-body-sm)', marginTop: 8 }}>{error}</div>}
{uploading && (
<div style={{ font: 'var(--text-caption)', color: 'var(--color-info)', marginTop: 4 }}>
{t('app_version_uploading_hint') || '正在上传,大文件需要较长时间,请耐心等待...'}
</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={onClose} style={{ padding: '8px 20px', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-full)', background: 'transparent', cursor: 'pointer', font: 'var(--text-label-sm)' }}> <button onClick={onClose} style={{ padding: '8px 20px', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-full)', background: 'transparent', cursor: 'pointer', font: 'var(--text-label-sm)' }}>
{t('cancel')} {t('cancel')}
</button> </button>
<button onClick={handleSubmit} disabled={uploading || parsing} style={{ ...primaryBtn, opacity: uploading || parsing ? 0.6 : 1 }}> <button onClick={handleSubmit} disabled={uploading} style={{ ...primaryBtn, opacity: uploading ? 0.6 : 1 }}>
{uploading ? t('app_version_uploading') : t('confirm')} {uploading ? t('app_version_uploading') : t('confirm')}
</button> </button>
</div> </div>