chargingDetailsPageComponent.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { View, Text, Pressable, Dimensions,Image, StyleSheet, ScrollView } from 'react-native';
  2. import { SafeAreaView } from 'react-native-safe-area-context';
  3. import { router, useLocalSearchParams } from 'expo-router';
  4. import { CrossLogoSvg } from '../global/SVG';
  5. import { useEffect, useMemo, useState, useRef } from 'react';
  6. import { chargeStationService } from '../../service/chargeStationService';
  7. import { ChargingDetails, Remark, ElectricityPrice, Special } from '../../service/type/chargeStationType';
  8. import { format, parseISO } from 'date-fns';
  9. import { useTranslation } from '../../util/hooks/useTranslation';
  10. const ChargingDetailsPageComponent = () => {
  11. const screenHeight = Dimensions.get('window').height;
  12. const params = useLocalSearchParams();
  13. const [list, setList] = useState<ChargingDetails>({} as ChargingDetails);
  14. const [remark, setRemark] = useState<Remark>({} as Remark);
  15. const [time, setTime] = useState<string>('');
  16. const { t } = useTranslation();
  17. useEffect(() => {
  18. const fetchData = async () => {
  19. const res = await chargeStationService.fetchChargingDetails(params.id.toString())
  20. if (res) {
  21. const firstItem = Array.isArray(res) && res.length > 0 ? res[0] : null;
  22. // 设置充电详情
  23. setList(firstItem || ({} as ChargingDetails));
  24. // 安全地解析 remark 字段
  25. let parsedRemark: Remark = {} as Remark;
  26. if (firstItem?.remark) {
  27. try {
  28. parsedRemark = JSON.parse(firstItem.remark) as Remark;
  29. } catch (e) {
  30. console.warn('Failed to parse remark:', e);
  31. parsedRemark = {} as Remark;
  32. }
  33. }
  34. setRemark(parsedRemark);
  35. // 解析并格式化日期
  36. const startTime = res[0].actual_start_time? parseISO(res[0].actual_start_time): null
  37. const endTime = res[0].actual_end_time?parseISO(res[0].actual_end_time): null
  38. let formattedDate = '';
  39. if (startTime && endTime) {
  40. // 格式化为指定格式
  41. formattedDate = `${format(startTime, 'yyyy/MM/dd HH:mm:ss')}-${format(endTime, 'HH:mm:ss')}`;
  42. } else {
  43. formattedDate = t('chargingHistory.charging_details.invalid_time');
  44. }
  45. setTime(formattedDate)
  46. }
  47. };
  48. fetchData();
  49. }, [])
  50. const totalPrice = useMemo(() => {
  51. if (list.promotion_name) {
  52. const price = list?.connector?.EquipmentID?.StationID?.price
  53. if (price && remark.TotalPower) {
  54. return `${(price * remark.TotalPower).toFixed(1)}`
  55. }
  56. return '0'
  57. } else {
  58. if (list.total_fee && list.withdraw_fee) {
  59. let actual_fee = list.total_fee - list.withdraw_fee
  60. const value = actual_fee <= 0? '0': actual_fee % 1 === 0? `${actual_fee}`: `${actual_fee.toFixed(1)}`
  61. return value
  62. }
  63. return '0';
  64. }
  65. }, [list]);
  66. const couponPrice = useMemo(() => {
  67. if (list.promotion_name) {
  68. let actual_fee = (list.total_fee - list.withdraw_fee) > 0 ? (list.total_fee - list.withdraw_fee) : 0;
  69. return actual_fee.toFixed(1)
  70. }
  71. }, [list])
  72. return (
  73. <SafeAreaView className="flex-1 bg-white" edges={['top']}>
  74. <ScrollView>
  75. <View style={{ minHeight: screenHeight, flex: 1 }}>
  76. <View className="mx-[3%]" style={{ marginTop: 25}}>
  77. <Pressable
  78. onPress={() => {
  79. if (router.canGoBack()) {
  80. router.back();
  81. } else {
  82. router.replace('/accountMainPage');
  83. }
  84. }}
  85. className='ml-2'
  86. >
  87. <CrossLogoSvg />
  88. </Pressable>
  89. <View className="items-center px-3">
  90. <Image
  91. source={require('../../assets/ccLogo.png')}
  92. resizeMode="contain"
  93. style={{
  94. width: 100,
  95. height: 100
  96. }}
  97. />
  98. <Text style={styles.totalPrice}>{list.promotion_name? couponPrice:totalPrice}</Text>
  99. <View style={styles.viewLine}></View>
  100. <View className='w-full flex-row justify-between mt-6'>
  101. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.order_number')}: </Text>
  102. <View style={{ flex: 1, marginLeft: 5 }}>
  103. <Text style={styles.rightLable}>{list.format_order_id}</Text>
  104. </View>
  105. </View>
  106. <View className='w-full flex-row justify-between my-3'>
  107. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.charging_time')}: </Text>
  108. <View style={{ flex: 1, marginLeft: 5 }}>
  109. <Text style={styles.rightLable}>{time}</Text>
  110. </View>
  111. </View>
  112. <View className='w-full flex-row justify-between mb-4'>
  113. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.charging_station')}:</Text>
  114. <View style={{ flex: 1, marginLeft: 5 }}>
  115. <Text style={styles.rightLable}>{params.chargeStationName}</Text>
  116. </View>
  117. </View>
  118. <View style={styles.viewLine}></View>
  119. <ChargingDataComponent list={list} remark={remark} totalPrice={totalPrice}/>
  120. <View style={styles.viewLine}></View>
  121. <View className='w-full flex-row justify-between mt-6'>
  122. <View>
  123. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.amount_paid')}:</Text>
  124. {list.promotion_name ? <Text style={{fontSize: 12, color:'#888888'}}>{t('chargingHistory.charging_details.coupon_payment')}</Text>: null}
  125. </View>
  126. <Text style={styles.rightLable}>${list.promotion_name? couponPrice:totalPrice}</Text>
  127. </View>
  128. </View>
  129. </View>
  130. </View>
  131. </ScrollView>
  132. </SafeAreaView>
  133. );
  134. };
  135. interface ChargingDataComponentProps {
  136. list: ChargingDetails;
  137. totalPrice: string;
  138. remark: Remark;
  139. }
  140. const ChargingDataComponent: React.FC<ChargingDataComponentProps> = ({
  141. list,
  142. totalPrice,
  143. remark
  144. }) => {
  145. const hasFetchedPrice = useRef(false); // 添加 ref 跟踪是否已获取过价格
  146. const [rush, setRush] = useState<Special>({} as Special)
  147. const [elses, setElses ] = useState<Special>({} as Special)
  148. const [off, setOff] = useState<Special>({} as Special)
  149. const { t } = useTranslation();
  150. useEffect(() => {
  151. if (!list.promotion_name && !hasFetchedPrice.current && list.actual_start_time) {
  152. chargeStationService.fetchElectricityPrice(list.pricemodel_id || 'a').then(res => {
  153. hasFetchedPrice.current = true; // 标记为已调用
  154. const date = new Date(list.actual_start_time);
  155. const str = (date.toLocaleString('en-US', { weekday: 'short' })).toLowerCase();
  156. if (res && res.length > 0) {
  157. if (remark.RushKwh) {
  158. const obj = (res[1][str as keyof ElectricityPrice]) as Special
  159. setRush(obj)
  160. }
  161. if (remark.ElseKwh) {
  162. const obj = (res[2][str as keyof ElectricityPrice]) as Special
  163. setElses(obj)
  164. }
  165. if (remark.OffKwh) {
  166. const obj = (res[0][str as keyof ElectricityPrice]) as Special
  167. setOff(obj)
  168. }
  169. }
  170. })
  171. }
  172. }, [list.actual_start_time])
  173. if (!list.promotion_name) {
  174. return (
  175. <View>
  176. {(remark.RushKwh) ?
  177. <View>
  178. <View className='w-full flex-row justify-between mt-4'>
  179. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.peak_electricity')}: </Text>
  180. <Text style={styles.rightLable}>{remark.RushKwh?.toFixed(1)}</Text>
  181. </View>
  182. <View className='w-full flex-row justify-between my-3'>
  183. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.peak_rate')}({rush.from}-{rush.to}):</Text>
  184. <Text style={styles.rightLable}>${rush.price}</Text>
  185. </View>
  186. <View className='w-full flex-row justify-between mb-3'>
  187. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.peak_fee')}:</Text>
  188. <Text style={styles.rightLable}>${remark.RushCharge?.toFixed(1)}</Text>
  189. </View>
  190. </View>: null }
  191. {(remark.ElseKwh) ?
  192. <View>
  193. <View className='w-full flex-row justify-between mt-4'>
  194. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.flat_electricity')}: </Text>
  195. <Text style={styles.rightLable}>{remark.ElseKwh?.toFixed(1)}</Text>
  196. </View>
  197. <View className='w-full flex-row justify-between my-3'>
  198. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.flat_rate')}({elses.from}-{elses.to}):</Text>
  199. <Text style={styles.rightLable}>${elses.price}</Text>
  200. </View>
  201. <View className='w-full flex-row justify-between mb-3'>
  202. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.flat_fee')}:</Text>
  203. <Text style={styles.rightLable}>${remark.ElseCharge?.toFixed(1)}</Text>
  204. </View>
  205. </View>: null }
  206. {(remark.OffKwh) ?
  207. <View>
  208. <View className='w-full flex-row justify-between mt-4'>
  209. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.valley_electricity')}: </Text>
  210. <Text style={styles.rightLable}>{remark.OffKwh?.toFixed(1)}</Text>
  211. </View>
  212. <View className='w-full flex-row justify-between my-3'>
  213. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.valley_rate')}({off.from}-{off.to}): </Text>
  214. <Text style={styles.rightLable}>${off.price}</Text>
  215. </View>
  216. <View className='w-full flex-row justify-between mb-3'>
  217. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.valley_fee')}:</Text>
  218. <Text style={styles.rightLable}>${remark.OffCharge?.toFixed(1)}</Text>
  219. </View>
  220. </View>: null }
  221. </View>
  222. )
  223. } else {
  224. return (
  225. <View>
  226. <View className='w-full flex-row justify-between mt-4'>
  227. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.total_electricity')}: </Text>
  228. <Text style={styles.rightLable}>{remark.TotalPower?.toFixed(1)}</Text>
  229. </View>
  230. <View className='w-full flex-row justify-between my-3'>
  231. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.electricity_rate')}: </Text>
  232. <Text style={styles.rightLable}>${list?.connector?.EquipmentID?.StationID?.price}</Text>
  233. </View>
  234. <View className='w-full flex-row justify-between mb-3'>
  235. <Text style={styles.leftLable}>{t('chargingHistory.charging_details.total_fee')}: </Text>
  236. <Text style={styles.rightLable}>${totalPrice}</Text>
  237. </View>
  238. </View>
  239. )
  240. }
  241. }
  242. const styles = StyleSheet.create({
  243. viewLine: {
  244. width: '100%',
  245. height: 1,
  246. backgroundColor: '#E5E5E5',
  247. },
  248. totalPrice: {
  249. fontSize: 26,
  250. fontWeight: 'bold',
  251. marginBottom: 25,
  252. },
  253. leftLable: {
  254. fontSize: 18,
  255. color:'#888888',
  256. },
  257. rightLable: {
  258. fontSize: 17,
  259. flex: 1,
  260. flexWrap: 'wrap',
  261. textAlign: 'right',
  262. },
  263. })
  264. export default ChargingDetailsPageComponent;