likes
comments
collection
share

Taro小程序的部分问题汇总

作者站长头像
站长
· 阅读数 49

最近在写 Taro 框架 vue 版的小程序项目,轻总结下,部分代码由同事提供。部分问题是小程序的通用问题。

新建 vue 文件,注意的问题有哪些

  • 页面的话,尽量用view标签阔起来
  • 引入 vue 组件的话,直接引入就好,引入的时候大写
  • style最好加moduleclass命名也需要加style
  • 需要导出
<template>
  <view :class="style['page']">
    <app-item></app-item>
    <!-- ... -->
  </view>
</template>

<script setup lang="ts">
import AppItem from './components/app-item/index.vue';
import { defineProps, defineEmits, watch, ref } from 'vue';
const props = defineProps<{
  icon: string;
  modelValue: string;
}>();
const emit = defineEmits(['update:modelValue']);
const innerValue = ref(props.modelValue);
watch(innerValue, (newVal) => {
  emit('update:modelValue', newVal);
});
watch(props.modelValue, (newVal) => {
  innerValue.value = newVal;
});
</script>

<script lang="ts">
export default { name: 'parent-control' };
</script>
<style module="style" lang="less">
.page {
  padding: 0 40px 40px;
  .title {
    font-size: 40px;
  }
}
</style>

新建页面 - 包小的话,直接配置在 pages

  1. src/app.config.ts 添加配置

  2. pages 里添加页面文件夹

    1. index.config.ts 有标题配置等
    2. index.vue

新建页面 - 包大的话配置在别的文件夹

subPackages: [
  {
    root: 'packageA',
    pages: ['pages/ch/index'],
  },
];
// 页面跳转的话,/packageA/pages/ch/index

动态 class

  • 数组模式,<view :class="[style['btn'], has && style['plain']]">
  • 对象模式,<view :class="{ [style['btn']]: true, [style['plain']]: has, }">

怎么修改小程序的原生样式

才知道,小程序还有另一种控制台,可以显示小程序标签的具体类名。

Taro小程序的部分问题汇总

点开之后,就显示整个软件界面的控制台,而模拟器里具体的页面是个webview

所以,在新打开的控制台,console面板先输入document.querySelectorAll('webview'),然后看下对应模拟器显示的是哪个webview,在输入document.querySelectorAll('webview')[1].showDevTools(true),就会打开对应webview的控制台啦

Taro小程序的部分问题汇总

看到原生类名之后,修改样式就so easy啦~

怎么切换网络

Taro小程序的部分问题汇总

怎么返回上一页并刷新数据

如果是原生小程序的代码,onShow 里进行请求就好。 这里使用下 Taro 的钩子:

import { useDidShow } from '@tarojs/taro';
useDidShow(() => {
  // 重新请求可能更改状态的数据
  initRequest();
});

标题乱码的时候,decodeURI

Taro.setNavigationBarTitle({
  title: decodeURI(routeParams.title || ''),
});

路由必备字段 没有就返回上一页

// 没有appCode表示非法进来,直接返回
if (!appCode) {
  Taro.navigateBack();
}

怎么修改小程序的单选

v-model本质上就是 modelValueupdate:modelValue,类似于 sync语法糖。

<template>
  <view>
    <radio-group class="tab-list" @change="change">
      <label
        v-for="({ name, value, checked }, index) in optionsCopy"
        :class="{ checked: checked, 'radio-item': true }"
        :key="index"
      >
        <radio hidden class="radio-native" :value="value" :checked="checked" />
        {{ name }}
      </label>
    </radio-group>
  </view>
</template>
<script setup lang='ts'>
// 写个单选组件,一个是精选,一个是分类

import { defineProps, defineEmits, watch, ref } from 'vue';

interface Props {
  modelValue: string | number;
  options: { name: string; value: string | number; checked?: boolean }[];
}

const props = defineProps<Props>();
// emit model事件
const emit = defineEmits(['update:modelValue', 'delete']);
const radioValue = ref(props.modelValue);
watch(radioValue, (newVal) => {
  console.log(newVal);
  emit('update:modelValue', newVal);
});
const optionsCopy = ref([...props.options]);
console.log(props, emit);
const change = (e) => {
  radioValue.value = e.detail.value;
  for (let i = 0, len = optionsCopy.value.length; i < len; ++i) {
    optionsCopy.value[i].checked = optionsCopy.value[i].value === e.detail.value;
  }
};
</script>
<script lang='ts'>
export default { name: 'TabList' };
</script>
<style lang="less" >
.tab-list {
  display: flex;
  justify-content: center;
}
.radio-native {
  display: none;
}
.radio-item {
  font-size: 44px;
  font-weight: 500;
  color: #606192;
  line-height: 60px;
  margin-left: 48px;
}
.checked {
  font-size: 48px;
  font-weight: 500;
  color: #7867e5;
  line-height: 64px;
  position: relative;
  &::after {
    content: '';
    width: 32px;
    height: 16px;
    background: #f69;
    position: absolute;
    top: 68px;
    margin-left: -50%;
    transform: translateX(-50%);
  }
}
</style>

上拉加载 和 下拉刷新

useBottomLoad.ts

通用上拉加载的通用钩子函数:

import { useReachBottom } from '@tarojs/taro';
import { reactive, onMounted, ref } from 'vue';
import { isMock, sleep } from '@/utils/utils';
import { mockList } from './mock';
/**
 * @description: 触底加载
 * @param {Function} apiList 请求列表的api
 * @param {Object} otherApiParams 其他api参数
 * @return {Object} list 列表数据
 * @return {Object} page 页码相关参数
 * @return {Object} isApiErr api有没有错误
 * @return {Function} requestList api请求
 * @example
 * const { list, page, isApiErr, requestList } = useBottomLoad(apiList, otherApiParams)
 */
export function useBottomLoad(apiList, otherApiParams) {
  // 初始值
  const initValue = { pageNo: 1, pageSize: 10, finish: false, loading: true };
  // 显示的列表
  const list = ref<any[]>([]);
  // 页码相关参数
  const page = reactive({ ...initValue });
  // 触底,请求下一页
  useReachBottom(() => {
    console.log('useReachBottom', page.finish);
    if (page.finish) return;
    page.pageNo++;
    requestList();
  });
  // 页面初始加载
  onMounted(() => {
    requestList();
  });
  // api有没有错误
  const isApiErr = ref(false);

  // api请求
  const requestList = async (isInit = false) => {
    // 加个初始化参数
    if (isInit) {
      page.pageNo = initValue.pageNo;
      list.value = [];
    }
    const params = {
      pageSize: page.pageSize,
      pageNo: page.pageNo,
      ...otherApiParams,
    };

    page.loading = true;
    isMock && (await sleep(3000));
    const { success: isSuccess, data } = isMock
      ? { success: true, data: mockList }
      : await apiList(params);
    page.loading = false;
    console.log('useReachBottom', data);
    if (!isSuccess) {
      page.pageNo = page.pageNo <= 1 ? 1 : page.pageNo--;
      isApiErr.value = true;
      return;
    }
    isApiErr.value = false;
    list.value = [...list.value, ...(data.rows || [])];
    page.finish = data.rows.length < page.pageSize;
  };
  return { list, page, isApiErr, requestList };
}

utils.ts

export function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
export const isMock = Taro.getStorageSync('mock');

mock.ts

export const mockList = {
  pageNo: 1,
  pageSize: 20,
  pages: 1,
  total: 1,
  rows: [
    {
      icon: 'https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/demo.png',
      appName: '1支付宝',
      size: '100M',
      installStatus: 1,
      appId: 1,
    },
  ],
};

页面使用

<template>
  <view :class="style['page']">
    <view :class="style['content']">
      <app-install-item v-for="(item, index) in list" :key="index" :info="item" />
    </view>
    <Loading v-if="!page.finish && page.loading" />
  </view>
</template>

<script setup lang="ts">
import Taro, { useRouter, usePullDownRefresh, useDidShow } from '@tarojs/taro';
import { ref } from 'vue';
import AppInstallItem from '@/components/app-install-item/index.vue';
import { apiList } from '@/packageControl/pages/app-install-list/service';
import { useBottomLoad } from './useBottomLoad';
import Loading from '@/components/loading.vue';
const paramsQuery = useRouter().params;
const catCode = paramsQuery.code;

const { list, isApiErr, requestList, page } = useBottomLoad(apiList, {
  category: catCode,
  pageSize: 10,
});

// 下拉刷新
usePullDownRefresh(() => {
  requestList(true);
  // 不会主动停止刷新,需要手动停止刷新
  setTimeout(() => {
    Taro.stopPullDownRefresh();
  }, 1000);
});

// 这里防止返回的时候,部分状态发生变化,所以加一个监听
useDidShow(() => {
  requestList(true);
});
</script>
<script lang="ts">
export default { name: 'app-install-list' };
</script>
<style module="style" lang="less">
.page {
  padding: 40px;
}
</style>

上拉加载

上拉加载,需要在页面的 config 里配置下:

export default definePageConfig({
  onReachBottomDistance: 600,
  enablePullDownRefresh: true,
});

部分页面的通用逻辑封装

Taro小程序的部分问题汇总

通用逻辑封装成组件:

<template>
  <view class="page-box">
    <ErrorHint
      v-if="isOffline"
      :imgUrl="require('@/assets/images/nonet.png')"
      @retry="initRequest"
      errorText="网络断开"
    ></ErrorHint>
    <!-- 有网,继续 -->
    <template v-else>
      <Loading v-if="isLoading" />
      <!-- 请求之后 -->
      <template v-else>
        <ErrorHint v-if="isErrorResult" @retry="initRequest" errorText="网络错误"></ErrorHint>
        <!-- 成功请求结果 -->
        <template v-else>
          <Empty v-if="isEmptyData" text="暂无数据"></Empty>
          <view v-else class="normal-page"> <slot :info="info"></slot> </view>
        </template>
      </template>
    </template>
  </view>
</template>

<script setup lang="ts">
/**
 * 没有网络、加载中、请求失败、请求成功但是数据为空、请求成功且有数据 统一合并为这个组件
 * isOffline 是否断网
 * isLoading 是否加载中
 * isErrorResult 请求结果是否失败
 * isEmptyData 请求结果是否为空
 * info 请求结果
 * @param initRequest 请求函数
 * 使用方法:
 import PageCommon from '@/components/page-common.vue';

 const initRequest = async () => {
  return {
    success: true,
    data: {x:1},
  };
};

  <page-common :initRequest="initRequest">
    <template v-slot:default="{ info }">
      <view :class="style['page']">
      </view>
    </template>
  </page-common>
    

 */
import { useNetWork } from '@/utils/hooks/useNetWork';
import Loading from '@/components/loading.vue';
import Empty from '@/components/empty.vue';
import ErrorHint from '@/components/error-hint.vue';
import { ref, onMounted, defineProps } from 'vue';
const props = defineProps<{
  initRequest: () => Promise<{ success: boolean; data: any; message?: string }>;
}>();

const info = ref();
const isErrorResult = ref(false);
const isEmptyData = ref(false);

// 1. (请求之前)判断有没有网路 - 没有网显示错误网络页面,有网继续往下走
const { showErrNet: isOffline } = useNetWork(props.initRequest);
// 2. 发出请求 - 结果回来之前,显示加载页面,结果回来之后,判断结果是否成功,成功继续往下走,失败显示错误页面
const isLoading = ref(true);
onMounted(async () => {
  const { success, data } = await props.initRequest();
  console.log(' success, data', success, data);
  isLoading.value = false;
  if (!success) {
    isErrorResult.value = true;
    return;
  }
  if (!data || (Array.isArray(data) && data.length === 0)) {
    isEmptyData.value = true;
    return;
  }
  info.value = data;
});
</script>
<script lang="ts">
export default { name: 'PageCommon' };
</script>

useNetwork

import { ref, onMounted, computed } from 'vue';
import Taro from '@tarojs/taro';
type netWork = 'offline' | 'online';

export const useNetWork = (reload) => {
  const netWorkStatus = ref<netWork>('online');
  onMounted(() => {
    // 进页面获取网络状态
    Taro.getNetworkType({
      success: (res) => {
        if (['unkonw', 'none'].includes(res.networkType)) {
          netWorkStatus.value = 'offline';
        } else {
          netWorkStatus.value = 'online';
        }
      },
    });
    // 监听网络状态变化
    Taro.onNetworkStatusChange(function (res) {
      console.log(res.isConnected);
      if (netWorkStatus.value === 'offline' && res.isConnected) {
        Taro.showToast({
          title: '网络已恢复',
          icon: 'none',
        });
        reload && reload();
      }
      netWorkStatus.value = res.isConnected ? 'online' : 'offline';
    });
  });
  const showErrNet = computed(() => {
    return netWorkStatus.value === 'offline';
  });
  return { showErrNet };
};

loading.vue

<template>
  <view class="loading">
    <view class="flower-loading">
      <view class="dot"></view>
      <view class="dot2 dot"></view>
      <view class="dot3 dot"></view>
      <view class="dot4 dot"></view>
      <view class="dot5 dot"></view>
      <view class="dot6 dot"></view>
      <view class="dot7 dot"></view>
      <view class="dot8 dot"></view>
      <view class="dot9 dot"></view>
      <view class="dot10 dot"></view>
      <view class="dot11 dot"></view>
      <view class="dot12 dot"></view>
    </view>
    <view class="center-text">加载中...</view>
  </view>
</template>

<script setup lang="ts"></script>
<script lang="ts">
export default { name: 'index-loading' };
</script>
<style lang="less">
.loading {
  display: flex;
  justify-content: center;
  align-items: center;
  .flower-loading {
    position: relative;
    width: 30px;
    height: 30px;

    .dot {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      &::before {
        content: '';
        display: block;
        margin: 0 auto;
        width: 3px;
        height: 25%;
        background-color: #666;
        border-radius: 40%;
        -webkit-animation: dots 1.2s infinite ease-in-out both;
        animation: dots 1.2s infinite ease-in-out both;
      }
    }
    .dot2 {
      transform: rotate(30deg);
      &::before {
        -webkit-animation-delay: -1.1s;
        animation-delay: -1.1s;
      }
    }
    .dot3 {
      transform: rotate(60deg);
      &::before {
        -webkit-animation-delay: -1s;
        animation-delay: -1s;
      }
    }
    .dot4 {
      transform: rotate(90deg);
      &::before {
        -webkit-animation-delay: -0.9s;
        animation-delay: -0.9s;
      }
    }
    .dot5 {
      transform: rotate(120deg);
      &::before {
        -webkit-animation-delay: -0.8s;
        animation-delay: -0.8s;
      }
    }
    .dot6 {
      transform: rotate(150deg);
      &::before {
        -webkit-animation-delay: -0.7s;
        animation-delay: -0.7s;
      }
    }
    .dot7 {
      transform: rotate(180deg);
      &::before {
        -webkit-animation-delay: -0.6s;
        animation-delay: -0.6s;
      }
    }
    .dot8 {
      transform: rotate(210deg);
      &::before {
        -webkit-animation-delay: -0.5s;
        animation-delay: -0.5s;
      }
    }
    .dot9 {
      transform: rotate(240deg);
      &::before {
        -webkit-animation-delay: -0.4s;
        animation-delay: -0.4s;
      }
    }
    .dot10 {
      transform: rotate(270deg);
      &::before {
        -webkit-animation-delay: -0.3s;
        animation-delay: -0.3s;
      }
    }
    .dot11 {
      transform: rotate(300deg);
      &::before {
        -webkit-animation-delay: -0.2s;
        animation-delay: -0.2s;
      }
    }
    .dot12 {
      transform: rotate(330deg);
      &::before {
        -webkit-animation-delay: -0.1s;
        animation-delay: -0.1s;
      }
    }
  }
  @-webkit-keyframes dots {
    0%,
    39%,
    100% {
      opacity: 0.4;
    }
    40% {
      opacity: 0.8;
    }
  }
  @keyframes dots {
    0%,
    39%,
    100% {
      opacity: 0.4;
    }
    40% {
      opacity: 0.8;
    }
  }
  .center-text {
    color: #666;
    padding-left: 4px;
    font-size: 24px;
  }
}
</style>

empty.vue

<template>
  <view class="empty-error-page" id="pageError">
    <image class="img" :src="imgUrl || defaultUrl" />
    <view class="des">{{ text }}</view>
  </view>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';
const defaultUrl = 'https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/empty_undone@2x.png';
defineProps({
  text: {
    type: String,
  },
  imgUrl: {
    type: String,
    default: '',
  },
});
</script>
<script lang="ts">
export default { name: 'empty-index' };
</script>
<style lang="less">
.empty-error-page {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 8;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  text-align: center;
  image {
    width: 240px;
    height: 240px;
  }
  .des {
    font-size: 28px;
    color: #999;
  }
}
</style>

error-hint.vue

<template>
  <view :class="style['error-hint']" :style="{ background: props.background }">
    <image :class="style['img']" :src="props.imgUrl || defaultImg" />
    <view :class="style['error-text']">{{ props.errorText }}</view>
    <view :class="style['retry-btn']" @tap="retry">{{ props.retryText }}</view>
  </view>
</template>

<script lang="ts" setup>
import { withDefaults, defineProps } from 'vue';

interface ErrorHintEmit {
  (event: 'retry'): void;
}

const defaultImg =
  'https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/empty_error_hint@2x.png';

const props = withDefaults(
  defineProps<{
    errorText?: string;
    retryText?: string;
    imgUrl?: string;
    background?: string;
  }>(),
  {
    errorText: '网络异常',
    retryText: '点击重试',
    imgUrl: '',
    background: '#fafafa',
  },
);

const emit = defineEmits<ErrorHintEmit>();

const retry = () => {
  emit('retry');
};
</script>

<script lang="ts">
export default { name: 'error-hint' };
</script>

<style lang="less" module="style">
.error-hint {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  padding-bottom: 40px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  z-index: 9999;

  .img {
    width: 240px;
    height: 240px;
  }
  .error-text {
    font-size: 28px;
    color: #999;
    text-align: center;
    margin-top: 32px;
  }
  .retry-btn {
    width: 238px;
    height: 64px;
    background: #9192ff;
    border-radius: 44px;
    font-size: 28px;
    font-weight: 400;
    color: #ffffff;
    line-height: $height;
    text-align: center;
    margin-top: 32px;
  }
}
</style>
转载自:https://juejin.cn/post/7236263262071849018
评论
请登录