zym2525
2025-02-23 797fa07355a312a06541ca105a00928e95dbded6
fix: some
26个文件已修改
12个文件已添加
1758 ■■■■■ 已修改文件
apps/taro/src/app.config.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Tabs/ProTabPane.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Tabs/ProTabs.vue 536 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Tabs/tabs.ts 354 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/constants/router.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/custom-tab-bar/index.tsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/hooks/user.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/pages/mine/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/stores/modules/user.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/login/loginByForm/verificationCodeLoginForm.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/order/order/InnerPage.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/recharge/phoneBillRecharge/InnerPage.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/recharge/rechargeElectricResult/rechargeElectricResult.config.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/recharge/rechargeElectricResult/rechargeElectricResult.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/recharge/rechargeResult/rechargeResult.config.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/recharge/selectPayType/InnerPage.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/utils/blLifeRecharge.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/utils/storage/auth.ts 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/assets/icon-back-top.png 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/InfiniteLoading/InfiniteLoading.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/Layout/LoadingLayout.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/Layout/layout.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/NoData/NoData.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/constants/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/hooks/index.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/hooks/infiniteLoading.ts 294 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/index.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/styles/index.scss 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/styles/loading.scss 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/styles/rechargeGrid.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/utils/lifeRecharge.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/utils/lifeRechargeAccountModel.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/utils/lifeRechargeServices.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/utils/types.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/views/Order/components/PhoneOrder.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/views/PhoneBillRecharge/PhoneBillRecharge.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/views/RechargeResultView/RechargeResultView.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/views/SelectPayTypeView/SelectPayTypeView.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/app.config.ts
@@ -58,6 +58,7 @@
        'electricBillRecharge/electricBillRecharge',
        'selectPayType/selectPayType',
        'rechargeResult/rechargeResult',
        'rechargeElectricResult/rechargeElectricResult',
      ],
    },
    {
apps/taro/src/components/Tabs/ProTabPane.vue
New file
@@ -0,0 +1,61 @@
<template>
  <view class="nut-tab-pane" v-if="shouldBeRender" :style="paneStyle">
    <slot></slot>
  </view>
</template>
<script setup lang="ts">
import { CSSProperties, inject, ref, computed, watch } from 'vue';
defineOptions({
  name: 'ProTabPane',
});
const props = defineProps({
  title: {
    type: [String, Number],
    default: '',
  },
  subTitle: {
    type: [String, Number],
    default: '',
  },
  paneKey: {
    type: [String, Number],
    default: '',
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  lazy: {
    type: Boolean,
    default: true,
  },
  badgeValue: {
    type: Number,
    default: 0,
  },
});
const parentOption = inject('tabsOpiton') as any;
const paneStyle = computed(() => {
  return {
    display:
      parentOption.animatedTime.value === 0 && props.paneKey !== parentOption.activeKey.value
        ? 'none'
        : undefined,
  } as CSSProperties;
});
const active = computed(() => props.paneKey === parentOption.activeKey.value);
const loaded = ref(active.value);
const shouldBeRender = computed(() => !props.lazy || loaded.value || active.value);
watch(active, (val) => {
  if (val) loaded.value = true;
});
</script>
apps/taro/src/components/Tabs/ProTabs.vue
New file
@@ -0,0 +1,536 @@
<template>
  <view
    ref="container"
    class="nut-tabs"
    :class="[direction, { fullHeight, noContent: !showPaneContent, flexTitle }]"
  >
    <div :class="['pro-tabs__titles_wrapper', { isTransparent }]">
      <scroll-view
        :id="`nut-tabs__titles_${name}`"
        :scroll-x="getScrollX"
        :scroll-y="getScrollY"
        :scroll-with-animation="true"
        :scroll-left="scrollLeft"
        :scroll-top="scrollTop"
        :enable-flex="true"
        class="nut-tabs__titles tabs-scrollview"
        :class="{
          [type]: type,
          scrollable: titleScroll,
          'scroll-vertical': getScrollY,
          [size]: size,
        }"
        :style="tabsNavStyle"
        enhanced
        :show-scrollbar="false"
        @scroll="onScroll"
      >
        <view class="nut-tabs__list">
          <slot v-if="$slots.titles" name="titles"></slot>
          <template v-else>
            <view
              v-for="(item, index) in titles"
              :key="item.paneKey"
              class="nut-tabs__titles-item taro"
              :style="titleStyle"
              :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
              @click="tabMethods.tabChange(item, index)"
            >
              <view
                v-if="type == 'line'"
                class="nut-tabs__titles-item__line"
                :style="tabsActiveStyle"
              ></view>
              <view
                v-if="type == 'smile'"
                class="nut-tabs__titles-item__smile"
                :style="tabsActiveStyle"
              >
                <JoySmile :color="color" />
              </view>
              <nut-badge
                right="-12"
                top="4"
                :value="item.badgeValue"
                :max="99"
                :hidden="!item.badgeValue"
                color="#FC0B0B"
              >
                <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }"
                  >{{ item.title }}
                </view>
              </nut-badge>
              <view class="nut-tabs__sub-titles-item__text" :class="{ ellipsis: ellipsis }"
                >{{ item.subTitle }}
              </view>
            </view>
            <view
              v-if="canShowLabel"
              class="nut-tabs__titles-item nut-tabs__titles-placeholder"
            ></view>
          </template>
        </view>
      </scroll-view>
      <slot name="right"></slot>
    </div>
    <view
      :id="'tabsContentRef-' + refRandomId"
      ref="tabsContentRef"
      class="nut-tabs__content"
      :style="contentStyle"
      @touchstart="touchMethods.onTouchStart"
      @touchmove="touchMethods.onTouchMove"
      @touchend="touchMethods.onTouchEnd"
      @touchcancel="touchMethods.onTouchEnd"
      v-show="showPaneContent"
    >
      <slot name="default"></slot>
    </view>
  </view>
</template>
<script setup lang="ts">
import { JoySmile } from '@nutui/icons-vue-taro';
import { tabsProps, Title, TypeOfFun, RectItem, raf, pxCheck, useTabContentTouch } from './tabs';
import Taro from '@tarojs/taro';
import {
  CSSProperties,
  useSlots,
  computed,
  ref,
  onMounted,
  provide,
  VNode,
  Ref,
  nextTick,
  watch,
  onActivated,
} from 'vue';
import { useTaroRect, useScrollDistance } from 'senin-mini/hooks';
defineOptions({
  name: 'ProTabs',
});
const props = defineProps(tabsProps);
const emit = defineEmits<{
  (e: 'update:modelValue', value: string | number): void;
  (e: 'click', item: Title): void;
  (e: 'change', item: Title): void;
}>();
const slots = useSlots();
const container = ref(null);
provide('tabsOpiton', {
  activeKey: computed(() => props.modelValue || '0'),
  autoHeight: computed(() => props.autoHeight),
  animatedTime: computed(() => props.animatedTime),
});
const titles: Ref<Title[]> = ref([]);
const renderTitles = (vnodes: VNode[]) => {
  vnodes.forEach((vnode: VNode, index: number) => {
    let type = vnode.type;
    type = (type as any).name || type;
    if (type === 'nut-tab-pane' || type === 'ProTabPane') {
      let title = new Title();
      if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.['paneKey']) {
        let paneKeyType = TypeOfFun(vnode.props?.['pane-key']);
        let paneIndex =
          paneKeyType === 'number' || paneKeyType === 'string'
            ? String(vnode.props?.['pane-key'])
            : null;
        let camelPaneKeyType = TypeOfFun(vnode.props?.['paneKey']);
        let camelPaneIndex =
          camelPaneKeyType === 'number' || camelPaneKeyType === 'string'
            ? String(vnode.props?.['paneKey'])
            : null;
        title.title = vnode.props?.title;
        title.subTitle = vnode.props?.subTitle;
        title.badgeValue = vnode.props?.badgeValue;
        title.paneKey = paneIndex || camelPaneIndex || String(index);
        title.disabled = vnode.props?.disabled;
      } else {
        // title.titleSlot = vnode.children?.title() as VNode[];
      }
      titles.value.push(title);
    } else {
      if (vnode.children === ' ') {
        return;
      }
      renderTitles(vnode.children as VNode[]);
    }
  });
};
const currentIndex = ref((props.modelValue as number) || 0);
const findTabsIndex = (value: string | number) => {
  let index = titles.value.findIndex((item) => item.paneKey === value);
  if (titles.value.length === 0) {
    // console.warn('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
  } else if (index === -1) {
    // console.warn('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
  } else {
    currentIndex.value = index;
  }
};
const getScrollX = computed(() => {
  return props.titleScroll && props.direction === 'horizontal';
});
const getScrollY = computed(() => {
  return props.titleScroll && props.direction === 'vertical';
});
const titleRef = ref([]) as Ref<HTMLElement[]>;
// const scrollLeft = ref(0);
const scrollTop = ref(0);
const scrollWithAnimation = ref(false);
const getRect = (selector: string) => {
  return new Promise((resolve) => {
    Taro.createSelectorQuery()
      .select(selector)
      .boundingClientRect()
      .exec((rect = []) => {
        resolve(rect[0]);
      });
  });
};
const getAllRect = (selector: string) => {
  return new Promise((resolve) => {
    Taro.createSelectorQuery()
      .selectAll(selector)
      .boundingClientRect()
      .exec((rect = []) => resolve(rect[0]));
  });
};
const navRectRef = ref();
const titleRectRef = ref<RectItem[]>([]);
const canShowLabel = ref(false);
const scrollIntoView = () => {
  if (!props.name) return;
  raf(() => {
    Promise.all([
      getRect(`#nut-tabs__titles_${props.name}`),
      getAllRect(`#nut-tabs__titles_${props.name} .nut-tabs__titles-item`),
    ]).then(([navRect, titleRects]: any) => {
      navRectRef.value = navRect;
      titleRectRef.value = titleRects;
      if (navRectRef.value) {
        if (props.direction === 'vertical') {
          const titlesTotalHeight = titleRects.reduce(
            (prev: number, curr: RectItem) => prev + curr.height,
            0
          );
          if (titlesTotalHeight > navRectRef.value.height) {
            canShowLabel.value = true;
          } else {
            canShowLabel.value = false;
          }
        } else {
          const titlesTotalWidth = titleRects.reduce(
            (prev: number, curr: RectItem) => prev + curr.width,
            0
          );
          if (titlesTotalWidth > navRectRef.value.width) {
            canShowLabel.value = true;
          } else {
            canShowLabel.value = false;
          }
        }
      }
      const titleRect: RectItem = titleRectRef.value[currentIndex.value];
      let to = 0;
      if (props.direction === 'vertical') {
        const DEFAULT_PADDING = 11;
        const top = titleRects
          .slice(0, currentIndex.value)
          .reduce((prev: number, curr: RectItem) => prev + curr.height + 0, DEFAULT_PADDING);
        to = top - (navRectRef.value.height - titleRect.height) / 2;
      } else {
        const DEFAULT_PADDING = 31;
        const left = titleRects
          .slice(0, currentIndex.value)
          .reduce((prev: number, curr: RectItem) => prev + curr.width + 20, DEFAULT_PADDING);
        to = left - (navRectRef.value?.width ?? 0 - titleRect?.width ?? 0) / 2;
      }
      nextTick(() => {
        scrollWithAnimation.value = true;
      });
      scrollDirection(to, props.direction);
    });
  });
};
const {
  onScroll,
  scrollDistance: scrollLeft,
  setScrollDistance: setScrollLeft,
} = useScrollDistance({ direction: 'horizontal' });
const scrollDirection = (to: number, direction: 'horizontal' | 'vertical') => {
  let count = 0;
  const from = direction === 'horizontal' ? scrollLeft.value : scrollTop.value;
  const frames = 1;
  function animate() {
    if (direction === 'horizontal') {
      scrollLeft.value = Math.max(0.1, to);
    } else {
      scrollTop.value += (to - from) / frames;
    }
    if (++count < frames) {
      raf(animate);
    }
  }
  if (direction === 'horizontal') {
    setScrollLeft(Math.max(0.1, to));
  } else {
    animate();
  }
};
const init = (vnodes: VNode[] = slots.default?.()) => {
  titles.value = [];
  vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
  if (vnodes && vnodes.length) {
    renderTitles(vnodes);
  }
  findTabsIndex(props.modelValue);
  setTimeout(() => {
    scrollIntoView();
  }, 500);
};
watch(
  () => slots.default?.(),
  (vnodes: VNode[]) => {
    init(vnodes);
  }
);
watch(
  () => props.modelValue,
  (value: string | number) => {
    findTabsIndex(value);
    setTimeout(() => {
      scrollIntoView();
    }, 500);
  }
);
onMounted(init);
onActivated(init);
const tabMethods = {
  isBegin: () => {
    return currentIndex.value === 0;
  },
  isEnd: () => {
    return currentIndex.value === titles.value.length - 1;
  },
  next: () => {
    currentIndex.value += 1;
    tabMethods.updateValue(titles.value[currentIndex.value]);
  },
  prev: () => {
    currentIndex.value -= 1;
    tabMethods.updateValue(titles.value[currentIndex.value]);
  },
  updateValue: (item: Title) => {
    emit('update:modelValue', item.paneKey);
    emit('change', item);
  },
  tabChange: (item: Title, index: number) => {
    emit('click', item);
    if (item.disabled || currentIndex.value === index) {
      return;
    }
    currentIndex.value = index;
    tabMethods.updateValue(item);
  },
  setTabItemRef: (el: HTMLElement, index: number) => {
    titleRef.value[index] = el;
  },
};
const { tabsContentRef, touchState, touchMethods } = useTabContentTouch(
  props,
  tabMethods,
  Taro,
  useTaroRect
);
const contentStyle = computed(() => {
  let offsetPercent = currentIndex.value * 100;
  if (touchState.moving) {
    offsetPercent += touchState.offset;
  }
  let style: CSSProperties = {
    transform:
      props.direction === 'horizontal'
        ? `translate3d(-${offsetPercent}%, 0, 0)`
        : `translate3d( 0,-${offsetPercent}%, 0)`,
    transitionDuration: touchState.moving ? undefined : `${props.animatedTime}ms`,
  };
  if (props.animatedTime === 0) {
    style = {};
  }
  return style;
});
const tabsNavStyle = computed(() => {
  return {
    background: props.background,
  };
});
const tabsActiveStyle = computed(() => {
  return {
    color: props.type === 'smile' ? props.color : '',
    background: props.type === 'line' ? props.color : '',
  };
});
const titleStyle = computed(() => {
  if (!props.titleGutter) return {};
  const px = pxCheck(props.titleGutter);
  if (props.direction === 'vertical') {
    return { marginTop: px, marginBottom: px };
  }
  return { marginLeft: px, marginRight: px };
});
const refRandomId = Math.random().toString(36).slice(-8);
</script>
<style lang="scss">
@import '@/styles/common.scss';
.pro-tabs__titles_wrapper {
  display: flex;
  align-items: center;
  background-color: #fff;
  position: relative;
  z-index: 10;
  box-shadow: none;
  padding: 0 boleGetCssVar('size', 'body-padding-h');
  margin-bottom: 8px;
  &.isTransparent {
    background-color: transparent;
    margin-bottom: 0;
  }
}
.nut-tabs.noContent {
  box-shadow: none;
  position: relative;
  z-index: 9;
}
.flexTitle {
  box-shadow: none;
  position: relative;
  z-index: 9;
  .pro-tabs__titles_wrapper {
    padding: 0;
    .nut-tabs__titles.tabs-scrollview .nut-tabs__list .nut-tabs__titles-item {
      flex: 1;
      margin: 0;
      min-width: 0;
      flex-shrink: 0;
    }
  }
}
.nut-tabs__titles.tabs-scrollview {
  background-color: transparent;
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  height: 104rpx;
  @include hiddenScrollBar;
  .nut-tabs__list {
    height: 104rpx;
    justify-content: flex-start;
    .nut-tabs__titles-item {
      flex: none;
      width: auto;
      margin-right: 78rpx;
      flex-direction: column;
      font-size: 26rpx;
      color: boleGetCssVar('text-color', 'secondary');
      align-items: center;
      transition: all 0.3s ease;
      min-width: 0;
      .nut-tabs__sub-titles-item__text {
        font-size: 20rpx;
        color: boleGetCssVar('text-color', 'regular');
      }
      .nut-tabs__titles-item__text {
        font-size: 28rpx;
        color: boleGetCssVar('text-color', 'regular');
        line-height: 40rpx;
      }
      &.active {
        color: #222;
        font-weight: 700;
        font-size: 30rpx;
        .nut-tabs__sub-titles-item__text {
          color: boleGetCssVar('color', 'primary');
        }
        .nut-tabs__titles-item__line {
          width: 100%;
        }
        .nut-tabs__titles-item__text {
          color: boleGetCssVar('text-color', 'primary');
        }
      }
      &:last-child {
        margin-right: 0;
      }
      &:first-child {
        margin-left: 0 !important;
      }
      .nut-tabs__titles-item__line {
        //   width: 100%;
        background: boleGetCssVar('color', 'primary');
        bottom: 0;
        height: 4rpx;
        max-width: 40rpx;
      }
    }
  }
}
.nut-tabs.fullHeight {
  height: 100%;
  display: flex;
  flex-direction: column;
  .nut-tabs__content {
    flex: 1;
    min-height: 0;
    .nut-tab-pane {
      padding: 0;
      background-color: transparent;
      overflow: hidden;
      display: flex;
      flex-direction: column;
    }
  }
}
</style>
apps/taro/src/components/Tabs/tabs.ts
New file
@@ -0,0 +1,354 @@
import { ref, PropType, onMounted, reactive } from 'vue';
export type TabsSize = 'large' | 'normal' | 'small';
export class Title {
  title = '';
  subTitle = '';
  //@ts-ignore
  titleSlot?: VNode[];
  paneKey = '';
  disabled = false;
  badgeValue = 0;
  constructor() {}
}
export const tabsProps = {
  modelValue: {
    type: [String, Number],
    default: 0,
  },
  color: {
    type: String,
    default: '',
  },
  direction: {
    type: String as PropType<'horizontal' | 'vertical'>,
    default: 'horizontal', //vertical
  },
  size: {
    type: String as PropType<TabsSize>,
    default: 'normal',
  },
  type: {
    type: String,
    default: 'smile', //card、line、smile
  },
  titleScroll: {
    type: Boolean,
    default: false,
  },
  ellipsis: {
    type: Boolean,
    default: true,
  },
  swipeable: {
    type: Boolean,
    default: false,
  },
  autoHeight: {
    type: Boolean,
    default: false,
  },
  background: {
    type: String,
    default: '',
  },
  animatedTime: {
    type: [Number, String],
    default: 0,
  },
  titleGutter: {
    type: [Number, String],
    default: 0,
  },
  sticky: {
    type: Boolean,
    default: false,
  },
  top: {
    type: Number,
    default: 0,
  },
  name: {
    type: String,
    default: '',
  },
  fullHeight: {
    type: Boolean,
    default: false,
  },
  showPaneContent: {
    type: Boolean,
    default: true,
  },
  flexTitle: {
    type: Boolean,
    default: false,
  },
  isTransparent: {
    type: Boolean,
    default: false,
  },
};
export const TypeOfFun = (value: any) => {
  if (null === value) {
    return 'null';
  }
  const type = typeof value;
  if ('undefined' === type || 'string' === type) {
    return type;
  }
  const typeString = toString.call(value);
  switch (typeString) {
    case '[object Array]':
      return 'array';
    case '[object Date]':
      return 'date';
    case '[object Boolean]':
      return 'boolean';
    case '[object Number]':
      return 'number';
    case '[object Function]':
      return 'function';
    case '[object RegExp]':
      return 'regexp';
    case '[object Object]':
      if (undefined !== value.nodeType) {
        if (3 === value.nodeType) {
          return /\S/.test(value.nodeValue) ? 'textnode' : 'whitespace';
        } else {
          return 'element';
        }
      } else {
        return 'object';
      }
    default:
      return 'unknow';
  }
};
export type RectItem = {
  bottom: number;
  dataset: { sid: string };
  height: number;
  id: string;
  left: number;
  right: number;
  top: number;
  width: number;
};
const _window = window as any;
export function requestAniFrame() {
  if (typeof _window !== 'undefined') {
    return (
      _window.requestAnimationFrame ||
      _window.webkitRequestAnimationFrame ||
      function (callback: Function) {
        _window.setTimeout(callback, 1000 / 60);
      }
    );
  } else {
    return function (callback: Function) {
      setTimeout(callback, 1000 / 60);
    };
  }
}
export const raf = requestAniFrame();
export const pxCheck = (value: string | number): string => {
  return isNaN(Number(value)) ? String(value) : `${value}px`;
};
const MIN_DISTANCE = 10;
type Direction = '' | 'vertical' | 'horizontal';
function getDirection(x: number, y: number) {
  if (x > y && x > MIN_DISTANCE) {
    return 'horizontal';
  }
  if (y > x && y > MIN_DISTANCE) {
    return 'vertical';
  }
  return '';
}
type TouchPosition = 'left' | 'right' | 'top' | 'bottom' | '';
export function useTouch() {
  const startX = ref(0);
  const startY = ref(0);
  const moveX = ref(0);
  const moveY = ref(0);
  const deltaX = ref(0);
  const deltaY = ref(0);
  const offsetX = ref(0);
  const offsetY = ref(0);
  const direction = ref<Direction>('');
  const isVertical = () => direction.value === 'vertical';
  const isHorizontal = () => direction.value === 'horizontal';
  const reset = () => {
    deltaX.value = 0;
    deltaY.value = 0;
    offsetX.value = 0;
    offsetY.value = 0;
    direction.value = '';
  };
  const start = (event: TouchEvent) => {
    reset();
    startX.value = event.touches[0].clientX;
    startY.value = event.touches[0].clientY;
  };
  const move = (event: TouchEvent) => {
    const touch = event.touches[0];
    deltaX.value = touch.clientX - startX.value;
    deltaY.value = touch.clientY - startY.value;
    moveX.value = touch.clientX;
    moveY.value = touch.clientY;
    offsetX.value = Math.abs(deltaX.value);
    offsetY.value = Math.abs(deltaY.value);
    if (!direction.value) {
      direction.value = getDirection(offsetX.value, offsetY.value);
    }
  };
  return {
    move,
    start,
    reset,
    startX,
    startY,
    moveX,
    moveY,
    deltaX,
    deltaY,
    offsetX,
    offsetY,
    direction,
    isVertical,
    isHorizontal,
  };
}
export const useTabContentTouch = (props: any, tabMethods: any, taro?: any, useTaroRect?: any) => {
  const tabsContentRef = ref<HTMLElement>();
  const tabsContentRefRect = ref({ width: 0, height: 0 });
  const initTaroWidth = () => {
    if (taro && taro.getEnv() !== taro.ENV_TYPE.WEB) {
      useTaroRect(tabsContentRef).then(
        (rect: any) => {
          tabsContentRefRect.value.width = rect?.width || 0;
          tabsContentRefRect.value.height = rect?.height || 0;
        },
        () => {}
      );
    } else {
      tabsContentRefRect.value.width = tabsContentRef.value?.clientWidth || 0;
      tabsContentRefRect.value.height = tabsContentRef.value?.clientHeight || 0;
    }
  };
  onMounted(() => {
    setTimeout(() => {
      initTaroWidth();
    }, 100);
  });
  const touchState = reactive({
    offset: 0,
    moving: false,
  });
  const touch = useTouch();
  let position: TouchPosition = '';
  const setoffset = (deltaX: number, deltaY: number) => {
    let offset = deltaX;
    if (props.direction === 'horizontal') {
      position = deltaX > 0 ? 'right' : 'left';
      // 计算拖拽 百分比
      offset = (Math.abs(offset) / tabsContentRefRect.value.width) * 100;
    } else {
      position = deltaY > 0 ? 'bottom' : 'top';
      offset = deltaY;
      // 计算拖拽 百分比
      offset = (Math.abs(offset) / tabsContentRefRect.value?.height) * 100;
    }
    // 拖拽阈值 85%
    if (offset > 85) {
      offset = 85;
    }
    switch (position) {
      case 'left':
      case 'top':
        // 起始tab拖拽拦截
        if (tabMethods.isEnd()) {
          offset = 0;
          touchState.moving = false;
        }
        break;
      case 'right':
      case 'bottom':
        offset = -offset;
        // 末位tab拖拽拦截
        if (tabMethods.isBegin()) {
          offset = 0;
          touchState.moving = false;
        }
        break;
    }
    touchState.offset = offset;
  };
  const touchMethods = {
    onTouchStart(event: TouchEvent) {
      if (!props.swipeable) return;
      touch.start(event);
    },
    onTouchMove(event: TouchEvent) {
      if (!props.swipeable) return;
      touch.move(event);
      touchState.moving = true;
      setoffset(touch.deltaX.value, touch.deltaY.value);
      if (props.direction === 'horizontal' && touch.isHorizontal()) {
        event.preventDefault();
        event.stopPropagation();
      }
      if (props.direction === 'vertical' && touch.isVertical()) {
        event.preventDefault();
        event.stopPropagation();
      }
    },
    onTouchEnd() {
      if (touchState.moving) {
        touchState.moving = false;
        switch (position) {
          case 'left':
          case 'top':
            // 大于 35%阈值 切换至下一 Tab
            if (touchState.offset > 35) {
              tabMethods.next();
            }
            break;
          case 'right':
          case 'bottom':
            if (touchState.offset < -35) {
              tabMethods.prev();
            }
            break;
        }
      }
    },
  };
  return { touchMethods, touchState, tabsContentRef };
};
apps/taro/src/constants/router.ts
@@ -13,4 +13,5 @@
  order = '/subpackages/order/order/order',
  selectPayType = '/subpackages/recharge/selectPayType/selectPayType',
  rechargeResult = '/subpackages/recharge/rechargeResult/rechargeResult',
  rechargeElectricResult = '/subpackages/recharge/rechargeElectricResult/rechargeElectricResult',
}
apps/taro/src/custom-tab-bar/index.tsx
@@ -73,7 +73,7 @@
          Message.confirm({ message: '请前往登录' })
            .then(() => {
              Taro.navigateTo({
                url: `${RouterPath.authorization}?redirect=${url}`,
                url: `${RouterPath.loginByForm}?redirect=${url}`,
              });
            })
            .finally(() => {
@@ -93,7 +93,7 @@
          Message.confirm({ message: '请前往登录' })
            .then(() => {
              Taro.navigateTo({
                url: `${RouterPath.authorization}?redirect=${RouterPath.home}`,
                url: `${RouterPath.loginByForm}?redirect=${RouterPath.home}`,
              });
            })
            .finally(() => {
apps/taro/src/hooks/user.ts
@@ -11,7 +11,8 @@
export function useUser() {
  const userStore = useUserStore();
  const { userDetail, userInfo, locationCity, virtualUserId } = storeToRefs(userStore);
  const { userDetail, userInfo, locationCity, virtualUserId, virtualPhoneNumber } =
    storeToRefs(userStore);
  function updateUserInfo() {
    return userStore.getCurrentUserInfo();
@@ -23,6 +24,7 @@
    updateUserInfo,
    locationCity,
    virtualUserId,
    virtualPhoneNumber,
  };
}
@@ -52,7 +54,7 @@
    }
    if (needAuth && !isLogin.value) {
      Taro.navigateTo({
        url: `${RouterPath.authorization}?redirect=${router.path}&${object2query(router.params)}`,
        url: `${RouterPath.loginByForm}?redirect=${router.path}&${object2query(router.params)}`,
      });
    }
  });
apps/taro/src/pages/mine/index.vue
@@ -13,7 +13,7 @@
    <div class="mine-page-top-view" @click="goLogin">
      <img class="mine-avatar" :src="DefaultAvatar" alt="" />
      <div class="user-info">
        <div class="user-info-name" v-if="isLogin">{{ userDetail?.userName ?? '123' }}</div>
        <div class="user-info-name" v-if="isLogin">{{ virtualPhoneNumber }}</div>
        <div class="mine-go-login" v-else>登录</div>
      </div>
    </div>
@@ -37,7 +37,7 @@
import { useUserStore } from '@/stores/modules/user';
import { useUserStoreWithOut } from '@/stores/modules/user';
const { userDetail } = useUser();
const { userDetail, virtualPhoneNumber } = useUser();
const isLogin = useIsLogin();
const systemStore = useSystemStore();
const userStore = useUserStore();
apps/taro/src/stores/modules/user.ts
@@ -11,6 +11,7 @@
  setStorageVirtualUserId,
  getStorageVirtualUserId,
  removeStorageVirtualUserId,
  LoginVirtualRes,
} from '@/utils/storage/auth';
import * as accountServices from '@life-payment/services/api/Account';
import * as userServices from '@life-payment/services/api/User';
@@ -34,6 +35,7 @@
  firstSetLocation?: boolean;
  virtualUserId?: string;
  virtualPhoneNumber?: string;
}
const goAuthorization = debounce(
@@ -57,6 +59,7 @@
  state: (): UserState => {
    const userInfo = getCacheUserInfo();
    const userDetail = getUserDetail();
    const storageVirtualUser = getStorageVirtualUserId();
    return {
      // user info
@@ -68,7 +71,8 @@
      userDetail: userDetail,
      firstGetUserDetail: true,
      virtualUserId: getStorageVirtualUserId() ?? '',
      virtualUserId: storageVirtualUser?.virtualUserId ?? '',
      virtualPhoneNumber: storageVirtualUser?.virtualPhoneNumber ?? '',
    };
  },
  getters: {
@@ -129,7 +133,10 @@
      );
      if (res) {
        this.loginVirtualSuccess(res);
        this.loginVirtualSuccess({
          virtualUserId: res,
          virtualPhoneNumber: data.phoneNumber,
        });
      }
      return res;
    },
@@ -158,15 +165,16 @@
      } catch (error) {}
    },
    async loginVirtualSuccess(virtualUserId: string) {
    async loginVirtualSuccess(virtualUserRes: LoginVirtualRes) {
      try {
        this.setVirtualUserId(virtualUserId);
        this.setVirtualUserId(virtualUserRes);
      } catch (error) {}
    },
    setVirtualUserId(virtualUserId: string) {
      this.virtualUserId = virtualUserId;
      setStorageVirtualUserId(virtualUserId);
    setVirtualUserId(virtualUserRes: LoginVirtualRes) {
      this.virtualUserId = virtualUserRes.virtualUserId;
      this.virtualPhoneNumber = virtualUserRes.virtualPhoneNumber;
      setStorageVirtualUserId(virtualUserRes);
    },
    async wxMiniAppUserLoginFromScan(wxIndentityRes: API.WxMiniAppIndentityInfo, uuid: string) {
apps/taro/src/subpackages/login/loginByForm/verificationCodeLoginForm.vue
@@ -102,7 +102,10 @@
            showLoading: false,
          }
        );
        userStore.loginVirtualSuccess(res);
        userStore.loginVirtualSuccess({
          virtualPhoneNumber: form.phoneNumber,
          virtualUserId: res,
        });
        jump();
      }
    } else {
apps/taro/src/subpackages/order/order/InnerPage.vue
@@ -1,17 +1,24 @@
<template>
  <ContentScrollView :paddingH="false">
    <Order />
  </ContentScrollView>
  <ProTabs v-model="orderType" name="user-home-tabs" class="user-home-tabs" flexTitle fullHeight>
    <ProTabPane title="话费订单" pane-key="1">
      <PhoneOrder />
    </ProTabPane>
    <ProTabPane title="电费订单" pane-key="2">
      <ElectricOrder />
    </ProTabPane>
  </ProTabs>
</template>
<script setup lang="ts">
import { Order } from '@life-payment/components';
import { PhoneOrder, ElectricOrder } from '@life-payment/components';
import Taro from '@tarojs/taro';
defineOptions({
  name: 'InnerPage',
});
const orderType = ref('1');
function goPay() {
  Taro.navigateTo({
    url: RouterPath.selectPayType,
apps/taro/src/subpackages/recharge/phoneBillRecharge/InnerPage.vue
@@ -5,7 +5,7 @@
</template>
<script setup lang="ts">
import { PhoneBillRecharge } from '@life-payment/components';
import { PhoneBillRecharge, BlLifeRecharge } from '@life-payment/components';
import Taro from '@tarojs/taro';
defineOptions({
@@ -14,7 +14,7 @@
function goPay(orderNo: string) {
  Taro.navigateTo({
    url: `${RouterPath.selectPayType}?orderNo=${orderNo}`,
    url: `${RouterPath.selectPayType}?orderNo=${orderNo}&lifePayOrderType=${BlLifeRecharge.constants.LifePayOrderTypeEnum.话费订单}`,
  });
}
</script>
apps/taro/src/subpackages/recharge/rechargeElectricResult/rechargeElectricResult.config.ts
New file
@@ -0,0 +1,3 @@
export default definePageConfig({
  disableScroll: true,
});
apps/taro/src/subpackages/recharge/rechargeElectricResult/rechargeElectricResult.vue
New file
@@ -0,0 +1,21 @@
<template>
  <PageLayout title="充值成功" class="rechargeElectricResult-page-wrapper" hasBorder>
    <ContentScrollView>
      <RechargeResultView
        style="margin-top: 40px"
        @go-back-home="goHome()"
        title="支付成功,充值款将在0-72小时内到账"
      />
    </ContentScrollView>
  </PageLayout>
</template>
<script setup lang="ts">
import { PageLayout } from '@/components';
import { goHome } from '@/utils';
import { RechargeResultView } from '@life-payment/components';
defineOptions({
  name: 'rechargeElectricResult',
});
</script>
apps/taro/src/subpackages/recharge/rechargeResult/rechargeResult.config.ts
@@ -0,0 +1,3 @@
export default definePageConfig({
  disableScroll: true,
});
apps/taro/src/subpackages/recharge/selectPayType/InnerPage.vue
@@ -1,13 +1,23 @@
<template>
  <ContentScrollView>
    <SelectPayTypeView style="margin-top: 40px" />
    <SelectPayTypeView style="margin-top: 40px" @paySuccess="handePaySuccess" />
  </ContentScrollView>
</template>
<script setup lang="ts">
import { SelectPayTypeView } from '@life-payment/components';
import { SelectPayTypeView, LifeRechargeConstants } from '@life-payment/components';
import Taro from '@tarojs/taro';
defineOptions({
  name: 'selectPayType',
});
function handePaySuccess(
  orderNo: string,
  lifePayOrderType: LifeRechargeConstants.LifePayOrderTypeEnum
) {
  Taro.navigateTo({
    url: `${RouterPath.rechargeResult}?orderNo=${orderNo}&lifePayOrderType=${lifePayOrderType}`,
  });
}
</script>
apps/taro/src/utils/blLifeRecharge.ts
@@ -4,5 +4,6 @@
export const blLifeRecharge = new BlLifeRecharge({
  request,
  userId: getStorageVirtualUserId() ?? '',
  userId: getStorageVirtualUserId()?.virtualUserId ?? '',
  phoneNumber: getStorageVirtualUserId()?.virtualPhoneNumber ?? '',
});
apps/taro/src/utils/storage/auth.ts
@@ -41,12 +41,17 @@
  return storageLocal.removeItem(StorageKey.USER_DETAIL_KEY);
}
export type LoginVirtualRes = {
  virtualUserId: string;
  virtualPhoneNumber: string;
};
export function getStorageVirtualUserId() {
  return storageLocal.getItem<string>(StorageKey.VirtualUserId_KEY);
  return storageLocal.getItem<LoginVirtualRes>(StorageKey.VirtualUserId_KEY);
}
export function setStorageVirtualUserId(virtualUserId: string) {
  return storageLocal.setItem(StorageKey.VirtualUserId_KEY, virtualUserId);
export function setStorageVirtualUserId(res: LoginVirtualRes) {
  return storageLocal.setItem(StorageKey.VirtualUserId_KEY, res);
}
export function removeStorageVirtualUserId() {
packages/components/assets/icon-back-top.png
packages/components/src/components/InfiniteLoading/InfiniteLoading.vue
@@ -64,7 +64,7 @@
import { FetchNextPageOptions } from '@tanstack/vue-query';
import { Loading } from '@nutui/icons-vue-taro';
import { useScrollDistance } from 'senin-mini/hooks';
import IconBackTop from '@/assets/components/icon-back-top.png';
import IconBackTop from '../../../assets/icon-back-top.png';
defineOptions({
  name: 'InfiniteLoading',
packages/components/src/components/Layout/LoadingLayout.vue
New file
@@ -0,0 +1,33 @@
<template>
  <div v-if="loading" class="loading-layout-loading-content-wrapper">
    <IconFont name="loading" />
    <div class="list-empty-hint-text">数据加载中......</div>
  </div>
  <Empty
    v-else-if="error"
    class="loading-layout-error-wrapper"
    status="error"
    description="加载失败"
  >
    <div :style="{ marginTop: '10px' }">
      <nut-button type="primary" @click="loadError"> 重试 </nut-button>
    </div>
  </Empty>
  <template v-else>
    <NoData v-if="showNoData" />
    <slot v-else></slot>
  </template>
</template>
<script setup lang="ts">
import { loadingLayoutProps } from './layout';
import { IconFont } from '@nutui/icons-vue-taro';
import NoData from '../NoData/NoData.vue';
import { Button as NutButton, Empty } from '@nutui/nutui-taro';
defineOptions({
  name: 'LoadingLayout',
});
const props = defineProps(loadingLayoutProps);
</script>
packages/components/src/components/Layout/layout.ts
New file
@@ -0,0 +1,19 @@
import { PropType } from 'vue';
export const loadingLayoutProps = {
  loading: {
    type: Boolean,
  },
  error: {
    type: Boolean,
  },
  showNoData: {
    type: Boolean,
  },
  loadError: {
    type: Function as PropType<(...args: any[]) => any>,
  },
  id: {
    type: String,
  },
};
packages/components/src/components/NoData/NoData.vue
New file
@@ -0,0 +1,22 @@
<template>
  <div class="no-data-wrapper">
    <img class="no-data-img" :src="NoDataImage" alt="" />
    <span class="no-data-text">暂无数据</span>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
const NoDataImage = `'https://parkmanagement.oss-cn-hangzhou.aliyuncs.com/mini/assets/no-data.png`;
export default defineComponent({
  name: 'NoData',
  setup() {
    return {
      NoDataImage,
    };
  },
});
</script>
packages/components/src/constants/index.ts
@@ -12,3 +12,14 @@
  [IspCode.dianxin]: '中国电信',
  [IspCode.liantong]: '中国联通',
};
export enum OrderInputType {
  /**
   * 升序
   */
  Asc,
  /**
   * 降序
   */
  Desc,
}
packages/components/src/hooks/index.ts
@@ -3,9 +3,13 @@
  LifePayRateListOutput,
  PhoneParValueOutput,
  PhoneParValueResponse,
  QueryLifePayOrderListInput,
  LifeRechargeConstants,
} from '../utils';
import { useQuery } from '@tanstack/vue-query';
import { computed } from 'vue';
import { computed, MaybeRef, reactive, unref } from 'vue';
import { useInfiniteLoading } from './infiniteLoading';
import { OrderInputType } from '../constants';
export function useGetRate() {
  const { blLifeRecharge } = useLifeRechargeContext();
@@ -57,3 +61,48 @@
    phoneParValueList,
  };
}
export type UseGetUserLifePayOrderPageOptions = {
  lifePayOrderType?: MaybeRef<LifeRechargeConstants.LifePayOrderTypeEnum>;
};
export function useGetUserLifePayOrderPage(options: UseGetUserLifePayOrderPageOptions = {}) {
  const { lifePayOrderType } = options;
  const { blLifeRecharge } = useLifeRechargeContext();
  // const queryState = reactive({
  //   lifePayOrderType: LifeRechargeConstants.LifePayOrderTypeEnum,
  // });
  const { infiniteLoadingProps } = useInfiniteLoading(
    ({ pageParam }) => {
      let params: QueryLifePayOrderListInput = {
        pageModel: {
          rows: 20,
          page: pageParam,
          orderInput: [{ property: 'id', order: OrderInputType.Desc }],
        },
        lifePayOrderType: unref(lifePayOrderType),
        userId: blLifeRecharge.accountModel.userId,
      };
      return blLifeRecharge.services.getUserLifePayOrderPage(params, {
        showLoading: false,
      });
    },
    {
      queryKey: [
        'blLifeRecharge/getUserLifePayOrderPage',
        {
          lifePayOrderType,
          userId: blLifeRecharge.accountModel.userId,
        },
      ],
    }
  );
  return {
    infiniteLoadingProps,
  };
}
packages/components/src/hooks/infiniteLoading.ts
New file
@@ -0,0 +1,294 @@
import {
  useInfiniteQuery,
  QueryKey,
  useMutation,
  useQueryClient,
  InfiniteData,
  RefetchOptions,
  RefetchQueryFilters,
} from '@tanstack/vue-query';
import { UnwrapNestedRefs, Ref, ref, reactive, computed } from 'vue';
import Taro from '@tarojs/taro';
export type BaseData<T = any> = {
  data?: T[];
  objectData?: any;
  pageModel?: {
    rows?: number;
    page?: number;
    totalCount?: number;
    totalPage?: number;
  };
};
export interface OrderInput {
  property?: string;
  order?: number;
}
export type ExtraParams = {
  [key: string]: any;
  orderByProperty?: string;
};
export type ServiceContext = {
  pageParam?: any;
  signal?: AbortSignal;
};
export type InfiniteGroupOptions = {
  groupIndex?: number;
  itemIndex?: number;
};
type Service<TData, TExtraParams extends ExtraParams> = (
  context: ServiceContext,
  extraParamsState: UnwrapNestedRefs<TExtraParams>
) => Promise<TData>;
type UseInfiniteLoadingOptions<T, TExtraParams extends ExtraParams> = {
  /**
   * @deprecated
   */
  defaultExtraParams?: TExtraParams;
  enabled?: Ref<boolean> | boolean;
  queryKey?: QueryKey;
  onSuccess?: (data: InfiniteData<BaseData<T>>) => void;
  onRefetch?: () => any;
  useLocalData?: boolean;
  refeshDidShow?: boolean;
  select?: (data: InfiniteData<BaseData<T>>) => InfiniteData<BaseData<T>>;
};
// QueryFunction<BaseData<T>, (string | DeepUnwrapRef<UnwrapNestedRefs<TExtraParams>>)[], any>
export function useInfiniteLoading<T, TExtraParams extends ExtraParams>(
  service: Service<BaseData<T>, TExtraParams>,
  options: UseInfiniteLoadingOptions<T, TExtraParams> = {}
) {
  const {
    defaultExtraParams = {} as TExtraParams,
    enabled = ref(true),
    queryKey,
    onSuccess,
    onRefetch: _onRefetch,
    useLocalData = false,
    refeshDidShow = true,
    select,
  } = options;
  const extraParamState = reactive({
    ...defaultExtraParams,
  });
  const localData = ref({
    pageParams: [],
    pages: [],
  });
  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    isLoading,
    isError,
    refetch,
    isInitialLoading,
    fetchPreviousPage,
    hasPreviousPage,
  } = useInfiniteQuery({
    queryKey: queryKey,
    queryFn: ({ pageParam = 1, signal }) => {
      return service({ pageParam, signal }, extraParamState);
    },
    // initialPageParam: 1,
    getNextPageParam: (lastPage, pages) => {
      if (!lastPage) return 1;
      if (
        (lastPage.pageModel.page - 1) * lastPage.pageModel.rows + lastPage.data.length <
          lastPage.pageModel.totalCount &&
        lastPage.pageModel.totalCount > 0
      ) {
        return lastPage.pageModel.page + 1;
      }
    },
    getPreviousPageParam: (firstPage, pages) => {
      if (!firstPage) return 1;
      if (
        (firstPage.pageModel.page - 1) * firstPage.pageModel.rows + firstPage.data.length <
          firstPage.pageModel.totalCount &&
        firstPage.pageModel.totalCount > 0
      ) {
        return firstPage.pageModel.page + 1;
      }
    },
    enabled: enabled,
    onSuccess(data) {
      // console.log('data2: ', data);
      localData.value.pageParams = data.pageParams;
      //@ts-ignore
      localData.value.pages = data.pages;
      onSuccess?.(data);
    },
    select(data) {
      return select ? select(data) : data;
    },
  });
  const _data = computed<InfiniteData<BaseData<T>>>(() =>
    useLocalData ? (localData.value as any) : data.value
  );
  const queryClient = useQueryClient();
  const { mutateAsync } = useMutation({
    mutationFn: ({ pageParam }: ServiceContext) => fetchNextPage(pageParam),
    // onSettled: async () => {
    //   return await queryClient.invalidateQueries({ queryKey: queryKey });
    // },
  });
  const { mutate: setListItem } = useMutation({
    mutationFn: (data: { dataKey?: string; data: Partial<T> }) => {
      return Promise.resolve(data);
    },
    onSuccess: ({ dataKey = 'id', data }) => {
      console.log('dataKey: ', data);
      const pagesArray: InfiniteData<BaseData<T>> = useLocalData
        ? localData.value
        : queryClient.getQueryData(queryKey);
      console.log('pagesArray: ', pagesArray);
      const newPagesArray =
        pagesArray?.pages.map((page) => {
          return {
            ...page,
            data: page.data.map((item) => {
              if (item[dataKey] === data[dataKey]) {
                return {
                  ...item,
                  ...data,
                };
              } else {
                return item;
              }
            }),
          };
        }) ?? [];
      if (useLocalData) {
        console.log('newPagesArray: ', newPagesArray);
        localData.value.pageParams = pagesArray.pageParams;
        localData.value.pages = newPagesArray;
      } else {
        queryClient.setQueryData(queryKey, () => ({
          pages: newPagesArray,
          pageParams: pagesArray.pageParams,
        }));
      }
    },
  });
  function onRefetch(options?: RefetchOptions & RefetchQueryFilters<unknown>) {
    _onRefetch?.();
    return refetch(options);
  }
  const infiniteLoadingProps = computed(() => ({
    fetchNextPage,
    listData: _data.value,
    flattenListData: flattenListData.value,
    // error: error.value,
    hasMore: hasNextPage.value,
    isFetching: isFetching.value,
    isFetchingNextPage: isFetchingNextPage.value,
    isLoading: isLoading.value,
    isError: !!isError.value,
    refetch: onRefetch,
  }));
  const flattenListData = computed(() => {
    let list: BaseData<T>['data'] = [];
    if (data && _data.value) {
      _data.value?.pages.forEach((group) => {
        group.data.forEach((item) => {
          list.push(item);
        });
      });
    }
    return list;
  });
  if (refeshDidShow) {
    useRefeshDidShow({ queryKey: queryKey });
  }
  const infiniteLoadingRef = ref<{ backToTop(): void; scrollToBottom(dis?: number): void }>();
  function setByIndex(options: InfiniteGroupOptions = {}) {
    refetch({ refetchPage: (page, index) => index === options.groupIndex, type: 'inactive' });
  }
  function updatePageByIndex(options: InfiniteGroupOptions = {}) {
    refetch({ refetchPage: (page, index) => index === options.groupIndex, type: 'inactive' });
  }
  function remove() {
    queryClient.invalidateQueries({ queryKey: queryKey });
  }
  function invalidateQueries() {
    return queryClient.invalidateQueries({ queryKey: queryKey });
  }
  const listActions = {
    setByIndex,
    updatePageByIndex,
    remove,
  };
  return {
    infiniteLoadingProps,
    flattenListData,
    extraParamState,
    mutateAsync,
    setListItem,
    cancel: () => queryClient.cancelQueries(queryKey),
    infiniteLoadingRef,
    listActions,
    invalidateQueries,
    fetchPreviousPage,
    hasPreviousPage,
  };
}
export type ListActionsType = {
  setByIndex: (options?: InfiniteGroupOptions) => void;
  updatePageByIndex: (options?: InfiniteGroupOptions) => void;
  remove: () => void;
};
type UseRefeshDidShowOptions = {
  queryKey: QueryKey;
};
export function useRefeshDidShow({ queryKey }: UseRefeshDidShowOptions) {
  const queryClient = useQueryClient();
  const showUpdate = ref(false);
  Taro.useDidShow(() => {
    if (showUpdate.value) {
      queryClient.invalidateQueries({
        queryKey: queryKey,
      });
    }
  });
  Taro.useDidHide(() => {
    showUpdate.value = true;
  });
  Taro.useUnload(() => {
    showUpdate.value = false;
  });
}
packages/components/src/index.ts
@@ -1,7 +1,8 @@
export { default as RechargeGrid } from './views/RechargeGrid/RechargeGrid.vue';
export { default as Order } from './views/Order/Order.vue';
export { default as PhoneBillRecharge } from './views/PhoneBillRecharge/PhoneBillRecharge.vue';
export { default as electricBillRecharge } from './views/electricBillRecharge/electricBillRecharge.vue';
export { default as SelectPayTypeView } from './views/SelectPayTypeView/SelectPayTypeView.vue';
export { default as RechargeResultView } from './views/RechargeResultView/RechargeResultView.vue';
export { default as PhoneOrder } from './views/Order/components/PhoneOrder.vue';
export { default as ElectricOrder } from './views/Order/components/ElectricOrder.vue';
export * from './utils';
packages/components/src/styles/index.scss
@@ -5,6 +5,7 @@
@use './layout.scss' as *;
@use './rechargeGrid.scss' as *;
@use './components.scss' as *;
@use './loading.scss' as *;
:root,
page {
packages/components/src/styles/loading.scss
New file
@@ -0,0 +1,43 @@
@use './common.scss' as *;
.no-data-wrapper {
  display: flex;
  width: 100%;
  height: 100%;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  .no-data-img {
    width: 284px;
    height: 202px;
  }
  .no-data-text {
    font-size: 20px;
    color: #333333;
    line-height: 2.4;
  }
}
.loading-layout-loading-content-wrapper {
  padding: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  .nut-icon {
    color: boleGetCssVar('text-color', 'primary');
    margin-right: 8px;
  }
  .list-empty-hint-text {
    line-height: 1;
    font-size: 30px;
    color: boleGetCssVar('text-color', 'primary');
  }
}
.loading-layout-error-wrapper {
  height: 100%;
}
packages/components/src/styles/rechargeGrid.scss
@@ -24,7 +24,7 @@
.parValue-radio-group {
  width: 100%;
  display: grid;
  display: grid !important;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 10px;
packages/components/src/utils/lifeRecharge.ts
@@ -5,31 +5,37 @@
} from './lifeRechargeServices';
import { IRequest, BlLifeRechargeOptions } from './types';
import { LifeRechargeConstants } from './lifeRechargeConstants';
import { BlLifeRechargeAccountModel } from './lifeRechargeAccountModel';
export class BlLifeRecharge<T extends IRequest = IRequest> {
  services: BlLifeRechargeServices<T>;
  userId = '';
  accountModel: BlLifeRechargeAccountModel;
  static constants = LifeRechargeConstants;
  constants = LifeRechargeConstants;
  constructor(options: BlLifeRechargeOptions<T>) {
    this.services = new BlLifeRechargeServices(options);
    this.userId = options.userId || '';
    this.accountModel = new BlLifeRechargeAccountModel({
      userId: options.userId,
      phoneNumber: options.phoneNumber,
    });
  }
  async login(body: PhoneMesssageCodeLoginInput, options?: RequestConfig) {
    let res = await this.services.lifePayPhoneMesssageCodeLogin(body, options);
    this.userId = res;
    this.accountModel.setUserId(res);
    this.accountModel.setPhoneNumber(body.phoneNumber);
    return res;
  }
  loginout() {
    this.userId = '';
    this.accountModel.setUserId('');
    this.accountModel.setPhoneNumber('');
  }
  isLogin() {
    return !!this.userId;
    return !!this.accountModel.userId;
  }
  getRechargeParValue(amount: number, rate: number) {
packages/components/src/utils/lifeRechargeAccountModel.ts
New file
@@ -0,0 +1,20 @@
import { BlLifeRechargeAccountModelOptions } from './types';
export class BlLifeRechargeAccountModel {
  userId = '';
  phoneNumber = '';
  constructor(options: BlLifeRechargeAccountModelOptions = {}) {
    const { userId, phoneNumber } = options;
    this.setUserId(userId);
    this.setPhoneNumber(phoneNumber);
  }
  setUserId(userId: string) {
    this.userId = userId;
  }
  setPhoneNumber(phoneNumber: string) {
    this.phoneNumber = phoneNumber;
  }
}
packages/components/src/utils/lifeRechargeServices.ts
@@ -99,6 +99,18 @@
      ...(options || {}),
    });
  }
  /** 获取我的订单分页数据 POST /api/LifePay/GetUserLifePayOrderPage */
  async getUserLifePayOrderPage(body: QueryLifePayOrderListInput, options?: RequestConfig) {
    return this.request<UserLifePayOrderOutputPageOutput>('/api/LifePay/GetUserLifePayOrderPage', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      data: body,
      ...(options || {}),
    });
  }
}
export interface PhoneMesssageCodeLoginInput {
@@ -210,3 +222,61 @@
  orderNo: string;
  lifePayType?: LifeRechargeConstants.LifePayTypeEnum;
}
export interface QueryLifePayOrderListInput {
  pageModel?: Pagination;
  lifePayOrderType?: LifeRechargeConstants.LifePayOrderTypeEnum;
  /** 开始支付时间 */
  beginPayTime?: string;
  /** 结束支付时间 */
  endPayTime?: string;
  payStatus?: LifeRechargeConstants.LifePayStatusEnum;
  lifePayOrderStatus?: LifeRechargeConstants.LifePayOrderStatusEnum;
  /** 开始完成时间 */
  beginFinishTime?: string;
  /** 结束完成时间 */
  endFinishTime?: string;
  /** 用户Id */
  userId?: string;
}
export interface Pagination {
  rows?: number;
  page?: number;
  orderInput?: OrderInput[];
  totalCount?: number;
  totalPage?: number;
}
export interface OrderInput {
  property?: string;
  order?: any;
}
export interface UserLifePayOrderOutputPageOutput {
  pageModel?: Pagination;
  objectData?: any;
  data?: UserLifePayOrderOutput[];
}
export interface UserLifePayOrderOutput {
  id?: string;
  lifePayType?: LifeRechargeConstants.LifePayTypeEnum;
  lifePayOrderType?: LifeRechargeConstants.LifePayOrderTypeEnum;
  /** 订单号 */
  orderNo?: string;
  /** 充值金额 */
  rechargeAmount?: number;
  /** 优惠金额 */
  discountAmount?: number;
  /** 实付金额 */
  payAmount?: number;
  /** 支付时间 */
  payTime?: string;
  payStatus?: LifeRechargeConstants.LifePayStatusEnum;
  lifePayOrderStatus?: LifeRechargeConstants.LifePayOrderStatusEnum;
  /** 完成时间 */
  finishTime?: string;
  /** 订单详细数据 */
  orderParamDetailJsonStr?: string;
}
packages/components/src/utils/types.ts
@@ -10,6 +10,10 @@
  request: T;
};
export type BlLifeRechargeOptions<T extends IRequest> = BlLifeRechargeServicesOptions<T> & {
export type BlLifeRechargeAccountModelOptions = {
  userId?: string;
  phoneNumber?: string;
};
export type BlLifeRechargeOptions<T extends IRequest> = BlLifeRechargeServicesOptions<T> &
  BlLifeRechargeAccountModelOptions & {};
packages/components/src/views/Order/components/PhoneOrder.vue
@@ -1,23 +1,25 @@
<template>
  <!-- <InfiniteLoading scrollViewClassName="common-infinite-scroll-list" v-bind="infiniteLoadingProps">
    <template #renderItem="{ item }"> -->
  <OrderCard title="话费充值" status="待支付">
    <OrderCardItem label="充值账号:" :value="'18888888888'" />
    <OrderCardItem label="下单时间:" :value="'18888888888'" />
    <OrderCardItem label="充值金额:" :value="'18888888888'" />
    <OrderCardItem label="优惠金额:" :value="'18888888888'" />
    <OrderCardItem label="实付金额:" :value="'18888888888'" />
    <OrderCardItem label="支付时间:" :value="'2025-02-19  17:15:54'" />
    <OrderCardItem label="完成时间:" :value="'2025-02-19  17:15:54'" />
  </OrderCard>
  <!-- </template>
  </InfiniteLoading> -->
  <InfiniteLoading scrollViewClassName="common-infinite-scroll-list" v-bind="infiniteLoadingProps">
    <template #renderItem="{ item }">
      <OrderCard title="话费充值" status="待支付">
        <OrderCardItem label="充值账号:" :value="'18888888888'" />
        <OrderCardItem label="下单时间:" :value="'18888888888'" />
        <OrderCardItem label="充值金额:" :value="'18888888888'" />
        <OrderCardItem label="优惠金额:" :value="'18888888888'" />
        <OrderCardItem label="实付金额:" :value="'18888888888'" />
        <OrderCardItem label="支付时间:" :value="'2025-02-19  17:15:54'" />
        <OrderCardItem label="完成时间:" :value="'2025-02-19  17:15:54'" />
      </OrderCard>
    </template>
  </InfiniteLoading>
</template>
<script setup lang="ts">
import InfiniteLoading from '../../../components/InfiniteLoading/InfiniteLoading.vue';
import OrderCard from '../../../components/Card/OrderCard.vue';
import OrderCardItem from '../../../components/Card/OrderCardItem.vue';
import { useGetUserLifePayOrderPage } from '../../../hooks';
import { BlLifeRecharge } from '../../../utils';
defineOptions({
  name: 'PhoneOrder',
@@ -27,7 +29,7 @@
// const props = withDefaults(defineProps<Props>(), {});
const infiniteLoadingProps = {};
const { infiniteLoadingProps } = useGetUserLifePayOrderPage({
  lifePayOrderType: BlLifeRecharge.constants.LifePayOrderTypeEnum.话费订单,
});
</script>
<style lang="scss"></style>
packages/components/src/views/PhoneBillRecharge/PhoneBillRecharge.vue
@@ -176,7 +176,7 @@
async function goPay() {
  try {
    let params: LifePhoneDataCreateLifePayOrderInput = {
      userId: blLifeRecharge.userId,
      userId: blLifeRecharge.accountModel.userId,
      productData: {
        ispCode: form.ispCode,
        parValue: 0.1,
packages/components/src/views/RechargeResultView/RechargeResultView.vue
@@ -1,6 +1,6 @@
<template>
  <div class="recharge-result-view">
    <div class="recharge-result-view-title">支付成功,充值款将在0-24小时内到账</div>
    <div class="recharge-result-view-title">{{ title }}</div>
    <div class="recharge-result-view-tips">
      同一号码充值期间,未到账前切勿在其他任何平台再次充值。因此造成的资金损失须用户自行承担!!!
    </div>
@@ -14,13 +14,24 @@
</template>
<script setup lang="ts">
import Taro from '@tarojs/taro';
import { BlLifeRecharge } from '@life-payment/components';
defineOptions({
  name: 'RechargeResultView',
});
// type Props = {};
type Props = {
  title?: string;
};
// const props = withDefaults(defineProps<Props>(), {});
const props = withDefaults(defineProps<Props>(), {
  title: '支付成功,充值款将在0-24小时内到账',
});
const router = Taro.useRouter();
const orderNo = router.params?.orderNo ?? '';
const lifePayOrderType = Number(router.params?.lifePayOrderType ?? '');
const emit = defineEmits<{
  (e: 'goBackHome'): void;
packages/components/src/views/SelectPayTypeView/SelectPayTypeView.vue
@@ -31,11 +31,16 @@
// const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{
  (e: 'paySuccess', id: number): void;
  (
    e: 'paySuccess',
    orderNo: string,
    lifePayOrderType: LifeRechargeConstants.LifePayOrderTypeEnum
  ): void;
}>();
const router = Taro.useRouter();
const orderNo = router.params?.orderNo ?? '';
const lifePayOrderType = Number(router.params?.lifePayOrderType ?? '');
const { blLifeRecharge } = useLifeRechargeContext();
@@ -58,22 +63,23 @@
  } catch (error) {}
}
// useQuery({
//   queryKey: ['platformServicePayServices/getPlaformServicePayQRCode', orderNo],
//   queryFn: async () => {
//     return await blLifeRecharge.services.getPayStatusByOrderNo(
//       {
//         orderNo,
//       },
//       {
//         showLoading: false,
//       }
//     );
//   },
//   onSuccess(data) {
//     if (data === blLifeRecharge.constants.LifePayStatusEnum.已支付) {
//     }
//   },
//   refetchInterval: 1000 * 3,
// });
useQuery({
  queryKey: ['platformServicePayServices/getPayStatusByOrderNo', orderNo],
  queryFn: async () => {
    return await blLifeRecharge.services.getPayStatusByOrderNo(
      {
        orderNo,
      },
      {
        showLoading: false,
      }
    );
  },
  onSuccess(data) {
    if (data === blLifeRecharge.constants.LifePayStatusEnum.已支付) {
      emit('paySuccess', orderNo, lifePayOrderType);
    }
  },
  refetchInterval: 1000 * 3,
});
</script>