RN下拉刷新(一):使用JavaScript实现

本文介绍了在React Native中自定义实现下拉刷新的过程,针对iOS平台遇到的困难和解决方案进行了详细阐述。通过UI布局、获取偏移量、手势处理等步骤实现下拉刷新功能,同时记录了在SectionList中获取滚动偏移量的正确方法和PanResponder的使用。文章末尾提到了一个未解决的问题,即在列表先上滑再下滑时无法触发下拉刷新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近一直在做React-Native相关的事情,需要实现一个下拉刷新,Android集成原生很容易,但iOS似乎比较麻烦,于是搅尽脑汁之后,最后根据Github上的下拉刷新库,但这个库并没有实现SectionList的下拉刷新,于是根据该库的代码自己的下拉刷新,为防止以后有需要时忘记或有迷惑,故记录在此。

效果展示

实现步骤

UI布局

首先,是整个UI的布局,也许SectionList有方法只是我不知道,我们知道,在原生开发中,iOS的UITableView可以使用setContentOffset让列表停止在任意的地方,原生下拉刷新的实现也正是依赖于这个属性,通过监听contentOffset,改变对应的状态。但是我在RN里面找不到这样的方法,每次下拉之后只要一放手立马回弹。所以不能直接将下拉刷新控件添加到列表上。之前我也有过这种布局思路,但是不知道怎么拿到列表的偏移量,在发现了这个库以后,我知道了可以用SectionList里面的方法来获取到滚动偏移,其实还有另外一种方法,就是使用onScroll方法。

所以布局如下所示:

render() {

        let refreshHeader = <View/>;
        if (this.props.RefreshControl !== undefined) {
            refreshHeader = this.props.RefreshControl;
        } else if (this.props.onRefresh !== undefined) {
            refreshHeader =
                <MessageRefreshHeader ref={(ref) => this._refreshHeader = ref}
                                      onRefresh={this.props.onRefresh} style={{transform: [{translateY: this.state.headerOffset}]}}/>;
        }

        let refreshFooter = <View/>;
        if (this.props.LoadMoreControl !== undefined) {
            refreshFooter = this.props.LoadMoreControl;
        } else if (this.props.onLoadMore !== undefined) {
            refreshFooter =
                <MessageRefreshFooter ref={(ref) => this._refreshFooter = ref} onLoadMore={this.props.onLoadMore}/>;
        }

        return (
            <View style={{flex: 1, flexGrow: 1}} {...this._panResponder.panHandlers}>
                <View pointerEvents='box-none' style={{flex: 1}} onLayout={(e) => {
                    if(e.nativeEvent.layout.width !== this.state.width || e.nativeEvent.layout.height !== this.state.height) {
                        this.setState({
                            width: e.nativeEvent.layout.width,
                            height: e.nativeEvent.layout.height,
                        });
                    }
                }}>
                    <Animated.View
                        style={[{...style.container},
                            {width: '100%', height: this.state.height + MessageConstant.refreshHeaderHeight, transform:[{translateY: this.state.translateY}]}]}>
                        {refreshHeader}
                        <Animated.View ref={(container) => this._scrollContainer = container} style={{flex: 1, transform: [{translateY: this.state.sectionOffset}]}}>
                            <SectionList
                                ref={sectionList => this._sectionList = sectionList}
                                initialNumToRender={3}
                                keyExtractor={(item, index) => item + index}
                                sections={this.props.sections}
                                renderItem={this.props.renderItem}
                                renderSectionHeader={this.props.renderSectionHeader}
                                stickySectionHeadersEnabled={this.props.stickySectionHeadersEnabled}
                                ItemSeparatorComponent={this.props.ItemSeparatorComponent}
                                scrollEnabled={true}
                                bounces={true}
                                onScroll={(e) => {
                                    this._onScroll(e);
                                    if (this.props.onScroll !== undefined) {
                                        this.props.onScroll(e);
                                    }
                                }}
                                onEndReachedThreshold={this.props.onEndReachedThreshold === undefined ? 1.0 : this.props.onEndReachedThreshold}
                                onEndReached={(e) => {
                                    this._onEndReached(e);

                                    if (this.props.onEndReached !== undefined) {
                                        this.props.onEndReached(e);
                                    }
                                }}
                                showsHorizontalScrollIndicator={this.props.showsHorizontalScrollIndicator}
                                showsVerticalScrollIndicator={this.props.showsVerticalScrollIndicator}
                                SectionSeparatorComponent={this.props.SectionSeparatorComponent}
                                ListFooterComponent={
                                    <View style={{flex: 1, flexDirection: 'column'}}>
                                        {this.props.ListFooterComponent}
                                        {refreshFooter}
                                    </View>
                                }
                                ListHeaderComponent={this.props.ListHeaderComponent}
                                ListEmptyComponent={this.props.ListEmptyComponent}
                            />
                        </Animated.View>
                    </Animated.View>
                </View>
            </View>
        );
    }

可以看到,以上有四层布局,

  • 最外层的View用于添加滑动手势,
  • 第二层View的作用并不大,主要是通过onLayout方法获取到宽高,
  • 第三层的Animated.View主要是用于移动,因为最外的两层是不移动的,只处理相关手势,真正移动的是第三层,
  • 第四层Animated.View用于调整SectionList的偏移量,可以看到,第三层的flex并不是1,因为在移动的时候,如果高度只占据剩余高度的话

根据上述代码可以看到,第三层的flex并不是1,因为如果flex设置为1的话,那么当整体往上移动的时候,下面将留出空白,所以高度设置为剩余高度+头部高度。这样就可以防止这个问题。

获取偏移量

这个下拉刷新的实现原理是根据列表滚动的偏移量来决定是否启用滑动手势,当列表滚动到<= 0的位置的时候,便启用滑动手势,此时列表不可滚动。那么如何获取偏移量呢,根据github上那个库的代码,他使用了

	this._flatList._listRef._getScrollMetrics().offset 

来获取,确实在FlatList下可以获取到,但是SectionList的_listRef是undefined,拿不到这个值,最终一步一步追踪源代码才发现,SectionList正确的获取方法是

	this._sectionList._wrapperListRef.getListRef()._getScrollMetrics().offset

中间多了一层_wrapperListRef。

手势处理

手势处理使用PanResponder

启用手势

手势什么时候该被启用?显而易见,当我们往下拉的时候,并且列表已经滚动到顶部的时候并且不处在刷新状态的时候,才能启用手势,否则不应启用,代码如下:

_isDownGesture(dx, dy) {
        return (dy > 0 && dy > Math.abs(dx));
    }

_onStartShouldSetPanResponder(evt, gestureState) {
        if(gestureState.dy <= 0 || this._refreshHeader.state.isRefreshing) {
            return;
        }

        this.lastY = this._sectionList._wrapperListRef.getListRef()._getScrollMetrics().offset;
        if(this._yContentOffset <= 0 && this._isDownGesture(gestureState.dx, gestureState.dy)) {
            return true;
        }
        return false;
    }

移动

手势启用之后,当手指滑动的时候,此时需要修改偏移量,使整个View能向下偏移,这样才能使下拉刷新显示出来,如下所示:

_onPanResponderMove(evt, gestureState) {
        if(gestureState.dy <= 0 || this._refreshHeader.state.isRefreshing) {
            return;
        }

        Animated.timing(
            this.state.translateY,
            {
                toValue: -MessageConstant.refreshHeaderHeight + gestureState.dy,
                duration: 10.0,
            }
        ).start();

        if(this._refreshHeader.onListScroll !== undefined) {
            this._refreshHeader.onListScroll(gestureState.dy);
        }

释放

当手指释放的时候,需要根据当前的偏移距离来进行判断,如果滑动超过了指定高度,则进入到刷新状态,否则回到原状态

_onPanResponderRelease(evt, gestureState) {
        if(this._refreshHeader && this._refreshHeader.state.isRefreshing) {
            return ;
        }

        if(this._refreshHeader && this._refreshHeader.changeRefreshState !== undefined) {
            let refresh = this._refreshHeader.changeRefreshState(gestureState.dy);
            if(refresh === false) {
                this._endHeaderRefreshAnimated();
            }else {
                Animated.timing(
                    this.state.translateY,
                    {
                        toValue: 0.0,
                        duration: 200.0,
                    }
                ).start();
            }
        }else {
            this._endHeaderRefreshAnimated();
        }
    }

    _endHeaderRefreshAnimated() {
        Animated.timing(
            this.state.translateY,
            {
                toValue: -MessageConstant.refreshHeaderHeight,
                duration: 500.0,
            }
        ).start();
        Animated.timing(
            this.state.headerOffset,
            {
                toValue: 0.0,
                duration: 500.0,
            }
        ).start();
        Animated.timing(
            this.state.sectionOffset,
            {
                toValue: 0.0,
                duration: 500.0,
            }
        ).start();
    }

启动和停止

有时候不需要通过手动下拉刷新的方式自动触发下拉刷新,所以需要添加下拉刷新的方法,如下


beginRefresh() {
        if(this._refreshHeader && this._refreshHeader.state.refreshState !== this._refreshHeader.RefreshState.REFRESHING) {
            Animated.timing(
                this.state.translateY,
                {
                    toValue: 0.0,
                    duration: 200.0,
                }
            ).start();

            if(this._refreshHeader.changeRefreshState) {
                this._refreshHeader.changeRefreshState(MessageConstant.refreshHeaderHeight);
            }
        }
    }

endRefresh() {
        if(this._refreshHeader !== undefined && this._refreshHeader.state.refreshState === this._refreshHeader.RefreshState.REFRESHING) {
            setTimeout(() => {
                this._endHeaderRefreshAnimated();
                this._refreshHeader.endRefresh();
            }, 3000);
        }
        if(this._refreshFooter !== undefined && this._refreshFooter.state.loadMoreState === this._refreshFooter.LoadMoreState.REFRESHING) {
            return this._refreshFooter.endRefresh();
        }
    }

下拉刷新

我们的下拉刷新是需要不断的更换图片的,所以一开始先确定好图片在每个位置的图片索引,

constructor(props) {
        super(props);

        this._imageIndex = 0;
        this._pullImage = {index: 1, count: 7};
        this._refreshImage = {index: 8, count: 28};
    }

这个下拉刷新就是一个图片,所以布局也是十分简单

render() {
        return (
            <Animated.View
                ref = {ref => this._pullRef = ref}
                style={[{...style.container}, {...this.props.style}]} onLayout={(e) => {
            }}>
                <Animated.Image ref={image => this._imageRef = image} source={blackRefreshGif[this._imageIndex % blackRefreshGif.length]} resizeMode={'center'} style={style.refreshImage}/>
            </Animated.View>
        );
    }

const style = StyleSheet.create({
    container: {
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        zIndex: -9999,
        backgroundColor: 'transparent',
    },
    refreshImage: {
        width: 108.0,
        height: 108.0,
    },
});

重点在下拉时候的图片状态的修改,代码如下:

validImageIndex(index) {
        let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;

        if(index < 0) {
            return 0;
        }else if(index >= imgArr.length) {
            return imgArr.length - 1;
        }
        return index;
    }

onListScroll(yOffset) {
        this._clearInterval();

        let refreshThreshold = this.props.refreshThresold === undefined ? 64.0 : this.props.refreshThresold;

        let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;
        if(yOffset < refreshThreshold) {
            this._imageRef.setNativeProps({
                ['source']: [resolveAssetSource(imgArr[0])],
            });
            this._imageIndex = this.validImageIndex(this._pullImage.index);

            if(this.state.refreshState !== this.RefreshState.IDLE) {
                this.setState({
                    isRefreshing: false,
                    refreshState: this.RefreshState.IDLE,
                });
            }
            return ;
        }else {
            if(this.state.refreshState !== this.RefreshState.PULLING) {
                this.setState({
                    isRefreshing: false,
                    refreshState: this.RefreshState.PULLING,
                });
            }

            if(yOffset > refreshThreshold && yOffset < MessageConstant.refreshHeaderHeight && this._lastYOffset > yOffset) {
                this._imageRef.setNativeProps({
                    ['source']: [resolveAssetSource(imgArr[this.validImageIndex(this._imageIndex--)])],
                });
            }else {
                if (this._imageIndex >= (this._pullImage.index + this._pullImage.count)) {
                    this._imageIndex = this.validImageIndex(this._pullImage.index + this._pullImage.count);
                }
                this._imageRef.setNativeProps({
                    ['source']: [resolveAssetSource(imgArr[this.validImageIndex(this._imageIndex++)])],
                });
            }
        }
        this._lastYOffset = yOffset;
    }

Image如果要频繁的修改图片,必须使用setNativeProps来修改source,传递一个图片数组,在Android下该字段为src,iOS下该字段为source, 要使用resolveAssetSource,必须在头部引入

let resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource');

代码说明如下:

  1. 首先确定拉动阈值,如果外部没有定义,则定义为64,这个值表示,当列表偏移小于该值时,图片不做更改,始终保持第一张图片,且状态始终保持在IDLE状态
  2. 当超过阈值的时候,修改下拉刷新组件的状态为PULLING状态,表示正在下拉,如果是往上拉状态并且偏移小于阈值的时候,则从当前图片开始一步一步减少图片,直到第一个状态
  3. 否则如果继续往下拉,则更新图片直到下拉状态的最后一张图片便保持不变

当手指释放的时候,此时我们需要判断下拉刷新的状态,根据偏移量,如果小于阈值的时候,此时下拉刷新回到原位置。否则修改下拉刷新状态为刷新中,并且启动定时器,不断的更新图片,如下所示:

_addInterval(handler, timeout) {
        this._clearInterval();
        this._refreshTimer = setInterval(handler, timeout);
    }

_clearInterval() {
    this._refreshTimer && clearInterval(this._refreshTimer);
}
    
changeRefreshState(yOffset) {

        let refreshThreshold = MessageConstant.refreshHeaderHeight;
        let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;

        if(yOffset < refreshThreshold) {
            return false;
        }

        if(this.state.refreshState !== this.RefreshState.REFRESHING) {
            this.setState({
                isRefreshing: true,
                refreshState: this.RefreshState.REFRESHING,
            });
        }

        this._imageIndex = this._refreshImage.index;
        this._addInterval(() => {
            if(this._imageIndex >= (this._refreshImage.index + this._refreshImage.count)) {
                this._imageIndex = this._refreshImage.index + 18.0;
            }
            this._imageRef.setNativeProps({
                ['source']: [resolveAssetSource(imgArr[this._imageIndex++])],
            });
        }, 40);

        if(this.props.onRefresh) {
            this.props.onRefresh();
        }

        return true;
    }

下拉刷新完成的时候,此时修改下拉刷新状态为IDLE,延迟一定时间后,恢复组件位置,同时从当前图片索引不断恢复图片至最开始的状态即可。

endRefresh() {
		if(this.state.refreshState === this.RefreshState.REFRESHING) {
            this.setState({
                isRefreshing: false,
                refreshState: this.RefreshState.IDLE,
            });
        }

        let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;
        this._addInterval(() => {
            if(this._imageIndex < 0) {
                this._imageIndex = 0;
                this._clearInterval();
            }

            this._imageRef.setNativeProps({
                ['source']: [resolveAssetSource(imgArr[this._imageIndex--])],
            });
        }, 40.0);
    }

问题记录

到这里,整个下拉刷新组件的封装就已经全部完成,但是依然有一个遗留的问题,就是当列表先上滑再往下滑的时候,无法出现下拉刷新,这是因为在列表滚动的时候无法启动手势,目前尚无确切的解决办法。看了一下第三方库,也没有办法解决这个问题,因此该问题目前只能暂时留下,待以后有解决方案的时候再进行更新记录。

<think>我们要求实现的效果是:在iOS实现Android风格的下拉刷新,即用户下拉到定位置后释放才触发刷新回调(而不是像iOS原生那样下拉到定位置自动触发)。同时,我们希望这个效果在iOS和Android上保持致。 根据之前的讨论,我们可以选择以下方案: 1. 使用 `react-native-smartrefreshlayout` 库(推荐),因为它支持跨平台且API统,并且可以自定义刷新效果。 2. 使用 `react-native-pull` 库,这是个纯JavaScript实现的方案。 但是,注意用户要求:释放手势后才触发回调。这正是Android原生下拉刷新的行为(即“Pull to refresh”),而iOS原生是“拉到定位置自动触发”(即“Release to refresh”)。因此,我们需要确保在iOS上也是释放后才触发。 下面分别给出两种实现方式: ### 方案使用 `react-native-smartrefreshlayout` 这个库在Android上使用原生的SmartRefreshLayout,在iOS使用原生的UIScrollView封装,因此性能较好,并且支持自定义头部。 安装: ```bash npm install react-native-smartrefreshlayout --save # 或者 yarn add react-native-smartrefreshlayout ``` 然后链接(对于RN<0.60,需要手动链接,RN>=0.60自动链接): ```bash react-native link react-native-smartrefreshlayout ``` 使用示例: ```jsx import React, { useRef } from 'react'; import { ScrollView, Text } from 'react-native'; import { SmartRefreshControl, DefaultHeader } from 'react-native-smartrefreshlayout'; const App = () => { const refreshControl = useRef(); const onRefresh = () => { // 模拟异步请求 setTimeout(() => { // 结束刷新 refreshControl.current && refreshControl.current.finishRefresh(); }, 2000); }; return ( <ScrollView style={{ flex: 1 }} refreshControl={ <SmartRefreshControl ref={refreshControl} onRefresh={onRefresh} headerComponent={<DefaultHeader />} // 使用默认头部,在iOS上也是释放后刷新 enableHeaderTranslation={true} // 确保头部可以随着下拉移动 /> } > <Text>下拉刷新示例</Text> {/* 更多内容 */} </ScrollView> ); }; export default App; ``` 注意:`DefaultHeader` 在iOS和Android上都会显示为释放后刷新的效果(即Android风格)。你也可以自定义头部组件。 ### 方案二:使用 `react-native-pull` 这是个纯JavaScript实现下拉刷新组件,因此不依赖原生,但性能可能不如原生方案。 安装: ```bash npm install react-native-pull --save # 或者 yarn add react-native-pull ``` 使用示例: ```jsx import React, { useState } from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; import PullView from 'react-native-pull'; const App = () => { const [refreshing, setRefreshing] = useState(false); const onPullRelease = (resolve) => { setRefreshing(true); // 模拟异步请求 setTimeout(() => { setRefreshing(false); resolve(); // 必须调用resolve来结束刷新状态 }, 2000); }; return ( <PullView style={{ flex: 1 }} onPullRelease={onPullRelease} header={ <View style={{ height: 60, alignItems: 'center', justifyContent: 'center' }}> {refreshing ? ( <ActivityIndicator size="small" color="#0000ff" /> ) : ( <Text>↓ 下拉刷新</Text> )} </View> } > <Text>下拉刷新示例</Text> {/* 更多内容 */} </PullView> ); }; export default App; ``` 在 `react-native-pull` 中,`onPullRelease` 就是在释放手势后触发的回调。因此,它符合我们的要求。 ### 方案三:使用原生`RefreshControl`并修改行为(不推荐,因为iOS原生不支持释放后刷新) 在iOS上,原生的`RefreshControl`是当用户下拉到定位置时自动触发刷新(即使没有释放)。因此,如果我们想要在释放后才触发,我们需要自己监听手势事件并控制刷新状态。这通常比较复杂,而且容易出错。 因此,我们更推荐使用上述两个第三方库来实现。 ### 总结 1. 如果你追求更好的性能和原生体验,推荐使用`react-native-smartrefreshlayout`。 2. 如果你希望纯JavaScript实现,并且项目比较简单,可以使用`react-native-pull`。 注意:在iOS使用`react-native-smartrefreshlayout`时,需要确保正确链接了原生模块。如果遇到问题,可以尝试手动链接(对于RN<0.60)或使用`pod install`(对于iOSRN>=0.60)。 以上两种方案都能在iOS实现Android风格的下拉刷新(即释放手势后触发回调)。 [^1]: 同时支持android和ios,并且拥有相同的Api,可以自定义下拉刷新样式;它里面已经实现了View,Scrollview,Listview和Flatlist的下拉刷新,可以支持绝大多数的React Native中的组件实现下拉刷新功能。</think>在 React Native实现 iOS 平台与 Android 风格致的下拉刷新效果(释放手势后才触发回调),可以通过以下方案实现: ### 最佳方案:使用 react-native-smartrefreshlayout 这个库完美支持跨平台统效果,特别满足「释放后才触发回调」的需求: ```jsx import { SmartRefreshControl, DefaultHeader } from 'react-native-smartrefreshlayout'; function App() { const refreshControl = useRef(); const onRefresh = () => { // 释放手势后才执行的回调 fetchData().then(() => { refreshControl.current?.finishRefresh(); // 刷新完成后调用 }); }; return ( <ScrollView refreshControl={ <SmartRefreshControl ref={refreshControl} onRefresh={onRefresh} headerComponent={<DefaultHeader />} enableOverScrollBounce={false} // 禁用iOS回弹效果 enableOverScrollDrag={true} // 启用Android风格拖拽 /> } > {/* 内容 */} </ScrollView> ); } ``` **关键特性**: 1. **释放手势触发**:完全符合 Android 风格,下拉过程中不触发,释放后才执行回调[^1] 2. **统视觉**:`DefaultHeader` 组件在 iOS 和 Android 上显示相同的指示器 3. **精细控制**:通过 `enableOverScrollDrag` 强制启用 Android 风格拖拽行为 4. **原生性能**:底层使用 iOS 的 `UIScrollView` 封装,确保流畅性 ### 备选方案:自定义手势检测 如果坚持使用原生组件,可通过手势监听模拟 Android 行为: ```jsx import { RefreshControl, ScrollView } from 'react-native'; function AndroidStyleRefresh() { const [refreshing, setRefreshing] = useState(false); const [releaseTriggered, setReleaseTriggered] = useState(false); const onScroll = ({ nativeEvent }) => { if (nativeEvent.contentOffset.y < -100 && !releaseTriggered) { setReleaseTriggered(true); } }; const onRelease = () => { if (releaseTriggered) { setRefreshing(true); // 执行刷新操作 setTimeout(() => { setRefreshing(false); setReleaseTriggered(false); }, 2000); } }; return ( <ScrollView onScroll={onScroll} onTouchEnd={onRelease} refreshControl={ <RefreshControl refreshing={refreshing} progressViewOffset={50} tintColor="#007AFF" /> } > {/* 内容 */} </ScrollView> ); } ``` **实现原理**: 1. 监听 `onScroll` 检测下拉距离(这里设为 -100) 2. 通过 `onTouchEnd` 捕获释放手势 3. 当释放时且达到阈值才设置 `refreshing=true` 4. 通过 `progressViewOffset` 调整指示器位置与 Android 致 ### 安装说明 ```bash # 推荐方案安装 npm install react-native-smartrefreshlayout npx pod-install # iOS 需要安装原生依赖 # 或使用纯JS方案 npm install react-native-pull ``` ### 平台差异处理要点 | 特性 | Android 原生 | iOS 需要调整 | |---------------------|----------------------|--------------------------| | 触发时机 | 释放后触发 | 需禁用自动触发 | | 拖拽阻力 | 较大 | 需设置 `enableOverScrollDrag` | | 视觉反馈 | 圆形进度条 | 使用 `DefaultHeader` | | 回弹效果 | 无回弹 | 需禁用 `enableOverScrollBounce` | 选择 `react-native-smartrefreshlayout` 是最可靠方案,它解决了: 1. iOS 自动触发问题(通过释放检测) 2. 视觉差异问题(统头部组件) 3. 手势反馈差异(调整拖拽参数) 4. 性能问题(原生组件实现)[^1][^2] --- 相关问题: 1. 如何在使用 `SmartRefreshControl` 时自定义 Lottie 动画作为刷新指示器?[^1] 2. `react-native-pull` 如何实现三种状态(下拉/可释放/刷新中)的样式切换?[^4] 3. 在 iOS 上禁用原生回弹效果对用户体验有何影响?[^2] 4. 如何统 iOS 和 Android 的下拉刷新位置偏移量?[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值