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:
parent
4309e9e645
commit
9a40769e0d
|
|
@ -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': 'バージョンを編集',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue