flutter和reactNative以及uniapp区别

首先我个人认为 这三个框架都有自己的好处 都是跨平台的框架

如果当前团队是vue技术栈 我个人推荐使用uniapp

如果说当前团队是react为主 推荐使用reactNative

如果说当前团队是原生开发为主 推荐采用flutter为主

首先为什么 我会这么说

第一 原生开发几乎没有很大的学习成本就可以立刻上手flutter这个框架 他们采用的都是声明式ui进行页面的开发

第二 为啥uniapp能够推荐呢 因为大部分的开发app其实uniapp是够用的 如果用户对于性能已经loading这种没有太大的反馈的话 几乎用这个可以很快的开发一个app出来

第三 为啥使用reactNative 首先 他是直接js操作直接生成原生的代码 几乎性能这一块跟原生是一样的 这样就不会出现用户吐槽说卡顿以及loading的效果

我用这三个框架具体实现了对应的效果

uniapp

复制代码
<template>
	<view class="content">
		<view class="user-detail">
			<view class="list-box">
				<view class="list-item" @click="upDateuserImage">
					<view>头像</view>
					<view class="left-box">
						<image v-if="memberInfo.headPath" :src="realImgPath" mode=""></image>
						<image v-else src="/static/missing-face.png" mode=""></image>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item" @click="updateValue('nickname','修改昵称')">
					<view>昵称</view>
					<view class="left-box">
						<view class="title">{{memberInfo.nickname || ''}}</view>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item" @click="updateValue('memberName','修改姓名')">
					<view>姓名</view>
					<view class="left-box">
						<view class="title">{{memberInfo.memberName || ''}}</view>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item" @click="showSexDialog">
					<view>性别</view>
					<view class="left-box">
						<view class="title">{{sex || ''}}</view>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item">
					<view>生日</view>
					<view class="left-box">
						<picker mode="date" :end="endDate" @change="birthdayChange" :value="memberInfo.birthday">
							<view class="title" style="width:480rpx;height:40rpx;text-align: right;">{{memberInfo.birthday || ''}}</view>
						</picker>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item" @click="chooseLocation">
					<view>所在城市</view>
					<view class="left-box">
						<view class="title">{{memberInfo.province || ''}} {{memberInfo.city || ''}}</view>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item" @click="showPsignatureDialog">
					<view>兴趣爱好</view>
					<view class="left-box">
						<view class="title clamp">{{memberInfo.psignature || ''}}</view>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<!-- <view class="list-item">
					<view>手机号</view>
					<view class="left-box" @click="updateValue('mobileNo','修改手机号')">
					<view class="left-box" @click="updateValue('mobileNo','修改手机号')">
						<view class="title">{{memberInfo.mobileNo}}</view> 
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view>
				<view class="list-item" @click="updateValue('userSig','修改个人签名')">
					<view>个性签名</view>
					<view class="left-box">
						<view class="title">{{memberInfo.userSig}}</view>
						<uni-icons type="arrowright"></uni-icons>
					</view>
				</view> -->
			</view>
		</view>
		<uni-popup ref="popup" type="center" :maskClick="false">
			<view class="pop-dialog">
				<view class="title">{{title}}</view>
				<view class="pop-content">
					<view class="withdraw">
						<input placeholder="请输入内容" maxlength="20" v-model="content" type="text"></input>
					</view>
				</view>
				<view class="g-row btn-group">
					<view class="btn" @tap="closeDialog('popup')">
						<text class="btn-txt">取消</text>
					</view>
					<view class="btn" @tap="confirm">
						<text class="btn-txt">确定</text>
					</view>
				</view>
			</view>
		</uni-popup>
	
		<uni-popup ref="sexPopup" type="bottom">
			<view class="pop-dialog sex-pupup">
				<view class="title">性别</view>
				<view class="pop-content">
					<picker-view :indicator-style="indicatorStyle" :value="currSex" @change="sexChange" class="picker-view">
						<picker-view-column>
							<view class="item" v-for="(item,index) in sexList" 
								:key="item">{{item=='M'?'男':item=='F'?'女':'保密'}}</view>
						</picker-view-column>
					</picker-view>
				</view>
				<view class="g-row btn-group">
					<view class="btn" @tap="closeDialog('sexPopup')">
						<text class="btn-txt">取消</text>
					</view>
					<view class="btn" @tap="confirmSex">
						<text class="btn-txt">确定</text>
					</view>
				</view>
			</view>
		</uni-popup>
		
		<uni-popup ref="psignaturePopup" type="center" :maskClick="false">
			<view class="pop-dialog psig-pupup">
				<view class="title">兴趣爱好</view>
				<view class="pop-content">
					<textarea placeholder="请输入兴趣爱好用','分割;例如 篮球,电影,美食" 
						v-model="psignature" maxlength="200"></textarea>
				</view>
				<view class="g-row btn-group">
					<view class="btn" @tap="closeDialog('psignaturePopup')">
						<text class="btn-txt">取消</text>
					</view>
					<view class="btn" @tap="confirmPsignature">
						<text class="btn-txt">确定</text>
					</view>
				</view>
			</view>
		</uni-popup>
		
		<w-picker mode="region" :regionLevel="2" :defaultVal="defaultArea" @confirm="onAreaConfirm" ref="region"></w-picker>
	</view>
</template>

<script>
	import uniPopupDialog from '@/components/uni-popup/uni-popup-dialog.vue' 
	import uniPopup from '@/components/uni-popup/uni-popup.vue'
	import { mapState, mapMutations } from 'vuex';
	import utils from '@/common/utils.js'
	import wPicker from "@/components/w-picker/w-picker.vue"
	
	export default {
		components: {
			wPicker
		},
		computed: {
			...mapState(['hasLogin', 'userInfo']),
			realImgPath(){
				return this.$realImagePath(this.memberInfo.headPath)
			},
			sex(){
				return this.memberInfo.sex == 'M' ? '男' : this.memberInfo.sex == 'F' ? '女' : '保密'
			},
			currSex(){
				return this.memberInfo.sex == 'M' ? [0] : this.memberInfo.sex == 'F' ? [1] : [2]
			},
			endDate(){
				return utils.formatDate(new Date(), 'yyyy-MM-dd')
			}
		},
		components: {
			uniPopup,
			uniPopupDialog
		},
		data() {
			return {
				title: '',
				name: '',
				content: '',
				psignature: '',
				sexList: ['M', 'F', ''],
				indicatorStyle: `height: 40px;`,
				memberInfo: {
					headPath: '',
					nickname: '',
					mobileNo: '',
					sex: '',
					memberName: '',
					certNo: '',
					psignature: '',
					userSig: '',
					province: '',
					city: '',
					area: ''
				},
				visible: {
					dizhiShow: false,
					sexShow: false,
				},
				memberRealName:'',
				defaultArea: []
			}
		},
		onPullDownRefresh() {
			this.getMemberInfo((data) => {
				this.memberInfo = data
				// this.memberRealName = data.memberInfo.memberName
			})
		},
		onLoad(options) {
			this.redirectPage = options.redirect || 'prev'
			this.getMemberInfo((data) => {
				this.memberInfo = data
				// this.memberRealName = data.memberInfo.memberName
			})
		},
		methods: {
			...mapMutations(['login']),
			showSexDialog(){
				this.$refs.sexPopup.open()
			},
			showPsignatureDialog(){
				this.psignature = this.memberInfo.psignature || ''
				this.$refs.psignaturePopup.open()
			},
			closeDialog(dialog){
				this.$refs[dialog].close()
			},
			birthdayChange(e){
				let _birthday = e.detail.value
				this.updateMemberInfo({birthTime: _birthday}, () => {
					this.memberInfo.birthday = _birthday
				})
			},
			confirmPsignature(){
				this.closeDialog('psignaturePopup')
				if(!this.psignature || this.psignature.trim().length == 0){
					return false
				}
				this.updateMemberInfo({psignature: this.psignature.trim()}, () => {
					this.$set(this.memberInfo, 'psignature', this.psignature.trim());
				})
			},
			onAreaConfirm(data){
				this.updateMemberInfo({province: data.checkArr[0], city: data.checkArr[1]}, () => {
					this.$set(this.memberInfo, 'province', data.checkArr[0]);
					this.$set(this.memberInfo, 'city', data.checkArr[1]);
				})
			},
			chooseLocation(){
				this.$refs['region'].show();
			},
			getMemberInfo(callback) {
				var _this = this
				let params = { token: _this.userInfo.token }
				_this.$http.post(`/api/memberInfo/getMemberInfo`, params,
					(data) => {
						if (data.status === 1) {
							callback && callback(data.data || {});
						} else {
							_this.$toast(data.msg)
						}
					}, (data) => {
						_this.$toast(data)
					}, () => {
						uni.stopPullDownRefresh()
					})
			},
			upDateuserImage() {
				let _this = this;
				uni.chooseImage({ // 从本地相册选择图片或使用相机拍照。
					count: 1, //默认选择1张图片
					sizeType: ['original', 'compressed'], //original 原图,compressed 压缩图,默认二者都有
					success: (res) => {
						console.log(res.tempFilePaths[0]); //成功则返回图片的本地文件路径列表 tempFilePaths
						uni.uploadFile({ //将本地资源上传到开发者服务器
							url: `${this.$baseFileUrl}/imageUpload.do`, //接口地址
							filePath: res.tempFilePaths[0], //图片地址
							name: 'uploadToImportFile',
							formData: {
								'id': new Date().getTime()
							},
							success: (uploadFileRes) => {
								console.log(uploadFileRes)
								let resTxt = uploadFileRes.data;
								let data = JSON.parse(resTxt);
								if (data.success) {
									let path = data.data;
									let realUrl = this.$baseFileUrl + path;
									_this.$http.post(`${_this.$baseUrl}/api/memberInfo/updateMemberHeadImg`, {
											token: _this.userInfo.token,
											headPath: path
										},
										(e) => {
											console.log(e)
										}

									)
									_this.memberInfo.headPath = realUrl;
								}
							}
						});
					}
				});
			},
			updateValue(name, title) {
				this.name = name
				this.title = title
				this.content = ''
				this.$refs.popup.open()
			},
			sexChange(e){
				this.sexEn = this.sexList[e.detail.value[0]]
			},
			confirmSex(){
				this.closeDialog('sexPopup')
				if(this.sexEn !== this.memberInfo.sex){
					this.updateMemberInfo({sex: this.sexEn}, () => {
						this.memberInfo.sex = this.sexEn
					})
				}
			},
			confirm() {
				let value = this.content || ''
				if(!value){
					return this.$toast('请输入内容')
				}
				this.setValueByName(value)
				let params = {}
				this.$set(params, this.name, value)
				if(this.name != 'mobileNo') {
					this.updateMemberInfo(params, (data) => {
						this.$refs.popup.close()
						if(this.name === 'nickname'){
							this.userInfo.nickname = value
							this.login(this.userInfo)
						}
					})
				} else {
					this.submitBindMobile(value)
				}
				
			},
			updateMemberInfo(params, callback) {
				params.token = this.userInfo.token
				var _this = this
				uni.showLoading({ title: '保存中...', mask: true })
				_this.$http.post(`${_this.$baseUrl}/api/memberInfo/updateMemberInfo`, params,
					(data) => {
						if (data.status === 1) {
							this.$toast('保存成功')
							callback && callback(data.data || {});
						} else {
							_self.$toast(data.msg)
						}
					}, (data) => {
					}, () => {
						uni.hideLoading()
					})
			},
			submitBindMobile(value){
				if (!/^1[3-9][0-9]{9}$/.test(value)) {
					this.$toast('请输入正确的手机号');
					return;
				}
				let params = {
					 mobileNo: value
				};
				this.bindMobile(params, (data)=>{
					this.$toast('绑定手机号成功')
					this.$refs.popup.close() 
				})
			},
			bindMobile(params, callback) {
				let _self = this;
				params.token = this.userInfo.token;
				uni.request({
					url: `${_self.$baseUrl}/api/member/bindMobile`,
					method: 'POST',
					header: {
						"content-type": "application/x-www-form-urlencoded"
					},
					data: params,
					success: (res) => {
						let data = res.data;
						if (data.status === 1) {
							// let mData = data.data || {};
							callback && callback();
						} else {
							_self.$toast(data.msg)
						}
					},
					fail: () => {
						_self.$toast('网络连接错误')
					},
					complete: () => {
						uni.hideLoading();
					}
				});
			},
			setValueByName(value) {
				this.$data.memberInfo[this.name] = value
			},
			hidePicker() {
				this.visible.sexShow = false
				this.visible.dizhiShow = false
			},
			showPicker(type, level) {
				if (type == 'sex') this.visible.sexShow = true
				if (type == 'dizhi') this.visible.dizhiShow = true
			},
			dizhiConfirm(data) {
				this.memberInfo.province = data.obj.province.label
				this.memberInfo.city = data.obj.city.label
				this.memberInfo.area = data.obj.area.label
				let params = {
					province: data.obj.province.label,
					city: data.obj.city.label,
					area: data.obj.area.label
				}
				this.updateMemberInfo(params, (data) => {})
				this.hidePicker()
			},
			sexConfirm(data) {
				this.memberInfo.sex = data.value
				let params = {
					sex: data.value
				}
				this.updateMemberInfo(params, (data) => {})
				this.hidePicker()
			},
		}
	}
</script>

<style lang="scss">
	.user-detail {
		padding-top: 20rpx;
		.list-box {
			.list-item {
				margin-bottom: 30rpx;
				padding: 20rpx 30rpx;
				display: flex;
				align-items: center;
				justify-content: space-between;
				border-bottom: 1px solid #f4f4f8;

				.left-box {
					display: flex;
					align-items: center;
					picker {
						width: 480rpx;
						height: 40rpx;
					}
					image {
						margin-right: 20rpx;
						width: 60rpx;
						height: 60rpx;
					}

					.title {
						margin-right: 20rpx;
						font-size: 30rpx;
						max-width: 460rpx;
					}
				}
			}
		}
	}
	.pop-dialog{
		&.nick-popup{
			width: 540rpx;
		}
		background-color: #FFFFFF;
		padding: 40rpx;
		border-radius: 20rpx;
		color: $font-color-dark;
		&.sex-pupup{
			border-radius: 20rpx 20rpx 0 0;
		}
		&.psig-pupup{
			width: 640rpx;
			.pop-content{
				display: flex;
			}
			textarea {
				flex: 1;
				background-color: #f8f8f8;
				padding: 20rpx;
				font-size: 14px;
				height: 240rpx;
			}
		}
		.title{
			display: flex;
			justify-content: center;
			font-weight: bold;
			font-size: 18px;
			padding-bottom: 20rpx;
			/* border-bottom: solid 1rpx rgba(255,255,255, 0.2); */
		}
		.dia-top {
			display: flex;
			justify-content: space-between;
			width: 100%;
			font-size: 14px;
			margin-top: 40rpx;
			margin-bottom: 24rpx;
			font-size: 13px;
			color: #42F5FF;
			>text:nth-child(2){
				color: #F8C343;
			}
		}
		.pop-content{
			padding: 30rpx 0;
			.tips{
				color: #939393;
				font-size: 14px;
				margin-top: 8rpx;
				line-height: 30rpx;
			}
			input {
				font-size: 14px;
				border: none;;
				background-color: #F7F7F7;
				border-radius: 40rpx;
				height: 80rpx;
				padding: 0 30rpx;
			}
			.picker-view {
				height: 400rpx;
				margin-top: 20rpx;
			}
			.item {
				line-height: 80rpx;
				text-align: center;
			}
		}
		.dia-bottom {
			display: flex;
			justify-content: flex-end;
			font-size: 13px;
			padding: 20rpx 0;
			color: #e9e9e9;
		}
		.btn-group{
			display: flex;
			margin-top: 16rpx;
			margin-bottom: 10rpx;
			height: 80rpx;
			justify-content: center;
			width: 100%;
			.btn {
				margin-top: 0;
				display: flex;
				justify-content: center;
				align-items: center;
				position: relative;
				width: 240rpx;
				height: 80rpx;
				background-color: #D49B43;
				color: #FFFFFF;
				border-radius: 40rpx ;
				&:first-child{
					margin-right: 40rpx;
					background-color: #ffffff;
					border: solid 2rpx #D49B43;
					.btn-txt {
						color: #D49B43;
					}
				}
				.btn-txt {
					position: absolute;
					font-size: 14px;
					color: #FFFFFF;
					text-align: center;
				}
			}
		}
	}
</style>

reactNative

复制代码
import React, { useEffect, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  Dimensions,
  Image,
  TouchableOpacity,
  Platform,
  Alert
} from 'react-native'
import { useLocalStore } from 'mobx-react';
import HomeStore from './HomeStore';
import { observer } from 'mobx-react';
import FlowList from '../../components/flowlist/FlowList.js';
import ResizeImage from '../../components/resizeImage/ResizeImage';
import Heart from '../../components/heart/Heart';
import TitleBar from './components/TitleBar';
import CategoryList from '../../components/categoryList/CategoryList';
import { save } from '../../utils/Storage';

import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import {
  checkUpdate,
  downloadUpdate,
  switchVersion,
  isFirstTime,
  isRolledBack,
  markSuccess,
  switchVersionLater
} from 'react-native-update';

import _updateConfig from '../../../update.json';
import Toast from '../../components/widget/Toast.js';
const { appKey } = (_updateConfig as any)[Platform.OS];

const { width: SCREEN_WIDTH } = Dimensions.get('window');

export default observer(() => {

  const store = useLocalStore(() => new HomeStore());

  const navigation = useNavigation<StackNavigationProp<any>>();

  useEffect(() => {
    store.requestHomeList();
    store.getCategoryList();

    checkPatch();

    // p_1.0_1
    // {"forceUpdate":true}
    if (isFirstTime) {
      markSuccess();
      // 补丁成功,上报服务器信息
      // 补丁安装成功率:99.5% ~ 99.7%
    } else if (isRolledBack) {
      // 补丁回滚,上报服务器信息
    }
  }, []);

  // 检查补丁更新
  // {"forceUpdate":true}
  const checkPatch = async () => {
    const info: any = await checkUpdate(appKey);
    const { update, name, description, metaInfo } = info;
    const metaJson = JSON.parse(metaInfo);
    save('patchVersion', name);
    const { forceUpdate } = metaJson;
    if (forceUpdate) {
      // 弹窗提示用户
    } else {
      // 不弹窗默默操作
    }
    if (update) {
      const hash = await downloadUpdate(
        info,
        {
          onDownloadProgress: ({ received, total }) => { },
        },
      );
      if (hash) {
        if (forceUpdate) {
          switchVersion(hash);
        } else {
          switchVersionLater(hash);
        }
      }
    }
  }

  const refreshNewData = () => {
    store.resetPage();
    store.requestHomeList();
  }

  const loadMoreData = () => {
    store.requestHomeList();
  }

  const onArticlePress = useCallback((article: any) => () => {
    navigation.push('ArticleDetail', { id: article.id })
  }, []);

  const renderItem = ({ item, index }: { item: any, index: number }) => {
    return (
      <TouchableOpacity
        style={styles.item}
        onPress={onArticlePress(item)}
      >
        <ResizeImage uri={item.image} />
        <Text style={styles.titleTxt}>{item.title}</Text>
        <View style={styles.nameLayout}>
          <Image style={styles.avatarImg} source={{ uri: item.avatarUrl }} />
          <Text style={styles.nameTxt}>{item.userName}</Text>
          <Heart
            value={item.isFavorite}
            onValueChanged={(value: boolean) => {
              console.log(value);
            }}
          />
          <Text style={styles.countTxt}>{item.favoriteCount}</Text>
        </View>
      </TouchableOpacity>
    );
  }

  const Footer = () => {
    return (
      <Text style={styles.footerTxt}>没有更多数据</Text>
    );
  }

  const categoryList = store.categoryList.filter((i: any) => i.isAdd);
  return (
    <View style={styles.root}>
      <TitleBar
        tab={1}
        onTabChanged={(tab: number) => {
          console.log(`tab=${tab}`)
        }}
      />
      <FlowList
        style={styles.flatList}
        data={store.homeList}
        keyExtrator={(item: any) => `${item.id}`}
        extraData={[store.refreshing]}
        contentContainerStyle={styles.container}
        renderItem={renderItem}
        numColumns={2}
        refreshing={store.refreshing}
        onRefresh={refreshNewData}
        onEndReachedThreshold={0.1}
        onEndReached={loadMoreData}
        ListFooterComponent={<Footer />}
        ListHeaderComponent={
          <CategoryList
            categoryList={categoryList}
            allCategoryList={store.categoryList}
            onCategoryChange={(category: any) => {
              console.log(JSON.stringify(category));
            }}
          />
        }
      />
    </View>
  );
});

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f0f0f0'
  },
  flatList: {
    width: '100%',
    height: '100%',
  },
  container: {
    // paddingTop: 6,
  },
  item: {
    width: SCREEN_WIDTH - 18 >> 1,
    backgroundColor: 'white',
    marginLeft: 6,
    marginBottom: 6,
    borderRadius: 8,
    overflow: 'hidden',
  },
  titleTxt: {
    fontSize: 14,
    color: '#333',
    marginHorizontal: 10,
    marginVertical: 4,
  },
  nameLayout: {
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 10,
    marginBottom: 10,
  },
  avatarImg: {
    width: 20,
    height: 20,
    resizeMode: 'cover',
    borderRadius: 10,
  },
  nameTxt: {
    fontSize: 12,
    color: '#999',
    marginLeft: 6,
    flex: 1,
  },
  heart: {
    width: 20,
    height: 20,
    resizeMode: 'contain',
  },
  countTxt: {
    fontSize: 14,
    color: '#999',
    marginLeft: 4,
  },
  footerTxt: {
    width: '100%',
    fontSize: 14,
    color: '#999',
    marginVertical: 16,
    textAlign: 'center',
    textAlignVertical: 'center',
  },
})

flutter

复制代码
import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_jdshop/model/ProductModel.dart';
import 'package:flutter_jdshop/utils/titleWidget.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart';
import 'package:flutter_jdshop/model/FocusModel.dart';
import 'package:dio/dio.dart';
import 'package:flutter_jdshop/components/loading.dart';

import '../../config/Config.dart';

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin{

  List _focusData = [];
  List _hotProductList = [];
  List _bestProductList = [];

  @override
  void initState() {
    super.initState();
    _getFocusData();
    _getHotProductData();
    _getBestProductData();
  }

  _getFocusData() async {
    var url = "${Config.domain}api/focus";
    try {
      var focusData = await Dio().get(url);
      var focusList = FocusModel.fromJson(focusData.data);
      setState(() {
        _focusData = focusList.result ?? [];
      });
    } catch (e) {
      print('Failed to get focus data: $e');
    }
  }

  _getHotProductData() async {
    var url = "${Config.domain}api/plist?is_hot=1";
    try {
      var result = await Dio().get(url);
      var hotProductList = ProductModel.fromJson(result.data);
      setState(() {
        _hotProductList = hotProductList.result ?? [];
      });
    } catch (e) {
      print('Failed to get focus data: $e');
    }
  }

  _getBestProductData() async {
    var url = "${Config.domain}api/plist";
    try {
      var result = await Dio().get(url);
      var bestProductList = ProductModel.fromJson(result.data);
      setState(() {
        _bestProductList = bestProductList.result ?? [];
      });
    } catch (e) {
      print('Failed to get focus data: $e');
    }
  }

  // 示例图片列表
  // final List<String> _imageUrls = [
  //   'https://img2.baidu.com/it/u=2612741288,182099192&fm=253&fmt=auto&app=138&f=JPEG?w=513&h=500',
  //   'https://img1.baidu.com/it/u=1791653389,599136142&fm=253&fmt=auto&app=120&f=JPEG?w=800&h=960',
  // ];

  Widget _SwiperWidget() {
    if (this._focusData.length > 0) {
      return Container(
        height: 200,
        child: Swiper(
          itemBuilder: (BuildContext context, int index) {
            String pic = this._focusData[index].pic;
            return Image.network(
              "${Config.domain}${pic.replaceAll('\\', '/')}",
              fit: BoxFit.fill,
            );
          },
          itemCount: this._focusData.length,
          pagination: SwiperPagination(),
          control: SwiperControl(),
          autoplay: true,
        ),
      );
    } else {
      return Container(
        height: 200.h,
        child: LoadingWidget(),
      );
    }
  }

  Widget _ListHorizontal() {
    if (this._hotProductList.length > 0) {
      return Container(
        height: 180.h,
        width: double.infinity,
        child: ListView.builder(
          // 水平从左到右 默认是从上到小
          scrollDirection: Axis.horizontal,
          itemBuilder: (context, index) {
            return Container(
              width: 70.w,
              margin: EdgeInsets.only(
                right: 10.w,
                left: 10.w,
              ),
              child: Column(
                children: [
                  Expanded(
                    child: Container(
                      width: double.infinity,
                      child: Image.network(
                        // pic
                        "${Config.domain}${_hotProductList[index].pic.replaceAll('\\', '/')}",
                        fit: BoxFit.fill,
                        loadingBuilder: (context, child, loadingProgress) {
                          if (loadingProgress == null) {
                            return child;
                          }
                          return Container(
                            color: Colors.grey[100],
                            height: 200,
                            child: Center(
                              child: LoadingWidget(),
                            ),
                          );
                        },
                        errorBuilder: (context, error, stackTrace) {
                          print('Image load error: $error');
                          return Container(
                            color: Colors.grey[100],
                            height: 200,
                            child: Icon(Icons.error_outline, color: Colors.red),
                          );
                        },
                      ),
                    ),
                  ),
                  SizedBox(height: 5.h),
                  Text(
                    "¥${_hotProductList[index].price}",
                    style: TextStyle(
                      color: Colors.red,
                      fontSize: 16.w,
                    ),
                  ),
                  // Text(
                  //   "${_hotProductList[index].title}",
                  //   maxLines: 1,
                  //   overflow: TextOverflow.ellipsis,
                  //   style: TextStyle(
                  //     fontSize: 14.w,
                  //     color: Colors.black54,
                  //   ),
                  // )
                ],
              ),
            );
          },
          itemCount: _hotProductList.length,
        ),
      );
    } else {
      return Container(
        height: 300.h,
        child: LoadingWidget(),
      );
    }
  }

  Widget recProductListWidget() {
    return Container(
      padding: EdgeInsets.all(10.w),
      child: Wrap(
        runSpacing: 10,
        spacing: 10,
        children: _bestProductList.map((value) {
          return _recProductItemWidget(
              value.pic, value.price, value.oldPrice, value.title);
        }).toList(),
      ),
    );
  }

  Widget _recProductItemWidget(
      String pic, int price, String oldPrice, String title) {
    return Container(
      padding: EdgeInsets.all(10.w),
      width: (ScreenUtil().screenWidth - 30.w) / 2,
      decoration: BoxDecoration(
        border: Border.all(
          color: Color.fromRGBO(233, 233, 233, .9),
          width: 1,
        ),
      ),
      child: Column(
        children: [
          Container(
            width: double.infinity,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.all(Radius.circular(10.w)),
            ),
            clipBehavior: Clip.antiAlias,
            child: Image.network(
              "${Config.domain}${pic.replaceAll('\\', '/')}",
              fit: BoxFit.cover,
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress == null) {
                  return child;
                }
                return Container(
                  color: Colors.grey[100],
                  height: 200,
                  child: Center(
                    child: LoadingWidget(),
                  ),
                );
              },
              errorBuilder: (context, error, stackTrace) {
                return Container(
                  color: Colors.grey[100],
                  height: 200,
                  child: Icon(Icons.error_outline, color: Colors.red),
                );
              },
            ),
          ),
          Text(
            title,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            style: TextStyle(
              fontSize: 14.w,
              color: Colors.black54,
            ),
          ),
          Padding(
            padding: EdgeInsets.only(top: 10.w),
            child: Stack(
              children: [
                Align(
                  alignment: Alignment.topLeft,
                  child: Text(
                    "¥$price",
                    style: TextStyle(
                      color: Colors.red,
                      fontSize: 16.w,
                    ),
                  ),
                ),
                Align(
                  alignment: Alignment.topRight,
                  child: Text(
                    "¥$oldPrice",
                    style: TextStyle(
                      color: Colors.black12,
                      fontSize: 16.w,
                      decoration: TextDecoration.lineThrough,
                    ),
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        _SwiperWidget(),
        SizedBox(
          height: 10,
        ),
        titleWidget("猜你喜欢"),
        SizedBox(
          height: 10,
        ),
        _ListHorizontal(),
        SizedBox(
          height: 10,
        ),
        titleWidget("热门推荐"),
        recProductListWidget()
      ],
    );
  }

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

其实可以根据代码来看 uniapp是最容易适合开发上手的 对于前端工程师来说几乎零成本 开发reactNative可能在对应的业务需求上需要具备一定的原生基础,flutter则需要具备学习成本

相关推荐
MrTan1 小时前
Uni-App 鸿蒙应用微信相关功能上架踩坑:自制微信安装检测插件
uni-app·harmonyos
旧时光_2 小时前
第5章:容器类组件 —— 5.1 填充(Padding)
flutter
2501_916007472 小时前
苹果应用商店上架的系统逻辑,从产品开发到使用 开心上架 上架IPA 交付审核流程
android·ios·小程序·https·uni-app·iphone·webview
renxhui2 小时前
Flutter 基础控件速查(面向 Android 开发者)
flutter
IT 前端 张2 小时前
Uni-app 实现全局无操作监听:自动退出弹窗倒计时功能
运维·服务器·uni-app
一只月月鸟呀2 小时前
使用node和@abandonware/bleno写一个ble模拟设备,用Uni-app模拟终端连接
uni-app
A懿轩A2 小时前
【2025版 OpenHarmony】 GitCode 口袋工具:Flutter + Dio 网路请求 打造随身的鸿蒙版 GitCode 搜索助手
windows·flutter·华为·鸿蒙·openharmony·开源鸿蒙
QuantumLeap丶3 小时前
《Flutter全栈开发实战指南:从零到高级》- 20 -主题与国际化
flutter·ios·前端框架
初遇你时动了情3 小时前
vue3 ts uniapp基本组件封装、通用组件库myCompont、瀑布流组件、城市选择组件、自定义导航栏、自定义底部菜单组件等
typescript·uni-app·vue3