2026-06-17 11:53:20 +08:00
|
|
|
|
import React, { useEffect, useRef, useState } from "react";
|
2026-06-03 14:35:08 +08:00
|
|
|
|
import { View, Text, StyleSheet, Alert, Platform } from "react-native";
|
2025-11-05 15:18:15 +08:00
|
|
|
|
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
|
|
|
|
|
import { RootStackParamList } from "../App";
|
|
|
|
|
|
import RNFS from "react-native-fs";
|
2026-06-03 14:35:08 +08:00
|
|
|
|
import {
|
|
|
|
|
|
startDfu,
|
2026-06-17 11:53:20 +08:00
|
|
|
|
addDfuEventListener,
|
2026-06-03 14:35:08 +08:00
|
|
|
|
DfuProgressEvent,
|
|
|
|
|
|
DfuStateEvent,
|
|
|
|
|
|
} from "@systemic-games/react-native-nordic-nrf5-dfu";
|
2025-12-25 16:16:01 +08:00
|
|
|
|
import MyStatusbar from "./component/MyStatusbar";
|
|
|
|
|
|
import MyHeader from "./component/MyHeader";
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
type Props = NativeStackScreenProps<RootStackParamList, "Dfu">;
|
|
|
|
|
|
|
|
|
|
|
|
interface DeviceInfo {
|
|
|
|
|
|
hardware: number;
|
|
|
|
|
|
latestFirmware: string;
|
|
|
|
|
|
download: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
interface ParsedFirmware {
|
|
|
|
|
|
hardware: number;
|
|
|
|
|
|
iteration: number;
|
|
|
|
|
|
build: string;
|
|
|
|
|
|
raw: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parseFirmwareVersion = (text: string): ParsedFirmware => {
|
|
|
|
|
|
const raw = String(text ?? "").trim();
|
|
|
|
|
|
const parts = raw.split(".");
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length < 2) {
|
|
|
|
|
|
throw new Error(`固件版本格式不正确: ${raw}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hardware = parseInt(parts[0], 10);
|
|
|
|
|
|
const iteration = parseInt(parts[1], 10);
|
|
|
|
|
|
const build = parts.length >= 3 ? parts[2] : "";
|
|
|
|
|
|
|
|
|
|
|
|
if (Number.isNaN(hardware) || Number.isNaN(iteration)) {
|
|
|
|
|
|
throw new Error(`固件版本无法解析: ${raw}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
hardware,
|
|
|
|
|
|
iteration,
|
|
|
|
|
|
build,
|
|
|
|
|
|
raw,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
const trace = (step: string, payload?: unknown) => {
|
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
|
if (payload === undefined) {
|
|
|
|
|
|
console.log(`[DFU-TRACE ${timestamp}] ${step}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[DFU-TRACE ${timestamp}] ${step}`, payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-05 15:18:15 +08:00
|
|
|
|
export default function DfuScreen({ route, navigation }: Props) {
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const {
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
name,
|
|
|
|
|
|
firmware: deviceFirmware,
|
|
|
|
|
|
} = route.params;
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
const [progress, setProgress] = useState(0);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const [state, setState] = useState("准备中...");
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const [error, setError] = useState<string>();
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const [latestVersion, setLatestVersion] = useState("读取中...");
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const [isDfuRunning, setIsDfuRunning] = useState(false);
|
2026-06-17 11:53:20 +08:00
|
|
|
|
const startedRef = useRef(false);
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const mapDfuStateToChinese = (s: string): string => {
|
|
|
|
|
|
switch (s) {
|
|
|
|
|
|
case "connecting":
|
|
|
|
|
|
return "连接中…";
|
|
|
|
|
|
case "starting":
|
|
|
|
|
|
return "初始化中…";
|
|
|
|
|
|
case "enablingDfuMode":
|
|
|
|
|
|
return "启用 DFU 模式…";
|
|
|
|
|
|
case "uploading":
|
|
|
|
|
|
return "上传固件中…";
|
|
|
|
|
|
case "validating":
|
|
|
|
|
|
return "校验固件…";
|
|
|
|
|
|
case "disconnecting":
|
|
|
|
|
|
return "断开连接…";
|
|
|
|
|
|
case "completed":
|
|
|
|
|
|
return "升级完成";
|
|
|
|
|
|
case "aborted":
|
|
|
|
|
|
return "已取消";
|
2025-11-05 15:18:15 +08:00
|
|
|
|
case "failed":
|
2026-06-03 14:35:08 +08:00
|
|
|
|
case "dfu_failed":
|
|
|
|
|
|
return "升级失败";
|
|
|
|
|
|
case "initializing":
|
|
|
|
|
|
return "启动中…";
|
|
|
|
|
|
case "errored":
|
|
|
|
|
|
return "升级出错!";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return s;
|
2025-11-05 15:18:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
|
|
|
|
|
|
if (!isDfuRunning) return;
|
|
|
|
|
|
e.preventDefault();
|
2026-06-03 14:35:08 +08:00
|
|
|
|
Alert.alert("请稍候", "正在升级,请勿返回或关闭应用!");
|
2025-11-05 15:18:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, [navigation, isDfuRunning]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("screen mounted", {
|
|
|
|
|
|
routeParams: {
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
name,
|
|
|
|
|
|
deviceFirmware,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
trace("screen unmounted");
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [deviceId, name, deviceFirmware]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const sub = addDfuEventListener("log", (ev) => {
|
|
|
|
|
|
trace("dfu native log", ev);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
sub.remove();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (startedRef.current) {
|
|
|
|
|
|
trace("runDfu duplicate effect skipped");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
startedRef.current = true;
|
|
|
|
|
|
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const runDfu = async () => {
|
|
|
|
|
|
try {
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("runDfu begin");
|
2025-11-05 15:18:15 +08:00
|
|
|
|
setIsDfuRunning(true);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
setError(undefined);
|
|
|
|
|
|
|
|
|
|
|
|
const rawDeviceId = String(deviceId ?? "").trim();
|
2026-06-17 11:53:20 +08:00
|
|
|
|
const safeDeviceId = rawDeviceId;
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
const firmwareText = String(deviceFirmware ?? "").trim();
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("resolved input", {
|
|
|
|
|
|
rawDeviceId,
|
|
|
|
|
|
safeDeviceId,
|
|
|
|
|
|
firmwareText,
|
|
|
|
|
|
});
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
if (!safeDeviceId) {
|
|
|
|
|
|
throw new Error("无法生成 DFU 目标设备 ID");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentFw = parseFirmwareVersion(firmwareText);
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("parsed current firmware", currentFw);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
const manifestUrl =
|
|
|
|
|
|
"https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json";
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const manifestPath = RNFS.CachesDirectoryPath + "/latest.json";
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("manifest fetch start", { manifestUrl, manifestPath });
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const manifestResp = await fetch(manifestUrl);
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("manifest fetch response", { status: manifestResp.status });
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
if (!manifestResp.ok) {
|
|
|
|
|
|
throw new Error(`manifest 下载失败,HTTP ${manifestResp.status}`);
|
|
|
|
|
|
}
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const manifestText = await manifestResp.text();
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("manifest text loaded", { length: manifestText.length });
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
await RNFS.writeFile(manifestPath, manifestText, "utf8");
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("manifest saved", { manifestPath });
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
let manifest: { devices: DeviceInfo[] };
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] };
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
throw new Error("manifest 不是合法 JSON: " + manifestText.slice(0, 200));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deviceInfo = manifest.devices.find(
|
|
|
|
|
|
(d) => d.hardware === currentFw.hardware
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("manifest device matched", { deviceInfo });
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
if (!deviceInfo) {
|
|
|
|
|
|
setIsDfuRunning(false);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
Alert.alert(
|
|
|
|
|
|
"无法升级",
|
|
|
|
|
|
`未找到 hardware=${currentFw.hardware} 的固件`,
|
|
|
|
|
|
[{ text: "确认", onPress: () => navigation.goBack() }]
|
|
|
|
|
|
);
|
2025-11-05 15:18:15 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLatestVersion(deviceInfo.latestFirmware);
|
|
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware);
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("parsed latest firmware", latestFw);
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
if (latestFw.hardware !== currentFw.hardware) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (latestFw.iteration <= currentFw.iteration) {
|
2025-11-05 15:18:15 +08:00
|
|
|
|
setIsDfuRunning(false);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
Alert.alert("无需升级", "已是最新固件,无需升级", [
|
|
|
|
|
|
{ text: "确认", onPress: () => navigation.goBack() },
|
2025-11-05 15:18:15 +08:00
|
|
|
|
]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("upgrade allowed", {
|
2026-06-03 14:35:08 +08:00
|
|
|
|
currentIteration: currentFw.iteration,
|
|
|
|
|
|
latestIteration: latestFw.iteration,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
const localPath = RNFS.CachesDirectoryPath + "/firmware.zip";
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
const oldFileExists = await RNFS.exists(localPath);
|
|
|
|
|
|
if (oldFileExists) {
|
|
|
|
|
|
await RNFS.unlink(localPath);
|
|
|
|
|
|
trace("removed stale firmware zip", { localPath });
|
2026-06-03 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("firmware download start", {
|
|
|
|
|
|
downloadUrl: deviceInfo.download,
|
|
|
|
|
|
localPath,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const downloadResult = await RNFS.downloadFile({
|
|
|
|
|
|
fromUrl: deviceInfo.download,
|
|
|
|
|
|
toFile: localPath,
|
|
|
|
|
|
background: false,
|
|
|
|
|
|
discretionary: false,
|
|
|
|
|
|
}).promise;
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("firmware download result", downloadResult);
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
if (downloadResult.statusCode !== 200) {
|
|
|
|
|
|
throw new Error(`固件包下载失败,HTTP ${downloadResult.statusCode}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fileExists = await RNFS.exists(localPath);
|
|
|
|
|
|
trace("firmware exists check", { fileExists });
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
if (!fileExists) {
|
|
|
|
|
|
throw new Error(`固件包下载后文件不存在: ${localPath}`);
|
|
|
|
|
|
}
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
const fileStat = await RNFS.stat(localPath);
|
|
|
|
|
|
const fileSize = Number(fileStat.size ?? 0);
|
|
|
|
|
|
trace("firmware stat", fileStat);
|
|
|
|
|
|
|
|
|
|
|
|
if (!Number.isFinite(fileSize) || fileSize <= 0) {
|
|
|
|
|
|
throw new Error(`固件包文件无效,大小为 ${fileStat.size}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dfuFilePath = "file://" + localPath;
|
|
|
|
|
|
let iosBootloaderName: string | undefined;
|
|
|
|
|
|
if (Platform.OS === "ios") {
|
|
|
|
|
|
if (name?.startsWith("PF-PM5-")) {
|
|
|
|
|
|
iosBootloaderName = "PF-PM5-DFU";
|
|
|
|
|
|
} else if (name?.startsWith("PF-STK-")) {
|
|
|
|
|
|
iosBootloaderName = "PF-STK-DFU";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
trace("startDfu call", {
|
2026-06-03 14:35:08 +08:00
|
|
|
|
safeDeviceId,
|
|
|
|
|
|
dfuFilePath,
|
2026-06-17 11:53:20 +08:00
|
|
|
|
localPath,
|
|
|
|
|
|
fileSize,
|
2026-06-03 14:35:08 +08:00
|
|
|
|
currentFirmware: currentFw.raw,
|
|
|
|
|
|
latestFirmware: latestFw.raw,
|
2026-06-17 11:53:20 +08:00
|
|
|
|
iosBootloaderName,
|
2026-06-03 14:35:08 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await startDfu(safeDeviceId, dfuFilePath, {
|
2026-06-17 11:53:20 +08:00
|
|
|
|
alternativeAdvertisingName: iosBootloaderName,
|
2026-06-03 14:35:08 +08:00
|
|
|
|
dfuStateListener: (ev: DfuStateEvent) => {
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("dfu state", ev);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
setState(ev.state);
|
|
|
|
|
|
},
|
|
|
|
|
|
dfuProgressListener: (ev: DfuProgressEvent) => {
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("dfu progress", ev);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
setProgress(ev.percent);
|
|
|
|
|
|
},
|
2025-11-05 15:18:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("startDfu resolved successfully");
|
2025-11-05 15:18:15 +08:00
|
|
|
|
setIsDfuRunning(false);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
Alert.alert("升级成功", "升级成功,请重连设备", [
|
|
|
|
|
|
{
|
|
|
|
|
|
text: "确认",
|
|
|
|
|
|
onPress: () => {
|
|
|
|
|
|
navigation.reset({
|
|
|
|
|
|
index: 0,
|
|
|
|
|
|
routes: [{ name: "Home" }],
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
2025-11-05 15:18:15 +08:00
|
|
|
|
} catch (err: any) {
|
2026-06-17 11:53:20 +08:00
|
|
|
|
trace("runDfu error", {
|
|
|
|
|
|
message: err?.message,
|
|
|
|
|
|
code: err?.code,
|
|
|
|
|
|
name: err?.name,
|
|
|
|
|
|
error: err,
|
|
|
|
|
|
});
|
2025-11-05 15:18:15 +08:00
|
|
|
|
setIsDfuRunning(false);
|
2026-06-03 14:35:08 +08:00
|
|
|
|
setError(err?.message || "DFU失败");
|
|
|
|
|
|
Alert.alert("升级失败", err?.message || "DFU失败", [
|
|
|
|
|
|
{ text: "确认", onPress: () => navigation.goBack() },
|
2025-11-05 15:18:15 +08:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
runDfu();
|
2026-06-17 11:53:20 +08:00
|
|
|
|
}, [deviceId, name, deviceFirmware, navigation]);
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-06-03 14:35:08 +08:00
|
|
|
|
<View style={styles.container}>
|
|
|
|
|
|
<MyStatusbar backgroundColor="#FFFFFF" dark />
|
|
|
|
|
|
<MyHeader
|
|
|
|
|
|
title="固件升级"
|
|
|
|
|
|
textColor="#333"
|
|
|
|
|
|
backgroundColor="#FFFFFF"
|
|
|
|
|
|
navigation={navigation}
|
|
|
|
|
|
/>
|
2025-12-25 16:16:01 +08:00
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
<View style={styles.content}>
|
2025-12-25 16:16:01 +08:00
|
|
|
|
<View style={styles.row}>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
<Text style={styles.titleText}>蓝牙名称: {name || "--"}</Text>
|
2025-12-25 16:16:01 +08:00
|
|
|
|
</View>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2025-12-25 16:16:01 +08:00
|
|
|
|
<View style={styles.row}>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
<Text style={styles.normalText}>最新版本: {latestVersion}</Text>
|
2025-12-25 16:16:01 +08:00
|
|
|
|
</View>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2025-12-25 16:16:01 +08:00
|
|
|
|
<View style={styles.row}>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
<Text style={styles.normalText}>当前版本: {deviceFirmware || "--"}</Text>
|
2025-12-25 16:16:01 +08:00
|
|
|
|
</View>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
|
2025-12-25 16:16:01 +08:00
|
|
|
|
<View style={styles.row}>
|
2026-06-03 14:35:08 +08:00
|
|
|
|
<Text style={styles.normalText}>
|
|
|
|
|
|
升级状态: {mapDfuStateToChinese(state)}
|
2025-12-25 16:16:01 +08:00
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.progressContainer}>
|
|
|
|
|
|
<View style={[styles.progressBar, { width: `${progress}%` }]} />
|
|
|
|
|
|
<Text style={styles.progressText}>{progress}%</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
2026-06-03 14:35:08 +08:00
|
|
|
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
2025-11-05 15:18:15 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
2026-06-03 14:35:08 +08:00
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: "#FFFFFF",
|
|
|
|
|
|
},
|
|
|
|
|
|
content: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
padding: 20,
|
|
|
|
|
|
backgroundColor: "#FFFFFF",
|
|
|
|
|
|
},
|
2025-11-05 15:18:15 +08:00
|
|
|
|
row: {
|
|
|
|
|
|
borderBottomWidth: 1,
|
2026-06-03 14:35:08 +08:00
|
|
|
|
borderBottomColor: "#E7141E",
|
|
|
|
|
|
paddingBottom: 6,
|
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
|
},
|
|
|
|
|
|
titleText: {
|
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
|
color: "#111111",
|
|
|
|
|
|
fontWeight: "600",
|
|
|
|
|
|
},
|
|
|
|
|
|
normalText: {
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
color: "#222222",
|
2025-11-05 15:18:15 +08:00
|
|
|
|
},
|
|
|
|
|
|
progressContainer: {
|
|
|
|
|
|
height: 30,
|
2026-06-03 14:35:08 +08:00
|
|
|
|
backgroundColor: "#EEEEEE",
|
2025-11-05 15:18:15 +08:00
|
|
|
|
borderRadius: 15,
|
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
|
marginTop: 40,
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
progressBar: {
|
|
|
|
|
|
height: "100%",
|
|
|
|
|
|
backgroundColor: "#E7141E",
|
|
|
|
|
|
},
|
|
|
|
|
|
progressText: {
|
|
|
|
|
|
position: "absolute",
|
|
|
|
|
|
alignSelf: "center",
|
|
|
|
|
|
fontWeight: "bold",
|
2026-06-03 14:35:08 +08:00
|
|
|
|
color: "#000000",
|
|
|
|
|
|
},
|
|
|
|
|
|
errorText: {
|
|
|
|
|
|
color: "red",
|
|
|
|
|
|
marginTop: 20,
|
|
|
|
|
|
fontSize: 14,
|
2025-11-05 15:18:15 +08:00
|
|
|
|
},
|
2026-06-17 11:53:20 +08:00
|
|
|
|
});
|