首先我个人认为 这三个框架都有自己的好处 都是跨平台的框架
如果当前团队是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则需要具备学习成本