保险查询工具小程序总结(taro+vue3)

581 阅读8分钟

背景

团队是做保险业务相关,微信小程序的项目是要借鉴“绩效新书”微信公众号保险查询工具,包含保险搜索、利益演示、重疾病条款对比、保费对比等功能,需要爬虫、前后端以及算法等各方面去实现相关功能,服务人员主要是给内部保险业务人员使用。整个项目的技术调研子在年前开始,在年后开工之前也进行一部分调研。

技术方案

微信小程序的开发是基于京东的Taro框架,使用了vue3+pinia进行状态管理,UI库使用了配套的京东的nutui,图表库使用了字节跳动的@visactor/vchart。一般都是taro+react的方案,社区里面插件的数量来说,还是react部分多一些,但是整个前端组还是vue为主,很少会react,所以为了更容易维护采用了此种方案。 涉及到文件上传、个性化设置等内webview内嵌页面,统一由单独的web项目维护,使用了vite + vue3 + pinia,UI库选择vant,使用了px2vw保证移动端兼容性。

开发问题总结

自定义头部导航栏

在整个项目中,为了尽量贴合设计稿,需要在头部插入查询组件、背景图片、自定义标题的样式和位置,因为微信小程序的导航栏无法插入图片,组件,无法修改标题的样式,所以选择部分页面使用自定义导航栏组件 主要具体的实现思路有很多,可以查看具体相关文章,比如:【Taro开发】-自定义导航栏NavBar(五)

此处只是简单记录实现思路,所以不会讲的很详细,但是本质就是两点:

  • 系统的状态栏的高度的计算(刘海屏等异性屏的处理)
  • 头部导航栏的标题定位和右侧胶南留出宽度
  • 头部导航栏的扩展和自定义是否显示返回、回到主页等处理

解决方案:

  • 利用Taro的getSystemInfoSync()获取的statusBarHeight获取导航栏高度
  • 利用Taro的getMenuButtonBoundingClientRect()获取头部胶囊的位置和宽高,获取胶囊位置是为了右侧留出空白和计算整个标题的位置处理
  • 在前两步骤没有问题的情况下,可以通过slot和props来达到拓展的目的,同时为了方便重复计算,第一二的结果会保存在project级别的store中

代码实现:

<template>
<view
   id="basicNavBar"
   :class="[styles.basicNavBarContainer, state.configStyle.ios ? styles.ios : styles.android, props.class]"
   :style="{
     height: `${state.configStyle.navBarHeight + state.configStyle.navBarExtendHeight}px`
   }"
>
 <slot name="navbarBg"/>
 <view
     :class="styles.navBarInner"
     :style="{
       top: state.configStyle.contentGroupTop,
       height: state.configStyle.contentGroupHeight,
     }"
 >
   <view :class="styles.otherInnerGroup">
     <view
         v-if="!props.isDisplayNoneBack"
         :class="styles.leftGroup"
     >
       <Left
           v-if="!props?.customBackIcon"
           :class="[styles.goBackIcon,
            !isShowBackIcon && styles.hiddenIcon,
             isShowHomeIcon && styles.displayIcon
             ]"
           @click="handleBackClick"
       ></Left>

       <slot
           v-else
           name="backIcon"
       >
       </slot>

       <Home
           v-if="isShowHomeIcon"
           @click="handleGoHomeClick"
           :class="[styles.goHomeIcon]"
       >
       </Home>
     </view>

     <view :class="styles.middleGroup">
       <view v-if="props.title" :class="styles.title">
         {{ props.title }}
       </view>
       <slot name="title"></slot>
     </view>

     <view :class="styles.rightGroup">
       <slot name="right" />
     </view>
   </view>
 </view>
</view>
</template>

<script setup>
 import styles from './index.module.scss';
 import { getSystemInfo, isFunction } from './utils';
 import Taro from '@tarojs/taro';
 import {
   defineProps, defineEmits,
   reactive, computed,
 } from 'vue';
 import {Left, Home} from "@nutui/icons-vue-taro";
 import {useProjectStore} from "@/stores/project";

 const projectStore = useProjectStore();

 const emits = defineEmits([
     'back',
     'home',
 ])

 const props = defineProps({
   // 是否设置自定义回退,如果设置自定义回退,则直接使用插槽
   customBackIcon: {
     type: Boolean,
     default: false,
   },
   isDisplayNoneBack: {
     type: Boolean,
     default: false,
   },
   onHome: {
     type: Function,
     default: () => null,
   },
   extClass: {
     type: String,
     default: '',
   },
   class: {
     type: String,
     default: '',
   },
   background: {
     type: String,
     default: "#ffffff",
   }, //导航栏背景
   color: {
     type: String,
     default: "#000000",
   },
   title: {
     type: String,
     default: '',
   },
   back: {
     type: Boolean,
     default: false,
   },
   home: {
     type: Boolean,
     default: false,
   },
   iconTheme: {
     type: String,
     default: "black",
   },
   delta: {
     type: Number,
     default: 1,
   },
 })

 const isShowBackIcon = computed(() => {
   return (props?.back && !props?.home) || (props?.back && props?.home)
 })

 const isShowHomeIcon = computed(() => {
   return (!props?.back && props?.home) || (props?.back && props?.home);
 })

 // 直接获取全局状态处理
 let globalSystemInfo = getSystemInfo();

 const state = reactive({
   configStyle: setStyle(globalSystemInfo),
 })

 function setStyle(systemInfo) {
   const {
     statusBarHeight,
     navBarHeight,
     capsulePosition,
     navBarExtendHeight,
     ios,
     windowWidth
   } = systemInfo;
   let contentGroupTop = `${capsulePosition?.top || 0}px`;
   let contentGroupHeight = `${capsulePosition?.height || 0}px`;
   const { back, home, title, color } = props;
   let rightDistance = windowWidth - capsulePosition.right; //胶囊按钮右侧到屏幕右侧的边距
   let leftWidth = windowWidth - capsulePosition.left; //胶囊按钮左侧到屏幕右侧的边距

   let navigationbarinnerStyle = [
     `color:${color}`,
     `height:${navBarHeight + navBarExtendHeight}px`,
     `padding-top:${statusBarHeight}px`,
     `padding-right:${leftWidth}px`,
     `padding-bottom:${navBarExtendHeight}px`
   ].join(";");
   let navBarLeft = [];
   if ((back && !home) || (!back && home)) {
     navBarLeft = [
       `width:${capsulePosition.width}px`,
       `height:${capsulePosition.height}px`,
       `margin-left:0px`,
       `margin-right:${rightDistance}px`
     ].join(";");
   } else if ((back && home) || title) {
     navBarLeft = [
       `width:${capsulePosition.width}px`,
       `height:${capsulePosition.height}px`,
       `margin-left:${rightDistance}px`
     ].join(";");
   } else {
     navBarLeft = [`width:auto`, `margin-left:0px`].join(";");
   }
   // 强行更新一次高度
   projectStore.updateNavBarHeight(navBarHeight)
   return {
     contentGroupTop,
     contentGroupHeight,
     navigationbarinnerStyle,
     navBarLeft,
     navBarHeight,
     capsulePosition,
     navBarExtendHeight,
     ios,
     rightDistance
   };
 }

 /**
  * 作用: 自定义回退逻辑
  */
 function handleBackClick() {
   emits('back');
   const pages = Taro.getCurrentPages();
   if (pages.length >= 2) {
     Taro.navigateBack({
       delta: props.delta
     });
   }
 }

 /**
  * 作用: 自定义返回主页逻辑
  */
 function handleGoHomeClick() {
   emits('home');
   Taro.reLaunch({
     url: '/pages/index/index',
   });
 }
</script>

滚动吸顶效果处理

小程序吸顶交互.gif

受限于微信本身的渲染规则,在web层面一般的吸顶效果处理,在小程序上会出现一些奇怪的问题,比如position:sticky的定位问题,监听scroll进行js的样式处理导致实际真机滚动起来很卡顿(本质是小程序的视图层和逻辑层不断阻塞导致),在开发过程中,图中类似效果采用两种办法之后实际效果并不好,所以使用Taro.createIntersectionObserver()判断元素是否相交进行组件显示隐藏控制能达到丝滑的效果,主要思路:将吸顶组件进行隐藏,判断是否判断该相交的元素固定,当判断相交的元素和页面或者其他基准组件能够触发intersection事件的时候,进行显示隐藏

简单代码处理:

<view
 :class="styles.compareInsuranceContainer"
>

<CustomTabList
   v-show="isShowFixedTabList && store?.compareDetailList?.length"
   :class="styles.fixedTabList"
   :style="{
     top: `${projectStore.navBarHeight}px`,
     zIndex: 2,
   }"
   v-model="selectedTabValue"
   :defaultValue="selectedTabValue"
   @change="handleTabChange"
   ref="ShowAndHiddenTabListRef"
   id="showAndHiddenTabList"
/>
</view>

<script>
  ...
  let customObserve1 = null;
  const isShowFixedTabList = ref(false);
  // 注意销毁,不然会导致小程序内存泄露
  watch(() => tabListGroupRef.value, async (newVal, oldVal) => {
   if (newVal) {
     Taro.nextTick(() => {
       customObserverTabList();
     })
   } else {
     customObserve1 && customObserve1.disconnect();
   }
 })
 function customObserverTabList() {
   const pageDefaultObserverOptions = {
     initialRatio: 1,
     thresholds: [0, 1],
   }
   // 进行相交滚动处理
   customObserve1 = Taro.createIntersectionObserver(null, pageDefaultObserverOptions)
       .relativeToViewport({
         top: -(projectStore.navBarHeight + 40),
       })
       .observe('#tabListGroup', (res) => {
         const rect =  res.intersectionRect;
         const ratio = res.intersectionRatio;
         if (!ratio) {
           isShowFixedTabList.value = true;
         } else {
           isShowFixedTabList.value = false;
         }
       })
 }

</script>

图表处理

有的开发看到上面的页面布局之后,会想到为什么不用小程序的scroll-view,滚动性能会更好,其实本质是为了底部canvas图表,scroll-view本质是不断渲染视线节点,不渲染离屏的节点提高性能,而这出现一个问题,就是canvas在离屏的情况下,canvas节点没有创建,导致vchart的图表无法渲染,导致图表空白,这是一个坑。

为什么使用字节@visactor/vchart的图表库,而不用echart这种成熟的方案,主要是图表库的配色和设计稿相比差异较大,不能很好的还原设计稿。但是在使用vchart图表库的时候,还是发现了问题:
+ 微信小程序层面需要二次封装,才能支持vue3的语法,官方提供了vchart-react的封装,没有提供vue3的处理,再看了相关实现之后,自己封装了一下使用,本质就是预留canvaschart本体、tooltip等指定canvas,指定运行环境和图片设定配置,可以看代码实现
+ vchart图表库较大,一定要分包使用,同时一定要注意,分包使用的时候,相关环境、plugin是否按需引入,不然导致图表显示不完整等问题

代码实现:
<script setup>
import { registerWXEnv } from '@visactor/vchart/esm/env';
import { VChart, vglobal } from '@visactor/vchart/esm/core';
import { registerAreaChart } from '@visactor/vchart/esm/chart';
import {
  registerCanvasTooltipHandler,
} from '@visactor/vchart/esm/plugin';
import {
  registerMarkLine,
  registerCartesianLinearAxis,
  registerCartesianBandAxis,
  registerTooltip,
  registerCartesianCrossHair,
  registerDiscreteLegend,
} from '@visactor/vchart/esm/component';

import Taro from '@tarojs/taro';
import {ref, watch} from 'vue';
import styles from './index.module.scss';

let chart = null, colorMap = {};

// 注册图表和组件
VChart.useRegisters([
  registerWXEnv,
  registerCanvasTooltipHandler,
  registerMarkLine,
  registerAreaChart,
  registerCartesianLinearAxis,
  registerCartesianBandAxis,
  registerTooltip,
  registerCartesianCrossHair,
  registerDiscreteLegend,
]);

const props = defineProps({
  canvasId: {
    type: String,
    default: '',
  },
  isShowChart: {
    type: Boolean,
    default: true,
  },
  dataSource: {
    type: Array,
    default: () => [],
  }
})

// 设置标记处理
const getMarkLineStyleConfig = (fill) => ({
  endSymbol: {
    style: {
      angle: 180,
      fill,
      dy: -5
    }
  },
  label: {
    dx: -4,
    // dy: -20,
    dy: -12,
    width: 28,
    labelBackground: {
      padding: 4,
      style: {
        fill,
        borderRadius: 5
      }
    },
    style: {
      fill: '#fff',
      fontSize: 8,
      fontWeight: 400,
    }
  }
})

const chartContainer = ref(null);
const handleEvent = (event) => {
  if (chart) {
    Object.defineProperty(event, 'chart', {
      writable: false,
      // value: chartConfig.vchart.getCanvas() // Tip: 必须设置
      value: chart.getCanvas() // Tip: 必须设置
    });
    chart.getStage().window.dispatchEvent(event);
  }
};


const getDomRef = async () => {
  return new Promise(resolve => {
    Taro.nextTick(() => {
      Taro.createSelectorQuery()
        .select(`#${props.canvasId}`)
        .boundingClientRect(domref => {
          resolve(domref);
        })
        .exec();
    });
  });
};

const getMarkLineConfigListByDataSource = (dataSource = []) => {
  const rateConfig = new Array(9).fill(null).reduce((preItem,item, index) => {
    if (!index) {
      preItem.push(index + 1);
    }
    preItem.push(index + 2);
    return preItem;
  }, []);
  // const rateConfig = new Set();
  const recordRateByKeyMap = new Map();
  const res = dataSource.reduce((preItem, currItem, index) => {
    const currRate = Math.floor(currItem.value / currItem.initValue);
    // 初始化record
    if (!recordRateByKeyMap.has(currItem.key)) {
      recordRateByKeyMap.set(currItem.key, []);
    }
    // rateConfig.find(item => item === currRate)
    if (rateConfig.find(item => item === currRate)) {
      // const fillColor = '#1777FF';
      const fillColor = colorMap?.[currItem.key] || '#1777FF';
      const markLineStyleConfig = getMarkLineStyleConfig(fillColor)
      // 抽取当前标记区间
      const config = {
        ...markLineStyleConfig,
        relativeSeriesId: currItem.key,
        label: {
          text: `x${currRate}`,
          ...markLineStyleConfig.label
        },
        x: (relativeSeriesData) => {
          const markLineSeriesData = relativeSeriesData.find(item => currItem.key === item.key && currItem.time === item.time);
          if (markLineSeriesData) {
            return markLineSeriesData.time;
          }
        },
        y: (relativeSeriesData) => {
          const markLineSeriesData = relativeSeriesData.find(item => currItem.key === item.key && currItem.time === item.time);
          if (markLineSeriesData) {
            return 0;
          }
        },
        y1: (relativeSeriesData) => {
          const markLineSeriesData = relativeSeriesData.find(item => currItem.key === item.key && currItem.time === item.time);
          if (markLineSeriesData) {
            return markLineSeriesData.value;
          }
        },
        line: {
          style: {
            opacity: 0,
          }
        }
      }
      // 判断当前是否重复
      if (!recordRateByKeyMap.get(currItem.key).includes(currRate)) {
        recordRateByKeyMap.set(currItem.key, [...recordRateByKeyMap.get(currItem.key), currRate])
        preItem.push(config);
      }
    }
    return preItem;
  }, []);
  return res;
}

function setFillColorMap(dataSource = []) {
  const keySet = new Set();
  const keyList = dataSource.reduce((preItem, currItem) => {
    if (!keySet.has(currItem.key)) {
      keySet.add(currItem.key);
      preItem.push(currItem.key);
    }
    return preItem;
  }, []);
  const colorList = ['#1777FF', '#00DB8B', '#FF7726']
  const configObj = keyList.reduce((preItem, currItem, index) => {
    preItem[currItem] = colorList[index];
    return preItem;
  }, {});
  colorMap = configObj;
  return configObj;
}

const getChartSpec = (dataSource = []) => {
  const spec = {
    // type: 'area',
    type: 'line',
    data: {
      id: 'lineAndAreaChart',
      values: dataSource
    },
    title: {
      visible: false,
    },
    stack: false,
    xField: 'time',
    yField: 'value',
    // seriesField: 'name',
    seriesField: 'key',
    axes: [
      {
        type: 'band',
        orient: 'bottom', // 声明显示的位置
        tick: {
          tickStep: 9
        },
      },
      {
        type: 'linear',
        orient: 'left', // 声明显示的位置
        domainLine: {
          visible: true,
        },
        label: {
          formatMethod: function(text, datum) {
            const val = Number(text);
            if (val >= 100000000) {
              return `${text.toString().slice(0, -8)}亿`
            }
            if (val >= 10000) {
              return `${text.toString().slice(0, -4)}万`
            }
            return text;
          }
        }
      }
    ],
    scrollBar: [
      {
        orient: 'bottom',
        field: 'time',
        roam: true,
        round: true,
      }
    ],
    line: {
      style: {
        curveType: 'monotone',
        lineWidth: 2,
        stroke: datum => {
          return colorMap?.[datum.key] || '#1777FF';
        },
      }
    },
    point: {
      style: {
        size: 0,
        fill: 'white',
        stroke: null,
      },
    },
    area: {
      style: {
        fillOpacity: 0,
        shadowColor: 'rgba(0,0,0,.2)',
        shadowOpacity: 0.4,
        shadowBlur: 4,
        shadowOffsetY: 4,
        stroke: datum => {
          return colorMap?.[datum.key] || '#1777FF';
        },
      },
    },
    legends: [
      {
        visible: true,
        position: 'middle',
        orient: 'top',
        layout: 'vertical',
        item: {
          shape: {
            style: {
              symbolType: 'roundLine',
              size: 10,
              opacity: 1,
              lineWidth: 0.5,
            },
            stroke: datum => {
              return colorMap?.[datum.key] || '#1777FF';
            },
          },
          label: {
            formatMethod: (text, item, index) => {
              // return item?.name || '-';
              const name = props.dataSource.find(item => item.key === text)?.name
              return name || '-';
            }
          },
        }
      },
    ],
    // 设置倍率标注
    markLine: getMarkLineConfigListByDataSource(dataSource),
    // 自定义tooltip
    tooltip: {
      mark: {
        visible: false,
      },
      dimension: {
        position: 'tl',
        shapeType: 'square',
        shapeHollow: false,
        shapeFill: datum => {
          return colorMap?.[datum.key] || '#1777FF';
        },
        title: {
          value: (datum) => {
            return `第${datum?.time || '-'}年`;
          }
        },
        content: {
          key: (datum) => {
            if (datum.name?.length > 8) {
              return `${datum.name.slice(0, 8)}...`
            }
            return `${datum.name}`
          },
          value: (datum) => {
            return `${datum.value}`
          },
        }
      },
      style: {
        panel: {
          padding: [8, 10],
          backgroundColor: 'rgba(255, 255, 255, .7)',
          shadow: {
            // color: 'rgba(0,0,0,0.15)',
            x: 0,
            y: 0,
            blur: 10,
            spread: 5,
          }
        },
        titleLabel: {
          fontSize: 10,
          fontFamily: 'PingFangSC, PingFang SC',
          fill: '#1777FF',
          fontWeight: 500,
        },
        keyLabel: {
          fontSize: 10,
          fontFamily: 'Times New Roman',
          fill: '#333333',
          fontWeight: 400,
          spacing: 10,
          maxWidth: 220,
        },
        valueLabel: {
          fontSize: 10,
          fill: '#333333',
          fontWeight: 400,
          spacing: 10
        },
      }
    }
  };
  return spec;
}


function renderChart() {
  Taro.nextTick(async () => {
    const spec = getChartSpec(props.dataSource);
    const domRef = await getDomRef();
    domRef.id = props.canvasId;
    // registerWXEnv();
    await vglobal.setEnv('wx', {
      domref: domRef,
      force: true,
      canvasIdLists: [props.canvasId, `${props.canvasId}Tooltip`, `${props.canvasId}Hidden`],
      freeCanvasIdx: 2,
      component: undefined
    })

    const defaultVtChartConfig = {
      mode: 'wx',
      modeParams: {
        force: true, // 是否强制使用 canvas 绘制
        canvasIdLists: [props.canvasId, `${props.canvasId}Tooltip`, `${props.canvasId}Hidden`], // canvasId 列表
        tooltipCanvasId: `${props.canvasId}Tooltip`, // tooltip canvasId
        freeCanvasIdx: 2 // 自由 canvas 索引
      },
      renderCanvas: props.canvasId,
      dpr: Taro.getSystemInfoSync().pixelRatio,
    }

    // chartConfig.vchart = new VChart(spec,{
    chart = new VChart(spec,{
      ...defaultVtChartConfig,
      modeParams: {
        ...defaultVtChartConfig.modeParams,
        domref: domRef,
      },
      // animation: false,
      renderCanvas: props.canvasId,
    });
    chart.renderAsync();
  });
}


function updateChartBySpec(dataSource = props.dataSource) {
  chart.updateSpec(getChartSpec(dataSource));
  chart.renderAsync();
}

watch(() => props.dataSource, async (newVal, oldVal) => {
  if (newVal) {
    colorMap = setFillColorMap(newVal);
    if (!chart) {
      await renderChart();
    } else {
      updateChartBySpec(newVal);
    }
  }
}, {
  deep: true,
  immediate: true,
})
</script>

<template>
  <view v-if="props.canvasId" :class="styles.lineChartContainer">
    <canvas
      type="2d"
      :canvasId="`${props.canvasId}Tooltip`"
      :id="`${props.canvasId}Tooltip`"
      :class="styles.style_cs_tooltip_canvas"
    ></canvas>
    <canvas
      type="2d"
      ref="chartContainer"
      :canvasId="props.canvasId"
      :id="props.canvasId"
      height="880rpx"
      width="100%"
      @touchstart="handleEvent"
      @touchmove="handleEvent"
      @touchend="handleEvent"
    ></canvas>
    <canvas
      type="2d"
      :canvasId="`${props.canvasId}Hidden`"
      :id="`${props.canvasId}Hidden`"
      :class="[styles.style_cs_canvas, styles.style_cs_canvas_hidden]"
    ></canvas>
  </view>
</template>

<style scoped>

</style>

文件上传

微信小程序自带uoloadFile()的文件上传很奇怪,并不是web层面可以直接读取文件系统,而是读取微信聊天记录列表、选取上传文件。为了达到和web端一样的打开系统的文件管理,所以决定涉及到上传的页面部分,单独开发一个web项目单独部署,使用viewview嵌套进行上传处理。这里涉及到三个问题:

  • web端页面要使用h5的开发规范,保证移动端的兼容性,包括px2vw处理、UI库的选择等
  • web端页面和小程序的认证,可以通过webview的url传递token,来保证认证的有效性
  • web端内嵌页面和小程序的通知交互,可以直接使用引入微信官方的jweixin-1.3.2.js来解决,可以直接调 wx?.miniProgram直接获取微信小程序操作
  • 因为涉及到webview的内嵌,所以需要在微信开发者平台,给webview嵌套的url设置白名单

自定义分享处理

因为业务设定相关,需要拦截微信小程序自带的分享功能,配合单独的分享标题、分享的过期时间等配置,所以需要自定义分享流程,抽取公共组件。遇到两个问题:

  • 停止右上角胶南里面自带的分享功能,可以使用Taro.hideShareMenu()禁用胶南的分享,同时在config.js中配置enableShareAppMessage: true开启自定义分享
  • 分享配置不符合要求的时候,中断分享流程,触发表单校验,这里使用Taro的useShareAppMessage的钩子,传递分享表单的配置参数,如果表单配置不符合,可以直接在该方法中throw Error中断分享
<template>
  <view
    :class="styles.shareCompareInsuranceBlockContainer"
  >
    <view
      v-if="props?.isShowShareSetting"
      :class="styles.settingBtn"
      @click="handleShowResultPopup"
    >
      <Setting
        color="#999999"
        :class="styles.icon"
      ></Setting>
      <view :class="styles.msg">展示设置</view>
    </view>

    <view :class="[styles.shareBtnGroup, props?.shareBtnClassConfig?.outsideBtnGroupClass]">
      <view :class="[styles.shareBtn, props?.shareBtnClassConfig?.outsideBtnClass]" @click="handleShowShareSettingDashBoard">
        <image
            :class="styles.bg"
            :src="ShareBtnBg"
        />
        <view :class="[styles.msg, props?.shareBtnClassConfig?.outsideBtnClass]">
          {{ props?.shareBtnTextConfig?.outsideBtnText || '分享' }}
        </view>
      </view>
    </view>

    <nut-popup
        key="showResultSettingPopUp"
        v-model:visible="isShowResultSettingPopup"
        title="展示设置修改"
        position="bottom"
        :catch-move="true"
        :z-index="10000"
        closeable
        round
        overlay
    >
      <view
          v-if="isShowResultSettingPopup"
          :class="styles.showResultFormGroup"
      >
        <view :class="styles.title">展示设置修改</view>
          <slot name="showResultSetting"></slot>
      </view>
    </nut-popup>

    <nut-popup
        key="showShareSettingPopUp"
        v-model:visible="isShowShareSettingActionSheet"
        @choose="handleActionSheetSubmit"
        :catch-move="true"
        closeable
        round
        overlay
        :style="{
          width: '100vw'
        }"
        :z-index="10000"
        position="bottom"
    >

      <view
          v-if="isShowShareSettingActionSheet"
          :class="styles.settingFormGroup"
      >
        <ShareSettingForm
            :imageUrl="props?.shareMessageConfig?.imageUrl || ''"
            ref="shareSettingFormRef"
        />


        <view :class="styles.demoBlock">

        </view>

        <view :class="styles.warningMsg">
          分享后,公众号会提醒您有谁看过
        </view>

        <view :class="styles.shareBtnGroup">
          <view :class="[styles.shareBtn, props?.shareBtnClassConfig?.outsideBtnClass]">
            <button
                style="width: 100%"
                open-type="share"
                :class="styles.coverShareBtn"
            >
              <image
                  :class="styles.bg"
                  :src="ShareBtnBg"
              />
              <view
                  open-type='share'
                  :class="[styles.msg, props?.shareBtnClassConfig?.outsideBtnClass]"
              >
                {{ props?.shareBtnTextConfig?.insideBtnText || '分享' }}
              </view>
            </button>
          </view>
        </view>
      </view>
    </nut-popup>

  </view>
</template>

<script setup>
import Taro, { useShareAppMessage } from '@tarojs/taro'
import { ref, defineProps, defineExpose } from 'vue';
import styles from "./index.module.scss"
import ShareBtnBg from "@/assets/components/shareBtnBg.png";
import { Setting } from "@nutui/icons-vue-taro";
import ShareSettingForm from '@/components/form/shareSetting';
import ShareCardIcon from '@/assets/components/shareCardIcon.png';
import { useProjectStore } from '@/stores';

const projectStore = useProjectStore();

const isShowShareSettingActionSheet = ref(false);
const isShowResultSettingPopup = ref(false);
const shareSettingFormRef = ref(null);

const props = defineProps({
  pageName: {
    type: String,
    default: '',
  },
  myShareSettingId: {
    type: Number,
    default: 0,
  },
  isShowShareSetting: {
    type: Boolean,
    default: true,
  },
  shareBtnClassConfig: {
    type: Object,
    default: () => ({
      outsideBtnClass: {},
      insideBtnClass: {},
      outsideBtnGroupClass: {},
    }),
  },
  customShareParams: {
    type: Function,
    default: () => {},
  },
  customSharePayload: {
    type: Function,
    default: () => {}
  },
  shareBtnTextConfig: {
    type: Object,
    default: () => ({
      outsideBtnText: '分享',
      insideBtnText: '分享'
    })
  },
  shareMessageConfig: {
    type: Object,
    default: () => null,
  }
})

function handleShowShareSettingDashBoard() {
  isShowShareSettingActionSheet.value = true;
}

function handleActionSheetSubmit() {

}


function generateShareParams(shareParamsObj = {}) {
  const result = Object.keys(shareParamsObj).reduce((preItem, currItem) => {
    preItem += `${currItem}=${shareParamsObj[currItem]}&`;
    return preItem;
  }, '?');
  return result;
}


// 存储本地分享配置
function handleSaveShareSettingInStorage(shareConfigObj) {
  const value = JSON.stringify(shareConfigObj);
  Taro.setStorageSync('shareConfig', value);
}

/**
 * 作用: 清除本地配置
 */
function handleClearShareSettingInStorage() {
  Taro.clearStorageSync('shareConfig');
}


useShareAppMessage(async (res) => {
  let title = '';
  let formData = {};
  const shareSettingData = await shareSettingFormRef?.value?.submit();
  const valid = shareSettingData.valid;
  if (!valid) throw new Error();
  formData = shareSettingData.formData;
  title = formData?.title || title;

  let shareSettingPayload = {
    data: {
      pageName: props.pageName,
      shareTitle: formData?.title || '',
      rememberNext: formData.isRemember ? 1 : 0,
      expireTimeType: formData.expireTime,
      isShowCard: formData.isShowPersonCard,
    }
  }
  // 没有选择保存配置,直接强制挂空
  if (!formData.isRemember) {
    shareSettingPayload = {
      data: {
        pageName: props.pageName,
        shareTitle: '',
        rememberNext: 0,
        expireTimeType: '3天有效',
        isShowCard: 1,
      }
    }
  }
  if (!props?.myShareSettingId) {
    await projectStore.addMyShareSetting(shareSettingPayload);
  } else {
    // 调用更新接口,覆盖当前配置
    shareSettingPayload.data.id = props.myShareSettingId;
    await projectStore.updateMyShareSetting(shareSettingPayload);
  }
  const shareParams = await props?.customShareParams(formData) || null;
  const paramsStr = shareParams ? generateShareParams(shareParams) : '';
  const sharePageConfig = {
    title: formData?.title || props?.shareMessageConfig?.title || title,
    path: `${props?.shareMessageConfig?.path || ''}${paramsStr}`,
    imageUrl: props?.shareMessageConfig?.imageUrl || ShareCardIcon,
  }
  return sharePageConfig;
})

function handleShowResultPopup() {
  isShowResultSettingPopup.value = true;
}

function updateShareSettingFormData(formData) {
  if (shareSettingFormRef?.value) {
    shareSettingFormRef.value.updateFormData(formData);
  }
}

function handleCloseSettingPopup() {
  isShowResultSettingPopup.value = false;
}

defineExpose({
  handleCloseSettingPopup,
  updateShareSettingFormData,
})

</script>

其他问题总结

taro中抽取公共依赖,减小分包大小

在config/index.js中,自定义webpackChain配置,覆盖原有taro的配置

optimizeMainPackage: {
  enable: true,
},
webpackChain(chain) {
  const taroBaseReg = /@tarojs[\/][a-z]+/;
  // 配置京东按需加载
  chain.plugin("unplugin-vue-components").use(
    ComponentsPlugin({
      resolvers: [NutUIResolver()],
    })
  );
  // 准备抽取vchart的配置文件
  chain.merge({
    optimization: {
      runtimeChunk: {
        name: config.isBuildPlugin ? "plugin/runtime" : "runtime",
      },
      splitChunks: {
        chunks: "all",
        maxInitialRequests: Infinity,
        minSize: 0,
        cacheGroups: {
          common: {
            name: config.isBuildPlugin ? "plugin/common" : "common",
            minChunks: 2,
            priority: 1,
          },
          vendors: {
            name: config.isBuildPlugin ? "plugin/vendors" : "vendors",
            minChunks: 2,
            maxSize: 2000000,
            test: (module) => {
              return (
                /[\/]node_modules[\/]/.test(module.resource) &&
                !/[\/]@visactor[\/]/.test(module.resource)
              );
            },
            // exclude: [/[\/]@visactor/],
            priority: 10,
          },
          // 抽取vchart部分
          myVChart: {
            name: config.isBuildPlugin ? "plugin/myVChart" : "myVChart",
            test: (module) => {
              return /[\/]@visactor[\/]vchart[\/]esm/.test(
                module.resource
              );
            },
            minChunks: 2,
            priority: 10,
          },
          taro: {
            name: config.isBuildPlugin ? "plugin/taro" : "taro",
            test: (module) => {
              return taroBaseReg.test(module.context);
            },
            priority: 100,
          },
        },
      },
    },
  });
  chain.merge({
    module: {
      rule: {
        mjsScript: {
          test: /.mjs$/,
          include: [/pinia/],
          use: {
            babelLoader: {
              loader: require.resolve("babel-loader"),
            },
          },
        },
      },
    },
  });
},

taro中自定义配置环境变量

需要将环境变量放置在defineConstants配置内部

神策数据埋点配置导致分享链接参数异常

需要注意默认配置项中的allow_amend_share_path,神策会自动插入一些埋点参数覆盖其他参数,设置为false不会覆盖分享参数