This commit is contained in:
dute7liang
2023-12-19 22:23:45 +08:00
commit 97daeafbe7
117 changed files with 19926 additions and 0 deletions

28
src/App.vue Normal file
View File

@@ -0,0 +1,28 @@
<template>
<!-- <router-view v-slot="{ Component }">-->
<!-- <transition name="fade-transition" mode="out-in">-->
<!-- <keep-alive>-->
<!-- <component :is="Component"></component>-->
<!-- </keep-alive>-->
<!-- </transition>-->
<!-- </router-view>-->
<!-- v-if="showBar"-->
<div>
<!-- <van-sticky>-->
<!-- </van-sticky>-->
<router-view />
</div>
</template>
<script setup lang="ts">
</script>
<style>
@import "style/index.scss";
body {
//background-color: #f8f8f8;
-webkit-font-smoothing: antialiased;
}
</style>

192
src/api/index.ts Normal file
View File

@@ -0,0 +1,192 @@
import {http} from "@/utils/http/axios";
import {RequestEnum} from "@/enums/httpEnum";
const baseUrl = '/api'
/**
* @description: getLoansInfo
*/
export function getLoansInfo() {
return http.request({
url: `${baseUrl}/app/home/setting/loans`,
method: 'GET'
});
}
/**
* @description: getHomeInfo
*/
export function getHomeInfo() {
return http.request({
url: `${baseUrl}/app/home/setting/home`,
method: 'GET'
});
}
/**
* @description: getCalLoan
*/
export function getCalLoan(data) {
return http.request({
url: `${baseUrl}/app/home/loans/calLoan`,
method: 'POST',
data
});
}
/**
* @description: getLoansUser
*/
export function getLoansUser() {
return http.request({
url: `${baseUrl}/app/home/loans/loansUser`,
method: 'GET'
});
}
/**
* @description: getUserInfo
*/
export function getUserInfo() {
return http.request({
url: `${baseUrl}/app/customer/card/info`,
method: 'GET'
});
}
/**
* @description: uploadCommon
*/
export function uploadCommon(file, onUploadProgress?) {
return http.uploadFile({
url: `${baseUrl}/v2/common/upload`,
method: 'POST',
onUploadProgress: (progressEvent) => {
onUploadProgress && onUploadProgress(progressEvent)
}
}, {
file: file
});
}
/**
* updateCustomerCard
* @param data
*/
export function updateCustomerCard(data) {
return http.request({
url: `${baseUrl}/app/customer/updateCustomerCard`,
method: 'POST',
data
});
}
/**
* getBankType
*/
export function getBankType() {
return http.request({
url: `${baseUrl}/app/home/setting/bankType`,
method: RequestEnum.GET
});
}
/**
* getBankType
*/
export function getAgreement() {
return http.request({
url: `${baseUrl}/app/home/setting/agreement`,
method: RequestEnum.GET
});
}
/**
* getCustomerInfo
*/
export function getCustomerInfo() {
return http.request({
url: `${baseUrl}/app/customer/info`,
method: RequestEnum.GET
});
}
/**
* getCustomerInfo
*/
export function getStepBorrow() {
return http.request({
url: `${baseUrl}/app/borrow/getStepBorrow`,
method: RequestEnum.GET
});
}
/**
* getBorrowPage
*/
export function getBorrowPage(params) {
return http.request({
url: `${baseUrl}/app/borrow/page`,
method: RequestEnum.GET,
params
});
}
/**
* getBorrowInfo
*/
export function getBorrowInfo(params) {
return http.request({
url: `${baseUrl}/app/borrow/info`,
method: RequestEnum.GET,
params
});
}
/**
* getBorrowWithdraw
*/
export function getBorrowWithdraw(params) {
return http.request({
url: `${baseUrl}/app/borrow/withdraw`,
method: RequestEnum.GET,
params
});
}
/**
* getSetting
*/
export function getSetting() {
return http.request({
url: `${baseUrl}/app/home/setting/home`,
method: RequestEnum.GET
});
}
/**
* getSetting
*/
export function getContract(params?) {
return http.request({
url: `${baseUrl}/app/borrow/getContract`,
method: RequestEnum.GET,
params
});
}
/**
* startBorrow
*/
export function startBorrow(data) {
return http.request({
url: `${baseUrl}/app/borrow/start`,
method: RequestEnum.POST,
data
});
}

97
src/api/login/index.ts Normal file
View File

@@ -0,0 +1,97 @@
import { http } from '@/utils/http/axios';
const baseUrl = '/api'
/**
* @description: sendSmsRegister
*/
export function sendSmsRegister(params) {
return http.request({
url: `${baseUrl}/customer/open/sms/register`,
method: 'GET',
params
},
{
withToken: false,
});
}
/**
* @description: sendSmsForget
*/
export function sendSmsForget(params) {
return http.request({
url: `${baseUrl}/customer/open/sms/forget`,
method: 'GET',
params
},
{
withToken: false,
});
}
/**
* @description: login
*/
export function login(data) {
return http.request({
url: `${baseUrl}/customer/login`,
method: 'POST',
data
},
{
isTransformResponse: false,
withToken: false,
});
}
/**
* @description: register
*/
export function register(data) {
return http.request({
url: `${baseUrl}/customer/open/register`,
method: 'POST',
data
},
{
withToken: false,
});
}
/**
* @description: updatePwd
*/
export function updatePwd(data) {
return http.request({
url: `${baseUrl}/customer/open/updatePwd`,
method: 'POST',
data
},
{
withToken: false,
});
}
/**
* @description: getUserInfo
*/
export function getUserInfo() {
return http.request({
url: `${baseUrl}/app/customer/info`,
method: 'GET'
});
}
/**
* 注销账号
* @description: closeAmount
*/
export function closeAmount() {
return http.request({
url: `${baseUrl}/app/user/close/amount`,
method: 'GET'
});
}

0
src/api/system/user.ts Normal file
View File

260
src/array.prototype.d.ts vendored Normal file
View File

@@ -0,0 +1,260 @@
interface Array {
/**
* 清空数组
*/
clear(): void;
/**
* 清空数组并添加
* @param items
*/
newPush(...items: T[]): void;
/**
* 数组根据对象删除指定元素
* @param val
*/
remove(val: T | T[]): void;
/**
* 数组根据下标删除指定元素
* @param index
* @param {number} index
*/
removeByIndex(index: number): void;
/**
* 数组指定位置添加元素
* @param index
* @param item
* @param {number} index
* @param {*} item
*/
insert(index: number, ...item: T): void;
/**
* 数组指定位置替换元素
* @param index
* @param item
* @param {number} index
* @param {*} item
*/
replace(index: number, ...item: T): void;
/**
* 数组排序(根据某一个字段排序或者直接排序)
* @param { key, desc } options
*/
shellSort(options?: {key: string, desc: boolean}): void;
/**
* 数组去重 (key为元素中的某一个属性默认对比整个对象)
* @param key
* @returns {*|*[]}
*/
deDuplication(key?: string): T[];
/**
* 判断元素中是否包含此元素 (key为元素中的某一个属性默认对比整个对象)
* @param val
* @param key1
* @param key2
* @returns {boolean}
* @param {any} val
* @param {string} key1
* @param {string | null} key2
*/
contain(val: any, key1?: string, key2?: string): boolean;
}
/**
* 清空数组
*/
Array.prototype.clear = function() {
this.length = 0
}
/**
* 清空数组并添加
* @param items
*/
Array.prototype.newPush = function(...items: T[]) {
this.clear()
// 判断是否传入了多个参数
if (items && Array.isArray(items)) {
// 循环多个参数
for (let i = 0; i < items.length; i++) {
// 判断每个参数是否为数组
if (items[i] && Array.isArray(items[i])) {
// 循环是数组的元素并添加
for (let j = 0; j < items[i].length; j++) {
this.push(items[i][j])
}
} else {
this.push(items[i])
}
}
} else {
this.push(items)
}
}
/**
* 数组根据对象删除指定元素
* @param val
*/
Array.prototype.remove = function (val: T | T[]) {
const index = this.indexOf(val)
if (index > -1) {
this.removeByIndex(index)
}
}
/**
* 数组根据下标删除指定元素
* @param index
* @param {number} index
*/
Array.prototype.removeByIndex = function (index: number) {
this.splice(index, 1)
}
/**
* 数组指定位置添加元素
* @param index
* @param item
* @param {number} index
* @param {*} item
*/
Array.prototype.insert = function (index: number, item: T) {
const len = this.length
if (len < index) {
for (let i = 0; i < (index - len); i++) {
this.push(null)
}
} else if (index < 0) {
for (let i = 0; i > index + len; i--) {
this.splice(i, 0, null)
}
}
this.splice(index, 0, item)
}
/**
* 数组指定位置替换元素
* @param index
* @param item
*/
Array.prototype.replace = function (index: number, item: T) {
if (index < 0) {
return
}
const len = this.length
if (len < index) {
this.insert(index, item)
} else {
this.splice(index, 1, item)
}
}
/**
* 数组排序(根据某一个字段排序或者直接排序)
* @param { key, desc } options
*/
Array.prototype.shellSort = function (options?: {key: string, desc: boolean} | null = {}) {
const {key, desc} = options
const N = this.length
let h = 1
while (h < N / 3) {
h = 3 * h + 1
}
while (h >= 1) {
for (let i = h; i < N; i++) {
if (key) {
for (let j = i; j >= h && (desc ? this[j][key] > this[j - h][key] : this[j][key] < this[j - h][key]); j -= h) {
const temp = this[j]
this[j] = this[j - h]
this[j - h] = temp
}
} else {
for (let j = i; j >= h && (desc ? this[j] > this[j - h] : this[j] < this[j - h]); j -= h) {
const temp = this[j]
this[j] = this[j - h]
this[j - h] = temp
}
}
}
h = (h - 1) / 3
}
}
/**
* 数组去重 (key为元素中的某一个属性默认对比整个对象)
* @param key
* @returns {*|*[]}
*/
Array.prototype.deDuplication = function (key?: string) {
return this.reduce((pre: Array, cur) => {
return pre.contain(cur, key) ? pre : pre.concat(cur)
}, [])
}
/**
* 判断元素中是否包含此元素 (key为元素中的某一个属性默认对比整个对象)
* @param val
* @param key1
* @param key2
* @returns {boolean}
* @param {any} val
* @param {string} key1
* @param {string} key2
*/
Array.prototype.contain = function (val: any, key1?: string, key2?: string): boolean {
if (key1) {
if (key2) {
for (let i = 0; i < this.length; i++) {
if (typeof val === 'object') {
if (this[i][key1][key2] === val[key1][key2]) {
return true
}
} else {
if (this[i][key1][key2] === val) {
return true
}
}
}
} else {
for (let i = 0; i < this.length; i++) {
if (typeof val === 'object') {
if (this[i][key1] === val[key1]) {
return true
}
} else {
// console.log(this[i][key1], '===', val)
if (this[i][key1] === val) {
return true
}
}
}
}
} else {
return this.includes(val)
}
return false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,22 @@
<template>
<div :style="{height: px2vw(<number>baseProps.height), background: baseProps.background, opacity: baseProps.opacity ? 0 : 'none'}">
</div>
</template>
<script lang="ts" setup>
import {px2vw} from "@/utils";
const baseProps = defineProps({
height: Number,
opacity: Boolean,
background: {
type: String,
default: () => '#ffffff00'
}
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div :style="{...props.style, ...(placement === 'bottom' ? { display: 'block' } : {})}" class="j-label">
<div
class="label"
:class="{'j-class-label' : props.labelBorder, 'required': props.required}"
v-if="label"
:style="{...labelStyleComputed, minWidth: (isNumber(props.labelWidth) ? props.labelWidth + 'px' : props.labelWidth)}"
>
{{ props.label }}
</div>
<div :class="props.labelBorder ? '' : 'content'">
<slot name="default"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { isNumber } from "@/utils/is";
import { basicProps } from './props';
import { addStyle } from "@/utils";
import {computed} from "vue";
// todo
const props = defineProps({ ...basicProps });
const labelStyleComputed = computed(() => {
return addStyle(props.labelStyle)
})
</script>
<style scoped lang="scss">
.j-label {
display: flex;
align-items: center;
.label {
padding-right: 8px;
}
.content {
flex: 1;
}
.required {
&:after {
content: '*';
color: #F35534;
}
}
}
.j-class-label {
font-weight: bolder;
padding-left: 6px;
border-left: 3px solid #008ded;
}
</style>

View File

@@ -0,0 +1 @@
export { default as JLabel } from './JLabel.vue';

View File

@@ -0,0 +1,39 @@
import { PropType } from "vue";
export const basicProps = {
// 标签宽度
labelWidth: {
type: [Number, String] as PropType<number | string>,
default: '70px',
},
// 标签样式
labelStyle: {
type: [Object, String] as PropType<Object | string>,
default: '',
},
// 样式
style: {
type: [Object] as PropType<Object>,
default: {}
},
// 标签
label: {
type: String as PropType<string>,
default: ''
},
// 标签
placement: {
type: String as PropType<'left' | 'bottom'>,
default: 'left'
},
// 标签
labelBorder: {
type: Boolean as PropType<boolean>,
default: false
},
// 标签
required: {
type: Boolean as PropType<boolean>,
default: false
}
}

View File

@@ -0,0 +1,110 @@
<template>
<div>
<van-pull-refresh v-model="loadingUp" @refresh="onRefresh">
<van-list
v-model:loading="loadingDown"
:finished="finished"
finished-text=""
@load="onLoad"
>
<div style="min-height: 78vh">
123
<van-empty v-if="orderList?.length <= 0" description="没有暂时还没有内容~"/>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script lang="ts" setup>
import {ref} from "vue";
import {onMounted, reactive} from "vue";
const baseProps = defineProps({
orderList: {
type: Array,
default: []
},
serviceStatus: {
type: String,
default: ''
}
})
const emit = defineEmits([
'cancelOrder',
'removeOrder',
'refundOrder',
'commitOrder',
'goInfo',
])
const orderList: any[] = reactive([])
const loadingUp = ref(false);
const loadingDown = ref(false);
const finished = ref(false);
const page = reactive({
pageNum: 1,
pageSize: 5,
total: 20,
})
const onRefresh = () => {
page.pageNum = 1
finished.value = true
orderList.clear()
_getUserOrderPage()
};
const onLoad = () => {
if (finished.value) {
return
}
page.pageNum = page.pageNum + 1
_getUserOrderPage()
};
defineExpose({
onRefresh
})
const _getUserOrderPage = () => {
}
const cancelOrderBtn = (order) => {
emit('cancelOrder', order)
}
const removeOrderBtn = (order) => {
emit('removeOrder', order)
}
const refundOrderBtn = (order) => {
emit('refundOrder', order)
}
const commitOrderBtn = (order) => {
emit('commitOrder', order)
}
const goInfoBtn = (order) => {
emit('goInfo', order)
}
onMounted(() => {
_getUserOrderPage()
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,62 @@
<template>
<van-nav-bar
:placeholder="baseProps.placeholder"
fixed
safe-area-inset-top
:title="title"
:style="{
'--van-nav-bar-background': baseProps.navBarBackground,
'--van-nav-bar-icon-color': baseProps.color,
'--van-nav-bar-text-color': baseProps.color,
'--van-nav-bar-title-text-color': baseProps.color,
'--van-border-width': '0',
}"
:left-arrow="showBar"
@click-left="onClickLeft"
/>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
import {useRoute, useRouter} from "vue-router";
const router = useRouter()
const route = useRoute()
const baseProps = defineProps({
navBarBackground: {
type: String,
default: '#151515'
},
color: {
type: String,
default: '#fff'
},
placeholder: {
type: Boolean as PropType<boolean>,
default: true
},
title: {
type: String,
}
})
const onClickLeft = () => {
router.back()
}
const title = computed(() => {
return baseProps.title || route.meta.title
})
const showBar = computed(() => {
return route.meta.showBar
})
</script>
<style lang="scss" scoped>
</style>

41
src/date.prototype.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
interface Date {
format(format: string): string
}
/**
* 时间格式化
* @param format
*/
Date.prototype.format = function (format: DateType | undefined = 'datetime'): string {
if (format === 'date') {
format = 'yyyy-MM-dd'
} else if (format === 'datetime') {
format = 'yyyy-MM-dd hh:mm:ss'
} else if (format === 'time') {
format = 'hh:mm:ss'
}
const o = {
"M+": this.getMonth() + 1, //month
"d+": this.getDate(), //day
"h+": this.getHours(), //hour
"m+": this.getMinutes(), //minute
"s+": this.getSeconds(), //second
"q+": Math.floor((this.getMonth() + 3) / 3), //quarter
"S": this.getMilliseconds() //millisecond
}
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (const k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
}
// String.prototype.formatDate = function (format) {
// const time = this.replaceAll(/-/g, "/");
// return new Date(time).format(format);
// }

34
src/enums/httpEnum.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* @description: 请求结果集
*/
export enum ResultEnum {
SUCCESS = 200,
ERROR = 400,
TIMEOUT = 10042,
TYPE = 'success',
}
/**
* @description: 请求方法
*/
export enum RequestEnum {
GET = 'GET',
POST = 'POST',
PATCH = 'PATCH',
PUT = 'PUT',
DELETE = 'DELETE',
}
/**
* @description: 常用的contentTyp类型
*/
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// json
TEXT = 'text/plain;charset=UTF-8',
// form-data 一般配合qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data 上传
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}

52
src/enums/orderEnum.ts Normal file
View File

@@ -0,0 +1,52 @@
export const OrderEnum = {
NO_PAY: {
value: 0,
name: '待支付'
},
NO_ORDER: {
value: 1,
name: '待接单'
},
ALREADY_ORDER: {
value: 2,
name: '待消费'
},
ALREADY_EVALUATION: {
value: 90,
name: '待评价'
},
COMMIT: {
value: 99,
name: '已完成'
},
CANCEL: {
value: -1,
name: '已取消'
},
REFUND: {
value: -2,
name: '已退款'
}
}
export const OrderEnumMap = {
0: '待支付',
1: '待接单',
2: '待消费',
90: '待评价',
99: '已完成',
'-1': '已取消',
'-2': '已退款',
}
export const OrderIconMap = {
0: 'order/pay.png',
1: 'order/djd.png',
2: 'order/daixiaofei.png',
90: 'order/dpj.png',
99: 'order/ywc.png',
'-1': 'order/yqx.png',
'-2': 'order/ytk.png',
}

View File

@@ -0,0 +1,26 @@
import {onMounted, onUnmounted} from "vue";
export const useBackgroundHook = () => {
let bodyList = document.getElementsByTagName('body');
let body = bodyList[0]
let bodyBackground = body.style.background
const setBodyBackground = (option?: {mounted?: Function | undefined, unmounted?: Function | undefined}) => {
let {mounted, unmounted} = option || {}
onMounted(() => {
body.style.background = 'linear-gradient(167.96deg, #E6FAE1 0%, #F2E7B7 98.44%) no-repeat'
mounted && mounted()
})
onUnmounted(() => {
body.style.background = bodyBackground
unmounted && unmounted()
})
}
return { setBodyBackground }
}

37
src/hooks/useMittHook.ts Normal file
View File

@@ -0,0 +1,37 @@
import mitt from 'mitt'
const emitter = mitt()
export const useMittHook = () => {
const emit = (eventName, params) => {
emitter.emit(eventName, params)
emitter.emit(`${eventName}Once`, eventName)
}
const on = (eventName, fun) => {
emitter.on(eventName, fun)
}
const once = (eventName, fun) => {
emitter.on(eventName, fun)
emitter.on(`${eventName}Once`, (en) => {
emitter.off(en)
})
}
const off = (eventName, params?) => {
emitter.off(eventName, params)
}
const clearAll = () => {
emitter.all.clear()
}
return {
emit,
on,
once,
off,
clearAll
}
}

20
src/main.ts Normal file
View File

@@ -0,0 +1,20 @@
import './array.prototype.d.ts';
import './date.prototype.d.ts';
import './string.prototype.d.ts';
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router";
import {setupStore} from "@/store";
import {setupCustomComponents} from "@/plugins/customComponents";
// 2. 引入组件样式
import 'vant/lib/index.css';
import 'vant/es/toast/style'
const app = createApp(App)
app.use(router)
app.mount('#app')
setupStore(app)
setupCustomComponents(app)

View File

@@ -0,0 +1,14 @@
import { App } from "vue";
import { JLabel } from "@/components/JLabel";
import JGap from "@/components/JGap/JGap.vue";
import JNavBar from "@/components/JNavBar/JNavBar.vue";
/**
* 全局注册自定义组件 待完善
* @param app
*/
export function setupCustomComponents(app: App<Element>) {
app.component('JGap',JGap)
app.component('JLabel',JLabel)
app.component('JNavBar',JNavBar)
}

253
src/router/index.ts Normal file
View File

@@ -0,0 +1,253 @@
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router';
import {useUserStore} from "@/store/modules/user";
export const constantRouter: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Root',
redirect: '/home',
meta: {
title: 'Root',
},
},
{
path: '/',
name: 'index',
component: () => import('@/views/index/index.vue'),
meta: {
title: '首页',
},
children: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/index/home/index.vue'),
meta: {
title: '首页',
showBar: false
},
},
{
path: '/serveList',
name: 'serveList',
component: () => import('@/views/index/serveList/index.vue'),
meta: {
isPermissions: true,
title: '钱包',
showBar: false
},
},
{
path: '/message',
name: 'message',
component: () => import('@/views/index/message/index.vue'),
meta: {
isPermissions: true,
title: '聊天',
showBar: false
},
},
{
path: '/my',
name: 'my',
component: () => import('@/views/index/my/index.vue'),
meta: {
isPermissions: true,
title: '我的',
showBar: false
},
}
]
},
{
path: '/userInfo',
name: 'userInfo',
component: () => import('@/views/my/userInfo/index.vue'),
meta: {
title: '我的资料',
isPermissions: true,
showBar: true
},
},
{
path: '/userInfo1',
name: 'userInfo1',
component: () => import('@/views/my/userInfo1/index.vue'),
meta: {
title: '基本信息',
isPermissions: true,
showBar: true
},
},
{
path: '/signature',
name: 'signature',
component: () => import('@/views/my/signature/index.vue'),
meta: {
title: '签名',
isPermissions: true,
showBar: true
},
},
{
path: '/contract',
name: 'contract',
component: () => import('@/views/my/contract/index.vue'),
meta: {
title: '合同',
isPermissions: true,
showBar: true
},
},
{
path: '/userInfo2',
name: 'userInfo2',
component: () => import('@/views/my/userInfo2/index.vue'),
meta: {
title: '提交资料',
isPermissions: true,
showBar: true
},
},
{
path: '/userInfo3',
name: 'userInfo3',
component: () => import('@/views/my/userInfo3/index.vue'),
meta: {
title: '收款银行卡',
isPermissions: true,
showBar: true
},
},
{
path: '/loansInfo',
name: 'loansInfo',
component: () => import('@/views/loans/info/index.vue'),
meta: {
title: '借款详情',
isPermissions: true,
showBar: true
},
},
{
path: '/loansInfo1',
name: 'loansInfo1',
component: () => import('@/views/loans/info1/index.vue'),
meta: {
title: '提现',
isPermissions: true,
showBar: true
},
},
{
path: '/myLoan',
name: 'myLoan',
component: () => import('@/views/my/myLoan/index.vue'),
meta: {
title: '我的借款',
isPermissions: true,
showBar: true
},
},
{
path: '/myRepayment',
name: 'myRepayment',
component: () => import('@/views/my/myRepayment/index.vue'),
meta: {
title: '我的还款',
isPermissions: true,
showBar: true
},
},
{
path: '/borrowInfo',
name: 'borrowInfo',
component: () => import('@/views/borrowInfo/index.vue'),
meta: {
title: '贷款详情',
isPermissions: true,
showBar: true
},
},
{
path: '/uploadPassword',
name: 'uploadPassword',
component: () => import('@/views/uploadPassword/index.vue'),
meta: {
title: '修改密码',
isPermissions: true,
showBar: true
},
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
showBar: false
},
},
{
path: '/register',
name: 'register',
component: () => import('@/views/register/index.vue'),
meta: {
title: '注册',
showBar: true
},
},
{
path: '/forget',
name: 'forget',
component: () => import('@/views/forget/index.vue'),
meta: {
title: '忘记密码',
showBar: true
},
},
{
path: '/agreement',
name: 'agreement',
component: () => import('@/views/agreement/agreement.vue'),
meta: {
title: '协议',
showBar: true
},
}
];
//需要验证权限
//普通路由 无需验证权限
const router = createRouter({
history: createWebHashHistory(''),
routes: constantRouter,
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
});
// @ts-ignore
router.beforeEach((to, from, next) => {
if (to.meta.title) { // 判断是否有标题
document.title = to.meta.title as string;
}
if (to.meta.isPermissions) {
const userStore = useUserStore()
if (!userStore.getToken) {
next({
path: '/login'
})
return
}
}
next()
})
export default router;

10
src/store/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { App } from 'vue';
import { createPinia } from 'pinia';
const store = createPinia();
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };

View File

@@ -0,0 +1,14 @@
const allModules: any = import.meta.glob('./*/index.ts', { eager: true });
const modules = {} as any;
Object.keys(allModules).forEach((path) => {
const fileName = path.split('/')[1];
modules[fileName] = allModules[path][fileName] || allModules[path].default || allModules[path];
});
// export default modules
// @ts-ignore
import user from './user';
export default {
user,
};

137
src/store/modules/user.ts Normal file
View File

@@ -0,0 +1,137 @@
import { defineStore } from 'pinia';
import { store } from '@/store';
import { storage } from '@/utils/Storage';
import {ACCESS_TOKEN, CURRENT_USER, LOCATION_MAP, OPEN_ID, WX_CONFIG} from "@/store/mutation-types";
import {getUserInfo, login} from "@/api/login";
import {ResultEnum} from "@/enums/httpEnum";
export interface IUserState {
showCoupon: boolean;
token: string;
openId: string;
username: string;
welcome: string;
avatar: string;
permissions: any[];
info: any;
wxConfig: any;
locationMap: any;
}
export const useUserStore = defineStore({
id: 'app-user',
state: (): IUserState => ({
showCoupon: true,
token: storage.get(ACCESS_TOKEN, ''),
openId: storage.get(OPEN_ID, ''),
// openId: storage.get(OPEN_ID, 'omWdJ62bH_6HXLQVOIefzN9J1oi4'),
username: '',
welcome: '',
avatar: '',
permissions: [],
info: storage.get(CURRENT_USER, {}),
wxConfig: storage.get(WX_CONFIG, null),
locationMap: storage.get(LOCATION_MAP, {}),
}),
getters: {
getShowCoupon(): boolean {
return this.showCoupon;
},
getToken(): string {
return this.token;
},
getOpenId(): string {
return this.openId;
},
getAvatar(): string {
return this.avatar;
},
getNickname(): string {
return this.username;
},
getPermissions(): [any][] {
return this.permissions;
},
getUserInfo(): object {
return this.info;
},
getWxConfig(): object {
return this.wxConfig;
},
getLocationMap(): object {
return this.locationMap;
},
},
actions: {
setShowCoupon(showCoupon: boolean) {
this.showCoupon = showCoupon;
},
setToken(token: string) {
const ex = 1 * 4 * 60 * 60;
storage.set(ACCESS_TOKEN, token, ex);
this.token = token;
},
setOpenId(openId: string) {
const ex = 1000 * 24 * 60 * 60;
storage.set(OPEN_ID, openId, ex);
this.openId = openId;
},
setAvatar(avatar: string) {
this.avatar = avatar;
},
setPermissions(permissions) {
this.permissions = permissions;
},
setUserInfo(info) {
const ex = 1 * 4 * 60 * 60;
storage.set(CURRENT_USER, info, ex);
this.info = info;
},
setWxConfig(wxConfig) {
const ex = 7000;
storage.set(WX_CONFIG, wxConfig, ex);
this.wxConfig = wxConfig;
},
setLocationMap(locationMap) {
const ex = 7000;
storage.set(LOCATION_MAP, locationMap, ex);
this.locationMap = locationMap;
},
// 登录
async login(loginData) {
try {
const {data, code, msg} = await login(loginData)
if (code === ResultEnum.SUCCESS) {
this.setToken(data.token);
const userInfo = await getUserInfo()
this.setUserInfo(userInfo)
return Promise.resolve(data.token);
} else {
return Promise.reject(msg);
}
} catch (e) {
return Promise.reject(e);
}
},
// 登出
async logout() {
this.setToken('');
this.setUserInfo(null);
this.setWxConfig(null);
// this.setLocationMap(null);
this.setUserInfo(null);
this.setOpenId('');
storage.remove(ACCESS_TOKEN);
storage.remove(CURRENT_USER);
storage.remove(WX_CONFIG);
// storage.remove(LOCATION_MAP);
storage.remove(OPEN_ID);
return Promise.resolve('');
},
},
});
// Need to be used outside the setup
export function useUserStoreWidthOut() {
return useUserStore(store);
}

View File

@@ -0,0 +1,5 @@
export const ACCESS_TOKEN = 'ACCESS-TOKEN'; // 用户token
export const OPEN_ID = 'OPEN-ID'; // 用户token
export const CURRENT_USER = 'CURRENT-USER'; // 当前用户信息
export const WX_CONFIG = 'WX-CONFIG'; // 当前用户信息
export const LOCATION_MAP = 'LOCATION-MAP'; // 当前用户信息

96
src/string.prototype.d.ts vendored Normal file
View File

@@ -0,0 +1,96 @@
interface String {
clearSpaces(pos?: string | null): string;
clearBr(): string;
clearSpacesAndBr(): string;
clearLeftSpaces(): string;
clearRightSpaces(): string;
clearBothSidesSpaces(): string;
clearInsideSpaces(): string;
formatDate(format: string): string;
replaceAll(searchValue: string | RegExp, replaceValue: string): string;
}
/**
* 清除字符串所有空格
* @param pos both(左右)|left|right|all|inner 默认all
* @constructor
*/
String.prototype.clearSpaces = function (pos?: string | null = 'all') {
if (pos === 'all') {
return this.replace(/\s+/g, '')
}
if (pos === 'both') {
return this.replace(/^\s+|\s+$/g, '')
}
if (pos === 'left') {
return this.replace(/^\s*/, '')
}
if (pos === 'right') {
return this.replace(/(\s*$)/g, '')
}
if (pos === 'inner') {
return this.replace(/\s/g, '')
}
return this
}
/**
* 清除字符串所有换行
* @return {string}
*/
String.prototype.clearBr = function () {
const str = this.replace(/<\/?.+?>/g, '')
str.replace(/[\r\n]/g, '')
return str
}
/**
* 清除字符串所有空格和回车
* @return {string}
*/
String.prototype.clearSpacesAndBr = function () {
let str = this.clearBr()
str = str.clearSpaces()
return str
}
// /**
// * 清除字符串左边空格
// * @return {string}
// */
// String.prototype.clearLeftSpaces = function () {
// return this.replace(/^\s*/g, '')
// }
// /**
// * 清除字符串右边空格
// * @return {string}
// */
// String.prototype.clearRightSpaces = function () {
// return this.replace(/\s*$/g, '')
// }
// /**
// * 清除字符串两边空格
// * @return {string}
// */
// String.prototype.clearBothSidesSpaces = function () {
// return this.replace(/(^\s*)|(\s*$)/g, '')
// }
/**
* 清除字符串中间空格
* @return {string}
*/
String.prototype.clearInsideSpaces = function () {
return this.replace(/\s/g, '')
}
String.prototype.formatDate = function (format) {
const time = this.replaceAll(/-/g, "/");
return new Date(time).format(format);
}
String.prototype.replaceAll = function (searchValue: string | RegExp, replaceValue: string): string{
const target = this;
return target.replace(new RegExp(searchValue, 'g'), replaceValue);
}

112
src/style/index.scss Normal file
View File

@@ -0,0 +1,112 @@
#app,
body,
html {
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei,
'\5FAE\8F6F\96C5\9ED1', Arial, sans-serif;
line-height: 1.5;
//background-color: #f7f7f7;
background-color: #f6f9fd;
//background: #f6f9fd
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
//重置样式
.anticon {
svg {
vertical-align: initial;
}
}
a {
color: #2d8cf0;
background: transparent;
text-decoration: none;
outline: none;
cursor: pointer;
transition: color 0.2s ease;
}
a:active,
a:hover {
outline-width: 0;
}
a:hover {
color: #57a3f3;
}
a:active {
color: #2b85e4;
}
a:active,
a:hover {
outline: 0;
text-decoration: none;
}
/* 滚动条凹槽的颜色,还可以设置边框属性 */
*::-webkit-scrollbar-track-piece {
background-color: #f8f8f8;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
/* 滚动条的宽度 */
*::-webkit-scrollbar {
width: 9px;
height: 9px;
}
/* 滚动条的设置 */
*::-webkit-scrollbar-thumb {
background-color: #ddd;
background-clip: padding-box;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
/* 滚动条鼠标移上去 */
*::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}
/* router view transition */
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: transform 0.35s, opacity 0.28s ease-in-out;
}
.zoom-fade-enter-from {
opacity: 0;
transform: scale(0.97);
}
.zoom-fade-leave-to {
opacity: 0;
transform: scale(1.03);
}
.j-ellipsis {
word-break:keep-all;/* 不换行 */
white-space:nowrap;/* 不换行 */
overflow:hidden;/* 内容超出宽度时隐藏超出部分的内容 */
text-overflow:ellipsis;/* 当对象内文本溢出时显示省略标记(...) 需与overflow:hidden;一起使用。*/
}
.yellow_color {
color: #BC7C1C;
}
.yellow_color1 {
color: #F9BF3A;
}
.gray_color {
color: #858B9C;
}

101
src/type/global.d.ts vendored Normal file
View File

@@ -0,0 +1,101 @@
import type {
ComponentRenderProxy,
VNode,
VNodeChild,
ComponentPublicInstance,
FunctionalComponent,
PropType as VuePropType,
} from 'vue';
declare global {
const __APP_INFO__: {
pkg: {
name: string;
version: string;
dependencies: Recordable<string>;
devDependencies: Recordable<string>;
};
lastBuildTime: string;
};
// declare interface Window {
// // Global vue app instance
// __APP__: App<Element>;
// }
// vue
declare type PropType<T> = VuePropType<T>;
declare type VueNode = VNodeChild | JSX.Element;
export type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
declare type Nullable<T> = T | null;
declare type NonNullable<T> = T extends null | undefined ? never : T;
declare type Recordable<T = any> = Record<string, T>;
declare type ReadonlyRecordable<T = any> = {
readonly [key: string]: T;
};
declare type Indexable<T = any> = {
[key: string]: T;
};
declare type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
declare type TimeoutHandle = ReturnType<typeof setTimeout>;
declare type IntervalHandle = ReturnType<typeof setInterval>;
declare interface ChangeEvent extends Event {
target: HTMLInputElement;
}
declare interface WheelEvent {
path?: EventTarget[];
}
interface ImportMetaEnv extends ViteEnv {
__: unknown;
}
declare interface ViteEnv {
VITE_PORT: number;
VITE_USE_MOCK: boolean;
VITE_PUBLIC_PATH: string;
VITE_GLOB_APP_TITLE: string;
VITE_GLOB_APP_SHORT_NAME: string;
VITE_DROP_CONSOLE: boolean;
VITE_GLOB_IMG_URL: string;
VITE_PROXY: [string, string][];
VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
}
declare function parseInt(s: string | number, radix?: number): number;
declare function parseFloat(string: string | number): number;
namespace JSX {
// tslint:disable no-empty-interface
type Element = VNode;
// tslint:disable no-empty-interface
type ElementClass = ComponentRenderProxy;
interface ElementAttributesProperty {
$props: any;
}
interface IntrinsicElements {
[elem: string]: any;
}
interface IntrinsicAttributes {
[elem: string]: any;
}
}
}
declare module 'vue' {
export type JSXComponent<Props = any> =
| { new (): ComponentPublicInstance<Props> }
| FunctionalComponent<Props>;
}

28
src/type/index.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
declare interface Fn<T = any, R = T> {
(...arg: T[]): R;
}
declare interface PromiseFn<T = any, R = T> {
(...arg: T[]): Promise<R>;
}
declare type RefType<T> = T | null;
declare type LabelValueOptions = {
label: string;
value: any;
disabled: boolean;
[key: string]: string | number | boolean;
}[];
declare type EmitType = (event: string, ...args: any[]) => void;
declare type TargetContext = '_self' | '_blank';
declare interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
$el: T;
}
declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null;
declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>;

127
src/utils/Storage.ts Normal file
View File

@@ -0,0 +1,127 @@
// 默认缓存期限为7天
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
/**
* 创建本地缓存对象
* @param {string=} prefixKey -
* @param {Object} [storage=localStorage] - sessionStorage | localStorage
*/
export const createStorage = ({ prefixKey = '', storage = localStorage } = {}) => {
/**
* 本地缓存类
* @class Storage
*/
const Storage = class {
private storage = storage;
private prefixKey?: string = prefixKey;
private getKey(key: string) {
return `${this.prefixKey}${key}`.toUpperCase();
}
/**
* @description 设置缓存
* @param {string} key 缓存键
* @param {*} value 缓存值
* @param expire
*/
set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
const stringData = JSON.stringify({
value,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
});
this.storage.setItem(this.getKey(key), stringData);
}
/**
* 读取缓存
* @param {string} key 缓存键
* @param {*=} def 默认值
*/
get(key: string, def: any = null) {
const item = this.storage.getItem(this.getKey(key));
if (item) {
try {
const data = JSON.parse(item);
const { value, expire } = data;
// 在有效期内直接返回
if (expire === null || expire >= Date.now()) {
return value;
}
this.remove(key);
} catch (e) {
return def;
}
}
return def;
}
/**
* 从缓存删除某项
* @param {string} key
*/
remove(key: string) {
this.storage.removeItem(this.getKey(key));
}
/**
* 清空所有缓存
* @memberOf Cache
*/
clear(): void {
this.storage.clear();
}
/**
* 设置cookie
* @param {string} name cookie 名称
* @param {*} value cookie 值
* @param {number=} expire 过期时间
* 如果过期时间未设置,默认关闭浏览器自动删除
* @example
*/
setCookie(name: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
document.cookie = `${this.getKey(name)}=${value}; Max-Age=${expire}`;
}
/**
* 根据名字获取cookie值
* @param name
*/
getCookie(name: string): string {
const cookieArr = document.cookie.split('; ');
for (let i = 0, length = cookieArr.length; i < length; i++) {
const kv = cookieArr[i].split('=');
if (kv[0] === this.getKey(name)) {
return kv[1];
}
}
return '';
}
/**
* 根据名字删除指定的cookie
* @param {string} key
*/
removeCookie(key: string) {
this.setCookie(key, 1, -1);
}
/**
* 清空cookie使所有cookie失效
*/
clearCookie(): void {
const keys = document.cookie.match(/[^ =;]+(?==)/g);
if (keys) {
for (let i = keys.length; i--; ) {
document.cookie = keys[i] + '=0;expire=' + new Date(0).toUTCString();
}
}
}
};
return new Storage();
};
export const storage = createStorage();
export default Storage;

41
src/utils/dataUtil.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* 重置对象会自动判断类型
* 当你传了第二个值之后会把两个对象合并
* 所有的值都置空
* 数字变为0
* @param obj
* @param newData
*/
export const resetData = (obj: object, newData?: object | null) => {
// 清空对象属性
Object.keys(obj).forEach(key => {
let type = "Undefined";
if (obj[key] !== undefined) {
type = toString.call(obj[key]).slice(8, -1); // 获取属性值的类型
}
switch (type) {
case "String":
obj[key] = "";
break;
case "Number":
obj[key] = null;
break;
case "Boolean":
obj[key] = false;
break;
case "Array":
obj[key] = [];
break;
case "Date":
obj[key] = null; // 日期类型置为 null
break;
case "Object":
resetData(obj[key]); // 对象类型递归遍历
break;
default:
obj[key] = null; // 其他类型都置为 null
}
});
// 合并新数据到原对象上
Object.assign(obj, newData);
};

View File

@@ -0,0 +1,212 @@
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
import axios from 'axios';
import { AxiosCanceler } from './axiosCancel';
import { isFunction } from '@/utils/is';
import { cloneDeep } from 'lodash-es';
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
import { ContentTypeEnum } from '@/enums/httpEnum';
export * from './axiosTransform';
/**
* @description: axios模块
*/
export class VAxios {
private axiosInstance: AxiosInstance;
private options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.axiosInstance = axios.create(options);
this.setupInterceptors();
}
getAxios(): AxiosInstance {
return this.axiosInstance;
}
/**
* @description: 重新配置axios
*/
configAxios(config: CreateAxiosOptions) {
if (!this.axiosInstance) {
return;
}
this.createAxios(config);
}
/**
* @description: 设置通用header
*/
setHeader(headers: any): void {
if (!this.axiosInstance) {
return;
}
Object.assign(this.axiosInstance.defaults.headers, headers);
}
/**
* @description: 请求方法
*/
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: AxiosRequestConfig = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
//这里重新 赋值成最新的配置
// @ts-ignore
conf.requestOptions = opt;
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
// 请求是否被取消
const isCancel = axios.isCancel(res);
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
try {
const ret = transformRequestData(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e));
return;
}
reject(e);
});
});
}
/**
* @description: 创建axios实例
*/
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config);
}
private getTransform() {
const { transform } = this.options;
return transform;
}
/**
* @description: 文件上传
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || 'file';
let conf: AxiosRequestConfig = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions);
const { beforeRequestHook } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, params.data![key]);
});
}
return this.axiosInstance.request<T>({
...conf,
method: 'POST',
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
ignoreCancelToken: true,
},
});
}
/**
* @description: 拦截器配置
*/
private setupInterceptors() {
const transform = this.getTransform();
if (!transform) {
return;
}
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch,
} = transform;
const axiosCanceler = new AxiosCanceler();
// 请求拦截器配置处理
// @ts-ignore
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
// @ts-ignore
const { headers: { ignoreCancelToken } } = config;
const ignoreCancel =
ignoreCancelToken !== undefined
? ignoreCancelToken
: this.options.requestOptions?.ignoreCancelToken;
!ignoreCancel && axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
// 请求拦截器错误捕获
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
// 响应结果拦截器处理
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
res && axiosCanceler.removePending(res.config);
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res, {});
}
return res;
}, undefined);
// 响应结果拦截器错误捕获
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
}
}

View File

@@ -0,0 +1,61 @@
import axios, { AxiosRequestConfig, Canceler } from 'axios';
import qs from 'qs';
import { isFunction } from '@/utils/is';
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
let pendingMap = new Map<string, Canceler>();
export const getPendingUrl = (config: AxiosRequestConfig) =>
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join('&');
export class AxiosCanceler {
/**
* 添加请求
* @param {Object} config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pendingMap.has(url)) {
// 如果 pending 中不存在当前请求,则添加进去
pendingMap.set(url, cancel);
}
});
}
/**
* @description: 清空所有pending
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
cancel && isFunction(cancel) && cancel();
});
pendingMap.clear();
}
/**
* 移除请求
* @param {Object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pendingMap.get(url);
cancel && cancel(url);
pendingMap.delete(url);
}
}
/**
* @description: 重置
*/
reset(): void {
pendingMap = new Map<string, Canceler>();
}
}

View File

@@ -0,0 +1,52 @@
/**
* 数据处理类,可以根据项目自行配置
*/
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestOptions, Result } from './types';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string;
transform?: AxiosTransform;
requestOptions?: RequestOptions;
}
export abstract class AxiosTransform {
/**
* @description: 请求之前处理配置
* @description: Process configuration before request
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
/**
* @description: 请求成功处理
*/
transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
/**
* @description: 请求失败处理
*/
requestCatch?: (e: Error) => Promise<any>;
/**
* @description: 请求之前的拦截器
*/
requestInterceptors?: (
config: AxiosRequestConfig,
options: CreateAxiosOptions
) => AxiosRequestConfig;
/**
* @description: 请求之后的拦截器
*/
responseInterceptors?: (res: AxiosResponse<any>, options: RequestOptions) => AxiosResponse<any>;
/**
* @description: 请求之前的拦截器错误处理
*/
requestInterceptorsCatch?: (error: Error) => void;
/**
* @description: 请求之后的拦截器错误处理
*/
responseInterceptorsCatch?: (error: Error) => void;
}

View File

@@ -0,0 +1,47 @@
import { showFailToast } from 'vant';
export function checkStatus(status: number, msg: string): void {
switch (status) {
case 400:
showFailToast(msg);
break;
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
showFailToast('用户没有权限(令牌、用户名、密码错误)!');
break;
case 403:
showFailToast('用户得到授权,但是访问是被禁止的。!');
break;
// 404请求不存在
case 404:
showFailToast('网络请求错误,未找到该资源!');
break;
case 405:
showFailToast('网络请求错误,请求方法未允许!');
break;
case 408:
showFailToast('网络请求超时');
break;
case 500:
showFailToast('服务器错误,请联系管理员!');
break;
case 501:
showFailToast('网络未实现');
break;
case 502:
showFailToast('网络错误');
break;
case 503:
showFailToast('服务不可用,服务器暂时过载或维护!');
break;
case 504:
showFailToast('网络超时');
break;
case 505:
showFailToast('http版本不支持该请求!');
break;
default:
showFailToast(msg);
}
}

View File

@@ -0,0 +1,47 @@
import { isObject, isString } from '@/utils/is';
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
export function joinTimestamp<T extends boolean>(
join: boolean,
restful: T
): T extends true ? string : object;
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join) {
return restful ? '' : {};
}
const now = new Date().getTime();
if (restful) {
return `?_t=${now}`;
}
return { _t: now };
}
/**
* @description: Format request parameter time
*/
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {
return;
}
for (const key in params) {
if (params[key] && params[key]._isAMomentObject) {
params[key] = params[key].format(DATE_TIME_FORMAT);
}
if (isString(key)) {
const value = params[key];
if (value) {
try {
params[key] = isString(value) ? value.trim() : value;
} catch (error) {
throw new Error(error as any);
}
}
}
if (isObject(params[key])) {
formatRequestDate(params[key]);
}
}
}

View File

@@ -0,0 +1,289 @@
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import {showDialog, showFailToast, showSuccessToast} from 'vant';
import { VAxios } from './Axios';
import { AxiosTransform } from './axiosTransform';
import axios, { AxiosResponse } from 'axios';
import { checkStatus } from './checkStatus';
import { joinTimestamp, formatRequestDate } from './helper';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { isString } from '@/utils/is/';
import { deepMerge, isUrl } from '@/utils';
import { setObjToUrlParams } from '@/utils/urlUtils';
import { RequestOptions, Result, CreateAxiosOptions } from './types';
import { useUserStoreWidthOut } from '@/store/modules/user';
const urlPrefix = '';
// import router from '@/router';
// import { storage } from '@/utils/Storage';
/**
* @description: 数据处理,方便区分多种处理方式
*/
const transform: AxiosTransform = {
/**
* @description: 处理请求数据
*/
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
const {
isShowMessage = false,
isShowErrorMessage,
isShowSuccessMessage,
successMessageText,
errorMessageText,
isTransformResponse,
isReturnNativeResponse,
} = options;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
return res.data;
}
// const $dialog = window['$dialog'];
// const $message = window['$message'];
if (!res.data) {
// return '[HTTP] Request has no return value';
throw new Error('请求出错,请稍候重试');
}
// rows 特殊处理
if (res.data.rows) {
res.data.data = res.data.rows
}
// 这里 coderesultmessage为 后台统一的字段,需要修改为项目自己的接口返回格式
const { code, data, msg } = res.data;
// 是否显示提示信息
if (isShowMessage) {
// 请求成功
const hasSuccess = data && Reflect.has(res.data, 'code') && code === ResultEnum.SUCCESS;
if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
// 是否显示自定义信息提示
showSuccessToast(successMessageText || msg || '操作成功!');
} else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
// 是否显示自定义信息提示
showFailToast(msg || errorMessageText || '操作失败!');
} else if (!hasSuccess && options.errorMessageMode === 'modal') {
// errorMessageMode=custom-modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
showDialog({
title: '提示',
message: msg,
confirmButtonText: '确定'
}).then(() => {
});
}
}
// 接口请求成功,直接返回结果
if (code === ResultEnum.SUCCESS) {
return data;
}
// 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
let errorMsg = msg;
switch (code) {
// 请求失败
case ResultEnum.ERROR:
showFailToast(errorMsg);
break;
// 登录超时
// case ResultEnum.TIMEOUT:
// const LoginName = PageEnum.BASE_LOGIN_NAME;
// const LoginPath = PageEnum.BASE_LOGIN;
// if (router.currentRoute.value?.name === LoginName) return;
// // 到登录页
// errorMsg = '登录超时,请重新登录!';
// $dialog.warning({
// title: '提示',
// content: '登录身份已失效,请重新登录!',
// positiveText: '确定',
// //negativeText: '取消',
// closable: false,
// maskClosable: false,
// onPositiveClick: () => {
// storage.clear();
// window.location.href = LoginPath;
// },
// onNegativeClick: () => {},
// });
// break;
}
throw new Error(errorMsg);
},
// 请求之前处理config
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
const isUrlStr = isUrl(config.url as string);
if (!isUrlStr && joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (!isUrlStr && apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
const data = config.data || false;
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
// 兼容restful风格
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
} else {
if (!isString(params)) {
formatDate && formatRequestDate(params);
if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
config.data = data;
config.params = params;
} else {
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data)
);
}
} else {
// 兼容restful风格
config.url = config.url + params;
config.params = undefined;
}
}
return config;
},
/**
* @description: 请求拦截器处理
*/
requestInterceptors: (config, options) => {
// 请求之前处理config
const headers = (config as Recordable).headers
const userStore = useUserStoreWidthOut();
const token = userStore.getToken;
headers.TYPE = 'TBS'
headers.channelId = 'MP'
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
// jwt token
headers.Authorization = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token;
}
return config;
},
/**
* @description: 响应错误处理
*/
responseInterceptorsCatch: (error: any) => {
// const $dialog = window['$dialog'];
// const $message = window['$message'];
const { response, code, message } = error || {};
// TODO 此处要根据后端接口返回格式修改
const msg: string =
response && response.data && response.data.message ? response.data.message : '';
const err: string = error.toString();
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
showFailToast('接口请求超时,请刷新页面重试!');
return;
}
if (err && err.includes('Network Error')) {
showFailToast('请检查您的网络连接是否正常')
// $dialog.info({
// title: '网络异常',
// content: '请检查您的网络连接是否正常',
// positiveText: '确定',
// //negativeText: '取消',
// closable: false,
// maskClosable: false,
// onPositiveClick: () => {},
// onNegativeClick: () => {},
// });
return Promise.reject(error);
}
} catch (error) {
throw new Error(error as any);
}
// 请求是否被取消
const isCancel = axios.isCancel(error);
if (!isCancel) {
checkStatus(error.response && error.response.status, msg);
} else {
console.warn(error, '请求被取消!');
}
//return Promise.reject(error);
return Promise.reject(response?.data);
},
};
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
deepMerge(
{
timeout: 10 * 1000,
authenticationScheme: '',
// 接口前缀
prefixUrl: urlPrefix,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 数据处理方式
transform,
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 默认将prefix 添加到url
joinPrefix: true,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: true,
// 消息提示类型
errorMessageMode: 'none',
// 接口地址
apiUrl: 'http://101.32.189.213:2781',
// apiUrl: '',
// 接口拼接地址
urlPrefix: urlPrefix,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
},
withCredentials: false,
},
opt || {}
)
);
}
export const http = createAxios();
// 项目,多个不同 api 地址,直接在这里导出多个
// src/api ts 里面接口,就可以单独使用这个请求,
// import { httpTwo } from '@/utils/http/axios'
// export const httpTwo = createAxios({
// requestOptions: {
// apiUrl: 'http://localhost:9001',
// urlPrefix: 'api',
// },
// });

View File

@@ -0,0 +1,66 @@
import { AxiosRequestConfig } from 'axios';
import { AxiosTransform } from '@/utils/http/axios/axiosTransform';
export interface CreateAxiosOptions extends AxiosRequestConfig {
transform?: AxiosTransform;
requestOptions?: RequestOptions;
authenticationScheme?: string;
}
// 上传文件
export interface UploadFileParams {
// 其他参数
data?: Recordable;
// 文件参数接口字段名
name?: string;
// 文件
file: File | Blob;
// 文件名称
filename?: string;
[key: string]: any;
}
export interface RequestOptions {
// 请求参数拼接到url
joinParamsToUrl?: boolean;
// 格式化请求参数时间
formatDate?: boolean;
// 是否显示提示信息
isShowMessage?: boolean;
// 是否解析成JSON
isParseToJson?: boolean;
// 成功的文本信息
successMessageText?: string;
// 是否显示成功信息
isShowSuccessMessage?: boolean;
// 是否显示失败信息
isShowErrorMessage?: boolean;
// 错误的文本信息
errorMessageText?: string;
// 是否加入url
joinPrefix?: boolean;
// 接口地址, 不填则使用默认apiUrl
apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
// 错误消息提示类型
errorMessageMode?: 'none' | 'modal';
// 是否添加时间戳
joinTime?: boolean;
// 不进行任何处理,直接返回
isTransformResponse?: boolean;
// 是否返回原生响应头
isReturnNativeResponse?: boolean;
//忽略重复请求
ignoreCancelToken?: boolean;
// 是否携带token
withToken?: boolean;
}
export interface Result<T = any> {
code: Number;
type?: 'success' | 'error' | 'warning';
msg: string;
data?: T;
rows?: T;
}

278
src/utils/index.ts Normal file
View File

@@ -0,0 +1,278 @@
import {isObject} from "@/utils/is";
/** px 转 vw, 375 是设置的屏幕宽度 */
export const px2vw = (px: number): string => {
return `${window.screen.width / 750 * px / window.screen.width * 100}vw`
}
/**
* 判断是否 url
* */
export function isUrl(url: string) {
return /^(http|https):\/\//g.test(url);
}
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
let key: string;
for (key in target) {
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
}
return src;
}
// ¥6,285
export function toIntMark(value) {
try {
if (value.toString().includes('%')) {
return value
}
const newVal = parseFloat(value).toFixed(0)
if (newVal == 'NaN') {
return value
}
return '¥' + newVal.replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, '$1,')
} catch (e) {
return value
}
}
// 6,285
export function toInt(value) {
try {
if (value.toString().includes('%')) {
return value
}
const newVal = parseFloat(value).toFixed(0)
if (newVal == 'NaN') {
return value
}
return newVal.replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, '$1,')
} catch (e) {
return value
}
}
// ¥6k
export function toThousand(value) {
try {
if (!value) {
return 0
}
if (value.toString().includes('%')) {
return value
}
const newVal = parseFloat(value / 1000 + '').toFixed(0)
if (newVal == 'NaN') {
return value
}
return '¥' + newVal.replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, '$1,') + 'k'
} catch (e) {
return value
}
}
// ¥6,285.00
export function toRoundMark(value: number | string) {
value = value || 0
const options = {
style: 'currency',
currency: 'CNY',
};
return value.toLocaleString('zh-CN', options)
}
// 6285.00
export function toRound(value) {
try {
if (value.toString().includes('%')) {
return value
}
const newVal = parseFloat(value).toFixed(2)
if (newVal == 'NaN') {
return value
}
return newVal
} catch (e) {
return value
}
}
export const regFenToYuan = (fen) => {
let num;
num=fen*0.01;
num+='';
const reg = num.indexOf('.') > -1 ? /(\d{1,3})(?=(?:\d{3})+\.)/g : /(\d{1,3})(?=(?:\d{3})+$)/g;
num=num.replace(reg,'$1');
num = toDecimal2(num)
return num
}
export const toDecimal2 = (x) => {
let f = parseFloat(x);
if (isNaN(f)) {
return false;
}
f = Math.round(x * 100) / 100;
let s = f.toString();
let rs = s.indexOf('.');
if (rs < 0) {
rs = s.length;
s += '.';
}
while (s.length <= rs + 2) {
s += '0';
}
return s;
}
/**
*
* @param customStyle
* @param target
*/
export function addStyle(customStyle, target = 'object') {
// 字符串转字符串,对象转对象情形,直接返回
if (empty(customStyle) || typeof(customStyle) === 'object' && target === 'object' || target === 'string' &&
typeof(customStyle) === 'string') {
return customStyle
}
// 字符串转对象
if (target === 'object') {
// 去除字符串样式中的两端空格(中间的空格不能去掉比如padding: 20px 0如果去掉了就错了),空格是无用的
customStyle = customStyle.clearSpaces('both')
// 根据";"将字符串转为数组形式
const styleArray = customStyle.split(';')
const style = {}
// 历遍数组,拼接成对象
for (let i = 0; i < styleArray.length; i++) {
// 'font-size:20px;color:red;',如此最后字符串有";"的话会导致styleArray最后一个元素为空字符串这里需要过滤
if (styleArray[i]) {
const item = styleArray[i].split(':')
style[item[0].clearSpaces('both')] = item[1].clearSpaces('both')
}
}
return style
}
// 这里为对象转字符串形式
let string = ''
for (const i in customStyle) {
// 驼峰转为中划线的形式否则css内联样式无法识别驼峰样式属性名
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase()
string += `${key}:${customStyle[i]};`
}
// 去除两端空格
return string.clearSpaces('both')
}
/**
* 判断是否为空
*/
export function empty(value) {
switch (typeof value) {
case 'undefined':
return true
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length === 0) return true
break
case 'boolean':
if (!value) return true
break
case 'number':
if (value === 0 || isNaN(value)) return true
break
case 'object':
if (value === null || value.length === 0) return true
// for (const i in value) {
// return false
// }
return true
}
return false
}
/**
* 获取本地图片
* @param name // 文件名 如 doc.png
* @returns {*|string}
*/
export function getAssetsImages(name) {
return new URL(`/src/assets/images/${name}`, import.meta.url).href;
}
/**
* 加法 (处理精度问题)
* @param num1
* @param num2
* @returns {string}
*/
export function numAdd(num1, num2) {
let baseNum, baseNum1, baseNum2, precision // 精度
try {
baseNum1 = num1.toString().split('.')[1].length
} catch (e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch (e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
precision = (baseNum1 >= baseNum2) ? baseNum1 : baseNum2
return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision)
}
/**
* 减法 (处理精度问题)
* @param num1
* @param num2
* @returns {string}
*/
export function numSub(num1, num2) {
let baseNum, baseNum1, baseNum2, precision // 精度
try {
baseNum1 = num1.toString().split('.')[1].length
} catch (e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch (e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
precision = (baseNum1 >= baseNum2) ? baseNum1 : baseNum2
return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision)
}
/**
* 打电话
* @param phoneNumber
*/
export const callPhone = (phoneNumber) => {
if (phoneNumber) {
window.location.href = "tel://" + phoneNumber;
}
}
export function debounce(func: Function, time: number, immediate = false) {
let timer: number | null = null;
return (...args: any) => {
if (timer) clearInterval(timer)
if (immediate) {
if (!timer) func.apply(this, args);
timer = window.setTimeout(() => {
timer = null
}, time)
} else {
timer = window.setTimeout(() => {
func.apply(this, args)
}, time)
}
}
}

118
src/utils/is/index.ts Normal file
View File

@@ -0,0 +1,118 @@
const toString = Object.prototype.toString;
/**
* @description: 判断值是否未某个类型
*/
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`;
}
/**
* @description: 是否为函数
*/
export function isFunction<T = Function>(val: unknown): val is T {
return is(val, 'Function') || is(val, 'AsyncFunction');
}
/**
* @description: 是否已定义
*/
export const isDef = <T = unknown>(val?: T): val is T => {
return typeof val !== 'undefined';
};
export const isUnDef = <T = unknown>(val?: T): val is T => {
return !isDef(val);
};
/**
* @description: 是否为对象
*/
export const isObject = (val: any): val is Record<any, any> => {
return val !== null && is(val, 'Object');
};
/**
* @description: 是否为时间
*/
export function isDate(val: unknown): val is Date {
return is(val, 'Date');
}
/**
* @description: 是否为数值
*/
export function isNumber(val: unknown): val is number {
return is(val, 'Number');
}
/**
* @description: 是否为AsyncFunction
*/
export function isAsyncFunction<T = any>(val: unknown): val is () => Promise<T> {
return is(val, 'AsyncFunction');
}
/**
* @description: 是否为promise
*/
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch);
}
/**
* @description: 是否为字符串
*/
export function isString(val: unknown): val is string {
return is(val, 'String');
}
/**
* @description: 是否为boolean类型
*/
export function isBoolean(val: unknown): val is boolean {
return is(val, 'Boolean');
}
/**
* @description: 是否为数组
*/
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val);
}
/**
* @description: 是否客户端
*/
export const isClient = () => {
return typeof window !== 'undefined';
};
/**
* @description: 是否为浏览器
*/
export const isWindow = (val: any): val is Window => {
return typeof window !== 'undefined' && is(val, 'Window');
};
export const isElement = (val: unknown): val is Element => {
return isObject(val) && !!val.tagName;
};
export const isServer = typeof window === 'undefined';
// 是否为图片节点
export function isImageDom(o: Element) {
return o && ['IMAGE', 'IMG'].includes(o.tagName);
}
export function isNull(val: unknown): val is null {
return val === null;
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val);
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}

47
src/utils/mapUtil.ts Normal file
View File

@@ -0,0 +1,47 @@
import AMapLoader from "@amap/amap-jsapi-loader";
import {useUserStore} from "@/store/modules/user";
export const getLocation = () => {
const user = useUserStore()
AMapLoader.load({
key: 'cd9769b4feec118fe176763bd10168a0',
version: "2.0",
AMapUI: {
version: "1.1",
},
Loca: {
version: "2.0.0"
},
}).then((AMap) => {
const map = new AMap.Map("container", {
zoom: 15,
features: ['bg', 'point', 'road', 'building'],
viewMode: '2D', //设置地图模式
// center: [116.47394,39.904211],
resizeEnable: true
})
AMap.plugin(['AMap.AutoComplete', 'AMap.PlaceSearch', 'AMap.Geolocation', 'AMap.CitySearch'], () => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, // 是否使用高精度定位默认true
timeout: 10000, // 设置定位超时时间,默认:无穷大
showButton: true, //显示定位按钮默认true
needAddress: true, //是否需要将定位结果进行逆地理编码操作
convert: true, //自动偏移坐标偏移后的坐标为高德坐标默认true
position: 'RB',
offset: [10, 20],
})
map.addControl(geolocation)
geolocation.getCurrentPosition()
// 获取用户当前的精确位置信息
geolocation.getCurrentPosition((status, result) => {
// myCity.value = result.addressComponent.province
user.setLocationMap(result)
})
})
})
}

37
src/utils/urlUtils.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* 将对象添加当作参数拼接到URL上面
* @param baseUrl 需要拼接的url
* @param obj 参数对象
* @returns {string} 拼接后的对象
* 例子:
* let obj = {a: '3', b: '4'}
* setObjToUrlParams('www.baidu.com', obj)
* ==>www.baidu.com?a=3&b=4
*/
export function setObjToUrlParams(baseUrl: string, obj: any): string {
let parameters = '';
let url = '';
for (const key in obj) {
parameters += key + '=' + encodeURIComponent(obj[key]) + '&';
}
parameters = parameters.replace(/&$/, '');
if (/\?$/.test(baseUrl)) {
url = baseUrl + parameters;
} else {
url = baseUrl.replace(/\/?$/, '?') + parameters;
}
return url;
}
/**
* 根据name从url中取值
* @param name
* @returns {*}
*/
export function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}

99
src/utils/wexin.ts Normal file
View File

@@ -0,0 +1,99 @@
import wx from "weixin-js-sdk";
import {jsapiToken} from "@/api/pay";
import {useUserStore} from "@/store/modules/user";
import {showToast} from "vant";
export const getWxConfig = (url) => {
const user = useUserStore()
return new Promise((resolve, reject) => {
if (user.getWxConfig) {
resolve(user.getWxConfig)
} else {
jsapiToken({
url: url
}).then(res => {
user.setWxConfig(res)
resolve(user.getWxConfig)
})
}
})
}
export const getLocation = (params, callback) => {
getWxConfig(params.url).then(res => {
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
appId: res.appId, // 必填,公众号的唯一标识
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.nonceStr, // 必填,生成签名的随机串
signature: res.signature, // 必填,签名
jsApiList: ['checkJsApi', 'getLocation'] // 必填需要使用的JS接口列表
})
wx.ready(() => {
wx.checkJsApi({
jsApiList: ['getLocation'],
success: function () {
wx.getLocation({
type: 'gcj02', // 默认为wgs84的gps坐标如果要返回直接给openLocation用的火星坐标可传入'gcj02'
success: (res) => {
console.log('res11111')
callback(res)
},
fail: (res) => {
// showToast('定位失败 -- > ' + JSON.stringify(res))
console.error('定位失败')
},
cancel: function (res) {
console.error(res)
},
});
},
fail: function (res) {
showToast('fail -- > ' + JSON.stringify(res))
console.error(res)
}
})
})
wx.error(err => {
showToast('err -- > ' + JSON.stringify(err))
console.error(err)
})
})
}
export const openLocation = (params) => {
getWxConfig(params.url).then(res => {
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
appId: res.appId, // 必填,公众号的唯一标识
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.nonceStr, // 必填,生成签名的随机串
signature: res.signature, // 必填,签名
jsApiList: ['checkJsApi', 'openLocation'] // 必填需要使用的JS接口列表
})
wx.ready(() => {
wx.checkJsApi({
jsApiList: ['openLocation'],
success: function () {
wx.openLocation({
latitude: params.latitude, // 纬度浮点数范围为90 ~ -90
longitude: params.longitude, // 经度浮点数范围为180 ~ -180。
name: '', // 位置名
address: '', // 地址详情说明
scale: 1, // 地图缩放级别,整型值,范围从1~28。默认为最大
infoUrl: '' // 在查看位置界面底部显示的超链接,可点击跳转
});
},
fail: function (res) {
console.error(res)
}
})
})
wx.error(err => {
console.error(err)
})
})
}

54
src/utils/wexinPay.ts Normal file
View File

@@ -0,0 +1,54 @@
import wx from 'weixin-js-sdk'
export function pay(signInfo, payData,callback,errorCallBack?) {
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
appId: signInfo.appId, // 必填,公众号的唯一标识
timestamp: signInfo.timestamp, // 必填,生成签名的时间戳
nonceStr: signInfo.nonceStr, // 必填,生成签名的随机串
signature: signInfo.signature, // 必填,签名
jsApiList: ['checkJsApi', 'chooseWXPay'] // 必填需要使用的JS接口列表
})
wx.ready(() => {
wx.checkJsApi({
jsApiList: ['chooseWXPay'],
success: function () {
wx.chooseWXPay({
// 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
// appId: payData.appId,
timestamp: payData.timeStamp, // 必填,生成签名的时间戳
nonceStr: payData.nonceStr, // 必填,生成签名的随机串
package: payData.package,
signType: payData.signType,
paySign: payData.paySign, // 必填,签名
success: function (res) { // 支付成功后的回调函数
console.log('支付成功' + res)
if (callback) {
callback()
}
},
fail: function (reg) {
console.log(JSON.stringify(reg))
if (errorCallBack) {
errorCallBack()
}
}
})
},
fail: function (res) {
console.log(JSON.stringify(res))
if (errorCallBack) {
errorCallBack()
}
}
})
})
wx.error(err => {
console.log(JSON.stringify(err))
if (errorCallBack) {
errorCallBack()
}
})
}

View File

@@ -0,0 +1,40 @@
<template>
<j-nav-bar navBarBackground="#fff" color="#000" :title="tittleMap[route.query.key]"/>
<div :style="{padding: px2vw(20)}">
<div v-html="agreement">
</div>
</div>
</template>
<script lang="ts" setup>
import {getAgreement} from "@/api";
import {onMounted, ref} from "vue";
import {useRoute} from "vue-router";
import {px2vw} from "@/utils";
const route = useRoute()
const agreement = ref('')
const _getSystemConfigOne = (key) => {
getAgreement().then(res => {
agreement.value = res[key]
})
}
const tittleMap = {
loansAgreement: '借款协议',
serviceAgreement: '平台服务协议',
authAgreement: '委托授权协议',
lawAgreement: '法律协议',
}
onMounted(() => {
tittleMap[route.query.key] ? document.title = tittleMap[route.query.key] : ''
_getSystemConfigOne(route.query.key)
})
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,230 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a" :placeholder="false"/>
<div class="content">
<j-gap height="120" background="#F9BF3A" opacity="0"/>
<div class="action">
<view class="tt">申请时间 {{ new Date(borrowInfo.createTime).format('yyyy-MM-dd hh:mm:ss') }}</view>
<view class="footer-content">
<view class="footer-content-1">
<van-steps active-icon="success" active-color="#07c160" :active="active">
<van-step v-for="(item, index) in stepBorrow.borrowStep" :key="index">{{ item.name }}</van-step>
</van-steps>
</view>
<view class="footer-content-2">
<view class="footer-content-2-tit gray_color">温馨提示</view>
<view class="footer-content-2-content" :style="{color: stepBorrow.borrowNameStyle || '#e84a10'}">
{{ stepBorrow.borrowRemark }}
</view>
</view>
</view>
</div>
<div class="action">
<view class="tt">
<view>贷款详情</view>
</view>
<van-cell title="贷款编号" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="borrowInfo.tradeNo" />
<van-cell title="借款金额" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="toRoundMark(borrowInfo.totalLoanMoney)" />
<!-- <van-cell title="借款期限" title-style="color: #8997ae;" style="&#45;&#45;van-cell-value-color: #000" :value="borrowInfo.totalMonth + '个月'" />-->
<van-cell title="贷款周期" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="borrowInfo.totalMonth + '个月'" />
<van-cell title="提现银行" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="borrowInfo.bankType" />
<van-cell title="每期还款" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="toRoundMark(borrowInfo.avgRepayment)" />
<van-cell title="描述" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="borrowInfo.noteRemark" />
</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {getBorrowInfo, getUserInfo} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {useRoute} from "vue-router";
import {toRoundMark} from "@/utils";
const route = useRoute()
const active = ref(0);
const stepBorrow = reactive({
"borrowNameStyle": "",
"borrowRemark": "",
"borrowStep": [
{
"name": "提交成功",
"over": true
},
{
"name": "银行卡异常",
"over": true
},
{
"name": "到账成功",
"over": false
}
]
})
const borrowInfo = reactive({
"auditFlag": true,
"avgRepayment": 0,
"backCardNum": "",
"bankType": "",
"borrowName": "",
"borrowNameStyle": "",
"borrowRemark": "",
"cardBackPicture": "",
"cardFrontPicture": "",
"cardNum": "",
"companyAddress": "",
"companyAddressInfo": "",
"companyName": "",
"companyPhone": "",
"companyTitle": "",
"companyYear": "",
"createTime": "",
"customerAddress": "",
"customerAddressInfo": "",
"customerId": 0,
"dueDate": 0,
"firstBackCardNum": "",
"firstBankType": "",
"firstRepayment": 0,
"id": 0,
"infoJson": "",
"kinsfolkName": "",
"kinsfolkPhone": "",
"kinsfolkRef": "",
"loanMonthRate": 0,
"loanProcessResp": {
"borrowNameStyle": "",
"borrowRemark": "",
"borrowStep": [
{
"name": "",
"over": false
}
]
},
"loanYearRate": 0,
"noteRemark": "",
"realName": "",
"remitFlag": 0,
"repayRemark": "",
"totalInterest": 0,
"totalLoanMoney": 0,
"totalMonth": 0,
"totalRepayment": 0,
"tradeNo": "",
"updateBackNum": 0,
"updateTime": ""
})
const _getBorrowInfo = () => {
getBorrowInfo({tradeNo: route.query.tradeNo}).then(res => {
resetData(borrowInfo, res)
resetData(stepBorrow, borrowInfo.loanProcessResp)
stepBorrow.borrowStep.forEach((sb, index) => {
if (sb.over) {
active.value = index
}
})
})
}
onMounted(() => {
_getBorrowInfo()
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
padding-bottom: 30px;
height: 100vh;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: start;
background: #f6f9fd;
color: #000;
font-size: 30px;
font-weight: bold;
padding: 20px 20px;
}
//
}
}
.font-22 {
font-size: 22px;
}
.footer-content {
width: 100%;
background: #fff;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-1 {
padding: 20px 80px;
//background: #f9fafb;
}
&-2 {
display: flex;
padding: 20px;
&-tit {
display: block;
width: 220px;
margin-right: 30px;
}
&-content {
display: block;
}
}
}
</style>

156
src/views/forget/index.vue Normal file
View File

@@ -0,0 +1,156 @@
<template>
<j-nav-bar :placeholder="false" color="#FFF" nav-bar-background="#F9BF3A00"/>
<div class="content">
<j-gap height="50" background="#F9BF3A" opacity="0"/>
<j-gap height="120" opacity="1"/>
<van-field
v-model="loginData.phoneNumber"
v-if="flag === '1'"
class="login-btn"
label="手机号码"
placeholder="请输入手机号码"
label-align="top"
style="background: #12332100"
type="tel"
/>
<van-field
v-model="loginData.code"
class="login-btn"
v-if="flag === '1'"
label="验证码"
placeholder="请输入验证码"
label-align="top"
style="background: #12332100"
type="number"
>
<template #button>
<div class="password-btn">
<van-count-down v-show="countDownFlag" ref="countDown" :auto-start="false" :time="time" @finish="onFinish">
<template #default="timeData">
<span class="block">{{ timeData.seconds }}</span>
</template>
</van-count-down>
<div v-show="!countDownFlag" style="color: #bc7c1c" @click="start">发送验证码</div>
</div>
</template>
</van-field>
<van-field
v-model="loginData.password"
class="login-btn"
v-if="flag === '2'"
label="登录密码"
label-align="top"
placeholder="请设置6-16位密码"
style="background: #12332100"
type="password"
/>
<van-field
v-model="loginData.confirmPassword"
class="login-btn"
v-if="flag === '2'"
label="确认密码"
label-align="top"
placeholder="请再次输入密码"
style="background: #12332100"
type="password"
/>
<van-button
:disabled="!loginData.phoneNumber || !loginData.code"
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; margin: 20px 0"
v-if="flag === '1'"
@click.stop="next"
>
下一步
</van-button>
<van-button
:disabled="!loginData.password || !loginData.confirmPassword"
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; margin: 20px 0"
v-if="flag === '2'"
@click.stop="updatePwdBtn"
>
确认修改
</van-button>
</div>
</template>
<script lang="ts" setup>
import {reactive, ref} from "vue";
import {sendSmsForget, updatePwd} from "@/api/login";
import {showToast} from "vant";
const loginData = reactive({
phone: null,
phoneNumber: '',
code: null,
password: '',
confirmPassword: '',
checkCode: '',
})
const flag = ref('1')
const time = ref(60 * 1000);
const countDown = ref();
const countDownFlag = ref(false);
const start = () => {
if (loginData.phoneNumber) {
countDown.value.start();
countDownFlag.value = true
sendSmsForget({
phoneNumber: loginData.phoneNumber
}).then(res => {
console.log(res)
loginData.code = res
loginData.checkCode = res
onFinish()
})
} else {
showToast('请输入手机号')
}
};
const next = () => {
flag.value = '2'
}
const updatePwdBtn = () => {
updatePwd({
checkCode: loginData.checkCode,
confirmPassword: loginData.confirmPassword,
password: loginData.password
}).then(res => {
console.log(res)
})
}
const onFinish = () => {
countDownFlag.value = false
countDown.value.reset();
}
</script>
<style lang="scss" scoped>
.content {
padding: 0 10px;
background-image: linear-gradient(to bottom, #F9BF3A, #ffffff, #ffffff, #ffffff, #ffffff);
height: 100vh;
}
</style>

View File

@@ -0,0 +1,444 @@
<template>
<div>
<j-nav-bar/>
<!-- banner -->
<div :style="{paddingBottom: px2vw(20)}" style="overflow: hidden;">
<van-swipe :autoplay="3000" lazy-render>
<van-swipe-item v-for="banner in bannerList" :key="banner.bannerUrl">
<van-image :src="banner.bannerUrl"></van-image>
</van-swipe-item>
</van-swipe>
</div>
<div :style="{padding: '0 ' + px2vw(30) + ' ' + px2vw(20)}">
<div class="product">
<div class="product-title">产品详情</div>
<div class="product-content">
<div class="product-content-label">
<div>最低日息</div>
<div><span>{{ calLoan.loanRate }}%</span></div>
</div>
<div class="product-content-label">
<div>借款额度</div>
<div><span>¥{{ loans.loansMinAccount }}-{{ loans.loansMaxAccount }}</span></div>
</div>
<div class="product-content-label">
<div>分期期限</div>
<div><span>可选{{ loans.loansMonth.replaceAll(',', '/') }}</span></div>
</div>
</div>
</div>
<div class="apply">
<div class="apply-title yellow_color">申请金额()</div>
<div class="apply-money">{{ strip }}</div>
<div class="apply-strip-box">
<div class="reduce" @click="moneyReduce(100)"></div>
<div class="slider">
<van-slider bar-height="16" active-color="linear-gradient(to bottom, #f3654d, #f9ad7d)" :max="loans.loansMaxAccount" :min="loans.loansMinAccount" :step="100" v-model="strip">
<template #button>
<div class="custom-button"></div>
</template>
</van-slider>
</div>
<div class="add" @click="moneyAdd(100)"></div>
</div>
<div class="apply-jkqx">
<div class="apply-jkqx-title">借款期限</div>
<div class="apply-jkqx-item-box">
<div v-for="lm in loans.loansMonthList" :key="lm" class="apply-jkqx-item" :class="{'checked': lm === lmChecked}" @click="lmChecked = lm">
{{ lm }}个月
</div>
</div>
</div>
<j-gap height="2" background="#fff"/>
<div class="apply-mqhk">
<div>每期还款</div>
<div>¥{{ calLoan.avgRepayment }}</div>
<div>(日利率{{ calLoan.loanRate }}% 总利息¥{{ calLoan.totalInterest }})</div>
</div>
</div>
</div>
<div class="notice">
<van-icon :name="getAssetsImages('home/xlb.png')" size="30"/>
<div>{{ loansUser.time }}</div>
<div style="color: #ec6401">{{ loansUser.phone }}</div>
<div>成功借款</div>
<div style="color: #BC7C1C; font-weight: 600;">¥{{ loansUser.amount }}</div>
</div>
<div class="xieyi">
<van-checkbox v-model="checked" :icon-size="px2vw(28)" style="align-items: baseline" checked-color="linear-gradient(to right, #F9D88D, #D2A64C)">
我已阅读并同意
<span class="yellow_color1" @click.stop="go('authAgreement')">委托授权协议</span>
<span class="yellow_color1" @click.stop="go('serviceAgreement')">平台服务协议</span>
<span class="yellow_color1" @click.stop="go('loansAgreement')">借款协议</span>
</van-checkbox>
</div>
<div style="padding: 0 20px 0">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; "
@click.stop="immediateBorrowing"
>
立即借款
</van-button>
</div>
<j-gap :height="170" opacity/>
</div>
</template>
<script lang="ts" setup>
import {debounce, getAssetsImages, px2vw} from "@/utils";
import {computed, onMounted, reactive, ref,onUnmounted} from "vue";
import {useRouter} from "vue-router";
import {useUserStore} from "@/store/modules/user";
import {getCalLoan, getHomeInfo, getLoansInfo, getLoansUser, getUserInfo} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {watch} from "vue-demi";
import {showToast} from "vant";
import JGap from "@/components/JGap/JGap.vue";
const user = useUserStore()
const router = useRouter()
const bannerList = [
{
bannerUrl: getAssetsImages('home/banner_home.png')
}
]
const checked = ref(false);
const strip = ref(3000)
const lmChecked = ref('')
const moneyReduce = (step: number) => {
strip.value-=step
}
const moneyAdd = (step: number) => {
strip.value+=step
}
const go = (key) => {
// agreement
router.push({
path: '/agreement',
query: {
key: key
}
})
}
const immediateBorrowing = () => {
if (checked.value) {
if (userInfo.infoFlag && userInfo.bankFlag && userInfo.cardFlag) {
router.push({
path: '/loansInfo',
query: {
"totalLoanMoney": strip.value,
"totalMonth": lmChecked.value
}
})
} else {
showToast('请先填写个人资料')
router.push({
path: '/userInfo'
})
}
return
}
showToast('请阅读并同意相关协议')
}
const loans = reactive({
dueDate: 0,
id: 0,
loansInitAccount: 0,
loansInitMonth: "",
loansMaxAccount: 0,
loansMinAccount: 0,
loansMonth: "",
loansMonthList: [],
serviceRate: "",
serviceRateList: [],
})
const _getLoansInfo = () => {
getLoansInfo().then(res => {
resetData(loans, res)
loans.loansMonthList.newPush(loans.loansMonth.split(','))
loans.serviceRateList.newPush(loans.serviceRate.split(','))
lmChecked.value = loans.loansInitMonth
strip.value = loans.loansInitAccount
})
}
const homeInfo = reactive({
"id": "1",
"homeTitle": "123123",
"bannerOne": "http://localhost:8082/profile/upload/2023/11/29/5ea3a159-81e9-4630-a72d-3ff2ce68b306.jpg",
"commonSeal": null
})
const _getHomeInfo = () => {
getHomeInfo().then(res => {
resetData(homeInfo, res)
bannerList.newPush({
bannerUrl: homeInfo.bannerOne
})
})
}
const calLoan = reactive({
"avgRepayment": 0,
"firstRepayment": 0,
"loanRate": 0,
"totalInterest": 0,
"totalLoanMoney": 0,
"totalMonth": 0,
"totalRepayment": 0
})
const params = computed(() => {
return {
"totalLoanMoney": strip.value,
"totalMonth": lmChecked.value
}
})
watch(params, () => {
if (params.value.totalLoanMoney && params.value.totalMonth) {
_getCalLoan()
}
})
const _getCalLoan = debounce(() => {
getCalLoan(params.value).then(res => {
resetData(calLoan, res)
})
}, 200)
const loansUser = reactive({
amount : "48000",
phone : "153****0552",
time : "2023/11/27"
})
const timer = ref(0)
const _getLoansUser = () => {
getLoansUser().then(res => {
resetData(loansUser, res)
})
timer.value = setInterval(() => {
getLoansUser().then(res => {
resetData(loansUser, res)
})
}, 30000);
}
const userInfo = reactive({
cardFlag: false,
infoFlag: false,
bankFlag: false
})
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
})
}
onMounted(() => {
_getLoansInfo()
_getHomeInfo()
_getLoansUser()
_getUserInfo()
})
onUnmounted(() => {
clearInterval(timer.value);
})
</script>
<style lang="scss" scoped>
.product {
&-title {
font-weight: bold;
font-size: 40px;
padding: 0 0 20px 0;
}
&-content {
background: url("../../../assets/images/home/detail_bg.png") no-repeat;
background-size: 100% 100%;
color: #a2a2a2;
padding: 30px;
&-label {
display: flex;
justify-content: space-between;
padding: 7px 0;
span {
color: #F9BF3A
}
}
}
}
.apply {
background: url("../../../assets/images/home/apply_bg.png") no-repeat;
background-size: 100% 100%;
padding: 30px;
margin: 30px 0 15px;
&-title {
text-align: center;
font-weight: bold;
font-size: 34px;
&:before {
content: "";
display: inline-block;
width: 120px;
height: 1px;
margin-right: 20px;
margin-bottom: 12px;
background-image: -webkit-gradient(linear, left top, right top, from(#fbde60), to(#e46f00));
background-image: linear-gradient(to right, #fbde60, #e46f00);
}
&:after {
content: "";
display: inline-block;
width: 120px;
height: 1px;
margin-left: 20px;
margin-bottom: 12px;
background-image: linear-gradient(to right, #e46f00, #fbde60);
}
}
&-money {
font-size: 80px;
text-align: center;
color: #5c2d00;
font-weight: bold;
}
&-strip-box{
margin-bottom: 60px;
display: flex;
justify-content: space-between;
align-items: center;
.reduce {
background: url("../../../assets/images/home/strip-reduce.png") no-repeat;
background-size: 100% 100%;
width: 40px;
height: 40px;
margin-right: 40px;
}
.add {
background: url("../../../assets/images/home/strip-add.png") no-repeat;
background-size: 100% 100%;
width: 40px;
height: 40px;
margin-left: 40px;
}
.slider {
flex: 1;
}
.custom-button {
background: url("../../../assets/images/home/strip-block.png") no-repeat;
background-size: 100% 100%;
height: 58px;
width: 43px;
}
}
&-jkqx {
margin-bottom: 40px;
&-title {
font-size: 28px;
font-weight: 600;
color: #141414;
margin-bottom: 20px;
}
&-item-box {
display: flex;
flex-wrap: wrap;
.apply-jkqx-item{
border-radius: 40px;
text-align: center;
background: #fffbba;
padding: 4px 16px;
color: #BC7C1C;
margin-right: 10px;
margin-bottom: 10px;
}
.checked {
background-image: linear-gradient(to bottom, #f3654d, #f9ad7d);
color: #fff;
}
}
}
&-mqhk {
margin-top: 40px;
display: flex;
justify-content: space-between;
align-items: end;
font-size: 28px;
div:nth-child(1) {
font-size: 25px;
font-weight: 600;
color: #141414;
}
div:nth-child(2) {
color: #b25700;
}
div:last-child {
color: #fff;
font-size: 24px;
}
}
}
.notice {
background-image: linear-gradient(to right, #fae1cf, #fcfaf8);
padding: 8px 20px;
margin: 0 30px;
border-radius: 40px;
font-size: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.xieyi {
padding: 30px 35px;
font-size: 24px;
}
</style>

68
src/views/index/index.vue Normal file
View File

@@ -0,0 +1,68 @@
<template>
<!-- <div style="background: linear-gradient(167.96deg, #E6FAE1 0%, #F2E7B7 98.44%) no-repeat;min-height: 100%" >-->
<div>
<router-view />
<van-tabbar route v-model="active" active-color="#F9BF3A">
<van-tabbar-item v-for="tabBar in tabBarList" :key="tabBar.name" :replace="tabBar.replace" :to="tabBar.to">
<span>{{ tabBar.text }}</span>
<template #icon="props">
<van-icon :color="props.active ? '' : 'rgb(229, 229, 229)'" :name="props.active ? tabBar.active : tabBar.inactive" />
</template>
</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
import JGap from "@/components/JGap/JGap.vue";
import {getAssetsImages} from "@/utils";
import {useBackgroundHook} from "@/hooks/useBackgroundHook";
const tabBarList = [
{
to: '/home',
name: 'home',
text: '首页',
replace: true,
active: getAssetsImages('tabBar/home-active.png'),
inactive: getAssetsImages('tabBar/home-inactive.png')
},
{
to: '/serveList',
name: 'serveList',
text: '钱包',
replace: true,
active: getAssetsImages('tabBar/artificer-active.png'),
inactive: getAssetsImages('tabBar/artificer-inactive.png')
},
{
to: '/message',
name: 'message',
text: '客服',
replace: true,
active: 'chat',
inactive: 'chat'
},
{
to: '/my',
name: 'my',
text: '我的',
replace: true,
active: getAssetsImages('tabBar/my-active.png'),
inactive: getAssetsImages('tabBar/my-inactive.png')
}
]
const active = ref(0)
// const {setBodyBackground} = useBackgroundHook()
// setBodyBackground()
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,46 @@
<template>
<j-nav-bar/>
<div ref="root" class="frame" style="height: calc(100vh - 50px - 46px)">
<iframe :src="iframeSrc" class="frame-iframe" ref="frameRef"></iframe>
</div>
</template>
<script lang="ts" setup>
import {ref} from "vue";
import {getSetting} from "@/api";
import {onMounted} from "vue";
const iframeSrc = ref('')
// const iframeSrc = ref('https://chatlink.mstatik.com/widget/standalone.html?eid=329d34187acc7ebda66a12a0671e3d70')
const _getSetting = () => {
getSetting().then(res => {
iframeSrc.value = res.chatUrl
})
}
onMounted(() => {
_getSetting()
})
</script>
<style lang="scss" scoped>
.frame {
position: relative;
top: 0;
left: 0;
right: 0;
bottom: 0;
&-iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<j-nav-bar/>
<div class="header">
<div class="header-bj">
<div class="header-head" :style="{'--bg-image': `url(${headerImage}) no-repeat`}"></div>
<div class="header-text">{{ customerInfo.nickName }}</div>
</div>
</div>
<div class="content">
<div class="action">
<van-cell title="我的资料" is-link to="userInfo" >
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">
<van-icon :name="getAssetsImages('my/my_info.png')" />
</div>
</template>
</van-cell>
<van-cell title="我的借款" is-link to="myLoan" >
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">
<van-icon :name="getAssetsImages('my/my_info1.png')" />
</div>
</template>
</van-cell>
<van-cell title="我的还款" is-link to="myRepayment" >
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">
<van-icon :name="getAssetsImages('my/my_info2.png')" />
</div>
</template>
</van-cell>
<!-- <van-cell title="获取开户权限" is-link to="" >-->
<!-- <template #icon>-->
<!-- <div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">-->
<!-- <van-icon name="/src/assets/images/my/my_info3.png" />-->
<!-- </div>-->
<!-- </template>-->
<!-- </van-cell>-->
<van-cell title="法律责任" is-link @click="go('/agreement', 'lawAgreement')" >
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">
<van-icon :name="getAssetsImages('my/my_info4.png')" />
</div>
</template>
</van-cell>
<!-- <van-cell title="签名" is-link @click="go('/signature', null)" >-->
<!-- <template #icon>-->
<!-- <div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">-->
<!-- <van-icon name="invitation" />-->
<!-- </div>-->
<!-- </template>-->
<!-- </van-cell>-->
<!-- <van-cell title="我的合同" is-link @click="go('/contract', null)" >-->
<!-- <template #icon>-->
<!-- <div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">-->
<!-- <van-icon name="completed" />-->
<!-- </div>-->
<!-- </template>-->
<!-- </van-cell>-->
</div>
<div class="action">
<van-cell title="修改密码" is-link to="uploadPassword" >
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">
<van-icon :name="getAssetsImages('my/my_info5.png')" />
</div>
</template>
</van-cell>
<van-cell title="退出登录" is-link @click="logout" >
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 5px">
<van-icon :name="getAssetsImages('my/my_info66.png')" />
</div>
</template>
</van-cell>
</div>
</div>
</template>
<script setup lang="ts">
import {showConfirmDialog} from "vant";
import {getAssetsImages} from "@/utils";
import {useUserStore} from "@/store/modules/user";
import {useRouter} from "vue-router";
import {onMounted, reactive, ref} from "vue";
import {getCustomerInfo} from "@/api";
import {resetData} from "@/utils/dataUtil";
const userStore = useUserStore()
const router = useRouter()
const headerImage = ref('https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg')
const logout = () => {
showConfirmDialog({
title: '提示',
message: '您确定要退出登录吗',
width: '500px'
})
.then(() => {
userStore.logout().then(res => {
router.push({
path: '/login'
})
})
})
.catch(() => {
// on cancel
});
}
const go = (url, key) => {
// agreement
router.push({
path: url,
query: {
key: key
}
})
}
const customerInfo = reactive({
"account": 0,
"borrowAccount": 0,
"id": 0,
"lastLoginIp": "",
"lastLoginTime": "",
"loansFlag": 0,
"nickName": "",
"phoneNumber": "",
"realNameAuth": 0,
"repaymentAccount": 0,
"status": 0,
"updateTime": "",
"withdrawFlag": 0
})
const _getCustomerInfo = () => {
getCustomerInfo().then(res => {
resetData(customerInfo, res)
})
}
onMounted(() => {
_getCustomerInfo()
})
</script>
<style scoped lang="scss">
.header {
background: #151515;
height: 400px;
display: flex;
justify-content: center;
align-items: end;
overflow: hidden;
&-bj {
background: url("../../../assets/images/my/user_card.png") no-repeat;
background-size: 100% 100%;
height: 330px;
width: 666px;
transform: translateY(80px);
display: flex;
align-items: center;
flex-flow: column;
.header-head {
height: 180px;
width: 180px;
border: 10px solid #f9c947;
border-radius: 100px;
background: #f9c947;
transform: translateY(-90px);
background: var(--bg-image);
background-size: 100% 100%;
}
.header-text {
font-size: 38px;
font-weight: bold;
color: #111a34;
transform: translateY(-60px);
}
}
}
.content {
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
//
}
}
</style>

View File

@@ -0,0 +1,290 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#F9BF3A" />
<div class="content">
<view class="bg-1">
<view class="bg-1-1">
<div class="header-head" :style="{'--bg-image': `url(${headerImage}) no-repeat`}"></div>
<view class="bg-1-1-text">我的贷款{{toRoundMark(customerInfo.borrowAccount)}}</view>
</view>
<view class="accountBalance" style="border-bottom: 1px #fff dashed;">
<view class="yellow_color">账户余额</view>
<view class="money">{{ toRoundMark(customerInfo.account) }}</view>
</view>
<view class="accountBalance">
<view class="yellow_color">待还款金额</view>
<view class="money">{{ toRoundMark(customerInfo.repaymentAccount) }}</view>
</view>
<view class="btn">
<van-button
color="#fff"
round
style="color: #e6a600;width: 80%"
@click.stop="withdrawalBtn"
>
立即提现
</van-button>
</view>
</view>
<view class="footer">
<view class="footer-t gray_color ">
<van-icon :name="getAssetsImages('common/yhd.png')" size="18"/>
账户资金安全由银行保障
</view>
<view class="footer-content">
<view class="footer-content-1">
<van-steps active-icon="success" active-color="#07c160" :active="active">
<van-step v-for="(item, index) in stepBorrow.borrowStep" :key="index">{{ item.name }}</van-step>
</van-steps>
</view>
<view class="footer-content-2">
<view class="footer-content-2-tit gray_color">温馨提示</view>
<view class="footer-content-2-content" :style="{color: stepBorrow.borrowNameStyle || '#e84a10'}">
{{ stepBorrow.borrowRemark }}
</view>
</view>
</view>
</view>
</div>
<van-dialog :width="px2vw(650)" v-model:show="withdrawalShow" title="提现" show-cancel-button @confirm="saveUserInfoBtn">
<van-field v-model="withdrawAmount" type="number" label="提现金额" placeholder="请输入你的提现金额"/>
</van-dialog>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {showConfirmDialog, showToast} from "vant";
import {useUserStore} from "@/store/modules/user";
import {useRouter} from "vue-router";
import {getBorrowWithdraw, getCustomerInfo, getStepBorrow} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {getAssetsImages, px2vw, toRoundMark} from "../../../utils";
const router = useRouter()
const userStore = useUserStore()
const withdrawalShow = ref(false);
const withdrawAmount = ref(0);
const active = ref(0);
const headerImage = ref('https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg')
const go = (key) => {
// agreement
router.push({
path: '/agreement',
query: {
key: key
}
})
}
const customerInfo = reactive({
"account": 0,
"borrowAccount": 0,
"id": 0,
"lastLoginIp": "",
"lastLoginTime": "",
"loansFlag": 0,
"nickName": "",
"phoneNumber": "",
"realNameAuth": 0,
"repaymentAccount": 0,
"status": 0,
"updateTime": "",
"withdrawFlag": 0
})
const _getCustomerInfo = () => {
getCustomerInfo().then(res => {
resetData(customerInfo, res)
})
}
const stepBorrow = reactive({
"borrowNameStyle": "",
"borrowRemark": "",
"borrowStep": [
{
"name": "提交成功",
"over": true
},
{
"name": "银行卡异常",
"over": true
},
{
"name": "到账成功",
"over": false
}
]
})
const _getStepBorrow = () => {
getStepBorrow().then(res => {
resetData(stepBorrow, res)
stepBorrow.borrowStep.forEach((sb, index) => {
if (sb.over) {
active.value = index
}
})
})
}
const withdrawalBtn = () => {
withdrawalShow.value = true
withdrawAmount.value = customerInfo.account
// router.push({
// path: '/loansInfo1',
// query: {
// account: customerInfo.account
// }
// })
}
const saveUserInfoBtn = () => {
// showConfirmDialog({
// title: '提示',
// message: '您确定要提现吗',
// width: '500px'
// })
// .then(() => {
getBorrowWithdraw({withdrawAmount: withdrawAmount.value}).then(res => {
showToast('提现成功')
router.push({
path: '/serveList'
})
})
// })
// .catch(() => {
// // on cancel
// });
}
onMounted(() => {
_getCustomerInfo()
_getStepBorrow()
})
</script>
<style lang="scss" scoped>
.content {
padding: 0 20px;
background-image: linear-gradient(to bottom, #F9BF3A, #ffffff);
height: 100vh;
.bg-1 {
display: block;
background: url("../../../assets/images/common/bg_wallet.png") no-repeat;
background-size: 100% 100%;
padding-top: 100px;
width: 692px;
height: 680px;
margin: 0 auto;
&-1 {
display: flex;
&-text {
color: #111a34;
font-weight: 600;
font-size: 30px;
}
.header-head {
height: 180px;
width: 180px;
border: 10px solid #f9c947;
border-radius: 100px;
background: #f9c947;
transform: translateY(-90px);
background: var(--bg-image);
background-size: 100% 100%;
margin-left: 45px;
margin-right: 30px;
}
}
.accountBalance {
display: flex;
flex-flow: column;
align-items: start;
padding-left: 80px;
padding-top: 30px;
padding-bottom: 30px;
margin: 0 20px;
transform: translateY(-70px);
font-size: 40px;
font-weight: bold;
.yellow_color {
color: #b25700;
}
.money {
padding-top: 10px;
font-size: 56px;
color: #5c2d00;
}
}
.btn {
//padding-top: 100px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
transform: translateY(-70px);
}
}
.footer {
display: flex;
justify-content: center;
align-items: center;
padding-top: 30px;
flex-flow: column;
&-t {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
padding-bottom: 30px;
}
&-content {
width: 100%;
background: #fff;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-1 {
padding: 20px 80px;
//background: #f9fafb;
}
&-2 {
display: flex;
justify-content: space-between;
padding: 20px;
&-tit {
display: block;
width: 220px;
margin-right: 30px;
}
&-content {
display: block;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a" :placeholder="false"/>
<div class="content">
<j-gap height="120" background="#F9BF3A" opacity="0"/>
<div class="action">
<view class="tt">
确认借款信息
</view>
<van-cell title="借款金额" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="toRoundMark(loansInfo.totalLoanMoney)" />
<van-cell title="借款期限" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="loansInfo.totalMonth + '个月'" />
<van-cell title="到账银行" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="userInfo.bankType" />
<van-cell title="收款账号" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="backCardNumDesensitization(userInfo.backCardNum)" />
</div>
<div class="action">
<view class="tt">
借款用途
</view>
<van-field
v-model="loansInfo.noteRemark"
rows="5"
autosize
type="textarea"
placeholder="请输入你的借款用途"
/>
</div>
<div class="xieyi">
<van-checkbox v-model="checked" :icon-size="px2vw(28)" style="align-items: baseline" checked-color="linear-gradient(to right, #F9D88D, #D2A64C)">
我已阅读并同意
<span class="yellow_color1" @click.stop="go('loansAgreement')">借款协议</span>
</van-checkbox>
</div>
<div style="padding: 0 20px 60px">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; "
@click.stop="saveUserInfoBtn"
:disabled="!checked"
>
提交申请
</van-button>
</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {useRoute, useRouter} from "vue-router";
import {px2vw, toRoundMark} from "@/utils";
import {useUserStore} from "@/store/modules/user";
import {getUserInfo, startBorrow} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {showConfirmDialog, showToast} from "vant";
const useUser = useUserStore()
const router = useRouter()
const route = useRoute()
const loansInfo = reactive({
"customerId": useUser.getUserInfo.id,
"noteRemark": "",
"totalLoanMoney": 0,
"totalMonth": 0
})
const checked = ref(false);
const go = (key) => {
// agreement
router.push({
path: '/agreement',
query: {
key: key
}
})
}
const saveUserInfoBtn = () => {
showConfirmDialog({
title: '提示',
message: '您确定要申请贷款吗',
width: '500px'
})
.then(() => {
startBorrow(loansInfo).then(res => {
showToast('申请成功')
router.push({
path: '/serveList'
})
})
})
.catch(() => {
// on cancel
});
}
const userInfo = reactive({
backCardNum: '',
bankType: '',
cardBackPicture: '',
cardFrontPicture: '',
cardNum: '',
companyAddress: '',
companyAddressInfo: '',
companyName: '',
companyPhone: '',
companyTitle: '',
companyYear: '',
customerAddress: '',
customerAddressInfo: '',
customerId: 0,
id: 0,
kinsfolkName: '',
kinsfolkPhone: '',
kinsfolkRef: '',
kinsfolkRefText: '',
realName: ''
})
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
})
}
// 收款账号脱敏
const backCardNumDesensitization = (backCardNum: string) => {
const blen = backCardNum.length
if (blen < 8) {
return backCardNum
}
let str = ''
for (let i = 0; i < blen - 7; i++) {
str+='*'
}
return backCardNum.slice(0, 4) + str + backCardNum.slice(blen-3, blen)
}
onMounted(() => {
loansInfo.totalLoanMoney = route.query.totalLoanMoney
loansInfo.totalMonth = route.query.totalMonth
_getUserInfo()
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
padding-bottom: 30px;
height: 100vh;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
text-align: center;
background: #f6f9fd;
color: #738aa4;
padding: 20px 0;
}
//
}
.xieyi {
padding: 30px 35px;
font-size: 24px;
}
}
.font-22 {
font-size: 22px;
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a" :placeholder="false"/>
<div class="content">
<j-gap height="120" background="#F9BF3A" opacity="0"/>
<div class="action">
<view class="tt">
确认提现信息
</view>
<van-cell title="到账银行" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="userInfo.bankType" />
<van-cell title="收款账号" title-style="color: #8997ae;" style="--van-cell-value-color: #000" :value="backCardNumDesensitization(userInfo.backCardNum)" />
</div>
<div class="action">
<view class="tt">
提现金额
</view>
<van-field
v-model="withdrawAmount"
label="金额"
type="number"
placeholder="请输入你的提现金额"
/>
</div>
<!-- <div class="xieyi">-->
<!-- <van-checkbox v-model="checked" :icon-size="px2vw(28)" style="align-items: baseline" checked-color="linear-gradient(to right, #F9D88D, #D2A64C)">-->
<!-- 我已阅读并同意-->
<!-- <span class="yellow_color1" @click.stop="go('loansAgreement')">借款协议</span>-->
<!-- </van-checkbox>-->
<!-- </div>-->
<div style="padding: 0 20px 60px">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; "
@click.stop="saveUserInfoBtn"
:disabled="!withdrawAmount || withdrawAmount == 0"
>
提现
</van-button>
</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {useRoute, useRouter} from "vue-router";
import {useUserStore} from "@/store/modules/user";
import {getBorrowWithdraw, getUserInfo} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {showConfirmDialog, showToast} from "vant";
const useUser = useUserStore()
const router = useRouter()
const route = useRoute()
const withdrawAmount = ref()
const go = (key) => {
// agreement
router.push({
path: '/agreement',
query: {
key: key
}
})
}
const saveUserInfoBtn = () => {
showConfirmDialog({
title: '提示',
message: '您确定要提现吗',
width: '500px'
})
.then(() => {
getBorrowWithdraw({withdrawAmount: withdrawAmount.value}).then(res => {
showToast('提现成功')
router.push({
path: '/serveList'
})
})
})
.catch(() => {
// on cancel
});
}
const userInfo = reactive({
backCardNum: '',
bankType: '',
cardBackPicture: '',
cardFrontPicture: '',
cardNum: '',
companyAddress: '',
companyAddressInfo: '',
companyName: '',
companyPhone: '',
companyTitle: '',
companyYear: '',
customerAddress: '',
customerAddressInfo: '',
customerId: 0,
id: 0,
kinsfolkName: '',
kinsfolkPhone: '',
kinsfolkRef: '',
kinsfolkRefText: '',
realName: ''
})
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
})
}
// 收款账号脱敏
const backCardNumDesensitization = (backCardNum: string) => {
const blen = backCardNum.length
if (blen < 8) {
return backCardNum
}
let str = ''
for (let i = 0; i < blen - 7; i++) {
str+='*'
}
return backCardNum.slice(0, 4) + str + backCardNum.slice(blen-3, blen)
}
onMounted(() => {
_getUserInfo()
withdrawAmount.value = route.query.account
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
padding-bottom: 30px;
height: 100vh;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
text-align: center;
background: #f6f9fd;
color: #738aa4;
padding: 20px 0;
}
//
}
.xieyi {
padding: 30px 35px;
font-size: 24px;
}
}
.font-22 {
font-size: 22px;
}
</style>

152
src/views/login/index.vue Normal file
View File

@@ -0,0 +1,152 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#ffffff00" :placeholder="false"/>
<div class="content">
<j-gap height="50" background="#F9BF3A" opacity="0"/>
<div class="slogan">
<div class="slogan-1">登录</div>
</div>
<div class="input-box">
<!-- 输入手机号调起手机号键盘 -->
<van-field
v-model="loginData.mobile"
label="账号"
class="login-btn"
placeholder="请输入账号"
label-align="top"
type="tel"
/>
<!-- 允许输入数字调起带符号的纯数字键盘 -->
<van-field
v-model="loginData.password"
label="密码"
placeholder="请输入密码"
class="login-btn"
label-align="top"
type="password"
/>
<van-button
:disabled="!loginData.password || !loginData.mobile"
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; margin: 20px 0"
@click.stop="loginBtn"
>
登录
</van-button>
<view class="op">
<view @click="goRegister" class="register">注册账号</view>
<view @click="goForget" class="forget">忘记密码</view>
</view>
</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {showToast} from "vant";
import {useUserStore} from "@/store/modules/user";
import {useRouter} from "vue-router";
const router = useRouter()
const userStore = useUserStore()
const loginData = reactive({
phone: null,
mobile: '',
// mobile: '15302786929',
mobileCode: null,
password: '',
// password: '123123',
loginRole: 5,
openId: '',
ticket: null,
randStr: null
})
const go = (key) => {
// agreement
router.push({
path: '/agreement',
query: {
key: key
}
})
}
const goRegister = () => {
router.push({
path: '/register'
})
}
const goForget = () => {
router.push({
path: '/forget'
})
}
const loginBtn = () => {
userStore.login(loginData).then(res => {
router.replace('/home')
}, err => {
showToast(err)
})
}
onMounted(() => {
// userStore.setOpenId('omWdJ62bH_6HXLQVOIefzN9J1oi4')
})
</script>
<style lang="scss" scoped>
.content {
padding: 0 20px;
background-image: linear-gradient(to bottom, #F9BF3A, #ffffff, #ffffff, #ffffff, #ffffff);
height: 100vh;
.slogan {
padding-top: 168px;
.slogan-1 {
font-size: 70px;
color: #3f3f3f;
}
.slogan-2 {
font-weight: 400;
font-size: 32px;
color: #333333;
}
}
.input-box {
padding-top: 120px;
.login-btn {
margin-bottom: 40px;
background: #12332100;
}
.password-btn {
border-left: 1px solid #CCCCCC;
padding-left: 20px;
}
}
.op {
display: flex;
justify-content: space-between;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div>
<j-nav-bar />
<div class="contract" v-if="contractHtml" v-html="contractHtml"></div>
<div v-else class="noData">暂无合同</div>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref} from 'vue'
import {getContract} from "@/api";
import {showToast} from "vant";
import {useRoute} from "vue-router";
const route = useRoute()
const contractHtml = ref('')
const _getContract = () => {
if (route.query.tradeNo) {
getContract({tradeNo: route.query.tradeNo}).then(res => {
contractHtml.value = res
})
} else {
showToast('暂无合同')
}
}
onMounted(() => {
_getContract()
})
</script>
<style lang="scss" scoped>
.contract {
//width: 100%;
padding: 20px ;
}
.noData {
height: 300px;
display: flex;
justify-content: center;
align-items: center;
color: #cccccc;
font-size: 50px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a"/>
<div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<view class="j-item" v-for="item in borrowList" :key="item.id" @click="goInfo(item.tradeNo)">
<view class="j-item-t">贷款编号{{item.tradeNo}}</view>
<view style="display: flex; justify-content: space-between">
<view class="j-item-c">
<view>贷款总额{{ item.totalLoanMoney }}</view>
<view class="yellow_color1">每期还款{{ item.avgRepayment }}*{{ item.totalMonth }}</view>
<view>贷款申请日期{{ new Date(item.createTime).format('yyyy-MM-dd hh:mm:ss') }}</view>
</view>
<view :style="{paddingRight: px2vw(20)}" @click.stop="toContract(item.tradeNo)" style="display: flex; justify-content: center; align-items: center; flex-flow: column">
<van-icon size="50" name="orders-o" />
合同
</view>
</view>
</view>
</van-list>
</van-pull-refresh>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {getBorrowPage} from "@/api";
import {useRouter} from "vue-router";
import {px2vw} from "@/utils";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const router = useRouter()
const onLoad = () => {
if (refreshing.value) {
list.value = [];
refreshing.value = false;
}
_getBorrowPage()
loading.value = false;
};
const onRefresh = () => {
// 清空列表数据
finished.value = false;
// 重新加载数据
// 将 loading 设置为 true表示处于加载状态
loading.value = true;
onLoad();
};
const borrowPage = {
pageNum: 0,
pageSize: 100,
}
const borrowList = reactive([])
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
borrowList.newPush(res)
})
}
const goInfo = (tradeNo) => {
router.push({
path: '/borrowInfo',
query: {
tradeNo: tradeNo
}
})
}
const toContract = (tradeNo) => {
router.push({
path: '/contract',
query: {
tradeNo: tradeNo
}
})
}
onMounted(() => {
_getBorrowPage()
})
</script>
<style lang="scss" scoped>
.j-item {
display: flex;
flex-flow: column;
margin: 20px;
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
background: #fff;
overflow: hidden;
&-t {
padding: 20px;
display: flex;
width: 100%;
background: #f9fbff;
color: #738aa4;
}
&-c {
padding: 20px;
display: flex;
width: 100%;
flex-flow: column;
}
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a"/>
<div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<view class="j-item" v-for="item in borrowList" :key="item.id" @click="goInfo(item.tradeNo)">
<view class="j-item-t">贷款编号{{item.tradeNo}}</view>
<view class="j-item-c">
<view>贷款总额{{ item.totalLoanMoney }}</view>
<view class="yellow_color1">每期还款{{ item.avgRepayment }}*{{ item.totalMonth }}</view>
<view>贷款申请日期{{ new Date(item.createTime).format('yyyy-MM-dd hh:mm:ss') }}</view>
</view>
</view>
</van-list>
</van-pull-refresh>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {getBorrowPage} from "@/api";
import {useRouter} from "vue-router";
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const router = useRouter()
const onLoad = () => {
if (refreshing.value) {
list.value = [];
refreshing.value = false;
}
_getBorrowPage()
loading.value = false;
};
const onRefresh = () => {
// 清空列表数据
finished.value = false;
// 重新加载数据
// 将 loading 设置为 true表示处于加载状态
loading.value = true;
onLoad();
};
const borrowPage = {
pageNum: 0,
pageSize: 100,
}
const borrowList = reactive([])
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
borrowList.newPush(res)
})
}
const goInfo = (tradeNo) => {
router.push({
path: '/borrowInfo',
query: {
tradeNo: tradeNo
}
})
}
onMounted(() => {
_getBorrowPage()
})
</script>
<style lang="scss" scoped>
.j-item {
display: flex;
flex-flow: column;
margin: 20px;
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
background: #fff;
overflow: hidden;
&-t {
padding: 20px;
display: flex;
width: 100%;
background: #f9fbff;
color: #738aa4;
}
&-c {
padding: 20px;
display: flex;
width: 100%;
flex-flow: column;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div>
<j-nav-bar v-if="!useEdit" />
<van-signature v-if="!useEdit" @submit="onSubmit" @clear="onClear" />
<div class="my-signature">我的签名</div>
<van-image class="my-signature-image" v-if="userInfo.signature" :src="userInfo.signature" />
</div>
</template>
<script lang="ts" setup>
import {reactive, ref, onMounted} from 'vue';
import {showToast} from "vant";
import {useRouter} from "vue-router";
import {getBorrowPage, getUserInfo, updateCustomerCard} from "@/api";
import {resetData} from "@/utils/dataUtil";
const image = ref('');
const onSubmit = (data) => {
userInfo.signature = data.image;
saveUserInfoBtn()
};
const borrowPage = {
pageNum: 0,
pageSize: 3,
}
const useEdit = ref(true)
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
useEdit.value = res.length !== 0;
})
}
const onClear = () => showToast('clear');
const userInfo = reactive({
signature: ''
})
const router = useRouter()
const saveUserInfoBtn = () => {
updateCustomerCard(userInfo).then(res => {
console.log('')
router.back()
})
}
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
})
}
onMounted(() => {
_getUserInfo()
_getBorrowPage()
})
</script>
<style lang="scss" scoped>
.my-signature {
padding: 20px;
}
.my-signature-image {
margin: 20px;
background: #fff;
border-radius: 10px;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#ffffff00" :placeholder="false"/>
<div class="content">
<j-gap height="120" background="#F9BF3A" opacity="0"/>
<van-cell-group inset>
<van-cell :style="{ padding: px2vw(40) + ' ' + px2vw(35)}" title="我的资料" is-link to="userInfo1" :value="userInfo.cardFlag ? '完整' : '不完整'">
<template #title>
<div class="t">身份信息</div>
<div class="t2">*让我们了解您的资料信息</div>
</template>
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 12px">
<van-icon :name="getAssetsImages('my/uuuu1.png')" size="45"/>
</div>
</template>
</van-cell>
<van-cell :style="{ padding: px2vw(40) + ' ' + px2vw(35)}" title="我的借款" is-link to="userInfo2" :value="userInfo.infoFlag ? '完整' : '不完整'">
<template #title>
<div class="t">资料信息</div>
<div class="t2">*让我们了解您的资料信息</div>
</template>
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 12px">
<van-icon :name="getAssetsImages('my/uuuu2.png')" size="45"/>
</div>
</template>
</van-cell>
<van-cell :style="{ padding: px2vw(40) + ' ' + px2vw(35)}" title="我的还款" is-link to="userInfo3" :value="userInfo.bankFlag ? '完整' : '不完整'">
<template #title>
<div class="t">收款银行卡</div>
<div class="t2">*让我们了解您的资料信息</div>
</template>
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 12px">
<van-icon :name="getAssetsImages('my/uuuu3.png')" size="45"/>
</div>
</template>
</van-cell>
<van-cell v-if="userInfo.allowSignature" :style="{ padding: px2vw(40) + ' ' + px2vw(35)}" title="我的借款" is-link to="signature" :value="userInfo.signatureFlag ? '完整' : '不完整'" >
<template #title>
<div class="t">签名信息</div>
<div class="t2">*让我们了解您的资料信息</div>
</template>
<template #icon>
<div style="display: flex; justify-content: center; align-items: center; padding-right: 12px">
<van-icon :name="getAssetsImages('my/uuuu2.png')" size="45"/>
</div>
</template>
</van-cell>
</van-cell-group>
<div style="padding: 60px 20px 60px">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; "
@click.stop="goHome"
>
立即借款
</van-button>
</div>
</div>
</template>
<script lang="ts" setup>
import {getAssetsImages, px2vw} from "@/utils";
import {useRouter} from "vue-router";
import {getBorrowPage, getUserInfo} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {onMounted, reactive} from "vue";
const router = useRouter()
const goHome = () => {
router.push({
path: '/home'
})
}
const userInfo = reactive({
cardFlag: false,
infoFlag: false,
bankFlag: false,
signatureFlag: false,
allowSignature: true
})
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
userInfo.signatureFlag = !!res.signature
})
}
const borrowPage = {
pageNum: 0,
pageSize: 100,
}
const borrowList = reactive([])
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
borrowList.newPush(res)
})
}
onMounted(() => {
_getUserInfo()
_getBorrowPage()
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
height: 100vh;
.t {
font-size: 36px;
font-weight: 600;
color: #111a34;
margin-bottom: 4px;
}
.t2 {
margin-top: 4px;
font-size: 24px;
color: #858b9c;
}
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a" :placeholder="false"/>
<van-form @submit="saveUserInfoBtn">
<div class="content">
<j-gap height="120" background="#F9BF3A" opacity="0"/>
<div class="action">
<view class="tt">填写真实有效的信息审核才会通过哦~</view>
<van-field required v-model="userInfo.realName" type="text" label="姓名" placeholder="请输入真实姓名" :rules="[{ required: true, message: '请输入真实姓名' }]"/>
<van-field required v-model="userInfo.cardNum" type="text" label="身份证号" placeholder="请输入真实身份证号" :rules="[{ required: true, message: '请输入真实身份证号' }]"/>
<!-- <van-field required label="身份证号" placeholder="请输入真实身份证号" v-model="userInfo.cardNum" readonly clickable @touchstart.stop="idCardKeyboardShow = true" :rules="[{ required: true, message: '请输入真实身份证号' }]"/>-->
<!-- <van-number-keyboard-->
<!-- :show="idCardKeyboardShow"-->
<!-- v-model="userInfo.cardNum"-->
<!-- extra-key="X"-->
<!-- close-button-text="完成"-->
<!-- @blur="idCardKeyboardShow = false"-->
<!-- />-->
</div>
<div class="action">
<view class="tt">
<view>*需本人身份证且内容清晰可辨</view>
<view>请您确认拍照权限已开启</view>
</view>
<view class="action-content">
<view class="action-content-item">
<van-uploader :rules="[{ required: true, message: '点击上传身份证人像面' }]" required :after-read="afterReadIdFront" :before-read="beforeRead" :max-count="1">
<div class="id-card-box">
<van-image v-if="userInfo.cardBackPicture" :height="px2vw(156)" :src="userInfo.cardBackPicture"
:width="px2vw(236)"/>
<van-image v-if="!userInfo.cardBackPicture" :height="px2vw(156)" :src="getAssetsImages('my/idcard1.png')"
:width="px2vw(236)"/>
<div :style="{fontSize: px2vw(26)}" style="color: #858B9C; width: 100%; text-align: center; padding-top: 10px">
点击上传身份证人像面
</div>
</div>
</van-uploader>
</view>
<view class="action-content-item">
<van-uploader :rules="[{ required: true, message: '点击上传身份证国徽面' }]" required :after-read="afterReadIdBack" :before-read="beforeRead" :max-count="1">
<div class="id-card-box">
<van-image v-if="userInfo.cardFrontPicture" :height="px2vw(156)" :src="userInfo.cardFrontPicture"
:width="px2vw(236)"/>
<van-image v-if="!userInfo.cardFrontPicture" :height="px2vw(156)" :src="getAssetsImages('my/idcard1.png')"
:width="px2vw(236)"/>
<div :style="{fontSize: px2vw(26)}" style="color: #858B9C; width: 100%; text-align: center; padding-top: 10px">
点击上传身份证国徽面
</div>
</div>
</van-uploader>
</view>
<view class="action-content-item">
<van-uploader :rules="[{ required: true, message: '点击上传手持身份证照' }]" required :after-read="afterReadAvatar" :before-read="beforeRead" :max-count="1">
<div class="id-card-box">
<van-image v-if="userInfo.handCardPicture" :height="px2vw(156)" :src="userInfo.handCardPicture"
:width="px2vw(236)"/>
<van-image v-if="!userInfo.handCardPicture" :height="px2vw(156)" :src="getAssetsImages('my/idcard1.png')"
:width="px2vw(236)"/>
<div :style="{fontSize: px2vw(26)}" style="color: #858B9C; width: 100%; text-align: center; padding-top: 10px">
点击上传手持身份证照
</div>
</div>
</van-uploader>
</view>
<view style="width: 100%; padding: 20px 0">
<view>拍摄要求</view>
<view style="display: flex; justify-content: space-between; align-items: center">
<view style="display: flex; justify-content: center; align-items: center; flex-flow: column">
<van-image :height="px2vw(92)" :width="px2vw(148)" :src="getAssetsImages('my/idcard2.png')"/>
<view class="gray_color font-22">标准拍摄</view>
</view>
<view style="display: flex; justify-content: center; align-items: center; flex-flow: column">
<van-image :height="px2vw(92)" :width="px2vw(148)" :src="getAssetsImages('my/idcard3.png')"/>
<view class="gray_color font-22">×边框缺失</view>
</view>
<view style="display: flex; justify-content: center; align-items: center; flex-flow: column">
<van-image :height="px2vw(92)" :width="px2vw(148)" :src="getAssetsImages('my/idcard4.png')"/>
<view class="gray_color font-22">×照片模糊</view>
</view>
<view style="display: flex; justify-content: center; align-items: center; flex-flow: column">
<van-image :height="px2vw(92)" :width="px2vw(148)" :src="getAssetsImages('my/idcard5.png')"/>
<view class="gray_color font-22">×闪光强烈</view>
</view>
</view>
</view>
</view>
</div>
</div>
<div style="padding: 0 20px 60px">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; "
native-type="submit"
:disabled="!useEdit || !(userInfo.realName && userInfo.cardNum)"
>
提交
</van-button>
</div>
</van-form>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {getBorrowPage, getUserInfo, updateCustomerCard, uploadCommon} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {getAssetsImages, px2vw} from "@/utils";
import {showToast} from "vant";
import {useRouter} from "vue-router";
const idCardKeyboardShow = ref(false)
const userInfo = reactive({
backCardNum: '',
bankType: '',
cardBackPicture: '',
cardFrontPicture: '',
handCardPicture: '',
cardNum: '',
companyAddress: '',
companyAddressInfo: '',
companyName: '',
companyPhone: '',
companyTitle: '',
companyYear: '',
customerAddress: '',
customerAddressInfo: '',
customerId: 0,
id: 0,
kinsfolkName: '',
kinsfolkPhone: '',
kinsfolkRef: '',
realName: ''
})
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
})
}
const borrowPage = {
pageNum: 0,
pageSize: 3,
}
const useEdit = ref(true)
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
useEdit.value = res.length === 0;
})
}
const beforeRead = (file) => {
if (file.type !== 'image/jpeg' && file.type !== 'image/png') {
showToast('请上传 jpg 或者 png 格式图片');
return false;
}
return true;
};
const afterReadAvatar = (file) => {
// 此时可以自行将文件上传至服务器
upLoadImage(file, 'handCardPicture')
};
const afterReadIdFront = (file) => {
upLoadImage(file, 'cardBackPicture')
};
const afterReadIdBack = (file) => {
upLoadImage(file, 'cardFrontPicture')
};
const upLoadImage = (file, flag) => {
uploadCommon(file.file).then(res => {
if (res.data.code == 200) {
userInfo[flag] = res.data.data.url
} else {
showToast('上传失败');
}
})
}
const router = useRouter()
const saveUserInfoBtn = () => {
updateCustomerCard(userInfo).then(res => {
console.log('')
router.back()
})
}
onMounted(() => {
_getBorrowPage()
_getUserInfo()
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
padding-bottom: 30px;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
text-align: center;
background: #f6f9fd;
color: #738aa4;
padding: 20px 0;
}
//
}
}
.font-22 {
font-size: 22px;
}
</style>

View File

@@ -0,0 +1,261 @@
<template>
<j-nav-bar color="#FFF" nav-bar-background="#f9bf3a" :placeholder="false"/>
<van-form @submit="saveUserInfoBtn">
<div class="content">
<j-gap height="120" background="#F9BF3A" opacity="0"/>
<div class="action">
<view class="tt">填写真实有效的信息审核才会通过哦~</view>
<!-- <van-field required v-model="userInfo.realName" type="text" label="姓名" placeholder="请输入真实姓名" :rules="[{ required: true, message: '请输入真实姓名' }]"/>-->
<van-field required v-model="userInfo.companyName" type="text" label="单位名称" placeholder="请输入单位名称" :rules="[{ required: true, message: '请输入单位名称' }]"/>
<van-field required v-model="userInfo.companyTitle" type="text" label="职位" placeholder="请输入职位" :rules="[{ required: true, message: '请输入职位' }]"/>
<van-field required v-model="userInfo.companyPhone" type="tel" label="单位电话" placeholder="号码加区号(非必填)" :rules="[{ required: true, message: '号码加区号' }]"/>
<van-field required v-model="userInfo.companyYear" type="number" label="工作年龄" placeholder="请输入工龄" :rules="[{ required: true, message: '请输入工龄' }]"/>
<van-field required v-model="userInfo.incomeWan" type="number" label="月薪" placeholder="请输入月薪" :rules="[{ required: true, message: '请输入月薪' }]"/>
<van-field
v-model="userInfo.companyAddress"
is-link
required
readonly
:rules="[{ required: true, message: '请选择省市区' }]"
label="单位地址"
placeholder="请选择省市区"
@click="companyAddressShow = true"
/>
<van-popup v-model:show="companyAddressShow" round position="bottom">
<van-cascader
v-model="cascaderValue"
title="请选择所在地区"
:options="options"
@close="companyAddressShow = false"
@finish="onFinish"
/>
</van-popup>
<van-field required v-model="userInfo.companyAddressInfo" type="text" label="详细地址" placeholder="请输入详细地址" :rules="[{ required: true, message: '请输入详细地址' }]"/>
<van-field
v-model="userInfo.customerAddress"
is-link
required
readonly
:rules="[{ required: true, message: '请选择所在地区' }]"
label="现居住地址"
placeholder="请选择所在地区"
@click="customerAddressShow = true"
/>
<van-popup v-model:show="customerAddressShow" round position="bottom">
<van-cascader
v-model="customerAddressValue"
title="请选择所在地区"
:options="options"
@close="customerAddressShow = false"
@finish="onCustomerAddressFinish"
/>
</van-popup>
<van-field required v-model="userInfo.customerAddressInfo" type="text" label="详细地址" placeholder="例东北石油大学启智寝室楼2A603" :rules="[{ required: true, message: '请输入详细地址' }]"/>
</div>
<div class="action">
<view class="tt">
直系亲属联系人
</view>
<van-field required v-model="userInfo.kinsfolkName" type="text" label="姓名" placeholder="请输入真实姓名" :rules="[{ required: true, message: '请输入真实姓名' }]"/>
<van-field required v-model="userInfo.kinsfolkPhone" type="tel" label="手机号码" placeholder="请输入手机号码" :rules="[{ required: true, message: '请输入手机号码' }]"/>
<van-field
v-model="userInfo.kinsfolkRefText"
is-link
:rules="[{ required: true, message: '请选择关系' }]"
required
readonly
label="关系"
placeholder="请选择关系"
@click="showPicker = true"
/>
<van-popup v-model:show="showPicker" round position="bottom">
<van-picker
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
</div>
</div>
<div style="padding: 0 20px 60px">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; "
native-type="submit"
:disabled="!useEdit"
>
提交
</van-button>
</div>
</van-form>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {getBorrowPage, getUserInfo, updateCustomerCard} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {showToast} from "vant";
import {useRouter} from "vue-router";
import {useCascaderAreaData} from "@vant/area-data";
const companyAddressShow = ref(false)
const customerAddressShow = ref(false)
const cascaderValue = ref('');
const customerAddressValue = ref('');
const columns = [
{ text: '父母', value: '1' },
{ text: '配偶', value: '2' },
{ text: '子女', value: '3' },
{ text: '祖父母', value: '4' },
];
const userInfo = reactive({
backCardNum: '',
incomeWan: 0,
bankType: '',
cardBackPicture: '',
cardFrontPicture: '',
cardNum: '',
companyAddress: '',
companyAddressInfo: '',
companyName: '',
companyPhone: '',
companyTitle: '',
companyYear: '',
customerAddress: '',
customerAddressInfo: '',
customerId: 0,
id: 0,
kinsfolkName: '',
kinsfolkPhone: '',
kinsfolkRef: '',
kinsfolkRefText: '',
realName: ''
})
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
columns.forEach(c => {
if (c.value == userInfo.kinsfolkRef) {
userInfo.kinsfolkRefText = c.text
}
})
})
}
const borrowPage = {
pageNum: 0,
pageSize: 3,
}
const useEdit = ref(true)
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
useEdit.value = res.length === 0;
})
}
const options = useCascaderAreaData();
const onFinish = ({ selectedOptions }) => {
companyAddressShow.value = false;
userInfo.companyAddress = selectedOptions.map((option) => option.text).join('/');
};
const onCustomerAddressFinish = ({ selectedOptions }) => {
customerAddressShow.value = false;
userInfo.customerAddress = selectedOptions.map((option) => option.text).join('/');
};
const showPicker = ref(false);
const onConfirm = ({ selectedOptions }) => {
showPicker.value = false;
userInfo.kinsfolkRef = selectedOptions[0].value;
userInfo.kinsfolkRefText = selectedOptions[0].text;
};
const router = useRouter()
const saveUserInfoBtn = (values) => {
updateCustomerCard(userInfo).then(res => {
router.back()
})
}
onMounted(() => {
_getUserInfo()
_getBorrowPage()
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
padding-bottom: 30px;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
text-align: center;
background: #f6f9fd;
color: #738aa4;
padding: 20px 0;
}
//
}
}
.font-22 {
font-size: 22px;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<j-nav-bar :placeholder="false" color="#FFF" nav-bar-background="#f9bf3a"/>
<van-form @submit="saveUserInfoBtn">
<div class="content">
<j-gap background="#F9BF3A" height="120" opacity="0"/>
<div class="action">
<view class="tt">填写真实有效的信息审核才会通过哦~</view>
<van-field
v-model="userInfo.bankType"
:rules="[{ required: true, message: '请选择开户银行' }]"
label="开户银行"
placeholder="请输入开户银行"
:readonly="!useEdit"
required
/>
<!-- <van-popup v-model:show="showPicker" position="bottom" round>
<van-picker
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>-->
<van-field :readonly="!useEdit" @paste="pasteField" @copy="copyField" v-model="userInfo.backCardNum" :rules="[{ required: true, message: '请输入银行卡号' }]" label="银行卡号" placeholder="请输入银行卡号" required
type="digit"/>
</div>
</div>
<div style="padding: 0 20px 60px">
<van-button
color="linear-gradient(to right, #D2A64C, #F9D88D)"
native-type="submit"
round
style="width: 100%; "
:disabled="!useEdit"
>
提交
</van-button>
</div>
</van-form>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {getBankType, getBorrowPage, getUserInfo, updateCustomerCard, uploadCommon} from "@/api";
import {resetData} from "@/utils/dataUtil";
import {showToast} from "vant";
import {useRouter} from "vue-router";
const userInfo = reactive({
backCardNum: '',
bankType: '',
cardBackPicture: '',
cardFrontPicture: '',
cardNum: '',
companyAddress: '',
companyAddressInfo: '',
companyName: '',
companyPhone: '',
companyTitle: '',
companyYear: '',
customerAddress: '',
customerAddressInfo: '',
customerId: 0,
id: 0,
kinsfolkName: '',
kinsfolkPhone: '',
kinsfolkRef: '',
realName: ''
})
const borrowPage = {
pageNum: 0,
pageSize: 3,
}
const useEdit = ref(true)
const _getBorrowPage = () => {
getBorrowPage(borrowPage).then(res => {
useEdit.value = res.length === 0;
})
}
const _getUserInfo = () => {
getUserInfo().then(res => {
resetData(userInfo, res)
})
}
const bankTypeList = reactive([])
const columns: any[] = reactive([])
const _getBankType = () => {
getBankType().then(res => {
bankTypeList.newPush(res)
bankTypeList.forEach(btl => {
columns.push({
value: btl,
text: btl
})
})
})
}
const showPicker = ref(false);
const onConfirm = ({selectedOptions}) => {
showPicker.value = false;
userInfo.bankType = selectedOptions[0].value;
};
const pasteField = (e) => {
e.preventDefault()
}
const copyField = (e) => {
e.preventDefault()
}
const router = useRouter()
const saveUserInfoBtn = () => {
updateCustomerCard(userInfo).then(res => {
console.log('')
router.back()
})
}
onMounted(() => {
_getUserInfo()
// _getBankType()
_getBorrowPage()
})
</script>
<style lang="scss" scoped>
.content {
background-image: linear-gradient(to bottom, #f9bf3a, #ffffff) !important;
padding-bottom: 30px;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
text-align: center;
background: #f6f9fd;
color: #738aa4;
padding: 20px 0;
}
//
}
}
.font-22 {
font-size: 22px;
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<j-nav-bar :placeholder="false" color="#FFF" nav-bar-background="#ffffff00"/>
<div class="content">
<j-gap height="50" background="#F9BF3A" opacity="0"/>
<div class="slogan">
<div class="slogan-1">注册</div>
</div>
<div class="input-box">
<!-- 允许输入数字调起带符号的纯数字键盘 -->
<van-field
v-model="loginData.phoneNumber"
class="login-btn"
label="手机号码"
placeholder="请输入手机号码"
type="tel"
/>
<van-field
v-model="loginData.code"
class="login-btn"
label="验证码"
placeholder="请输入验证码"
type="number"
>
<template #button>
<div class="password-btn">
<van-count-down v-show="countDownFlag" ref="countDown" :auto-start="false" :time="time" @finish="onFinish">
<template #default="timeData">
<span class="block">{{ timeData.seconds }}</span>
</template>
</van-count-down>
<div v-show="!countDownFlag" style="color: #bc7c1c" @click="start">发送验证码</div>
</div>
</template>
</van-field>
<!-- 允许输入数字调起带符号的纯数字键盘 -->
<van-field
v-model="loginData.password"
class="login-btn"
label="登录密码"
placeholder="请设置6-16位密码"
type="text"
/>
</div>
<van-button
:disabled="!loginData.password || !loginData.phoneNumber"
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; margin: 20px 0"
@click.stop="registerBtn"
>
注册
</van-button>
<view class="op">
<view class="register" @click="goLogin">登录</view>
</view>
</div>
</template>
<script lang="ts" setup>
import {reactive, ref} from "vue";
import {showToast} from "vant";
import {useRouter} from "vue-router";
import {register, sendSmsRegister} from "@/api/login";
import {useUserStore} from "@/store/modules/user";
const router = useRouter()
const userStore = useUserStore()
const loginData = reactive({
phone: null,
phoneNumber: '',
code: null,
password: '',
loginRole: 5,
openId: '',
ticket: null,
randStr: null
})
const time = ref(60 * 1000);
const countDown = ref();
const countDownFlag = ref(false);
const start = () => {
if (loginData.phoneNumber) {
countDown.value.start();
countDownFlag.value = true
sendSmsRegister({
phoneNumber: loginData.phoneNumber
}).then(res => {
console.log(res)
loginData.code = res
onFinish()
})
} else {
showToast('请输入手机号')
}
};
const registerBtn = () => {
register(loginData).then(res => {
// goLogin()
userStore.login({
mobile: loginData.phoneNumber,
password: loginData.password
}).then(res => {
router.replace('/home')
}, err => {
router.replace('/home')
})
})
}
const goLogin = () => {
router.back()
}
const onFinish = () => {
countDownFlag.value = false
countDown.value.reset();
}
</script>
<style lang="scss" scoped>
.content {
padding: 0 20px;
background-image: linear-gradient(to bottom, #F9BF3A, #ffffff, #ffffff, #ffffff, #ffffff);
height: 100vh;
.slogan {
padding-top: 168px;
.slogan-1 {
font-size: 70px;
color: #3f3f3f;
}
.slogan-2 {
font-weight: 400;
font-size: 32px;
color: #333333;
}
}
.input-box {
margin-top: 40px;
padding: 20px 0 10px 0;
background: #fff;
border-radius: 20px;
box-shadow: 0px 0px 7px -3px #b4b3b3;
.login-btn {
margin-bottom: 40px;
background: #12332100;
}
.password-btn {
border-left: 1px solid #CCCCCC;
padding-left: 20px;
}
}
.op {
display: flex;
justify-content: space-between;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,221 @@
<template>
<j-nav-bar :placeholder="false" color="#FFF" nav-bar-background="#F9BF3A00"/>
<div class="content">
<j-gap height="50" background="#F9BF3A" opacity="0"/>
<j-gap height="120" opacity="1"/>
<view class="action">
<van-field
v-model="loginData.phoneNumber"
disabled
class="login-btn"
label="手机号码"
placeholder="请输入手机号码"
label-align="top"
style="background: #12332100"
type="tel"
/>
<van-field
v-model="loginData.code"
class="login-btn"
label="验证码"
placeholder="请输入验证码"
label-align="top"
style="background: #12332100"
type="number"
>
<template #button>
<div class="password-btn">
<van-count-down v-show="countDownFlag" ref="countDown" :auto-start="false" :time="time" @finish="onFinish">
<template #default="timeData">
<span class="block">{{ timeData.seconds }}</span>
</template>
</van-count-down>
<div v-show="!countDownFlag" style="color: #bc7c1c" @click="start">发送验证码</div>
</div>
</template>
</van-field>
<van-field
v-model="loginData.password"
class="login-btn"
label="登录密码"
label-align="top"
placeholder="请设置6-16位密码"
style="background: #12332100"
type="password"
/>
<van-field
v-model="loginData.confirmPassword"
class="login-btn"
label="确认密码"
label-align="top"
placeholder="请再次输入密码"
style="background: #12332100"
type="password"
/>
<view style="padding: 20px">
<van-button
:disabled="!loginData.password || !loginData.confirmPassword"
color="linear-gradient(to right, #D2A64C, #F9D88D)"
round
style="width: 100%; margin: 20px 0"
@click.stop="updatePwdBtn"
>
确认修改
</van-button>
</view>
</view>
</div>
</template>
<script lang="ts" setup>
import {onMounted, reactive, ref} from "vue";
import {sendSmsForget, updatePwd} from "@/api/login";
import {showToast} from "vant";
import {getCustomerInfo} from "@/api";
import {resetData} from "@/utils/dataUtil";
const loginData = reactive({
phone: null,
phoneNumber: '',
code: null,
password: '',
confirmPassword: '',
checkCode: '',
})
const flag = ref('1')
const time = ref(60 * 1000);
const countDown = ref();
const countDownFlag = ref(false);
const start = () => {
if (loginData.phoneNumber) {
countDown.value.start();
countDownFlag.value = true
sendSmsForget({
phoneNumber: loginData.phoneNumber
}).then(res => {
console.log(res)
loginData.code = res
loginData.checkCode = res
onFinish()
})
} else {
showToast('请输入手机号')
}
};
const next = () => {
flag.value = '2'
}
const updatePwdBtn = () => {
updatePwd({
checkCode: loginData.checkCode,
confirmPassword: loginData.confirmPassword,
password: loginData.password
}).then(res => {
console.log(res)
showToast('修改成功')
})
}
const customerInfo = reactive({
"account": 0,
"borrowAccount": 0,
"id": 0,
"lastLoginIp": "",
"lastLoginTime": "",
"loansFlag": 0,
"nickName": "",
"phoneNumber": "",
"realNameAuth": 0,
"repaymentAccount": 0,
"status": 0,
"updateTime": "",
"withdrawFlag": 0
})
const _getCustomerInfo = () => {
getCustomerInfo().then(res => {
resetData(customerInfo, res)
loginData.phoneNumber = customerInfo.phoneNumber
})
}
const onFinish = () => {
countDownFlag.value = false
countDown.value.reset();
}
onMounted(() => {
_getCustomerInfo()
})
</script>
<style lang="scss" scoped>
.content {
padding: 0 10px;
background-image: linear-gradient(to bottom, #F9BF3A, #ffffff, #ffffff, #ffffff, #ffffff);
height: 100vh;
.action {
margin: 20px;
border-radius: 20px;
overflow: hidden;
background: #fff;
display: flex;
flex-flow: column;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-content {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding: 20px 20px;
box-shadow: 0 0 1vw 0px #e0e0e0;
&-item {
margin: 20px 0;
}
.id-card-box {
border-radius: 10px;
box-shadow: 0 0 1vw 0px #e0e0e0;
width: 500px;
height: 300px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
}
.tt {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
text-align: center;
background: #f6f9fd;
color: #738aa4;
padding: 20px 0;
}
//
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More