基于uni-app实现微信小程序 --- vue3+ts+sass

1,068 阅读4分钟

基于uni-app实现微信小程序 --- vue3+ts+sass

作者:逆风翱翔的青鴍

项目背景

基于uni-app实现微信小程序,使用框架:vue3+ts+sass

为什么要自定义picker?

原生小程序不支持自定义样式

该自定义组件支持:

自定义数据 自定义样式 联动

思路:

使用小程序嵌入页面的滚动选择器picker-view实现选择滚动。
在pick-view中使用picker-view-column,可以使其孩子节点的高度自动设置成与picker-view的选中框的高度一致,并不在页面中显示

picker-view介绍:
属性类型必填说明最低版本
valueArray.<number>数组中的数字依次表示 picker-view 内的 picker-view-column 选择的第几项(下标从 0 开始),数字大于 picker-view-column 可选项长度时,选择最后一项。1.0.0
indicator-stylestring设置选择器中间选中框的样式1.0.0
indicator-classstring设置选择器中间选中框的类名1.1.0
mask-stylestring设置蒙层的样式1.5.0
mask-classstring设置蒙层的类名 1.5.0
bindchangeeventhandle滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)1.0.0
bindpickstarteventhandle当滚动选择开始时候触发事件2.3.1
bindpickendeventhandle当滚动选择结束时候触发事件2.3.1

实现:

1.页面布局:
<template>
    <view :class="['fh-full-box', isOpen ? 'fh-cur' : '']">
        <view class="fh-picker">
            <view class="fh-picker-header" :style="pickerHeaderStyle">
                <view @click="cancle">
                    <text :style="cancelStyle">{{ cancelText }}</text>
                </view>
                <text :style="titleStyle">{{ titleText }}</text>
                <view @click="sure">
                    <text :style="sureStyle">{{ sureText }}</text>
                </view>
            </view>
            <picker-view
                :value="value"
                class="fh-picker-content"
                @pickstart="pickStart"
                @change="pickChange"
                @pickend="pickEnd"
                :indicator-style="indicatorStyle"
            >
                <picker-view-column
                    v-for="(items, index) in columnsData"
                    :key="index"
                >
                    <view v-for="(item, index) in items" :key="index">
                        <text class="fh-line">{{
                            isUseKeywordOfShow ? item[keyWordsOfShow] : item
                        }}</text>
                    </view>
                </picker-view-column>
            </picker-view>
        </view>
    </view>
</template>
样式:

由于组件引入后是覆盖着页面上面的会影响正常页面的点击,所以使用了pointer-events。
下面介绍一下pointer-events
pointer-events用来指定在什么情况下元素可以成为鼠标事件的target 属性有很多值,但是对于浏览器来说,只有auto和none两个值可用,所以介绍一下auto和none
auto——效果和没有定义pointer-events属性相同,鼠标不会穿透当前层。
none——元素永远不会成为鼠标事件的target(目标)
支持:Firefox 3.6+和chrome 2.0+ 以及safari 4.0+都支持这个CSS3属性,IE6/7/8/9都不支持,Opera在SVG中支持该属性但是HTML中不支持。

<style lang="scss" scoped>
    .fh-full-box {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        top: 0;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.4);
        transition: all 0.4s ease-in-out 0;
        pointer-events: none;
        opacity: 0;
        .fh-picker {
            position: absolute;
            left: 0;
            bottom: -470rpx;
            display: flex;
            flex-direction: column;
            width: 100%;
            height: 470rpx;
            background: #ffffff;
            transition: all 0.4s ease-in-out 0;
        }
        .fh-picker-header {
            height: 20%;
            box-sizing: border-box;
            padding: 0 20rpx;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #eeeeee;
            view {
                height: 100%;
                display: flex;
                justify-content: center;
                align-items: center;
                text {
                    font-size: 36rpx;
                }
            }
        }
        .fh-picker-content {
            flex-grow: 1;
            view {
                height: 100%;
                display: flex;
                justify-content: center;
                align-items: center;
                text {
                    font-size: 36rpx;
                }
            }
            .fh-line {
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
        }
    }
    .fh-full-box.fh-cur {
        opacity: 1;
        pointer-events: auto;
        .fh-picker {
            bottom: 0;
        }
    }
</style>
实现逻辑:

接收父组件传值props

props: {
    scrollType: {
        // "link": scroll间联动  "normal": scroll相互独立
        type: String,
        value: "normal",
    },
    titleText: {
        // 标题文案
        type: String,
    },
    cancelText: {
        // 取消按钮文案
        type: String,
    },
    sureText: {
        // 确定按钮文案
        type: String,
    },
    listData: {
        //数据源
        type: Array,
        default: () => [],
    },
    defaultPickData: {
        //默认选择
        type: Array,
        default: () => [],
    },
    pickerHeaderStyle: String, // 标题栏样式 view
    sureStyle: String, // 标题栏确定样式  text
    cancelStyle: String, // 标题栏取消样式 text
    titleStyle: String, // 标题栏标题样式  view
    indicatorStyle: {
        // 选中框样式
				type: String,
				default: "height:48px;",
		},
    keyWordsOfShow: {
        // 展示关键字段
        type: String,
        default: "name",
    },
    isShowPicker: {
        // 是否展示弹窗
        type: Boolean,
        default: false,
    },
},

定义响应式数据:

const DATA = reactive<BOOTOMPOPDATA>({
    columnsData: [], // 数组
    value: [], // 选中的value
    isOpen: false, // 是否显示picker
    isUseKeywordOfShow: false, // 是否根据关键字展示
    scrollEnd: true, // 滚动是否结束
    tempValue: [], // 选中数据的索引
});
// 用ts定义接口约束数据
type dataARR = Array<string | number | object>;
export interface BOOTOMPOPDATA {
    columnsData: dataARR,
    value: dataARR,
    isOpen: boolean,
    isUseKeywordOfShow: boolean,
    scrollEnd: boolean,
    tempValue: dataARR,
}

一些相关按钮操作事件:

/**
* @description: 关闭弹窗
*/
function closePicker() {
    DATA.isOpen = false;
}

/**
* @description: 打开弹窗
*/
function openPicker() {
    // 保存重新打开时,选中上次保存所选中的内容
    setDefault();
    DATA.isOpen = true;
}

/**
* @description: 当滚动选择开始时候触发事件
*/
function pickStart() {
    DATA.scrollEnd = false;
}

/**
* @description: 当滚动选择结束时候触发事件
*/
function pickEnd() {
    DATA.scrollEnd = true;
}

/**
* @description: 点击取消按钮
*/
function cancle() {
    closePicker();
    context.emit("cancleFn");
}

/**
* @description: 点击确认按钮
*/
function sure() {
    const { scrollEnd, tempValue } = DATA;
    if (!scrollEnd) return;
    const backData = getBackDataFromValue(tempValue);
    closePicker();
    context.emit("sureFn", {
        choosedData: backData,
        choosedIndexArr: tempValue,
    });
}

通过判断scrollType来进行数据初始化处理

function setDefault() {
  const { scrollType } = props;
  const { tempValue } = DATA;
  let listData: any[] = props.listData;
  let defaultPickData: any[] = props.defaultPickData;
  switch (scrollType) {
    case "normal":
      if (isPlainObject(listData[0][0])) {
        DATA.isUseKeywordOfShow = true;
      }
      if (
        Array.isArray(defaultPickData) &&
        defaultPickData.length > 0
      ) {
        DATA.tempValue = defaultPickData;
      }
      DATA.columnsData = listData;
      DATA.value = defaultPickData;
      break;
    case "link":
      let columnsData = [];
      if (
        Array.isArray(defaultPickData) &&
        defaultPickData.length > 0
      ) {
        if (defaultPickData.every((v) => isPlainObject(v))) {
          const key = Object.keys(defaultPickData[0])[0];
          const arr: any[] = [];

          getIndexByIdOfObject(
            listData,
            defaultPickData,
            key,
            arr
          );
          defaultPickData = arr;
        }
        let tempI = 0;
        do {
          columnsData.push(getColumnData(listData));
          listData =
            listData[defaultPickData[tempI]].children;
          tempI++;
        } while (listData);
      } else {
        do {
          tempValue.push(0);
          columnsData.push(getColumnData(listData));
          listData = listData[0].children;
        } while (listData);
      }
      DATA.tempValue = defaultPickData;
      DATA.isUseKeywordOfShow = true;
      DATA.columnsData = columnsData;
      DATA.value = defaultPickData;
      break;
  }
}

/**
* @description: 根据id获取索引
*/
function getIndexByIdOfObject(
    listData: any[],
    idArr: any[],
    key: string,
    arr: any[]
): any {
    if (!Array.isArray(listData)) return;
    for (let i = 0, len = listData.length; i < len; i++) {
        if (listData[i][key] === idArr[arr.length][key]) {
            arr.push(i);
            return getIndexByIdOfObject(
                listData[i].children,
                idArr,
                key,
                arr
            );
        }
    }
}

/**
* @description: 过滤数据,不要children
*/
function getColumnData(arr: Array<object>) {
    return arr.map((v) => fomateObj(v));
}
function fomateObj(o: any) {
    const temp: any = {};
    for (const k in o) {
        k !== "children" && (temp[k] = o[k]);
    }
    return temp;
}

滚动选择时触发的change事件,根据scrollType判断是否进行联动处理

/**
* @description: 滚动选择时触发change事件
*/
function pickChange(e: any) {
  const { scrollType } = props;
  const { tempValue } = DATA;
  let val = e.detail.value;
  switch (scrollType) {
    case "normal":
      DATA.tempValue = val.concat();
      DATA.value = val.concat();
      break;
    case "link":
      const tempArray: any[] = [];
      val = validate(val);
      if (val.length > 1) {
        val.slice(0, val.length - 1).reduce(
          (t: any, c: any) => {
            let v;
            if (t.length < c || t.length == c) {
              v = t[t.length - 1].children;
            } else {
              v = t[c].children;
            }
            tempArray.push(getColumnData(v));
            return v;
          },
          props.listData
        );
        //
        var columnsData = [DATA.columnsData[0], ...tempArray];

        // 设置value关联
        var compareIndex = getScrollCompareIndex(
          tempValue,
          val
        );
        if (compareIndex > -1) {
          let tempI = 1;
          while (val[compareIndex + tempI] !== undefined) {
            val[compareIndex + tempI] = 0;
            tempI++;
          }
        }
        DATA.tempValue = val.concat();
        DATA.columnsData = columnsData;
        DATA.value = val;
        break;
      }
  }
}

/**
* @description: 联动,滚动时判断子索引是否是在0,不是则返回,进行处理
*/
function getScrollCompareIndex(arr1: any[], arr2: any[]) {
    let tempIndex = -1;
    for (let i = 0, len = arr1.length; i < len; i++) {
        if (arr1[i] !== arr2[i]) {
            tempIndex = i;
            break;
        }
    }
    return tempIndex;
}
/**
* @description: 当默认选择的子元素下标不是0,当改变第一列时,所有的子元素下标修改为0
*/
function validate(val: number[]) {
  const { tempValue } = DATA;
  const len = tempValue.length;
  for (let i = 0; i < len; i++) {
    if (tempValue[i] != val[i] && val[i] == 0) {
      const arr = val.splice(i + 1, len - 1);
      for (let k = 0; k < arr.length; k++) {
        val.push(0);
      }
    }
  }
  return val;
}

判断是不是object方法:isPlainObject()

/**
 * @description: 获取数据类型
 */
function _typeof(obj: any) {
    return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
/**
 * @description: 判断是否object
 */
export function isPlainObject(obj: any) {
    return _typeof(obj) === 'object';
}

使用:

参数说明
nametyperequireddefaultdescription
isShowPickerBooleanfalse显示隐藏picker
listDataArray[]数据源
scrollTypeString"normal"
cancelTextString""取消按钮文案
sureTextString""确认按钮文案
sureStyleString""确认按钮样式
cancelStyleString""取消按钮样式
titleTextString""标题文案
titleStyleString""标题样式
pickerHeaderStyleString""标题栏样式
defaultPickDataArray[]默认选择数据
keyWordsOfShowString"name"当 listData 的的每一个成员,是由对象组成的数组时,keyWordsOfShow 作为对象的 key,其 value 用于显示;或者当 picker='link'时,供显示的 key
cancleFnFunciton""点击取消触发的事件
sureFnFunciton""点击确定触发的事件

在页面中引入该组件

import bottomPop from "@/components/bottomPop/bottomPop.vue";
<bottom-pop
   :isShowPicker="showPicker2"
   :listData="listData2"
    scrollType="normal"
    cancelText="取消"
    sureText="确认"
    sureStyle="color:green"
    cancelStyle="color:red"
    titleText="多列非联动"
    titleStyle="color:orange"
    pickerHeaderStyle="height:160rpx;"
    :defaultPickData="defaultPickData2"
    keyWordsOfShow="str"
    @cancleFn="cancleCallBack2"
    @sureFn="sureCallBack2"
></bottom-pop>

点击确认,取消事件

function sureCallBack2(e: any) {
    console.log("点击了确认");
}
function cancleCallBack2() {
    console.log("点击了关闭");
}

技术分享宣传图@3x.png