添加登录逻辑
9
.hbuilderx/launch.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version" : "1.0",
|
||||
"configurations" : [
|
||||
{
|
||||
"playground" : "standard",
|
||||
"type" : "uni-app:app-ios"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
.prettierrc.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// @ts-check
|
||||
|
||||
module.exports = {
|
||||
printWidth: 74,
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
arrowParens: 'avoid',
|
||||
singleQuote: true,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
endOfLine: 'auto',
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
'objectCurly-newline': [
|
||||
'error',
|
||||
{
|
||||
multiline: true
|
||||
}
|
||||
],
|
||||
'array-bracket-newline': ['error', 'consistent'],
|
||||
vueIndentScriptAndStyle: true
|
||||
}
|
||||
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"uni-helper.uni-app-snippets-vscode"
|
||||
]
|
||||
}
|
||||
51
App.vue
@@ -1,34 +1,35 @@
|
||||
<script setup>
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { useTokenStore } from '@/stores/token'
|
||||
import { reLaunch } from '@/utils/router'
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { useTokenStore } from './stores/token'
|
||||
import { reLaunch } from './utils/router'
|
||||
|
||||
/** 静默登录逻辑 */
|
||||
const silentLogin = async () => {
|
||||
const tokenStore = useTokenStore()
|
||||
if (tokenStore.token && !tokenStore.isTokenExpired()) {
|
||||
console.log('去验证token')
|
||||
return
|
||||
}
|
||||
/** 静默登录逻辑 */
|
||||
const silentLogin = async () => {
|
||||
const tokenStore = useTokenStore()
|
||||
if (tokenStore.token && !tokenStore.isTokenExpired()) {
|
||||
console.log('去验证token')
|
||||
return
|
||||
}
|
||||
|
||||
// 没有token去登录页
|
||||
reLaunch('/pages/login/login?id=1')
|
||||
}
|
||||
// 没有token去登录页
|
||||
reLaunch('/pages/login/login?id=1')
|
||||
}
|
||||
|
||||
onLaunch(() => {
|
||||
console.log('App Launch111')
|
||||
silentLogin()
|
||||
})
|
||||
onLaunch(() => {
|
||||
console.log('App Launch111')
|
||||
silentLogin()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
console.log('App Show222')
|
||||
})
|
||||
onShow(() => {
|
||||
console.log('App Show222')
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
console.log('App Hide333')
|
||||
})
|
||||
onHide(() => {
|
||||
console.log('App Hide333')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
<style lang="scss">
|
||||
/*每个页面公共css */
|
||||
@import './styles/global.scss';
|
||||
</style>
|
||||
|
||||
62
components/agreement-checkbox/agreement-checkbox.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
const isShow = defineModel({
|
||||
type: Boolean,
|
||||
default: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="agreement-checkbox">
|
||||
<view
|
||||
v-show="!isShow"
|
||||
class="checkbox-box"
|
||||
@click="isShow = true"
|
||||
></view>
|
||||
<image
|
||||
v-show="isShow"
|
||||
src="/static/images/public/check-to-confirm.png"
|
||||
mode="aspectFit"
|
||||
class="left-icon"
|
||||
@click="isShow = false"
|
||||
></image>
|
||||
<text class="text">我已阅读并同意</text>
|
||||
<text class="on">《用户权益》</text>
|
||||
<text class="text">与</text>
|
||||
<text class="on">《隐私政策》</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agreement-checkbox {
|
||||
margin-top: 48px;
|
||||
display: flex;
|
||||
|
||||
.checkbox-box,
|
||||
.left-icon {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.checkbox-box {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 48rpx;
|
||||
border: 2rpx solid #999999;
|
||||
}
|
||||
.left-icon {
|
||||
width: 36.25rpx;
|
||||
height: 36.25rpx;
|
||||
}
|
||||
.text,
|
||||
.on {
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.on {
|
||||
color: #00d9c5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
components/cb-button/cb-button.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// 抛出点击事件
|
||||
const emits = defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="cb-button">
|
||||
<button
|
||||
:disabled="props.disabled"
|
||||
@click="emits('click')"
|
||||
class="cb-button"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cb-button {
|
||||
button {
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
border-radius: 96rpx;
|
||||
background: linear-gradient(180deg, #00d993 0%, #00d9c5 100%);
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
&[disabled] {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
components/cb-input/cb-input.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import account from '@/static/images/login/account.png'
|
||||
import password from '@/static/images/login/password.png'
|
||||
import phone from '@/static/images/login/phone.png'
|
||||
import invitation from '@/static/images/login/invitation.png'
|
||||
import email from '@/static/images/login/email.png'
|
||||
import codeIcon from '@/static/images/login/code.png'
|
||||
import viewPassword from '@/static/images/login/view.png'
|
||||
import concealPassword from '@/static/images/login/conceal.png'
|
||||
|
||||
const name = defineModel({
|
||||
type: String,
|
||||
default: ''
|
||||
})
|
||||
|
||||
const isCode = defineModel('code', {
|
||||
type: Boolean,
|
||||
default: false
|
||||
})
|
||||
|
||||
/** 倒计时 */
|
||||
const countdown = ref(0)
|
||||
|
||||
/** 启动倒计时 */
|
||||
const startCountdown = () => {
|
||||
countdown.value = 60
|
||||
const timer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--
|
||||
} else {
|
||||
isCode.value = false
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isCode.value,
|
||||
v => {
|
||||
if (v) {
|
||||
startCountdown()
|
||||
} else {
|
||||
countdown.value = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 输入框状态类型
|
||||
* text: 文本
|
||||
* password: 密码
|
||||
* number: 数字
|
||||
* tel: 手机号
|
||||
* email: 邮箱
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
/**
|
||||
* 输入框图标
|
||||
* 1: 手机号/邮箱
|
||||
* 2: 密码
|
||||
* 3: 手机号
|
||||
* 4: 邀请码
|
||||
* 5: 邮箱
|
||||
* 6: 验证码
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: '1'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入'
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['onGetCode'])
|
||||
|
||||
/** 切换查看密码状态 */
|
||||
const showPassword = ref(false)
|
||||
|
||||
const placeholderStyle = `font-family: PingFang SC, PingFang SC; font-weight: 500; color: #D9D9D9; font-size: 28rpx; font-style: normal; text-transform: none;`
|
||||
|
||||
const leftIcon = computed(() => {
|
||||
switch (props.icon) {
|
||||
case '1':
|
||||
return account
|
||||
case '2':
|
||||
return password
|
||||
case '3':
|
||||
return phone
|
||||
case '4':
|
||||
return invitation
|
||||
case '5':
|
||||
return email
|
||||
case '6':
|
||||
return codeIcon
|
||||
default:
|
||||
return account
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="cb-input">
|
||||
<image :src="leftIcon" mode="heightFix" class="left-icon"></image>
|
||||
<input
|
||||
v-if="props.type === 'password'"
|
||||
v-model="name"
|
||||
:password="!showPassword"
|
||||
:placeholder-style="placeholderStyle"
|
||||
:placeholder="props.placeholder"
|
||||
class="input-box"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="name"
|
||||
:type="props.type"
|
||||
:placeholder-style="placeholderStyle"
|
||||
:placeholder="props.placeholder"
|
||||
class="input-box"
|
||||
/>
|
||||
<text
|
||||
v-if="props.icon === '6'"
|
||||
:class="{ 'text-decoration': isCode }"
|
||||
class="right-text"
|
||||
@click="!isCode && emits('onGetCode')"
|
||||
>
|
||||
{{ isCode ? `${countdown}秒后重新获取` : '获取验证码' }}
|
||||
</text>
|
||||
<image
|
||||
v-if="props.type === 'password'"
|
||||
:src="showPassword ? viewPassword : concealPassword"
|
||||
mode="heightFix"
|
||||
class="right-icon"
|
||||
@click="showPassword = !showPassword"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cb-input + .cb-input {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
.cb-input {
|
||||
width: calc(100% - 64rpx);
|
||||
height: 96rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 128rpx;
|
||||
padding: 0 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.left-icon,
|
||||
.right-icon {
|
||||
flex-shrink: 0;
|
||||
height: 48rpx;
|
||||
}
|
||||
.left-icon {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.input-box {
|
||||
width: 100%;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.right-icon {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.right-text {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
width: 140rpx;
|
||||
color: #00d9c5;
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.text-decoration {
|
||||
width: 202rpx;
|
||||
text-align: right;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
components/nav-bar/nav-bar.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { navigateBack } from '@/utils/router'
|
||||
|
||||
const props = defineProps({
|
||||
})
|
||||
|
||||
const onBack = () => {
|
||||
navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="nav-bar">
|
||||
<view class="status_bar">
|
||||
<!-- 这里是状态栏 -->
|
||||
</view>
|
||||
<view class="nav-bar-box">
|
||||
<view @click="onBack">
|
||||
<!-- 返回图标插槽 -->
|
||||
<slot name="back"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.status_bar {
|
||||
height: var(--status-bar-height);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-box {
|
||||
padding: 0 36rpx;
|
||||
height: 58rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
119
components/register-app/register-app.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup>
|
||||
import { reactive, computed } from 'vue'
|
||||
import { reLaunch } from '@/utils/router'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 注册方式
|
||||
* phone: 手机号
|
||||
* email: 邮箱
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
default: 'phone'
|
||||
}
|
||||
})
|
||||
|
||||
const isPhone = computed(() => props.type === 'phone')
|
||||
|
||||
const formData = reactive({
|
||||
// 手机号
|
||||
name: '',
|
||||
// 验证码
|
||||
code: '',
|
||||
// 密码
|
||||
password: '',
|
||||
// 确认密码
|
||||
confirmPassword: '',
|
||||
// 邀请码
|
||||
inviteCode: '',
|
||||
agreement: false
|
||||
})
|
||||
|
||||
const isBtn = computed(() => {
|
||||
return (
|
||||
formData.name &&
|
||||
formData.code &&
|
||||
formData.password &&
|
||||
formData.confirmPassword &&
|
||||
formData.inviteCode &&
|
||||
!formData.agreement
|
||||
)
|
||||
})
|
||||
|
||||
const onRegister = () => {
|
||||
console.log('注册')
|
||||
}
|
||||
|
||||
const onLogin = () => {
|
||||
reLaunch('/pages/login/login')
|
||||
}
|
||||
|
||||
const onTopRight = () => {
|
||||
console.log('切换注册方式', isPhone.value)
|
||||
const url = isPhone.value
|
||||
? '/pages/login/email-register/email-register'
|
||||
: '/pages/login/phone-register/phone-register'
|
||||
reLaunch(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="register-app">
|
||||
<view class="top-register-nav">
|
||||
<text class="title-left">{{ isPhone ? '手机' : '邮箱' }}注册</text>
|
||||
<text class="title-right" @click="onTopRight">
|
||||
{{ isPhone ? '邮箱' : '手机号' }}注册
|
||||
</text>
|
||||
</view>
|
||||
<div class="input-wrapper">
|
||||
<cb-input
|
||||
v-model="formData.name"
|
||||
:type="isPhone ? 'number' : 'email'"
|
||||
icon="3"
|
||||
:placeholder="`请输入${isPhone ? '手机号' : '邮箱'}`"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.code"
|
||||
type="number"
|
||||
icon="6"
|
||||
placeholder="请输入验证码"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
icon="2"
|
||||
placeholder="请输入密码"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.confirmPassword"
|
||||
type="password"
|
||||
icon="2"
|
||||
placeholder="请输入确认密码"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.inviteCode"
|
||||
type="number"
|
||||
icon="4"
|
||||
placeholder="请输入邀请码"
|
||||
></cb-input>
|
||||
<agreement-checkbox v-model="formData.agreement" />
|
||||
<cb-button class="bottom-btn" :disabled="isBtn" @click="onRegister">
|
||||
注册
|
||||
</cb-button>
|
||||
<view class="bottom-text">
|
||||
<text class="text">已有账号?</text>
|
||||
<text class="text" @click="onLogin">去登录</text>
|
||||
</view>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '/styles/login.scss';
|
||||
.register-app {
|
||||
.bottom-btn {
|
||||
margin: 100rpx 0 64rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
main.js
@@ -6,7 +6,7 @@ import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
@@ -15,11 +15,11 @@ app.$mount()
|
||||
import { createSSRApp } from 'vue'
|
||||
import * as Pinia from 'pinia'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(Pinia.createPinia())
|
||||
return {
|
||||
app,
|
||||
Pinia, // 此处必须将 Pinia 返回
|
||||
}
|
||||
const app = createSSRApp(App)
|
||||
app.use(Pinia.createPinia())
|
||||
return {
|
||||
app,
|
||||
Pinia // 此处必须将 Pinia 返回
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
24
pages.json
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
"pages": [
|
||||
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
@@ -11,6 +12,27 @@
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/phone-register/phone-register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "手机注册",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/email-register/email-register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "邮箱注册",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/forgot-password/forgot-password",
|
||||
"style": {
|
||||
"navigationBarTitleText": "忘记密码",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
10
pages/login/email-register/email-register.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<register-app type="email"></register-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
86
pages/login/forgot-password/forgot-password.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
})
|
||||
const isCode = ref(false)
|
||||
|
||||
const getCode = () => {
|
||||
console.log('获取验证码')
|
||||
isCode.value = true
|
||||
}
|
||||
|
||||
const onBottomBtn = () => {
|
||||
console.log('确认')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="forgot-password">
|
||||
<nav-bar>
|
||||
<template #back>
|
||||
<image
|
||||
src="/static/images/login/back.png"
|
||||
mode="heightFix"
|
||||
class="left-icon"
|
||||
></image>
|
||||
</template>
|
||||
</nav-bar>
|
||||
<view class="top-nav">
|
||||
<text class="title-left">忘记密码</text>
|
||||
</view>
|
||||
<div class="input-wrapper">
|
||||
<cb-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入手机号/邮箱"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.code"
|
||||
v-model:code="isCode"
|
||||
type="number"
|
||||
icon="6"
|
||||
placeholder="请输入验证码"
|
||||
@onGetCode="getCode"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
icon="2"
|
||||
placeholder="请输入密码"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.confirmPassword"
|
||||
type="password"
|
||||
icon="2"
|
||||
placeholder="请输入确认密码"
|
||||
></cb-input>
|
||||
<cb-button
|
||||
class="bottom-btn"
|
||||
:disabled="
|
||||
!formData.username ||
|
||||
!formData.password ||
|
||||
!formData.confirmPassword ||
|
||||
!formData.code
|
||||
"
|
||||
@click="onBottomBtn"
|
||||
>
|
||||
确定
|
||||
</cb-button>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '/styles/login.scss';
|
||||
.left-icon {
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.bottom-btn {
|
||||
margin-top: 140rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,75 @@
|
||||
<template>
|
||||
<view>{{ name }}</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { reactive } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useUI } from '@/utils/use-ui'
|
||||
import { reLaunch, navigateTo } from '@/utils/router'
|
||||
|
||||
const name = ref('登1录')
|
||||
onLoad(e => {
|
||||
console.log('接收==:', e.id)
|
||||
})
|
||||
const { showToast } = useUI()
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
agreement: false
|
||||
})
|
||||
|
||||
const onLogin = () => {
|
||||
showToast('登录成功')
|
||||
console.log('登录:', formData)
|
||||
}
|
||||
|
||||
const onRegister = () => {
|
||||
reLaunch('/pages/login/phone-register/phone-register')
|
||||
}
|
||||
|
||||
const onTopRight = () => {
|
||||
navigateTo('/pages/login/forgot-password/forgot-password')
|
||||
}
|
||||
|
||||
onLoad(e => {
|
||||
console.log('接收==:', e.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<view class="login">
|
||||
<view class="top-nav">
|
||||
<text class="title-left">登录</text>
|
||||
<text class="title-right" @click="onTopRight">忘记密码</text>
|
||||
</view>
|
||||
<div class="input-wrapper">
|
||||
<cb-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入手机号/邮箱"
|
||||
></cb-input>
|
||||
<cb-input
|
||||
v-model="formData.password"
|
||||
type="password"
|
||||
icon="2"
|
||||
placeholder="请输入密码"
|
||||
></cb-input>
|
||||
<agreement-checkbox v-model="formData.agreement" />
|
||||
<cb-button
|
||||
class="bottom-btn"
|
||||
:disabled="
|
||||
!formData.username || !formData.password || !formData.agreement
|
||||
"
|
||||
@click="onLogin"
|
||||
>
|
||||
登录
|
||||
</cb-button>
|
||||
<view class="bottom-text">
|
||||
<text class="text">还没账号?</text>
|
||||
<text class="text" @click="onRegister">去注册</text>
|
||||
</view>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '/styles/login.scss';
|
||||
.login {
|
||||
.bottom-btn {
|
||||
margin: 160rpx 0 64rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
10
pages/login/phone-register/phone-register.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<register-app></register-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
BIN
static/images/login/account.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/login/back.png
Normal file
|
After Width: | Height: | Size: 276 B |
BIN
static/images/login/code.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/images/login/conceal.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/login/email.png
Normal file
|
After Width: | Height: | Size: 842 B |
BIN
static/images/login/invitation.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
static/images/login/password.png
Normal file
|
After Width: | Height: | Size: 799 B |
BIN
static/images/login/phone.png
Normal file
|
After Width: | Height: | Size: 561 B |
BIN
static/images/login/top.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
static/images/login/view.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/public/check-to-confirm.png
Normal file
|
After Width: | Height: | Size: 825 B |
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { STORAGE_KEYS } from '@/constants/storageKeys';
|
||||
import { STORAGE_KEYS } from '@/constants/storage-keys';
|
||||
import { getToken, removeToken } from '@/utils/storage';
|
||||
|
||||
/** 登录状态 */
|
||||
|
||||
4
styles/global.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
/* 设置全局背景色 */
|
||||
page {
|
||||
background-color: #fff;
|
||||
}
|
||||
75
styles/login.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
// 登录,忘记密码顶部样式
|
||||
.top-nav {
|
||||
padding: 0 32rpx;
|
||||
height: 446rpx;
|
||||
background-image: url('/static/images/login/top.png');
|
||||
background-size: cover;
|
||||
background-position: 10rpx 0;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
.title-left,
|
||||
.title-right {
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
margin-bottom: 86rpx;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
font-size: 40rpx;
|
||||
color: #333333;
|
||||
}
|
||||
.title-right {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册顶部样式
|
||||
.top-register-nav {
|
||||
padding: 120rpx 32rpx 66rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.title-left,
|
||||
.title-right {
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
font-size: 40rpx;
|
||||
color: #333333;
|
||||
}
|
||||
.title-right {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框排版样式
|
||||
.input-wrapper {
|
||||
margin-top: 10rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
// 底部文字
|
||||
.bottom-text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.text {
|
||||
font-family: PingFang SC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #00d9c5;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
&:first-child {
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
uni.scss
@@ -21,32 +21,32 @@ $uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color: #333; //基本色
|
||||
$uni-text-color-inverse: #fff; //反色
|
||||
$uni-text-color-grey: #999; //辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
$uni-text-color-disable: #c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
$uni-bg-color: #ffffff;
|
||||
$uni-bg-color-grey: #f8f8f8;
|
||||
$uni-bg-color-hover: #f1f1f1; //点击状态颜色
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); //遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
$uni-border-color: #c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
$uni-img-size-sm: 20px;
|
||||
$uni-img-size-base: 26px;
|
||||
$uni-img-size-lg: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
@@ -68,9 +68,9 @@ $uni-spacing-col-lg: 12px;
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-title: #2c405a; // 文章标题颜色
|
||||
$uni-font-size-title: 20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
$uni-font-size-subtitle: 26px;
|
||||
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||
$uni-font-size-paragraph: 15px;
|
||||
|
||||
@@ -32,7 +32,7 @@ const request = (options) => {
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
|
||||
@@ -7,60 +7,66 @@
|
||||
* @returns {string} 拼接后的完整 url
|
||||
*/
|
||||
const appendParams = (url, params) => {
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
return url
|
||||
}
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
return url
|
||||
}
|
||||
|
||||
const query = Object.entries(params).map(([key, value]) => {
|
||||
// 处理复杂类型(如对象、数组)需序列化
|
||||
if (typeof value === 'object') {
|
||||
value = encodeURIComponent(JSON.stringify(value))
|
||||
} else {
|
||||
value = encodeURIComponent(String(value))
|
||||
}
|
||||
return `${key}=${value}`
|
||||
}).join('&')
|
||||
const query = Object.entries(params)
|
||||
.map(([key, value]) => {
|
||||
// 处理复杂类型(如对象、数组)需序列化
|
||||
if (typeof value === 'object') {
|
||||
value = encodeURIComponent(JSON.stringify(value))
|
||||
} else {
|
||||
value = encodeURIComponent(String(value))
|
||||
}
|
||||
return `${key}=${value}`
|
||||
})
|
||||
.join('&')
|
||||
|
||||
return url.includes('?') ? `${url}&${query}` : `${url}?${query}`
|
||||
return url.includes('?') ? `${url}&${query}` : `${url}?${query}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通跳转(保留返回)
|
||||
*/
|
||||
export const navigateTo = (url, params = {}) => {
|
||||
const finalUrl = appendParams(url, params)
|
||||
return uni.navigateTo({ url: finalUrl })
|
||||
const finalUrl = appendParams(url, params)
|
||||
return uni.navigateTo({
|
||||
url: finalUrl
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前页,跳转到应用内某个页面(不可返回)
|
||||
*/
|
||||
export const redirectTo = (url, params = {}) => {
|
||||
const finalUrl = appendParams(url, params)
|
||||
return uni.redirectTo({ url: finalUrl })
|
||||
const finalUrl = appendParams(url, params)
|
||||
return uni.redirectTo({ url: finalUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有页面,打开到应用内某个页面
|
||||
*/
|
||||
export const reLaunch = (url, params = {}) => {
|
||||
const finalUrl = appendParams(url, params)
|
||||
return uni.reLaunch({ url: finalUrl })
|
||||
const finalUrl = appendParams(url, params)
|
||||
return uni.reLaunch({ url: finalUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页(可指定 delta)
|
||||
*/
|
||||
export const navigateBack = (delta = 1) => {
|
||||
return uni.navigateBack({ delta })
|
||||
return uni.navigateBack({ delta })
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到 tabBar 页面(只能用 switchTab)
|
||||
*/
|
||||
export const switchTab = (url, params = {}) => {
|
||||
if (Object.keys(params).length > 0) {
|
||||
console.warn('switchTab 不支持携带参数,请使用全局状态或 storage 传递')
|
||||
}
|
||||
return uni.switchTab({ url })
|
||||
if (Object.keys(params).length > 0) {
|
||||
console.warn(
|
||||
'switchTab 不支持携带参数,请使用全局状态或 storage 传递'
|
||||
)
|
||||
}
|
||||
return uni.switchTab({ url })
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STORAGE_KEYS } from '@/constants/storageKeys'
|
||||
import { STORAGE_KEYS } from '@/constants/storage-keys'
|
||||
|
||||
/** 保存 token */
|
||||
export const setToken = (v) => {
|
||||
|
||||
56
utils/use-ui.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// 加载状态,提示语
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 全局 loading 状态(可用于页面绑定 v-if)
|
||||
const isLoading = ref(false)
|
||||
|
||||
/**
|
||||
* 显示 loading
|
||||
* @param {string} title - 提示文字(H5 支持,App 小程序部分支持)
|
||||
*/
|
||||
const showLoading = (title = '加载中...') => {
|
||||
isLoading.value = true
|
||||
// uni.showLoading 在 H5 和 App 中行为略有不同,但基本可用
|
||||
uni.showLoading({
|
||||
title,
|
||||
mask: true // 防止穿透点击
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏 loading
|
||||
*/
|
||||
const hideLoading = () => {
|
||||
isLoading.value = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一 Toast 提示
|
||||
* @param {string} message - 提示内容
|
||||
* @param {string} type - 'success' | 'error' | 'warning' | 'none'
|
||||
* @param {number} duration - 持续时间(毫秒)
|
||||
*/
|
||||
const showToast = (message, type = 'none', duration = 2000) => {
|
||||
let icon = 'none'
|
||||
if (type === 'success') icon = 'success'
|
||||
if (type === 'error') icon = 'error'
|
||||
if (type === 'warning') icon = 'none'
|
||||
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon,
|
||||
duration,
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
// 导出响应式状态和方法
|
||||
export const useUI = () => {
|
||||
return {
|
||||
isLoading: isLoading, // 可用于模板中 v-if="isLoading"
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showToast
|
||||
}
|
||||
}
|
||||