feat: add new login page. #1

Merged
huqi merged 4 commits from dev_1.0.0 into main 6 months ago

@ -39,6 +39,16 @@ src -- 源码目录
- 微信开发者工具开启服务端口:`设置->安全设置>服务端口开启`
- 在`HBuilder X`中使用`运行->运行到小程序模拟器->微信开发者工具`运行项目,运行成功后会自动打开微信开发者工具并编译项目
## 开发须知
- 登录相关
* 接口地址39.103.180.196:9012
* 接口文档:[luoo-user API](http://39.103.180.196:9012/doc.html?open_in_browser=true)
- 商城相关
* 接口地址43.248.137.154:8085
* 接口文档:[mall前台系统](http://43.248.137.154:8085/swagger-ui/index.html)
## 许可证

@ -17,3 +17,93 @@ export function memberInfo() {
url: '/sso/info'
})
}
/*
* 获取支持的手机号国家码
* */
export function supportedCountryCode() {
return request({
requestBase:'USER', // 携带 USER 模块独有的配置
method: 'GET',
url: '/user/user/supportedCountryCode'
})
}
/*
* 发送短信验证码
* @data mobile 手机号 (必填)
* @data deviceId 设备id (必填)
* @data countryCode 国家码默认为+86
* @data imageCheckCode 图形验证码
**/
export function sendsms(data) {
return request({
requestBase:'USER', // 携带 USER 模块独有的配置
header: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
method: 'POST',
url: '/user/user/sendsms',
data: data
})
}
/*
* 登录/注册
* @data mobile 手机号 (必填)
* @data deviceId 设备id (必填)
* @data mobileCheckCode 6位验证码 (必填)
* @data deviceBrand 设备品牌
**/
export function appLogin(data) {
return request({
requestBase:'USER', // 携带 USER 模块独有的配置
header: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
method: 'POST',
url: '/user/user/appLogin',
data: data
})
}
/*
* 微信wxId登录/注册
* @data code code码 (必填)
* @data deviceId 设备id (必填)
* @data mobile 手机号
* @data mobileCheckCode 6位验证码
* @data deviceBrand 设备品牌
**/
export function wxIdLogin(data) {
return request({
requestBase:'USER', // 携带 USER 模块独有的配置
method: 'POST',
url: '/user/user/wxIdLogin',
data: data
})
}
/*
* 退出登录
* @hearder Authorization
**/
export function logout() {
return request({
requestBase:'USER', // 携带 USER 模块独有的配置
method: 'POST',
url: '/user/user/logout'
})
}
/*
* 获取个人信息
* */
export function getUserInfo() {
return request({
requestBase:'USER', // 携带 USER 模块独有的配置
method: 'GET',
url: '/user/my/userInfo'
})
}

@ -0,0 +1,96 @@
const codeInput = {
adjustPosition: true,
maxlength: 6,
dot: false,
mode: 'box',
hairline: false,
space: 10,
value: '',
focus: false,
bold: false,
color: '#606266',
fontSize: 18,
size: 35,
disabledKeyboard: false,
borderColor: '#c9cacc',
disabledDot: true
}
export default {
props: {
// 键盘弹起时,是否自动上推页面
adjustPosition: {
type: Boolean,
default: codeInput.adjustPosition
},
// 最大输入长度
maxlength: {
type: [String, Number],
default: codeInput.maxlength
},
// 是否用圆点填充
dot: {
type: Boolean,
default: codeInput.dot
},
// 显示模式box-盒子模式line-底部横线模式
mode: {
type: String,
default: codeInput.mode
},
// 是否细边框
hairline: {
type: Boolean,
default: codeInput.hairline
},
// 字符间的距离
space: {
type: [String, Number],
default: codeInput.space
},
// 预置值
value: {
type: [String, Number],
default: codeInput.value
},
// 是否自动获取焦点
focus: {
type: Boolean,
default: codeInput.focus
},
// 字体是否加粗
bold: {
type: Boolean,
default: codeInput.bold
},
// 字体颜色
color: {
type: String,
default: codeInput.color
},
// 字体大小
fontSize: {
type: [String, Number],
default: codeInput.fontSize
},
// 输入框的大小,宽等于高
size: {
type: [String, Number],
default: codeInput.size
},
// 是否隐藏原生键盘如果想用自定义键盘的话需设置此参数为true
disabledKeyboard: {
type: Boolean,
default: codeInput.disabledKeyboard
},
// 边框和线条颜色
borderColor: {
type: String,
default: codeInput.borderColor
},
// 是否禁止输入"."符号
disabledDot: {
type: Boolean,
default: codeInput.disabledDot
}
}
}

@ -0,0 +1,264 @@
<template>
<view class="u-code-input">
<view
class="u-code-input__item"
:style="[itemStyle(index)]"
v-for="(item, index) in codeLength"
:key="index"
>
<view
class="u-code-input__item__dot"
v-if="dot && codeArray.length > index"
></view>
<text
v-else
:style="{
fontSize: addUnit(fontSize),
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
>{{codeArray[index]}}</text>
<view
class="u-code-input__item__line"
v-if="mode === 'line'"
:style="[lineStyle]"
></view>
<!-- #ifndef APP-PLUS -->
<view v-if="isFocus && codeArray.length === index" :style="{backgroundColor: color}" class="u-code-input__item__cursor"></view>
<!-- #endif -->
</view>
<input
:disabled="disabledKeyboard"
type="number"
:focus="focus"
:value="inputValue"
:maxlength="maxlength"
:adjustPosition="adjustPosition"
class="u-code-input__input"
@input="inputHandler"
:style="{
height: addUnit(size)
}"
@focus="isFocus = true"
@blur="isFocus = false"
/>
</view>
</template>
<script>
import { addUnit, getPx } from '@/utils/index.js'
import props from './props.js';
/**
* CodeInput 验证码输入
* @description 该组件一般用于验证用户短信验证码的场景也可以结合uView的键盘组件使用
* @tutorial https://www.uviewui.com/components/codeInput.html
* @property {String | Number} maxlength 最大输入长度 默认 6
* @property {Boolean} dot 是否用圆点填充 默认 false
* @property {String} mode 显示模式box-盒子模式line-底部横线模式 默认 'box'
* @property {Boolean} hairline 是否细边框 默认 false
* @property {String | Number} space 字符间的距离 默认 10
* @property {String | Number} value 预置值
* @property {Boolean} focus 是否自动获取焦点 默认 false
* @property {Boolean} bold 字体和输入横线是否加粗 默认 false
* @property {String} color 字体颜色 默认 '#606266'
* @property {String | Number} fontSize 字体大小单位px 默认 18
* @property {String | Number} size 输入框的大小宽等于高 默认 35
* @property {Boolean} disabledKeyboard 是否隐藏原生键盘如果想用自定义键盘的话需设置此参数为true 默认 false
* @property {String} borderColor 边框和线条颜色 默认 '#c9cacc'
* @property {Boolean} disabledDot 是否禁止输入"."符号 默认 true
*
* @event {Function} change 输入内容发生改变时触发具体见上方说明 value当前输入的值
* @event {Function} finish 输入字符个数达maxlength值时触发见上方说明 value当前输入的值
* @example <u-code-input v-model="value4" :focus="true"></u-code-input>
*/
export default {
name: 'uni-code-input',
// mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
mixins: [props],
data() {
return {
inputValue: '',
isFocus: this.focus,
addUnit
}
},
watch: {
value: {
immediate: true,
handler(val) {
//
this.inputValue = String(val).substring(0, this.maxlength)
}
},
},
computed: {
// v-for
codeLength() {
return new Array(Number(this.maxlength))
},
// item
itemStyle() {
return index => {
const style = {
width: addUnit(this.size),
height: addUnit(this.size)
}
//
if (this.mode === 'box') {
// 0.5px
style.border = `${this.hairline ? 0.5 : 1}px solid ${this.borderColor}`
// 0
if (getPx(this.space) === 0) {
//
if (index === 0) {
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
}
if (index === this.codeLength.length - 1) {
style.borderTopRightRadius = '3px'
style.borderBottomRightRadius = '3px'
}
//
if (index !== this.codeLength.length - 1) {
style.borderRight = 'none'
}
}
}
if (index !== this.codeLength.length - 1) {
// margin-right
style.marginRight = addUnit(this.space)
} else {
//
style.marginRight = 0
}
return style
}
},
// item
codeArray() {
return String(this.inputValue).split('')
},
// 线线
lineStyle() {
const style = {}
style.height = this.hairline ? '2px' : '4px'
style.width = addUnit(this.size)
// 线
style.backgroundColor = this.borderColor
return style
}
},
methods: {
//
inputHandler(e) {
const value = e.detail.value
this.inputValue = value
// .
if(this.disabledDot) {
this.$nextTick(() => {
this.inputValue = value.replace('.', '')
})
}
// maxlengthchangefinish
this.$emit('change', value)
// v-model
this.$emit('input', value)
//
if (String(value).length >= Number(this.maxlength)) {
this.$emit('finish', value)
}
}
}
}
</script>
<style lang="scss" scoped>
// @import "../../libs/css/components.scss";
$u-content-color: #606266;
$u-code-input-cursor-width: 1px;
$u-code-input-cursor-height: 40%;
$u-code-input-cursor-animation-duration: 1s;
$u-code-input-cursor-animation-name: u-cursor-flicker;
// scssmixin4css
// nvuedisplay:flex
@mixin flex($direction: row) {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: $direction;
}
.u-code-input {
@include flex;
position: relative;
overflow: hidden;
&__item {
@include flex;
justify-content: center;
align-items: center;
position: relative;
&__text {
font-size: 15px;
color: $u-content-color;
}
&__dot {
width: 7px;
height: 7px;
border-radius: 100px;
background-color: $u-content-color;
}
&__line {
position: absolute;
bottom: 0;
height: 4px;
border-radius: 100px;
width: 40px;
background-color: $u-content-color;
}
/* #ifndef APP-PLUS */
&__cursor {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: $u-code-input-cursor-width;
height: $u-code-input-cursor-height;
animation: $u-code-input-cursor-animation-duration u-cursor-flicker infinite;
}
/* #endif */
}
&__input {
// input
//
position: absolute;
left: -750rpx;
width: 1500rpx;
top: 0;
background-color: transparent;
text-align: left;
}
}
/* #ifndef APP-PLUS */
@keyframes u-cursor-flicker {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* #endif */
</style>

@ -56,10 +56,11 @@
"mp-weixin" : {
/* */
"usingComponents" : true,
"appid" : "",
"appid" : "wx69b3bdd669084b09",
"setting" : {
"urlCheck" : true
}
},
"darkmode" : true
},
"h5" : {
"devServer" : {

@ -77,17 +77,7 @@
"animationType": "slide-in-bottom"
}
}
}, {
"path": "pages/public/register",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom",
"app-plus": {
"titleNView": false,
"animationType": "slide-in-bottom"
}
}
}, {
},{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "我的",
@ -260,6 +250,13 @@
}
}
}
},
{
"path" : "pages/webview/webview",
"style" :
{
"navigationBarTitleText" : ""
}
}
],
"globalStyle": {

@ -42,6 +42,7 @@
updateAddress,
fetchAddressDetail
} from '@/api/address.js';
import rule from '@/utils/rule.js'
export default {
data() {
return {
@ -108,7 +109,7 @@
this.$api.msg('请填写收货人姓名');
return;
}
if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(data.phoneNumber)) {
if (!rule.mobile(data.phoneNumber)) {
this.$api.msg('请输入正确的手机号码');
return;
}

@ -216,7 +216,7 @@
</template>
<script>
import share from '@/components/share';
import share from '@/components/share.vue';
import {
fetchProductDetail
} from '@/api/product.js';

@ -0,0 +1,102 @@
<template>
<view class="checkModal">
<view class="topModal" @click.stop="closeModal"></view>
<view class="checkArea" >
<view class="checkBox">
<view class="checkTitle">
{{'选择国家地/地区'}}
</view>
<scroll-view scroll-y="true" class="checkList">
<view v-for="item in data.areaList" :key="item.countryCode" @click="checked(item)" class="checkItem">
<text>{{item.countryName}}</text>
<text>{{item.countryCode}}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'CheckArea',
props: {
data: {
default: () => {},
type: Object,
}
},
methods: {
closeModal() {
this.$emit('closeModal', 'area');
},
checked(item) {
this.$emit('checkedCode', item);
}
}
}
</script>
<style lang="scss" scoped>
.checkModal {
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
z-index: 9;
background-color: rgba(0, 0, 0, 0.4);
.topModal {
height: 100vh;
width: 100vw;
}
.checkArea {
width: 100vw;
position: absolute;
top: 34vh;
left: 0;
animation: fade-in 0.6s;
.checkBox {
width: 100vw;
height: 66vh;
background: #fff;
border-radius: 48upx 48upx 0 0;
display: flex;
flex-direction: column;
.checkTitle {
height: 96upx;
line-height: 96upx;
text-align: center;
font-size: 36upx;
}
.checkList {
height: calc(66vh - 96upx);
padding-bottom: 48rpx;
.checkItem {
height: 48upx;
line-height: 48upx;
font-size: 32upx;
display: flex;
justify-content: space-between;
padding: 24upx;
box-sizing: content-box;
}
}
}
}
}
@keyframes fade-in {
0% {
top: 100vh
}
100% {
top:34vh
}
}
</style>

@ -0,0 +1,140 @@
<template>
<view class="checkCode">
<view class="back-btn" @click="closeModal">
<image src="@/static/icons/left_back@3x.png"></image>
</view>
<view class="wrapper">
<view class="tips">
<view class="tips_title">{{tips.title}}</view>
<view class="tips_text">{{tips.text}} {{data.area}} {{data.mobile}}</view>
</view>
<view class="numBox">
<uni-code-input v-model="data.mobileCheckCode" mode="line" :space="12" :focus="true" borderColor="rgba(0, 0, 0, 0.1)"
hairline :maxlength="6" @finish="finish"></uni-code-input>
</view>
<view class="cutdownBox">
<view v-if="time>0" class="time">{{time}}s</view>
<view v-else @click="regetCode" class="regetBtn">重新获取</view>
</view>
</view>
</view>
</template>
<script>
// import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue';
import uniCodeInput from '@/components/uni-code-input/uni-code-input.vue';
export default {
name: 'CheckCode',
components: {
uniCodeInput
// uniLoadMore
},
data() {
return {
tips: {
title: '输入验证码',
text: '验证码已发送至'
},
time: 60,
resetTime: 60,
timer: null,
}
},
props: {
data: {
default: () => {},
type: Object,
}
},
mounted() {
this.cutdown()
},
destroyed() {
this.timer && clearInterval(this.timer)
},
methods: {
closeModal() {
this.$emit('closeModal', 'code');
},
cutdown() {
this.timer && clearInterval(this.timer)
this.timer = setInterval(
() => {
this.time--
}, 1000)
},
regetCode() {
//
this.time = this.resetTime
this.cutdown()
this.$emit('regetCode')
},
finish(e) {
console.log('输入结束,当前值为:' + e);
this.$emit('login', this.data)
}
}
}
</script>
<style lang="scss" scoped>
.checkCode {
width: 100vw;
height: 100vh;
position: relative;
.back-btn {
position: absolute;
left: 40upx;
padding-top: var(--status-bar-height);
top: 40upx;
font-size: 40upx;
color: $font-color-dark;
z-index: 9;
image {
width: 40rpx;
height: 40rpx;
}
}
.wrapper {
position: relative;
padding-top: 200upx;
.tips {
width: 100%;
height: 136upx;
padding: 0 48rpx;
.tips_title {
font-size: 64upx;
margin-bottom: 26upx;
}
.tips_text {
font-size: 28upx;
}
}
.numBox {
height: 80rpx;
margin: 80rpx 0 0;
display: flex;
justify-content: center;
}
.cutdownBox {
height: 40rpx;
padding: 48rpx;
color: rgba(0, 0, 0, .95);
.time {
color: rgba(0, 0, 0, .4)
}
}
}
}
</style>

@ -1,214 +1,349 @@
<template>
<view class="container">
<view class="left-bottom-sign"></view>
<view class="back-btn yticon icon-zuojiantou-up" @click="navBack"></view>
<view class="right-top-sign"></view>
<!-- 设置白色背景防止软键盘把下部绝对定位元素顶上来盖住输入框等 -->
<view class="wrapper">
<view class="left-top-sign">LOGIN</view>
<view class="welcome">
欢迎回来
<view class="bottom-sign">
<image v-if="theme" :src="`/static/public/login_bg_${theme}.png`"></image>
</view>
<view class="back-btn" @click="navBack">
<image src="@/static/icons/left_back@3x.png"></image>
</view>
<view class="input-content">
<view class="input-item">
<text class="tit">用户名</text>
<input type="text" v-model="username" placeholder="请输入用户名" maxlength="11"/>
<view class="wrapper">
<view class="tips">
<view class="tips_title">{{tips.title}}</view>
<view class="tips_text">{{tips.text}}</view>
</view>
<view class="input-item">
<text class="tit">密码</text>
<input type="text" v-model="password" placeholder="8-18位不含特殊字符的数字、字母组合" placeholder-class="input-empty" maxlength="20"
password @confirm="toLogin" />
<view class="input_box">
<view class="area_select" @click="isShowAreaCheck = true">
<view class="area">{{checkData.checkedCode}}</view>
<image class="icon_down" src="@/static/icons/icon_down.png"></image>
</view>
<input type="number" maxlength="11" v-model="mobile" placeholder="输入手机号" />
<image v-if="mobile" class="icon_clear" src="@/static/icons/icon_clear.png" @click="mobile=''"></image>
</view>
<button class="confirm-btn" @click="toLogin" :disabled="logining">登录</button>
<view class="forget-section" @click="toRegist">
忘记密码?
<button :class="{'confirm-btn':true,disbled:!canSend}" :disabled="!canSend"
@click="getCode">获取短信验证码</button>
<view class="checkbox_box">
<label class="radio" @click="isRead = !isRead">
<radio value="" :checked="isRead" />已阅读并同意 <text class="textBtn"
@click.stop="toRead(0)">用户协议</text>
<text class="textBtn" @click.stop="toRead(1)">隐私政策</text>
</label>
</view>
<view class="login_icon">
<image class="wechat" v-if="useWechatLogin" @click="loginByWechat" src="@/static/icons/icon_wechat.png"></image>
<image class="apple" v-if="useAppleLogin" @click="loginByApple" src="@/static/icons/icon_apple.png"></image>
</view>
<view class="register-section">
还没有账号?
<text @click="toRegist"></text>
</view>
<check-code :data="showData" @closeModal="closeModal" @login="doLogin" @regetCode="regetCode" v-if="isShowCode"
class="modal-box" />
<check-area :data="checkData" @closeModal="closeModal" @checkedCode="checkedCode" v-if="isShowAreaCheck" />
</view>
</template>
<script>
import {
mapState,
mapMutations
} from 'vuex';
import {
memberLogin,memberInfo
sendsms,
appLogin,
wxIdLogin,
memberInfo,
supportedCountryCode,
} from '@/api/member.js';
import CheckCode from './components/checkCode.vue'
import CheckArea from './components/checkArea.vue'
import rule from '@/utils/rule.js'
import { USE_WECHAT_LOGIN, USE_APPLE_LOGIN } from '@/utils/appConfig.js';
export default {
data() {
return {
username: '',
password: '',
logining: false
tips: {
title: '注册/登录',
text: '输入手机号,开启雀乐;未注册手机,将自动注册'
},
//
area: '+86',
//
mobile: '',
//
mobileCheckCode: '',
//
isRead: false,
//
isShowCode: false,
//
isLogining: false,
//
showData: {
area: '',
mobile: '',
deviceId: '',
mobileCheckCode: '',
deviceBrand: ''
},
//
isShowAreaCheck: false,
checkData: {
areaList: [],
checkedCode: '+86'
},
// ()
useWechatLogin: USE_WECHAT_LOGIN,
// ()
useAppleLogin: USE_APPLE_LOGIN,
}
},
components: {
CheckCode,
CheckArea
},
computed: {
...mapState(['theme', 'deviceId', 'deviceBrand']),
canSend() {
return this.mobile.length === 11 && !this.isLogining
}
},
onLoad() {
this.username = uni.getStorageSync('username') || '';
this.password = uni.getStorageSync('password') || '';
this.getCountryCode()
},
methods: {
...mapMutations(['login']),
navBack() {
uni.navigateBack();
},
toRegist() {
uni.navigateTo({url:'/pages/public/register'});
getCode() {
if (!rule.mobile(this.mobile)) {
this.$api.msg('请输入正确的手机号码');
return;
}
if (!this.isRead) {
this.$api.msg('请先同意隐私政策');
return;
}
this.regetCode()
},
async toLogin() {
this.logining = true;
memberLogin({
username: this.username,
password: this.password
}).then(response => {
let token = response.data.tokenHead+response.data.token;
uni.setStorageSync('token',token);
uni.setStorageSync('username',this.username);
uni.setStorageSync('password',this.password);
memberInfo().then(response=>{
closeModal(type) {
console.log('close', type)
if (type === 'code') {
this.isShowCode = false
}
if (type === 'area') {
this.isShowAreaCheck = false
}
},
async doLogin(data) {
uni.showLoading({
title: '登录中……',
mask: true
})
appLogin(data)
.then(res => {
if (res.code === 200 && res.data) {
uni.setStorageSync('token', res.data);
memberInfo().then(response => {
this.login(response.data);
uni.navigateBack();
});
}).catch(() => {
this.logining = false;
});
}
})
.catch(err => {
console.log('err', err)
uni.showToast({
title: '登录失败',
icon: 'error'
})
})
.finally(
() => {
uni.hideLoading()
}
)
},
regetCode() {
this.isLogining = true
sendsms({
mobile: this.mobile,
deviceId: this.deviceId
})
.then(res => {
if (res.code === 200) {
this.showData = {
area: this.area,
mobile: this.mobile,
deviceId: this.deviceId,
deviceBrand: this.deviceBrand,
mobileCheckCode: '',
}
this.isShowCode = true
}
})
.catch(
err => {
console.log(err)
uni.showToast({
duration: 3000,
title: err
})
})
.finally(() => {
this.isLogining = false
})
},
toRead(index) {
const item = [{
url: 'https://m.indie.cn/agreement/registrationAgreement.html',
webviewStyles: {
progress: true
}
</script>
},
{
url: 'https://m.indie.cn/agreement/privacyPolicy.html',
webviewStyles: {
progress: true
}
}
]
uni.navigateTo({
url: '/pages/webview/webview?item=' + encodeURIComponent(JSON.stringify(item[index]))
})
},
getCountryCode() {
supportedCountryCode()
.then(res => {
console.log(res)
if(res.code === 200 && res.data) {
this.checkData = {
areaList: res.data,
checkedCode: '+86'
}
}
})
.catch(err => {
console.log(err)
})
},
checkedCode(item) {
this.checkData.checkedCode = item.countryCode
this.isShowAreaCheck = false
},
loginByWechat() {
uni.showToast({
title: '功能开发中……',
icon: 'none'
})
},
loginByApple() {
uni.showToast({
title: '功能开发中……',
icon: 'none'
})
},
},
<style lang='scss'>
page {
background: #fff;
}
</script>
<style lang='scss' scoped>
.container {
padding-top: 115px;
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #fff;
}
.wrapper {
position: relative;
z-index: 90;
background: #fff;
padding-bottom: 40upx;
.bottom-sign {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
height: 100vw;
image {
width: 100%;
height: 100%;
}
}
.back-btn {
position: absolute;
left: 40upx;
z-index: 9999;
padding-top: var(--status-bar-height);
top: 40upx;
font-size: 40upx;
color: $font-color-dark;
}
z-index: 9;
.left-top-sign {
font-size: 120upx;
color: $page-color-base;
position: relative;
left: -16upx;
image {
width: 40upx;
height: 40upx;
}
}
.right-top-sign {
position: absolute;
top: 80upx;
right: -30upx;
z-index: 95;
.wrapper {
position: relative;
padding-top: 200upx;
&:before,
&:after {
display: block;
content: "";
width: 400upx;
height: 80upx;
background: #b4f3e2;
}
.tips {
width: 100%;
height: 136upx;
padding: 0 48upx;
&:before {
transform: rotate(50deg);
border-radius: 0 50px 0 0;
.tips_title {
font-size: 64upx;
margin-bottom: 26upx;
}
&:after {
position: absolute;
right: -198upx;
top: 0;
transform: rotate(-50deg);
border-radius: 50px 0 0 0;
/* background: pink; */
.tips_text {
font-size: 28upx;
}
}
.left-bottom-sign {
position: absolute;
left: -270upx;
bottom: -320upx;
border: 100upx solid #d0d1fd;
border-radius: 50%;
padding: 180upx;
}
.welcome {
.input_box {
display: flex;
align-items: center;
padding: 88upx 48upx;
position: relative;
left: 50upx;
top: -90upx;
font-size: 46upx;
color: #555;
text-shadow: 1px 0px 1px rgba(0, 0, 0, .3);
}
.input-content {
padding: 0 60upx;
}
.input-item {
.area_select {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 0 30upx;
background: $page-color-light;
height: 120upx;
border-radius: 4px;
margin-bottom: 50upx;
align-items: center;
&:last-child {
margin-bottom: 0;
.area {
font-size: 34upx;
}
.tit {
height: 50upx;
line-height: 56upx;
font-size: $font-sm+2upx;
color: $font-color-base;
.icon_down {
width: 48upx;
height: 48upx;
margin: 0 23upx 0 14upx;
}
}
input {
height: 60upx;
font-size: $font-base + 2upx;
color: $font-color-dark;
width: 100%;
font-size: 34upx;
border-left: 2upx solid rgba(0, 0, 0, 0.4);
padding-left: 54upx;
}
.icon_clear {
width: 48upx;
height: 48upx;
position: absolute;
right: 48upx;
top: 50%;
margin-top: -24upx;
}
}
.confirm-btn {
width: 630upx;
height: 76upx;
line-height: 76upx;
border-radius: 50px;
margin-top: 70upx;
background: $uni-color-primary;
width: 566upx;
height: 88upx;
line-height: 88upx;
border-radius: 999px;
background: #C43737;
color: #fff;
font-size: $font-lg;
@ -217,40 +352,73 @@
}
}
.confirm-btn2 {
width: 630upx;
height: 76upx;
line-height: 76upx;
border-radius: 50px;
margin-top: 40upx;
background: $uni-color-primary;
color: #fff;
font-size: $font-lg;
.confirm-btn.disbled {
background-color: #ccc;
}
&:after {
border-radius: 100px;
.checkbox_box {
padding: 42upx;
.radio {
font-size: 24upx;
vertical-align: middle;
.textBtn {
display: inline-block;
height: 24upx;
line-height: 24upx;
font-size: 24upx;
margin: 0;
padding: 0;
}
}
radio {
transform: scale(0.7)
}
}
.forget-section {
font-size: $font-sm+2upx;
color: $font-color-spec;
text-align: center;
margin-top: 40upx;
.login_icon {
padding-top: 32upx;
display: flex;
justify-content: center;
image {
width: 92upx;
height: 92upx;
}
image.wechat {
margin-right: 24upx;
}
image.apple {
margin-left: 24upx;
}
}
}
.register-section {
.modal-box {
position: absolute;
top: 0;
left: 0;
bottom: 50upx;
width: 100%;
font-size: $font-sm+2upx;
color: $font-color-base;
text-align: center;
height: 100vh;
width: 100vw;
background: #fff;
z-index: 9;
}
}
text {
color: $font-color-spec;
margin-left: 10upx;
/* 暗黑模式 */
@media (prefers-color-scheme: dark) {
.container {
.wrapper .input_box input {
border-left: 2upx solid #fff;
}
.back-btn {
color: #fff;
}
}
}
</style>

@ -0,0 +1,256 @@
<template>
<view class="container">
<view class="left-bottom-sign"></view>
<view class="back-btn yticon icon-zuojiantou-up" @click="navBack"></view>
<view class="right-top-sign"></view>
<!-- 设置白色背景防止软键盘把下部绝对定位元素顶上来盖住输入框等 -->
<view class="wrapper">
<view class="left-top-sign">LOGIN</view>
<view class="welcome">
欢迎回来
</view>
<view class="input-content">
<view class="input-item">
<text class="tit">用户名</text>
<input type="text" v-model="username" placeholder="请输入用户名" maxlength="11"/>
</view>
<view class="input-item">
<text class="tit">密码</text>
<input type="text" v-model="password" placeholder="8-18位不含特殊字符的数字、字母组合" placeholder-class="input-empty" maxlength="20"
password @confirm="toLogin" />
</view>
</view>
<button class="confirm-btn" @click="toLogin" :disabled="logining">登录</button>
<view class="forget-section" @click="toRegist">
忘记密码?
</view>
</view>
<view class="register-section">
还没有账号?
<text @click="toRegist"></text>
</view>
</view>
</template>
<script>
import {
mapMutations
} from 'vuex';
import {
memberLogin,memberInfo
} from '@/api/member.js';
export default {
data() {
return {
username: '',
password: '',
logining: false
}
},
onLoad() {
this.username = uni.getStorageSync('username') || 'member';
this.password = uni.getStorageSync('password') || 'member123';
},
methods: {
...mapMutations(['login']),
navBack() {
uni.navigateBack();
},
toRegist() {
uni.navigateTo({url:'/pages/public/register'});
},
async toLogin() {
this.logining = true;
memberLogin({
username: this.username,
password: this.password
}).then(response => {
let token = response.data.tokenHead+response.data.token;
uni.setStorageSync('token',token);
uni.setStorageSync('username',this.username);
uni.setStorageSync('password',this.password);
memberInfo().then(response=>{
this.login(response.data);
uni.navigateBack();
});
}).catch(() => {
this.logining = false;
});
},
},
}
</script>
<style lang='scss'>
page {
background: #fff;
}
.container {
padding-top: 115px;
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #fff;
}
.wrapper {
position: relative;
z-index: 90;
background: #fff;
padding-bottom: 40upx;
}
.back-btn {
position: absolute;
left: 40upx;
z-index: 9999;
padding-top: var(--status-bar-height);
top: 40upx;
font-size: 40upx;
color: $font-color-dark;
}
.left-top-sign {
font-size: 120upx;
color: $page-color-base;
position: relative;
left: -16upx;
}
.right-top-sign {
position: absolute;
top: 80upx;
right: -30upx;
z-index: 95;
&:before,
&:after {
display: block;
content: "";
width: 400upx;
height: 80upx;
background: #b4f3e2;
}
&:before {
transform: rotate(50deg);
border-radius: 0 50px 0 0;
}
&:after {
position: absolute;
right: -198upx;
top: 0;
transform: rotate(-50deg);
border-radius: 50px 0 0 0;
/* background: pink; */
}
}
.left-bottom-sign {
position: absolute;
left: -270upx;
bottom: -320upx;
border: 100upx solid #d0d1fd;
border-radius: 50%;
padding: 180upx;
}
.welcome {
position: relative;
left: 50upx;
top: -90upx;
font-size: 46upx;
color: #555;
text-shadow: 1px 0px 1px rgba(0, 0, 0, .3);
}
.input-content {
padding: 0 60upx;
}
.input-item {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 0 30upx;
background: $page-color-light;
height: 120upx;
border-radius: 4px;
margin-bottom: 50upx;
&:last-child {
margin-bottom: 0;
}
.tit {
height: 50upx;
line-height: 56upx;
font-size: $font-sm+2upx;
color: $font-color-base;
}
input {
height: 60upx;
font-size: $font-base + 2upx;
color: $font-color-dark;
width: 100%;
}
}
.confirm-btn {
width: 630upx;
height: 76upx;
line-height: 76upx;
border-radius: 50px;
margin-top: 70upx;
background: $uni-color-primary;
color: #fff;
font-size: $font-lg;
&:after {
border-radius: 100px;
}
}
.confirm-btn2 {
width: 630upx;
height: 76upx;
line-height: 76upx;
border-radius: 50px;
margin-top: 40upx;
background: $uni-color-primary;
color: #fff;
font-size: $font-lg;
&:after {
border-radius: 100px;
}
}
.forget-section {
font-size: $font-sm+2upx;
color: $font-color-spec;
text-align: center;
margin-top: 40upx;
}
.register-section {
position: absolute;
left: 0;
bottom: 50upx;
width: 100%;
font-size: $font-sm+2upx;
color: $font-color-base;
text-align: center;
text {
color: $font-color-spec;
margin-left: 10upx;
}
}
</style>

@ -1,137 +0,0 @@
<template>
<view class="container">
<view class="left-bottom-sign"></view>
<view class="back-btn yticon icon-zuojiantou-up" @click="navBack"></view>
<view class="right-top-sign"></view>
<!-- 设置白色背景防止软键盘把下部绝对定位元素顶上来盖住输入框等 -->
<view class="wrapper">
<view class="empty">
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
onLoad() {
},
methods: {
navBack() {
uni.navigateBack();
},
},
}
</script>
<style lang='scss'>
page {
background: #fff;
}
.empty {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
padding-bottom: 100upx;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
background: #fff;
image {
width: 420upx;
height: 420upx;
margin-bottom: 30upx;
}
.empty-tips {
display: flex;
font-size: $font-sm+16upx;
color: $font-color-disabled;
.navigator {
color: $uni-color-primary;
margin-left: 0upx;
}
}
}
.container {
padding-top: 115px;
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #fff;
}
.wrapper {
position: relative;
z-index: 90;
background: #fff;
padding-bottom: 40upx;
}
.back-btn {
position: absolute;
left: 40upx;
z-index: 9999;
padding-top: var(--status-bar-height);
top: 40upx;
font-size: 40upx;
color: $font-color-dark;
}
.left-top-sign {
font-size: 120upx;
color: $page-color-base;
position: relative;
left: -16upx;
}
.right-top-sign {
position: absolute;
top: 80upx;
right: -30upx;
z-index: 95;
&:before,
&:after {
display: block;
content: "";
width: 400upx;
height: 80upx;
background: #b4f3e2;
}
&:before {
transform: rotate(50deg);
border-radius: 0 50px 0 0;
}
&:after {
position: absolute;
right: -198upx;
top: 0;
transform: rotate(-50deg);
border-radius: 50px 0 0 0;
/* background: pink; */
}
}
.left-bottom-sign {
position: absolute;
left: -270upx;
bottom: -320upx;
border: 100upx solid #d0d1fd;
border-radius: 50%;
padding: 180upx;
}
</style>

@ -0,0 +1,28 @@
<template>
<web-view :src="url" :webview-styles="webviewStyles"></web-view>
</template>
<script>
export default {
data() {
return {
url: '',
webviewStyles: {}
};
},
onLoad: function(option) {
const item = JSON.parse(decodeURIComponent(option.item));
const {
url,
webviewStyles
} = item
this.url = url
this.webviewStyles = webviewStyles ? webviewStyles : {}
}
}
</script>
<style lang="scss">
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@ -7,6 +7,9 @@ const store = new Vuex.Store({
state: {
hasLogin: false,
userInfo: {},
theme: uni.getSystemInfoSync().theme,
deviceId: uni.getDeviceInfo().deviceId,
deviceBrand: uni.getDeviceInfo().deviceBrand,
},
mutations: {
login(state, provider) {
@ -28,6 +31,9 @@ const store = new Vuex.Store({
uni.removeStorage({
key: 'token'
})
},
changeTheme(state, theme) {
state.theme = theme
}
},
actions: {

@ -0,0 +1,24 @@
{
"light": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#F7F7F7",
"backgroundColor": "#F7F7F7",
"tabBarColor": "#7A7E83",
"tabBarSelectedColor": "#3cc51f",
"tabBarBorderStyle": "black",
"tabBarBackgroundColor": "#F7F7F7"
},
"dark": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1F1F1F",
"backgroundColor": "#1F1F1F",
"tabBarColor": "#cacaca",
"tabBarSelectedColor": "#51A937",
"tabBarBorderStyle": "white",
"tabBarBackgroundColor": "#1F1F1F"
}
}

@ -1,7 +1,15 @@
// appConfig.js
//配置API请求的基础路径
// export const API_BASE_URL = 'http://localhost:8085';
export const API_BASE_URL = 'https://portal-api.macrozheng.com';
export const API_BASE_URL = 'http://43.248.137.154:8085';
export const API_USER_URL = 'http://39.103.180.196:9012';
// export const API_BASE_URL = 'https://portal-api.macrozheng.com';
//是否启用支付宝支付
export const USE_ALIPAY = false;
export const USE_ALIPAY = true;
// 是否启用微信登录
export const USE_WECHAT_LOGIN = false;
// 是否启用Apple登录
export const USE_APPLE_LOGIN = false;

@ -0,0 +1,29 @@
import rule from './rule.js'
/**
* @description 添加单位如果有rpxupx%px等单位结尾或者值为auto直接返回否则加上px单位结尾
* @param {string|number} value 需要添加单位的值
* @param {string} unit 添加的单位名 比如px
*/
export function addUnit(value = 'auto', unit = uni?.$u?.config?.unit ?? 'px') {
value = String(value)
// 用uView内置验证规则中的number判断是否为数值
return rule.number(value) ? `${value}${unit}` : value
}
/**
* @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx"取出其数值部分如果是"xxxrpx"还需要用过uni.upx2px进行转换
* @param {number|string} value 用户传递值的px值
* @param {boolean} unit
* @returns {number|string}
*/
export function getPx(value, unit = false) {
if (rule.number(value)) {
return unit ? `${value}px` : Number(value)
}
// 如果带有rpx先取出其数值部分再转为px值
if (/(rpx|upx)$/.test(value)) {
return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value)))
}
return unit ? `${parseInt(value)}px` : parseInt(value)
}

@ -1,9 +1,13 @@
import Request from '@/js_sdk/luch-request/request.js'
import { API_BASE_URL} from '@/utils/appConfig.js';
import {
API_BASE_URL,
API_USER_URL,
} from '@/utils/appConfig.js';
const http = new Request()
http.setConfig((config) => { /* 设置全局配置 */
http.setConfig((config) => {
/* 设置全局配置 */
config.baseUrl = API_BASE_URL /* 根域名不同 */
config.header = {
...config.header
@ -20,14 +24,20 @@ http.validateStatus = (statusCode) => {
return statusCode === 200
}
http.interceptor.request((config, cancel) => { /* 请求之前拦截器 */
http.interceptor.request((config, cancel) => {
/* 请求之前拦截器 */
if (config.requestBase === 'USER') {
config.baseUrl = API_USER_URL
} else {
config.baseUrl = API_BASE_URL
}
const token = uni.getStorageSync('token');
if(token){
if (token) {
config.header = {
'Authorization':token,
'Authorization': token,
...config.header
}
}else{
} else {
config.header = {
...config.header
}
@ -40,21 +50,23 @@ http.interceptor.request((config, cancel) => { /* 请求之前拦截器 */
return config
})
http.interceptor.response((response) => { /* 请求之后拦截器 */
http.interceptor.response((response) => {
/* 请求之后拦截器 */
const res = response.data;
if (res.code !== 200) {
//提示错误信息
uni.showToast({
title:res.message,
duration:1500
title: res.message,
duration: 1500,
icon: 'error'
})
//401未登录处理
if (res.code === 401) {
uni.showModal({
title: '提示',
content: '你已被登出,可以取消继续留在该页面,或者重新登录',
confirmText:'重新登录',
cancelText:'取消',
confirmText: '重新登录',
cancelText: '取消',
success: function(res) {
if (res.confirm) {
uni.navigateTo({
@ -74,13 +86,13 @@ http.interceptor.response((response) => { /* 请求之后拦截器 */
//提示错误信息
console.log('response error', response);
uni.showToast({
title:response.errMsg,
duration:1500
title: response.errMsg,
duration: 1500
})
return Promise.reject(response);
})
export function request (options = {}) {
export function request(options = {}) {
return http.request(options);
}

@ -0,0 +1,289 @@
/**
* 验证电子邮箱格式
*/
function email(value) {
return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value)
}
/**
* 验证手机格式
* source: https://m.jihaoba.com/tools/haoduan/
*/
function mobile(value) {
return /^((13[0-9])|(14[0|5|6|7|9])|(15[0|1|2|3|5|6|7|8|9])|(16[2|5|6|7])|(17[0|1|2|3|5|6|7|8])|(18[0-9])|(19[0|1|2|3|5|6|7|8|9]))\d{8}$/.test(value)
}
/**
* 验证URL格式
*/
function url(value) {
return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/
.test(value)
}
/**
* 验证日期格式
*/
function date(value) {
if (!value) return false
// 判断是否数值或者字符串数值(意味着为时间戳)转为数值否则new Date无法识别字符串时间戳
if (number(value)) value = +value
return !/Invalid|NaN/.test(new Date(value).toString())
}
/**
* 验证ISO类型的日期格式
*/
function dateISO(value) {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)
}
/**
* 验证十进制数字
*/
function number(value) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
}
/**
* 验证字符串
*/
function string(value) {
return typeof value === 'string'
}
/**
* 验证整数
*/
function digits(value) {
return /^\d+$/.test(value)
}
/**
* 验证身份证号码
*/
function idCard(value) {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
value
)
}
/**
* 是否车牌号
*/
function carNo(value) {
// 新能源车牌
const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/
// 旧车牌
const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/
if (value.length === 7) {
return creg.test(value)
} if (value.length === 8) {
return xreg.test(value)
}
return false
}
/**
* 金额,只允许2位小数
*/
function amount(value) {
// 金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value)
}
/**
* 中文
*/
function chinese(value) {
const reg = /^[\u4e00-\u9fa5]+$/gi
return reg.test(value)
}
/**
* 只能输入字母
*/
function letter(value) {
return /^[a-zA-Z]*$/.test(value)
}
/**
* 只能是字母或者数字
*/
function enOrNum(value) {
// 英文或者数字
const reg = /^[0-9a-zA-Z]*$/g
return reg.test(value)
}
/**
* 验证是否包含某个值
*/
function contains(value, param) {
return value.indexOf(param) >= 0
}
/**
* 验证一个值范围[min, max]
*/
function range(value, param) {
return value >= param[0] && value <= param[1]
}
/**
* 验证一个长度范围[min, max]
*/
function rangeLength(value, param) {
return value.length >= param[0] && value.length <= param[1]
}
/**
* 是否固定电话
*/
function landline(value) {
const reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/
return reg.test(value)
}
/**
* 判断是否为空
*/
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
}
/**
* 是否json字符串
*/
function jsonString(value) {
if (typeof value === 'string') {
try {
const obj = JSON.parse(value)
if (typeof obj === 'object' && obj) {
return true
}
return false
} catch (e) {
return false
}
}
return false
}
/**
* 是否数组
*/
function array(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
}
/**
* 是否对象
*/
function object(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
/**
* 是否短信验证码
*/
function code(value, len = 6) {
return new RegExp(`^\\d{${len}}$`).test(value)
}
/**
* 是否函数方法
* @param {Object} value
*/
function func(value) {
return typeof value === 'function'
}
/**
* 是否promise对象
* @param {Object} value
*/
function promise(value) {
return object(value) && func(value.then) && func(value.catch)
}
/**
* @param {Object} value
*/
function image(value) {
const newValue = value.split('?')[0]
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i
return IMAGE_REGEXP.test(newValue)
}
/**
* 是否视频格式
* @param {Object} value
*/
function video(value) {
const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i
return VIDEO_REGEXP.test(value)
}
/**
* 是否为正则对象
* @param {Object}
* @return {Boolean}
*/
function regExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]'
}
export default {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
isEmpty: empty,
jsonString,
landline,
object,
array,
code,
func,
promise,
video,
image,
regExp,
string
}
Loading…
Cancel
Save