2.0.4-rc • Published 2 years ago

raodaor-app1 v2.0.4-rc

Weekly downloads
-
License
-
Repository
-
Last release
2 years ago

raodaor-app

Third App Store

Document

https://reactnative.dev/

Getting Started

Upgrading

  • recommend npx expo install [package name] have dependencie with auto version check.
  • yarn add expo@46.0.0
    • expo-cli 6.0.2
      • build:ios path Superseded by eas build in eas-cli
      • build:android path Superseded by eas build in eas-cli
      • build:status path Superseded by eas build:list in eas-cli
      • eject path Superseded by expo prebuild
      • upload:android path Superseded by eas submit in eas-cli
      • upload:ios path Superseded by eas submit in eas-cli
      • client:ios path Superseded by Expo Dev Clients
  • yarn add react-native@0.69.4
    • http://file.camnpr.com/expo/react-native-sdk-46.0.0.tar.gz
    • yarn add http://file.camnpr.com/expo/react-native-sdk-45.0.0.tar.gz
    • yarn add https://github.com/expo/react-native/archive/sdk-45.0.0.tar.gz
  • yarn add react-native-web@0.18.7
  • use yarn command

    expo sdk versions are locked to a specific react-native version. you can't update the react-native version until we release a new sdk version that supports it. if you'd like to use hooks right now, you will need to run npm i -g react-native-cli and then react-native init.

bug review

npm run start

yarn add lark-cms@0.3.5-rn --registry http://npm.camnpr.com:4873/

Runs the app in development mode.

Open http://localhost:19002/ to view it in the browser.

The page will reload if you make edits.

Press w to browser vist.

npm run app

Builds the app for production to the build folder.

browser Web

npm start and Press w

APPEntry > package.json > main > index.js(customize) OR node_modules/expo/AppEntry.js(default)

npm run web:build

make web-build folder. by use view demo.

eg: config website to web-build folder before view http://localhost:2021/index.html

expo customize:web change web index.html of template.

data source

SaasPlatform background make public/data.js file.

keystore

  • super-app-package-raodaor.keystore password ? skyboxgogo123
  • Keystore alias > superAppRaodaor

usage: new keystore

Upgrading Expo SDK

Expo SDKreactreact-domreact native
4618.0.018.0.0react-native-sdk-46.0.0.tar.gz ~0.69.1 use react-native@0.69.1
4516.14.016.14.0react-native-sdk-45.0.0.tar.gz ~0.68.1
4416.14.016.14.0react-native-sdk-44.0.0.tar.gz ~0.64.3
4316.13.116.13.1react-native-sdk-43.0.0.tar.gz ~0.63.2

🚨 With Expo SDK 46, we are migrating to a new suite of tooling that is versioned with the expo package. Use npx create-expo-app to initialize a new Expo project instead of expo init.

schema

  • raodaor://

首页       raodaor://homeDefault?key=3fo8nH32gJhRHt4EhNVH2G
知识乐园   raodaor://homeDefault?key=21QWxtWtq1kQHKzAgKnnuo
有话说     raodaor://homeDefault?key=iLebArvMiSLWrtfDQMFt9e
广告点点   raodaor://homeDefault?key=rRQhAHpv89Bhs3sYUNkGUp
私活有道   raodaor://homeDefault?key=pbKaFLs6DAwjEMHCBQQohi

定位地址   raodaor://addressDefault?key=xxx
拍照/录像  raodaor://cameraDefault?key=xxx
扫一扫     raodaor://cameraScanner?key=xxx

分类列表   raodaor://categoryDefault?key=xxx
购物车     raodaor://cartDefault?key=xxx

消息中心   raodaor://imList?key=xxx
聊天会话   raodaor://imChat?                    ###{title: 标题或者聊天人昵称, groupId: 群组ID(包括两人群)}

文章列表   raodaor://articlesList?key=xxx       ###{itemId: classId, title: className}
文章详情   raodaor://detailsArticle?key=xxx     ###{itemId: 文章ID,classId: 分类ID,}

产品列表   raodaor://productList?key=xxx        ###{itemId: classId, title: className}
产品详情   raodaor://detailsProduct?key=xxx     ###{itemId: 产品ID,classId: 分类ID,}

发布文章   raodaor://publishArticle?key=xxx
发布产品   raodaor://publishProduct?key=xxx

浏览器     raodaor://webViewDefault?key=xxx

我的       raodaor://myDefault?key=xxx
设置       raodaor://mySetting?key=xxx
登录       raodaor://myLogin?key=xxx
用户中心   raodaor://myInfos?key=xxx
注册       raodaor://myRegister?key=xxx

纯净CMS页  raodaor://toolPureCmsView?key=xxx
热榜       raodaor://toolHotList?key=xxx
口算题     raodaor://toolVerbalexercise?key=xxx
404        raodaor://toolStatusCode404?key=xxx

全部订单    raodaor://listOrder?tab=4&key=xxx // 待付款、待收货、退换/售后、全部订单

附近好店    raodaor://activityNearStore?key=xxx
PLUS会员    raodaor://activityPlusVip?key=xxx
签到有礼    raodaor://activitySign?key=xxx
领券中心    raodaor://activityCouponCenter?key=xxx

table of contents

  • src/pages
    • home 首页
      • default.tsx 默认页
      • speed.tsx 极速版
    • address 地址
      • default.tsx 定位地址
    • camera 照相机
      • default.tsx 拍照/录像
      • scanner.tsx 扫一扫
    • im 消息
      • list.tsx 消息列表页
      • chat.tsx 消息会话页
    • my 我的
      • default.tsx 默认页
      • login.tsx 登录页
      • register.tsx 注册页
      • infos.tsx 用户中心页
      • setting.tsx 设置页
    • cart 购物车
      • default.tsx 默认页
    • category 分类
      • default.tsx 默认页
    • list 列表页
      • article.tsx 文章列表页
      • product.tsx 产品列表页
    • details 详情页
      • article.tsx 文章详情页
      • product.tsx 产品详情页
    • publish 发布信息
      • article.tsx 发布文章
      • product.tsx 发布产品
    • webview APP内浏览器
      • default.tsx 默认页

document

NativeAPI.md

notice

  • FlatListSectionListVirtualizedListFlashList 当用List相关的组件时,为了性能,列表中当前不可见的元素,其实是获取不到它们的位置信息的(未渲染)
    // simulate code
    <FlatList>
      // Single Item Save Position
      <View onLayout={(event) => {
          var {x, y, width, height} = event.nativeEvent.layout;
          refLarkCmsObj.scrollTo({ x: x, y: y, animated: false });
          // Error: y equal 0
          console.log("View onLayout===>", event)
        }}></View>
    </FlatList>
    // other remark
    <VirtualizedList 
      ref={setRefCityList}
      initialNumToRender={1}
      onScrollToIndexFailed={null}
      initialScrollIndex={9}
      getItemLayout={(data, index) => {}}
      keyExtractor={item => item}
      renderItem={({item}) => renderItem(item)}
      getItemCount={data => dataList.length}
      getItem={renderGetItem}
      data={dataList}
      // onEndReached={loadMoreItems}
      // onEndReachedThreshold={0.5} // 距离底部
    />

app.json

  • name: 作为APP的名字,它将出现在 Expo Go 和 你手机屏幕上的 独立APP 的名字。
  • slug: 用于发布时友好的APP名字。比如:配置:myAppName 将会添加到线上平台:expo.io/@project-owner/myAppName 项目上。
  • (enum) - Defaults to unlisted. unlisted hides the project from search results. hidden restricts access to the project page to only the owner and other users that have been granted access. Valid values: public, unlisted, hidden.
  • splash image size: 1242 x 2436 https://docs.expo.dev/tutorial/configuration/

Component Intro

  • StatusBar: 定义状态栏,比如:顶部的带信号的状态栏。 可以通过: hidden={true} 来隐藏顶部状态栏。 backgroundColor="#f70f0f" 设置背景色。
  • Expo 不能使用文件上传组件:@types/rn-fetch-blob Does rn-fetch-blob work with Expo?
  • react-navigation 给每个页面 (Screen) 传入了两个对象,可以通过 this.props.routethis.props.navigation 调用。
    • route 主要是一些关于路由的信息
    • navigation 主要提供了一些页面跳转的方法 navigation-prop
  • expo publish 发布后,APP会自动更新(也可自己搭建更新服务器)
    • [Error: You cannot check for updates in development mode. To test manual updates, publish your project using `expo publish` and open the published version in this development client.]
  • TabView 组件 增加:orientation = "horizontal" or "vertical" 方向配置。
  • TODO:App的状态,前台运行 还是 后台运行 (备份代码)
  import { 
    AppState,
  } from "react-native";
  // APP的准备状态
  const [appIsReady, setAppIsReady] = useState(false);

  // App的状态,前台运行 还是 后台运行
  const appState = useRef(AppState.currentState);
  const [appStateVisible, setAppStateVisible] = useState(appState.current);

  /* App状态监测(可以告诉你应用程序是在前台还是后台,并在状态改变时通知你)*/
  useEffect(() => {

    // 【相当于】 componentDidMount() 组件第一次渲染完成, 【也相当于】componentDidUpdate()
    AppState.addEventListener("change", _handleAppStateChange);

    // 【相当于】componentWillUnmount() 在此处完成组件的卸载和数据的销毁
    return () => {
      AppState.removeEventListener("change", _handleAppStateChange);
    }
  }, [])// 传入第二个参数,表示,只在componentDidMount生命周期里执行,否则,setState也会触发 componentDidUpdate 造成死循环执行。

  /* App 状态发生变化 */
  const _handleAppStateChange = (nextAppState: any) => {
    if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
      console.log('APP当前状态====> App has come to the foreground!');
    }

    appState.current = nextAppState;
    setAppStateVisible(appState.current);
    console.log('APP当前状态====> AppState', appState.current);
  }
/*
 * @Author: yinhongwei 
 * @Date: 2021-01-25 15:11:29 
 * @Last Modified by: yinhongwei
 * @Last Modified time: 2022-08-21 16:34:09
 * @Description: 文章详情页面 【参数:{title: "标题", itemId: 文章ID, }】
 */
import React, { useRef } from "react";
// import Constants from "expo-constants";
import { ActionSheetProvider, connectActionSheet, /* ActionSheetProps, */ } from "@expo/react-native-action-sheet";
import Toast from "react-native-toast-message";
import { StatusBar } from "expo-status-bar";
import { Audio } from "expo-av";
// import RNFetchBlob from "rn-fetch-blob";
import { 
  SafeAreaView, 
  View,
  ScrollView, 
  RefreshControl,
  FlatList,
  StyleSheet, 
  TouchableOpacity,
  Pressable,
  Dimensions,
  Animated,
  PanResponder,
  // Modal,
  Text,
  ImageBackground,
  Image,
} from "react-native";
// 接口相关的
import * as PublicConstant from "../../utils/constant";
import { formatDataShort, extractResourceInfo, millisToMinutesAndSeconds } from "../../utils/common";
import { InterfaceRequest, FileFetchRequest } from "../../utils/api";
import ComponentsPageStatus from "../../component/pageStatus";

// 定义一个接口,目的是为后面的state提供类型,以便通过编译器的检查
interface stateDefined {
  refreshing: boolean, // 刷新状态
  pageStatus: string, // 页面状态 init(页面骨架图),loading,nodata,error
  pageStatusText: string, // 页面状态提示语
  articleData: any, // 文章详情内容

  lastAudioFile: string, // 最新录制的音频文件地址
  sound: any, // 音频播放对象
  // soundInitStatus: any, // 音频状态
  soundPositionMillis: number, // 音频时长的位置(毫秒)[收到变更为倒计时形式]
  soundItemFlag: number, // 当前播放视频的flag标记
  soundProgressMonitor: any, // 音频监听播放进度
  recording: any, // 录音对象
  recordingUri: string, // 录音后的地址文件

  commentModal: boolean, // 评论列表浮层
  CommentRefreshing: boolean, // 评论刷新状态
  commentListData: Array<any>, // 评论列表数据
  commentDataCount: number, // 总数据量
  commentPageNumber: number, // 页码数
  commentPageSize: number, // 每页个数

  pageData: Array<any>, // 页面的配置数据
};
// type Props = ActionSheetProps;

// 这里的any用来定义props的类型,stateDefined接口用来定义this.state的类型
class DetailsArticle extends React.Component<any, stateDefined> {
  // 定义拖动对象XY
  public panObject = new Animated.ValueXY(); // useRef(new Animated.ValueXY()).current;
  public panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onPanResponderMove: Animated.event([
      null,
      {
        dx: this.panObject.x,
        dy: this.panObject.y,
      }
    ]),
    onPanResponderRelease: () => {
      // Animated.spring(
      //   this.panObject,
      //   { toValue: 0 } 
      // ).start();
    }
  })
  constructor(props:any) {
    super(props)
    this.state = {
      refreshing: false,
      // 页面状态
      pageStatus: "",
      pageStatusText: "",
      articleData: {},

      lastAudioFile: "",
      sound: undefined,
      // soundInitStatus: undefined,
      soundPositionMillis: 0,
      soundItemFlag: 0,
      soundProgressMonitor: undefined,
      recording: undefined,
      recordingUri: "",

      commentModal: false,
      CommentRefreshing: false,
      commentListData: [],
      commentDataCount: 0,
      commentPageNumber: 0, // 默认从0开始
      commentPageSize: 10,

      pageData: [],
    }
  }

  /**
   * 设置导航的标题
   * @param {string} title 标题
   * @param {string} message 分享的内容
   */
  onSetPageTitle = (title: string, message: string) => {
    this.props.navigation && this.props.navigation.setOptions({ 
      title: title || "文章详情",
      headerRight: () => (
        <View style={{flexDirection: "row"}}>
          <TouchableOpacity 
            onPress={ (route)=> {PublicConstant.EnvConstant.onChat(this.props.navigation, `✅【${title}】\r\n📝${message}`)} }>
            <Image
              style={{width: 30, height: 30}}
              source={require("../../../assets/nav-chat.png")}
            />
          </TouchableOpacity>
          <TouchableOpacity 
            onPress={ (route)=> {PublicConstant.EnvConstant.onShare(title, `✅【${title}】\r\n📝${message}\r\n⬆️⬆️⬆️✏️${PublicConstant.defaultAppInfo.appName}`)} }>
            <Image
              style={{width: 30, height: 30, marginLeft: 10, marginRight: 15}}
              source={require("../../../assets/nav-share.png")}
            />
          </TouchableOpacity>
        </View>
      ),
    })
  }
  /* 初始化 */
  UNSAFE_componentWillMount() {
    this.setState({
      pageStatus: "loading",
      pageStatusText: "正在努力加载中..."
    });
    this.onFetchData(null)
  }
  /* 卸载处理 */
  componentWillUnmount() {
    const { sound, recording } = this.state;
    if (sound) {
      this.onStopSound()
    }
    if (recording) {
      this.onStopRecording()
    }
  }
  /* 获取文章内容 */
  onFetchData = (callback: any) => {
    let { params } = this.props.route || {};
    // TODO: test
    // params = params || {"title": "相思", "itemId": 2};
    // this.onSetPageTitle(params && params["title"]);
    
    let _this = this;
    if (params && params["itemId"]) {
      // 获取文章的详情
      InterfaceRequest(
        PublicConstant.EnvConstant.articles_detail, 
        {
          body: {
            id: params.itemId,
            userKey: PublicConstant.EnvConstant.appUserInfo.key,
          }
        }, 
        "POST", 
        (data: any)=>{
          if (data.code === 200) {
            _this.setState({
              articleData: data.data
            }, ()=>{
              _this.setState({
                pageStatus: "",
                pageStatusText: ""
              })
            })
            _this.onSetPageTitle(data.data.title, data.data.desc);
          } else {
            _this.setState({
              pageStatus: "error",
              pageStatusText: data.message || "服务异常,小主,请稍后~"
            })
            // Alert.alert(data.message || "服务异常,小主,请稍后~")
          }
          callback && callback();
        }, 
        (error: any)=>{
          _this.setState({
            pageStatus: "error",
            pageStatusText: "服务太忙了,小主,请稍后"
          })
          callback && callback();
        })
    } else {
      _this.setState({
        pageStatus: "error",
        pageStatusText: "没有指定内容,无法查询呢,请返回重试!"
      })
    }
  }
  /* 处理刷新 */
  onRefresh = () => {
    let _this = this;
    this.setState({
      refreshing: true,
      pageStatus: "",
      pageStatusText: ""
    })
    this.onFetchData(()=>{
      _this.setState({
        refreshing: false
      })
    })
  };
  /* 开始录音 */
  onStartRecording = async () => {
    // 登录检测
    let isLogin = await PublicConstant.EnvConstant.checkLoginFunc(this.props.navigation);

    if (isLogin) {
      try {
        // console.log("请求录音权限..");
        await Audio.requestPermissionsAsync();
        await Audio.setAudioModeAsync({
          allowsRecordingIOS: true,
          playsInSilentModeIOS: true,
        }); 
        // console.log("开始录音..");
        const { recording } = await Audio.Recording.createAsync(
          Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY
        );
        this.setState({
          recording: recording
        })
        // console.log("录音已经开始");
        Toast.show({ type: "info", text1: "录音已经开始,请说话哟!", props: {width:300} });
      } catch (err) {
        // console.error("开始录音时失败", err);
        Toast.show({ type: "error", text1: "录音发生异常!" });
      }
    }
  };
  /* 停止录音 */
  onStopRecording = async () => {
    // console.log("停止录音..");
    await this.state.recording.stopAndUnloadAsync();
    const uri = this.state.recording.getURI(); 
    this.setState({
      recording: undefined,
      recordingUri: uri
    })

    Toast.show({ type: "info", text1: "录音已经停止,正在上传您的录音哟~", props: {width: 300} });

    // 上传录音文件
    let fileExtension = uri.substring(uri.lastIndexOf(".") + 1);
    let _this = this;
    
    FileFetchRequest(PublicConstant.EnvConstant.file_upload + "?type=audio", {
      body: {
        type: `audio/${fileExtension}`,
        name: `article-audio-record.${fileExtension}`,
        uri: uri,
      }
    }, (data: any)=>{
      // console.log("=====>", data);
      if (data.href) {
        let { params } = this.props.route || {};
        // TODO: test
        params = params || {"title": "相思", "itemId": 2};
        InterfaceRequest(PublicConstant.EnvConstant.articles_comment_addition, {
          body: {
            pid: params.itemId, 
            audio: data.href,
            text: data.filename,
            size: data.size
          }
        },"POST", 
        (res: any)=>{
          if (res.code === 200) {
            _this.setState({
              lastAudioFile: data.href,
            })
            Toast.show({ type: "info", text1: "录音评价完成" });
            // 显示评价窗口
            _this.onViewComment();
          } else {
            Toast.show({ type: "error", text1: res.message || "服务异常,小主,请稍后~", props: {width: 290} });
          }
        }, 
        (error: any)=>{
          Toast.show({ type: "error", text1: "服务太忙了,小主,请稍~", props: {width: 290} });
        })
      } else {
        Toast.show({ type: "error", text1: data.message || "录音文件上传失败~", });
      }
    }, (error: any)=>{
      Toast.show({ type: "error", text1: "录音文件上传失败!", });
    })
    // console.log("录音停止,录音文件在:", uri);
  };
  /**
   * 播放录音
   * @param {string} uri 录音文件地址,优先使用 格式: 402553|http://micro-api.app.raodaor.com:9002/assets/uploads/POETRY34A33A1FC270325752EAF29C2B2022/0329/article-audio-record-1648488632763.m4a
   * @param {number} itemid 记录的id
   */
  onPlaySound = async (uri: string, itemid: number) => {
    // 解析获取资源信息
    let truthUri = extractResourceInfo(uri, "uri");
    const { lastAudioFile } = this.state;
    // console.log("加载音频文件");
    const { sound } = await Audio.Sound.createAsync(
      // { uri: "http://10.2.190.34:9002/assets/uploads/POETRY34A33A1FC270325752EAF29C2B2022/0211/article-audio-record-1644571314954.m4a"},
      { uri: truthUri || lastAudioFile || require("../../assets/audio/汽修师-宝宝巴士儿歌.m4a")},
      { shouldPlay: true }
    );
    // 监听音频对象播放进度
    let soundProgressMonitor = setInterval(async ()=>{
      let monitorProgress = await sound.getStatusAsync()
      // "isPlaying": false,
      // "positionMillis": 8034,
      // 表示播放停止
      if (monitorProgress["isPlaying"] === false) {
        this.onStopSound()
      } else {
        this.setState({
          // 总播放的时长 - 当前播放的位置时长
          soundPositionMillis: monitorProgress["durationMillis"] - monitorProgress["positionMillis"]
        })
      }
      // console.log("isPlaying===>positionMillis", monitorProgress["positionMillis"])
    }, 1000);
    // 获取音频状态
    let soundInitStatus = await sound.getStatusAsync()
    // 公共的播放器对象
    this.setState({
      sound: sound,
      soundPositionMillis: soundInitStatus["durationMillis"],
      soundItemFlag: itemid,
      soundProgressMonitor: soundProgressMonitor
    }, async() => {
      // console.log("开始播放音频文件");
      await this.state.sound.playAsync();
    })
  };
  /* 停止播放 */
  onStopSound = async () => {
    // console.log("开始停止播放..");
    const { sound, soundProgressMonitor } = this.state;
    sound && await sound.stopAsync(); // replayAsync  重新播放, pauseAsync  暂停播放
    sound && await sound.unloadAsync(); // 卸载
    soundProgressMonitor && clearInterval(soundProgressMonitor); // 清除定时监听
    this.setState({
      sound: undefined,
      soundPositionMillis: 0,
      soundProgressMonitor: undefined,
    })
  };
  /* 关闭评论弹层 */
  onCloseComment = async () => {
    this.onStopSound();
    this.setState({
      commentModal: false,
    })
  };
  /**
   * 去外网查询
   * @param {object} query 查询参数
   */
  onGoToQuery = (query: any) => {
    this.props.navigation.navigate("webViewDefault", {
      title: "查询 " + query,
      uri: "https://www.baidu.com/s?wd=" + query
    })
  };
  /* 展示评论浮层 */
  onViewComment = () => {
    this.setState({
      commentModal: true
    })
    this.onRefreshComment();
  }
  /* 刷新评论列表 */
  onRefreshComment = () => {
    let _this = this;
    this.setState({
      CommentRefreshing: true,
    })
    this.onFetchDataComment(()=>{
      _this.setState({
        CommentRefreshing: false
      })
    })
  };
  /* 获取评论列表 */
  onFetchDataComment = (callback: any) => {
    let { params } = this.props.route || {};
    // TODO: test
    // params = params || {"title": "相思", "itemId": 2};
    
    let _this = this;
    if (params && params["itemId"]) {
      // 获取评论列表
      InterfaceRequest(
        PublicConstant.EnvConstant.articles_comment_lists, 
        {
          body: {
            id: params.itemId,
            userKey: PublicConstant.EnvConstant.appUserInfo.key,
          }
        }, 
        "POST", 
        (data: any)=>{
          // console.log("====评论列表====>", data)
          if (data.code === 200) {
            _this.setState({
              commentListData: data.data.rows,
              commentDataCount: data.data.count
            })
          } else {
            Toast.show({ type: "error", text1: data.message || "获取失败,小主,请稍后~" });
          }
          callback && callback()
        }, 
        (error: any)=>{
          callback && callback()
          Toast.show({ type: "error", text1: "服务异常,小主,请稍后~", });
        })
    }
  }
  /**
   * 列表滑动触底后触发的事件
   * by https://docs.expo.dev/versions/v44.0.0/react-native/flatlist/#onendreached
   */
  onEndReachedComment = (info: {distanceFromEnd: number}) => {
    // console.log("触底了~, 距离有:", info.distanceFromEnd, this.state.commentPageNumber);
    const { commentDataCount, commentListData, commentPageSize } = this.state;
    if (!this.state.CommentRefreshing 
      && commentListData.length >= commentPageSize 
      && commentDataCount > commentListData.length) 
    {
      this.setState({
        commentPageNumber: this.state.commentPageNumber + 1
      }, ()=>{
        this.onFetchDataComment(null)
      })
    }
  };
  /* 跳转到个人用户 */
  onJumpUserInfo = (item: any) => {
    // this.onCloseComment();
    this.props.navigation.navigate("myInfos", {
      userKey: item.user_key,
    });
  };
  /* 跳转到举报页面 */
  onJumpReport = (item: any) => {
    // this.onCloseComment();
    this.props.navigation.navigate("report", {
      userKey: item.user_key,
    });
  }

  render() {
    // 获取State变量
    const { 
      refreshing, CommentRefreshing, 
      commentModal, commentListData, commentDataCount,
      articleData, 
      pageStatus, pageStatusText, 
      recording, sound, soundPositionMillis, soundItemFlag, 
    } = this.state;

    const _renderCommentItem = (obj: any) => {
      let { item } = obj; // {index: 0, item: Object {}}
      // console.log(item, "=====>item comment")
      return <View key={`comment-list-${item.id}`} style={styles.commentItemWrap}>
        {/* 评论人头像 */}
        <TouchableOpacity 
          // style={styles.itemWrap}
          onPress={ this.onJumpUserInfo.bind(null, item) }
        >
          <Image
            style={styles.commentAvatar}
            onError={(error)=>{}}
            source={
              {uri: item.avatar || PublicConstant.EnvConstant.defaultAvatar}
            }
          />
        </TouchableOpacity>
        {/* 评论信息 */}
        <View style={styles.commentInfo}>
          {/* 评论的内容区 */}
          <Text style={styles.nickTxt}>{item.nick || item.user_key}</Text>
          <View>
            {/* 目前只展示 评价文字和录音 */}
            <Text style={styles.commentTxt}>{item.text}</Text>
            {/* <Text>{item.img}</Text> */}
            {/* <Text>{item.audio}</Text> */}
            <TouchableOpacity style={styles.playWrapper} onPress={()=>{ sound ? this.onStopSound() : this.onPlaySound(item.audio, item.id) }}>
              <Text style={styles.btnAudioTxt}>{(sound && soundItemFlag === item.id) ? "停止播放" : "播放录音"}</Text>
              <Text style={styles.btnAudioTips}>{(sound && soundItemFlag === item.id) ? millisToMinutesAndSeconds(soundPositionMillis) : extractResourceInfo(item.audio, "size")}</Text>
            </TouchableOpacity>
            {/* <Text>{item.video}</Text> */}
            {/* <Text>{item.file}</Text> */}
          </View>
          {/* 底部区域 */}
          <View style={styles.commentBottom}>
            <Text>{formatDataShort(item.time)}</Text>
            {/* 快捷按钮(比如:举报) */}
            <View style={styles.commentTool}>
              <TouchableOpacity onPress={ this.onJumpReport.bind(null, item) }>
                <Text>点赞</Text>
              </TouchableOpacity>
              <TouchableOpacity onPress={ this.onJumpReport.bind(null, item) }>
                <Text>举报</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </View>
    }

    return (
      // SafeAreaView 是IOS的组件
      <SafeAreaView style={styles.container}>
        <StatusBar
          animated={true}
          style="light"
          backgroundColor="#f70f0f" />
        {/* 页面状态 */}
        <ComponentsPageStatus status={pageStatus} statusTxt={pageStatusText} callback={this.onRefresh} />
        <ScrollView 
        refreshControl={<RefreshControl refreshing={refreshing} onRefresh={this.onRefresh} />} >
          {/* 文章内容是占位符,上下左右应该还有其它内容,来自平台配置 */}
          <Pressable onPress={this.onCloseComment}>
            <Text 
              style={styles.articleContent} 
              // onLongPress={(e)=>{alert("长按了")}}
              selectable={true} 
              selectionColor="#c4cdff">
              {(articleData["content"] || "").replace(/<.*?>/g,"")}
            </Text>
          </Pressable>
        </ScrollView>
        {/* 按钮区域 */}
        <View style={styles.moreToolWrap}>
          <Pressable onPress={this.onViewComment}>
            <Text style={styles.btnDefault}>用户评论</Text>
          </Pressable>
          <Pressable onPress={recording ? this.onStopRecording : this.onStartRecording}>
            <Text style={styles.btnDefault}>{recording ? "停止录音" : "开始录音"}</Text>
          </Pressable>
          <Pressable onPress={this.onGoToQuery.bind(null, articleData["title"])}>
            <Text style={styles.btnDefault}>百度查询</Text>
          </Pressable>
        </View>
        {/* 悬浮按钮区域 */}
        <Animated.View {...this.panResponder.panHandlers} style={[this.panObject.getLayout(), styles.floatWrap]}>
          <Image style={styles.btnLightNight} source={require("../../assets/public/icon-night.png")} />
          <Image style={styles.btnLightNight} source={require("../../assets/public/icon-light.png")} />
        </Animated.View>
        {/* 评论内容区域-浮层展示 */}
        {/* <Modal
          animationType="slide"
          transparent={true}
          visible={commentModal}
          onRequestClose={this.onCloseComment}> */}
          {
            commentModal ? 
          <View style={styles.commentModalWrap}>
            <View style={styles.modalHead}>
              <Text style={styles.modalTitle}>{commentDataCount}条评论</Text>
              <Pressable
                style={styles.modalClose}
                onPress={this.onCloseComment}>
                <ImageBackground source={require("../../../assets/nav-cancel.png")} style={{width: 20, height: 20}}>
                </ImageBackground>
              </Pressable>
            </View>
            <FlatList
              data={commentListData}
              renderItem={_renderCommentItem}
              keyExtractor={item => `${item.user_key}-${item.id}`}
              // numColumns={2} // 要渲染多列,可以指定此项
              refreshControl={
                <RefreshControl
                  refreshing = {CommentRefreshing}
                  onRefresh = {this.onRefreshComment}
                />
              }
              onEndReachedThreshold={0.5}
              onEndReached={this.onEndReachedComment}
            />
          </View>
           : null
          }
        {/* </Modal> */}
      </SafeAreaView>
    );
  }
}

// 最上层的Layout
export default class AppContainer extends React.Component {
  render() {
    const ConnectedApp = connectActionSheet<object>(DetailsArticle);
    return (
      <ActionSheetProvider>
        <ConnectedApp {...this.props} />
      </ActionSheetProvider>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#ffffff"
    // marginTop: Constants.statusBarHeight,
  },
  articleContent: {
    lineHeight: 25,
    margin: 10,
    fontSize: 18,
  },
  // 更多按钮工具区域
  moreToolWrap: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    height: 60,
    paddingLeft: 30,
    paddingRight: 30,
    marginBottom: 20,
  },
  // 默认按钮样式
  btnDefault: {
    borderRadius: 10,
    fontSize: 15,
    color: "#ffffff",
    backgroundColor: "#f70f0f",
    paddingLeft: 10,
    paddingRight: 10,
    paddingTop: 5,
    paddingBottom: 5,
  },
  // 评论列表
  commentModalWrap: {
    width: Dimensions.get("window").width,
    height: Dimensions.get("window").height / 2 + 150,
    backgroundColor: "#ffffff",
    marginTop: Dimensions.get("window").height / 2 - 150,

    position: "absolute",
    left: 0,
    bottom: 0,
    zIndex: 99,
  },
  // 弹框的标题部分
  modalHead: {
    height: 50,
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    // borderBottomColor: "#f3ecec",
    // borderBottomWidth: 1,
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    // 阴影效果
    elevation: 1, // 该属性只支持>=android 5.0
    shadowColor: "#b7b2b2", // IOS
    shadowOffset: {width:0, height:0}, // IOS
    shadowOpacity: 1, // IOS
    shadowRadius: 1, // IOS
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: "500"
  },
  modalClose: {
    width: 50,
    height: 50,
    position: "absolute",
    right: 10,
    top: 0,
    alignItems: "center",
    justifyContent: "center"
  },
  // 评论相关
  commentItemWrap: {
    flexDirection: "row",
    margin: 16,
  },
  commentAvatar: {
    width: 32,
    height: 32,
    borderRadius: 32,
    borderWidth: 1,
    borderColor: "#eee"
  },
  commentInfo: {
    marginLeft: 10,
    width: Dimensions.get("window").width - 80
  },
  commentBottom: {
    flexDirection: "row",
    justifyContent: "space-between",
    paddingBottom: 10,
    borderBottomColor: "#f7f2f2",
    borderBottomWidth: 1,
  },
  commentTool: {
    flexDirection: "row",
    width: 65,
    justifyContent: "space-between"
  },
  // 昵称
  nickTxt: {
    fontSize: 15,
    marginBottom: 5,
  },
  // 文本评价内容
  commentTxt: {
    fontSize: 14,
  },
  // 播放容器
  playWrapper: {
    flexDirection: "row",
    alignItems: "center",
    marginTop: 5,
    marginBottom: 5,
  },
  // 按钮样式
  btnAudioTxt: {
    backgroundColor: "#f01d00cc",
    fontSize: 14,
    color: "#ffffff",
    width: 75,
    height: 26,
    borderRadius: 26,
    lineHeight: 26,
    textAlign: "center",
  },
  btnAudioTips: {
    fontSize: 12,
    marginLeft: 5,
    color: "#504e4e",
  },
  btnLightNight: {
    width: 50,
    height: 50,
  },
  // 浮动元素
  floatWrap: {
    position: "absolute",
    right: 0,
    top: 100,
  }
});

runs with Expo Go