import React, {useState} from 'react' import {Linking, Platform, Pressable, StyleSheet, Text, View} from 'react-native' import {UpdateSDK} from '@xuqm/rn-sdk' interface Props { appVersionCode: number rnModuleId: string rnCurrentVersion: string onLog: (msg: string) => void } type StepStatus = 'idle' | 'loading' | 'ok' | 'error' interface CheckResult { needsUpdate: boolean label: string detail: string } export function UpdatePanel({appVersionCode, rnModuleId, rnCurrentVersion, onLog}: Props) { const [appStep, setAppStep] = useState('idle') const [rnCheckStep, setRnCheckStep] = useState('idle') const [rnDownloadStep, setRnDownloadStep] = useState('idle') const [rnCacheStep, setRnCacheStep] = useState('idle') const [appResult, setAppResult] = useState(null) const [rnResult, setRnResult] = useState(null) const [rnDownloadInfo, setRnDownloadInfo] = useState('') const [rnCacheInfo, setRnCacheInfo] = useState('') const reset = () => { setAppStep('idle') setRnCheckStep('idle') setRnDownloadStep('idle') setRnCacheStep('idle') setAppResult(null) setRnResult(null) setRnDownloadInfo('') setRnCacheInfo('') } const runAppUpdate = async () => { reset() setAppStep('loading') try { const res = await UpdateSDK.checkAppUpdate(appVersionCode) if (!res.needsUpdate) { setAppResult({ needsUpdate: false, label: '✓ App 已是最新版本', detail: `当前版本码:${appVersionCode},服务端无更高版本`, }) setAppStep('ok') onLog('App 更新检查:当前最新') return } setAppResult({ needsUpdate: true, label: `↑ 发现新版本 ${res.versionName ?? ''}(code=${res.versionCode ?? ''})`, detail: [ res.changeLog ? `更新说明:${res.changeLog}` : '', `强制更新:${res.forceUpdate ? '是' : '否'}`, res.downloadUrl ? `下载:${res.downloadUrl}` : '', res.appStoreUrl ? `App Store:${res.appStoreUrl}` : '', ].filter(Boolean).join('\n'), }) setAppStep('ok') onLog(`发现 App 新版本:${res.versionName}`) } catch (e) { const msg = e instanceof Error ? e.message : 'App 更新检查失败' setAppResult({needsUpdate: false, label: '✗ 检查失败', detail: msg}) setAppStep('error') onLog(`App 更新失败:${msg}`) } } const runRnUpdate = async () => { setRnCheckStep('loading') setRnDownloadStep('idle') setRnCacheStep('idle') setRnResult(null) setRnDownloadInfo('') setRnCacheInfo('') let checkInfo: Awaited> try { checkInfo = await UpdateSDK.checkRnUpdate(rnModuleId, rnCurrentVersion) if (!checkInfo.needsUpdate) { setRnResult({ needsUpdate: false, label: '✓ 插件已是最新版本', detail: `当前版本:${rnCurrentVersion},服务端无更高版本`, }) setRnCheckStep('ok') onLog('插件更新检查:当前最新') return } setRnResult({ needsUpdate: true, label: `↑ 发现插件版本 ${checkInfo.latestVersion}(当前 ${rnCurrentVersion})`, detail: [ `模块:${rnModuleId}`, `平台:${Platform.OS}`, `md5:${checkInfo.md5}`, checkInfo.note ? `说明:${checkInfo.note}` : '', `minCommonVersion:${checkInfo.minCommonVersion}`, ].filter(Boolean).join('\n'), }) setRnCheckStep('ok') onLog(`发现插件新版本:${checkInfo.latestVersion}`) } catch (e) { const msg = e instanceof Error ? e.message : '检查失败' setRnResult({needsUpdate: false, label: '✗ 检查失败', detail: msg}) setRnCheckStep('error') onLog(`插件更新检查失败:${msg}`) return } // Step 2: Download setRnDownloadStep('loading') let source: string try { source = await UpdateSDK.downloadRnBundle(checkInfo.downloadUrl) setRnDownloadInfo(`下载完成,源码长度:${source.length} 字符`) setRnDownloadStep('ok') onLog(`插件 bundle 下载完成:${source.length} chars`) } catch (e) { const msg = e instanceof Error ? e.message : '下载失败' setRnDownloadInfo(`✗ 下载失败:${msg}`) setRnDownloadStep('error') onLog(`插件下载失败:${msg}`) return } // Step 3: Cache setRnCacheStep('loading') try { const cached = await UpdateSDK.cacheRnBundle( rnModuleId, checkInfo.latestVersion, checkInfo.md5, source, ) setRnCacheInfo( `已写入 AsyncStorage\n版本:${cached.version}\nmd5:${cached.md5}\n时间:${cached.downloadedAt}`, ) setRnCacheStep('ok') onLog(`插件 bundle 已缓存:${cached.version}`) } catch (e) { const msg = e instanceof Error ? e.message : '缓存失败' setRnCacheInfo(`✗ 缓存失败:${msg}`) setRnCacheStep('error') onLog(`插件缓存失败:${msg}`) } } const handleOpenDownload = () => { if (appResult?.detail) { const match = appResult.detail.match(/下载:(.+)/) const url = match?.[1]?.trim() if (url) Linking.openURL(url) } } return ( {/* App 整包更新 */} App 整包更新(APK / IPA) Android 返回 APK 下载链接,iOS 返回 App Store 跳转。 本地版本码写死为 {appVersionCode}。 {appResult && ( )} {appResult?.needsUpdate && appStep === 'ok' && ( 打开下载链接 ↗ )} {/* RN 插件热更新 */} RN 插件热更新(JS Bundle) 模块 {rnModuleId},本地版本 {rnCurrentVersion}。 热更新全流程:检查 → 下载 → 缓存至 AsyncStorage。 {rnResult && ( )} {rnCheckStep === 'ok' && rnResult?.needsUpdate && ( <> {rnDownloadInfo ? ( ) : null} {rnCacheInfo ? ( ) : null} )} ) } function StepRow({ step, label, status, onPress, buttonText, }: { step: number label: string status: StepStatus onPress?: () => void buttonText?: string }) { const icon = status === 'idle' ? '○' : status === 'loading' ? '⟳' : status === 'ok' ? '✓' : '✗' const iconColor = status === 'ok' ? '#22c55e' : status === 'error' ? '#ef4444' : status === 'loading' ? '#f59e0b' : '#9ca3af' return ( {icon} 步骤 {step}: {label} {onPress && buttonText && ( {status === 'loading' ? '检查中…' : buttonText} )} ) } function ResultBox({label, detail, ok}: {label: string; detail: string; ok: boolean}) { return ( {label} {detail ? {detail} : null} ) } const styles = StyleSheet.create({ container: {gap: 16}, block: {gap: 10}, blockTitle: {fontSize: 15, fontWeight: '800', color: '#1f2933'}, blockHint: {fontSize: 13, lineHeight: 19, color: '#6b7280'}, mono: {fontFamily: 'monospace', fontSize: 12}, stepRow: {flexDirection: 'row', alignItems: 'center', gap: 8}, stepIcon: {fontSize: 18, width: 24, textAlign: 'center'}, stepBody: {flex: 1}, stepLabel: {fontSize: 13, color: '#374151'}, stepNum: {fontWeight: '700', color: '#1f2933'}, stepBtn: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 12, backgroundColor: '#1f2933', }, stepBtnText: {color: '#fff', fontSize: 12, fontWeight: '700'}, btnDisabled: {opacity: 0.5}, resultBox: { borderRadius: 12, padding: 10, gap: 4, }, resultBoxOk: {backgroundColor: '#f0fdf4', borderWidth: 1, borderColor: '#86efac'}, resultBoxInfo: {backgroundColor: '#fff7ed', borderWidth: 1, borderColor: '#fdba74'}, resultLabel: {fontSize: 13, fontWeight: '600', color: '#1f2933'}, resultDetail: {fontSize: 12, color: '#4b5563', lineHeight: 18, fontFamily: 'monospace'}, openBtn: { minHeight: 40, borderRadius: 12, alignItems: 'center', justifyContent: 'center', backgroundColor: '#2563eb', }, openBtnText: {color: '#fff', fontSize: 13, fontWeight: '700'}, })