uniapp开发小程序

       这一篇主要介绍小程序开发所用的前端技术,为后续的实际应用做好铺垫,上节课学习的vue3和typescript,在此次uniapp小程序开发中将大有所用。

根据涉及的知识点,将从以下几个方面来学习:

项目框架

       项目采用vite、vue3、typescript、setup、uniapp等技术,ui框架采用uview-plus下面将做详细的介绍。

UI框架

       UI框架先介绍下uview-plus,uview-plus官方网站,虽举例用的vue3但最底部git源代码则是vue2方式,而本文将以vue3 setup写法来举例<script setup lang="ts"></script>,安装步骤参照官网中npm的方式和配置流程。下面先以常用的表单为例,介绍下如何使用uview-plus的ui组件,代码在支付宝小程序上运行效果如图1所示:(提示:以reactive解构获取属性变量时,除对象、数组属性外,如number、string等变量属性,在reactive解构时会丢失响应性

<template>
  <view class="main-content">
    <up-form
      labelPosition="left"
      :model="state.formModel"
      :rules="state.rules"
      ref="formRef"
    >
      <up-form-item
        label="姓名"
        prop="name"
        borderBottom
      >
        <up-input
          v-model="state.formModel.name"
          border="none"
        ></up-input>
      </up-form-item>
      <up-form-item
        label="性别"
        prop="sex"
        borderBottom
        @click="state.showSex = true;"
      >
        <up-input
          v-model="state.formModel.sex"
          disabled
          disabledColor="#ffffff"
          placeholder="请选择性别"
          border="none"
        ></up-input>
        <template #right>
          <up-icon
            name="arrow-right"
          ></up-icon>
        </template>
      </up-form-item>
      <up-button type="primary" @click="submit()">确认</up-button>
    </up-form>
    <up-action-sheet
      :show="state.showSex"
      :actions="state.actions"
      title="请选择性别"
      description="如果选择保密会报错"
      @close="state.showSex = false"
      @select="sexSelect"
    >
    </up-action-sheet>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive } from "vue";
import type UniFormRef from "uview-plus";

const state = reactive({
  showSex: false,
  formModel: {
    name: 'uview-plus UI',
    sex: ''
  },
  actions: [
    { name: '男' },
    { name: '女' },
    { name: '保密' },
  ],
  rules: {
    'name': {
      type: 'string',
      required: true,
      message: '请填写姓名',
      trigger: ['blur', 'change'],
    },
    'sex': {
      type: 'string',
      max: 1,
      required: true,
      message: '请选择男或女',
      trigger: ['blur', 'change'],
    },
  },
  radio: '',
  switchVal: false,
});
const formRef = ref<UniFormRef | null>(null);

// 定义方法
function sexSelect(e: any) {
  state.formModel.sex = e.name;
  if (formRef.value) {
    (formRef.value as any).validateField("sex");
  }
}

function submit() {
  formRef.value?.validate().then((valid: any) => {
    if (valid) {
      uni.$u.toast(JSON.stringify(state.formModel));
    } else {
      uni.$u.toast("校验失败");
    }
  });
}

</script>

<style lang="scss">
.main-content {
  padding: 0 40rpx;
}
</style>

图1

       在上面的基础上,再介绍下uview-plus中的"加载更多"组件的使用,它在后续的列表和数据加载中会经常使用到,对up-loadmore组件封装成XtxGuess.vue组件如下,使'加载更多'组件功能上手即用:


<script setup lang="ts">
import { reactive, ref, withDefaults } from 'vue'
import type { GoodsItem } from '@/types/global'
import { onReachBottom, onReady } from '@dcloudio/uni-app'
import { getHomeGoodsGuessLikeAPI } from '@/services/home'

const props = withDefaults(defineProps<{
  busType?: string,
  pagination: {
    pages: number
    pageNo: number,
    pageSize: number
  },
  apiFunc?: (params: { page: number, pageSize: number }) => Promise<any>
}>(),{
  apiFunc: getHomeGoodsGuessLikeAPI
});

const more = ref();
const loadmoreText = ref('加载更多');
const emits = defineEmits(["queryMore"]);

let list: GoodsItem[] = reactive([]);
const STATUS = {IDLE: 'loadmore',  LOADING: 'loading',  NO_MORE: 'nomore'};
let status = ref(STATUS.IDLE);
/***
 *计算页数
 *page
 */
const calculatePages = (total: number, pageSize: number) => {
  if (total === 0 || pageSize === 0) {
    return 0
  }
  return Math.ceil(total / pageSize)
}
const initFuc = async () => {
  if (status.value === STATUS.LOADING) return;
  // 将状态设置为'loading'
  status.value = STATUS.LOADING;
  await props.apiFunc({ page: props.pagination.pageNo, pageSize: props.pagination.pageSize }).then(res=>{
    const pages = calculatePages(res.result.counts, props.pagination.pageSize);
    list.push(...res.result.items);
    emits("queryMore", pages);
  }).catch(e=>{
    status.value = STATUS.IDLE;
    loadmoreText.value = "重新加载"
  });
  status.value = STATUS.IDLE;
}

const query = async () => {
  // 如果页码大于总页数,设置状态为'nomore'并返回
  if (props.pagination.pages > 0 && props.pagination.pageNo > props.pagination.pages) {
    status.value = STATUS.NO_MORE;
    return;
  }
  await initFuc();
}
onReady(async () => {
  await query()
})
onReachBottom(async () => {  await query()  })

</script>
<template>
  <view class="guess">
    <!--    列表类型-->
    <template v-if="busType=='list'">
      <navigator
        class="guess-item guess-item1"
        v-for="item in list"
        :key="item.id"
        :url="`/pages/goods/goods?id=${item.id}`"
      >
        <view> {{ item.name }} </view>
      </navigator>
    </template>
    <!--    其它类型-->
    <template v-else>
      <navigator
        class="guess-item"
        v-for="item in list"
        :key="item.id"
        :url="`/pages/goods/goods?id=${item.id}`"
      >
        <image class="image" mode="aspectFill" :src="item.picture"></image>
        <view class="name"> {{ item.name }} </view>
        <view class="price">
          <text class="small">¥</text>
          <text>{{ item.price }}</text>
        </view>
      </navigator>
    </template>
    <up-loadmore ref="more" :loadmore-text="loadmoreText" :status="status" icon-color="#028BF7" marginTop="20" />
  </view>
</template>

<style lang="scss">
:host {
  display: block;
}
/* 猜你喜欢 */
.guess {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  padding: 0 20rpx;
  .guess-item {
    width: 345rpx;
    padding: 24rpx 20rpx 20rpx;
    margin-bottom: 20rpx;
    border-radius: 10rpx;
    background-color: #fff;
  }
  .guess-item1{
    width: 100%;
  }
  .image {
    width: 304rpx;
    height: 304rpx;
  }
  .name {
    height: 75rpx;
    margin: 10rpx 0;
    font-size: 26rpx;
    color: #262626;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
  .price {
    line-height: 1;
    padding-top: 4rpx;
    color: #cf4444;
    font-size: 26rpx;
  }
  .small {
    font-size: 80%;
  }
}
</style>
使用:let pagination = reactive({ pageNo: 1, pageSize: 20, pages: 0 });getMemberOrderAPI即对应接口名称的promise
<XtxGuess busType="list" :api-func="getMemberOrderAPI" :pagination="pagination" @queryMore="(p)=>{pagination.pageNo++; pagination.pages = p;}">
</XtxGuess>
路由page.json文件

        在easycom中配置uview-plus内容,工程里所有的页面在pages属性里面定义,小程序中的下导航tab在属性tabBar中定义,全局样式在globalStyle属性中编写。

{
  "easycom": {
    "autoscan": true,
    "custom": {
      "^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
      "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
      "^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
    }
  },
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/my/index",
      "style": {
        "navigationBarTitleText": "我的"
      }
    },
    {
      "path": "pages/componentsB/tag/index",
      "style": {
        "navigationBarTitleText": "测试"
      }
    },
    {
      "path": "pages/componentsB/alipay/index",
      "style": {
        "navigationBarTitleText": "支付宝授权登录"
      }
    }
  ],
  "tabBar": {
    "color": "#333333",
    "selectedColor": "#207DFF",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [{
      "pagePath": "pages/index/index",
      "text": "首页",
      "iconPath": "/static/tabBar/un_home.png",
      "selectedIconPath": "/static/tabBar/home.png"
    }, {
      "pagePath": "pages/my/index",
      "text": "我的",
      "iconPath": "/static/tabBar/un_user.png",
      "selectedIconPath": "/static/tabBar/user.png"
    }]
  },
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#ffffff",
    "backgroundColor": "#ffffff"
  }
}
变量存储

       比如项目中需要存储用户登录和授权状态,此时就需要考虑到状态变量存储,我们使用vuex来管理状态变量。

接口请求
    uni自带http,uni.$u.http中有get和post方法直接可用无需封装。
提示:项目中可以直接使用uni.$u,需要你配置相应的type,为了方便使用支付宝小程序自带my变量,my变量类型约束也需要一起定义下,在src下env.d.ts文件中写入如下类型约束即可
const my: any;
interface Uni { $u: any };
declare module 'uview-plus';
授权登录

       不同的小程序比如微信、支付宝小程序授权登录流程稍有不同,我们以支付宝小程序为例-授权需另起页面,在首页中添加如下代码,跳转到支付宝小程序授权页,页面效果如图2

uni.getProvider({
  service: "oauth", //service	String	服务类型  oauth授权登录 share分享 payment支付 push推送
  success: function(res) { 
    //判断是否完成授权登陆
    const isLogin = false;
    const provider = res.provider[0] as "weixin"||"alipay";
    if (isLogin) {
      uni.getUserInfo({
        provider: provider,
        success: (res) => {
          const userInfo = res.userInfo;
          console.log(userInfo);
        },
        fail: (error) => {
          console.log(error);
        }
      });
    } else {
      // #ifdef MP-ALIPAY
      uni.navigateTo({
        url: "/pages/componentsB/alipay/index"
      });
      // #endif

      // #ifdef MP-WEIXIN
      uni.login({
        provider: provider,
        success: function(loginRes) {
          console.log("uniapp登录凭证", loginRes);
        }
      });
      // #endif
    }
  }
});

       支付宝小程序授权页面详细代码如下,根据获取到的authCode,调用后台接口完成授权登录,返回用户基础信息。

<template>
  <view class="page">
    <view class="page-section">
      <view>请不要一进入小程序就弹框授权,建议先了解小程序的服务内容</view>
      <button class="signin-btn" open-type="getUserInfo" @click="getAuthCode">授权登录</button>
    </view>
  </view>
</template>
<script setup lang="ts">
function getAuthCode() {
  my.getAuthCode({
    scopes: 'auth_user',
    success: (authCode:string) => {
      //根据authCode完成后续操作
    },
  });
}
</script>

<style scoped lang="scss">
.page {
  font-family: -apple-system-font,Helvetica Neue,Helvetica,sans-serif;
  font-size: 24rpx;
  padding: 32rpx;
  flex: 1;
  .page-section {
    background: #fff;
    margin-bottom: 32rpx;
    button {
      margin-top:20rpx;
      margin-bottom:20rpx;
      background-color: #49A9EE;
      color: white;
    }
  }
}
</style>

图2

实际开发

       以'点击更多'跳转到选择附近的地址为例,页面布局上面是搜索栏,下面是地图和地址选择框,假设需求设计稿如图3所示,下面将介绍如何开发这样的一个需求(门店列表),涉及的知识点有页面布局、地图使用、地址选择框的封装、点击联系店铺调起打电话、定位、搜索框等:

1使用地图

       首先在高德开发者平台创建应用,选择web服务 切记不是微信小程序,因为在获取到经纬度以后需要调用高德地图web服务(https://restapi.amap.com接口)来反推出当前详细地址,而此时用真机调试上述接口会出现未配置域名白名单错误,点击链接控制台首页 - 开放平台 (alipay.com),在开发设置中配置服务器域名白名单可解决此问题,高德地图web服务API接口需要在真机上才起作用,如下图所示:

详细代码如下,其中scrollView.vue即上面封装的加载更多组件

<template>
  <view class="address">
    <view class="search-top">
      <up-search v-if="!focusFlag"
                 :show-action="false"
                 placeholder="搜索地点"
                 v-model="condition"
                 @focus="inputFocus"
      >
      </up-search>
      <up-search v-else
                 :show-action="true"
                 actionText="取消"
                 :focus="true"
                 @custom="cancel()"
                 :animation="true"
                 v-model="condition"
                 @search="searchClick"
                 @clickIcon="searchClick"
      ></up-search>
    </view>
    <view class="search-map" v-show="!focusFlag">
      <map style="width:100%; height:100%" id="map" scale="13" :latitude="latitude" show-location :longitude="longitude"
           :markers="markers"></map>
    </view>
    <scrollView
                :list="list"
                :pagination="pagination"
                :pages="pages"
                @queryList="searchList">
      <view class="search-list" :class="item.checked?'selected-list':''" v-for="(item, index) in list" :key="index"
            @click="bindPointClick(item)">
        <view class="status">
          <image class="logo" src="../../../static/drk.png"/>
          <text class="yyz">营业中</text>
          <text class="picker">{{ item.cityname }}</text>
        </view>
        <view>
          <text class="name">{{ item.name }}</text>
        </view>
        <view>
          <text class="detail">{{ item.location + 'km' }}</text>
        </view>
        <view class="phone">
          <up-tag text="联系店铺" icon="phone-fill" plain size="mini" @click="callNumber" type="warning"></up-tag>
        </view>
      </view>
    </scrollView>
  </view>
</template>
<script setup lang="ts">
import {onMounted, reactive, ref} from "vue";
import scrollView from "../common/scrollView.vue";

interface Interface {
  name: string,
  location: string,
  checked: boolean,
  cityname: string
}

interface InterfaceMarkers {
  id: number,
  longitude: number,
  latitude: number,
  width: number,
  height: number,
  iconPath: string,
  customCallout: object
}

const focusFlag = ref(false);
const condition = ref("");
let list = ref<Interface[]>([]);
let markers = ref<InterfaceMarkers[]>([]);
let latitude = ref();
let longitude = ref();

let pagination = reactive({pageNo: 1, pageSize: 20});
let pages = ref(0);


const inputFocus = () => {
  focusFlag.value = true;
  pagination.pageNo = 1;
  list.value = [];
};
const cancel = () => {
  focusFlag.value = false;
  openMap({latitude: '', longitude: '', name: ''});
}
const searchClick = () => {
  inputFocus();
  searchList(pagination)
}
const searchList = async (params:any) => {
  await uni.$u.http.get('http://47.110.131.107/admin-api/drawdowns/records', {
    data: {
      current: params.pageNo,
      limit: params.pageSize,
      listType: 2
    }
  }).then((res: any) => {
    let data = res.data.data;
    pages.value = calculatePages(data.total, params.pageSize);
    list.value.push(...data.records);
  });
  //页面新增一页
  pagination.pageNo = ++pagination.pageNo;
};
const calculatePages = (total: number, pageSize: number) => {
  if (total === 0 || pageSize === 0) {
    return 0;
  }
  return Math.ceil(total / pageSize);
}


const callNumber = () => {
  uni.makePhoneCall({
    phoneNumber: "10086",
    success: () => {
      console.log("拨打电话成功!");
    },
    fail: () => {
      console.error("拨打电话失败!");
    }
  });
};

const bindPointClick = (e: any) => {
  openMap({latitude: e.latitude, longitude: e.longitude, name: e.name});
};
const truncateString = (str: string, len: number) => {
  if (str.length > len) {
    return str.substring(0, len).concat("...");
  } else {
    return str;
  }
};
const openMap = (params: { latitude: "", longitude: "", name: "" }) => {
  uni.getLocation({
    success: res => {
      longitude.value = res.longitude;
      latitude.value = res.latitude;
      uni.openLocation({
        latitude: latitude.value,
        longitude: longitude.value
      });
      my.httpRequest({
        url: "https://restapi.amap.com/v3/geocode/regeo?key=f941ea8ff6ea10c2b83db08ba5296f39&location=" + longitude.value + "," + latitude.value,
        method: "GET",
        success: function (res: any) {
          let address = res.data.regeocode.addressComponent.district + res.data.regeocode.addressComponent.township;
          markers.value = [{
            id: 3,
            latitude: params.latitude ? params.latitude : latitude.value,
            longitude: params.longitude ? params.longitude : longitude.value,
            width: 25,
            height: 31,
            iconPath: "https://gw.alipayobjects.com/mdn/rms_dfc0fe/afts/img/A*x9yERpemTRsAAAAAAAAAAAAAARQnAQ",
            "customCallout": {
              "type": 2,
              "descList": [{
                "desc": params.name ? truncateString(params.name, 20) : truncateString(address, 20),
                "descColor": "#000000"
              }],
              "isShow": 1
            }
          }];
          my.createMapContext("map").moveToLocation({
            latitude: params.latitude ? params.latitude : latitude.value,
            longitude: params.longitude ? params.longitude : longitude.value
          });
        },
        fail: function (err: any) {
          console.log("高德地图API调用失败", err);
        }
      });
    }
  });
};

onMounted(() => {
  openMap({latitude: "", longitude: "", name: ""});
})
</script>

<style scoped lang="scss">

.address {
  font-family: -apple-system-font, Helvetica Neue, Helvetica, sans-serif;
  font-size: 24rpx;
  padding: 32rpx;
  flex: 1;
  background: #fff;

  .search-top {
    background-color: #FFFFFF;
    padding: 8rpx 0 8rpx 0;
    display: flex;
    flex-direction: row;
    align-items: center;
  }

  .search-map {
    height: 600rpx;
  }

  .search-list {
    display: flex;
    flex-direction: column;
    width: 650rpx;
    height: 210rpx;
    margin-bottom: 14rpx;
    padding-left: 30rpx;
    padding-top: 30rpx;
    border: 1px solid #dcdcdc;
    border-radius: 14rpx;

    .status {
      display: flex;
      flex-direction: row;
      height: 48rpx;
      line-height: 48rpx;

      .yyz {
        background: #f6b62d;
        width: 78rpx;
        text-align: center;
        margin-top: 7rpx;
        height: 34rpx;
        line-height: 34rpx;
        color: white;
      }

      .picker {
        font-size: 28rpx;
      }

      .logo {
        width: 48rpx;
        height: 48rpx
      }
    }

    .phone {
      width: 180rpx;
      margin-top: 10rpx;
    }

    .name {
      font-size: 28rpx;
      margin-left: 46rpx;
    }

    .hook {
      font-size: 26rpx;
    }

    .detail {
      font-size: 24rpx;
      margin-left: 46rpx;
      color: #cecece;
    }

    .selected-list {
      border: 1px solid #fd8e02;
    }
  }
}

</style>

        因为在支付宝小程序上地图不支持cover-view嵌套使用,于是去官方网站上查看marker使用教程小程序文档 - 支付宝文档中心,实现了需求中的页面布局、地图使用、根据选择地址定位、拨打电话等功能,效果图如下图所示

打包发布

       打包,根据uni配置对应的小程序和app打包指令,如支付宝小程序代码,打包后代码在dist>mp-alipay文件夹下,发布,还没发过生产后续再重新补上流程。

GIT源代码

未完待续

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值