|
@@ -0,0 +1,296 @@
|
|
|
|
|
+import { Stack, useRouter } from 'expo-router';
|
|
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
|
|
+import { FlatList, ImageBackground, RefreshControl, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
|
|
|
+import services from '../../services/api';
|
|
|
|
|
+
|
|
|
|
|
+const TABS = [
|
|
|
|
|
+ { title: '全部', value: '' },
|
|
|
|
|
+ { title: '收入', value: 'IN' },
|
|
|
|
|
+ { title: '支出', value: 'OUT' },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// Define types
|
|
|
|
|
+interface TransactionItem {
|
|
|
|
|
+ itemId: string;
|
|
|
|
|
+ itemDesc: string;
|
|
|
|
|
+ createTime: string;
|
|
|
|
|
+ type: string;
|
|
|
|
|
+ money: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default function WalletScreen() {
|
|
|
|
|
+ const router = useRouter();
|
|
|
|
|
+ const [balance, setBalance] = useState('0.00');
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState(TABS[0]);
|
|
|
|
|
+ const [transactions, setTransactions] = useState<TransactionItem[]>([]);
|
|
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
|
|
+ const [refreshing, setRefreshing] = useState(false);
|
|
|
|
|
+ const [page, setPage] = useState(1);
|
|
|
|
|
+ const [hasMore, setHasMore] = useState(true);
|
|
|
|
|
+
|
|
|
|
|
+ const fetchWalletInfo = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await services.wallet.info('CASH');
|
|
|
|
|
+ if (res && res.balance) {
|
|
|
|
|
+ setBalance(res.balance);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to fetch wallet info', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchTransactions = async (pageNum = 1, isRefresh = false) => {
|
|
|
|
|
+ if (loading) return;
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await services.wallet.bill(pageNum, 20, 'CASH', activeTab.value);
|
|
|
|
|
+ if (res && res.records) {
|
|
|
|
|
+ if (isRefresh) {
|
|
|
|
|
+ setTransactions(res.records);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setTransactions(prev => [...prev, ...res.records]);
|
|
|
|
|
+ }
|
|
|
|
|
+ setHasMore(res.records.length >= 20);
|
|
|
|
|
+ setPage(pageNum);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (isRefresh) setTransactions([]);
|
|
|
|
|
+ setHasMore(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to fetch transactions', error);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ setRefreshing(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchWalletInfo();
|
|
|
|
|
+ fetchTransactions(1, true);
|
|
|
|
|
+ }, [activeTab]);
|
|
|
|
|
+
|
|
|
|
|
+ const onRefresh = () => {
|
|
|
|
|
+ setRefreshing(true);
|
|
|
|
|
+ fetchWalletInfo();
|
|
|
|
|
+ fetchTransactions(1, true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const loadMore = () => {
|
|
|
|
|
+ if (hasMore && !loading) {
|
|
|
|
|
+ fetchTransactions(page + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ const renderItem = ({ item }: { item: TransactionItem }) => (
|
|
|
|
|
+ <View style={styles.cell}>
|
|
|
|
|
+ <View>
|
|
|
|
|
+ <Text style={styles.itemDesc}>{item.itemDesc}</Text>
|
|
|
|
|
+ <Text style={styles.itemTime}>{item.createTime}</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ <Text style={[styles.itemAmount, item.type === 'IN' ? styles.amountIn : styles.amountOut]}>
|
|
|
|
|
+ {item.type === 'IN' ? '+' : '-'}{item.money}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <ImageBackground
|
|
|
|
|
+ source={{ uri: 'https://cdn.acetoys.cn/kai_xin_ma_te/supermart/mine/kaixinMineBg.png' }}
|
|
|
|
|
+ style={styles.container}
|
|
|
|
|
+ resizeMode="cover"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Stack.Screen options={{
|
|
|
|
|
+ title: '钱包',
|
|
|
|
|
+ headerTitleStyle: { color: '#fff' },
|
|
|
|
|
+ headerStyle: { backgroundColor: 'transparent' },
|
|
|
|
|
+ headerTransparent: true,
|
|
|
|
|
+ headerTintColor: '#fff',
|
|
|
|
|
+ }} />
|
|
|
|
|
+
|
|
|
|
|
+ <SafeAreaView style={{ flex: 1, marginTop: 100 }}>
|
|
|
|
|
+ {/* Info Card */}
|
|
|
|
|
+ <View style={styles.infoCard}>
|
|
|
|
|
+ <View style={styles.balanceContainer}>
|
|
|
|
|
+ <Text style={styles.balanceAmount}>{balance}</Text>
|
|
|
|
|
+ <Text style={styles.balanceLabel}>余额</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ <View style={styles.actionButtons}>
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ style={styles.btnWithdraw}
|
|
|
|
|
+ onPress={() => router.push('/wallet/withdraw')}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text style={styles.btnWithdrawText}>我要提现</Text>
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ style={styles.btnRecharge}
|
|
|
|
|
+ onPress={() => router.push('/wallet/recharge')}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text style={styles.btnRechargeText}>充值</Text>
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Tabs */}
|
|
|
|
|
+ <View style={styles.tabsContainer}>
|
|
|
|
|
+ {TABS.map((tab, index) => (
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ key={index}
|
|
|
|
|
+ style={styles.tabItem}
|
|
|
|
|
+ onPress={() => setActiveTab(tab)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
|
|
|
|
|
+ {tab.title}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Transaction List */}
|
|
|
|
|
+ <View style={styles.listContainer}>
|
|
|
|
|
+ <FlatList
|
|
|
|
|
+ data={transactions}
|
|
|
|
|
+ renderItem={renderItem}
|
|
|
|
|
+ keyExtractor={(item) => item.itemId || Math.random().toString()}
|
|
|
|
|
+ contentContainerStyle={styles.listContent}
|
|
|
|
|
+ refreshControl={
|
|
|
|
|
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#333" />
|
|
|
|
|
+ }
|
|
|
|
|
+ onEndReached={loadMore}
|
|
|
|
|
+ onEndReachedThreshold={0.5}
|
|
|
|
|
+ // ListFooterComponent={loading && !refreshing ? <ActivityIndicator style={{marginTop: 10}} /> : null}
|
|
|
|
|
+ ListEmptyComponent={!loading ? <View style={styles.emptyContainer}><Text style={styles.emptyText}>暂无记录</Text></View> : null}
|
|
|
|
|
+ />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </SafeAreaView>
|
|
|
|
|
+ </ImageBackground>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const styles = StyleSheet.create({
|
|
|
|
|
+ container: {
|
|
|
|
|
+ flex: 1,
|
|
|
|
|
+ backgroundColor: '#f5f5f5',
|
|
|
|
|
+ },
|
|
|
|
|
+ infoCard: {
|
|
|
|
|
+ marginHorizontal: 14,
|
|
|
|
|
+ marginTop: 12,
|
|
|
|
|
+ marginBottom: 15,
|
|
|
|
|
+ borderRadius: 10,
|
|
|
|
|
+ height: 139,
|
|
|
|
|
+ backgroundColor: '#3d3f41',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ balanceContainer: {
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ marginTop: 10,
|
|
|
|
|
+ },
|
|
|
|
|
+ balanceAmount: {
|
|
|
|
|
+ fontSize: 30,
|
|
|
|
|
+ fontWeight: 'bold',
|
|
|
|
|
+ color: '#fff',
|
|
|
|
|
+ },
|
|
|
|
|
+ balanceLabel: {
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ color: 'rgba(255,255,255,0.8)',
|
|
|
|
|
+ marginTop: 2,
|
|
|
|
|
+ marginBottom: 15,
|
|
|
|
|
+ },
|
|
|
|
|
+ actionButtons: {
|
|
|
|
|
+ flexDirection: 'row',
|
|
|
|
|
+ justifyContent: 'space-around',
|
|
|
|
|
+ width: '75%',
|
|
|
|
|
+ },
|
|
|
|
|
+ btnWithdraw: {
|
|
|
|
|
+ width: 90,
|
|
|
|
|
+ height: 32,
|
|
|
|
|
+ borderRadius: 16,
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ borderColor: '#fff',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ btnWithdrawText: {
|
|
|
|
|
+ color: '#fff',
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ },
|
|
|
|
|
+ btnRecharge: {
|
|
|
|
|
+ width: 90,
|
|
|
|
|
+ height: 32,
|
|
|
|
|
+ borderRadius: 16,
|
|
|
|
|
+ backgroundColor: '#fff',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ btnRechargeText: {
|
|
|
|
|
+ color: '#3d3f41',
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ },
|
|
|
|
|
+ tabsContainer: {
|
|
|
|
|
+ flexDirection: 'row',
|
|
|
|
|
+ justifyContent: 'flex-start',
|
|
|
|
|
+ paddingHorizontal: 15,
|
|
|
|
|
+ marginBottom: 10,
|
|
|
|
|
+ },
|
|
|
|
|
+ tabItem: {
|
|
|
|
|
+ width: '32%',
|
|
|
|
|
+ paddingVertical: 10,
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ tabText: {
|
|
|
|
|
+ color: '#3d3f41',
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ },
|
|
|
|
|
+ activeTabText: {
|
|
|
|
|
+ fontWeight: 'bold',
|
|
|
|
|
+ fontSize: 15,
|
|
|
|
|
+ // Add underline or color change if needed, logic in Uniapp was mostly bold
|
|
|
|
|
+ },
|
|
|
|
|
+ listContainer: {
|
|
|
|
|
+ flex: 1,
|
|
|
|
|
+ marginHorizontal: 14,
|
|
|
|
|
+ marginBottom: 14,
|
|
|
|
|
+ borderRadius: 10,
|
|
|
|
|
+ backgroundColor: '#fff',
|
|
|
|
|
+ overflow: 'hidden',
|
|
|
|
|
+ },
|
|
|
|
|
+ listContent: {
|
|
|
|
|
+ paddingBottom: 20,
|
|
|
|
|
+ },
|
|
|
|
|
+ cell: {
|
|
|
|
|
+ flexDirection: 'row',
|
|
|
|
|
+ justifyContent: 'space-between',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ paddingVertical: 18,
|
|
|
|
|
+ paddingHorizontal: 15,
|
|
|
|
|
+ borderBottomWidth: 1,
|
|
|
|
|
+ borderBottomColor: '#eee',
|
|
|
|
|
+ },
|
|
|
|
|
+ itemDesc: {
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ fontWeight: 'bold',
|
|
|
|
|
+ color: '#666666',
|
|
|
|
|
+ },
|
|
|
|
|
+ itemTime: {
|
|
|
|
|
+ fontSize: 10,
|
|
|
|
|
+ color: '#999',
|
|
|
|
|
+ marginTop: 4,
|
|
|
|
|
+ },
|
|
|
|
|
+ itemAmount: {
|
|
|
|
|
+ fontSize: 16,
|
|
|
|
|
+ },
|
|
|
|
|
+ amountIn: {
|
|
|
|
|
+ color: '#ff9600', // Uniapp color-theme
|
|
|
|
|
+ },
|
|
|
|
|
+ amountOut: {
|
|
|
|
|
+ color: '#666666', // Uniapp color-1
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyContainer: {
|
|
|
|
|
+ padding: 60,
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyText: {
|
|
|
|
|
+ color: '#999',
|
|
|
|
|
+ },
|
|
|
|
|
+});
|