├── src ├── app.rn.scss ├── components │ ├── box-shadow │ │ ├── index.scss │ │ └── index.tsx │ ├── keyboard-aware-scroll-view │ │ ├── index.rn.scss │ │ ├── index.scss │ │ └── index.tsx │ ├── safe-area-view │ │ ├── index.rn.scss │ │ ├── index.scss │ │ ├── type.d.ts │ │ └── index.tsx │ ├── linear-gradient │ │ ├── index.scss │ │ ├── index.rn.scss │ │ └── index.tsx │ ├── modal │ │ ├── index.rn.scss │ │ ├── index.scss │ │ ├── constant.ts │ │ ├── type.d.ts │ │ ├── index.tsx │ │ └── index.rn.tsx │ ├── picker │ │ ├── index.rn.scss │ │ ├── type.d.ts │ │ ├── index.h5.tsx │ │ ├── index.scss │ │ ├── index.tsx │ │ └── index.weapp.tsx │ ├── status-bar │ │ └── index.tsx │ └── index.ts ├── assets │ └── images │ │ ├── yz_prop_icon_arrow.png │ │ ├── comm_form_icon_gouxuan.png │ │ ├── esf_comm_icon_nav_back.png │ │ ├── esf_calculator_img_mark.png │ │ ├── esf_comm_icon_nav_wechat.png │ │ ├── yz_prop_icon_arrow_white.png │ │ ├── comm_form_icon_weigouxuan.png │ │ ├── esf_calculator_icon_close.png │ │ ├── esf_calculator_img_building.png │ │ ├── esf_calculator_img_percent.png │ │ ├── esf_calculator_icon_question.png │ │ ├── esf_calculator_img_bggradient.png │ │ ├── comm_navbar_icon_back_black_transparent.png │ │ └── comm_navbar_icon_message_black_transparent.png ├── pages │ └── calculator │ │ ├── history │ │ ├── index.scss │ │ ├── index.config.ts │ │ ├── index.rn.scss │ │ └── index.tsx │ │ ├── index.config.ts │ │ ├── monthly-payments │ │ ├── index.config.ts │ │ ├── index.scss │ │ ├── index.rn.scss │ │ └── index.tsx │ │ ├── compute-header │ │ ├── index.scss │ │ ├── index.rn.scss │ │ └── index.tsx │ │ ├── index.scss │ │ ├── title-tpl.tsx │ │ ├── helper.ts │ │ ├── line-wrap.tsx │ │ ├── index.rn.scss │ │ ├── constants.ts │ │ └── index.tsx ├── utils │ ├── index.ts │ ├── global-data.ts │ ├── common.ts │ ├── across-api.ts │ └── is-type.tsx ├── app.scss ├── app.ts ├── app.config.ts └── index.html ├── .eslintignore ├── metro.config.js ├── example.png ├── .commitlintrc.js ├── h5-qrcode.png ├── mini-qrcode.jpg ├── config ├── dev.js ├── prod.js └── index.js ├── .editorconfig ├── babel.config.js ├── .npmrc ├── project.config.json ├── global.d.ts ├── LICENSE ├── .eslintrc ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── README.md ├── package.json └── .gitignore /src/app.rn.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/box-shadow/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | config 2 | *.config.js 3 | ios 4 | android -------------------------------------------------------------------------------- /src/components/keyboard-aware-scroll-view/index.rn.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // resetCache: true 3 | }; 4 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/example.png -------------------------------------------------------------------------------- /src/components/keyboard-aware-scroll-view/index.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | flex: 1 3 | } 4 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } -------------------------------------------------------------------------------- /h5-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/h5-qrcode.png -------------------------------------------------------------------------------- /src/components/safe-area-view/index.rn.scss: -------------------------------------------------------------------------------- 1 | .safe-area-view { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /mini-qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/mini-qrcode.jpg -------------------------------------------------------------------------------- /src/assets/images/yz_prop_icon_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/yz_prop_icon_arrow.png -------------------------------------------------------------------------------- /src/assets/images/comm_form_icon_gouxuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/comm_form_icon_gouxuan.png -------------------------------------------------------------------------------- /src/assets/images/esf_comm_icon_nav_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_comm_icon_nav_back.png -------------------------------------------------------------------------------- /src/assets/images/esf_calculator_img_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_calculator_img_mark.png -------------------------------------------------------------------------------- /src/assets/images/esf_comm_icon_nav_wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_comm_icon_nav_wechat.png -------------------------------------------------------------------------------- /src/assets/images/yz_prop_icon_arrow_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/yz_prop_icon_arrow_white.png -------------------------------------------------------------------------------- /src/assets/images/comm_form_icon_weigouxuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/comm_form_icon_weigouxuan.png -------------------------------------------------------------------------------- /src/assets/images/esf_calculator_icon_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_calculator_icon_close.png -------------------------------------------------------------------------------- /src/assets/images/esf_calculator_img_building.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_calculator_img_building.png -------------------------------------------------------------------------------- /src/assets/images/esf_calculator_img_percent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_calculator_img_percent.png -------------------------------------------------------------------------------- /src/assets/images/esf_calculator_icon_question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_calculator_icon_question.png -------------------------------------------------------------------------------- /src/assets/images/esf_calculator_img_bggradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/esf_calculator_img_bggradient.png -------------------------------------------------------------------------------- /src/components/linear-gradient/index.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | } 4 | 5 | .linear-gradient__box { 6 | width: 100%; 7 | @import './index.rn.scss'; 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/images/comm_navbar_icon_back_black_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/comm_navbar_icon_back_black_transparent.png -------------------------------------------------------------------------------- /src/assets/images/comm_navbar_icon_message_black_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Taro-Mortgage-Calculator/HEAD/src/assets/images/comm_navbar_icon_message_black_transparent.png -------------------------------------------------------------------------------- /config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"development"' 4 | }, 5 | defineConstants: { 6 | }, 7 | isWatch: true, 8 | mini: {}, 9 | h5: { 10 | esnextModules: ['taro-ui'] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/calculator/history/index.scss: -------------------------------------------------------------------------------- 1 | page { 2 | background-color: #ffffff; 3 | } 4 | 5 | .history { 6 | @import './index.rn.scss'; 7 | 8 | .wrap { 9 | border-style: none; 10 | border-bottom: 1px solid #E7EBEE; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/calculator/index.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '房贷计算器', 3 | navigationBarBackgroundColor: '#fff', 4 | navigationBarTextStyle: 'black', 5 | // @ts-ignore 6 | disableScroll: process.env.TARO_ENV === 'rn', 7 | }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel-preset-taro 更多选项和默认值: 2 | // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md 3 | module.exports = { 4 | presets: [ 5 | ['taro', { 6 | framework: 'react', 7 | ts: true 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/modal/index.rn.scss: -------------------------------------------------------------------------------- 1 | .taro-modal-content { 2 | position: relative; 3 | } 4 | 5 | .close-icon-box { 6 | position: absolute; 7 | right: 20px; 8 | top: -15px; 9 | z-index: 999; 10 | } 11 | .close-icon { 12 | width: 56px; 13 | height: 56px; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/modal/index.scss: -------------------------------------------------------------------------------- 1 | @import './index.rn.scss'; 2 | 3 | .at-modal-content { 4 | padding: 40px 30px 30px 30px; 5 | 6 | &-close-iocn { 7 | right: 20px; 8 | top: 20px; 9 | width: 56px; 10 | height: 56px; 11 | position: absolute; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/safe-area-view/index.scss: -------------------------------------------------------------------------------- 1 | .safe-area-view { 2 | background-color: #ffffff; 3 | padding-bottom: constant(safe-area-inset-bottom); 4 | padding-bottom: env(safe-area-inset-bottom); 5 | width: 100%; 6 | } 7 | 8 | taro-safe-area-view { 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/modal/constant.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:36:22 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import CLOSE_ICON from "../../assets/images/esf_calculator_icon_close.png"; 9 | 10 | export { CLOSE_ICON }; 11 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-11 00:05:19 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | export * from './across-api'; 9 | export * from './is-type'; 10 | export * from './global-data'; 11 | export * from './common'; 12 | -------------------------------------------------------------------------------- /src/components/linear-gradient/index.rn.scss: -------------------------------------------------------------------------------- 1 | .linear-gradient__box { 2 | position: relative; 3 | width: 100%; 4 | z-index: 2; 5 | } 6 | 7 | .linear-gradient__box__img { 8 | width: 100%; 9 | height: 100%; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | bottom: 0; 14 | right: 0; 15 | z-index: -1; 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/calculator/history/index.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-09 23:39:04 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | export default { 9 | navigationBarTitleText: "房贷计算历史", 10 | navigationBarTextStyle: "black", 11 | navigationBarBackgroundColor: "#fff" 12 | }; 13 | -------------------------------------------------------------------------------- /src/pages/calculator/monthly-payments/index.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-09 23:38:37 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | export default { 9 | navigationBarTitleText: "对比月供", 10 | navigationBarTextStyle: "black", 11 | navigationBarBackgroundColor: "#fff" 12 | }; 13 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | @import "~taro-ui/dist/style/index.scss"; 2 | 3 | 4 | /* #ifdef h5 */ 5 | * { 6 | box-sizing: border-box; 7 | } 8 | taro-view-core, 9 | taro-text-core, 10 | div { 11 | font-size: 32px; 12 | } 13 | /* #endif */ 14 | 15 | /* #ifdef weapp */ 16 | view, 17 | text { 18 | box-sizing: border-box; 19 | } 20 | /* #endif */ 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import "./app.scss"; 3 | 4 | class App extends Component { 5 | componentDidMount() {} 6 | 7 | componentDidShow() {} 8 | 9 | componentDidHide() {} 10 | 11 | componentDidCatchError() {} 12 | 13 | // this.props.children 是将要会渲染的页面 14 | render() { 15 | return this.props.children; 16 | } 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/pages/calculator/compute-header/index.scss: -------------------------------------------------------------------------------- 1 | @import './index.rn.scss'; 2 | 3 | ._h_l_c-header { 4 | &-header-box { 5 | z-index: 3; 6 | 7 | .weapp-header { 8 | box-sizing: border-box; 9 | } 10 | } 11 | 12 | &-loan-info { 13 | box-sizing: border-box; 14 | } 15 | 16 | &-loan-box { 17 | 18 | &__amount { 19 | margin-top: -20px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/calculator/monthly-payments/index.scss: -------------------------------------------------------------------------------- 1 | page { 2 | background-color: #ffffff; 3 | } 4 | .montyly-payments { 5 | @import './index.rn.scss'; 6 | overflow: hidden; 7 | height: 100vh; 8 | 9 | .content { 10 | box-sizing: border-box; 11 | height: 100vh; 12 | } 13 | 14 | .compared { 15 | box-sizing: border-box; 16 | } 17 | 18 | .no-checked { 19 | box-sizing: border-box; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: { 6 | }, 7 | mini: {}, 8 | h5: { 9 | /** 10 | * 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。 11 | * 参考代码如下: 12 | * webpackChain (chain) { 13 | * chain.plugin('analyzer') 14 | * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) 15 | * } 16 | */ 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/picker/index.rn.scss: -------------------------------------------------------------------------------- 1 | .picker-title { 2 | padding: 30px; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: space-between; 6 | align-items: center; 7 | background:rgba(248,249,251,1); 8 | font-size: 32px; 9 | 10 | &-cancel { 11 | color: #474B4E; 12 | } 13 | &-ok { 14 | font-weight: bold; 15 | color: #1fb081; 16 | } 17 | } 18 | 19 | .line { 20 | display: flex; 21 | height: 1px; 22 | width: 100%; 23 | background: #E5E5E5; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/global-data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-11 00:04:58 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | const globalData = {}; 9 | 10 | if (IS_RN) { 11 | global.globalData = {}; 12 | } 13 | 14 | const setGlobalData = (key: string, val: any) => { 15 | (IS_RN ? global.globalData : globalData)[key] = val; 16 | }; 17 | 18 | const getGlobalData = (key: string) => { 19 | return (IS_RN ? global.globalData : globalData)[key]; 20 | }; 21 | 22 | export { setGlobalData, getGlobalData }; 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | disturl=https://npmmirror.com/dist 3 | sass_binary_site=https://npmmirror.com/mirrors/node-sass/ 4 | phantomjs_cdnurl=https://npmmirror.com/mirrors/phantomjs/ 5 | electron_mirror=https://npmmirror.com/mirrors/electron/ 6 | chromedriver_cdnurl=https://npmmirror.com/mirrors/chromedriver 7 | operadriver_cdnurl=https://npmmirror.com/mirrors/operadriver 8 | selenium_cdnurl=https://npmmirror.com/mirrors/selenium 9 | node_inspector_cdnurl=https://npmmirror.com/mirrors/node-inspector 10 | fsevents_binary_host_mirror=http://npmmirror.com/mirrors/fsevents/ 11 | -------------------------------------------------------------------------------- /src/components/modal/type.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:36:31 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | export interface TaroModalProps { 9 | visible: boolean; 10 | closable?: boolean; 11 | maskClosable?: boolean; 12 | style?: {}; 13 | bodyStyle?: {}; 14 | animationType?: string; 15 | onClose?(): void; 16 | footer?: never[]; 17 | transparent?: boolean; 18 | popup?: boolean; 19 | animateAppear?: boolean; 20 | operation?: boolean; 21 | closeIconStyle?: any; 22 | closeIconName?: string; 23 | className?: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/status-bar/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-11-24 18:09:15 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React from "react"; 9 | import { View } from '@tarojs/components'; 10 | import { isAndroid } from '@utils'; 11 | 12 | let StatusBar: any; 13 | if (IS_RN && isAndroid()) { 14 | StatusBar = require('react-native').StatusBar; 15 | } 16 | 17 | export default function TaroStatusBar(props: any) { 18 | return StatusBar ? ( 19 | 20 | ) : ( 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": ".dist", 3 | "projectname": "Mortgage Calculator", 4 | "description": "", 5 | "appid": "wxb604d548939868ce", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": false, 9 | "postcss": false, 10 | "preloadBackgroundData": false, 11 | "minified": false, 12 | "newFeature": true, 13 | "autoAudits": false, 14 | "coverView": true, 15 | "showShadowRootInWxmlPanel": false, 16 | "scopeDataCheck": false, 17 | "useCompilerModule": false 18 | }, 19 | "compileType": "miniprogram", 20 | "simulatorType": "wechat", 21 | "simulatorPluginLibVersion": {}, 22 | "condition": {} 23 | } 24 | -------------------------------------------------------------------------------- /src/components/safe-area-view/type.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2021-01-09 14:00:01 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import { ViewProps } from '@tarojs/components/types/View'; 9 | import { FunctionComponent } from 'react'; 10 | import { NativeSafeAreaViewProps } from 'react-native-safe-area-context'; 11 | 12 | export interface TaroSafeAreaViewProps { 13 | style?: object; 14 | } 15 | 16 | export type TaroSafeAreaViewType = FunctionComponent< 17 | NativeSafeAreaViewProps & ViewProps 18 | >; 19 | 20 | declare const TaroSafeAreaView: TaroSafeAreaViewType; 21 | 22 | export default TaroSafeAreaView; 23 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:40:24 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | /** 9 | * 组件内根据平台require导致小程序编译失败 10 | * 小程序不支持运行时require 等待后续支持吧 11 | */ 12 | export { default as BoxShadow } from "./box-shadow"; 13 | export { default as SafeAreaView } from "./safe-area-view"; 14 | export { default as LinearGradient } from "./linear-gradient"; 15 | export { default as KeyboardAwareScrollView } from "./keyboard-aware-scroll-view"; 16 | export { default as Modal } from "./modal"; 17 | export { default as Pciker } from "./picker"; 18 | export { default as StatusBar } from "./status-bar"; 19 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | pages: [ 3 | "pages/calculator/index", 4 | "pages/calculator/monthly-payments/index", 5 | "pages/calculator/history/index" 6 | ], 7 | window: { 8 | backgroundTextStyle: "light", 9 | navigationBarBackgroundColor: "#fff", 10 | navigationBarTitleText: "WeChat", 11 | navigationBarTextStyle: "black" 12 | }, 13 | rn: { 14 | screenOptions: { 15 | // 设置页面的options,参考https://reactnavigation.org/docs/stack-navigator/#options 16 | shadowOffset: { width: 0, height: 0 }, 17 | borderWidth: 0, 18 | elevation: 0, 19 | shadowOpacity: 1, 20 | borderBottomWidth: 0 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Taro Mortgage Calculator 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | declare module "*.gif"; 3 | declare module "*.jpg"; 4 | declare module "*.jpeg"; 5 | declare module "*.svg"; 6 | declare module "*.css"; 7 | declare module "*.less"; 8 | declare module "*.scss"; 9 | declare module "*.sass"; 10 | declare module "*.styl"; 11 | declare module "*.mp4"; 12 | 13 | declare namespace NodeJS { 14 | interface ProcessEnv { 15 | TARO_ENV: 16 | | "weapp" 17 | | "swan" 18 | | "alipay" 19 | | "h5" 20 | | "rn" 21 | | "tt" 22 | | "quickapp" 23 | | "qq" 24 | | "jd"; 25 | } 26 | interface Global { 27 | globalData: object; 28 | } 29 | } 30 | 31 | declare const IS_H5: any; 32 | declare const IS_WEAPP: any; 33 | declare const IS_RN: any; 34 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-30 22:12:43 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import Taro from '@tarojs/taro'; 9 | 10 | export const getStorageData = async (key: string) => { 11 | let result: any; 12 | try { 13 | const { data } = await Taro.getStorage({ key }); 14 | result = data; 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | return result; 19 | }; 20 | 21 | // num为传入的值,n为保留的小数位 22 | export const formatFloat = (num: number | string, n: number) => { 23 | let f = parseFloat(num as string); 24 | if (Number.isNaN(f)) { 25 | return false; 26 | } 27 | f = Math.round((num as number) * Math.pow(10, n)) / Math.pow(10, n); // n 幂 28 | return f; 29 | }; 30 | -------------------------------------------------------------------------------- /src/pages/calculator/history/index.rn.scss: -------------------------------------------------------------------------------- 1 | .history { 2 | width: 100%; 3 | height: 100%; 4 | flex: 1; 5 | background: rgba(255, 255, 255, 1); 6 | } 7 | 8 | .history-content { 9 | padding: 0 40px; 10 | } 11 | 12 | .wrap { 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | justify-content: space-between; 17 | padding: 40px 0; 18 | border-bottom-width: 1px; 19 | border-style: solid; 20 | border-bottom-color: #E7EBEE; 21 | 22 | &-item { 23 | display: flex; 24 | flex-direction: column; 25 | } 26 | } 27 | 28 | .title { 29 | font-size: 24px; 30 | font-family: PingFangSC-Regular; 31 | color: rgba(71, 75, 78, 1); 32 | } 33 | 34 | .amount { 35 | margin-top: 16px; 36 | font-size: 32px; 37 | font-family: PingFangSC-Medium; 38 | color: rgba(11, 15, 18, 1); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:36:13 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { FunctionComponent } from "react"; 9 | import { AtModal } from 'taro-ui'; 10 | import './index.scss'; 11 | import { View, Image } from '@tarojs/components'; 12 | import { CLOSE_ICON } from './constant'; 13 | import { TaroModalProps } from './type'; 14 | 15 | const TaroModal: FunctionComponent = (props) => { 16 | const { visible = false, closable = false, onClose = () => {} } = props; 17 | 18 | return ( 19 | 20 | {closable && ( 21 | 22 | )} 23 | {props.children} 24 | 25 | ); 26 | }; 27 | 28 | export default TaroModal; 29 | -------------------------------------------------------------------------------- /src/components/keyboard-aware-scroll-view/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:38:41 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React from "react"; 9 | import { View } from "@tarojs/components"; 10 | import "./index.scss"; 11 | 12 | let KeyboardAwareScrollView: any; 13 | if (IS_RN) { 14 | KeyboardAwareScrollView = require("@codler/react-native-keyboard-aware-scroll-view") 15 | .KeyboardAwareScrollView; 16 | } 17 | 18 | function TaroKeyboardAwareScrollView(props: any) { 19 | if (IS_RN) { 20 | return ( 21 | 22 | {props.children} 23 | 24 | ); 25 | } 26 | const { className = "" } = props; 27 | return {props.children}; 28 | } 29 | 30 | TaroKeyboardAwareScrollView.options = { 31 | addGlobalClass: true 32 | }; 33 | 34 | export default TaroKeyboardAwareScrollView; 35 | -------------------------------------------------------------------------------- /src/utils/across-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-07-09 11:14:17 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import Taro from "@tarojs/taro"; 9 | 10 | let RN: any = {}; 11 | if (IS_RN) { 12 | RN = require("react-native"); 13 | } 14 | 15 | export const isAndroid = () => { 16 | if (IS_RN) { 17 | return RN.Platform.OS === "android"; 18 | } 19 | const { platform, system } = Taro.getSystemInfoSync(); 20 | return platform === "devtools" 21 | ? system.includes("Android") 22 | : platform === "Android"; 23 | }; 24 | 25 | export const initBackHandler = (callback: () => boolean = () => false) => { 26 | // callback 返回 true 阻止返回 默认返回false 27 | if (IS_RN) { 28 | RN.BackHandler.addEventListener("hardwareBackPress", function() { 29 | if (Taro.getCurrentPages().length === 1) { 30 | const result = callback(); 31 | !result && Taro.navigateBack({ delta: 1 }); 32 | return result; 33 | } 34 | return callback(); 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wuba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/safe-area-view/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:39:59 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React from "react"; 9 | import { View } from "@tarojs/components"; 10 | import "./index.scss"; 11 | import { TaroSafeAreaViewType } from "./type"; 12 | 13 | let SafeAreaView: any; 14 | if (process.env.TARO_ENV === "rn") { 15 | SafeAreaView = require("react-native-safe-area-context").SafeAreaView; 16 | } 17 | 18 | const TaroSafeAreaView: TaroSafeAreaViewType = props => { 19 | const { className = "", style = {}, edges = ["right", "bottom", "left"] } = props; 20 | 21 | if (process.env.TARO_ENV === "rn") { 22 | return ( 23 | 28 | {props.children} 29 | 30 | ); 31 | } 32 | return ( 33 | 34 | {props.children} 35 | 36 | ); 37 | }; 38 | 39 | export default TaroSafeAreaView; 40 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["taro/react"], 3 | "rules": { 4 | "semi": ["error", "always"], 5 | "no-undef": 0, 6 | "import/first": 0, 7 | "space-infix-ops": 1, 8 | "array-bracket-spacing": 0, 9 | "object-curly-spacing": ["error","always"], 10 | "spaced-comment": 2, 11 | "no-unused-vars": 0, 12 | "react/no-unused-state": 0, 13 | "react/sort-comp": 0, 14 | "react/self-closing-comp": 1, 15 | "react/no-multi-comp": 0, 16 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }], 17 | "@typescript-eslint/no-unused-vars": [1, { "varsIgnorePattern": "React" }], 18 | "@typescript-eslint/no-undef": 0, 19 | "@typescript-eslint/explicit-function-return-type": 0, 20 | "@typescript-eslint/no-empty-function": 0, 21 | "@typescript-eslint/no-explicit-any": 0, 22 | "jsx-quotes": [ 23 | "error", 24 | "prefer-double" 25 | ], 26 | "import/prefer-default-export": 0, 27 | "@typescript-eslint/camelcase": 0, 28 | "@typescript-eslint/ban-ts-ignore": 0, 29 | "@typescript-eslint/no-inferrable-types": 0, 30 | "@typescript-eslint/no-shadow": 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "noImplicitAny": false, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "lib", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "rootDir": ".", 18 | "jsx": "react", 19 | "jsxFactory": "React.createElement", 20 | "allowJs": true, 21 | "resolveJsonModule": true, 22 | "typeRoots": [ 23 | "node_modules/@types", 24 | "global.d.ts" 25 | ], 26 | "paths": { 27 | "@components": [ 28 | "src/components/" 29 | ], 30 | "@components/*": [ 31 | "src/components/*" 32 | ], 33 | "@utils": [ 34 | "src/utils" 35 | ], 36 | "@utils/*": [ 37 | "src/utils/*" 38 | ], 39 | "@service": [ 40 | "src/service" 41 | ], 42 | "@service/*": [ 43 | "src/service/*" 44 | ], 45 | }, 46 | }, 47 | "exclude": [ 48 | "node_modules", 49 | "dist" 50 | ], 51 | "compileOnSave": false 52 | } 53 | -------------------------------------------------------------------------------- /src/components/modal/index.rn.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:36:03 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { FunctionComponent } from "react"; 9 | import { View, Image } from '@tarojs/components'; 10 | import Modal from '@ant-design/react-native/lib/modal'; 11 | import './index.scss'; 12 | import { CLOSE_ICON } from './constant'; 13 | import { TaroModalProps } from './type'; 14 | 15 | const TaroModal: FunctionComponent = (props) => { 16 | const { visible = false, closable = false, onClose = () => {}, closeIconStyle = {}, closeIconName = '' , animationType = 'fade' } = props; 17 | 18 | return ( 19 | 29 | {!closable && ( 30 | 31 | 32 | 33 | )} 34 | {props.children} 35 | 36 | ); 37 | }; 38 | 39 | export default TaroModal; 40 | -------------------------------------------------------------------------------- /src/pages/calculator/index.scss: -------------------------------------------------------------------------------- 1 | page { 2 | background-color: #ffffff; 3 | } 4 | 5 | .calculator { 6 | @import 'index.rn.scss'; 7 | 8 | width: 100%; 9 | background-color: #ffffff; 10 | 11 | .pseudo-content { 12 | &__pseudo { 13 | left: calc(50% - 15px); 14 | } 15 | } 16 | 17 | .picker-box { 18 | 19 | &__picker { 20 | &__text { 21 | display: inline-flex; 22 | } 23 | } 24 | } 25 | 26 | .broker { 27 | box-sizing: border-box; 28 | 29 | &-box { 30 | box-sizing: border-box; 31 | } 32 | 33 | &-info { 34 | 35 | &-companyName { 36 | overflow: hidden; 37 | white-space: nowrap; 38 | text-overflow: ellipsis; 39 | } 40 | } 41 | 42 | &-img { 43 | border-radius: 50%; 44 | } 45 | 46 | } 47 | 48 | .compute { 49 | position: fixed; 50 | height: auto !important; 51 | } 52 | 53 | .keyboard-box { 54 | position: fixed; 55 | bottom: 0; 56 | box-sizing: border-box; 57 | } 58 | 59 | .fixed { 60 | position: fixed; 61 | } 62 | 63 | .mask { 64 | position: fixed; 65 | } 66 | 67 | .compute-modal { 68 | padding-top: 20px; 69 | } 70 | 71 | .explain { 72 | 73 | &-btn { 74 | border: none; 75 | margin-bottom: 30px; 76 | 77 | &::after { 78 | content: none; 79 | } 80 | } 81 | 82 | } 83 | 84 | /* #ifdef h5 */ 85 | // h5中视频控制跳 在controls={false} showFullscreenBtn={false}时还存在 这里直接通过样式隐藏 86 | taro-video-control { 87 | display: none; 88 | } 89 | /* #endif */ 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: [ v* ] 4 | workflow_dispatch: 5 | 6 | jobs: 7 | taro_release_job: 8 | runs-on: ubuntu-latest 9 | name: Taro Bundle Release 10 | steps: 11 | - name: Checkout Project 12 | uses: actions/checkout@v2 13 | - name: Cache node_modules Folder 14 | uses: actions/cache@v2 15 | with: 16 | path: ${{ github.workspace }}/node_modules 17 | key: ${{ runner.os }}-node_modules 18 | restore-keys: ${{ runner.os }}-node_modules 19 | - name: Get Yarn Cache Directory Path 20 | id: yarn-cache-dir-path 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | - name: Cache Yarn 23 | uses: actions/cache@v2 24 | env: 25 | cache-name: yarn-cache 26 | with: 27 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | - name: Install Dependencies 32 | run: | 33 | yarn 34 | - name: Release Taro React Native bundle 35 | uses: zhiqingchen/taro-react-native-release@v1 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | appname: Taro-Mortgage-calculator 39 | logo: https://pic3.58cdn.com.cn/nowater/fangfe/n_v25b1523466b894881b9bdeda7618a8af2.png 40 | - name: Upload Qr Image 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: bundle-qr-code 44 | path: | 45 | release/qrcode/ios.png 46 | release/qrcode/android.png 47 | -------------------------------------------------------------------------------- /src/components/picker/type.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:39:47 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | export interface RangeItem { 9 | value: string | number; 10 | label: string; 11 | children?: RangeItem[]; 12 | } 13 | 14 | export interface TaroPickerSelectorProps { 15 | /** 选择器类型 16 | * @supported weapp, h5, rn 17 | * @default selector 18 | */ 19 | mode: "selector" | "multiSelector"; 20 | /** mode为 selector 或 multiSelector 时,range 有效 21 | * @supported weapp, h5, rn 22 | * @default [] 23 | */ 24 | range: RangeItem[] | RangeItem[][]; 25 | /** 当 range 是一个 Object Array 时,通过 rangeKey 来指定 Object 中 key 的值作为选择器显示内容 26 | * @supported weapp, h5, rn 27 | * @default label 28 | */ 29 | // rangeKey?: 'label'; 30 | /** 选择的值 31 | * @supported weapp, h5, rn 32 | */ 33 | value: number[]; 34 | /** 点击确定时触发 value 35 | * @supported weapp, h5, rn 36 | */ 37 | onChange: (value: any[]) => void; 38 | /** value 改变时触发 value 39 | * @supported weapp, h5, rn 40 | */ 41 | onValueChange?: (value: any[]) => void; 42 | 43 | /** 选择器样式类名 44 | * @supported weapp, h5, rn 45 | */ 46 | styleName?: string; 47 | /** 48 | * 级联选择时需要传入 49 | * @supported rn 50 | * @default 3 51 | */ 52 | cols?: number; 53 | /** 54 | * 选择器标题 55 | * @supported weapp, h5, rn 56 | */ 57 | title?: string; 58 | /** 59 | * 级联选择 60 | * @supported rn 61 | * @default false 62 | */ 63 | cascade?: boolean; 64 | /** 65 | * 每列标签样式,已知问题: height、fontWeight 安卓和IOS表现不一致 66 | * @default false 67 | */ 68 | itemStyle?: object; 69 | /** 70 | * 多列情况下是否开启,选择前一列,重置下一列 71 | */ 72 | columnReset?: boolean; 73 | } 74 | -------------------------------------------------------------------------------- /src/pages/calculator/title-tpl.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-06-28 13:47:24 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { FunctionComponent } from "react"; 9 | import { View, Text } from "@tarojs/components"; 10 | import "./index.scss"; 11 | 12 | interface TitleTplProps { 13 | title: string; 14 | data: any[]; 15 | activeIndex: number; 16 | onWayClick: (...args: any) => void; 17 | } 18 | 19 | export const TitleTpl: FunctionComponent = props => { 20 | const { 21 | title = "", 22 | data = [], 23 | onWayClick = () => {}, 24 | activeIndex = 0 25 | } = props; 26 | 27 | const handleClick = (item: any, index: number) => () => { 28 | onWayClick(item, index); 29 | }; 30 | 31 | return ( 32 | 33 | {title} 34 | 35 | {data.map((item: any, index: number) => { 36 | return ( 37 | 42 | 49 | {item.name} 50 | 51 | {activeIndex === item.index && ( 52 | 53 | )} 54 | 55 | ); 56 | })} 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/box-shadow/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:36:40 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import { Color } from "@tarojs/taro"; 9 | import { View } from "@tarojs/components"; 10 | import React, { FunctionComponent, CSSProperties } from "react"; 11 | import "./index.scss"; 12 | import { isArray } from "@utils"; 13 | 14 | interface ShadowOffset { 15 | width: number; 16 | height: number; 17 | } 18 | interface BoxShadowProps { 19 | shadowColor?: Color; 20 | shadowOffset?: ShadowOffset; 21 | shadowOpacity?: CSSProperties["opacity"]; 22 | shadowRadius?: number; 23 | // 安卓只支持下面的属性 @see https://www.reactnative.cn/docs/view-style-props#elevation 24 | elevation?: number; 25 | // 非RN平台支持 26 | boxShadow?: CSSProperties["boxShadow"]; 27 | styleName?: string; 28 | className?: string; 29 | style?: CSSProperties; 30 | } 31 | 32 | const BoxShadow: FunctionComponent = props => { 33 | const { 34 | shadowColor = "#000", 35 | shadowOffset = { width: 0, height: 0 }, 36 | shadowOpacity = 1, 37 | shadowRadius = 0, 38 | elevation = 1, 39 | boxShadow = "", 40 | style = {}, 41 | className = "", 42 | ...rest 43 | } = props; 44 | let customeStyle: CSSProperties & BoxShadowProps = IS_RN 45 | ? { 46 | shadowColor, 47 | shadowOffset, 48 | shadowOpacity, 49 | shadowRadius, 50 | elevation 51 | } 52 | : { 53 | boxShadow 54 | }; 55 | const propsStyle = Object.assign( 56 | customeStyle, 57 | ...(isArray(style) ? style : ([style] as any)) 58 | ); 59 | return !IS_WEAPP ? ( 60 | 65 | {props.children} 66 | 67 | ) : ( 68 | 69 | {props.children} 70 | 71 | ); 72 | }; 73 | 74 | export default BoxShadow; 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mortgage-Calculator 2 | 3 | Taro 3 - MortgageCalculator 4 | 5 | > 从左到右:React Native、Weapp、H5 6 | 7 | ![](./example.png) 8 | 9 | 基于Taro 3开发的多端(React Native、Weapp、H5)实例 10 | 11 | 12 | > 开发React Native, 推荐阅读[React Native 端开发流程](https://nervjs.github.io/taro/docs/react-native)和[React Native 端开发前注意](https://nervjs.github.io/taro/docs/react-native-remind) 13 | 14 | 15 | ### 项目介绍 16 | 17 | 目录结构: 18 | ``` 19 | ├── config 20 | ├── global.d.ts 21 | ├── metro.config.js // Taro 3 RN metro 配置文件 22 | ├── package.json 23 | ├── project.config.json 24 | ├── src 25 | │ ├── app.config.ts 26 | │ ├── app.rn.scss // 针对RN的单独样式 27 | │ ├── app.scss 28 | │ ├── app.ts 29 | │ ├── assets 30 | │ ├── components // 封装的一些多端组件 31 | │ ├── index.html 32 | │ ├── pages 33 | │ └── utils 34 | ├── tsconfig.json 35 | └── yarn.lock 36 | ``` 37 | 38 | 此项目旨在为Taro 3开发多端应用提供一个可参考的案例,封装一些支持多端的组件,提供一份开发多端应用的思路、技巧,能够快速上手开发 39 | 40 | 41 | ## 在线预览 42 | 43 | 44 | |
React Native
|
小程序
|
H5
| 45 | |--------------|-------|----| 46 | | 安卓:[taroDemo.apk](https://github.com/wuba/Taro-Mortgage-Calculator/raw/e0c432bdc6096a08d9020542e7ce401861026bfa/app-arm64-v8a-release.apk.1.zip)
IOS:[taroDemo.app](https://github.com/wuba/Taro-Mortgage-Calculator/raw/a67459bc6667b0478978621482d33103d04e7538/taroDemo.app.zip)(IOS模拟器包) | ![](./mini-qrcode.jpg) | ![](./h5-qrcode.png)
https://wuba.github.io/Taro-Mortgage-Calculator | 47 | 48 | ## 使用 Taro Playground 预览 49 | 50 | 0. 下载安装 [Taro Playground](https://github.com/wuba/taro-playground#app-download)。 51 | 1. 本地运行 `yarn dev:rn`,打印二维码。或者打开[releases](https://github.com/wuba/Taro-Mortgage-Calculator/releases)页面。 52 | 2. 使用 APP 扫描二维码加载 bundle,进行预览。 53 | 54 | ## 本地运行 55 | 56 | ``` 57 | # clone到本地 58 | git clone https://github.com/wuba/Taro-Mortgage-Calculator.git 59 | 60 | # 进去项目根目录 61 | cd Taro-Mortgage-Calculator 62 | 63 | # 安装依赖 64 | yarn 65 | 66 | # 运行RN 默认端口8081 67 | yarn dev:rn 68 | 69 | # 运行微信小程序 70 | yarn dev:weapp 71 | 72 | # 运行H5 73 | yarn dev:h5 74 | ``` 75 | 76 | ## License 77 | 78 | MIT 79 | -------------------------------------------------------------------------------- /src/pages/calculator/history/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:40:47 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { Component } from "react"; 9 | import { View, Text } from "@tarojs/components"; 10 | import "./index.scss"; 11 | import { SafeAreaView, StatusBar } from "@components"; 12 | import { getStorageData } from "@utils"; 13 | 14 | export default class HouseLoanComputeMontylyPayments extends Component< 15 | any, 16 | any 17 | > { 18 | constructor(props: any) { 19 | super(props); 20 | this.state = { 21 | historyList: [] 22 | }; 23 | } 24 | 25 | async componentDidMount() { 26 | const data = await getStorageData("LOAN_HISTORY") || {}; 27 | this.setState({ 28 | historyList: data 29 | }); 30 | } 31 | 32 | render() { 33 | const { historyList = [] } = this.state; 34 | return ( 35 | 36 | 37 | 38 | {historyList.map((item: any, index: number) => { 39 | return ( 40 | 41 | 42 | 43 | 公积金贷{item.accumulatFundYear}年 44 | 45 | {item.accumulatTotalPirce}万 46 | 47 | 48 | 商业贷{item.commerceLoanYear}年 49 | {item.commerceTotalPirce}万 50 | 51 | 52 | {item.payMonthStr} 53 | {item.firstPay}元 54 | 55 | 56 | ); 57 | })} 58 | 59 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/picker/index.h5.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:39:23 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { FunctionComponent } from "react"; 9 | import { Picker } from "@tarojs/components"; 10 | import { isArray } from "@utils"; 11 | import "./index.scss"; 12 | import { TaroPickerSelectorProps, RangeItem } from "./type"; 13 | 14 | const TaroPickerSelector: FunctionComponent = props => { 15 | const { 16 | range = [], 17 | onChange = () => {}, 18 | onValueChange = () => {}, 19 | value = [0], 20 | styleName = "", 21 | title = "", 22 | mode = "selector", 23 | columnReset = false 24 | } = props; 25 | 26 | const handleChange = (e: any) => { 27 | if (mode === "multiSelector") { 28 | const valueList = isArray(e.detail.value) 29 | ? e.detail.value 30 | : [e.detail.value]; 31 | const realValue = valueList.map((v: any, i: number) => range[i][v].value); 32 | onChange(realValue); 33 | return; 34 | } 35 | onChange([(range[e.detail.value] as RangeItem).value]); 36 | }; 37 | 38 | const handleValueChange = (e: any) => { 39 | if (mode === "multiSelector") { 40 | const { column } = e.detail; 41 | const valueList = [...value]; 42 | valueList[column] = range[column][e.detail.value].value; 43 | onValueChange(valueList as number[]); 44 | return; 45 | } 46 | onValueChange([range[e.detail.value]]); 47 | }; 48 | 49 | const getVlaueIndex = (selectValue: any[]) => { 50 | return selectValue.map((v, i) => { 51 | let index = 0; 52 | const data = (mode === "multiSelector" ? range[i] : range) || []; 53 | (data as RangeItem[]).forEach((r: any, ri: number) => { 54 | if (r.value === v) { 55 | index = ri; 56 | } 57 | }); 58 | return index; 59 | }); 60 | }; 61 | 62 | return ( 63 | 76 | {props.children} 77 | 78 | ); 79 | }; 80 | 81 | export default TaroPickerSelector; 82 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const config = { 3 | projectName: "Mortgage Calculator", 4 | date: "2020-11-7", 5 | designWidth: 750, 6 | deviceRatio: { 7 | 640: 2.34 / 2, 8 | 750: 1, 9 | 828: 1.81 / 2 10 | }, 11 | compiler: { 12 | type: 'webpack5', 13 | prebundle: { 14 | enable: false, 15 | }, 16 | }, 17 | sourceRoot: "src", 18 | outputRoot: `dist/${process.env.TARO_ENV}`, 19 | plugins: [], 20 | defineConstants: { 21 | IS_H5: process.env.TARO_ENV === "h5", 22 | IS_RN: process.env.TARO_ENV === "rn", 23 | IS_WEAPP: process.env.TARO_ENV === "weapp" 24 | }, 25 | alias: { 26 | "@src": path.resolve(__dirname, "..", "src"), 27 | "@components": path.resolve(__dirname, "..", "src/components"), 28 | "@scss": path.resolve(__dirname, "..", "src/scss"), 29 | "@utils": path.resolve(__dirname, "..", "src/utils"), 30 | "@service": path.resolve(__dirname, "..", "src/service") 31 | }, 32 | copy: { 33 | patterns: [], 34 | options: {} 35 | }, 36 | framework: "react", 37 | mini: { 38 | postcss: { 39 | pxtransform: { 40 | enable: true, 41 | config: {} 42 | }, 43 | url: { 44 | enable: true, 45 | config: { 46 | limit: 1024 // 设定转换尺寸上限 47 | } 48 | }, 49 | cssModules: { 50 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 51 | config: { 52 | namingPattern: "module", // 转换模式,取值为 global/module 53 | generateScopedName: "[name]__[local]___[hash:base64:5]" 54 | } 55 | } 56 | } 57 | }, 58 | h5: { 59 | publicPath: "/Taro-Mortgage-Calculator/", 60 | staticDirectory: "static", 61 | esnextModules: ['taro-ui'], 62 | postcss: { 63 | pxtransform: { 64 | enable: true, 65 | }, 66 | autoprefixer: { 67 | enable: true, 68 | config: {} 69 | }, 70 | cssModules: { 71 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 72 | config: { 73 | namingPattern: "module", // 转换模式,取值为 global/module 74 | generateScopedName: "[name]__[local]___[hash:base64:5]" 75 | } 76 | } 77 | } 78 | }, 79 | rn: { 80 | output: { 81 | ios: 'ios/main.jsbundle', 82 | iosAssetsDest: 'ios', 83 | android: 'android/index.android.bundle', 84 | androidAssetsDest: 'android' 85 | }, 86 | } 87 | }; 88 | 89 | module.exports = function(merge) { 90 | if (process.env.NODE_ENV === "development") { 91 | return merge({}, config, require("./dev")); 92 | } 93 | return merge({}, config, require("./prod")); 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/picker/index.scss: -------------------------------------------------------------------------------- 1 | @import './index.rn.scss'; 2 | 3 | /* H5样式 */ 4 | .weui-picker { 5 | padding-bottom: env(safe-area-inset-bottom); 6 | padding-bottom: constant(safe-area-inset-bottom); 7 | background: #fff; 8 | } 9 | 10 | .weui-picker__hd { 11 | background: rgba(248, 249, 251, 1); 12 | display: flex; 13 | align-items: center; 14 | 15 | &::after { 16 | content: none; 17 | } 18 | } 19 | 20 | .weui-picker__action { 21 | &:first-child { 22 | color: #474B4E !important; 23 | } 24 | 25 | &:last-child { 26 | font-weight: bold; 27 | color: #1fb081 !important; 28 | } 29 | } 30 | 31 | .weui-picker__title { 32 | font-size: 32px; 33 | font-family: PingFangSC-Medium, PingFang SC; 34 | color: rgba(11, 15, 18, 1); 35 | } 36 | 37 | /* 小程序样式 */ 38 | .selector__modal__content { 39 | 40 | @keyframes slide-up { 41 | from { 42 | bottom: -45%; 43 | } 44 | 45 | to { 46 | bottom: 0; 47 | } 48 | } 49 | 50 | @keyframes slide-down { 51 | from { 52 | bottom: 0; 53 | } 54 | 55 | to { 56 | bottom: -45%; 57 | } 58 | } 59 | 60 | @keyframes fade-in { 61 | from { 62 | opacity: 0; 63 | } 64 | 65 | to { 66 | opacity: 1; 67 | } 68 | } 69 | 70 | @keyframes fade-out { 71 | from { 72 | opacity: 1; 73 | } 74 | 75 | to { 76 | opacity: 0; 77 | } 78 | } 79 | 80 | .mask { 81 | position: fixed; 82 | top: 0; 83 | left: 0; 84 | bottom: 0; 85 | right: 0; 86 | background-color: rgba(0, 0, 0, 0.5); 87 | 88 | &.slide-up { 89 | animation: fade-in 0.2s linear; 90 | } 91 | 92 | &.slide-down { 93 | animation: fade-out 0.2s linear; 94 | } 95 | } 96 | 97 | .selector__modal { 98 | position: fixed; 99 | bottom: 0; 100 | left: 0; 101 | background-color: #FFF; 102 | text-align: center; 103 | box-sizing: border-box; 104 | width: 100%; 105 | z-index: 999; 106 | padding-bottom: env(safe-area-inset-bottom); 107 | padding-bottom: constant(safe-area-inset-bottom); 108 | 109 | &.slide-up { 110 | animation: slide-up 0.2s linear; 111 | } 112 | 113 | &.slide-down { 114 | animation: slide-down 0.2s linear; 115 | } 116 | 117 | } 118 | 119 | 120 | .selector__modal-indicator { 121 | display: flex; 122 | align-items: center; 123 | justify-content: center; 124 | } 125 | 126 | picker-view { 127 | height: 600px; 128 | 129 | picker-view-column { 130 | view { 131 | display: flex; 132 | align-items: center; 133 | justify-content: center; 134 | } 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/utils/is-type.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-11 11:27:12 5 | * @Last Modified by: qiuz 6 | * @Last Modified time: 2021-01-04 21:46:33 7 | */ 8 | 9 | const pattern = { 10 | // http://emailregex.com/ 11 | email: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 12 | url: new RegExp( 13 | "^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$", 14 | "i" 15 | ), 16 | hex: /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i 17 | }; 18 | 19 | export const isNumber = (value: any) => { 20 | return typeof value === "number"; 21 | }; 22 | 23 | export const isRegExp = (value: any) => { 24 | return Object.prototype.toString.call(value) === "[object RegExp]"; 25 | }; 26 | 27 | export const isObject = (value: any) => { 28 | return Object.prototype.toString.call(value) === "[object Object]"; 29 | }; 30 | 31 | export const isError = (value: any) => { 32 | return Object.prototype.toString.call(value) === "[object Error]"; 33 | }; 34 | 35 | export const isString = (value: any) => { 36 | return typeof value === "string"; 37 | }; 38 | 39 | export const isFunction = (value: any) => { 40 | return typeof value === "function"; 41 | }; 42 | 43 | export const isArray = (value: any) => { 44 | return Object.prototype.toString.call(value) === "[object Array]"; 45 | }; 46 | 47 | export const isEmptyObject = (obj: any) => { 48 | if (obj === null || obj === undefined) 49 | return "Cannot convert undefined or null to object"; 50 | return Object.keys(obj).length === 0; 51 | }; 52 | 53 | export const isBoolean = (value: any) => { 54 | return ( 55 | value === true || 56 | value === false || 57 | (isObject(value) && 58 | Object.prototype.toString.call(value) === "[object Boolean]") 59 | ); 60 | }; 61 | 62 | export const isInteger = (value: any) => { 63 | return isNumber(value) && parseInt(value, 10) === value; 64 | }; 65 | export const isFloat = (value: any) => { 66 | return isNumber(value) && !isInteger(value); 67 | }; 68 | 69 | export const isDate = (value: any) => { 70 | if (!value) return false; 71 | return ( 72 | typeof value.getTime === "function" && 73 | typeof value.getMonth === "function" && 74 | typeof value.getYear === "function" 75 | ); 76 | }; 77 | 78 | export const isEmail = (value: any) => { 79 | return ( 80 | typeof value === "string" && 81 | !!value.match(pattern.email) && 82 | value.length < 255 83 | ); 84 | }; 85 | 86 | export const isUrl = (value: any) => { 87 | return typeof value === "string" && !!value.match(pattern.url); 88 | }; 89 | 90 | export const isHex = (value: any) => { 91 | return typeof value === "string" && !!value.match(pattern.hex); 92 | }; 93 | 94 | export const isPromise = (value: any) => { 95 | return value && typeof value.then === "function"; 96 | }; 97 | -------------------------------------------------------------------------------- /src/pages/calculator/compute-header/index.rn.scss: -------------------------------------------------------------------------------- 1 | ._h_l_c-header { 2 | 3 | &-header { 4 | display: flex; 5 | flex-direction: column; 6 | padding: 0 40px; 7 | width: 100%; 8 | 9 | &-box { 10 | width: 100%; 11 | height: 320px; 12 | position: relative; 13 | } 14 | } 15 | 16 | &-percent-icon { 17 | position: absolute; 18 | width: 210px; 19 | height: 100%; 20 | bottom: -17px; 21 | right: 0; 22 | z-index: -1; 23 | } 24 | 25 | &-header__tip { 26 | margin-top: 10px; 27 | width: 100%; 28 | font-size: 28px; 29 | line-height: 35px; 30 | font-weight: normal; 31 | color: rgba(255, 255, 255, 1); 32 | } 33 | 34 | &-header__info { 35 | margin-top: 40px; 36 | display: flex; 37 | flex-direction: row; 38 | align-items: center; 39 | justify-content: space-between; 40 | } 41 | 42 | &-info__title { 43 | display: flex; 44 | flex-direction: row; 45 | align-items: center; 46 | 47 | &__text { 48 | font-size: 40px; 49 | font-weight: bold; 50 | color: rgba(255, 255, 255, 1); 51 | } 52 | 53 | &__price { 54 | font-size: 44px; 55 | font-weight: bold; 56 | color: rgba(255, 255, 255, 1); 57 | margin: 0 10px; 58 | } 59 | } 60 | 61 | &-history-btn { 62 | display: flex; 63 | flex-direction: row; 64 | align-items: center; 65 | 66 | &__text { 67 | font-size: 28px; 68 | font-weight: normal; 69 | color: rgba(255, 255, 255, 1); 70 | } 71 | 72 | } 73 | 74 | &-loan-info { 75 | margin: 0 auto; 76 | height: 224px; 77 | background: rgba(255, 255, 255, 1); 78 | display: flex; 79 | // flex: 1; 80 | width: 100%; 81 | flex-direction: row; 82 | align-items: flex-start; 83 | justify-content: space-between; 84 | padding: 50px 60px; 85 | position: relative; 86 | z-index: 10; 87 | width: 90%; 88 | margin: 0 auto; 89 | margin-top: -112px; 90 | border-radius: 4px; 91 | 92 | &__unit { 93 | font-size: 24px; 94 | height: 100%; 95 | color: rgba(11, 15, 18, 1); 96 | } 97 | } 98 | 99 | &-loan-box { 100 | display: flex; 101 | flex-direction: column; 102 | 103 | &__title { 104 | font-size: 24px; 105 | margin-bottom: 12px; 106 | font-weight: normal; 107 | color: rgba(11, 15, 18, 1); 108 | } 109 | 110 | 111 | 112 | &__amount { 113 | display: flex; 114 | flex-direction: row; 115 | align-items: baseline; 116 | height: 48px; 117 | 118 | &__number { 119 | font-size: 48px; 120 | font-weight: bold; 121 | color: rgba(11, 15, 18, 1); 122 | } 123 | } 124 | } 125 | 126 | &-monty-pay { 127 | align-items: flex-end; 128 | 129 | &__tip { 130 | display: flex; 131 | flex-direction: row; 132 | align-items: center; 133 | margin-top: 20px; 134 | 135 | &__text { 136 | font-size: 24px; 137 | font-weight: normal; 138 | color: rgba(71, 75, 78, 1); 139 | } 140 | } 141 | } 142 | 143 | &-arrow-right { 144 | width: 20px; 145 | height: 20px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mortgage_calculator", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Taro 3 - Mortgage Calculator", 6 | "templateInfo": { 7 | "name": "default", 8 | "typescript": true, 9 | "css": "sass" 10 | }, 11 | "scripts": { 12 | "build:weapp": "taro build --type weapp", 13 | "build:swan": "taro build --type swan", 14 | "build:alipay": "taro build --type alipay", 15 | "build:tt": "taro build --type tt", 16 | "build:h5": "taro build --type h5", 17 | "build:rn": "taro build --type rn", 18 | "build:rn-ios": "taro build --type rn --platform ios", 19 | "build:rn-android": "taro build --type rn --platform android", 20 | "build:qq": "taro build --type qq", 21 | "build:jd": "taro build --type jd", 22 | "build:quickapp": "taro build --type quickapp", 23 | "dev:weapp": "npm run build:weapp -- --watch", 24 | "dev:swan": "npm run build:swan -- --watch", 25 | "dev:alipay": "npm run build:alipay -- --watch", 26 | "dev:tt": "npm run build:tt -- --watch", 27 | "dev:h5": "npm run build:h5 -- --watch", 28 | "dev:rn": "npm run build:rn -- --watch --qr", 29 | "dev:qq": "npm run build:qq -- --watch", 30 | "dev:jd": "npm run build:jd -- --watch", 31 | "dev:quickapp": "npm run build:quickapp -- --watch", 32 | "publish:pages": "yarn build:h5 && yarn gh-pages -d dist/h5 -t" 33 | }, 34 | "browserslist": [ 35 | "last 3 versions", 36 | "Android >= 4.1", 37 | "ios >= 8" 38 | ], 39 | "author": "qiuz", 40 | "lint-staged": { 41 | "*.{js,jsx,ts,tsx}": [ 42 | "eslint --fix" 43 | ] 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 48 | "pre-commit": "lint-staged" 49 | } 50 | }, 51 | "dependencies": { 52 | "@babel/runtime": "^7.7.7", 53 | "@codler/react-native-keyboard-aware-scroll-view": "^2.0.0", 54 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", 55 | "@tarojs/cli": "^3.5.7", 56 | "@tarojs/components": "^3.5.7", 57 | "@tarojs/plugin-framework-react": "^3.5.7", 58 | "@tarojs/react": "^3.5.7", 59 | "@tarojs/rn-runner": "^3.5.7", 60 | "@tarojs/runtime": "^3.5.7", 61 | "@tarojs/taro": "^3.5.7", 62 | "@tarojs/taro-rn": "^3.5.7", 63 | "lodash": "4.17.21", 64 | "react": "^17.0.2", 65 | "react-dom": "^17.0.2", 66 | "react-native": "^0.68.2", 67 | "react-refresh": "^0.14.0", 68 | "webpack": "5.69.0", 69 | "taro-ui": "^3.0.0-alpha.3" 70 | }, 71 | "devDependencies": { 72 | "@babel/core": "^7.8.0", 73 | "@commitlint/cli": "^11.0.0", 74 | "@commitlint/config-conventional": "^11.0.0", 75 | "@tarojs/webpack5-runner": "^3.5.7", 76 | "@types/react": "^17.0.2", 77 | "@types/webpack-env": "^1.13.6", 78 | "@typescript-eslint/eslint-plugin": "^2.x", 79 | "@typescript-eslint/parser": "^2.x", 80 | "babel-preset-taro": "^3.5.7", 81 | "eslint": "^6.8.0", 82 | "eslint-config-taro": "^3.5.7", 83 | "eslint-plugin-import": "^2.12.0", 84 | "eslint-plugin-react": "^7.8.2", 85 | "eslint-plugin-react-hooks": "^4.2.0", 86 | "gh-pages": "^3.1.0", 87 | "husky": "^4.3.5", 88 | "lint-staged": "^10.5.3", 89 | "react-native-linear-gradient": "^2.5.6", 90 | "stylelint": "9.3.0", 91 | "typescript": "^4.1.0" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/picker/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:39:32 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { Component } from "react"; 9 | import { View, Text } from "@tarojs/components"; 10 | import Modal from "@ant-design/react-native/lib/modal"; 11 | import PickerView from "@ant-design/react-native/lib/picker-view"; 12 | import "./index.scss"; 13 | import { TaroPickerSelectorProps } from "./type"; 14 | 15 | export default class TaroPickerSelector extends Component< 16 | TaroPickerSelectorProps, 17 | any 18 | > { 19 | static defaultProps = { 20 | range: [], 21 | value: [], 22 | cols: 1, 23 | cascade: true, 24 | onChange: () => {} 25 | }; 26 | 27 | static options = { 28 | addGlobalClass: true 29 | }; 30 | 31 | constructor(props: TaroPickerSelectorProps) { 32 | super(props); 33 | this.state = { 34 | visible: false 35 | }; 36 | } 37 | 38 | componentWillReceiveProps(nextProps: TaroPickerSelectorProps) { 39 | if (nextProps.value !== this.props.value) { 40 | const { value = [0] } = nextProps; 41 | this.setState({ 42 | selectedValue: value 43 | }); 44 | } 45 | } 46 | 47 | showModal = () => { 48 | const { value = [0] } = this.props; 49 | this.setState({ 50 | visible: true, 51 | selectedValue: value 52 | }); 53 | }; 54 | 55 | closeModal = () => { 56 | this.setState({ 57 | visible: false 58 | }); 59 | }; 60 | 61 | handleChange = (value: any) => { 62 | this.setState( 63 | { 64 | selectedValue: value 65 | }, 66 | () => { 67 | this.props.onValueChange && this.props.onValueChange(value); 68 | } 69 | ); 70 | }; 71 | 72 | onConfirm = () => { 73 | const { selectedValue } = this.state; 74 | this.props.onChange(selectedValue); 75 | this.closeModal(); 76 | }; 77 | 78 | render() { 79 | const { range = [], cols, cascade, title } = this.props; 80 | const { visible, selectedValue } = this.state; 81 | return ( 82 | 83 | 91 | 92 | 93 | 取消 94 | 95 | {title} 96 | 97 | 确定 98 | 99 | 100 | 112 | 113 | {this.props.children} 114 | 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/linear-gradient/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-curly-brace-presence */ 2 | /* 3 | * @Author: qiuz 4 | * @Github: 5 | * @Date: 2020-12-10 23:38:52 6 | * @Last Modified by: qiuz 7 | */ 8 | 9 | import React, { FunctionComponent } from "react"; 10 | import { View, Image } from "@tarojs/components"; 11 | import { LinearGradientProps } from "react-native-linear-gradient"; 12 | import "./index.scss"; 13 | import { ITouchEvent } from "@tarojs/components/types/common"; 14 | import { isArray } from "@utils"; 15 | 16 | let UIManager = {}, 17 | LinearGradient: any = null; 18 | if (IS_RN) { 19 | LinearGradient = require("react-native-linear-gradient"); 20 | UIManager = require("react-native").UIManager; 21 | } 22 | 23 | interface TaroLinearGradientProps { 24 | style?: object; 25 | } 26 | 27 | export interface LinearGradientType extends LinearGradientProps { 28 | // 优先图片 29 | src?: string; 30 | color?: string; 31 | className?: string; 32 | useColors?: boolean; 33 | onClick?: (event: ITouchEvent) => void; 34 | } 35 | 36 | const TaroLinearGradient: FunctionComponent< 37 | LinearGradientType & TaroLinearGradientProps 38 | > = props => { 39 | const { 40 | src = "", 41 | style = {}, 42 | color = "", 43 | className = "", 44 | onClick = () => {}, 45 | colors = [], 46 | angle = 180, 47 | useColors = false 48 | } = props; 49 | const len = colors.length; 50 | const LinearGradientColors = 51 | colors && len <= 0 ? ["#ffffff", "#ffffff"] : colors; 52 | if (IS_RN) { 53 | // 兼容低版本不支持 取第一个色值 54 | const customeStyle = { backgroundColor: color || LinearGradientColors[0] }; 55 | const propsStyle = isArray(style) 56 | ? Object.assign(customeStyle, ...(style as any)) 57 | : Object.assign(customeStyle, style); 58 | // 如果有切图 以切图为主 59 | if (!src && UIManager["BVLinearGradient"]) { 60 | return ( 61 | 67 | { 68 | // @ts-ignore 69 | {props.children} 70 | } 71 | 72 | ); 73 | } 74 | return ( 75 | 80 | 81 | {props.children} 82 | 83 | ); 84 | } 85 | let background = color; 86 | if (useColors) { 87 | const colorString = LinearGradientColors.map( 88 | (colorStr: string, index: number) => 89 | `${colorStr} ${index === len - 1 ? "100" : (index / len) * 100}%` 90 | ).join(","); 91 | background = `linear-gradient(${angle}deg, ${colorString})`; 92 | } 93 | return ( 94 | 98 | {src && ( 99 | 104 | )} 105 | {props.children} 106 | 107 | ); 108 | }; 109 | 110 | export default TaroLinearGradient; 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | deploy_versions/ 3 | node_modules/ 4 | .DS_Store 5 | .temp/ 6 | .rn_temp/ 7 | .next/ 8 | .idea 9 | next-env.d.ts 10 | rn_bundle/ 11 | .vscode/ 12 | 13 | # Built application files 14 | *.apk 15 | *.ap_ 16 | *.aab 17 | 18 | # Files for the ART/Dalvik VM 19 | *.dex 20 | 21 | # Java class files 22 | *.class 23 | 24 | # Generated files 25 | gen/ 26 | out/ 27 | 28 | # Gradle files 29 | .gradle/ 30 | build/ 31 | 32 | # Local configuration file (sdk path, etc) 33 | local.properties 34 | 35 | # Proguard folder generated by Eclipse 36 | proguard/ 37 | 38 | # Log Files 39 | *.log 40 | 41 | # Android Studio Navigation editor temp files 42 | .navigation/ 43 | 44 | # Android Studio captures folder 45 | captures/ 46 | 47 | # IntelliJ 48 | *.iml 49 | .idea/workspace.xml 50 | .idea/tasks.xml 51 | .idea/gradle.xml 52 | .idea/assetWizardSettings.xml 53 | .idea/dictionaries 54 | .idea/libraries 55 | .idea/caches 56 | # Android Studio 3 in .gitignore file. 57 | .idea/caches/build_file_checksums.ser 58 | .idea/modules.xml 59 | 60 | # Keystore files 61 | # Uncomment the following lines if you do not want to check your keystore files in. 62 | #*.jks 63 | #*.keystore 64 | 65 | # External native build folder generated in Android Studio 2.2 and later 66 | .externalNativeBuild 67 | 68 | # Google Services (e.g. APIs or Firebase) 69 | # google-services.json 70 | 71 | # Freeline 72 | freeline.py 73 | freeline/ 74 | freeline_project_description.json 75 | 76 | # fastlane 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots 80 | fastlane/test_output 81 | fastlane/readme.md 82 | 83 | # Version control 84 | vcs.xml 85 | 86 | # lint 87 | lint/intermediates/ 88 | lint/generated/ 89 | lint/outputs/ 90 | lint/tmp/ 91 | # lint/reports/ 92 | 93 | # Xcode 94 | # 95 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 96 | 97 | ## Build generated 98 | build/ 99 | DerivedData/ 100 | Pods 101 | 102 | ## Various settings 103 | *.pbxuser 104 | !default.pbxuser 105 | *.mode1v3 106 | !default.mode1v3 107 | *.mode2v3 108 | !default.mode2v3 109 | *.perspectivev3 110 | !default.perspectivev3 111 | xcuserdata/ 112 | 113 | ## Other 114 | *.moved-aside 115 | *.xccheckout 116 | *.xcscmblueprint 117 | 118 | ## Obj-C/Swift specific 119 | *.hmap 120 | *.ipa 121 | *.dSYM.zip 122 | *.dSYM 123 | 124 | # CocoaPods 125 | # 126 | # We recommend against adding the Pods directory to your .gitignore. However 127 | # you should judge for yourself, the pros and cons are mentioned at: 128 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 129 | # 130 | # Pods/ 131 | # 132 | # Add this line if you want to avoid checking in source code from the Xcode workspace 133 | # *.xcworkspace 134 | 135 | # Carthage 136 | # 137 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 138 | # Carthage/Checkouts 139 | 140 | Carthage/Build 141 | 142 | # fastlane 143 | # 144 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 145 | # screenshots whenever they are needed. 146 | # For more information about the recommended setup visit: 147 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 148 | 149 | fastlane/report.xml 150 | fastlane/Preview.html 151 | fastlane/screenshots/**/*.png 152 | fastlane/test_output 153 | 154 | # Code Injection 155 | # 156 | # After new code Injection tools there's a generated folder /iOSInjectionProject 157 | # https://github.com/johnno1962/injectionforxcode 158 | 159 | iOSInjectionProject/ 160 | 161 | ios/Pods 162 | dist 163 | node_modules 164 | .cache 165 | .DS_Store 166 | lerna-debug.log 167 | yarn-error.log 168 | _book 169 | .idea 170 | .vscode 171 | temp 172 | website/build 173 | website/package.json 174 | 175 | ## taro-react-native-release 176 | !release/** -------------------------------------------------------------------------------- /src/pages/calculator/monthly-payments/index.rn.scss: -------------------------------------------------------------------------------- 1 | .montyly-payments { 2 | width: 100%; 3 | background: #FFFFFF; 4 | display: flex; 5 | flex-direction: column; 6 | overflow: hidden; 7 | flex: 1; 8 | height: 1000px; 9 | } 10 | 11 | .content { 12 | padding: 0 40px; 13 | } 14 | 15 | .title { 16 | margin-top: 44px; 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | flex-wrap: nowrap; 21 | 22 | &-text { 23 | font-size: 40px; 24 | // font-weight: 500; 25 | color: rgba(11, 15, 18, 1); 26 | font-family: PingFangSC-Medium; 27 | } 28 | 29 | &-amount { 30 | font-size: 44px; 31 | font-weight: bold; 32 | font-family: Avenir-Black; 33 | color: rgba(11, 15, 18, 1); 34 | margin: 0 10px; 35 | } 36 | } 37 | 38 | .tip-info { 39 | margin-top: 10px; 40 | display: flex; 41 | font-size: 28px; 42 | font-family: PingFangSC-Regular; 43 | font-weight: 400; 44 | color: rgba(71, 75, 78, 1); 45 | } 46 | 47 | .compared { 48 | width: 100%; 49 | background: rgba(255, 255, 255, 1); 50 | border-radius: 4px; 51 | border: 1px solid rgba(230, 230, 230, 1); 52 | padding: 56px 54px; 53 | padding-bottom: 60px; 54 | display: flex; 55 | justify-content: space-around; 56 | flex-direction: row; 57 | margin-top: 40px; 58 | } 59 | 60 | .equal-box { 61 | &-title { 62 | font-size: 36px; 63 | font-family: PingFangSC-Medium; 64 | // font-weight: 500; 65 | color: rgba(11, 15, 18, 1); 66 | } 67 | 68 | &-wrap { 69 | margin-top: 60px; 70 | display: flex; 71 | flex-direction: column; 72 | 73 | &-title { 74 | font-size: 24px; 75 | font-family: PingFangSC-Regular; 76 | font-weight: 400; 77 | color: rgba(71, 75, 78, 1); 78 | } 79 | } 80 | } 81 | 82 | .amount { 83 | font-size: 40px; 84 | font-family: Avenir-Black; 85 | // font-weight: 900; 86 | color: rgba(11, 15, 18, 1); 87 | margin-top: 10px; 88 | } 89 | 90 | .advant { 91 | margin-top: 18px; 92 | font-size: 28px; 93 | font-family: PingFangSC-Regular; 94 | color: rgba(11, 15, 18, 1); 95 | } 96 | 97 | .btn-wrap { 98 | display: flex; 99 | flex-direction: row; 100 | align-items: center; 101 | 102 | &-text { 103 | font-size: 28px; 104 | font-family: PingFangSC-Regular; 105 | color: rgba(11, 15, 18, 1); 106 | } 107 | } 108 | 109 | .radio { 110 | width: 24px; 111 | height: 24px; 112 | margin-right: 10px; 113 | } 114 | 115 | .no-checked { 116 | border: 1px solid #D3D7DA; 117 | border-radius: 100px; 118 | display: flex; 119 | width: 24px; 120 | height: 24px; 121 | } 122 | 123 | .report { 124 | height: 250px; 125 | background: #F5F7FA; 126 | margin-top: 60px; 127 | flex: 1; 128 | display: flex; 129 | align-items: center; 130 | flex-direction: column; 131 | justify-content: center; 132 | position: relative; 133 | 134 | &-title { 135 | font-size: 36px; 136 | font-family: Helvetica; 137 | color: rgba(31, 35, 38, 1); 138 | } 139 | 140 | &-info { 141 | margin: 30px 0; 142 | display: flex; 143 | flex-direction: row; 144 | position: relative; 145 | z-index: 1; 146 | 147 | &-text { 148 | font-size: 30px; 149 | font-family: PingFangSC-Regular; 150 | color: rgba(31, 35, 38, 1); 151 | } 152 | 153 | &-amount { 154 | color: #FD4D39; 155 | } 156 | } 157 | 158 | &-look { 159 | display: flex; 160 | flex-direction: row; 161 | align-items: center; 162 | 163 | &-btn { 164 | font-size: 30px; 165 | font-family: PingFangSC-Regular; 166 | color: rgba(14, 59, 189, 1); 167 | } 168 | } 169 | } 170 | 171 | .arrow-right { 172 | width: 20px; 173 | height: 20px; 174 | } 175 | 176 | .pay-monty { 177 | margin-bottom: 100px; 178 | 179 | &-title { 180 | display: flex; 181 | font-size: 40px; 182 | font-family: PingFangSC-Medium; 183 | color: rgba(11, 15, 18, 1); 184 | margin-top: 60px; 185 | margin-bottom: 30px; 186 | } 187 | } 188 | 189 | .line { 190 | display: flex; 191 | flex-direction: row; 192 | align-items: center; 193 | justify-content: space-between; 194 | height: 76px; 195 | 196 | &-text { 197 | display: flex; 198 | flex: 1; 199 | justify-content: center; 200 | text-align: center; 201 | font-size: 24px; 202 | font-family: PingFangSC-Regular; 203 | color: rgba(11, 15, 18, 1); 204 | } 205 | 206 | &-even { 207 | background: #FFFFFF; 208 | } 209 | 210 | &-odd { 211 | background: #FCFBFC; 212 | } 213 | 214 | &-first { 215 | background: #EFF1F1; 216 | 217 | &-title { 218 | font-weight: bold; 219 | font-family: PingFangSC-Medium; 220 | } 221 | } 222 | 223 | &-monty { 224 | text-align: center; 225 | font-size: 24px; 226 | font-family: PingFangSC-Medium; 227 | font-weight: bold; 228 | color: rgba(11, 15, 18, 1); 229 | } 230 | 231 | } 232 | 233 | .mark { 234 | width: 20px; 235 | height: 20px; 236 | margin-right: 10px; 237 | } 238 | 239 | .bg { 240 | width: 150px; 241 | height: 136px; 242 | position: absolute; 243 | right: 0; 244 | bottom: 0; 245 | z-index: 0; 246 | } 247 | -------------------------------------------------------------------------------- /src/components/picker/index.weapp.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:39:41 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { Component } from "react"; 9 | import { View, Text, PickerView, PickerViewColumn } from "@tarojs/components"; 10 | import "./index.scss"; 11 | import { isArray } from "@utils"; 12 | import { TaroPickerSelectorProps, RangeItem } from "./type"; 13 | 14 | export default class TaroPickerSelector extends Component< 15 | TaroPickerSelectorProps, 16 | any 17 | > { 18 | static defaultProps = { 19 | range: [], 20 | value: [], 21 | cols: 1, 22 | cascade: true, 23 | // rangeKey: 'label', 24 | onChange: () => {}, 25 | onValueChange: () => {} 26 | }; 27 | 28 | static options = { 29 | addGlobalClass: true 30 | }; 31 | realValue: any; 32 | 33 | constructor(props: TaroPickerSelectorProps) { 34 | super(props); 35 | this.state = { 36 | visible: false 37 | }; 38 | } 39 | 40 | componentWillReceiveProps(nextProps: TaroPickerSelectorProps) { 41 | if (nextProps.value !== this.props.value) { 42 | const { value = [0] } = nextProps; 43 | this.setState({ 44 | selectedValue: value 45 | }); 46 | } 47 | } 48 | 49 | showModal = (e: any) => { 50 | e.stopPropagation(); 51 | const { value = [0] } = this.props; 52 | this.setState({ 53 | visible: true, 54 | animation: "slide-up", 55 | selectedValue: value 56 | }); 57 | }; 58 | 59 | closeModal = (e?: any) => { 60 | e && e.stopPropagation(); 61 | this.setState({ 62 | animation: "slide-down" 63 | }); 64 | // 延时 以展示完收起动画 65 | setTimeout(() => { 66 | this.setState({ 67 | visible: false 68 | }); 69 | }, 150); 70 | }; 71 | 72 | handleChange = (e: any) => { 73 | const { range, mode } = this.props; 74 | if (mode === "multiSelector") { 75 | const valueList = isArray(e.detail.value) 76 | ? e.detail.value 77 | : [e.detail.value]; 78 | this.realValue = valueList.map((v: any, i: number) => range[i][v].value); 79 | this.props.onValueChange!(this.realValue); 80 | return; 81 | } 82 | this.realValue = [(range[e.detail.value] as RangeItem).value]; 83 | this.props.onValueChange!(this.realValue); 84 | }; 85 | 86 | onConfirm = () => { 87 | const { selectedValue } = this.state; 88 | this.props.onChange(this.realValue || selectedValue); 89 | // 展示过渡动画 90 | setTimeout(this.closeModal); 91 | }; 92 | 93 | renderMultiPicker = (data: any) => { 94 | return data.map((item: any, index: number) => { 95 | return ( 96 | 97 | {item.map((i: any) => { 98 | return ( 99 | 100 | {i.label} 101 | 102 | ); 103 | })} 104 | 105 | ); 106 | }); 107 | }; 108 | 109 | getVlaueIndex = (selectValue: any[]) => { 110 | const { range = [], mode } = this.props; 111 | return selectValue.map((v, i) => { 112 | let index = 0; 113 | const data = (mode === "multiSelector" ? range[i] : range) || []; 114 | (data as RangeItem[]).forEach((r: any, ri: number) => { 115 | if (r.value === v) { 116 | index = ri; 117 | } 118 | }); 119 | return index; 120 | }); 121 | }; 122 | 123 | render() { 124 | const { range = [], value, mode, title } = this.props; 125 | const { visible, animation } = this.state; 126 | return ( 127 | 128 | {visible && ( 129 | 130 | )} 131 | {visible && ( 132 | 133 | 134 | 135 | 取消 136 | 137 | {title} 138 | 139 | 确定 140 | 141 | 142 | {/* */} 143 | 147 | {mode === "multiSelector" ? ( 148 | this.renderMultiPicker(range) 149 | ) : ( 150 | 151 | {(range as RangeItem[]).map((i: any) => { 152 | return ( 153 | 154 | {i.label} 155 | 156 | ); 157 | })} 158 | 159 | )} 160 | 161 | 162 | )} 163 | {this.props.children} 164 | 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/pages/calculator/helper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-09 14:14:02 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | /** 9 | * 等额本息计算方式 10 | * 每月还款额=贷款本金×[月利率×(1+月利率)^还款月数]÷[(1+月利率)^还款月数-1] 11 | * 总支付利息:总利息=还款月数×每月月供额-贷款本金 12 | * 每月应还利息=贷款本金×月利率×〔(1+月利率)^还款月数-(1+月利率)^(还款月序号-1)〕÷〔(1+月利率)^还款月数-1〕 13 | * 每月应还本金=贷款本金×月利率×(1+月利率)^(还款月序号-1)÷〔(1+月利率)^还款月数-1〕 14 | * 总利息=还款月数×每月月供额-贷款本金 15 | * 16 | * 等额本金计算方式编辑 17 | * 每月月供额=(贷款本金÷还款月数)+(贷款本金-已归还本金累计额)×月利率 18 | * 每月应还本金=贷款本金÷还款月数 19 | * 每月应还利息=剩余本金×月利率=(贷款本金-已归还本金累计额)×月利率。 20 | * 每月月供递减额=每月应还本金×月利率=贷款本金÷还款月数×月利率 21 | * 总利息=还款月数×(总贷款额×月利率-月利率×(总贷款额÷还款月数)*(还款月数-1)÷2+总贷款额÷还款月数) 22 | */ 23 | 24 | interface ParamsProps { 25 | totalPrice: number; 26 | commerceLoanYear: number; 27 | commerceLoanRate: number; 28 | commerceTotalPirce: number; 29 | accumulatFundYear: number; 30 | accumulatFundRate: number; 31 | accumulatTotalPirce: number; 32 | } 33 | export const equalInterestCalc = ({ 34 | commerceLoanYear, 35 | commerceLoanRate, 36 | commerceTotalPirce, 37 | accumulatFundYear, 38 | accumulatFundRate, 39 | accumulatTotalPirce 40 | }: ParamsProps) => 41 | new Promise(resolve => { 42 | const commerceMonth = commerceLoanYear * 12; 43 | const commercePayMonty: number = getInterestEveryMonth( 44 | commerceTotalPirce, 45 | commerceLoanRate / 12, 46 | commerceMonth 47 | ); 48 | const accumlatMonth = accumulatFundYear * 12; 49 | const accumlatPayMonty: number = getInterestEveryMonth( 50 | accumulatTotalPirce, 51 | accumulatFundRate / 12, 52 | accumlatMonth 53 | ); 54 | const interestTotal = getInterestTotal( 55 | commercePayMonty * commerceMonth + accumlatPayMonty * accumlatMonth, 56 | commerceTotalPirce + accumulatTotalPirce 57 | ); 58 | const principalCommList = getPrincipalListByMonth( 59 | commerceTotalPirce, 60 | commerceLoanRate / 12, 61 | commerceMonth 62 | ); 63 | const principalAccuList = getPrincipalListByMonth( 64 | accumulatTotalPirce, 65 | accumulatFundRate / 12, 66 | accumlatMonth 67 | ); 68 | const principalList = mergeList( 69 | principalCommList, 70 | principalAccuList, 71 | commerceMonth, 72 | accumlatMonth 73 | ); 74 | const principalTotal = getPrincipalTotal( 75 | principalList, 76 | commerceTotalPirce + accumulatTotalPirce 77 | ); 78 | const equalInterestPayMonth = parseInt( 79 | ((commercePayMonty + accumlatPayMonty) * 10000).toFixed(0), 80 | 10 81 | ); 82 | resolve({ 83 | equalInterest: { 84 | payMonth: equalInterestPayMonth, 85 | totalInterest: interestTotal.toFixed(1), 86 | monthDecline: 0 87 | }, 88 | equalPrincipal: { 89 | payMonth: principalList[0], 90 | totalInterest: principalTotal.toFixed(1), 91 | monthDecline: principalList[0] - principalList[1] 92 | }, 93 | equalInterestMonthList: new Array( 94 | Math.max(commerceMonth, accumlatMonth) 95 | ).fill(equalInterestPayMonth), 96 | equalPrincipalMonthList: principalList 97 | }); 98 | }); 99 | 100 | /** 101 | * 获取(等额本息)每月还款额 102 | * 103 | * @param loanTotalPrice 贷款总额 104 | * @param monthLoanRate 每月利息 105 | * @param monthNum 贷款期限(月份) 106 | * @return 107 | */ 108 | const getInterestEveryMonth = ( 109 | loanTotalPrice: number, 110 | monthLoanRate: number, 111 | monthNum: number 112 | ): number => { 113 | if (Math.pow(1 + monthLoanRate, monthNum) - 1 == 0) { 114 | return 0; 115 | } 116 | return ( 117 | (loanTotalPrice * monthLoanRate * Math.pow(1 + monthLoanRate, monthNum)) / 118 | (Math.pow(1 + monthLoanRate, monthNum) - 1) 119 | ); 120 | }; 121 | 122 | /** 123 | * 获取(等额本息)利息总额 124 | * 125 | * @param repaymentTotal 还款总额 126 | * @param loanTotalPrice 贷款总额 127 | * @return 128 | */ 129 | const getInterestTotal = ( 130 | repaymentTotal: number, 131 | loanTotalPrice: number 132 | ): number => { 133 | return repaymentTotal - loanTotalPrice; 134 | }; 135 | 136 | const getPrincipalTotal = (list: number[], loanTotalPrice: number): number => { 137 | return list.reduce((p, c) => p + c) / 10000 - loanTotalPrice; 138 | }; 139 | 140 | const getPrincipalListByMonth = ( 141 | loanTotalPrice: number, 142 | monthLoanRate: number, 143 | monthNum: number 144 | ): number[] => { 145 | const accumulatList: number[] = []; 146 | let finshRepayTotal = 0; 147 | for (let i = 0; i < monthNum; i++) { 148 | const repayMonth = 149 | loanTotalPrice / monthNum + 150 | (loanTotalPrice - finshRepayTotal) * monthLoanRate; 151 | finshRepayTotal = finshRepayTotal + loanTotalPrice / monthNum; 152 | accumulatList.push(parseInt((repayMonth * 10000).toFixed(0), 10)); 153 | } 154 | return accumulatList; 155 | }; 156 | 157 | /** 158 | * 在组合贷款下拼接(等额本金)和(等额本金)分月还款额 159 | * 160 | * @param commList 商业贷款分月还款数额列表 161 | * @param accuList 公积金贷款分月还款数额列表 162 | * @param commMonthNum 商业贷款期限(月份) 163 | * @param accuMonthNum 公积金贷款期限(月份) 164 | * @return 165 | */ 166 | const mergeList = ( 167 | commList: number[], 168 | accuList: number[], 169 | commMonthNum: number, 170 | accuMonthNum: number 171 | ): number[] => { 172 | const size = Math.min(commMonthNum, accuMonthNum); 173 | 174 | const list: number[] = []; 175 | for (let i = 0; i < size; i++) { 176 | list.push((commList[i] || 0) + (accuList[i] || 0)); 177 | } 178 | return list; 179 | }; 180 | 181 | -------------------------------------------------------------------------------- /src/pages/calculator/compute-header/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-01 13:55:57 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { FunctionComponent } from "react"; 9 | import { View, Text, Image } from "@tarojs/components"; 10 | import "./index.scss"; 11 | import { 12 | GRADIENT_BG, 13 | PERCENT_ICON, 14 | RIGHT_ARROW, 15 | RIGHT_ARROW_WHITE 16 | } from "../constants"; 17 | import { BoxShadow, LinearGradient } from "@components"; 18 | import { formatFloat } from "@utils"; 19 | 20 | interface HouseLoanComputeHeaderProps { 21 | way: number; 22 | houseTotal: number; 23 | userLoanWay: string; 24 | tip: string; 25 | downPayRate: number; 26 | equalInterestPayMonth: string; 27 | equalPrincipalPayMonth: string; 28 | goHistory: () => void; 29 | goMonthlyPayments: () => void; 30 | } 31 | 32 | const HouseLoanComputeHeader: FunctionComponent = props => { 33 | const { 34 | way, 35 | houseTotal, 36 | tip, 37 | userLoanWay, 38 | downPayRate, 39 | equalInterestPayMonth, 40 | equalPrincipalPayMonth, 41 | goHistory, 42 | goMonthlyPayments 43 | } = props; 44 | 45 | return ( 46 | 47 | 55 | 56 | 57 | 58 | 59 | 房屋总价 60 | 69 | {way === 1 ? houseTotal : "--"} 70 | 71 | 72 | 73 | 74 | 查看历史 75 | 79 | 80 | 81 | {tip} 82 | 83 | 84 | 96 | 97 | 首付款 98 | 99 | 108 | {way === 1 109 | ? Math.floor( 110 | formatFloat(houseTotal * downPayRate || 0, 1) as number 111 | ) 112 | : "--"} 113 | {way === 1 && ( 114 | 115 | )} 116 | 117 | 118 | 119 | 120 | 121 | {userLoanWay === "等额本息" 122 | ? "每月应还(等额本息)" 123 | : "首月应还(等额本金)"} 124 | 125 | 126 | 134 | {userLoanWay === "等额本息" 135 | ? equalInterestPayMonth 136 | : equalPrincipalPayMonth} 137 | 138 | 139 | 140 | 144 | 145 | 对比{userLoanWay === "等额本息" ? "等额本金" : "等额本息"} 146 | 月供 147 | 148 | 149 | 150 | 151 | 152 | 153 | ); 154 | }; 155 | 156 | export default React.memo(HouseLoanComputeHeader); 157 | -------------------------------------------------------------------------------- /src/pages/calculator/monthly-payments/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:41:04 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { Component } from "react"; 9 | import Taro from "@tarojs/taro"; 10 | import { View, Text, Image, ScrollView } from "@tarojs/components"; 11 | import { 12 | MONTY_DATA, 13 | MONTY_TITLE, 14 | CHECK_RIDIO_Y, 15 | CHECK_RIDIO 16 | } from "../constants"; 17 | import "./index.scss"; 18 | import { getGlobalData, getStorageData } from "@utils"; 19 | import { SafeAreaView, StatusBar } from "@components"; 20 | 21 | export default class HouseLoanComputeMontylyPayments extends Component< 22 | any, 23 | any 24 | > { 25 | page: number = 1; 26 | total: number; 27 | 28 | constructor(props: any) { 29 | super(props); 30 | this.state = { 31 | checked: "equalInterest", 32 | equalInterest: {}, 33 | equalPrincipal: {}, 34 | equalInterestMonthList: [], 35 | interestList: [], 36 | assessInfo: null, 37 | equalPrincipalMonthList: [], 38 | principalList: [], 39 | tip: "", 40 | loanAmount: 0 41 | }; 42 | } 43 | 44 | async componentDidMount() { 45 | const params = getGlobalData("COMPUTE_RESULT") || {}; 46 | this.init(params); 47 | const { type = "equalInterest" } = 48 | (await getStorageData("USER_LOAN_WAY")) || {}; 49 | this.setState({ 50 | checked: type 51 | }); 52 | } 53 | 54 | init = async (data: any = {}) => { 55 | try { 56 | const { 57 | equalInterestMonthList = [], 58 | equalPrincipalMonthList = [] 59 | } = data; 60 | this.total = Math.floor(equalInterestMonthList.length / 10); 61 | this.setState({ 62 | interestList: equalInterestMonthList.slice(0, 10), 63 | principalList: equalPrincipalMonthList.slice(0, 10), 64 | ...data 65 | }); 66 | } catch (error) { 67 | console.log(error); 68 | } 69 | }; 70 | 71 | seleceFirst = (data: any) => async () => { 72 | await Taro.setStorage({ key: "USER_LOAN_WAY", data }); 73 | this.setState( 74 | { 75 | checked: data.type 76 | }, 77 | () => { 78 | Taro.showToast({ 79 | title: `月供将以${data.title}的形式展示`, 80 | icon: "none" 81 | }); 82 | } 83 | ); 84 | }; 85 | 86 | onScrollToLower = () => { 87 | if (this.page >= this.total) return; 88 | this.page++; 89 | const { equalInterestMonthList, equalPrincipalMonthList } = this.state; 90 | this.setState({ 91 | interestList: equalInterestMonthList.slice(0, this.page * 10), 92 | principalList: equalPrincipalMonthList.slice(0, this.page * 10) 93 | }); 94 | }; 95 | 96 | render() { 97 | const { 98 | checked, 99 | interestList, 100 | principalList, 101 | loanAmount, 102 | tip 103 | } = this.state; 104 | return ( 105 | 106 | 107 | 113 | 114 | 115 | 贷款总额 116 | {loanAmount} 117 | 118 | 119 | {tip} 120 | 121 | 122 | {MONTY_DATA.map((item: any) => { 123 | return ( 124 | 125 | {item.title} 126 | 127 | 128 | {MONTY_TITLE[item.type]} 129 | 130 | 131 | {this.state[item.type].payMonth} 132 | 133 | 134 | 135 | 136 | 利息总额(万元) 137 | 138 | 139 | {this.state[item.type].totalInterest} 140 | 141 | 142 | 143 | 特点 144 | 145 | {item.type !== "equalPrincipal" 146 | ? "每月月供稳定" 147 | : `每月递减${this.state[item.type].monthDecline}元`} 148 | 149 | 150 | 154 | 160 | 优先看{item.title} 161 | 162 | 163 | ); 164 | })} 165 | 166 | 167 | 168 | 还款细则 169 | 170 | 171 | 172 | 等额本息 173 | 174 | 175 | 等额本金 176 | 177 | 178 | {interestList.map((item: any, index: number) => { 179 | return ( 180 | 186 | 187 | 第{index + 1}个月 188 | 189 | ¥{item} 190 | 191 | ¥{principalList[index]} 192 | 193 | 194 | ); 195 | })} 196 | 197 | 198 | 199 | 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/pages/calculator/line-wrap.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-06-28 13:47:24 5 | * @Last Modified by: qiuz 6 | * @Last Modified time: 2021-01-04 22:04:16 7 | */ 8 | 9 | import React, { Component } from "react"; 10 | import { View, Text, Image, Input, Button } from "@tarojs/components"; 11 | import "./index.scss"; 12 | import { RIGHT_ARROW } from "./constants"; 13 | import { Pciker, Modal } from "@components"; 14 | 15 | interface LineWrapProps { 16 | type: string[]; 17 | data: any[]; 18 | onChangePicker: (...args: any) => void; 19 | onInputChange: (...args: any) => void; 20 | onBlur: (...args: any) => void; 21 | } 22 | 23 | export class LineWrap extends Component { 24 | static defaultProps = { 25 | data: [], 26 | type: [], 27 | onChangePicker: () => {}, 28 | onInputChange: () => {}, 29 | onBlur: () => {} 30 | }; 31 | 32 | static options = { 33 | addGlobalClass: true 34 | }; 35 | 36 | constructor(props: LineWrapProps) { 37 | super(props); 38 | this.state = { 39 | visible: false, 40 | explainData: {}, 41 | focus: [] 42 | }; 43 | } 44 | 45 | handlePickerChange = (data: any, index: number) => (value: any) => { 46 | const valueMap = data.range.filter( 47 | (item: any) => item.value === Number(value[0]) 48 | ); 49 | this.props.onChangePicker(data, valueMap[0] || data.range[0], index); 50 | }; 51 | 52 | handleInputChange = (item: any, index: number) => (e: any) => { 53 | let { value } = e.detail; 54 | if (item.inputType === "number" || item.keyboardType === "number-pad") { 55 | value = parseInt(value, 10); 56 | } 57 | this.props.onInputChange(item, value, index); 58 | }; 59 | 60 | showExplain = (data: any) => () => { 61 | this.setState({ 62 | explainData: data, 63 | visible: true 64 | }); 65 | }; 66 | 67 | closeModal = () => { 68 | this.setState({ 69 | visible: false 70 | }); 71 | }; 72 | 73 | onMoreClick = (_url: string) => () => {}; 74 | 75 | onFocus = (index: number) => () => { 76 | const { focus } = this.state; 77 | focus[index] = true; 78 | this.setState({ 79 | focus 80 | }); 81 | }; 82 | 83 | onBlur = (loan: any, index: number) => (e: any) => { 84 | const { focus } = this.state; 85 | focus[index] = false; 86 | this.setState( 87 | { 88 | focus 89 | }, 90 | () => { 91 | loan.blurCheck && this.props.onBlur(e); 92 | } 93 | ); 94 | }; 95 | render() { 96 | const { data, type } = this.props; 97 | const { visible, explainData, focus } = this.state; 98 | const list = data.filter( 99 | _item => type.indexOf(_item && _item.renderType) > -1 100 | ); 101 | return ( 102 | 103 | {explainData.title && ( 104 | 112 | 113 | {explainData.title} 114 | 115 | 116 | {explainData.content} 117 | 118 | 119 | 120 | 123 | 124 | 125 | )} 126 | {list.map((loan: any, index: number) => { 127 | let valueIndex = 0; 128 | if (loan.range) { 129 | loan.range = loan.rangeFilter 130 | ? loan.range.filter(_r => _r.limit === loan.rangeFilter) 131 | : loan.range; 132 | valueIndex = loan.range.findIndex( 133 | (item: any) => item.value === Number(loan.value) 134 | ); 135 | } 136 | return ( 137 | 138 | 139 | {loan.name} 140 | {loan.icon && ( 141 | 142 | 146 | 147 | )} 148 | 149 | 150 | {loan.type === "selector" ? ( 151 | 152 | 160 | 161 | {loan.range[valueIndex] && loan.range[valueIndex].label} 162 | 163 | 164 | 165 | ) : ( 166 | 190 | )} 191 | 192 | {loan.unit === "arrowright" ? ( 193 | 194 | ) : ( 195 | 196 | {loan.unit} 197 | 198 | )} 199 | 200 | 201 | 202 | 203 | ); 204 | })} 205 | 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/pages/calculator/index.rn.scss: -------------------------------------------------------------------------------- 1 | .calculator { 2 | width: 100%; 3 | background-color: #ffffff; 4 | flex: 1; 5 | position: relative; 6 | } 7 | 8 | .monty-pay { 9 | align-items: flex-end; 10 | 11 | &__tip { 12 | display: flex; 13 | flex-direction: row; 14 | align-items: center; 15 | margin-top: 20px; 16 | 17 | &__text { 18 | font-size: 24px; 19 | font-weight: normal; 20 | color: rgba(71, 75, 78, 1); 21 | } 22 | } 23 | } 24 | 25 | .unit { 26 | flex-shrink: 0; 27 | 28 | &__text { 29 | font-size: 32px; 30 | color: rgba(151, 155, 158, 1); 31 | } 32 | } 33 | 34 | .content { 35 | padding: 0 40px; 36 | padding-bottom: 200px; 37 | } 38 | 39 | .compute-way { 40 | padding-top: 50px; 41 | margin-bottom: 30px; 42 | display: flex; 43 | flex-direction: row; 44 | align-items: baseline; 45 | justify-content: space-between; 46 | 47 | &__title { 48 | font-size: 40px; 49 | font-weight: bold; 50 | color: rgba(11, 15, 18, 1); 51 | } 52 | 53 | &__way-box { 54 | display: flex; 55 | flex-direction: row; 56 | align-items: flex-end; 57 | } 58 | } 59 | 60 | .pseudo-content { 61 | margin-left: 40px; 62 | position: relative; 63 | align-items: center; 64 | display: flex; 65 | 66 | &__text { 67 | font-size: 28px; 68 | font-weight: bold; 69 | color: rgba(11, 15, 18, 1); 70 | &-active { 71 | color: #1FB081; 72 | } 73 | } 74 | 75 | &__pseudo { 76 | position: absolute; 77 | bottom: -10px; 78 | align-self: center; 79 | display: flex; 80 | width: 30px; 81 | height: 4px; 82 | background: rgba(35, 201, 147, 1); 83 | } 84 | } 85 | 86 | .arrow-right { 87 | width: 20px; 88 | height: 20px; 89 | } 90 | 91 | .active { 92 | color: rgba(31, 176, 129, 1); 93 | position: relative; 94 | } 95 | 96 | .input-content { 97 | display: flex; 98 | flex-direction: row; 99 | justify-content: space-between; 100 | align-items: center; 101 | padding: 44px 0; 102 | position: relative; 103 | 104 | &-line { 105 | width: 100%; 106 | position: absolute; 107 | bottom: 0; 108 | height: 1PX; 109 | background: #E7EBEE; 110 | } 111 | 112 | &__label { 113 | width: 168px; 114 | margin-right: 36px; 115 | flex-shrink: 0; 116 | display: flex; 117 | flex-direction: row; 118 | align-items: center; 119 | 120 | &-text { 121 | color: rgba(11, 15, 18, 1); 122 | font-size: 32px; 123 | font-weight: normal; 124 | } 125 | 126 | &-icon { 127 | width: 27px; 128 | height: 27px; 129 | margin-left: 5px; 130 | } 131 | } 132 | 133 | } 134 | 135 | .value-wrap { 136 | flex-direction: row; 137 | align-items: center; 138 | display: flex; 139 | flex: 1; 140 | 141 | &__unit { 142 | font-size: 32px; 143 | font-weight: normal; 144 | color: rgba(151, 155, 158, 1); 145 | flex-shrink: 0; 146 | } 147 | 148 | } 149 | 150 | 151 | .input { 152 | flex: 1; 153 | padding: 0; 154 | font-size: 32px; 155 | color: #0B0F12; 156 | font-family: PingFangSC-Regular; 157 | } 158 | 159 | .picker-box { 160 | flex: 1; 161 | 162 | &__picker { 163 | display: flex; 164 | height: 1000px; 165 | &__text { 166 | font-size: 32px; 167 | color: #0B0F12; 168 | } 169 | } 170 | } 171 | 172 | .at-icon { 173 | margin-left: 10px; 174 | } 175 | 176 | 177 | .broker { 178 | width: 100%; 179 | 180 | &-title { 181 | margin-top: 60px; 182 | margin-bottom: 24px; 183 | display: flex; 184 | font-weight: bold; 185 | font-size: 36px; 186 | font-family: PingFangSC-Medium; 187 | color: rgba(11, 15, 18, 1); 188 | } 189 | 190 | &-img { 191 | width: 80px; 192 | height: 80px; 193 | border-radius: 40px; 194 | margin-right: 20px; 195 | } 196 | 197 | &-btn-img { 198 | width: 72px; 199 | height: 72px; 200 | background: #E9F9F4; 201 | border-radius: 36px; 202 | overflow: hidden; 203 | 204 | &-1 { 205 | margin-right: 36px; 206 | } 207 | } 208 | 209 | 210 | &-box { 211 | display: flex; 212 | flex-direction: row; 213 | flex-shrink: 0; 214 | align-items: center; 215 | justify-content: space-between; 216 | padding: 20px 30px; 217 | width: 100%; 218 | background: rgba(255, 255, 255, 1); 219 | border-radius: 4px; 220 | border: 1px solid rgba(211, 215, 218, 0.8); 221 | 222 | &-left { 223 | display: flex; 224 | flex-direction: row; 225 | align-items: center; 226 | } 227 | 228 | &-right { 229 | display: flex; 230 | flex-direction: row; 231 | align-items: center; 232 | } 233 | } 234 | 235 | &-info { 236 | display: flex; 237 | flex-direction: column; 238 | 239 | &-name { 240 | font-size: 32px; 241 | font-family: PingFangSC-Medium; 242 | } 243 | 244 | &-companyName { 245 | font-size: 24px; 246 | font-family: PingFangSC-Regular; 247 | font-weight: 400; 248 | color: rgba(11, 15, 18, 1); 249 | margin-top: 4px; 250 | width: 300px; 251 | overflow: hidden; 252 | } 253 | } 254 | 255 | 256 | } 257 | 258 | .compute { 259 | position: absolute; 260 | bottom: 0; 261 | left: 0; 262 | right: 0; 263 | width: 100%; 264 | z-index: 10; 265 | background: rgba(255, 255, 255, 1); 266 | display: flex; 267 | align-items: center; 268 | justify-content: center; 269 | 270 | &-btn { 271 | margin: 16px auto; 272 | display: flex; 273 | align-items: center; 274 | justify-content: center; 275 | width: 95%; 276 | height: 100px; 277 | background: rgba(35, 201, 147, 1); 278 | border-radius: 4px; 279 | text-align: center; 280 | flex: 1; 281 | 282 | &-text { 283 | font-size: 36px; 284 | font-family: PingFangSC-Regular; 285 | font-weight: 400; 286 | color: rgba(255, 255, 255, 1); 287 | } 288 | } 289 | } 290 | 291 | .fixed { 292 | position: absolute; 293 | top: 0; 294 | left: 0; 295 | bottom: 0; 296 | right: 0; 297 | width: 100%; 298 | height: 100%; 299 | z-index: 9999; 300 | } 301 | 302 | .keyboard-box { 303 | position: absolute; 304 | bottom: 0; 305 | height: 100px; 306 | left: 0; 307 | right: 0; 308 | z-index: 99; 309 | background: #FFFFFF; 310 | display: flex; 311 | align-items: center; 312 | flex-direction: row; 313 | 314 | &-text { 315 | height: 54px; 316 | line-height: 54px; 317 | display: flex; 318 | align-items: center; 319 | font-size: 32px; 320 | font-family: PingFangSC-Regular; 321 | font-weight: 400; 322 | color: rgba(171, 175, 177, 1); 323 | margin-left: 40px; 324 | flex-shrink: 0; 325 | } 326 | 327 | &-input { 328 | height: 54px; 329 | } 330 | 331 | &-view { 332 | padding: 0 10px; 333 | flex: 1; 334 | margin-left: 54px; 335 | border-radius: 4px; 336 | background: rgba(240, 240, 240, 1); 337 | } 338 | 339 | &-confirm { 340 | flex-shrink: 0; 341 | margin: 0 32px; 342 | font-size: 32px; 343 | font-family: PingFangSC-Regular; 344 | font-weight: 400; 345 | color: rgba(31, 176, 129, 1); 346 | } 347 | } 348 | 349 | .mask { 350 | position: absolute; 351 | top: 0; 352 | left: 0; 353 | bottom: 0; 354 | right: 0; 355 | z-index: 90; 356 | width: 100%; 357 | height: 100%; 358 | background: rgba(0, 0, 0, .6); 359 | } 360 | 361 | .explain { 362 | display: flex; 363 | flex-direction: column; 364 | padding: 10px; 365 | padding-top: 0; 366 | 367 | &-title { 368 | margin-top: 20px; 369 | margin-bottom: 40px; 370 | text-align: center; 371 | width: 100%; 372 | font-size: 40px; 373 | font-family: PingFangSC-Semibold; 374 | font-weight: bold; 375 | color: rgba(11, 15, 18, 1); 376 | } 377 | 378 | &-tip { 379 | display: flex; 380 | flex-direction: column; 381 | padding-bottom: 40px; 382 | 383 | &-text { 384 | font-size: 32px; 385 | font-family: PingFangSC-Regular; 386 | font-weight: 400; 387 | color: rgba(11, 15, 18, 1); 388 | line-height: 48px; 389 | } 390 | 391 | &-more { 392 | color: #0E3BBD; 393 | } 394 | } 395 | 396 | &-btn { 397 | width: 100%; 398 | height: 100px; 399 | background: rgba(35, 201, 147, 1); 400 | border-radius: 4px; 401 | margin-bottom: 20px; 402 | 403 | &-text { 404 | font-size: 32px; 405 | font-family: PingFangSC-Semibold; 406 | font-weight: bold; 407 | color: rgba(255, 255, 255, 1); 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/pages/calculator/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-10 23:41:11 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | export const imgPrefix = "../../assets/images"; 9 | 10 | import RIGHT_ARROW from "../../assets/images/yz_prop_icon_arrow.png"; 11 | 12 | // export const RIGHT_ARROW = `../../assets/images/yz_prop_icon_arrow.png`; 13 | import QUESTION_ICON from "../../assets/images/esf_calculator_icon_question.png"; 14 | import RIGHT_ARROW_WHITE from "../../assets/images/yz_prop_icon_arrow_white.png"; 15 | import REPORT_BG_MARK from "../../assets/images/esf_calculator_img_mark.png"; 16 | import REPORT_BG_BUILDING from "../../assets/images/esf_calculator_img_building.png"; 17 | import CHECK_RIDIO_Y from "../../assets/images/comm_form_icon_gouxuan.png"; 18 | import CHECK_RIDIO from "../../assets/images/comm_form_icon_weigouxuan.png"; 19 | 20 | import PERCENT_ICON from "../../assets/images/esf_calculator_img_percent.png"; 21 | import GRADIENT_BG from "../../assets/images/esf_calculator_img_bggradient.png"; 22 | 23 | export { 24 | RIGHT_ARROW, 25 | PERCENT_ICON, 26 | RIGHT_ARROW_WHITE, 27 | REPORT_BG_MARK, 28 | REPORT_BG_BUILDING, 29 | CHECK_RIDIO_Y, 30 | CHECK_RIDIO, 31 | GRADIENT_BG, 32 | }; 33 | 34 | export const LIST_TYPE = { 35 | 0: ["reservedFunds", "busniessLoan", "group"], 36 | 1: ["busniessLoan"], 37 | 2: ["reservedFunds"] 38 | }; 39 | 40 | export const COMPUTE_WAY = { 41 | 0: ["loan"], 42 | 1: ["house", "loan"] 43 | }; 44 | 45 | export const getRenderList = (data: any) => { 46 | const { loanLrpType, commerceLoanYear, accumulatFundYear } = data; 47 | const LPR = 48 | loanLrpType === 1 49 | ? [ 50 | { 51 | name: "LPR", 52 | icon: QUESTION_ICON, 53 | value: data["loanLrp"], 54 | readOnly: true, 55 | key: "loanLrp", 56 | valueStyle: { 57 | color: "#0B0F12" 58 | }, 59 | ratio: 100, 60 | explain: { 61 | title: "LPR(贷款市场报价利率)", 62 | content: 63 | "自2019年10月起,商贷利率开始改用LPR(贷款市场报价利率)计算。LPR基准利率每月更新一次,实际贷款利率在LPR的基础上进行一定的浮动。" 64 | }, 65 | unit: "%", 66 | renderType: "busniessLoan" 67 | }, 68 | { 69 | name: "基点", 70 | icon: QUESTION_ICON, 71 | value: data["commercialLoanBasePoint"], 72 | key: "commercialLoanBasePoint", 73 | unit: "BP(‱)", 74 | explain: { 75 | title: "什么是基点?", 76 | content: 77 | "1个基点=0.01%,如果浮动10个基点,相当于在LPR的基础上增加0.1%为实际贷款利率。" 78 | }, 79 | inputType: "number", 80 | keyboardType: "number-pad", 81 | renderType: "busniessLoan" 82 | }, 83 | { 84 | name: "商贷利率", 85 | readOnly: true, 86 | value: data["commerceLoanRateEqua"], 87 | valueStyle: { 88 | color: "#979B9E" 89 | }, 90 | key: "commerceLoanRateEqua", 91 | unit: data["commerceLoanRateNewUnit"], 92 | unitStyle: { 93 | color: "#0B0F12" 94 | }, 95 | renderType: "busniessLoan" 96 | } 97 | ] 98 | : [ 99 | { 100 | name: "商贷利率", 101 | readOnly: true, 102 | range: data.options["commerceLoanRate"] || [], 103 | value: data["commerceLoanRate"], 104 | type: "selector", 105 | rangeFilter: 106 | commerceLoanYear > 5 107 | ? "outFive" 108 | : commerceLoanYear > 1 109 | ? "inFive" 110 | : "inOne", 111 | key: "commerceLoanRate", 112 | unit: "arrowright", 113 | renderType: "busniessLoan" 114 | } 115 | ]; 116 | return [ 117 | { 118 | name: "房屋总价", 119 | value: data["houseTotal"], 120 | key: "houseTotal", 121 | unit: "万", 122 | maxLength: 6, 123 | inputType: "number", 124 | keyboardType: "number-pad", 125 | renderType: "house", 126 | hidden: "" 127 | }, 128 | { 129 | name: "首付选择", 130 | value: data["downPayRate"], 131 | type: "selector", 132 | key: "downPayRate", 133 | range: data.options["downPayRate"] || [], 134 | unit: "arrowright", 135 | renderType: "house" 136 | }, 137 | { 138 | name: "贷款金额", 139 | value: data["loanAmount"], 140 | inputType: "number", 141 | readOnly: data.way === 1, 142 | keyboardType: "number-pad", 143 | key: "loanAmount", 144 | unit: "万", 145 | valueStyle: { 146 | color: "#0B0F12" 147 | }, 148 | renderType: "loan" 149 | }, 150 | { 151 | name: "公积金金额", 152 | value: data["accumulatTotalPirce"], 153 | key: "accumulatTotalPirce", 154 | unit: "万", 155 | blurCheck: true, 156 | inputType: "number", 157 | keyboardType: "number-pad", 158 | renderType: "group" 159 | }, 160 | { 161 | name: "公积金年限", 162 | value: data["accumulatFundYear"], 163 | type: "selector", 164 | key: "accumulatFundYear", 165 | range: data.options["accumulatFundYear"] || [], 166 | unit: "arrowright", 167 | renderType: "reservedFunds" 168 | }, 169 | { 170 | name: "公积金利率", 171 | value: data["accumulatFundRate"], 172 | type: "selector", 173 | key: "accumulatFundRate", 174 | renderType: "reservedFunds", 175 | range: data.options["accumulatFundRate"] || [], 176 | rangeFilter: accumulatFundYear > 5 ? "outFive" : "inFive", 177 | unit: "arrowright" 178 | }, 179 | { 180 | name: "商贷金额", 181 | value: data["commerceTotalPirce"], 182 | key: "commerceTotalPirce", 183 | unit: "万", 184 | inputType: "number", 185 | keyboardType: "number-pad", 186 | renderType: "group" 187 | }, 188 | { 189 | name: "商贷年限", 190 | value: data["commerceLoanYear"], 191 | type: "selector", 192 | key: "commerceLoanYear", 193 | range: data.options["commerceLoanYear"] || [], 194 | unit: "arrowright", 195 | renderType: "busniessLoan" 196 | }, 197 | { 198 | name: "利率方式", 199 | value: data["loanLrp"], 200 | type: "selector", 201 | key: "loanLrp", 202 | range: data.options["loanLrp"] || [], 203 | unit: "arrowright", 204 | renderType: "busniessLoan" 205 | }, 206 | ...LPR 207 | ]; 208 | }; 209 | 210 | export const COMPUTE_WAY_TITLE = [ 211 | { 212 | id: 1, 213 | index: 0, 214 | name: "按贷款总额", 215 | key: "way" 216 | }, 217 | { 218 | id: 2, 219 | index: 1, 220 | name: "按房屋总价", 221 | key: "way" 222 | } 223 | ]; 224 | export const LOAN_WAY_TITLE = [ 225 | { 226 | id: 3, 227 | index: 0, 228 | name: "组合贷", 229 | key: "loanType" 230 | }, 231 | { 232 | id: 4, 233 | index: 1, 234 | name: "商业贷", 235 | key: "loanType" 236 | }, 237 | { 238 | id: 5, 239 | index: 2, 240 | name: "公积金贷", 241 | key: "loanType" 242 | } 243 | ]; 244 | 245 | export const MONTY_DATA = [ 246 | { 247 | title: "等额本息", 248 | type: "equalInterest", 249 | advant: "每月月供稳定" 250 | }, 251 | { 252 | title: "等额本金", 253 | type: "equalPrincipal", 254 | advant: "每月递减20元" 255 | } 256 | ]; 257 | 258 | export const MONTY_TITLE = { 259 | equalInterest: "每月应还(元)", 260 | equalPrincipal: "首月应还(元)" 261 | }; 262 | 263 | export const OPTION = { 264 | options: { 265 | commerceLoanYear: [ 266 | { 267 | label: "1年", 268 | value: 1.0 269 | }, 270 | { 271 | label: "2年", 272 | value: 2.0 273 | }, 274 | { 275 | label: "3年", 276 | value: 3.0 277 | }, 278 | { 279 | label: "4年", 280 | value: 4.0 281 | }, 282 | { 283 | label: "5年", 284 | value: 5.0 285 | }, 286 | { 287 | label: "6年", 288 | value: 6.0 289 | }, 290 | { 291 | label: "7年", 292 | value: 7.0 293 | }, 294 | { 295 | label: "8年", 296 | value: 8.0 297 | }, 298 | { 299 | label: "9年", 300 | value: 9.0 301 | }, 302 | { 303 | label: "10年", 304 | value: 10.0 305 | }, 306 | { 307 | label: "11年", 308 | value: 11.0 309 | }, 310 | { 311 | label: "12年", 312 | value: 12.0 313 | }, 314 | { 315 | label: "13年", 316 | value: 13.0 317 | }, 318 | { 319 | label: "14年", 320 | value: 14.0 321 | }, 322 | { 323 | label: "15年", 324 | value: 15.0 325 | }, 326 | { 327 | label: "16年", 328 | value: 16.0 329 | }, 330 | { 331 | label: "17年", 332 | value: 17.0 333 | }, 334 | { 335 | label: "18年", 336 | value: 18.0 337 | }, 338 | { 339 | label: "19年", 340 | value: 19.0 341 | }, 342 | { 343 | label: "20年", 344 | value: 20.0 345 | }, 346 | { 347 | label: "21年", 348 | value: 21.0 349 | }, 350 | { 351 | label: "22年", 352 | value: 22.0 353 | }, 354 | { 355 | label: "23年", 356 | value: 23.0 357 | }, 358 | { 359 | label: "24年", 360 | value: 24.0 361 | }, 362 | { 363 | label: "25年", 364 | value: 25.0 365 | }, 366 | { 367 | label: "26年", 368 | value: 26.0 369 | }, 370 | { 371 | label: "27年", 372 | value: 27.0 373 | }, 374 | { 375 | label: "28年", 376 | value: 28.0 377 | }, 378 | { 379 | label: "29年", 380 | value: 29.0 381 | }, 382 | { 383 | label: "30年", 384 | value: 30.0 385 | } 386 | ], 387 | commerceLoanRate: [ 388 | { 389 | label: "3.43%(旧版基准利率7折)", 390 | value: 0.0343 391 | }, 392 | { 393 | label: "3.68%(旧版基准利率7.5折)", 394 | value: 0.0368 395 | }, 396 | { 397 | label: "3.92%(旧版基准利率8折)", 398 | value: 0.0392 399 | }, 400 | { 401 | label: "4.17%(旧版基准利率8.5折)", 402 | value: 0.0417 403 | }, 404 | { 405 | label: "4.41%(旧版基准利率9折)", 406 | value: 0.0441 407 | }, 408 | { 409 | label: "4.66%(旧版基准利率9.5折)", 410 | value: 0.0466 411 | }, 412 | { 413 | label: "4.9%(旧版基准利率)", 414 | value: 0.049 415 | }, 416 | { 417 | label: "5.15%(旧版基准利率1.05倍)", 418 | value: 0.0515 419 | }, 420 | { 421 | label: "5.39%(旧版基准利率1.1倍)", 422 | value: 0.0539 423 | }, 424 | { 425 | label: "5.64%(旧版基准利率1.15倍)", 426 | value: 0.0564 427 | }, 428 | { 429 | label: "5.88%(旧版基准利率1.2倍)", 430 | value: 0.0588 431 | }, 432 | { 433 | label: "6.13%(旧版基准利率1.25倍)", 434 | value: 0.0613 435 | }, 436 | { 437 | label: "6.37%(旧版基准利率1.3倍)", 438 | value: 0.0637 439 | } 440 | ], 441 | commerceLoanInFiveYearRate: [ 442 | { 443 | label: "3.33%(旧版基准利率7折)", 444 | value: 0.0333 445 | }, 446 | { 447 | label: "3.56%(旧版基准利率7.5折)", 448 | value: 0.0356 449 | }, 450 | { 451 | label: "3.8%(旧版基准利率8折)", 452 | value: 0.038 453 | }, 454 | { 455 | label: "4.04%(旧版基准利率8.5折)", 456 | value: 0.0404 457 | }, 458 | { 459 | label: "4.28%(旧版基准利率9折)", 460 | value: 0.0428 461 | }, 462 | { 463 | label: "4.51%(旧版基准利率9.5折)", 464 | value: 0.0451 465 | }, 466 | { 467 | label: "4.75%(旧版基准利率)", 468 | value: 0.0475 469 | }, 470 | { 471 | label: "4.99%(旧版基准利率1.05倍)", 472 | value: 0.0499 473 | }, 474 | { 475 | label: "5.23%(旧版基准利率1.1倍)", 476 | value: 0.0523 477 | }, 478 | { 479 | label: "5.46%(旧版基准利率1.15倍)", 480 | value: 0.0546 481 | }, 482 | { 483 | label: "5.7%(旧版基准利率1.2倍)", 484 | value: 0.057 485 | }, 486 | { 487 | label: "5.94%(旧版基准利率1.25倍)", 488 | value: 0.0594 489 | }, 490 | { 491 | label: "6.18%(旧版基准利率1.3倍)", 492 | value: 0.0618 493 | } 494 | ], 495 | commerceLoanInOneYearRate: [ 496 | { 497 | label: "3.05%(旧版基准利率7折)", 498 | value: 0.0305 499 | }, 500 | { 501 | label: "3.26%(旧版基准利率7.5折)", 502 | value: 0.0326 503 | }, 504 | { 505 | label: "3.48%(旧版基准利率8折)", 506 | value: 0.0348 507 | }, 508 | { 509 | label: "3.7%(旧版基准利率8.5折)", 510 | value: 0.037 511 | }, 512 | { 513 | label: "3.92%(旧版基准利率9折)", 514 | value: 0.0392 515 | }, 516 | { 517 | label: "4.13%(旧版基准利率9.5折)", 518 | value: 0.0413 519 | }, 520 | { 521 | label: "4.35%(旧版基准利率)", 522 | value: 0.0435 523 | }, 524 | { 525 | label: "4.57%(旧版基准利率1.05倍)", 526 | value: 0.0457 527 | }, 528 | { 529 | label: "4.79%(旧版基准利率1.1倍)", 530 | value: 0.0479 531 | }, 532 | { 533 | label: "5%(旧版基准利率1.15倍)", 534 | value: 0.05 535 | }, 536 | { 537 | label: "5.22%(旧版基准利率1.2倍)", 538 | value: 0.0522 539 | }, 540 | { 541 | label: "5.44%(旧版基准利率1.25倍)", 542 | value: 0.0544 543 | }, 544 | { 545 | label: "5.66%(旧版基准利率1.3倍)", 546 | value: 0.0566 547 | } 548 | ], 549 | loanLrp: [ 550 | { 551 | label: "使用最新LPR", 552 | value: 0.0465 553 | }, 554 | { 555 | label: "使用旧版基准利率", 556 | value: 0.0 557 | } 558 | ], 559 | loanInFiveYearLrp: null, 560 | accumulatFundYear: [ 561 | { 562 | label: "1年", 563 | value: 1.0 564 | }, 565 | { 566 | label: "2年", 567 | value: 2.0 568 | }, 569 | { 570 | label: "3年", 571 | value: 3.0 572 | }, 573 | { 574 | label: "4年", 575 | value: 4.0 576 | }, 577 | { 578 | label: "5年", 579 | value: 5.0 580 | }, 581 | { 582 | label: "6年", 583 | value: 6.0 584 | }, 585 | { 586 | label: "7年", 587 | value: 7.0 588 | }, 589 | { 590 | label: "8年", 591 | value: 8.0 592 | }, 593 | { 594 | label: "9年", 595 | value: 9.0 596 | }, 597 | { 598 | label: "10年", 599 | value: 10.0 600 | }, 601 | { 602 | label: "11年", 603 | value: 11.0 604 | }, 605 | { 606 | label: "12年", 607 | value: 12.0 608 | }, 609 | { 610 | label: "13年", 611 | value: 13.0 612 | }, 613 | { 614 | label: "14年", 615 | value: 14.0 616 | }, 617 | { 618 | label: "15年", 619 | value: 15.0 620 | }, 621 | { 622 | label: "16年", 623 | value: 16.0 624 | }, 625 | { 626 | label: "17年", 627 | value: 17.0 628 | }, 629 | { 630 | label: "18年", 631 | value: 18.0 632 | }, 633 | { 634 | label: "19年", 635 | value: 19.0 636 | }, 637 | { 638 | label: "20年", 639 | value: 20.0 640 | }, 641 | { 642 | label: "21年", 643 | value: 21.0 644 | }, 645 | { 646 | label: "22年", 647 | value: 22.0 648 | }, 649 | { 650 | label: "23年", 651 | value: 23.0 652 | }, 653 | { 654 | label: "24年", 655 | value: 24.0 656 | }, 657 | { 658 | label: "25年", 659 | value: 25.0 660 | }, 661 | { 662 | label: "26年", 663 | value: 26.0 664 | }, 665 | { 666 | label: "27年", 667 | value: 27.0 668 | }, 669 | { 670 | label: "28年", 671 | value: 28.0 672 | }, 673 | { 674 | label: "29年", 675 | value: 29.0 676 | }, 677 | { 678 | label: "30年", 679 | value: 30.0 680 | } 681 | ], 682 | accumulatFundRate: [ 683 | { 684 | label: "3.25%(最新基准利率1倍)", 685 | value: 0.0325 686 | }, 687 | { 688 | label: "3.58%(最新基准利率1.1倍)", 689 | value: 0.0358 690 | }, 691 | { 692 | label: "3.9%(最新基准利率1.2倍)", 693 | value: 0.039 694 | }, 695 | { 696 | label: "4.23%(最新基准利率1.3倍)", 697 | value: 0.0423 698 | } 699 | ], 700 | accumulatFundInFiveYearRate: [ 701 | { 702 | label: "2.75%(最新基准利率1倍)", 703 | value: 0.0275 704 | }, 705 | { 706 | label: "3.03%(最新基准利率1.1倍)", 707 | value: 0.0303 708 | }, 709 | { 710 | label: "3.3%(最新基准利率1.2倍)", 711 | value: 0.033 712 | }, 713 | { 714 | label: "3.58%(最新基准利率1.3倍)", 715 | value: 0.0358 716 | } 717 | ], 718 | downPayRate: [ 719 | { 720 | label: "10%", 721 | value: 0.1 722 | }, 723 | { 724 | label: "15%", 725 | value: 0.15 726 | }, 727 | { 728 | label: "20%", 729 | value: 0.2 730 | }, 731 | { 732 | label: "25%", 733 | value: 0.25 734 | }, 735 | { 736 | label: "30%", 737 | value: 0.3 738 | }, 739 | { 740 | label: "35%", 741 | value: 0.35 742 | }, 743 | { 744 | label: "40%", 745 | value: 0.4 746 | }, 747 | { 748 | label: "45%", 749 | value: 0.45 750 | }, 751 | { 752 | label: "50%", 753 | value: 0.5 754 | }, 755 | { 756 | label: "55%", 757 | value: 0.55 758 | }, 759 | { 760 | label: "60%", 761 | value: 0.6 762 | }, 763 | { 764 | label: "65%", 765 | value: 0.65 766 | }, 767 | { 768 | label: "70%", 769 | value: 0.7 770 | }, 771 | { 772 | label: "75%", 773 | value: 0.75 774 | }, 775 | { 776 | label: "80%", 777 | value: 0.8 778 | }, 779 | { 780 | label: "85%", 781 | value: 0.85 782 | }, 783 | { 784 | label: "90%", 785 | value: 0.9 786 | }, 787 | { 788 | label: "95%", 789 | value: 0.95 790 | } 791 | ] 792 | }, 793 | default: { 794 | commerceLoanYear: 30, 795 | commerceLoanRate: 0.049, 796 | commerceLoanInFiveYearRate: 0.0475, 797 | commerceLoanInOneYearRate: 0.0435, 798 | loanLrp: 0.0465, 799 | loanInOneYearLrp: 0.0385, 800 | accumulatFundYear: 30, 801 | accumulatFundRate: 0.0325, 802 | accumulatFundInFiveYearRate: 0.0275, 803 | downPayRate: 0.3, 804 | accumulatLoanLimit: 120.0 805 | } 806 | }; 807 | -------------------------------------------------------------------------------- /src/pages/calculator/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qiuz 3 | * @Github: 4 | * @Date: 2020-12-09 13:42:01 5 | * @Last Modified by: qiuz 6 | */ 7 | 8 | import React, { Component } from "react"; 9 | import Taro, { getCurrentInstance } from "@tarojs/taro"; 10 | import { View, Text, Input } from "@tarojs/components"; 11 | import "./index.scss"; 12 | import { 13 | KeyboardAwareScrollView, 14 | SafeAreaView, 15 | BoxShadow, 16 | StatusBar 17 | } from "@components"; 18 | import HouseLoanComputeHeader from "./compute-header"; 19 | import { TitleTpl } from "./title-tpl"; 20 | import { LineWrap } from "./line-wrap"; 21 | import { 22 | getRenderList, 23 | COMPUTE_WAY_TITLE, 24 | LOAN_WAY_TITLE, 25 | LIST_TYPE, 26 | COMPUTE_WAY, 27 | OPTION, 28 | } from "./constants"; 29 | import { equalInterestCalc } from "./helper"; 30 | import { isAndroid, setGlobalData, getStorageData, formatFloat } from "@utils"; 31 | 32 | export default class HouseLoanCompute extends Component { 33 | loading: any; 34 | scroll: any; 35 | computeResult: any; 36 | isFirstChange: boolean = true; 37 | 38 | constructor(props: any) { 39 | super(props); 40 | const { price = 0 } = getCurrentInstance().router!.params; 41 | this.state = { 42 | // 计算结果显示 43 | showResult: false, 44 | // 月付 45 | equalPrincipalPayMonth: 0, 46 | equalInterestPayMonth: 0, 47 | // 用户优先贷款方式 48 | userLoanWay: "等额本息", 49 | // 计算方式 50 | way: price ? 1 : 0, 51 | // 贷款方式 loanType + 1 = 1: 组合贷款 2: 商业贷款 3:公积金贷款 52 | loanType: 1, 53 | // 表单渲染项 54 | renderList: [], 55 | // 配置项 56 | options: {}, 57 | // 参数 58 | params: { 59 | // 房屋总价 60 | houseTotal: price || 0, 61 | // 首付百分比 62 | downPayRate: 30, 63 | // 贷款金额 64 | loanAmount: 0, 65 | // 公积金金额 66 | accumulatTotalPirce: 0, 67 | // 公积金贷款上限 68 | accumulatLoanLimit: 0, 69 | // 基点 70 | commercialLoanBasePoint: 0, 71 | // 商贷利率 72 | commerceLoanRate: 0, 73 | // 公积金利率 74 | publicReserveFundsRate: 0, 75 | // 商贷年限 76 | commercialLoanTerm: 0, 77 | // 商贷利率方式 78 | commercialLoanWay: 0, 79 | // 商贷金额 80 | commerceTotalPirce: 0 81 | }, 82 | // 默认值 83 | defaultValue: {}, 84 | keyboardHeight: -1, 85 | // 利率方式, 1 最新 | 0 旧版 86 | loanLrpType: 1, 87 | downPayRateCustom: "", 88 | // 安卓上 手动输入时 隐藏计算按钮 89 | btnOpacity: 1, 90 | // 安卓状态栏 91 | backgroundColor: "#fff" 92 | }; 93 | } 94 | 95 | componentDidMount() { 96 | this.getData(); 97 | } 98 | 99 | async componentDidShow() { 100 | const { title = "等额本息" } = await getStorageData("USER_LOAN_WAY") || {}; 101 | this.setState({ userLoanWay: title }); 102 | } 103 | 104 | /** 105 | * 获取渲染项,处理picker range以及默认值 106 | * 需要在每次表单值改变后重新调用 107 | */ 108 | getRenderList = () => { 109 | const { 110 | params, 111 | options, 112 | loanLrpType, 113 | commerceLoanRateNew, 114 | way 115 | } = this.state; 116 | const commerceLoanRateEqua = `${params.loanLrp * 100}% + ${ 117 | params.commercialLoanBasePoint 118 | }‱ = `; 119 | 120 | const commerceLoanRateNewUnit = `${formatFloat( 121 | commerceLoanRateNew * 100, 122 | 2 123 | )}%`; 124 | this.setState({ 125 | renderList: getRenderList({ 126 | ...params, 127 | options, 128 | way, 129 | downPayRateCustom: params.downPayRate, 130 | loanLrpType, 131 | commerceLoanRateEqua, 132 | commerceLoanRateNewUnit 133 | }) 134 | }); 135 | }; 136 | 137 | /** 138 | * @description 请求配置 139 | */ 140 | getData = () => { 141 | const { params } = this.state; 142 | const { default: defaultData, options } = OPTION; 143 | // 处理首付比例 手动输入选项 以及公积金利率一年 五年 商贷利率 144 | const { 145 | downPayRate = [], 146 | commerceLoanRate = [], 147 | commerceLoanInFiveYearRate = [], 148 | commerceLoanInOneYearRate = [], 149 | accumulatFundRate = [], 150 | accumulatFundInFiveYearRate = [] 151 | } = options; 152 | downPayRate.splice(0, 0, { 153 | value: -1, 154 | label: "手动输入" 155 | }); 156 | this.handleDownPaySelectLabel(params.houseTotal, downPayRate); 157 | params.loanAmount = Math.ceil( 158 | this.handleAmount(params.houseTotal, defaultData.downPayRate) as number 159 | ); 160 | // 处理旧版商贷利率 关联 商贷年限 161 | commerceLoanRate.forEach((rate: any) => { 162 | // 大于五年 163 | rate.limit = "outFive"; 164 | }); 165 | params.commerceOutFiveLoanRate = defaultData.commerceLoanRate; 166 | commerceLoanInFiveYearRate.forEach((rate: any) => { 167 | // 2-5年期 168 | rate.limit = "inFive"; 169 | }); 170 | commerceLoanInOneYearRate.forEach((rate: any) => { 171 | // <1年期 172 | rate.limit = "inOne"; 173 | }); 174 | options.commerceLoanRate = [ 175 | ...commerceLoanRate, 176 | ...commerceLoanInFiveYearRate, 177 | ...commerceLoanInOneYearRate 178 | ]; 179 | // 处理公积金利率 关联 公积金年限 180 | accumulatFundRate.forEach((rate: any) => { 181 | // >5年期 182 | rate.limit = "outFive"; 183 | }); 184 | params.accumulatOutFiveFundRate = defaultData.accumulatFundRate; 185 | accumulatFundInFiveYearRate.forEach((rate: any) => { 186 | // <=5年期 187 | rate.limit = "inFive"; 188 | }); 189 | options.accumulatFundRate = [ 190 | ...accumulatFundRate, 191 | ...accumulatFundInFiveYearRate 192 | ]; 193 | const commerceLoanRateNew = formatFloat( 194 | defaultData.loanLrp + params.commercialLoanBasePoint / 10000, 195 | 4 196 | ); 197 | this.setState( 198 | { 199 | ...OPTION, 200 | commerceLoanRateNew, 201 | params: { ...params, ...defaultData } 202 | }, 203 | () => { 204 | this.getRenderList(); 205 | params.houseTotal && this.submit(); 206 | } 207 | ); 208 | }; 209 | 210 | /** 211 | * picker 选择回调 212 | * @param data 当前选择配置项数据 213 | * @param selectObj 已选的数据项 214 | */ 215 | onChangePicker = ( 216 | data: any, 217 | selectObj: { value: number | string; label: string } 218 | ) => { 219 | const { key } = data; 220 | const { params } = this.state; 221 | // 处理首付比例切换 222 | if (key === "downPayRate") { 223 | const isInput = selectObj.value === -1; 224 | params[key] = isInput ? params[key] : selectObj.value; 225 | if (!isInput) { 226 | params.loanAmount = Math.ceil( 227 | this.handleAmount( 228 | params.houseTotal, 229 | selectObj.value as number 230 | ) as number 231 | ); 232 | params.commerceTotalPirce = Math.max( 233 | parseInt(params.loanAmount) - params.accumulatTotalPirce, 234 | 0 235 | ); 236 | params.accumulatTotalPirce = 237 | parseInt(params.loanAmount) - params.commerceTotalPirce; 238 | } 239 | this.setState( 240 | { 241 | params, 242 | btnOpacity: isInput ? 0 : 1, 243 | // -1 标识手动输入 244 | keyboardHeight: isInput ? 0 : -1 245 | }, 246 | this.getRenderList 247 | ); 248 | return; 249 | } 250 | 251 | // 切换公积金年限 修改默认值 252 | if (data.key === "accumulatFundYear") { 253 | params.accumulatFundRate = 254 | selectObj.value > 5 255 | ? params.accumulatOutFiveFundRate 256 | : params.accumulatFundInFiveYearRate; 257 | } 258 | // 切换商贷年限 修改默认值 259 | if (data.key === "commerceLoanYear") { 260 | params.commerceLoanRate = 261 | selectObj.value > 5 262 | ? params.commerceOutFiveLoanRate 263 | : selectObj.value > 1 264 | ? params.commerceLoanInFiveYearRate 265 | : params.commerceLoanInOneYearRate; 266 | } 267 | params[key] = selectObj.value; 268 | let loanLrpTypeObj = {}; 269 | if (data.key === "loanLrp") { 270 | loanLrpTypeObj = { 271 | loanLrpType: selectObj.label.indexOf("最新") > -1 ? 1 : 0 272 | }; 273 | } 274 | 275 | this.setState( 276 | { 277 | params, 278 | ...loanLrpTypeObj 279 | }, 280 | this.getRenderList 281 | ); 282 | }; 283 | 284 | /** 285 | * 处理首付展示 286 | */ 287 | handleDownPaySelectLabel = (data: number | string, range?: any[]) => { 288 | const { options } = this.state; 289 | const list = range || options.downPayRate; 290 | list.forEach((pay: any) => { 291 | pay.labelCopy = pay.labelCopy || pay.label; 292 | const amount = Math.floor( 293 | formatFloat(pay.value * parseInt(data as string, 10), 1) as number 294 | ); 295 | if (pay.value !== -1 && amount >= 0) { 296 | pay.label = `${pay.labelCopy} (${amount}万)`; 297 | } else { 298 | pay.label = pay.labelCopy; 299 | } 300 | }); 301 | this.setState({ 302 | options 303 | }); 304 | }; 305 | 306 | /** 307 | * input 值改变回调 308 | * @param data 配置项 309 | * @param value 输入的值 310 | * @param _index 当前配置项的索引 311 | */ 312 | onInputChange = (data: any, _value: number, _index: number) => { 313 | const { params } = this.state; 314 | const value = _value > 0 ? _value : 0; 315 | 316 | // 处理房屋总价输入时自动计算贷款金额 317 | if (data.key === "houseTotal") { 318 | this.handleDownPaySelectLabel(value); 319 | const { downPayRate } = params; 320 | params.loanAmount = Math.ceil( 321 | this.handleAmount(value, downPayRate) as number 322 | ); 323 | } 324 | 325 | params[data.key] = value; 326 | 327 | // 修改贷款金额或房屋总价(两种计算方式)更新商贷金额 328 | if (data.key === "loanAmount" || data.key === "houseTotal") { 329 | params.commerceTotalPirce = 330 | parseInt(params.loanAmount) - params.accumulatTotalPirce; 331 | params.commerceTotalPirce = 332 | params.commerceTotalPirce > 0 ? params.commerceTotalPirce : 0; 333 | } 334 | const baseParams: any = {}; 335 | // 处理新版商贷利率 基点修改 336 | if (data.key === "commercialLoanBasePoint") { 337 | baseParams.commerceLoanRateNew = formatFloat( 338 | params.loanLrp + params.commercialLoanBasePoint / 10000, 339 | 4 340 | ); 341 | } 342 | // fix: weapp中 当超出限制时,onInput取的值始终是上限值,但页面依然能够输入 343 | // 修改公积金金额时 更新商贷金额 344 | if (data.key === "accumulatTotalPirce") { 345 | params.accumulatTotalPirceMaxValue = -1; 346 | params.commerceTotalPirce = 347 | parseInt(params.loanAmount) - params.accumulatTotalPirce; 348 | if (params.commerceTotalPirce <= 0) { 349 | params.commerceTotalPirce = 0; 350 | params.accumulatTotalPirce = 351 | parseInt(params.loanAmount) - params.commerceTotalPirce; 352 | params.accumulatTotalPirceMaxValue = params.accumulatTotalPirce; 353 | } 354 | } 355 | const { accumulatLoanLimit } = params; 356 | // 修改商贷金额时 更新公积金金额 357 | if (data.key === "commerceTotalPirce") { 358 | params.commerceTotalPirce = 359 | params.commerceTotalPirce > params.loanAmount 360 | ? params.loanAmount 361 | : params.commerceTotalPirce; 362 | params.accumulatTotalPirce = 363 | parseInt(params.loanAmount) - params.commerceTotalPirce; 364 | } 365 | // 校验公积金金额是否大于上限 366 | if (params.accumulatTotalPirce > accumulatLoanLimit) { 367 | // 修改商贷时 只提示一次 368 | if (this.isFirstChange || data.key !== "commerceTotalPirce") { 369 | Taro.showToast({ 370 | title: `当前城市公积金最高可贷${accumulatLoanLimit}万`, 371 | icon: "none" 372 | }); 373 | params.commerceTotalPirce = 374 | parseInt(params.loanAmount) - accumulatLoanLimit; 375 | } 376 | this.isFirstChange = !(data.key === "commerceTotalPirce"); 377 | params.accumulatTotalPirce = accumulatLoanLimit; 378 | params.accumulatTotalPirceMaxValue = accumulatLoanLimit; 379 | } 380 | this.setState( 381 | { 382 | params, 383 | ...baseParams 384 | }, 385 | this.getRenderList 386 | ); 387 | }; 388 | 389 | /** 390 | * 处理切换计算方式时,贷款总额或房屋总价 391 | */ 392 | handleAmount = (value: string | number, ratio: number) => { 393 | return formatFloat(parseInt(value + "", 10) * (1 - ratio), 1); 394 | }; 395 | 396 | /** 397 | * 计算方式、贷款方式改变事件 398 | * @param data 399 | */ 400 | onWayClick = (data: any) => { 401 | const { key, index } = data; 402 | let obj = {}; 403 | // 处理切换成按房屋总价时 房屋总价根据 贷款总额 * (1 + 首付比例) 404 | if (data.key === "way" && index === 1) { 405 | const { params } = this.state; 406 | const { downPayRate, loanAmount } = params; 407 | params.houseTotal = Math.floor( 408 | formatFloat(loanAmount / (1 - downPayRate), 1) as number 409 | ); 410 | obj = { params }; 411 | this.handleDownPaySelectLabel(params.houseTotal); 412 | } 413 | if (data.key === "way" && index === 0) { 414 | const { params } = this.state; 415 | const { loanAmount } = params; 416 | params.loanAmount = Math.ceil(formatFloat(loanAmount, 1) as number); 417 | obj = { params }; 418 | } 419 | this.setState( 420 | { 421 | [key]: index, 422 | showResult: false, 423 | ...obj 424 | }, 425 | this.getRenderList 426 | ); 427 | }; 428 | 429 | /** 430 | * 页面跳转 431 | * @param path 跳转路径 432 | */ 433 | goPage = (path: string, data: object = {}) => () => { 434 | setGlobalData("COMPUTE_RESULT", data); 435 | Taro.navigateTo({ 436 | url: `/pages/calculator/${path}/index` 437 | }); 438 | }; 439 | 440 | /** 441 | * 监听键盘出现事件 442 | * @param frames 键盘对象 443 | */ 444 | onKeyboardDidShow = (frames: Record) => { 445 | const { endCoordinates } = frames; 446 | console.log(endCoordinates); 447 | this.setState({ 448 | btnOpacity: 0, 449 | keyboardHeight: endCoordinates.height 450 | }); 451 | }; 452 | 453 | /** 454 | * 监听键盘收起事件 455 | * @param frames 键盘对象 456 | */ 457 | onKeyboardDidHide = (_frames: Record) => { 458 | this.setState({ 459 | keyboardHeight: -1, 460 | btnOpacity: 1 461 | }); 462 | }; 463 | 464 | /** 465 | * 首付选择手动输入处理 466 | * @param e 467 | */ 468 | downPayRateHandle = (e: any) => { 469 | const { value } = e.detail; 470 | const valueNumbe = parseInt(value, 10); 471 | // 输入范围0-99 472 | this.setState({ 473 | downPayRateCustom: valueNumbe 474 | }); 475 | }; 476 | 477 | /** 478 | * 确定手动输入首付比例 479 | */ 480 | downPayRateConfirm = () => { 481 | const { options, params, downPayRateCustom } = this.state; 482 | const { downPayRate } = options; 483 | const value = parseInt(downPayRateCustom, 10); 484 | if (!(value > 0 && value <= 99)) { 485 | this.setState({ 486 | btnOpacity: 1, 487 | keyboardHeight: -1 488 | }); 489 | return; 490 | } 491 | const realValue = value / 100; 492 | const existIndex = downPayRate.findIndex( 493 | (item: any) => item.value === realValue 494 | ); 495 | if (realValue && existIndex < 0) { 496 | const maxIndex = downPayRate.length - 1; 497 | const insertIndex = downPayRate.findIndex( 498 | (item: any, index: number) => 499 | realValue > item.value && 500 | realValue < 501 | (index < maxIndex ? downPayRate[index + 1].value : Infinity) 502 | ); 503 | downPayRate.splice(insertIndex + 1, 0, { 504 | value: realValue, 505 | label: `${downPayRateCustom}%` 506 | }); 507 | } 508 | params.downPayRate = realValue; 509 | params.loanAmount = Math.ceil( 510 | this.handleAmount(params.houseTotal, params.downPayRate) as number 511 | ); 512 | params.commerceTotalPirce = 513 | parseInt(params.loanAmount) - params.accumulatTotalPirce; 514 | this.setState( 515 | { 516 | options, 517 | params, 518 | btnOpacity: 1, 519 | keyboardHeight: -1 520 | }, 521 | () => { 522 | this.handleDownPaySelectLabel(params.houseTotal); 523 | this.getRenderList(); 524 | } 525 | ); 526 | }; 527 | 528 | /** 529 | * 检验公积金金额 530 | */ 531 | checkAccumulatLoanAmount = () => { 532 | const { params } = this.state; 533 | const { accumulatLoanLimit, accumulatTotalPirce } = params; 534 | if (accumulatTotalPirce > accumulatLoanLimit) { 535 | Taro.showToast({ 536 | title: `当前城市公积金最高可贷${accumulatLoanLimit}万`, 537 | icon: "none" 538 | }); 539 | params.accumulatTotalPirce = accumulatLoanLimit; 540 | return; 541 | } 542 | const amount = parseInt(params.loanAmount) - params.commerceTotalPirce; 543 | params.accumulatTotalPirce = amount; 544 | if (amount > accumulatLoanLimit) { 545 | Taro.showToast({ 546 | title: `当前城市公积金最高可贷${accumulatLoanLimit}万`, 547 | icon: "none" 548 | }); 549 | params.accumulatTotalPirce = accumulatLoanLimit; 550 | params.commerceTotalPirce = 551 | parseInt(params.loanAmount) - params.accumulatTotalPirce; 552 | } 553 | }; 554 | 555 | checkParams = () => { 556 | const { params, loanType } = this.state; 557 | const { 558 | loanAmount, 559 | accumulatTotalPirce, 560 | commerceTotalPirce, 561 | accumulatLoanLimit 562 | } = params; 563 | if (loanAmount === 0) { 564 | Taro.showToast({ 565 | title: `贷款金额不能为0`, 566 | icon: "none" 567 | }); 568 | return false; 569 | } 570 | if ( 571 | loanType === 0 && 572 | loanAmount != commerceTotalPirce + accumulatTotalPirce 573 | ) { 574 | Taro.showToast({ 575 | title: `商贷金额和公积金贷款金额之和必须等于贷款总额`, 576 | icon: "none" 577 | }); 578 | return false; 579 | } 580 | if (loanType === 2 && loanAmount > accumulatLoanLimit) { 581 | Taro.showToast({ 582 | title: `当前城市公积金最高可贷${accumulatLoanLimit}万`, 583 | icon: "none" 584 | }); 585 | return false; 586 | } 587 | return true; 588 | }; 589 | 590 | getTip = (showMonthlyPay = true) => { 591 | const { 592 | params, 593 | loanType, 594 | loanLrpType, 595 | commerceLoanRateNew, 596 | userLoanWay 597 | } = this.state; 598 | const { 599 | commerceLoanYear, 600 | accumulatFundYear, 601 | accumulatFundRate, 602 | accumulatTotalPirce, 603 | commerceTotalPirce, 604 | commerceLoanRate, 605 | loanAmount, 606 | downPayRate 607 | } = params; 608 | const downPayRateStr = `首付${formatFloat(downPayRate * 100, 2)}%`; 609 | const accumulatStr = `公积金贷${ 610 | loanType === 2 ? loanAmount : accumulatTotalPirce 611 | }万·${accumulatFundYear}年·利率${formatFloat(accumulatFundRate * 100, 2)}%`; 612 | const commerceLoanRateStr = `${formatFloat( 613 | (loanLrpType === 1 ? commerceLoanRateNew : commerceLoanRate) * 100, 614 | 2 615 | )}`; 616 | const commerceStr = `商业贷${ 617 | loanType === 1 ? loanAmount : commerceTotalPirce 618 | }万·${commerceLoanYear || 0}年·利率${commerceLoanRateStr}%`; 619 | const loanStr = 620 | loanType === 0 621 | ? [accumulatStr, commerceStr] 622 | : loanType === 1 623 | ? [commerceStr] 624 | : [accumulatStr]; 625 | const loanWayStr = `${userLoanWay || "等额本息"}`; 626 | const tip = (showMonthlyPay 627 | ? [downPayRateStr, ...loanStr, loanWayStr] 628 | : [downPayRateStr, ...loanStr] 629 | ).join("、"); 630 | return tip; 631 | }; 632 | 633 | submit = async () => { 634 | if (this.loading) return; 635 | if (!this.checkParams()) return; 636 | Taro.showLoading({ 637 | title: "计算中..." 638 | }); 639 | 640 | const { 641 | params, 642 | loanType, 643 | loanLrpType, 644 | commerceLoanRateNew, 645 | userLoanWay 646 | } = this.state; 647 | const { 648 | commerceLoanYear, 649 | accumulatFundYear, 650 | accumulatFundRate, 651 | accumulatTotalPirce, 652 | loanAmount, 653 | commerceTotalPirce, 654 | commerceLoanRate, 655 | houseTotal 656 | } = params; 657 | const res: any = await equalInterestCalc({ 658 | totalPrice: houseTotal, 659 | commerceLoanYear, 660 | commerceLoanRate: 661 | loanLrpType === 1 ? commerceLoanRateNew : commerceLoanRate, 662 | accumulatFundYear, 663 | accumulatFundRate, 664 | accumulatTotalPirce: loanType === 2 ? loanAmount : accumulatTotalPirce, 665 | commerceTotalPirce: loanType === 1 ? loanAmount : commerceTotalPirce 666 | }); 667 | try { 668 | const tip = this.getTip(false); 669 | this.computeResult = { 670 | ...res, 671 | loanAmount, 672 | tip 673 | }; 674 | const backgroundColor = "#12B983"; 675 | this.setState({ 676 | tip, 677 | showResult: true, 678 | equalInterestPayMonth: res.equalInterest.payMonth, 679 | equalPrincipalPayMonth: res.equalPrincipal.payMonth, 680 | backgroundColor 681 | }); 682 | Taro.setNavigationBarColor({ 683 | frontColor: "#ffffff", 684 | backgroundColor 685 | }); 686 | IS_RN 687 | ? this.scroll.scrollTo({ x: 0, animation: true }) 688 | : Taro.pageScrollTo({ 689 | scrollTop: 0, 690 | duration: 300 691 | }); 692 | const list: any = (await getStorageData("LOAN_HISTORY")) || []; 693 | const historyList = [ 694 | { 695 | commerceLoanYear: commerceLoanYear, 696 | commerceTotalPirce: 697 | loanType === 1 698 | ? loanAmount 699 | : loanType === 2 700 | ? 0 701 | : commerceTotalPirce, 702 | accumulatFundYear, 703 | accumulatTotalPirce: 704 | loanType === 1 705 | ? 0 706 | : loanType === 2 707 | ? loanAmount 708 | : accumulatTotalPirce, 709 | payMonthStr: 710 | userLoanWay === "等额本息" 711 | ? "每月应还(等额本息)" 712 | : "首月应还(等额本金)", 713 | firstPay: 714 | userLoanWay === "等额本息" 715 | ? res.equalInterest.payMonth 716 | : res.equalPrincipal.payMonth 717 | }, 718 | ...list 719 | ]; 720 | await Taro.setStorage({ 721 | key: "LOAN_HISTORY", 722 | data: historyList.slice(0, 10) 723 | }); 724 | } catch (e) { 725 | console.log(e); 726 | } finally { 727 | setTimeout(() => { 728 | Taro.hideLoading(); 729 | }, 1000); 730 | this.loading = false; 731 | } 732 | }; 733 | 734 | render() { 735 | const { 736 | way, 737 | loanType, 738 | renderList, 739 | params, 740 | keyboardHeight, 741 | btnOpacity, 742 | userLoanWay, 743 | equalInterestPayMonth, 744 | equalPrincipalPayMonth, 745 | showResult, 746 | backgroundColor 747 | } = this.state; 748 | const { houseTotal, downPayRate } = params; 749 | return ( 750 | 751 | 752 | {keyboardHeight >= 0 && ( 753 | 754 | 755 | 756 | 762 | 请输入 763 | 770 | 786 | 787 | 788 | 792 | 确定 793 | 794 | 795 | 796 | )} 797 | 798 | { 800 | this.scroll = ref; 801 | }} 802 | style={{ flex: 1 }} 803 | > 804 | {showResult && ( 805 | 819 | )} 820 | 821 | 822 | 828 | 834 | 840 | 841 | 848 | 849 | 850 | 851 | 866 | 867 | 868 | 开始计算 869 | 870 | 871 | 872 | 873 | ); 874 | } 875 | } 876 | --------------------------------------------------------------------------------