wupengfei
2025-02-21 6ab8060d51e7eda0006943057f1d983d0d44abea
feat: 电费
9个文件已添加
7个文件已修改
488 ■■■■■ 已修改文件
apps/taro/src/components/Input/ChooseInput.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Input/ChooseInputWithDatePicker.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Input/ChooseInputWithPicker.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Input/CommonInputField.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Input/NumberInput.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Input/input.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/pages/mine/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/subpackages/recharge/electricBillRecharge/InnerPage.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/Dialog/ConfirmDialog.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/Input/ChooseInput.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/Input/ChooseInputWithPicker.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/components/Input/input.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/styles/rechargeGrid.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/views/PhoneBillRecharge/PhoneBillRecharge.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
packages/components/src/views/electricBillRecharge/electricBillRecharge.vue 62 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
apps/taro/src/components/Input/ChooseInput.vue
New file
@@ -0,0 +1,37 @@
<template>
  <nut-input
    class="nut-input-text bole-input-text"
    type="text"
    readonly
    alwaysEmbed
    v-bind="$attrs"
  >
    <template #clear>
      <slot name="clear"></slot>
    </template>
    <template #right>
      <slot name="right">
        <RectRight :size="12" class="common-choose-input-icon" />
      </slot>
    </template>
  </nut-input>
</template>
<script setup lang="ts">
import { RectRight } from '@nutui/icons-vue-taro';
defineOptions({
  name: 'ChooseInput',
});
</script>
<style lang="scss">
@import '@/styles/common.scss';
.common-choose-input-icon {
  //   width: 13px;
  //   height: 23px;
  margin-left: 18px;
  color: boleGetCssVar('text-color', 'primary');
}
</style>
apps/taro/src/components/Input/ChooseInputWithDatePicker.vue
New file
@@ -0,0 +1,58 @@
<template>
  <ChooseInput :modelValue="modelValue" @click="handleOpen()"></ChooseInput>
</template>
<script setup lang="ts">
import ChooseInput from './ChooseInput.vue';
import { Popup, DatePicker } from '@nutui/nutui-taro';
import { Portal } from 'senin-mini/components';
import { h } from 'vue';
import dayjs from 'dayjs';
defineOptions({
  name: 'ChooseInputWithDatePicker',
});
type Props = {
  modelValue: string | number;
};
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{
  (e: 'update:modelValue', val: string | number): void;
}>();
function handleOpen() {
  const _modelValue = [props.modelValue];
  Portal.add((key) => {
    return h(
      Portal.Container,
      { keyNumber: key, delayOpen: true },
      {
        default: ({ open, onClose }) =>
          h(
            Popup,
            {
              visible: open.value,
              'onUpdate:visible': (value) => !value && onClose(),
              position: 'bottom',
            },
            {
              default: () =>
                h(DatePicker, {
                  modelValue: _modelValue,
                  onCancel: onClose,
                  onConfirm: ({ selectedValue }) => {
                    console.log('selectedValue: ', selectedValue);
                    emit('update:modelValue', dayjs(selectedValue).format('YYYY-MM-DD'));
                    onClose();
                  },
                }),
            }
          ),
      }
    );
  });
}
</script>
apps/taro/src/components/Input/ChooseInputWithPicker.vue
New file
@@ -0,0 +1,73 @@
<template>
  <ChooseInput :modelValue="inputValue" @click="handleOpen()"></ChooseInput>
</template>
<script setup lang="ts">
import ChooseInput from './ChooseInput.vue';
import { Popup, Picker } from '@nutui/nutui-taro';
import { convertOptions, ValueEnum } from 'senin-mini/utils';
import { Portal } from 'senin-mini/components';
import { computed, h } from 'vue';
defineOptions({
  name: 'ChooseInputWithPicker',
});
type Props = {
  enumLabelKey?: string;
  enumValueKey?: string;
  valueEnum?: ValueEnum;
  modelValue: string | number;
};
const props = withDefaults(defineProps<Props>(), {
  enumLabelKey: 'name',
  enumValueKey: 'id',
});
const emit = defineEmits<{
  (e: 'update:modelValue', val: string | number): void;
}>();
const options = computed(() =>
  convertOptions(props.valueEnum, props.enumLabelKey, props.enumValueKey)
);
const inputValue = computed(
  () => options.value?.find((x) => x.value === props.modelValue)?.text ?? ''
);
function handleOpen() {
  const _modelValue = [props.modelValue];
  Portal.add((key) => {
    return h(
      Portal.Container,
      { keyNumber: key, delayOpen: true },
      {
        default: ({ open, onClose }) =>
          h(
            Popup,
            {
              visible: open.value,
              'onUpdate:visible': (value) => !value && onClose(),
              position: 'bottom',
            },
            {
              default: () =>
                h(Picker, {
                  modelValue: _modelValue,
                  columns: options.value,
                  onCancel: onClose,
                  onConfirm: ({ selectedValue, selectedOptions }) => {
                    console.log('selectedValue: ', selectedValue, selectedOptions);
                    emit('update:modelValue', selectedOptions[0].value);
                    onClose();
                  },
                }),
            }
          ),
      }
    );
  });
}
</script>
apps/taro/src/components/Input/CommonInputField.vue
New file
@@ -0,0 +1,39 @@
<template>
  <div class="common-input-field-wrapper">
    <div class="common-input-field">
      <slot></slot>
    </div>
    <RectRight :size="12" class="common-input-field-icon" />
  </div>
</template>
<script setup lang="ts">
import { RectRight } from '@nutui/icons-vue-taro';
defineOptions({
  name: 'CommonInputField',
});
// type Props = {};
// const props = withDefaults(defineProps<Props>(), {});
</script>
<style lang="scss">
@import '@/styles/common.scss';
.common-input-field-wrapper {
  display: flex;
  width: 100%;
  .common-input-field {
    flex: 1;
    min-width: 0;
  }
  .common-input-field-icon {
    margin-left: 18px;
    color: boleGetCssVar('text-color', 'primary');
  }
}
</style>
apps/taro/src/components/Input/NumberInput.vue
New file
@@ -0,0 +1,71 @@
<template>
  <nut-input type="number" :formatter="formatter" formatTrigger="onBlur" v-model="innerModelValue">
    <template #right>
      <slot name="right"></slot>
    </template>
  </nut-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
defineOptions({
  name: 'NumberInput',
});
type Props = {
  min?: number;
  max?: number;
  precision?: number;
  modelValue?: number | string;
};
const props = withDefaults(defineProps<Props>(), {
  max: Math.pow(2, 53) - 1,
});
const emit = defineEmits<{
  (e: 'update:modelValue', val: string | number): void;
}>();
const innerModelValue = computed({
  get() {
    return props.modelValue + '';
  },
  set(val) {
    emit('update:modelValue', val);
  },
});
function formatter(value: string): string {
  const newVal = value !== '' ? Number.parseFloat(value) : '';
  if (Number.isNaN(newVal)) {
    return '';
  }
  if (newVal && newVal > props.max) {
    return `${toPrecision(props.max)}`;
  }
  if (props.min !== undefined && !!`${newVal}` && (newVal as number) < props.min) {
    return `${toPrecision(props.min)}`;
  }
  return newVal !== '' ? `${toPrecision(newVal)}` : newVal;
}
function toPrecision(num: number) {
  if (props.precision) {
    if (props.precision === 0) return Math.round(num);
    let snum = String(num);
    const pointPos = snum.indexOf('.');
    if (pointPos === -1) return num;
    const nums = snum.replace('.', '').split('');
    const datum = nums[pointPos + props.precision];
    if (!datum) return num;
    const length = snum.length;
    if (snum.charAt(length - 1) === '5') {
      snum = `${snum.slice(0, Math.max(0, length - 1))}6`;
    }
    return Number(snum).toFixed(props.precision);
  }
  return String(num);
}
</script>
apps/taro/src/components/Input/input.ts
New file
@@ -0,0 +1,4 @@
export type ChooseCheckBoxOptionItem = {
  text: string;
  value: string | number;
};
apps/taro/src/pages/mine/index.vue
@@ -18,8 +18,9 @@
      </div>
    </div>
    <ContentScrollView>
      <List class="mine-list-wrapper">
      <List class="mine-list-wrapper" v-if="isLogin">
        <ListItem title="订单管理" @click="goOrderManage"></ListItem>
        <ListItem title="退出登录" @click="goLogout"></ListItem>
      </List>
    </ContentScrollView>
  </PageLayoutWithBg>
@@ -33,10 +34,13 @@
import DefaultAvatar from '@/assets/components/icon-default-avatar.png';
import { useSystemStore } from '@/stores/modules/system';
import PageLayoutWithBg from '@/components/Layout/PageLayoutWithBg.vue';
import { useUserStore } from '@/stores/modules/user';
import { useUserStoreWithOut } from '@/stores/modules/user';
const { userDetail } = useUser();
const isLogin = useIsLogin();
const systemStore = useSystemStore();
const userStore = useUserStore();
const { goLoginFn } = useGoLogin();
const bgHeight = computed(() => 133 + systemStore.navHeight);
@@ -66,6 +70,11 @@
});
function goOrderManage() {}
function goLogout() {
  userStore.logout();
  useUserStoreWithOut().logout();
}
</script>
<style lang="scss">
apps/taro/src/subpackages/recharge/electricBillRecharge/InnerPage.vue
@@ -1,11 +1,11 @@
<template>
  <ContentScrollView :paddingH="false">
    <PhoneBillRecharge @goPay="goPay" />
    <electricBillRecharge @goPay="goPay" />
  </ContentScrollView>
</template>
<script setup lang="ts">
import { PhoneBillRecharge } from '@life-payment/components';
import { electricBillRecharge } from '@life-payment/components';
import Taro from '@tarojs/taro';
defineOptions({
packages/components/src/components/Dialog/ConfirmDialog.vue
@@ -1,14 +1,18 @@
<template>
  <nut-dialog title="请核对充值信息并支付" v-model:visible="visible">
  <nut-dialog title="请核对充值信息并支付" v-model:visible="visible" class="confirm-dialog-wrapper">
    <div class="confirm-dialog-content">
      <div class="confirm-dialog-content-tips">
        该产品为慢充模式,0-24小时内到账,介意请勿付款! 充值前请仔细阅读充值须知!
        <slot name="tips">
          该产品为慢充模式,0-24小时内到账,介意请勿付款! 充值前请仔细阅读充值须知!
        </slot>
      </div>
      <div class="confirm-dialog-content-info">
        <slot name="info"></slot>
      </div>
      <div class="confirm-dialog-content-warning">
        同一号码充值期间,未到账前切勿在其他任何平台再次充值。因此造成的资金损失须用户自行承担!!!
        <slot name="warning">
          同一号码充值期间,未到账前切勿在其他任何平台再次充值。因此造成的资金损失须用户自行承担!!!
        </slot>
      </div>
    </div>
  </nut-dialog>
packages/components/src/components/Input/ChooseInput.vue
New file
@@ -0,0 +1,37 @@
<template>
  <nut-input
    class="nut-input-text bole-input-text"
    type="text"
    readonly
    alwaysEmbed
    v-bind="$attrs"
  >
    <template #clear>
      <slot name="clear"></slot>
    </template>
    <template #right>
      <slot name="right">
        <RectRight :size="12" class="common-choose-input-icon" />
      </slot>
    </template>
  </nut-input>
</template>
<script setup lang="ts">
import { RectRight } from '@nutui/icons-vue-taro';
defineOptions({
  name: 'ChooseInput',
});
</script>
<style lang="scss">
@import '@/styles/common.scss';
.common-choose-input-icon {
  //   width: 13px;
  //   height: 23px;
  margin-left: 18px;
  color: boleGetCssVar('text-color', 'primary');
}
</style>
packages/components/src/components/Input/ChooseInputWithPicker.vue
New file
@@ -0,0 +1,73 @@
<template>
  <ChooseInput :modelValue="inputValue" @click="handleOpen()"></ChooseInput>
</template>
<script setup lang="ts">
import ChooseInput from './ChooseInput.vue';
import { Popup, Picker } from '@nutui/nutui-taro';
import { convertOptions, ValueEnum } from 'senin-mini/utils';
import { Portal } from 'senin-mini/components';
import { computed, h } from 'vue';
defineOptions({
  name: 'ChooseInputWithPicker',
});
type Props = {
  enumLabelKey?: string;
  enumValueKey?: string;
  valueEnum?: ValueEnum;
  modelValue: string | number;
};
const props = withDefaults(defineProps<Props>(), {
  enumLabelKey: 'name',
  enumValueKey: 'id',
});
const emit = defineEmits<{
  (e: 'update:modelValue', val: string | number): void;
}>();
const options = computed(() =>
  convertOptions(props.valueEnum, props.enumLabelKey, props.enumValueKey)
);
const inputValue = computed(
  () => options.value?.find((x) => x.value === props.modelValue)?.text ?? ''
);
function handleOpen() {
  const _modelValue = [props.modelValue];
  Portal.add((key) => {
    return h(
      Portal.Container,
      { keyNumber: key, delayOpen: true },
      {
        default: ({ open, onClose }) =>
          h(
            Popup,
            {
              visible: open.value,
              'onUpdate:visible': (value) => !value && onClose(),
              position: 'bottom',
            },
            {
              default: () =>
                h(Picker, {
                  modelValue: _modelValue,
                  columns: options.value,
                  onCancel: onClose,
                  onConfirm: ({ selectedValue, selectedOptions }) => {
                    console.log('selectedValue: ', selectedValue, selectedOptions);
                    emit('update:modelValue', selectedOptions[0].value);
                    onClose();
                  },
                }),
            }
          ),
      }
    );
  });
}
</script>
packages/components/src/components/Input/input.ts
New file
@@ -0,0 +1,4 @@
export type ChooseCheckBoxOptionItem = {
  text: string;
  value: string | number;
};
packages/components/src/index.ts
@@ -1,5 +1,6 @@
export { default as RechargeGrid } from './views/RechargeGrid/RechargeGrid.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 * from './utils';
packages/components/src/styles/rechargeGrid.scss
@@ -118,7 +118,7 @@
  }
}
.phone-bill-recharge {
.order-bill-recharge {
  .recharge-button {
    width: 100%;
    margin: 20px 0;
packages/components/src/views/PhoneBillRecharge/PhoneBillRecharge.vue
@@ -4,7 +4,7 @@
    ref="formRef"
    :rules="rules"
    label-position="top"
    class="phone-bill-recharge"
    class="order-bill-recharge phone"
  >
    <FormItem label="选择运营商:" class="bole-form-item" prop="ispCode" required>
      <RadioGroup v-model="form.ispCode" direction="horizontal">
packages/components/src/views/electricBillRecharge/electricBillRecharge.vue
@@ -4,13 +4,34 @@
    ref="formRef"
    :rules="rules"
    label-position="top"
    class="phone-bill-recharge"
    class="order-bill-recharge electric"
  >
    <FormItem label="充值手机号" class="bole-form-item" prop="phone" required>
    <FormItem label="所在城市" class="bole-form-item" prop="type" required>
      <ChooseInputWithPicker
        v-model="form.type"
        placeholder="请选择城市"
        :value-enum="IspCodeText"
      />
    </FormItem>
    <FormItem label="电网类型" class="bole-form-item" prop="type" required>
      <ChooseInputWithPicker
        v-model="form.type"
        placeholder="请选择电网类型"
        :value-enum="IspCodeText"
      />
    </FormItem>
    <FormItem label="电费类型" class="bole-form-item" prop="type" required>
      <ChooseInputWithPicker
        v-model="form.type"
        placeholder="请选择电费类型"
        :value-enum="IspCodeText"
      />
    </FormItem>
    <FormItem label="电网户号" class="bole-form-item" prop="phone" required>
      <Input
        v-model.trim="form.phone"
        class="bole-input-text"
        placeholder="请填写您需要充值的手机号码"
        placeholder="请输入13位数字编号"
        type="text"
      />
    </FormItem>
@@ -40,18 +61,25 @@
    <div class="common-content">
      <nut-button class="recharge-button" type="primary" @click="recharge">
        <div class="recharge-button-inner">
          <div>¥{{ form.parValue }}</div>
          <div>¥{{ realParValue }}</div>
          <div class="recharge-button-text">立即充值</div>
        </div>
      </nut-button>
      <RechargeTipsView :tips="tips" />
    </div>
    <ConfirmDialog v-model:visible="confirmDialogVisible" @ok="goPay">
      <template #tips>
        该产品为慢充模式,0-72小时内到账,介意请勿付款!充值前请仔细阅读充值须知!
      </template>
      <template #info>
        <ConfirmDialogInfoItem label="充值账号" content="18858418480" />
        <ConfirmDialogInfoItem label="电网类型" content="国家电网" />
        <ConfirmDialogInfoItem label="电费类型" content="住宅" />
        <ConfirmDialogInfoItem label="充值金额" :content="`¥${form.parValue}`" danger />
        <ConfirmDialogInfoItem label="优惠金额" :content="`¥${discountParValue}`" />
        <ConfirmDialogInfoItem label="实付金额" :content="`¥${realParValue}`" danger />
      </template>
      <template #warning>
        同一电费账户在充值期间,未到账前切勿在其他任何平台再次充值。因此造成的资金损失须用户自行承担!!!
      </template>
    </ConfirmDialog>
  </Form>
@@ -67,6 +95,7 @@
import RechargeTipsView from '../../components/RechargeTipsView/RechargeTipsView.vue';
import ConfirmDialog from '../../components/Dialog/ConfirmDialog.vue';
import ConfirmDialogInfoItem from '../../components/Dialog/ConfirmDialogInfoItem.vue';
import ChooseInputWithPicker from '../../components/Input/ChooseInputWithPicker.vue';
defineOptions({
  name: 'electricBillRecharge',
@@ -80,11 +109,12 @@
  ispCode: IspCode.yidong,
  phone: '',
  parValue: 100,
  type: IspCodeText.yidong,
});
const rate = 0.96;
const parValueList = [50, 100, 200];
const parValueList = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1500, 2000, 3000];
const realParValue = computed(() => blLifeRecharge.getRechargeParValue(form.parValue, rate));
const discountParValue = computed(() => form.parValue - Number(realParValue.value));
@@ -104,12 +134,11 @@
}
const tips = [
  '平台提供慢充服务,订单提交后,话费将于0 - 24小时内到账。若未能按时到账,系统将自动发起退款。',
  '充值期间,若同一号码款项未到账,请勿在其他平台重复充值;主副卡不可同时充值。因上述操作导致的资金损失,由用户自行承担。',
  '本平台话费充值服务不适用于已停机号码。电信号码若有欠费,也无法完成充值。电信已完成维护的区域包括:广东、江苏、湖北、四川、江西、河北、河南、福建、辽宁。其它区域正在分批次进行维护中,在此期间可能会出现充值不成功并自动退款的情况,请您谅解。',
  '平台提供慢充服务,订单提交后,电费将于0 - 72 小时内到账,若未能按时到账,系统将自动发起退款。',
  '充值期间,若同一账户的充值款未到账,请勿在其他平台重复充值,因上述操作导致的资金损失,由用户自行承担。',
  '为确保充值顺利进行,目前平台不支持对欠款金额超过1000元的账户进行充值,且每次充值金额必须高于欠费总额。',
  '如接到陌生来电,对方以缴费或误操作等理由要求处理款项,务必立即拉黑,谨防诈骗。',
  '售后服务期为充值完成之日起3天。申请售后服务时,需提供录屏证据,请确认接受此要求后再下单,逾期平台不再受理售后申请。',
  '充值发票由运营商提供,您可登录网上营业厅下载电子发票。',
  '下单时,请您务必准确填写完整的省市及户号信息。充值完成后,发票由运营商提供,您可登录网上营业厅下载对应的电子发票。',
];
const confirmDialogVisible = ref(false);
@@ -122,3 +151,14 @@
  emit('goPay');
}
</script>
<style lang="scss">
.order-bill-recharge {
  &.electric {
    .nut-dialog {
      .nut-dialog__content {
        max-height: 700px;
      }
    }
  }
}
</style>